函数栈2:gcc及llvm中x86机器的栈分配

假设:

  • 假设有函数main, f1,f2和g。其中,mainf1和f2,并且f1调用g,f2和g不再调用任何函数。
  • 栈空间从高地址往低地址增长(linux是这样的)。
  • 假设编译时没有开启eliminate frame pointer优化。如果开启该优化的话,对于参数固定的函数,不需要使用EBP寄存器来存储当前frame pointer。
  • 如下的栈布局只是一般的情况,对于一些特殊编译器优化,还会有其它的元素。
  • 本文也不讨论关于栈帧对齐以及栈内数据对齐的问题。

用户程序的栈空间

--------------- g函数栈结束  
local variables of g
---------------
callee-save registers
---------------
EBP
---------------
return address
++++++++++++++++++++++++ g函数栈开始
++++++++++++++++++++++++ f1函数栈结束
parameters of g
---------------
local variables of f1
---------------
callee-save registers
---------------
EBP
---------------
return address
+++++++++++++++++++++++++ f1及f2函数栈开始
+++++++++++++++++++++++++ main函数栈结束:L3
parameters of f1 and f2
---------------
local variables of main
---------------
callee-save regsiters
---------------EBP
--------------- :L2
return address
+++++++++++++++++++++++++ main函数栈开始(大小依赖于实际的main函数) :L1
+++++++++++++++++++++++++ __libc_start_main函数结束
parameters of main, init, fini,exit, .etc
----------------
local variables of __libc_start_main
----------------
callee-save registers
----------------
EBP
---------------
return address
+++++++++++++++++++++++++ __libc_start_main函数栈开始(大小为0x70字节):L0
+++++++++++++++++++++++++ __start函数结束
parameters of __libc_start_main
+++++++++++++++++++++++++ _start函数栈开始 (大小为0x20字节)

解释

  1. 首先在shell中的命令行及参数,传递给内核代码。内核代码进行一些处理以后(包括设置用户栈的起始地址——从2^32+2^31(3G)地址开始的第一个页(假设为4K大小)开始,但是页内地址随机),将控制权,以及命令、参数传递给用户代码。用户代码从_start函数开始,这可以从对应可执行文件的代码段的起始地址看出来。

  2. _start函数拷贝内核代码传给的命令行参数,将EBP清零,将栈顶指针按照16字节对齐,然后将得到的命令行参数,以及_init,_fini,和main函数的地址传递给__libc_start_main函数。传递参数给__libc_start_main的过程,通过将参数在当前栈顶上一一入栈实现。

  3. _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)的形式参数。

  4. __libc_start_main函数调用main函数之前,栈顶指针已经调整到图中L1所示。__libc_start_main将main函数的参数值传递到最接近栈顶指针的位置(假设main函数有两个参数,那么分别是esp和esp+0x4位置)。然后call指令自动将返回地址入栈,栈顶指针指向L2。

  5. 然后,像__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)。