最近在学gdb调试,感觉gdb调试还有好多可以深挖的内容,故函数的调用栈作为gdb分析的基础,不可不会,就参照网上的文章手敲了一篇入门的分析笔记。
源码
通过一个简单的代码来进行分析
// 源码
#include <stdio.h>
int sumUp(int a, int b) {
return a + b;
}
int main() {
int a = 1;
int b = 2;
int sum = sumUp(a, b);
printf("sum of %d + %d = %d\n", a, b, sum);
return 0;
}
反汇编
gdb反汇编之后:
(gdb) disas main
Dump of assembler code for function main():
0x0000000000400551 <+0>: push %rbp // 保存当前栈的栈基地址
0x0000000000400552 <+1>: mov %rsp,%rbp // 建立新的栈帧,以当前栈顶为新栈帧的栈基
0x0000000000400555 <+4>: sub $0x10,%rsp // 扩展栈帧,分配用于保存自动变量的空间
0x0000000000400559 <+8>: movl $0x1,-0x4(%rbp) // 创建变量a
0x0000000000400560 <+15>: movl $0x2,-0x8(%rbp) // 创建变量b
0x0000000000400567 <+22>: mov -0x8(%rbp),%edx
0x000000000040056a <+25>: mov -0x4(%rbp),%eax
0x000000000040056d <+28>: mov %edx,%esi // 传入第一个参数
0x000000000040056f <+30>: mov %eax,%edi // 传入第二个参数
0x0000000000400571 <+32>: callq 0x40053d <sumUp(int, int)>
0x0000000000400576 <+37>: mov %eax,-0xc(%rbp) // 创建变量sum保存返回值
......
0x0000000000400598 <+71>: leaveq // 栈的销毁
0x0000000000400599 <+72>: retq // 保存栈中保存的下一条指令到rip寄存器,控制权返回给调用者
End of assembler dump.
以上输出每行指示一条汇编指令,除程序源码外共有三列,各列含义为:
- 0x0000000000400692: 该指令对应的虚拟内存地址
- <+0>: 该指令的虚拟内存地址偏移量
- push %rbp: 汇编指令
汇编分析
建立新的栈帧
一个函数被调用,首先默认要完成以下动作:
- 将调用函数的栈帧栈底地址入栈,即将bp寄存器的值压入调用栈中
- 建立新的栈帧,将被调函数的栈帧栈底地址放入bp寄存器中
以下两条指令即完成上面动作:
0x0000000000400551 <+0>: push %rbp // 保存当前栈的栈基地址
0x0000000000400552 <+1>: mov %rsp,%rbp //建立新的栈帧,以当前栈顶为新栈帧的栈基
这里我们可以看到main函数也有这两条指令,有点奇怪。 其实不奇怪,因为main并不是程序拉起后第一个被执行的函数,它被_start
函数调用。
创建临时变量
0x0000000000400555 <+4>: sub $0x10,%rsp // 扩展栈帧,分配用于保存自动变量的空间
0x0000000000400559 <+8>: movl $0x1,-0x4(%rbp) // 创建变量a
0x0000000000400560 <+15>: movl $0x2,-0x8(%rbp) // 创建变量b
这里可以看到在rbp之后存放的就是创建的临时变量。
准备函数参数
0x0000000000400567 <+22>: mov -0x8(%rbp),%edx
0x000000000040056a <+25>: mov -0x4(%rbp),%eax
0x000000000040056d <+28>: mov %edx,%esi // 传入第一个参数
0x000000000040056f <+30>: mov %eax,%edi // 传入第二个参数
此处使用了%edx和%eax来辅助进行变量传输,不清楚这样做的原因。
调用函数
0x0000000000400571 <+32>: callq 0x40053d <sumUp(int, int)>
一条call指令,完成了两个任务:
- 将调用函数(main)中的下一条指令(这里为0x400576)入栈,被调函数返回后将取这条指令继续执行,64位rsp寄存器的值减8;
- 修改指令指针寄存器rip的值,使其指向被调函数(sumUp)的执行位置,这里为0x400576;
子函数执行
(gdb) disas sumUp
Dump of assembler code for function sumUp(int, int):
0x000000000040053d <+0>: push %rbp // 保存栈基
0x000000000040053e <+1>: mov %rsp,%rbp // 创建新栈基
0x0000000000400541 <+4>: mov %edi,-0x4(%rbp) // 创建临时变量a
0x0000000000400544 <+7>: mov %esi,-0x8(%rbp) // 创建临时变量b
0x0000000000400547 <+10>: mov -0x8(%rbp),%eax // 变量a存入寄存器%eax
0x000000000040054a <+13>: mov -0x4(%rbp),%edx // 变量b
存入寄存器%edx
0x000000000040054d <+16>: add %edx,%eax // 做变量加法,将结果存入%eax返回结果寄存器中
0x000000000040054f <+18>: pop %rbp // 恢复上一栈的栈基
0x0000000000400550 <+19>: retq
End of assembler dump.
流程基本类似。
函数返回
函数调用过程对应着调用栈的建立,而函数返回则是进行调用栈的销毁.
0x0000000000400598 <+71>: leaveq // 栈的销毁
0x0000000000400599 <+72>: retq // 保存栈中保存的下一条指令到rip寄存器,控制权返回给调用者
leave
指令等价于以下两条指令:
mov %rbp, %rsp
pop %rbp
这两条指令将%rbp和%rsp寄存器中的值还原为函数调用前的值,是函数开头两条指令的逆向过程。
ret
指令修改了%rip寄存器的值,将其设置为原函数栈帧中将要执行的指令地址。
注:这里的q指的是64位操作数。
堆栈分析
main函数入口
在程序运行前在main函数入口处打上断点,可以通过disas /rm查看源码、汇编码及当前位置:
(gdb) b *main
Breakpoint 1 at 0x400551: file test_gdb.cc, line 7.
(gdb) r
Starting program: /home/C++/test/./a.out
Breakpoint 1, main () at test_gdb.cc:7
7 int main() {
(gdb) disas /rm
Dump of assembler code for function main():
7 int main() {
=> 0x0000000000400551 <+0>: 55 push %rbp
0x0000000000400552 <+1>: 48 89 e5 mov %rsp,%rbp
0x0000000000400555 <+4>: 48 83 ec 10 sub $0x10,%rsp
8 int a = 1;
0x0000000000400559 <+8>: c7 45 fc 01 00 00 00 movl $0x1,-0x4(%rbp)
9 int b = 2;
0x0000000000400560 <+15>: c7 45 f8 02 00 00 00 movl $0x2,-0x8(%rbp)
10
11 int sum = sumUp(a, b);
0x0000000000400567 <+22>: 8b 55 f8 mov -0x8(%rbp),%edx
0x000000000040056a <+25>: 8b 45 fc mov -0x4(%rbp),%eax
0x000000000040056d <+28>: 89 d6 mov %edx,%esi
0x000000000040056f <+30>: 89 c7 mov %eax,%edi
0x0000000000400571 <+32>: e8 c7 ff ff ff callq 0x40053d <sumUp(int, int)>
0x0000000000400576 <+37>: 89 45 f4 mov %eax,-0xc(%rbp)
12 printf("sum of %d + %d = %d\n", a, b, sum);
0x0000000000400579 <+40>: 8b 4d f4 mov -0xc(%rbp),%ecx
0x000000000040057c <+43>: 8b 55 f8 mov -0x8(%rbp),%edx
0x000000000040057f <+46>: 8b 45 fc mov -0x4(%rbp),%eax
0x0000000000400582 <+49>: 89 c6 mov %eax,%esi
0x0000000000400584 <+51>: bf 30 06 40 00 mov $0x400630,%edi
0x0000000000400589 <+56>: b8 00 00 00 00 mov $0x0,%eax
0x000000000040058e <+61>: e8 8d fe ff ff callq 0x400420 <printf@plt>
13
14 return 0;
0x0000000000400593 <+66>: b8 00 00 00 00 mov $0x0,%eax
15 }
0x0000000000400598 <+71>: c9 leaveq
0x0000000000400599 <+72>: c3 retq
End of assembler dump.
打印中箭头=>
所指向的就是当前所处位置,可以看到,执行完run之后,现在程序停留在0x0000000000400567位置。
此时_star函数刚调用main函数,查看两个寄存器的地址:
(gdb) info reg rbp rsp
rbp 0x0 0x0
rsp 0x7fffffffe3e8 0x7fffffffe3e8
此时堆栈内容:
堆栈内容 | 注释 |
---|---|
...... | _start 栈帧 |
0x7fffffffe3e8 _start/当前 rsp |
sumUp函数入口
继续打断点到sumUp函数开头
(gdb) b *sumUp
Breakpoint 2 at 0x40053d: file test_gdb.cc, line 3.
(gdb) c
Continuing.
Breakpoint 2, sumUp (a=0, b=0) at test_gdb.cc:3
3 int sumUp(int a, int b) {
(gdb) disas /rm
Dump of assembler code for function sumUp(int, int):
3 int sumUp(int a, int b) {
=> 0x000000000040053d <+0>: 55 push %rbp
0x000000000040053e <+1>: 48 89 e5 mov %rsp,%rbp
0x0000000000400541 <+4>: 89 7d fc mov %edi,-0x4(%rbp)
0x0000000000400544 <+7>: 89 75 f8 mov %esi,-0x8(%rbp)
4 return a + b;
0x0000000000400547 <+10>: 8b 45 f8 mov -0x8(%rbp),%eax
0x000000000040054a <+13>: 8b 55 fc mov -0x4(%rbp),%edx
0x000000000040054d <+16>: 01 d0 add %edx,%eax
5 }
0x000000000040054f <+18>: 5d pop %rbp
0x0000000000400550 <+19>: c3 retq
End of assembler dump.
继续查看两个寄存器地址:
(gdb) info reg rbp rsp
rbp 0x7fffffffe3e0 0x7fffffffe3e0
rsp 0x7fffffffe3c8 0x7fffffffe3c8
此时rbp是0x7fffffffe3e0, 与刚才看到的rsp差了8个字节,我们看下这8个字节的内容
(gdb) x /1xg 0x7fffffffe3e0
0x7fffffffe3e0: 0x0000000000000000
地址内容是0,其实就是刚才_start的rbp内容,说明 _start的rbp的存储内存不算在main的栈帧中,而且push rbp时rsp的值会自动减8。
接着查看main栈帧的内容,我们可以知道这是main函数产生的两个临时变量a和b,还有函数返回后的下一条指令,但是很奇怪,这里出现的是一个奇怪的0x00007fffffffe4c0,不是我们预期的下一条指令地址0x0000000000400576。
(gdb) x /2xg 0x7fffffffe3d0
0x7fffffffe3d0: 0x00007fffffffe4c0 0x0000000100000002
(gdb) x /2xw 0x7fffffffe3d8
0x7fffffffe3d8: 0x00000002 0x00000001
不要着急,我们沿着堆栈继续往下看:
(gdb) x /3xg 0x7fffffffe3c8
0x7fffffffe3c8: 0x0000000000400576 0x00007fffffffe4c0
0x7fffffffe3d8: 0x0000000100000002
这里我们可以看到,在这条奇怪的数据之后就是我们预期的下一条指令的地址0x0000000000400576,那么中间保存的这个地址代表什么呢?
我们查看此时所有寄存器的值:
(gdb) info reg
rax 0x1 1
rbx 0x0 0
rcx 0x40 64
rdx 0x2 2
rsi 0x2 2
rdi 0x1 1
rbp 0x7fffffffe3e0 0x7fffffffe3e0
rsp 0x7fffffffe3c8 0x7fffffffe3c8
r8 0x7ffff75b6e80 140737343352448
r9 0x0 0
r10 0x7fffffffdf20 140737488346912
r11 0x7ffff7211350 140737339528016
r12 0x400450 4195408
r13 0x7fffffffe4c0 140737488348352
r14 0x0 0
r15 0x0 0
rip 0x40053d 0x40053d <sumUp(int, int)>
eflags 0x202 [ IF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0
可以从这里找到其实此时的这条奇怪的数据就是r13寄存器的值,虽然我们还是不知道为什么要保护这个寄存器。(网上说因为r13属于被调用者保护,所以如果调用者用到了这个寄存器就需要进行备份,但是我没在sumUp的汇编代码中找到使用r13寄存器的汇编指令)。
故此时:
堆栈内容 | 注释 |
---|---|
...... | _start 栈帧 |
_start rbp | |
0x7fffffffe3e0 main/当前 rbp | |
变量a | 4字节 |
变量b | 4字节 |
r13寄存器值 | 8字节 |
下一条指令地址 | 8字节 |
0x7fffffffe3c8 main/当前 rsp |
sumUp函数出口
此时调用si
命令暂停在sumUp函数返回前,查看rbp和rsp
(gdb) si 7
5 }
(gdb) disas /rm
Dump of assembler code for function sumUp(int, int):
3 int sumUp(int a, int b) {
0x000000000040053d <+0>: 55 push %rbp
0x000000000040053e <+1>: 48 89 e5 mov %rsp,%rbp
0x0000000000400541 <+4>: 89 7d fc mov %edi,-0x4(%rbp)
0x0000000000400544 <+7>: 89 75 f8 mov %esi,-0x8(%rbp)
4 return a + b;
0x0000000000400547 <+10>: 8b 45 f8 mov -0x8(%rbp),%eax
0x000000000040054a <+13>: 8b 55 fc mov -0x4(%rbp),%edx
0x000000000040054d <+16>: 01 d0 add %edx,%eax
5 }
=> 0x000000000040054f <+18>: 5d pop %rbp
0x0000000000400550 <+19>: c3 retq
End of assembler dump.
(gdb) info reg rbp rsp
rbp 0x7fffffffe3c0 0x7fffffffe3c0
rsp 0x7fffffffe3c0 0x7fffffffe3c0
此时rbp和rsp都是相同的值,看样子CPU偷懒了,想着马上就要返回了,即使分配了两个函数参数的位置,也不想改rsp的值了。
此时的堆栈情况:
堆栈内容 | 注释 |
---|---|
...... | _start 栈帧 |
_start rbp | |
0x7fffffffe3e0 main rbp | |
变量a | 4字节 |
变量b | 4字节 |
main r13 | 8字节 |
下一条指令地址 | 8字节 |
0x7fffffffe3c8 main rsp | |
main rbp | 8字节 |
0x7fffffffe3c0 sumUp/当前 rbp/rsp | |
sumUp 参数1 | 4字节 |
sumUp 参数2 | 4字节 |
sumUp退出后
继续调用si
命令暂停在sumUp函数返回后,此时的位置:
(gdb) si 2
0x0000000000400576 in main () at test_gdb.cc:11
11 int sum = sumUp(a, b);
(gdb) disas /rm
Dump of assembler code for function main():
7 int main() {
......
10
11 int sum = sumUp(a, b);
0x0000000000400567 <+22>: 8b 55 f8 mov -0x8(%rbp),%edx
0x000000000040056a <+25>: 8b 45 fc mov -0x4(%rbp),%eax
0x000000000040056d <+28>: 89 d6 mov %edx,%esi
0x000000000040056f <+30>: 89 c7 mov %eax,%edi
0x0000000000400571 <+32>: e8 c7 ff ff ff callq 0x40053d <sumUp(int, int)>
=> 0x0000000000400576 <+37>: 89 45 f4 mov %eax,-0xc(%rbp)
12 printf("sum of %d + %d = %d\n", a, b, sum);
......
13
14 return 0;
0x0000000000400593 <+66>: b8 00 00 00 00 mov $0x0,%eax
15 }
0x0000000000400598 <+71>: c9 leaveq
0x0000000000400599 <+72>: c3 retq
此时的寄存器信息:
(gdb) info reg rbp rsp rip
rbp 0x7fffffffe3e0 0x7fffffffe3e0
rsp 0x7fffffffe3d0 0x7fffffffe3d0
rip 0x400576 0x400576 <main()+37>
我们可以看到,rbp和rsp都恢复到了调用sunUp函数之前的状态了。另外rip的位置已经来到了callq指令的后面一条指令位置,也是之前保存的地址0x400576。
此时的堆栈状态:
堆栈内容 | 注释 |
---|---|
...... | _start 栈帧 |
_start rbp | |
0x7fffffffe3e0 main/当前 rbp | |
变量a | 4字节 |
变量b | 4字节 |
main r13 | 8字节 |
0x7fffffffe3d0 main/当前 rsp |
后面main函数返回的流程也基本类似,这里就不继续写了。
小结
通过gdb简单分析了一下函数调用过程中的栈变化流程,为学习gdb开个头,打个基础。后续分析复杂的代码也逃离不出这个大框架。
网友评论