1 程序的地址空间布局
一个程序在内存中运行,它靠四个东西:代码、栈、堆、数据段。代码段主要存放的就是可执行文件中的代码;数据段存放的就是程序中全局变量和静态变量;堆中是程序的动态内存区域,当程序使用malloc或new得到的内存是来自堆的;栈中维护的是函数调用的上下文,离开了栈就不可能实现函数的调用。在linux中它们的地址空间分布如下:
其中最让我迷惑的还是栈,它是怎么保存程序执行的上下文的?我对它的理解还是保留在数据结构学的栈,什么先进先出,只对栈顶进行操作,对于它的具体应用还真是不太了解。以前写代码就很好奇,当调用一个程序时,栈中到底保留了些什么东西?今天终于有了点理解。
2 堆栈帧
堆栈帧也叫活动记录,保存的是一个函数调用所需要维护的所有信息。它主要包含三个内容:
- 函数的返回地址和参数
- 临时变量:包括函数的非静态局部变量以及编译器自动生成的其它临时变量
- 保存的上下文:包括在函数调用前后需要保存不变的寄存器值
在i386中,一个函数的活动记录用ebp和esp这两个寄存器来划定范围。esp始终指向栈的顶部,同时也指向当前活动记录的顶部。相对的,ebp指向函数活动记录的一个固定位置,ebp又被称为帧指针。一个很常见的活动记录如下:
参数及参数之后的数据是当前的活动记录,ebp固定在上图的位置,不随函数的执行而改变,相反地,esp始终指向栈顶,因此随着函数的执行,它总是变化的。ebp+4是这个函数的返回地址,ebp+8、ebp+12等是这个函数的参数。ebp指向的是调用该函数前ebp的值,这样在函数返回的时候,ebp就可以通过这个恢复到调用前的值。ebp下面的值是要保存的寄存器的值和函数中的局部变量,当然也可以不保存ebp的值,不过这样会减慢帧上寻址速度和无法准确定位函数的调用轨迹。之所以会形成这样的活动记录,是因为一个i386中函数总是这样调用的:
- 把所有或者部分参数压入栈中
- 把当前call调用指令的下一条指令地址压入栈中
- 跳转到call调用的函数去执行
后面两步由call指令自动完成,i386函数体的开头一般是这样的:
- push ebp;保存调用前的ebp值
- push ebp,esp;ebp指向栈中保存调用前ebp值的位置
- sub esp,xxx;在栈上分配xxx字节的空间,这个是可选的
- push xxx;保存xxx寄存器的值,这个也是可选的
在函数返回时,一般是这样的:
- pop xxx;恢复寄存器的值;如果开头有push xxx
- mov esp,ebp;恢复esp,同时回收局部变量的空间
- pop ebp;恢复调用前ebp的值
- mov eax,xxx;如果函数有返回值,那么返回值一般放在eax中
- ret;从栈中取回返回地址,并跳转到该位置,栈中参数空间的回收和调用惯例有个,看以参考http://www.cnblogs.com/chengxuyuancc/archive/2013/05/28/3103956.html
第2和3条指令可以用leave指令代替
为了加深印象,下面反汇编下面一个函数看看:
int foo(int a) { int b; b = a * 100; return b; } int main ( int argc, char *argv[] ) { foo(2); return 0; } /* ---------- end of function main ---------- */
反汇编代码:
可以明显的看到foo和main函数中的代码和前面我们给出的函数开始和结束的一般代码是一样的。在main函数中调用foo前先是用movl指令把参数放入栈顶。