札记之函数调用之帧栈

什么是栈帧 :

  • 栈帧就是一个函数执行的环境:函数调用框架、函数参数、函数的局部变量、函数执行完后返回到哪里等等,栈是从高地址向低地址延伸的。
  • 每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。
  • 寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低地址)

名词解释:

寄存器和一些汇编指令
名称 参数说明
EAX 累加(Accumulator)寄存器,常用于函数返回值
EBX 基址(Base)寄存器,以它为基址访问内存
ECX 计数器(Counter)寄存器,常用作字符串和循环操作中的计数器
EDX 数据(Data)寄存器,常用于乘除法和I/O指针
ESI 源变址寄存器
DSI 目的变址寄存器
ESP 堆栈(Stack)指针寄存器,指向堆栈顶部
EBP 基址指针寄存器,指向当前堆栈底部
EIP 指令寄存器,指向下一条指令的地址
call 1将当前指令的下一条指令保存,保存的目的是为了恢复(入栈保存;2跳转至目标函数的地址处;
ret 1将当前栈顶的内容出栈;2用该内容修改eip
入栈push和出栈pop
  • push ebp就等于将ebp的值保存到栈中,并且将当前esp下移
  • pop ebp就等于将ebp的值从栈中取出来,将ebp指向这个值

通过一段代码查看函数调用过程

1
2
3
4
5
6
7
8
9
10
11
12
1 #include <stdio.h>
2 int add(int x, int y)
3 {
4 return x+y;
5 }
6 int main()
7 {
8 int x = 10;
9 int y = 20;
10 int num = add(x, y);
11 printf("num %d \n", num);
12 }
GDB调试结果
1
2
3
4
5
6
7
8
//对应源码第3行
{
//push就是压栈,把ebp 的地址压入栈中(每次压栈后,esp都指向最新的栈顶位置)
0x0000000000400531 <main+0>: 55 push %rbp
//使rbp=rsp,即rbp也指向栈顶位置
0x0000000000400532 <main+1>: 48 89 e5 mov %rsp,%rbp
//rsp偏移10,开辟一个main空间
0x0000000000400535 <main+4>: 48 83 ec 10 sub $0x10,%rsp
如图所示:

札记之函数调用之帧栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//对应源码第8行
//x值给到rbp偏移-4的位置
int x = 10;
0x0000000000400539 <main+8>: c7 45 fc 0a 00 00 00 movl $0xa,-0x4(%rbp)

//对应源码第9行
y值给到rbp偏移-8的位置
int y = 20;
0x0000000000400540 <main+15>: c7 45 f8 14 00 00 00 movl $0x14,-0x8(%rbp)

//对应源码第10行
int num = add(x, y);
//10 =》edx
0x0000000000400547 <main+22>: 8b 55 f8 mov -0x8(%rbp),%edx
//20 =》eax
0x000000000040054a <main+25>: 8b 45 fc mov -0x4(%rbp),%eax
//10 =》esi
0x000000000040054d <main+28>: 89 d6 mov %edx,%esi 0x000000000040054f <main+30>: 89 c7 mov %eax,%edi
//20 =》edi
0x0000000000400551 <main+32>: e8 c7 ff ff ff callq 0x40051d
如图所示:

札记之函数调用之帧栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
##### 对应源码第3
//1、将当前指令的下一条指令的地址压入栈中;
//2、跳转至目标函数的地址开始过程调用。
当执行到callq =》add (x=0, y=0) at tst.c:4
{
//压栈rbp
0x000000000040051d <add+0>: 55 push %rbp
//栈顶=栈底
0x000000000040051e <add+1>: 48 89 e5 mov %rsp,%rbp
//把edi的值赋给rbp寄存器偏移8位的地址
0x0000000000400521 <add+4>: 89 7d fc mov %edi,-0x4(%rbp)
//把esi的值赋给rbp寄存器偏移8位的地址,此时rsp地址减小
0x0000000000400524 <add+7>: 89 75 f8 mov %esi,-0x8(%rbp)

##### 对应源码第4
return x+y;
//值10赋给寄存器eax
0x0000000000400527 <add+10>: 8b 45 f8 mov -0x8(%rbp),%eax
//值20赋给寄存器edx
0x000000000040052a <add+13>: 8b 55 fc mov -0x4(%rbp),%edx
//eax = edx+eax
0x000000000040052d <add+16>: 01 d0 add %edx,%eax

##### 对应源码第5
}
//#弹出父函数的rbp,并把rsp往下移动,(此时rsp在值和在未调用之前的一致的)
0x000000000040052f <add+18>: 5d pop %rbp //从栈中pop出一条指令
0x0000000000400530 <add+19>: c3 retq //根据这条指令退栈到main的寄存器rbp地址处
如图所示:

札记之函数调用之帧栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
0x0000000000400556 in main () at tst.c:12
0x0000000000400556 <main+37>: 89 45 f4 mov %eax,-0xc(%rbp) //add的值给到rbp

//对应源码第11行
printf("num %d \n", num);
0x0000000000400559 <main+40>: 8b 45 f4 mov -0xc(%rbp),%eax //赋值给eax
0x000000000040055c <main+43>: 89 c6 mov %eax,%esi //eax 值赋给esi
0x000000000040055e <main+45>: bf 00 06 40 00 mov $0x400600,%edi //置空edi
0x0000000000400563 <main+50>: b8 00 00 00 00 mov $0x0,%eax //置空eax
0x0000000000400568 <main+55>: e8 93 fe ff ff callq 0x400400 <printf@plt>

执行到callq
0x0000000000400400 in printf@plt ()
0x0000000000400400 <printf@plt+0>: ff 25 12 0c 20 00 jmpq *0x200c12(%rip) # 0x601018

0x0000000000400406 in printf@plt ()
0x0000000000400406 <printf@plt+6>: 68 00 00 00 00 pushq $0x0

}
num 30
//leaveq相当于:1: movq %rbp, %rsp, 2: popq %rbp
//清栈,退出
0x000000000040056d <main+60>: c9 leaveq
0x000000000040056e <main+61>: c3 retq
如图所示:

札记之函数调用之帧栈

  • 此时函数整个调用过程就结束了,main函数栈恢复到了调用之前的状态
-------------本文结束感谢您的阅读-------------
坚持原创技术分享,您的支持将鼓励我继续创作!