如何将 gdb stacktrace 与运行时生成的机器代码一起使用?

Dov*_*eld 5 c gdb x86-64

我继承了一些聪明的 GNU/Linux x64 机器代码,它围绕 c 函数调用创建了机器代码包装器。我猜想,用高级语言术语来说,代码可能被称为装饰器或闭包。该代码运行良好,但不幸的是,当调用包装器时,它会吞噬 gdb 中的堆栈跟踪。

\n\n

根据我从网上了解到的信息,gdb 使用https://en.wikipedia.org/wiki/DWARF作为分离堆栈中堆栈帧的指南。这对于静态代码来说效果很好,但显然运行时生成和调用的代码没有在 DWARF 框架中注册。

\n\n

我的问题是,在这种情况下是否有任何方法可以挽救堆栈跟踪?

\n\n

这是一些显示问题的类似 C 代码。

\n\n
typedef 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}\n
Run Code Online (Sandbox Code Playgroud)\n\n

编译它:

\n\n
gcc -g -o mcode mcode.c\nexecstack -s mcode\n
Run Code Online (Sandbox Code Playgroud)\n\n

并通过以下方式在 gdb 中运行它:

\n\n
$ gdb mcode\n(gdb) break wuz\n
Run Code Online (Sandbox Code Playgroud)\n\n

如果我们反汇编 wbar,我们会得到与 中的字节序列非常相似的东西wbarcode[]。唯一的区别是我更改了调用的调用约定wuz()

\n\n
(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.\n
Run Code Online (Sandbox Code Playgroud)\n\n

如果我们现在运行该程序,它将在 wuz() 中停止两次。第一次\n通过我们的 c 调用,我们可以通过 bt 请求堆栈跟踪。

\n\n
Breakpoint 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\n
Run Code Online (Sandbox Code Playgroud)\n\n

这是一个正常的堆栈跟踪,显示我们从main()\xe2\x86\x92 wbar()\xe2\x86\x92获得wuz()

\n\n

但如果我们现在继续,我们会到达wuz()第二次,并且我们再次请求堆栈跟踪:

\n\n
(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\n
Run Code Online (Sandbox Code Playgroud)\n\n

即使我们执行了相同的两个分层调用,我们仍会得到包含错误帧的堆栈跟踪。在我原来继承的包装器代码中,情况更糟,因为堆栈跟踪在 5 帧后结束,顶层的地址为 0。

\n\n

所以问题又来了,是否有任何额外的代码可以添加到 \n 中wbarcode[],从而导致 gdb 输出有效的堆栈跟踪?或者是否有任何其他运行时技术可用于使 gdb 识别堆栈帧?

\n

Tom*_*mey 3

在某些体系结构上,您可以使框架具有该端口的 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 源代码才能真正理解它应该如何工作。