先写个简单的小demo, 分析一下c的函数调用过程。
void test(int a, int b) { a++; b+=2; } void haha() { test(3, 5); } int main() { haha(); return 0; }
下面是haha()的汇编代码:
Dump of assembler code for function haha:
0x080483a1 <+0>: push %ebp
0x080483a2 <+1>: mov %esp,%ebp
0x080483a4 <+3>: sub $0x8,%esp
=> 0x080483a7 <+6>: movl $0x5,0x4(%esp)
0x080483af <+14>: movl $0x3,(%esp)
0x080483b6 <+21>: call 0x8048394 <test>
0x080483bb <+26>: leave
0x080483bc <+27>: ret
End of assembler dump.
此时程序暂停在test(3, 5) 这一行, 调用test时, 首先会把函数参数压栈。
0x080483a4 <+3>: sub $0x8,%esp 表示栈顶加8个字节, 可以保留两个int型参数。
0x080483a7 <+6>: movl $0x5,0x4(%esp) 第二个参数先入栈
0x080483af <+14>: movl $0x3,(%esp) 第一个参数后如栈
0x080483b6 <+21>: call 0x8048394 <test> call会调用test函数, 这里隐含的操作是eip寄存器内容入栈,用来保存函数调用结束后应该执行的命令地址。
下面是test的汇编代码:
Dump of assembler code for function test:
0x08048394 <+0>: push %ebp
0x08048395 <+1>: mov %esp,%ebp
=> 0x08048397 <+3>: addl $0x1,0x8(%ebp)
0x0804839b <+7>: addl $0x2,0xc(%ebp)
0x0804839f <+11>: pop %ebp
0x080483a0 <+12>: ret
End of assembler dump.
0x08048394 <+0>: push %ebp 首先保存调用函数(haha)的栈底地址
0x08048395 <+1>: mov %esp,%ebp 然后把esp 内容赋给ebp寄存器, ebp保存新栈帧 (test)的栈底地址。 此时esp ebp指向栈的同一个位置。
此时ebp的内容保存着调用函数的栈底地址, 以此为基准, %ebp + 4 地址保存返回地址, %ebp + 8 保存 第一个参数, &ebp + 12 保存第二个参数, 而比%ebp - x (x 为正数) 保存test函数的局部变量(这个例子没有用到局部变量)。
0x08048397 <+3>: addl $0x1,0x8(%ebp) 第一个参数+1
0x0804839b <+7>: addl $0x2,0xc(%ebp) 第二个参数+2
0x080483a0 <+12>: ret 返回。
ebp 具有十分的意义, 它总是保存上层调用时的ebp值, 而且在没层调用中都可一通过ebp (向栈底方向)找到返回地址, 参数, (向栈顶方向) 找到局部变量。 这是一个递归的过程。
如上所示, 当程序执行到 0x08048397 <+3>: addl $0x1,0x8(%ebp) 的时候, 打印ebp 内容:
(gdb) p $ebp
$1 = (void *) 0xbffff310
表示test的栈底地址为0xbffff310
0xbffff310 + 8 为第一参数3
(gdb) x /1uw 0xbffff318
0xbffff318: 3
0xbffff310 + 12 为第一参数5
(gdb) x /1uw 0xbffff31c
0xbffff31c: 5
0xbffff310 + 4 为 返回地址
(gdb) x /1aw 0xbffff314
0xbffff314: 0x80483bb <haha+26>
而 0x80483bb <haha+26> 对应函数haha中的 0x080483bb <+26>: leave