一般来讲,应用程序使用的内存空间里有如下的默认区域:
1 栈:用于维护函数调用的上下文。栈通常在用户空间的最高地址出分配,通常有数兆字节的大小
2 堆:堆是用来容纳应用程序动态分配的内存区域。比如使用malloc和new分配内存就从堆里分配。
3 可执行文件镜像:这里存储着可执行文件在内存里的映射
首先来介绍栈:
在操作系统中,栈总是向下增长的,栈顶由称为esp的寄存器进行定位,压栈的操作使栈顶的地址减小,弹出的操作使栈顶的地址增大。栈保存了一个函数调用所需要维护的信息,这通常称为堆栈帧或活动记录。堆栈帧包括如下几个方面的内容:
1 函数返回地址和参数
2 临时变量:包括函数的非静态局部变量以及编译器自动生成的其他临时变量
3 保存的上下文:包括在函数调用前后需要保持不变的寄存器。
在i386中,一个函数的活动记录用ebp和esp这两个寄存器划定范围。esp寄存器始终指向栈的顶部,ebp寄存器指向了函数活动记录的一个固定位置,ebp寄存器又称为帧指针。常见的活动记录如下图所示:
在ebp之前首先是这个函数的返回地址,它的地址是ebp-4, 再往前是压入栈中的参数,它们的地址分别是ebp-8,ebp-12等等。ebp所直接指向的的数据是调用该函数前ebp的值,这样函数在返回的时候,ebp可以读取这个值恢复到调用前的值。所以一个i386的程序调用顺序如下:
1 把所有或者一部分参数压入栈中,如果有其他参数没有入栈,那么使用某些特定的寄存器传递
2 把当前指令的下一条指令的地址压入栈中
3 跳转到函数体执行
其中2,3由执行call一起执行。跳转到函数体之后就开始执行函数。I386函数体的标准开头过程如下:
1 push ebp: 把ebp压入栈中,也就是old ebp
2 move ebp,esp: ebp=esp(ebp指向栈顶,此时栈顶就是old ebp)
3 sub esp,xxx 在栈上分配xxx字节的临时空间
4 push xxx 保存名为xxx的寄存器
把ebp压入栈中,是为了在函数返回的时候便于恢复以前的ebp值,函数返回的时候过程正好相反。
1 pop xxx
2 mov esp,ebp 恢复esp同时收回局部变量空间
3 pop ebp:从栈中恢复保存的ebp的值
4 ret: 从栈中取得返回地址,并跳转到该位置。
我们用一个简单的函数调用然后查看汇编代码来看下这个过程
#include <stdio.h>
int foo()
{
return 123;
}
int main()
{
int ret;
ret=foo();
return 1;
}
objdump –d stack_test.o 可以看到如下结果
00000000000005fa <foo>:
5fa: 55 push