gdb如何为C ++重建stacktrace?

时间:2010-12-03 19:28:59

标签: gdb g++ stack-trace

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

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

1 个答案:

答案 0 :(得分:9)

说到Pseudocode,你可以把堆栈称为“一个打包的堆栈帧数组”,其中每个堆栈帧都是一个可变大小的数据结构,你可以表达如下:

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

问题是每个函数都有不同的<N> - 帧大小各不相同。

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

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

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

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

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

http://sourceware.org/cgi-bin/cvsweb.cgi/src/gdb/frame.c?rev=1.287&content-type=text/x-cvsweb-markup&cvsroot=src

GDB采用了各种这样的技术。