一.堆栈在地址空间中的位置
任何一个程序通常都包含代码段和数据段,这些代码和数据本身都是静态的。程序想要运行,首先要由操作系统为其创建进程,并在进程的虚拟地址空间为其代码段和数据段建立映射。光有代码段和数据段是不够的,进程在运行过程中还要有其动态环境,其中最重要的就是堆栈。如图所示为Linux下进程的地址空间分布:
首先,execv(2)会为进程代码段和数据段建立映射,真正将代码段和数据段的内容读入内存是由系统的缺页异常处理程序按需完成的。另外,execv(2)还会将bss段清零,这就是为什么未赋初值的全局变量以及static变量其初值为零的原因。进程用户空间的最高位置用来存放程序运行时的命令行参数和环境变量的,在这段地址空间的下方和bss段的上方还留有一个很大的空间,而作为进程动态运行环境的堆栈和堆就在其中,其中堆栈向下伸展,堆向上伸展。
二.堆栈帧的结构
堆栈中实际上存放的是与每个函数对应的堆栈帧,当函数调用发生时,新的堆栈帧被压入堆栈;当函数返回时,相应的堆栈帧从堆栈中弹出。堆栈帧结构如图所示:
堆栈帧的顶部为函数的实参,下面是函数的返回地址以及前一个堆栈帧的指针,最下面是分配给函数的局部变量使用的空间。一个堆栈帧通常有两个指针,一个为堆栈帧指针,另一个为栈顶指针。前者所指向的位置是固定的,而后者所指向的位置在函数运行过程中可变。因此,在函数访问实参或者是局部变量时都是以堆栈帧指针为基址,再加上偏移量。由图可知,实参的偏移量为正,局部变量的偏移量为负。
三.函数栈帧的分析
如下代码:
int function(int a,int b,int c)
{
char buffer[14];
int sum;
sum=a+b+c;
return sum;
}
int main()
{
int i;
i=function(1,2,3);
}
函数function的堆栈帧如图所示:
(1)函数function堆栈帧的构建
其中,function是在main函数中被调用的,三个实参的值分别为1,2,3。由于C语言中参数传递遵循反向压栈顺序,,因此三个参数从右至左依次被压入堆栈。接下来除了将控制转移到function之外,还要将下一条指令addl的地址,也就是function函数的返回地址压入堆栈。接着进入function函数,首先将main函数的堆栈帧指针ebp保存着堆栈中,并在下一次将当前的栈顶指针esp保存着堆栈帧指针ebp中,最后为function函数的局部变量buffer[14]和sum 在堆栈中分配空间。
(2)函数function将a,b,c的值赋给sum的过程
在函数中访问实参和局部变量时都是以堆栈帧指针为基址,再加上偏移量。在这里堆栈帧指针就是ebp,为了清楚起见,图中标出了堆栈帧中所以成分相对于堆栈帧指针ebp的偏移。
(3)函数hufunction执行完之后与其对应的堆栈帧销毁过程
首先,leave指令将堆栈帧指针ebp拷贝到esp中(即esp=ebp,这样esp指向ebp的地址,所以局部变量sum,buffer的空间就被释放了),于是在堆栈中为局部变量buffer[14]和sum分配的空间就被释放了;
leave指令还有一个功能,就是从堆栈帧弹出一个机器字并将其存放到ebp中(即接着上边的操作,从堆栈中pop一个字其实就是把previous ebp的值取出来放到ebp寄存器中),这样ebp就被恢复为main函数的堆栈帧指针了。后边的ret指令再次从堆栈帧弹出一个机器字并将其存放在指令指针eip中(即将address of addl pop出来赋给寄存器eip),这样控制就返回了main函数中的addl指令处,addl指令将栈顶指针esp加上12,这样调入函数function之前压入堆栈的三个实参所占用的堆栈空间也就被释放了。
转载于:https://blog.51cto.com/10548195/1826617