深⼊理解C语⾔的函数调⽤过程
本⽂主要从进程栈空间的层⾯复习⼀下C语⾔中函数调⽤的具体过程,以加深对⼀些基础知识的理解。
先看⼀个最简单的程序:
点击(此处)折叠或打开
1. /*test.c*/
2. #include <stdio.h>
3.
4.
5. int foo1(int m,int n,int p)
6. {
7. int x = m + n + p;
8. return x;
9. }
10.
11. int main(int argc,char** argv)
12. {
13. int x,y,z,result;
14. x=11;
15. y=22;
16. z=33;
printf函数的执行顺序17. result = foo1(x,y,z);
18. printf("result=%d\n",result);
19. return 0;
20. }
主函数main⾥定义了4个局部变量,然后调⽤同⽂件⾥的foo1()函数。4个局部变量毫⽆疑问都在进程的栈空间上,当进程运⾏起来后我们逐步了解⼀下main函数⾥是如何基于栈实现了对foo1()的调⽤过程,⽽foo1()⼜是怎么返回到main函数⾥的。为了便于观察的粒度更细致⼀些,我们对test.c⽣成的汇编代码进⾏调试。如下:
点击(此处)折叠或打开
1. .file "test.c"
2. .text
3. .globl foo1
4. .type foo1, @function
5. foo1:
6. pushl %ebp
7. movl %esp, %ebp
8. subl $16, %esp
9. movl 12(%ebp), %eax
10. movl 8(%ebp), %edx
11. leal (%edx,%eax), %eax
12. addl 16(%ebp), %eax
13. movl %eax, -4(%ebp)
14. movl -4(%ebp), %eax
15. leave
16. ret
17. .size foo1, .-foo1
18. .section .rodata
19. .LC0:
20. .string "result=%d\n"
21. .text
22. .globl main
23. .type main, @function
24. main:
25. pushl %ebp
26. movl %esp, %ebp
27. andl $-16, %esp
28. subl $32, %esp
29. movl $11, 16(%esp)
30. movl $22, 20(%esp)
31. movl $33, 24(%esp)
32. movl 24(%esp), %eax
33. movl %eax, 8(%esp)
34. movl 20(%esp), %eax
35. movl %eax, 4(%esp)
36. movl 16(%esp), %eax
37. movl %eax, (%esp)
38. call foo1
39. movl %eax, 28(%esp)
40. movl $.LC0, %eax
41. movl 28(%esp), %edx
42. movl %edx, 4(%esp)
43. movl %eax, (%esp)
44. call printf
45. movl $0, %eax
46. leave
47. ret
48. .size main, .-main
49. .ident "GCC: (GNU) 4.4.4 20100726 (Red Hat 4.4.4-13)"
50. .section .note.GNU-stack,"",@progbits
上⾯的汇编源代码和最终⽣成的可执⾏程序主体结构上已经⾮常类似了:[root@maple 1]# gcc -g -o test test.s
[root@maple 1]# objdump -D test >testbin
[root@maple 1]# vi testbin
//… 省略部分不相关代码
80483c0: ff d0 call *%eax
80483c2: c9 leave
80483c3: c3 ret
080483c4 :
80483c4: 55 push %ebp
80483c5: 89 e5 mov %esp,%ebp
80483c7: 83 ec 10 sub $0x10,%esp
80483ca: 8b 45 0c mov 0xc(%ebp),%eax
80483cd: 8b 55 08 mov 0x8(%ebp),%edx
80483d0: 8d 04 02 lea (%edx,%eax,1),%eax
80483d3: 03 45 10 add 0x10(%ebp),%eax
80483d6: 89 45 fc mov %eax,-0x4(%ebp)
80483d9: 8b 45 fc mov -0x4(%ebp),%eax
80483dc: c9 leave
80483dd: c3 ret
080483de
:
80483de: 55 push %ebp
80483df: 89 e5 mov %esp,%ebp
80483e1: 83 e4 f0 and $0xfffffff0,%esp
80483e4: 83 ec 20 sub $0x20,%esp
80483e7: c7 44 24 10 0b 00 00 movl $0xb,0x10(%esp) 80483ee: 00
80483ef: c7 44 24 14 16 00 00 movl $0x16,0x14(%esp) 80483f6: 00
80483f7: c7 44 24 18 21 00 00 movl $0x21,0x18(%esp) 80483fe: 00
80483ff: 8b 44 24 18 mov 0x18(%esp),%eax 8048403: 89 44 24 08 mov %eax,0x8(%esp)
8048407: 8b 44 24 14 mov 0x14(%esp),%eax 804840b: 89 44 24 04 mov %eax,0x4(%esp)
804840f: 8b 44 24 10 mov 0x10(%esp),%eax 8048413: 89 04 24 m
ov %eax,(%esp)
8048416: e8 a9 ff ff ff call 80483c4
804841b: 89 44 24 1c mov %eax,0x1c(%esp) 804841f: b8 04 85 04 08 mov $0x8048504,%eax 8048424: 8b 54 24 1c mov 0x1c(%esp),%edx 8048428: 89 54 24 04 mov %edx,0x4(%esp) 804842c: 89 04 24 mov %eax,(%esp)
804842f: e8 c0 fe ff ff call 80482f4
8048434: b8 00 00 00 00 mov $0x0,%eax
8048439: c9 leave
804843a: c3 ret
804843b: 90 nop
804843c: 90 nop
/
/… 省略部分不相关代码
⽤GDB调试可执⾏程序test:
在main函数第⼀条指令执⾏前我们看⼀下进程test的栈空间布局。因为我们最终的可执⾏程序是通过glibc库启动的,在main的第⼀条指令运⾏前,其实还有很多故事的,这⾥就不展开了,以后有时间再细究,这⾥只要记住⼀点:main函数执⾏前,其进程空间的栈⾥已经有了相当多的数据。我的系统⾥此时栈顶指针esp的值是0xbffff63c,栈基址指针ebp的值0xbffff6b8,指令寄存器eip的值是0x80483de正好是下⼀条马上即将执⾏的指令,即main函数内的第⼀条指令“push %ebp”。那么此时,test进程的栈空间布局⼤致如下:
然后执⾏如下三条指令:
点击(此处)折叠或打开
1. 25 pushl %ebp //将原来ebp的值0xbffff6b8如栈,esp⾃动增长4字节
2. 26 movl %esp, %ebp //⽤ebp保存当前时刻esp的值
3. 27 andl $-16, %esp //内存地址对其,可以忽略不计
执⾏完上述三条指令后栈⾥的数据如上图所⽰,从0xbffff630到0xbffff638的8字节是为了实现地址对齐的填充数据。此时ebp的值
0xbffff638,该地址处存放的是ebp原来的值0xbffff6b8。详细布局如下:
第28条指令“subl $32,%esp”是在栈上为函数⾥的本地局部变量预留空间,这⾥我们看到main主函数有4个int型的变量,理论上说预留16字节空间就可以了,但这⾥却预留了32字节。GCC编译器在⽣成汇编代码时,已经考虑到函数调⽤时其输⼊参数在栈上的空间预留的问题,这⼀点我们后⾯会看到。当第28条指令执⾏完后栈空间⾥的数据和布局如下:
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论