gdb函数调用栈简单分析

2021-12-21  本文已影响0人  slxixiha

最近在学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.

以上输出每行指示一条汇编指令,除程序源码外共有三列,各列含义为:

  1. 0x0000000000400692: 该指令对应的虚拟内存地址
  2. <+0>: 该指令的虚拟内存地址偏移量
  3. push %rbp: 汇编指令

汇编分析

建立新的栈帧

一个函数被调用,首先默认要完成以下动作:

以下两条指令即完成上面动作:

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指令,完成了两个任务:

  1. 将调用函数(main)中的下一条指令(这里为0x400576)入栈,被调函数返回后将取这条指令继续执行,64位rsp寄存器的值减8;
  2. 修改指令指针寄存器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开个头,打个基础。后续分析复杂的代码也逃离不出这个大框架。

参考文章

函数调用堆栈
X86-64寄存器和栈帧
函数调用栈

上一篇下一篇

猜你喜欢

热点阅读