函数调用
前言
本来打算在写完 数组、结构体、指针 等之后再写函数调用的,因为函数调用会牵扯到这些东西,但是我觉得那样做的话总会露出点意犹未尽的马脚,所以还是先简单地分析一下函数调用吧,之后再不断的完善函数调用这个大家伙。
C源程序(double.c)
#include <stdio.h> int Double(int b) { int c; c = b + b; ++b; // 会影响到 a 吗? return c; } int main() { int a = 1; int d = Double(a); printf("a:%d d:%d\n", a, d); return 0; }
反汇编
gcc -S double.c
gcc -S double.c默认就是把汇编源代码输出到 double.s 中,之前一直用 -o 选项是为了避免过多的解释O(∩_∩)O~, double.s 中的内容简化后:
Double: pushl %ebp movl %esp, %ebp subl $16, %esp movl 8(%ebp), %eax addl %eax, %eax movl %eax, -4(%ebp) addl $1, 8(%ebp) movl -4(%ebp), %eax leave ret main: pushl %ebp movl %esp, %ebp andl $-16, %esp subl $32, %esp movl $1, 28(%esp) movl 28(%esp), %eax movl %eax, (%esp) call Double movl %eax, 24(%esp) movl $.LC0, %eax movl 24(%esp), %edx movl %edx, 8(%esp) movl 28(%esp), %edx movl %edx, 4(%esp) movl %eax, (%esp) call printf movl $0, %eax leave ret
分析
其中包含了对 Double 函数的一次完整调用。
现在从第18行开始分析:
|
|
这是给 局部变量 a 赋值 假设栈指针寄存器 esp 现在的值是 8000,这条指令执行完后栈的情况如右图 |
|
|
|
这两条指令将 a 的值 1 写入了地址为 8000 的内存块(4字节),这可能是 b 吧? 现在还不要妄下结论。 |
|
|
|
call 指令的执行,可分为两个步骤:
然后就跳转到 Double 处取指令执行了。 |
|
|
|
先将旧的 ebp 压栈,然后把这个时候的 esp 赋值给 ebp,后面会看到这样做的目地 | |
|
|
Double 的局部变量空间就这么被开拓了(虽然有点浪费) | |
|
|
8(%ebp) 当前刚好表示 8000 内存块,从这 4 条指令对 8000 内存块的值进行的处理,我们可以确定 8000 就是参数 b,而 -4(%ebp) 则是 c。 | |
|
|
这就是最后的收尾部分了,函数的返回值要存储在累加寄存器 eax 中, leave 等同于以下两条指令: movl %ebp, %esp popl %ebp 与刚进入 Double 时的两条指针首尾呼应。 结果就是局部空间被回收了,ebp 也复原了。 而 ret 则相当于 popl %eip,程序就继续执行 call 指令之后的指令了。 虽然 Double 函数的局部空间被回收了,但是其中的值还是保持不变的,一直到之后调用 printf 函数的时候, Double 函数的局部变量以及参数的内容才被覆盖。 |
整理
一步步的分析结束后,再从大粒度上回顾一次 Double 函数:
Double: pushl %ebp #----------帧指针 ebp 切换 movl %esp, %ebp #---------/ subl $16, %esp #----------开拓局部变量空间 movl 8(%ebp), %eax #\ addl %eax, %eax #-\ movl %eax, -4(%ebp) #-C 的操作 addl $1, 8(%ebp) #/ movl -4(%ebp), %eax #-----将返回值保存到 eax 寄存器 leave #--------\ ret #---------退栈、恢复帧指针、返回
其他函数编译为汇编后,指令也可以这样来进行划分。
由于 Double 函数太过简单,所以没有出现保护寄存器的指令(eax 寄存器不需要保护,每个函数都知道它是用来存返回值的,在函数的最后部分肯定会被修改),复杂的函数会在函数的最前面 pushl 将被修改的寄存器,而在 ret 之前 popl 寄存器以恢复原来的值。
小结
通过对 Double 函数的分析,我们注意到 ebp 跟 esp 的作用类似,函数中经常也通过 ebp + 偏移 的方式来访问 参数 和 局部变量,现在可以向大家承诺了:esp 或 ebp 加偏移就是局部变量最后的表现形式。关于 帧指针寄存器 ebp 后面会再出一篇来进行分析。
同时我们也可以明显地看出值传递的过程:调用前将 a 复制到 b(值拷贝,它们分别是不同的内存块),函数调用完后 b 直接被忽略了,也不会再拷贝回 a,所以虽然我在 Double 中 ++b 了,而 a 的值仍然为 1。程序的运行结果如下:
[lqy@localhost temp]$ gcc -o double double.c [lqy@localhost temp]$ ./double a:1 d:2 [lqy@localhost temp]$
下一篇中再继续讨论值传递。