程序的内存布局
Linux默认情况下将高地址的1GB空间分配给内核,用户进程使用剩下2GB或者3GB的内存空间。在用户空间里,也有很多地址区间有特殊的地位,一般来讲,应用程序使用的内存空间里有如下"默认"的区域
1、栈:用于维护函数调用的上下文,离开了栈函数调用就没法实现。栈通常在用户空间的最高地址处分配,通常有数兆字节的大小。
2、堆:用来容纳应用程序动态分配的内存区域,当程序使用malloc或new分配内存时,得到的内存来自堆里。堆通常在于栈的下方(低地址方向),在某些时候,堆也可能没有固定统一的存储区域。堆一般比栈大很多,可以有几十至数百兆字节的容量。
3、可执行文件影像:这里存储着可执行文件在内存里的影像,由装载器在装载时将可执行文件的内存读取或映射到这里
4、保留区:保留区并不是一个单一的内存区域,而是对内存中受到保护而禁止访问的内存区域的总称呼,例如,大多数操作系统里,极小的地址通常都是不允许访问的,如NULL。通常C语言将无效指针赋值为0也是出于这个考虑,因为0地址上正常情况下不可能有有效的可访问数据。
如图1是Linux下一个进程里典型的内存布局。
图1 Linux进程地址空间布局
在图1中,有一个没有介绍的区域:"动态链接库映射区",这个区域用于映射装载的动态链接库。在Linux下,如果可执行文件依赖其他共享库,那么系统就会为它在从0x40000000开始的地址分配相应的空间,并将共享库载入到该空间。
栈和调用惯例
栈是现代计算机程序里最为重要的概念之一,几乎每一个程序都使用了栈,没有栈就没有函数,没有局部变量,也就没有我们如今能够看见的所有的计算机语言。
在经典的计算机科学中,栈被定义为一个特殊的容器,用户可以把数据压入栈中(入栈),也可以将已经压入栈中的数据弹出。
在计算机系统中,栈则是一个具有以上属性的动态内存区域。程序可以将数据压入栈中,也可以将数据从栈顶弹出。压栈操作使得栈增大,而弹出操作使得栈减少。
在经典的操作系统里,栈总是向下增长的。在i386下,栈顶由称为esp的寄存器进行定位。压栈的操作使得栈顶的地址减小,弹出的操作使得栈顶的地址增大。图2是一个程序栈实例。
图2 程序栈实例
这里的栈底的地址是0xbfffffff,而esp寄存器标明了栈顶,地址为0xbffffff4。在栈上压入数据会导致esp减小,弹出数据使得esp增大。相反,直接减少esp的值也等效于在栈上开辟空间,直接增大esp的空间等效于在栈上回收空间。
栈在程序运行中具有举足轻重的地址。最重要的,栈保存了一个函数调用所需要的维护信息,这常常被称为堆栈帧或者活动记录。堆栈帧一般包括如下几方面的内容:
1、函数的返回地址
2、临时变量:包括函数的非静态局部变量以及编译器自动生成的其他临时变量
3、保存的上下文:包括在函数调用前后需要保持不变的寄存器
在i386中,一个函数的活动记录用ebp和esp这两个寄存器划定范围。esp寄存器始终指向栈的顶部,同时也就指向了当前函数的活动记录的顶部。而相对,ebp寄存器指向了函数活动记录的一个固定位置,ebp寄存器又被称为帧指针。一个很常见的活动记录所图3所示。
图3 活动记录
在参数之后的数据(包括参数)即是当前函数的活动记录,ebp固定在图中所示的位置,不随这个函数的执行而变化,相反地,esp始终指向栈顶,因此随着函数的执行,esp会不断变化。固定不变的ebp可以用来定位函数活动记录的各个数据。在ebp之前首先是这个函数的返回地址,它的地址是ebp-4,再往前是压入栈中的参数,它们的地址分别是ebp-8,ebp-12等,视参数数量和大小而定。ebp所直接指向的数据是调用该函数前ebp的值,这样在函数返回的时候,ebp可以通过读取这个值恢复到调用前的值。之所以函数的活动记录会形成这样的结构,是因为函数本身是如此书写的:一个i386的函数总是这样调用:
1、把所有或一部分参数压入栈中,如果有其他参数没有入栈,那么使用某些特定的寄存器传递
2、把当前指令的下一条指令的地址压入栈中
3、跳转到函数体执行
其中第2步,第3步由指令call一起执行。跳转到函数体之后即开始执行函数,而i386函数体的"标准"开始是这样的:
* push ebp:把ebp压入栈中(称为old ebp)
* move ebp, esp:ebp=esp(这时ebp指向栈顶,而此时栈顶就是old ebp)
* sub esp,XXX:在栈上分配XXX字节的临时空
* push XXX:如有必要,保存名为XXX寄存器
把ebp压入栈中,是为了在函数返回的时候便于恢复以前的ebp的值。而之所以可能要保存一些寄存器,在于编译器可能要求某些寄存器在调用前后保持不变,那么函数就可以在调用开始时将这些寄存器的值压入栈中,在结束后再取出。不难想象,在函数返回时,所进行的"标准'结尾和"标准"开始正好相反。
* pop XXX:如有必要,恢复保存国的寄存器
* move esp,ebp:恢复ESP同时回收局部变量空间
* pop ebp:从栈中恢复保存的ebp的值
* ret:从栈中取得返回地址,并跳转到该位置