gdb如何为C+重建stacktrace+;?

gdb如何为C+重建stacktrace+;?,gdb,g++,stack-trace,Gdb,G++,Stack Trace,我把整个问题分成几个小问题: GDB能够使用哪种不同的算法来重建堆栈跟踪 每种stacktrace重建算法在高层次上是如何工作的?利与弊 每个stacktrace重建算法需要在程序中提供什么样的元信息编译器才能工作 以及相应的启用/禁用特定算法的g++编译器开关 说到伪代码,您可以将堆栈称为“压缩堆栈帧数组”,其中每个堆栈帧都是大小可变的数据结构,您可以这样表示: template struct stackframe<N> { uintptr_t contents[N]; #

我把整个问题分成几个小问题:

  • GDB能够使用哪种不同的算法来重建堆栈跟踪
  • 每种stacktrace重建算法在高层次上是如何工作的?利与弊
  • 每个stacktrace重建算法需要在程序中提供什么样的元信息编译器才能工作
  • 以及相应的启用/禁用特定算法的g++编译器开关

  • 说到伪代码,您可以将堆栈称为“压缩堆栈帧数组”,其中每个堆栈帧都是大小可变的数据结构,您可以这样表示:

    template struct stackframe<N> {
        uintptr_t contents[N];
    #ifndef OMIT_FRAME_POINTER
        struct stackframe<> *nextfp;
    #endif
        void *retaddr;
    };
    
    模板结构stackframe{
    uintptr_t内容[N];
    #ifndef省略\u帧\u指针
    结构stackframe*nextfp;
    #恩迪夫
    无效*重新寻址;
    };
    
    问题是每个函数都有不同的
    ——帧大小不同

    编译器知道帧的大小,如果创建调试信息,通常会将这些信息作为其中的一部分发出。然后,调试器需要做的就是找到最后一个程序计数器,在符号表中查找函数,然后使用该名称在调试信息中查找帧大小。将其添加到stackpointer,就可以进入下一帧的开头

    如果使用此方法,则不需要帧链接,即使使用
    -fomit frame pointer
    ,回溯也可以正常工作。另一方面,如果您有帧链接,那么迭代堆栈只是在一个链接列表之后-因为新stackframe中的每个帧指针都由函数prologue代码初始化以指向上一个帧指针

    如果您既没有帧大小信息也没有帧指针,但仍然有一个符号表,那么您还可以通过一点反向工程来执行回溯,以从实际二进制文件计算帧大小。从程序计数器开始,在符号表中查找它所属的函数,然后从一开始分解该函数。隔离函数开头和实际修改堆栈指针的程序计数器之间的所有操作(将任何内容写入堆栈和/或分配堆栈空间)。这将计算当前函数的帧大小,因此从stackpointer中减去它,您应该(在大多数体系结构上)找到输入函数之前写入堆栈的最后一个字,这通常是调用者的返回地址。根据需要重新迭代

    最后,您可以对堆栈的内容执行启发式分析-隔离堆栈中位于进程地址空间可执行映射段内的所有字(因此可能是函数偏移量或返回地址),并玩一个查找内存的假设游戏,分解那里的指令,看看它是否真的是一个排序的调用指令,如果是的话,它是否真的调用了“next”,以及是否可以从中构造一个不间断的调用序列。即使二进制文件被完全剥离(尽管在这种情况下,您只能得到一个返回地址列表),这种方法也能在一定程度上起作用。我不认为GDB采用这种技术,但一些嵌入式低级调试器采用这种技术。在x86上,由于指令长度的变化,这是非常困难的,因为您无法轻松地通过指令流“后退”,但在RISC上,指令长度是固定的,例如在ARM上,这要简单得多

    有些漏洞使得这些算法的简单或复杂/穷举实现有时会失败,比如尾部递归函数、内联代码等等。gdb源代码可能会为您提供更多想法:


    GDB采用了各种这样的技术

    ,编译器添加调试信息,将代码偏移量映射到源代码行。这篇wikipedia文章没有详细介绍其他算法,只是简单的“调用另一个函数时将RBP存储到堆栈中”。例如,如果在编译时设置-fomit帧指针,该算法将不再工作。堆栈展开描述符呢?那个是如何用来重建stacktrace的?还有其他算法吗?使用
    -fomit帧指针
    GDB将被禁用。没有GDB不使用-fomit帧指针禁用。当您调试的程序崩溃时,在GDB中尝试BT命令-它仍然生成正确的回溯。我想这与unwind描述符有关,它仍然允许重建stacktrace“-fomit frame pointer不将不需要帧指针的函数的帧指针保留在寄存器中。这避免了保存、设置和恢复帧指针的指令;它还可以在许多功能中使用额外的寄存器。它也使得一些机器无法调试。“叶函数变得很复杂。我应该说,这不是特别针对C++的。C++在取消异常堆栈时发挥作用,但这不是调试器所做的事情(如果你在<代码> Spple()中崩溃)你还没有解开异常堆栈,如果你在一个
    捕获{}
    中崩溃,那么你已经这么做了;如果你在解开过程中崩溃,那么你就有了一个编译器错误…调试这些错误就留给真正的神了…。我刚刚找到另一个源代码,表明.eh_框架(特定于C++)可能在GDB中用于倒带堆栈:x86
    call rel32
    总是5字节长,因此很容易检查可能的调用站点。x86 GDB对没有
    的函数进行回溯。eh_frame
    元数据可以扫描堆栈上的DWORD或QWORD,查看指针作为可能的代码地址。我认为GDB可以对有限的范围;因为如果它只执行一次
    推送
    ,它仍然可以通过没有
    .cfi指令的手工编写的asm函数进行回溯,并且推送一个恰好持有另一个指针的伪寄存器会导致额外的函数出现在