资源描述:
《main函数调用子函数堆栈解析》由会员上传分享,免费在线阅读,更多相关内容在行业资料-天天文库。
1、·任何一个程序通常都包括代码段和数据段,这些代码和数据本身都是静态的。程序要想运行,首先要由操作系统负责为其创建进程,并在进程的虚拟地址空间中为其代码段和数据段建立映射。光有代码段和数据段是不够的,进程在运行过程中还要有其动态环境,其中最重要的就是堆栈。图3所示为Linux下进程的地址空间布局:图3Linux下进程地址空间的布局 首先,execve(2)会负责为进程代码段和数据段建立映射,真正将代码段和数据段的内容读入内存是由系统的缺页异常处理程序按需完成的。另外,execve(2)还会将bss段清零,这就
2、是为什么未赋初值的全局变量以及static变量其初值为零的原因。进程用户空间的最高位置是用来存放程序运行时的命令行参数及环境变量的,在这段地址空间的下方和bss段的上方还留有一个很大的空洞,而作为进程动态运行环境的堆栈和堆就栖身其中,其中堆栈向下伸展,堆向上伸展。知道了堆栈在进程地址空间中的位置,我们再来看一看堆栈中都存放了什么。相信读者对C语言中的函数这样的概念都已经很熟悉了,实际上堆栈中存放的就是与每个函数对应的堆栈帧。当函数调用发生时,新的堆栈帧被压入堆栈;当函数返回时,相应的堆栈帧从堆栈中弹出。典型
3、的堆栈帧结构如图4所示。堆栈帧的顶部为函数的实参,下面是函数的返回地址以及前一个堆栈帧的指针,最下面是分配给函数的局部变量使用的空间。一个堆栈帧通常都有两个指针,其中一个称为堆栈帧指针,另一个称为栈顶指针。前者所指向的位置是固定的,而后者所指向的位置在函数的运行过程中可变。因此,在函数中访问实参和局部变量时都是以堆栈帧指针为基址,再加上一个偏移。对照图4可知,实参的偏移为正,局部变量的偏移为负。图4典型的堆栈帧结构 介绍了堆栈帧的结构,我们再来看一下在Inteli386体系结构上堆栈帧是如何实现的。图5和图
4、6分别是一个简单的C程序及其编译后生成的汇编程序。图5一个简单的C程序example1.cintfunction(inta,intb,intc){ charbuffer[14]; int sum; sum=a+b+c; returnsum;}voidmain(){ int i; i=function(1,2,3);}图6example1.c编译后生成的汇编程序example1.s1 .file "example1.c"2 .version "01.01"3gcc
5、2_compiled.:4.text5 .align46.globlfunction7 .type function,@function8function:9 pushl%ebp10 movl%esp,%ebp11 subl$20,%esp12 movl8(%ebp),%eax13 addl12(%ebp),%eax14 movl16(%ebp),%edx15 addl%eax,%edx16 movl%edx,-20(%ebp)17 movl-20(%ebp),%eax18 jmp
6、.L119 .align420.L1:21 leave22 ret23.Lfe1:24 .size function,.Lfe1-function25 .align426.globlmain27 .type main,@function28main:29 pushl%ebp30movl%esp,%ebp31 subl$4,%esp32 pushl$333 pushl$234 pushl$135 callfunction36 addl$12,%esp37 movl%eax,%e
7、ax38 movl%eax,-4(%ebp)39.L2:40 leave41 ret42.Lfe2:43 .size main,.Lfe2-main44 .ident "GCC:(GNU)2.7.2.3"这里我们着重关心一下与函数function对应的堆栈帧形成和销毁的过程。从图5中可以看到,function是在main中被调用的,三个实参的值分别为1、2、3。由于C语言中函数传参遵循反向压栈顺序,所以在图6中32至34行三个实参从右向左依次被压入堆栈。接下来35行的call指令除了将控制转移到
8、function之外,还要将call的下一条指令addl的地址,也就是function函数的返回地址压入堆栈。下面就进入function函数了,首先在第9行将main函数的堆栈帧指针ebp保存在堆栈中并在第10行将当前的栈顶指针esp保存在堆栈帧指针ebp中,最后在第11行为function函数的局部变量buffer[14]和sum在堆栈中分配空间。至此,函数function的堆栈帧就构建完成了,其结构如图7所示