先列出栈调用的基本原则
- 栈是通过rsp(栈顶指针)、rbp(栈底指针)两个指针来标识的
- 对于栈上的访问都是通过栈底指针($rbp)的偏移来访问的
- 函数调用跳转时(callq)在新帧的栈首8Bytes存放上一帧的指令地址($rip的值)
- 通常函数的起始操作为push $rbp,将上帧的栈底地址8Bytes压入栈中
- 在保存完指令地址和栈底地址后,会进行一次sub xxx,$rsp,为当前函数内所有在栈上的局部变量都申请好需要的栈空间
- 函数调用前将需要保存的寄存器值和超过6个的参数都压入栈中
- 函数结束ret指令干了两件事:先出栈;再将出栈的值放到CPU的IP寄存器中。(因为IP寄存器中永远放的是下一次执行指令的地址,所以就顺理成章的在函数调用完之后依旧接着原来的代码继续执行)
以下面简单函数为例,解析函数调用栈的过程。
#include <stdio.h>
int sum(int a, int b)
{
int tmp = 0;
tmp = a+b;
return tmp;
}
int main(void)
{
int a=1;
int b=2;
int ret = 0;
ret = sum(a,b);
return ret;
}
汇编语言如下:
(gdb) disassemble main
Dump of assembler code for function main:
0x00000000004004f1 <+0>: push %rbp
0x00000000004004f2 <+1>: mov %rsp,%rbp
0x00000000004004f5 <+4>: sub $0x10,%rsp-----------设置main函数需要的栈空间
0x00000000004004f9 <+8>: movl $0x1,-0x4(%rbp)-------放入局部变量
0x0000000000400500 <+15>: movl $0x2,-0x8(%rbp)
0x0000000000400507 <+22>: movl $0x0,-0xc(%rbp)
0x000000000040050e <+29>: mov -0x8(%rbp),%edx-------reg中写入sum函数入参
0x0000000000400511 <+32>: mov -0x4(%rbp),%eax
0x0000000000400514 <+35>: mov %edx,%esi
0x0000000000400516 <+37>: mov %eax,%edi
0x0000000000400518 <+39>: callq 0x4004d0 <sum>--------调用时,在$rsp 0x7fffffffe8e0
压栈下一次执行命令的地址 0x 040051d,
0x000000000040051d <+44>: mov %eax,-0xc(%rbp)
0x0000000000400520 <+47>: mov -0xc(%rbp),%eax
0x0000000000400523 <+50>: leaveq
0x0000000000400524 <+51>: retq
End of assembler dump.
(gdb) disassemble sum
Dump of assembler code for function sum:
0x00000000004004d0 <+0>: push %rbp-------压栈main函数rbp指针,0x7fffffffe8f0
0x00000000004004d1 <+1>: mov %rsp,%rbp-------设置sum函数栈底指针,参数入栈从右向左(原因时因为不定长参数)
0x00000000004004d4 <+4>: mov %edi,-0x14(%rbp)-----读入参数放入sum栈空间
0x00000000004004d7 <+7>: mov %esi,-0x18(%rbp)
0x00000000004004da <+10>: movl $0x0,-0x4(%rbp)
0x00000000004004e1 <+17>: mov -0x18(%rbp),%eax
0x00000000004004e4 <+20>: mov -0x14(%rbp),%edx
0x00000000004004e7 <+23>: add %edx,%eax
0x00000000004004e9 <+25>: mov %eax,-0x4(%rbp)
0x00000000004004ec <+28>: mov -0x4(%rbp),%eax
0x00000000004004ef <+31>: pop %rbp--------出栈main函数的指针
0x00000000004004f0 <+32>: retq ----出栈(callq压栈的main函数下一次执行命令的地址0x 040051d),并将值放入rip指针当中
在调用函数sum时,单步执行,查看frame 信息和栈信息如下
(gdb) frame
#0 sum (a=1, b=2) at text.c:6
6 tmp = a+b;
(gdb) info frame
Stack level 0, frame at 0x7fffffffe8e0:
rip = 0x4004e1 in sum (text.c:6); saved rip 0x40051d
called by frame at 0x7fffffffe900
source language c.
Arglist at 0x7fffffffe8d0, args: a=1, b=2
Locals at 0x7fffffffe8d0, Previous frame's sp is 0x7fffffffe8e0
Saved registers:
rbp at 0x7fffffffe8d0, rip at 0x7fffffffe8d8
(gdb) x/100wx 0x7fffffffe8a0
0x7fffffffe8a0: 0x00400540 0x00000000 0x00400343 0x00000000
0x7fffffffe8b0: 0x00000001 0x00c30000 0x00000002 0x00000001
0x7fffffffe8c0: 0x00000000 0x00000000 0x52e1cbc0 0x00000000
0x7fffffffe8d0: 0xffffe8f0 0x00007fff 0x0040051d 0x00000000
0x7fffffffe8e0: 0xffffe9d0 0x00000000 0x00000002 0x00000001
0x7fffffffe8f0: 0x00000000 0x00000000 0x5301d9f4 0x00000037
0x7fffffffe900: 0x00400370 0x00000000 0xffffe9d8 0x00007fff
0x7fffffffe910: 0x00000000 0x00000001 0x004004f1 0x00000000
0x7fffffffe920: 0x52e1cbc0 0x00000037 0x7d1a95d2 0x03d7b1a3
0x7fffffffe930: 0x00000000 0x00000000 0xffffe9d0 0x00007fff
0x7fffffffe940: 0x00000000 0x00000000 0x00000000 0x00000000