美文网首页
gdb函数调用栈简单分析

gdb函数调用栈简单分析

作者: slxixiha | 来源:发表于2021-12-21 22:19 被阅读0次

    最近在学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: 汇编指令

    汇编分析

    建立新的栈帧

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

    • 将调用函数的栈帧栈底地址入栈,即将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指令,完成了两个任务:

    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寄存器和栈帧
    函数调用栈

    相关文章

      网友评论

          本文标题:gdb函数调用栈简单分析

          本文链接:https://www.haomeiwen.com/subject/wsjfqrtx.html