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