gdb如何重建Ctract stacktrace? [英] How gdb reconstructs stacktrace for C++?

查看:95
本文介绍了gdb如何重建Ctract stacktrace?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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


  1. GDB可以使用什么样的算法来重建堆栈跟踪?

  2. 每个堆栈轨迹重建算法如何在高层次上工作?优点和缺点?

  3. 在程序中需要提供哪种元信息编译器才能使每个栈跟踪重建算法起作用?
  4. 还有相应的g ++编译器开关,用于启用/禁用特定算法?
  5. 一堆打包的堆栈框架,其中每个堆栈框架都是可变大小的数据结构,您可以这样表示:

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

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



    编译器知道帧大小,如果创建调试信息通常会将这些信息作为其中的一部分发送出去。所有调试器然后需要做的是定位最后一个程序计数器,在符号表中查找函数,然后使用该名称查找调试信息中的帧大小。如果使用这种方法,你不需要帧链接,并且回溯工作甚至可以很好地工作。如果您使用 -fomit-frame-pointer 。另一方面,如果你有框架链接,那么迭代堆栈就在链表之后 - 因为新堆栈中的每个框架指针都被功能序言代码初始化为指向前一个。



    如果您既没有帧大小信息也没有帧指向器,但仍然是一个符号表,那么您也可以通过一些逆向工程来执行回溯以计算实际二进制的帧大小。从程序计数器开始,在符号表中查找它所属的函数,然后从头开始分解函数。隔离函数的开头和实际修改堆栈指针的程序计数器之间的所有操作(将任何内容写入堆栈和/或分配堆栈空间)。它计算当前函数的帧大小,因此将其从堆栈指针中减去,并且(在大多数体系结构中)应该在函数输入之前找到写入堆栈的最后一个字 - 这通常是调用者的返回地址。最后,您可以对堆栈内容进行启发式分析 - 隔离堆栈中所有可执行映射段内的单词进程地址空间(因此可以是函数偏移量又称为返回地址),以及播放查看内存的假设游戏,在那里拆分指令并查看它是否实际上是一种排序的调用指令,如果是的话,是否真的调用'下一个',如果你可以从中构建一个不间断的呼叫序列。即使二进制文件被完全删除(尽管在这种情况下你可以得到的是一个返回地址列表),这在某种程度上是有效的。我不认为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采用多种类型技术。

    I have divided the whole question into smaller ones:

    1. What kind of different algorithms GDB is capable to use to reconstruct stacktraces?
    2. How each of the stacktrace reconstruction algorithm works at high level? Advantages and disadvantages?
    3. What kind of meta-information compiler needs to provide in program for each stacktrace reconstruction algorithm to work?
    4. And also corresponding g++ compiler switches that enable/disable particular algorithm?

    解决方案

    Speaking Pseudocode, you could call the stack "an array of packed stack frames", where every stack frame is a data structure of variable size you could express like:

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

    Problem is that every function has a different <N> - frame sizes vary.

    The compiler knows frame sizes, and if creating debugging information will usually emit these as part of that. All the debugger then needs to do is to locate the last program counter, look up the function in the symbol table, then use that name to look up the framesize in the debugging information. Add that to the stackpointer and you get to the beginning of the next frame.

    If using this method you don't require frame linkage, and backtracing will work just fine even if you use -fomit-frame-pointer. On the other hand, if you have frame linkage, then iterating the stack is just following a linked list - because every framepointer in a new stackframe is initialized by the function prologue code to point to the previous one.

    If you have neither frame size information nor framepointers, but still a symbol table, then you can also perform backtracing by a bit of reverse engineering to calculate the framesizes from the actual binary. Start with the program counter, look up the function it belongs to in the symbol table, and then disassemble the function from the start. Isolate all operations between the beginning of the function and the program counter that actually modify the stackpointer (write anything to the stack and/or allocate stackspace). That calculates the frame size for the current function, so subtract that from the stackpointer, and you should (on most architectures) find the last word written to the stack before the function was entered - which is usually the return address into the caller. Re-iterate as necessary.

    Finally, you can perform a heuristic analysis of the contents of the stack - isolate all words in the stack that are within executably-mapped segments of the process address space (and thereby could be function offsets aka return addresses), and play a what-if game looking up the memory, disassembling the instruction there and see if it actually is a call instruction of sort, if so whether that really called the 'next' and if you can construct an uninterrupted call sequence from that. This works to a degree even if the binary is completely stripped (although all you could get in that case is a list of return addresses). I don't think GDB employs this technique, but some embedded lowlevel debuggers do. On x86, due to the varying instruction lengths, this is terribly difficult to do because you can't easily "step back" through an instruction stream, but on RISC, where instruction lengths are fixed, e.g. on ARM, this is much simpler.

    There are some holes that make simple or even complex/exhaustive implementations of these algorithms fall over sometimes, like tail-recursive functions, inlined code, and so on. The gdb sourcecode might give you some more ideas:

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

    GDB employs a variety of such techniques.

    这篇关于gdb如何重建Ctract stacktrace?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆