深入理解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;
  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                         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

 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指令才可以正常工作。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/509163.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Unity3D打包后日志文件输出目录

Unity3D打包后日志文件输出目录&#xff0c;包括日志文件和崩溃时记录文件 C:\Users\Administrator\AppData\LocalLow\长沙迪迈科股份有限公司\镍矿探秘 其中"..\长沙迪迈科股份有限公司\镍矿探秘" 为unity的公司和产品设置

Unity3d LookAt参数说明

Unity3d LookAt参数说明 //// 摘要: // Rotates the transform so the forward vector points at targets current position.//// 参数: // target:// Object to point towards.//// worldUp:// Vector specifying the upward direction.public void LookAt(…

初入职场的你不应错过的一些书籍

在职场中&#xff0c;听过最接地气的一句话就是&#xff1a;在职场中要眼睛里有活儿&#xff0c;知道什么该说什么不该说&#xff0c;也不要说自己不确定的事情。今天来推荐一些职场老手建议看的书 《好好说话》 有太多人初入职场不会说话&#xff0c;而说话的能力是可以培养的…

关于 Unity WebGL 的探索

转自:https://www.cnblogs.com/yaukey/p/unity_webgl_explore_1.html 查找了 Unity 的官方资料&#xff0c;我们如果需要使用 WebGL 需要面对以下几个挑战&#xff1a; Native Plugin&#xff1a;也就是说各种原生插件&#xff08;C/C等编译的本地机器码库&#xff09;&#…

Unity脚本生命周期与执行顺序

目录 脚本生命周期 MonoBehavior生命周期图脚本执行顺序 自定义执行顺序 (文章目录) 在Unity中&#xff0c;脚本可以理解为附加在游戏对象上的用于定义游戏对象行为的指令代码。必须绑定在游戏对象上才能开始它的生命周期。游戏对象可以理解为能容纳各种组件的容器&#xff0c…

Unity3D(UE4)加载倾斜摄影数据OSGB格式

在Unity3D平台动态加载调度倾斜摄影数据&#xff0c;利用多线程动态加载瓦片数据&#xff0c;可以顺畅加载海量的瓦片数据。目前测试可流畅加载100G左右数据&#xff0c;支持加载本地数据&#xff0c;数据可不放在Unity工程内&#xff0c;也可以将数据放置在服务器上实现网络加…

Unity打包失败解决方案

更改设置即可 &#xff1a;Edit -> Graphics Emulation-> Shader Hardware Tier 1

Unity3D实现谷歌数字地球

Unity3D实现谷歌地球 在Unity3d平台实现的类似谷歌地球的功能&#xff0c;可动态加载谷歌&#xff0c;ArcGis,BingMap,天地图影像&#xff0c;也可加载国界线等矢量文件以及在线加载高程文件。 视频链接: 地球操作:https://www.bilibili.com/video/BV1mT4y1P771 地球漫游:h…

Unity罗技方向盘接入

要想在Unity中接入罗技方向盘的数据&#xff0c;首先必须安装驱动&#xff0c;并且打开安装的软件&#xff0c;否则在Unity中会一直连接不成功。状态如下&#xff1a; 然后下载相应的开发包Logitech SDK即可&#xff0c;需要替换相应的LogitechSteeringWheelEnginesWrapper.dll…

sscanf

sscanf&#xff08;&#xff09; 2010-01-28 11:53:42| 分类&#xff1a; Work|举报|字号 订阅 下载LOFTER我的照片书 |定义函数 int sscanf (const char *str,const char * format,........); 函数说明 sscanf()会将参数str的字符串根据参数format字符串来转换并格…

字典树

微博:TankyWoo基新博客:TankyWooTanky Woo的前博客 字典树&#xff08;讲解模版&#xff09; 又称单词查找树&#xff0c;Trie树&#xff0c;是一种树形结构&#xff0c;是一种哈希树的变种。典型应用是用于统计&#xff0c;排序和保存大量的字符串&#xff08;但不仅限于字符串…

Unity3D谷歌地球

Unity3D实现谷歌地球 在Unity3d平台实现的类似谷歌地球的功能&#xff0c;可动态在线加载谷歌&#xff0c;ArcGis,BingMap,天地图等影像&#xff0c;也可加载国界线等矢量文件以及在线加载高程文件。 视频链接: 地球操作:https://www.bilibili.com/video/BV1mT4y1P771 地球…

线段树的操作

登录 | 注册 MetalSeed 思绪来得快去的也快&#xff0c;偶尔会在这里停留。 目录视图摘要视图订阅 移动信息安全的漏洞和逆向原理 程序员11月书讯&#xff0c;评论得书啦 Get IT技能知识库&#xff0c;50个领域一键直达数据结构专题——线段树 标签&#xff1a; …

UE4加载osgb倾斜摄影数据

在UE4引擎中动态加载调度osgb倾斜摄影数据&#xff0c;利用多线程动态加载瓦片数据&#xff0c;可以顺畅加载海量的瓦片数据。最大可加载200G以上数量级。 Unity3D加载osgb倾斜摄影数据:Unity3d(UE4)动态加载osgb倾斜摄影数据_Answer-3的博客-CSDN博客_unity加载osgb qq:1749…

Unity/UE读取OPC UA和OPC DA数据(UE4)

Unity/UE4通过OPC UA和OPC DA协议读取PLC数据&#xff0c;通过采集服务将数据采集到数据库中&#xff0c;Unity3d和UE4再从数据库中读取数据进行展示&#xff0c;用于三维数字孪生系统接入自动化系统的硬件数据。支持WinCC,组态王&#xff0c;Kepware等组态软件的数据接入。 W…

Unity3d(UE4)动态加载osgb倾斜摄影数据

在Unity3D平台动态加载调度倾斜摄影数据&#xff0c;利用多线程动态加载瓦片数据&#xff0c;可以顺畅加载海量的瓦片数据。目前测试可流畅加载200G左右数据&#xff0c;支持加载本地数据&#xff0c;数据可不放在Unity工程内&#xff0c;也可以将数据放置在服务器上实现网络加…

RMQ算法

RMQ算法 标签&#xff1a; 算法querytable2012-08-28 20:53 14613人阅读 评论(7) 收藏 举报分类&#xff1a;nyist&#xff08;26&#xff09; 数据结构&#xff08;5&#xff09; 1. 概述 RMQ&#xff08;Range Minimum/Maximum Query&#xff09;&#xff0c;即区间最值查询…

bellman ford 算法 判断是否存在负环

Flyer 目录视图摘要视图订阅 微信小程序实战项目——点餐系统 程序员11月书讯&#xff0c;评论得书啦 Get IT技能知识库&#xff0c;50个领域一键直达关闭bellman ford 算法 2013-05-25 15:36 11148人阅读 评论(0) 收藏 举报分类&#xff1a;ACM&#xff08;11&a…

C++ vector用法

C vector用法 在c中&#xff0c;vector是一个十分有用的容器&#xff0c;下面对这个容器做一下总结。 1 基本操作 (1)头文件#include<vector>. (2)创建vector对象&#xff0c;vector<int> vec; (3)尾部插入数字&#xff1a;vec.push_back(a); (4)使用下标访问元素&…

字典树(Trie树)

字典树(Trie树)字典树&#xff0c;又称单词查找树&#xff0c;Trie树&#xff0c;是一种树形结构&#xff0c;典型应用是用于统计&#xff0c;排序和保存大量的字符串&#xff0c;所以经常被搜索引擎系统用于文本词频统计。它的优点是&#xff1a;利用字符串的公共前缀来节约存…