引言

函数调用对于程序员而言,就像每天吃饭睡觉一样普通寻常。几乎每种编程语言都会提供函数定义、函数调用的功能。但是,在看起来寻常不过的函数调用背后,系统内核帮助我们做了很多事情。下面,我打算通过反汇编的方法,从汇编语言的层次来阐释函数调用的实现。

基础知识

先回顾几个概念,这样可以帮助我们顺利地理解后面实验的结果。

调用函数(caller)和被调函数(callee)

调用函数(caller)向被调函数(callee)传入参数,被调函数(callee)返回结果。首先要明确这两个名词,免得被下文的表述弄混淆。

高地址和低地址

每个进程都有自己的虚拟地址空间。高地址和低地址是相对的,我们通常用16进制数来表示一个内存地址。例如,相比于0x000x04数值上比0x00大,所以0x04称为高地址, 0x00 称为低地址。

进程内存布局

如图,一个进程的内存布局从低地址到高地址分别是

  1. 代码段
  2. 数据段,包括初始化区和未初始化区(bss)
  3. 堆段
  4. 栈段
  5. 内核地址空间

栈段(stack segment)

栈是最常用的数据结构之一,可以进行push/pop,且只允许在一端进行操作,后进先出(LIFO)。但就是这个最简单的数据结构,构成了计算机中程序执行的基础,用于内核中程序执行的栈具有以下特点:

  • 每一个进程在用户态对应一个调用栈结构(call stack)
  • 程序中每一个未完成运行的函数对应一个栈帧(stack frame),栈帧中保存函数局部变量、传递给被调函数的参数等信息
  • 栈底对应高地址,栈顶对应低地址,栈由内存高地址向低地址生长

一个进程的调用栈图示如下:

寄存器(register)

寄存器位于CPU内部,用于存放程序执行中用到的数据和指令,CPU从寄存器中取数据,相比从内存中取快得多。

寄存器又分通用寄存器特殊寄存器

通用寄存器有ax/bx/cx/dx/di/si,尽管这些寄存器在大多数指令中可以任意选用,但也有一些规定某些指令只能用某个特定“通用”寄存器,例如函数返回时需将返回值mov到ax寄存器中;特殊寄存器有bp/sp/ip等,特殊寄存器均有特定用途,例如sp寄存器用于存放以上提到的栈帧的栈顶地址,除此之外,不用于存放局部变量,或其他用途。

对于有特定用途的几个寄存器,简要介绍如下:

  • ax(accumulator): 可用于存放函数返回值
  • bp(base pointer): 用于存放执行中的函数对应的栈帧的栈底地址
  • sp(stack poinger): 用于存放执行中的函数对应的栈帧的栈顶地址
  • ip(instruction pointer): 指向当前执行指令的下一条指令

不同架构的CPU,寄存器名称被添以不同前缀以指示寄存器的大小。例如对于x86架构,字母“e”用作名称前缀,指示各寄存器大小为32位;对于x86_64寄存器,字母“r”用作名称前缀,指示各寄存器大小为64位。

大学课程(例如微机原理、汇编语言)里应该都会介绍Intel 8086汇编或类似知识,相信应该可以触类旁通,很多时候只是寄存器的名字发生了变化,大体的思想还是共通的。

函数调用样例

在掌握了基础知识之后,我们选取下面这个简单的例子进行分析。

//call_example.c
int add(int a, int b) { return a + b; }
int main(void) {add(2, 5);return 0;
}

通过gcc call_example.c -g -o call_example命令得到可执行文件call_example

加上参数-g是为了让目标文件call_example包含符号表等调试信息。

我们可以用objdump -D -M att ./call_example命令先来对call_example进行反汇编看看结果。截取了部分结果如下:

00000000004004a6 <add>:4004a6:    55                       push   %rbp4004a7:    48 89 e5                 mov    %rsp,%rbp4004aa:    89 7d fc                 mov    %edi,-0x4(%rbp)4004ad:    89 75 f8                 mov    %esi,-0x8(%rbp)4004b0:    8b 55 fc                 mov    -0x4(%rbp),%edx4004b3:    8b 45 f8                 mov    -0x8(%rbp),%eax4004b6:    01 d0                    add    %edx,%eax4004b8:    5d                       pop    %rbp4004b9:    c3                       retq   00000000004004ba <main>:4004ba:    55                       push   %rbp4004bb:    48 89 e5                 mov    %rsp,%rbp4004be:    be 05 00 00 00           mov    $0x5,%esi4004c3:    bf 02 00 00 00           mov    $0x2,%edi4004c8:    e8 d9 ff ff ff           callq  4004a6 <add>4004cd:    b8 00 00 00 00           mov    $0x0,%eax4004d2:    5d                       pop    %rbp4004d3:    c3                       retq   4004d4:    66 2e 0f 1f 84 00 00     nopw   %cs:0x0(%rax,%rax,1)4004db:    00 00 004004de:    66 90                    xchg   %ax,%ax

objdump 固然是一个好工具,但是有时候看起来不是那么直观,下面我着重介绍用gdb进行分析反汇编分析。

利用gdb进行反汇编分析

我们利用gdb跟踪main->add的过程。

启动

利用gdb载入可执行程序call_example

$ gdb ./call_example
GNU gdb (GDB) 7.12.1
Reading symbols from ./call_example...done.
(gdb) start
Temporary breakpoint 1 at 0x4004be: file call_example.c, line 3.
Starting program: /tmp/call_exampleTemporary breakpoint 1, main () at call_example.c:3
3         add(2, 5);
(gdb)

start命令用于拉起被调试程序,并执行至main函数的开始位置,程序被执行之后与一个用户态的调用栈关联。

main函数

现在程序停止在main函数,用disassemble命令显示当前函数的汇编信息:

(gdb) disassemble /mr
Dump of assembler code for function main:
2       int main(void) {0x00000000004004ba <+0>:     55      push   %rbp0x00000000004004bb <+1>:     48 89 e5        mov    %rsp,%rbp3         add(2, 5);
=> 0x00000000004004be <+4>:     be 05 00 00 00  mov    $0x5,%esi0x00000000004004c3 <+9>:     bf 02 00 00 00  mov    $0x2,%edi0x00000000004004c8 <+14>:    e8 d9 ff ff ff  callq  0x4004a6 <add>4         return 0;0x00000000004004cd <+19>:    b8 00 00 00 00  mov    $0x0,%eax5       }0x00000000004004d2 <+24>:    5d      pop    %rbp0x00000000004004d3 <+25>:    c3      retq   End of assembler dump.
(gdb)

disassemble命令的/m指示显示汇编指令的同时,显示相应的程序源码;/r指示显示十六进制的计算机指令(raw instruction)。

以上输出每行指示一条汇编指令,除程序源码外共有四列,各列含义为:

  1. 0x00000000004004ba: 该指令对应的虚拟内存地址
  2. <+0>: 该指令的虚拟内存地址偏移量
  3. 55: 该指令对应的计算机指令
  4. push %rbp: 汇编指令

回忆一下我们用汇编语言写调用函数的代码时,第一步是“保护现场”,也就是:

  1. 将调用函数的栈帧栈底地址入栈,即将bp寄存器的值压入调用栈中
  2. 建立新的栈帧,将被调函数的栈帧栈底地址放入bp寄存器中,其值为调用函数的栈顶地址sp

以下两条指令即完成上面动作:

push %rbp
mov  %rsp, %rbp

通过objdumpgdb的结果,我们发现main函数也包含了这两条指令,这是因为main函数也会被__libc_start_main所调用,这里不多加赘述。

main调用add函数,两个参数传入通用寄存器中:

mov    $0x5,%esi
mov    $0x2,%edi

咦?汇编语言课上老师不是教过传递的参数会被压入栈中么?

其实,x86和x86_64定义了不同的函数调用规约(calling convention)。x86_64采用将参数传入通用寄存器的方式,x86则将参数压入调用栈中。我们利用gcc -S -m32 call_example.c来直接生成x86平台的汇编代码,找到传递参数那段代码:

pushl    $5
pushl    $2
call    add

原来如此!

准备完参数之后,就可以放心大胆的将控制权交给add函数了,callq指令完成这里的交接任务:

0x00000000004004c8 <+14>:    e8 d9 ff ff ff  callq  0x4004a6 <add>

callq指令会在调用函数的时候将下一条指令的地址push到stack上,当本次调用结束后,retq指令会跳转到被保存的返回地址处使程序继续执行。

本次callq指令,完成了两个任务:

  1. 将调用函数(main)中的下一条指令(这里为0x00000000004004cd)入栈,被调函数返回后将取这条指令继续执行
  2. 修改指令指针寄存器rip的值,使其指向被调函数(add)的执行位置,这里为0x00000000004004a6

我们可以用stepi指令进行指令级别的操作,相比于一般调试时候按行调试的粒度会更精细。

(gdb) stepi 3
add (a=0, b=4195248) at call_example.c:1
1       int add(int a, int b) { return a + b; }
(gdb) disassemble /mr
Dump of assembler code for function add:
1       int add(int a, int b) { return a + b; }
=> 0x00000000004004a6 <+0>:     55      push   %rbp0x00000000004004a7 <+1>:     48 89 e5        mov    %rsp,%rbp0x00000000004004aa <+4>:     89 7d fc        mov    %edi,-0x4(%rbp)0x00000000004004ad <+7>:     89 75 f8        mov    %esi,-0x8(%rbp)0x00000000004004b0 <+10>:    8b 55 fc        mov    -0x4(%rbp),%edx0x00000000004004b3 <+13>:    8b 45 f8        mov    -0x8(%rbp),%eax0x00000000004004b6 <+16>:    01 d0   add    %edx,%eax0x00000000004004b8 <+18>:    5d      pop    %rbp0x00000000004004b9 <+19>:    c3      retq   End of assembler dump.
(gdb)

至此,main函数的执行到此就暂时告一段落了,我们进入了add函数的新篇章。

add函数

add函数也是一样的套路,头两条指令先建立自己的栈帧,然后调用add指令计算结果,结果存放在eax寄存器中。计算完之后,需要“恢复现场”:

0x00000000004004b8 <+18>:    5d      pop    %rbp

因为此例比较特殊,add函数没有包含局部变量,main和add函数的栈顶恰好相同,所以忽略了对栈顶rsp的恢复。

通常,完整的“恢复现场”需要以下两条指令:

mov %rbp, %rsp
pop %rbp

参考:

https://web.stanford.edu/clas...

从汇编角度看待函数调用相关推荐

  1. 从汇编角度理解 ebpesp 寄存器、函数调用过程、函数参数传递以及堆栈平衡

    关于函数参数的传递及堆栈指针的变化,一直缺乏系统的认识和了解,各种博客也只是片面的讲解某个局部知识点,并没有全局的把握和对栈的深刻理解.本文试图从汇编以及整体上,讲解函数调用时,堆栈的变化,以及到底是 ...

  2. 视频教程-C语言-从汇编角度理解C语言的本质-C/C++

    C语言-从汇编角度理解C语言的本质 擅长JavaWeb开发,游戏逆向外挂与反外挂,游戏保护对抗 孙冉 ¥49.00 立即订阅 扫码下载「CSDN程序员学院APP」,1000+技术好课免费看 APP订阅 ...

  3. DayDayUp:寒门女孩考入北大→换角度看待表达《感谢贫穷》—关于吃苦与穷~~~Python之wordcloud词云图可视化

    DayDayUp:寒门女孩考入北大→换角度看待表达<感谢贫穷>-关于吃苦与穷~~~Python之wordcloud词云图可视化 目录 博主看法-关于吃苦与穷 文本内容 寒门女孩考入北大-& ...

  4. 从运营角度看待UE设计

    我们有没有经常吐槽各种游戏"这操作真变扭""升星累死了""界面怎么这么多图标都找到活动在哪了"等等,从运营的角度该如何看待UE设计呢? UE ...

  5. 献给汇编初学者-函数调用堆栈变化分析

    献给汇编初学者-函数调用堆栈变化分析 标 题: 献给汇编初学者-函数调用堆栈变化分析 作 者: 堕落天才 时 间: 2007-01-19,19:20 链 接: http://bbs.pediy.com ...

  6. 一切皆服务:以蓝天的角度看待云

    本文讲的是一切皆服务:以蓝天的角度看待云,[IT168 报道]当人们谈到云计算,不乏各种意见.预测,甚至定义.其结果是:也不乏困惑和混淆.好在人们有一点达成了共识:我们正处于一场重大市场变革的顶峰,这 ...

  7. 从数据结构及汇编角度深入学习go语言

    原文连接 Golang基础知识 源码调试 从汇编角度理解go go/c/c++常用功能对应的汇编指令 数据结构 内建容器简介 array/slice map 字符串 结构体 接口 常用关键字 for和 ...

  8. 【比赛总结】从编程位队长的角度看待第十三届华中杯数学建模比赛A题

    前言 有幸以编程位和队长的身份大一就参加了一次数学建模比赛,这次比赛是"华中杯",所以第一次打还是比较有新鲜感和有很多收获的,故记于此. 因为--在前期找指导老师的时候一说是大一的 ...

  9. 1、从软件开发角度看待PCI和PCIe

    1.从软件开发角度看待PCI和PCIe 转载教程 01 1. 最容易访问的设备是什么 2. 地址空间的概念 3. 理解PCI和PCIE的关键 3.1 地址空间转换 3.2 PCI接口速览 3.3 PC ...

最新文章

  1. MySQL字符串函数substring:字符串截取
  2. 李宏毅深度学习——Why Deep?
  3. jquery找祖先包含_jquery如何获取祖先元素
  4. oracle 1天后,Oracle Code One - 第1天 精彩亮点回顾
  5. 浏览器输入一个url会发生什么
  6. mysql 联表比对,MySQL联表查询详解/超详细mysql left join,right join,inner join用法分析比较...
  7. 用python写出九九乘法表
  8. OpenGL(5)——变换
  9. JVM垃圾收集器(2)
  10. NO.4 计算有序数组的平方
  11. 【零知ESP8266教程】快速入门28 六轴传感器模块的使用
  12. 了解CSS的float高度坍塌的原理,并懂得怎么解决高度坍塌!
  13. 高项_第十三章项目合同管理
  14. 小孟5w接了个盲盒小程序,三周开发完毕
  15. 有关神经网络的训练算法,神经网络算法通俗解释
  16. sbrkr.c:(.text._sbrk_r+0xc): undefined reference to `_sbrk'
  17. 完美解决各种spring项目报错问题
  18. 计算机科学应用论文题目,比较好写的计算机科学与应用论文题目 计算机科学与应用论文题目怎么取...
  19. nestjs入门(controller,service,module)
  20. 浅谈UPS不间断电源的重要性

热门文章

  1. mysql数据库mha_MySQL高可用性大杀器之MHA
  2. python 遍历list并删除部分元素
  3. P2658 汽车拉力比赛
  4. karaf中利用Bundle引入外部log4j配置文件
  5. [原创]关于设置linux中vim 显示行号
  6. (5):Silverlight 2 实现简单的拖放功能
  7. 网站流量和金钱的关系
  8. Hadoop编译打包记录
  9. mysql bin值总是变化_MySQL|update字段为相同的值是否会记录binlog
  10. java的mysql语句规范_常用的标准SQL 语句