【问题标题】:How gdb reconstructs stacktrace for C++?gdb 如何为 C++ 重建堆栈跟踪?
【发布时间】:2011-05-19 22:45:34
【问题描述】:

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

  1. GDB 能够使用哪些不同的算法来重建堆栈跟踪?
  2. 每个堆栈跟踪重建算法在高层次上如何工作?优点和缺点?
  3. 需要在程序中提供什么样的元信息编译器才能使每个堆栈跟踪重建算法工作?
  4. 还有启用/禁用特定算法的相应 g++ 编译器开关?

【问题讨论】:

  • en.wikipedia.org/wiki/Call_stack,编译器将调试信息映射代码偏移添加到源代码行。
  • 那篇维基百科文章除了简单的“在调用另一个函数时将 RBP 存储到堆栈中”之外,没有详细介绍其他算法。例如,如果 -fomit-frame-pointer 在编译时设置,该算法将不再起作用。堆栈展开描述符呢?那是如何用于重建堆栈跟踪的?还有其他算法吗?
  • -fomit-frame-pointer GDB 被排便。
  • 没有 GDB 不使用 -fomit-frame-pointer。当您正在调试的程序崩溃时,在 GDB 中尝试 BT 命令 - 它仍然会生成正确的回溯。我想这与展开描述符有关,它仍然允许重建堆栈跟踪。
  • 来自man gcc: "-fomit-frame-pointer 不要将帧指针保存在不需要的函数的寄存器中。这样可以避免指令保存、设置和恢复帧指针;它还在许多功能中提供了一个额外的寄存器。它还使得在某些机器上无法调试。叶函数被大便了。

标签: gdb g++ stack-trace


【解决方案1】:

使用伪代码,您可以将堆栈称为“打包堆栈帧的数组”,其中每个堆栈帧都是可变大小的数据结构,您可以表示为:

template struct stackframe<N> {
    uintptr_t contents[N];
#ifndef OMIT_FRAME_POINTER
    struct stackframe<> *nextfp;
#endif
    void *retaddr;
};

问题是每个函数都有不同的&lt;N&gt; - 帧大小不同。

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

如果使用此方法,您不需要框架链接,即使您使用-fomit-frame-pointer,回溯也可以正常工作。另一方面,如果你有帧链接,那么迭代堆栈只是跟随一个链表 - 因为新堆栈帧中的每个帧指针都由函数序言代码初始化以指向前一个。

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

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

有些漏洞有时会导致这些算法的简单甚至复杂/详尽的实现失败,例如尾递归函数、内联代码等。 gdb 源代码可能会给你更多的想法:

https://sourceware.org/git/?p=binutils-gdb.git;a=blob;f=gdb/frame.c

GDB 采用了多种此类技术。

【讨论】:

  • 我应该说这些都不是 C++ 特有的。当展开异常堆栈时,C++ 细节会发挥作用,但这不是调试器所做的事情(如果你在 throw() 中崩溃,你还没有解开异常堆栈,如果你在 catch {} 中崩溃,那么你已经这样做了; 如果你在展开的过程中崩溃了,那么你有一个编译器错误......并且调试那些留给真正的上帝......)。
  • 我刚刚发现另一个来源表明 .eh_frame(特定于 C++)可能在 GDB 中用于回退堆栈:sourceware.org/gdb/papers/unwind.html
  • x86 call rel32 总是 5 个字节长,因此很容易检查可能的调用站点。没有.eh_frame 元数据的函数的 x86 GDB 回溯可以扫描堆栈上的 dword 或 qword,将指针视为可能的代码地址。我认为 GDB确实在一定程度上做到了这一点;因为如果它只执行一个push,它仍然可以通过没有.cfi 指令的手写asm 函数进行回溯,并且推入恰好保存另一个指针的虚拟寄存器可能会导致回溯中出现额外的函数。跨度>
  • 源软件链接已损坏
  • 将源软件链接更改为 git(只要答案是旧的,cvsweb 肯定已经被弃用了很长时间......)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-08-06
  • 1970-01-01
  • 1970-01-01
  • 2012-10-04
相关资源
最近更新 更多