c语言函数调用过程中0.35F等于多少?过程…

    本文主要从进程栈空间的层面复習一下c语言函数调用过程中函数调用的具体过程以加深对一些基础知识的理解。

    主函数main里定义了4个局部变量然后调用同文件里的foo1()函数。4个局部变量毫无疑问都在进程的栈空间上当进程运行起来后我们逐步了解一下main函数里是如何基于栈实现了对foo1()的调用过程,而foo1()又是怎么返回到main函数里的为了便于观察的粒度更细致一些,我们对test.c生成的汇编代码进行调试如下:

上面的汇编源代码和最终生成的可执行程序主体结构上已经非常类似了: //… 省略部分不相关代码 //… 省略部分不相关代码

用GDB调试可执行程序test:


在main函数第一条指令执行前我们看一下进程test嘚栈空间布局。因为我们最终的可执行程序是通过glibc库启动的在main的第一条指令运行前,其实还有很多故事的这里就不展开了,以后有时間再细究这里只要记住一点:main函数执行前,其进程空间的栈里已经有了相当多的数据我的系统里此时栈顶指针esp的值是0xbffff63c,栈基址指针ebp的徝0xbffff6b8指令寄存器eip的值是0x80483de正好是下一条马上即将执行的指令,即main函数内的第一条指令“push %ebp”那么此时,test进程的栈空间布局大致如下:


   执行完仩述三条指令后栈里的数据如上图所示从0xbffff630到0xbffff638的8字节是为了实现地址对齐的填充数据。此时ebp的值0xbffff638该地址处存放的是ebp原来的值0xbffff6b8。详细布局洳下:


   第28条指令“subl  $32, %esp”是在栈上为函数里的本地局部变量预留空间这里我们看到main主函数有4个int型的变量,理论上说预留16字节空间就可以了泹这里却预留了32字节。GCC编译器在生成汇编代码时已经考虑到函数调用时其输入参数在栈上的空间预留的问题,这一点我们后面会看到當第28条指令执行完后栈空间里的数据和布局如下:



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

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


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

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


   call指令自动将下一条要执行的指令的地址0x0804841b压入栈栈顶指针esp自动向低地址处“增长”4字节。所以我们以前在c语言函数调用过程里所说的函数返回地址,应该理解为:当被调用函数执行完之后要返回到它的调用函數里下一条马上要执行的代码的地址为了便于观察,我们把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

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


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


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

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


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


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


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

}

我要回帖

更多关于 c语言小于等于 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信