深入理解C语言的函数调用过程 【转】

转自:http://blog.chinaunix.net/uid-25909619-id-4240084.html

原文地址:深入理解C语言的函数调用过程 作者:wjlkoorey258

本文主要从进程栈空间的层面复习一下C语言中函数调用的具体过程,以加深对一些基础知识的理解。
    先看一个最简单的程序:

点击(此处)折叠或打开

  1. /*test.c*/
  2. #include <stdio.h>
  3. int foo1(int m,int n,int p)
  4. {
  5. int x = m + n + p;
  6. return x;
  7. }
  8. int main(int argc,char** argv)
  9. {
  10. int x,y,z,result;
  11. x=11;
  12. y=22;
  13. z=33;
  14. result = foo1(x,y,z);
  15. printf("result=%d\n",result);
  16. return 0;
  17. }

主函数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

上面的汇编源代码和最终生成的可执行程序主体结构上已经非常类似了:


[[email protected] 1]# gcc -g -o test test.s

[[email protected] 1]# objdump -D test > testbin

[[email protected] 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                         mov    %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<[email protected]>

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条指令执行完后栈空间里的数据和布局如下:

然后main函数里的变量x,y,z的值放到栈上,就是接下来的三条指令:

点击(此处)折叠或打开

  1. 29 movl $11, 16(%esp)
  2. 30 movl $22, 20(%esp)
  3. 31 movl $33, 24(%esp)

这是三条寄存器间接寻址指令,将立即数11,22,33分别放到esp寄存器所指向的地址0xbffff610向高位分别偏移16、20、24个字节处的内存单元里,最后结果如下:

注意:这三条指令并没有改变esp寄存器的值。

接下来main函数里就要为了调用foo1函数而做准备了。由于mov指令的两个操作数不能都是内存地址,所以要将x,y和z的值传递给foo1函数,则必须借助通用寄存器来完成,这里我们看到eax承担了这样的任务:

点击(此处)折叠或打开

  1. 32 movl 24(%esp), %eax
  2. 33 movl %eax, 8(%esp)
  3. 34 movl 20(%esp), %eax
  4. 35 movl %eax, 4(%esp)
  5. 36 movl 16(%esp), %eax
  6. 37 movl %eax, (%esp)

当foo1函数所需要的所有输入参数都已经按正确的顺序入栈后,紧接着就需要调用call指令来执行foo1函数的代码了。前面的博文说过,call指令执行时分两步:首先会将call指令的下一条指令(movl  %eax, 28(%esp))的地址(0x0804841b)压入栈,然后跳转到函数foo1入口处开始执行。当第38条指令“call foo1”执行完后,栈空间布局如下:

call指令自动将下一条要执行的指令的地址0x0804841b压入栈,栈顶指针esp自动向低地址处“增长”4字节。所以,我们以前在C语言里所说的函数返回地址,应该理解为:当被调用函数执行完之后要返回到它的调用函数里下一条马上要执行的代码的地址。为了便于观察,我们把foo1函数最后生成指令再列出来:

点击(此处)折叠或打开

  1. 3 .globl foo1
  2. 4           .type foo1, @function
  3. 5 foo1:
  4. 6           pushl %ebp
  5. 7           movl %esp, %ebp
  6. 8           subl $16, %esp
  7. 9           movl 12(%ebp), %eax
  8. 10          movl 8(%ebp), %edx
  9. 11          leal (%edx,%eax), %eax
  10. 12          addl 16(%ebp), %eax
  11. 13          movl %eax, -4(%ebp)
  12. 14          movl -4(%ebp), %eax
  13. 15          leave
  14. 16          ret
  15. 17          .size foo1, .-foo1

进入到foo1函数里,开始执行该函数里的指令。当执行完第6、7、8条指令后,栈里的数据如下。这三条指令就是汇编层面函数的“序幕”,分别是保存ebp到栈,让ebp指向当前栈顶,然后为函数里的局部变量预留空间:

接下来第9和第10条指令,也并没有改变栈上的任何数据,而是将函数输入参数列表中的的x和y的值分别转载到eax和edx寄存器,和main函数刚开始时做的事情一样。此时eax=22、edx=11。然后用了一条leaf指令完成x和y的加法运算,并将运算结果存在eax里。第12条指令“addl 16(%ebp), %eax”将第三个输入参数p的值,这里是实参z的值为33,同样用寄存器间接寻址模式累加到eax里。此时eax=11+22+33=66就是我们最终要得计算结果。

因为我们foo1()函数的C代码中,最终计算结果是保存到foo1()里的局部变量x里,最后用return语句将x的值通过eax寄存器返回到mian函数里,所以我们看到接下来的第13、14条指令有些“多此一举”。这足以说明gcc人家还是相当严谨的,C源代码的函数里如果有给局部变量赋值的语句,生成汇编代码时确实会在栈上为本地变量预留的空间里的正确位置为其赋值。当然gcc还有不同级别的优化技术来提高程序的执行效率,这个不属于本文所讨论的东西。让我们继续,当第13、14条指令执行完后,栈布局如下:

将ebp-4的地址处0xbffff604(其实就是foo1()里的第一个局部参数x的地址)的值设置为66,然后再将该值复制到eax寄存器里,等会儿在main函数里就可以通过eax寄存器来获取最终的计算结果。当第15条指令leave执行完后,栈空间的数据和布局如下:

我们发现,虽然栈顶从0xbffff5f8移动到0xbffff60c了,但栈上的数据依然存在。也就是说,此时你通过esp-8依旧可以访问foo1函数里的局部变量x的值。当然,这也是说得通的,因为函数此时还没有返回。我们看栈布局可以知道当前的栈顶0xbffff60c处存放的是下一条即将执行的指令的地址,对照反汇编结果可以看到这正是main函数里的第18条指令(在整个汇编源文件test.s里的行号是39)“movl  %eax, 28(%esp)”。leave指令其实完成了两个任务:
   1、将栈上为函数预留的空间“收回”;
   2、恢复ebp;

也就是说leave指令等价于下面两条指令,你将leave替换成它们编译运行,结果还是对的:

点击(此处)折叠或打开

  1. movl %ebp,%esp
  2. popl %ebp

前面我们也说过,ret指令会自动到栈上去pop数据,相当于执行了“popl %eip”,会使esp增大4字节。所以当执行完第16条指令ret后,esp从0xbffff60c增长到0xbffff610处,栈空间结构如下:

现在已经从foo1里返回了,但是由于还没执行任何push操作,栈顶“上部”的数据依旧还是可以访问到了,即esp-12的值就是foo1里的局部变量x的值、esp-4的值就是函数的返回地址,当执行第39条指令“movl %eax,28(%esp)”后栈布局变成下面的样子:

第39条指令就相当于给main里的result变量赋值66,如上红线标注的地方。接下来main函数里要执行printf("result=%d\n",result)语句了,而printf又是C库的一个常用的输出函数,这里就又会像前面调用foo1那样,初始化栈,然后用“call printf的地址”来调用C函数。当40~43这4条指令执行完后,栈里的数据如下:

点击(此处)折叠或打开

  1. 40 movl $.LC0, %eax
  2. 41 movl 28(%esp), %edx
  3. 42 movl %edx, 4(%esp)
  4. 43 movl %eax, (%esp)

上图为了方便理解,将栈顶的0x08048504替换了成字符串“result=%d\n”,但进程实际运行时此时栈顶esp的值是字符串所在的内存地址。当第44条指令执行完后,栈布局如下:

由于此时栈已经用来调用printf了,所以栈顶0xbffff610“以上”部分的空间里就找不到foo1的任何影子了。最后在main函数里,当第46、47条指令执行完后栈的布局分别是:

当main函数里的ret执行完,其实是返回到了C库里继续执行剩下的清理工作。
   所以,最后关于C的函数调用,我们可以总结一下:
   1、函数输入参数的入栈顺序是函数原型中形参从右至左的原则;
   2、汇编语言里调用函数通常情况下都用call指令来完成
   
3、汇编语言里的函数大部分情况下都符合以下的函数模板:

点击(此处)折叠或打开

  1. .globl fun_name
  2. .type fun_name, @function
  3. fun_name:
  4. pushl %ebp
  5. movl %esp, %ebp
  6. <函数主体代码>
  7. leave
  8. ret

如果我们有个函数原型:int funtest(int x,int y int z char* ptr),在汇编层面,当调用它时栈的布局结构一般是下面这个样子:

而有些资料上将ebp指向函数返回地址的地方,这是不对的。正常情况下应该是ebp指向old ebp才对,这样函数末尾的leave和ret指令才可以正常工作。

时间: 2024-12-24 02:18:31

深入理解C语言的函数调用过程 【转】的相关文章

C语言中函数调用过程(如何管理栈空间)

ps:先做草稿,以后有时间再整理并贴图,:) 主要是利用栈底寄存器(ebp).栈顶寄存器(esp)跟eax寄存器(存储返回值)来实现. 假设P调用Q: P() { Q(1,2); } 1.调用前准备,将Q的参数放到栈中(非push) mov $1, (%esp) mov $2, 4(%esp) 2.调用call 0x12345678 (Q的地址) 首先将函数的返回地址(call语句后的那条指令的地址)进栈, 然后跳到0x12345678执行Q的代码. 3.将旧的ebp进栈(用于退出Q时还原) p

深入理解Java对象的创建过程:类的初始化与实例化

"-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> 深入理解Java对象的创建过程:类的初始化与实例化 - Rico's Blogs - 博客频道 - CSDN.NET Rico's Blogs 潜心修炼,成为一个更好的人. 目录视图 摘要视图 订阅 [活动]2017 CSDN博客专栏评选 &nbsp [5月书讯]流畅

c语言中函数调用的本质从汇编角度分析

今天下午写篇博客吧,分析分析c语言中函数调用的本质,首先我们知道c语言中函数的本质就是一段代码,但是给这段代码起了一个名字,这个名字就是他的的这段代码的开始地址 这也是函数名的本质,其实也就是汇编中的标号.下面我们会接触到一些东西 比如 eip 就是我们常常说的程序计数器,还有ebp和esp (这里是俩个指针,记得我们以前学8086也就一个sp堆栈指针)分别为EBP是指向栈底的指针,在过程调用中不变,又称为帧指针.ESP指向栈顶,程序执行时移动,ESP减小分配空间,ESP增大释放空间,ESP又称

C语言程序编译过程

最近在编译DM8168的ARM端程序时经常出现未定义.重定义等报错,由于源码文件多,包含关系比较多,所以自己添加时容易乱.深深的体会到,好的代码风格是如此重要,之前也在看代码重构,以后应该更加注意代码的质量.经思考总结规律如下: 1.公用的数据结构等写为一个头文件,其他源文件包含此头文件.同时为了让不同源文件里的函数都可以使用,公用的函数可以放在此头文件中声明. 2.其他源文件里声明的变量,如果想在另一个文件里用,需要extern声明,这样可以避免各种全局变量的交互混杂. 理解的比较浅,希望高人

c函数调用过程原理及函数栈帧分析

转载自地址:http://blog.csdn.net/zsy2020314/article/details/9429707       今天突然想分析一下函数在相互调用过程中栈帧的变化,还是想尽量以比较清晰的思路把这一过程描述出来,关于c函数调用原理的理解是很重要的. 1.关于栈 首先必须明确一点也是非常重要的一点,栈是向下生长的,所谓向下生长是指从内存高地址->低地址的路径延伸,那么就很明显了,栈有栈底和栈顶,那么栈顶的地址要比栈底低.对x86体系的CPU而言,其中 ---> 寄存器ebp(

从汇编角度来理解linux下多层函数调用堆栈执行状态

注:在linux下开发经常使用的辅助小工具: readelf .hexdump.od.objdump.nm.telnet.nc 等,详细能够man一下. 我们用以下的C代码来研究函数调用的过程. C++ Code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int bar(int c, int d) { int e = c + d; return e; } int foo(int a, int b) { return bar(a, b); } int main(

ARM系统中函数调用过程中的参数传递-转

在 嵌入式软件编程中,经常会用到函数调用,之前在学习如何在C语言中嵌入汇编时有了解到C语言之前的参数调用是使用寄存器R0传递第一个参数,R1传递到第 二个..一直到R3传递第四个参数.但是实际上有时可能传递的参数非常多,超过8个,或是参数中有浮点数之类,参数也会超过4个寄存器,对于超出的部份并 不使用R4,而是使用堆栈的方式,但具体是如何的方式很多网站就没了下文了. 对于ARM体系来说,不同语言撰写的函数之间相互调用(mix calls)遵循的是 ATPCS(ARM-Thumb Procedur

《深入理解计算机系统》3.7过程

所谓过程在C语言中就是函数的意思. 本章将介绍,函数调用过程的细节. 栈帧 IA32程序用栈来支持程序的运行,栈用来存放调用时候暂存的数据. 它可以: 传递函数的参数 存储返回数据 保存某些寄存器的数据,以便后面恢复 每一段函数都会在栈中构建一块空间,名为栈帧.之所以叫栈帧是因为这块空间用栈指针和帧指针界定. 栈指针:%esp,s代表stack,它指向栈帧的顶部,该指针是可移动的. 帧指针:%ebp,b我猜应该是base,它指向栈帧的底部,该指针是不可移动的,常用它的地址加上偏移量来获取保存在栈

(转)函数调用过程探究

转自:http://www.cnblogs.com/bangerlee/archive/2012/05/22/2508772.html 引言 如何定义函数.调用函数,是每个程序员学习编程的入门课.调用函数(caller)向被调函数(callee)传入参数,被调函数返回结果,看似简单的过程,其实CPU和系统内核在背后做了很多工作.下面我们通过反汇编工具,来看函数调用的底层实现. 基础知识 我们先来看几个概念,这有助于理解后面反汇编的输出结果. 栈(stack) 栈,相信大家都十分熟悉,push/p