我继承了一些聪明的 GNU/Linux x64 机器代码,它围绕 c 函数调用创建了机器代码包装器。我猜想,用高级语言术语来说,代码可能被称为装饰器或闭包。该代码运行良好,但不幸的是,当调用包装器时,它会吞噬 gdb 中的堆栈跟踪。
\n\n根据我从网上了解到的信息,gdb 使用https://en.wikipedia.org/wiki/DWARF作为分离堆栈中堆栈帧的指南。这对于静态代码来说效果很好,但显然运行时生成和调用的代码没有在 DWARF 框架中注册。
\n\n我的问题是,在这种情况下是否有任何方法可以挽救堆栈跟踪?
\n\n这是一些显示问题的类似 C 代码。
\n\ntypedef int (*ftype)(int x);\nint wuz(int x) { return x + 7; }\nint wbar(int x) { return wuz(x)+5; }\nint main(int argc, char **argv)\n{\n const unsigned char wbarcode[] = {\n 0x55 , // push %rbp\n 0x48,0x89,0xe5 , // mov %rsp,%rbp\n 0x48,0x83,0xec,0x08 , // sub $0x8,%rsp\n 0x89,0x7d,0xfc , // mov %edi,-0x4(%rbp)\n 0x8b,0x45,0xfc , // mov -0x4(%rbp),%eax\n 0x89,0xc7 , // mov %eax,%edi\n 0x48,0xc7,0xc0,0xf6,0x04,0x40,00, // mov $0x4004f6,%rax\n 0xff,0xd0, // callq *%rax\n 0x83,0xc0,0x05 , // add $0x5,%eax\n 0xc9 , // leaveq\n 0xc3 // retq\n };\n\n int wb = wbar(5);\n ftype wf = (ftype)wbarcode;\n int fwb = wf(5);\n}\nRun Code Online (Sandbox Code Playgroud)\n\n编译它:
\n\ngcc -g -o mcode mcode.c\nexecstack -s mcode\nRun Code Online (Sandbox Code Playgroud)\n\n并通过以下方式在 gdb 中运行它:
\n\n$ gdb mcode\n(gdb) break wuz\nRun Code Online (Sandbox Code Playgroud)\n\n如果我们反汇编 wbar,我们会得到与 中的字节序列非常相似的东西wbarcode[]。唯一的区别是我更改了调用的调用约定wuz()。
(gdb) disas/r wbar\nDump of assembler code for function wbar:\n 0x0000000000400505 <+0>: 55 push %rbp\n 0x0000000000400506 <+1>: 48 89 e5 mov %rsp,%rbp\n 0x0000000000400509 <+4>: 48 83 ec 08 sub $0x8,%rsp\n 0x000000000040050d <+8>: 89 7d fc mov %edi,-0x4(%rbp)\n 0x0000000000400510 <+11>: 8b 45 fc mov -0x4(%rbp),%eax\n 0x0000000000400513 <+14>: 89 c7 mov %eax,%edi\n 0x0000000000400515 <+16>: e8 dc ff ff ff callq 0x4004f6 <wuz>\n 0x000000000040051a <+21>: 83 c0 05 add $0x5,%eax\n 0x000000000040051d <+24>: c9 leaveq\n 0x000000000040051e <+25>: c3 retq\nEnd of assembler dump.\nRun Code Online (Sandbox Code Playgroud)\n\n如果我们现在运行该程序,它将在 wuz() 中停止两次。第一次\n通过我们的 c 调用,我们可以通过 bt 请求堆栈跟踪。
\n\nBreakpoint 3, wuz (x=5) at mcode.c:2\n=> 0x00000000004004fd <wuz+7>: 8b 45 fc mov -0x4(%rbp),%eax\n 0x0000000000400500 <wuz+10>: 83 c0 07 add $0x7,%eax\n 0x0000000000400503 <wuz+13>: 5d pop %rbp\n 0x0000000000400504 <wuz+14>: c3 retq\n(gdb) bt\n#0 wuz (x=5) at mcode.c:2\n#1 0x000000000040051a in wbar (x=5) at mcode.c:3\n#2 0x00000000004005b0 in main (argc=1, argv=0x7fffffffe528) at mcode.c:20\nRun Code Online (Sandbox Code Playgroud)\n\n这是一个正常的堆栈跟踪,显示我们从main()\xe2\x86\x92 wbar()\xe2\x86\x92获得wuz()。
但如果我们现在继续,我们会到达wuz()第二次,并且我们再次请求堆栈跟踪:
(gdb) c\nContinuing.\n\nBreakpoint 3, wuz (x=5) at mcode.c:2\n=> 0x00000000004004fd <wuz+7>: 8b 45 fc mov -0x4(%rbp),%eax\n 0x0000000000400500 <wuz+10>: 83 c0 07 add $0x7,%eax\n 0x0000000000400503 <wuz+13>: 5d pop %rbp\n 0x0000000000400504 <wuz+14>: c3 retq\n(gdb) bt\n#0 wuz (x=5) at mcode.c:2\n#1 0x00007fffffffe419 in ?? ()\n#2 0x0000000500000001 in ?? ()\n#3 0x00007fffffffe440 in ?? ()\n#4 0x00000000004005c6 in main (argc=0, argv=0xffffffff) at mcode.c:22\nBacktrace stopped: frame did not save the PC\nRun Code Online (Sandbox Code Playgroud)\n\n即使我们执行了相同的两个分层调用,我们仍会得到包含错误帧的堆栈跟踪。在我原来继承的包装器代码中,情况更糟,因为堆栈跟踪在 5 帧后结束,顶层的地址为 0。
\n\n所以问题又来了,是否有任何额外的代码可以添加到 \n 中wbarcode[],从而导致 gdb 输出有效的堆栈跟踪?或者是否有任何其他运行时技术可用于使 gdb 识别堆栈帧?
在某些体系结构上,您可以使框架具有该端口的 gdb 默认展开程序所期望的布局。然而,这并不适用于所有架构。读取x86-64端口(参见gdb/amd64-tdep.c,特别是函数amd64_frame_cache_1),我认为这里gdb想要知道函数边界,因此它可以尝试分析序言。但是,函数边界来自 (ELF) 符号表,所以你运气不好。
不过还是有办法的。由于最近(以 gdb 术语来说)JIT 编译器的兴起,gdb 提供了另外三种方法来处理这个问题。
一种方法是你的程序可以在内存中发出一个特殊的 ELF 对象(实际上是 gdb 理解的任何对象格式,IIRC),并调用运行时挂钩来通知 gdb 它的存在。gdb 将读取该对象,包括它包含的任何调试信息。这种方法相当繁重,但可以访问 gdb 的大部分功能——您不仅可以指定展开,还可以指定类型、局部变量等。
第二种方式有点类似。你的程序仍然调用一个特殊的钩子。但是,您还提供了一个由 gdb 加载的插件。该插件可以从下级读取符号和其他信息,但在这种情况下,符号和展开信息不必采用任何特定格式。
最后一种方法(gdb 7.10 中的新方法)是您可以用 Python 编写展开程序。在开发JIT unwinder时,我选择了这种方法,因为它调试简单、部署简单、相当灵活,并且不需要对下层进行任何特定的更改。
这些方法都记录在gdb手册中。但在某些情况下,我认为文档还有一些不足之处。您可能需要找到一些示例代码或深入研究 gdb 源代码才能真正理解它应该如何工作。