本来打算在写完 数组、结构体、指针 等之后再写函数调用的, 因为函数调用会牵扯到这些东西, 但是我觉得那样做的话总会露出点意犹未尽的马脚, 所以还是先简单地分析一下函数调用吧, 之后再不断的完善函数调用这个大家伙。
#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 函数的一次完整调用。
现在从 main 函数中第 1 条粗体指令开始分析:
一步步的分析结束后,再从大粒度上回顾一次 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]$
下一篇中再继续讨论值传递。