1. 测试代码如下,下文的分析基于该代码

#include <stdio.h>

void func_b(int va, int vb)
{
    int vc;

    vc = va + vb;
}
void func_a(void)
{
    int va = 2, vb = 3;
    func_b(va, vb);
}
int main(void)
{
    func_a();
    return 0;
}

2 使用gdb,断点查看当前进程寄存器信息

(gdb) info reg  
eax            0x5      5                       //<--- 累加器
ecx            0x6cb4f478       1823798392      //<--- 计数寄存器
edx            0x2      2                       //<--- 数据寄存器
ebx            0x283ff4 2637812                 //<--- 基址寄存器
esp            0xbffff750       0xbffff750      //<--- 堆栈指针寄存器
ebp            0xbffff760       0xbffff760      //<--- 基指针寄存器
esi            0x0      0                       //<--- 变址寄存器
edi            0x0      0                       //<--- 变址寄存器
eip            0x80483c6        0x80483c6 <func_b+18> //<--- 指令指针寄存器
eflags         0x200286 [ PF SF IF ID ]         //<--- 标志寄存器
cs             0x73     115                     //<--- 代码段寄存器
ss             0x7b     123                     //<--- 堆栈段寄存器
ds             0x7b     123                     //<--- 数据段寄存器
es             0x7b     123                     //<--- 附加段寄存器
fs             0x0      0                       //<--- 附加段寄存器
gs             0x33     51                      //<--- 附加段寄存器

3. 函数调用过程及栈空间信息

(1). 将函数参数从右至左入栈。
(2). 函数指针入栈,调用函数。
(3). ebp指针入栈。
(4). 函数局部变量从前至后入栈。

high 
	| 。。。           |
	| 返回地址(main)   |
	| 上一层ebp地址    |      
	| 局部变量va       | <---- 局部变量从前至后压栈
	| 局部变量vb       | 
	| 。。。           |
	| 参数vb           | <---- 函数形参按照从右至左压栈
	| 参数va           |
	| 返回地址(func_a) | <---- eip 地址
	| 上一层ebp地址    | <---- ebp 地址
	| 局部变量vc       | 
	| 。。。           | <---- esp 栈顶指针
low

4. 查看从栈顶指针esp后的一段连续地址空间内容

	(gdb) x/14x 0xbffff750 
	0xbffff750:     0x0011e030      
	0xbffff754:     0x08049ff4
	0xbffff758:     0xbffff788
	0xbffff75c:     0x00000005  //<------ func_b函数的局部变量vc
	0xbffff760:     0xbffff780  //<------ 保存上一层函数ebp的地址,用于回溯
	0xbffff764:     0x080483ee  //<------ 保存上一层函数eip的值,即为上一层函数指针地址,这里为func_a
	0xbffff768:     0x00000002  //<------ 保存当前函数的参数,这里为func_b函数的va
	0xbffff76c:     0x00000003  //<------ 保存当前函数的参数,这里为func_b函数的vb
	0xbffff770:     0x0015d4a5 
	0xbffff774:     0x0011e030
	0xbffff778:     0x00000003  //<------ func_a函数的局部变量vb
	0xbffff77c:     0x00000002  //<------ func_a函数的局部变量va
	0xbffff780:     0xbffff788  //<------ 保存上一层函数ebp的地址,用于回溯
	0xbffff784:     0x080483f8  //<------ 保存上一层函数eip的值,即为上一层函数指针地址,这里为main

5. 通过反汇编来了解函数压栈过程

使用命令objdump xxx.elf -d 生成汇编代码

	080483b4 <func_b>: //<------ 到这里时 esp=0xbffff764 ebp=0xbffff780
	80483b4:		push   %ebp                   //ebp压栈,把0xbffff780压入[0xbffff760],
													并且调整栈顶指针esp - 0x4 = 0xbffff760
	80483b5:		mov    %esp,%ebp              //保存栈顶指针esp至ebp ,ebp=0xbffff760
	80483b7:		sub    $0x10,%esp             //调整栈顶指针esp, esp - 0x10 = 0xbffff750
	80483ba:		mov    0xc(%ebp),%eax         //取[ebp + 0xc] = [0xbffff76b]位置的vb=3赋值给eax
	80483bd:		mov    0x8(%ebp),%edx         //取[ebp + 0x8] = [0xbffff768]位置的va=2赋值给edx
	80483c0:		lea    (%edx,%eax,1),%eax     //计算 eax = eax + edx
	80483c3:		mov    %eax,-0x4(%ebp)        //将vc=5放入局部变量地址[ebp - 4] = [0xbffff75b]
	80483c6:		leave  
	80483c7:		ret    

	080483c8 <func_a>: //<------ 到这里时 esp=0xbffff784 ebp=0xbffff788
	80483c8:		push   %ebp                   //ebp压栈,即将值0xbffff788写入地址[0xbffff780],并且调整栈顶指针esp - 0x4 = 0xbffff780
	80483c9:		mov    %esp,%ebp              //保存栈顶指针esp至ebp,ebp=0xbffff780
	80483cb:		sub    $0x18,%esp             //调整栈顶指针esp,esp - 0x18 = 0xbffff768
	80483ce:		movl   $0x2,-0x4(%ebp)        //局部变量压栈,ebp-4位置赋值为va=2,即将值2写入地址[0xbffff77b]位置
	80483d5:		movl   $0x3,-0x8(%ebp)        //局部变量压栈,ebp-8位置赋值为vb=3,即将值3写入地址[0xbffff778]位置
	80483dc:		mov    -0x8(%ebp),%eax        //eax = 3
	80483df:		mov    %eax,0x4(%esp)         //形参从右至左压栈,即将vb=3的值写入地址[esp + 0x4] =[0xbffff76b]位置
	80483e3:		mov    -0x4(%ebp),%eax        //eax = 2
	80483e6:		mov    %eax,(%esp)            //形参从右至左压栈,即将vb=2的值写入地址[esp] = [0xbffff768]位置
	80483e9:		call   80483b4 <func_b>       //将eip压栈,即将值0x080483ee写入地址[0xbffff764], 并且调整栈顶指针esp - 0x4 = 0xbffff780,然后调用函数func_b
	80483ee:		leave  
	80483ef:		ret    

6. 利用ebp寄存器实现完整栈回溯信息

可用于gdb调试时backtrace显示为??导致无法获取回溯信息的情况。

7. 待分析

(1) 函数调用中sub $0x18,%esp的 $0x18是如何计算出

X86函数局部变量需要16Bytes对齐,func_a的局部变量为8Bytes,向上对齐到0x10。
加上函数参数压栈的va及vb的8Bytes。
http://stackoverflow.com/questions/612443/why-does-the-mac-abi-require-16-byte-stack-alignment-for-x86-32

(2) 栈空间中未在汇编指令中出现的未知值如何生成的

由于局部变量对齐产生的缝隙,应该是为未指定的变量,每次结果不一定一样。

8.参考链接

(1) X86寄存器 http://www.eecg.toronto.edu/~amza/www.mindsec.com/files/x86regs.html
(2) 《程序员的自我修养-链接、装载与库》10.2节
(3) http://devpit.org/wiki/x86ManualBacktrace
(4) wiki http://en.wikibooks.org/wiki/X86_Disassembly/Functions_and_Stack_Frames

Author: chenxiawei@gmail.com