假设:
- 假设有函数main, f1,f2和g。其中,mainf1和f2,并且f1调用g,f2和g不再调用任何函数。
- 栈空间从高地址往低地址增长(linux是这样的)。
- 假设编译时没有开启eliminate frame pointer优化。如果开启该优化的话,对于参数固定的函数,不需要使用EBP寄存器来存储当前frame pointer。
- 如下的栈布局只是一般的情况,对于一些特殊编译器优化,还会有其它的元素。
- 本文也不讨论关于栈帧对齐以及栈内数据对齐的问题。
用户程序的栈空间
--------------- g函数栈结束 |
解释
首先在shell中的命令行及参数,传递给内核代码。内核代码进行一些处理以后(包括设置用户栈的起始地址——从2^32+2^31(3G)地址开始的第一个页(假设为4K大小)开始,但是页内地址随机),将控制权,以及命令、参数传递给用户代码。用户代码从_start函数开始,这可以从对应可执行文件的代码段的起始地址看出来。
_start函数拷贝内核代码传给的命令行参数,将EBP清零,将栈顶指针按照16字节对齐,然后将得到的命令行参数,以及_init,_fini,和main函数的地址传递给__libc_start_main函数。传递参数给__libc_start_main的过程,通过将参数在当前栈顶上一一入栈实现。
_start函数调用__libc_start_main函数之前,栈顶指针指向两个函数的栈帧的分界线(上图的L0所示)。call指令自动将返回地址存储在当前栈顶。__libc_start_main函数首先通过“push ebp; mov ebp esp”,存储上个栈帧的指针,同时将ebp寄存器更新为当前esp。也就是说,栈帧指针(即当前ebp的值),比上图中的当前函数栈起点少两个地址值所占的空间大小(返回地址以及EBP地址,在32位机器上一共是8个字节)。
然后,__libc_start_main函数继续通过push指令,保存相关寄存器(callee-saveregisters)的值。
接着,__libc_start_main函数通过“sub esp, offe”指令,将栈顶指针调整到__libc_start_main函数栈的结束地址(图中L1所示)。这样,可以通过esp加上一个正数偏移量访问__libc_start_main函数的局部变量,以及__libc_start_main函数调用的其它函数(init,fini, main, exit)的形式参数。
__libc_start_main函数调用main函数之前,栈顶指针已经调整到图中L1所示。__libc_start_main将main函数的参数值传递到最接近栈顶指针的位置(假设main函数有两个参数,那么分别是esp和esp+0x4位置)。然后call指令自动将返回地址入栈,栈顶指针指向L2。
然后,像__libc_start_main函数一样,main函数保存ebp,修改ebp为当前栈顶指针,调整esp到图中L3所示位置。这样main函数通过ebp+0x8,ebp+0x12等可以访问__libc_start_main传递给自己的参数。因为main函数调用f1和f2,所以传递给f1和f2的参数也都在最接近L3的位置存储(假设f1有2个参数,f2有3个参数,那么f1的2个参数分别在esp和esp+0x4, f2的3个参数在esp、esp+0x4和esp+0x8)。