【问题标题】:What do the addresses in DMD stack traces mean?DMD 堆栈跟踪中的地址是什么意思?
【发布时间】:2019-06-30 06:01:43
【问题描述】:

我编译文件stacktrace.d: void main(){assert(false);} 关闭了 ASLR,运行时我得到:

core.exception.AssertError@stacktrace.d(2): Assertion failure
----------------
??:? _d_assertp [0x55586ed8]
??:? _Dmain [0x55586e20]

objdump -t stacktrace|grep _Dmain给了

0000000000032e0c w F .text 0000000000000019 _Dmain

如果我运行gdb -q -nx -ex start -ex 'disas /rs _Dmain' -ex q stacktrace:

...
Dump of assembler code for function _Dmain:
   0x0000555555586e0c <+0>: 55  push   %rbp
   0x0000555555586e0d <+1>: 48 8b ec    mov    %rsp,%rbp
=> 0x0000555555586e10 <+4>: be 02 00 00 00  mov    $0x2,%esi
   0x0000555555586e15 <+9>: 48 8d 3d 44 c0 02 00    lea    0x2c044(%rip),%rdi        # 0x5555555b2e60 <_TMP0>
   0x0000555555586e1c <+16>:    e8 47 00 00 00  callq  0x555555586e68 <_d_assertp>
   0x0000555555586e21 <+21>:    31 c0   xor    %eax,%eax
   0x0000555555586e23 <+23>:    5d  pop    %rbp
   0x0000555555586e24 <+24>:    c3  retq   

因此,即使前两个 0x55 字节被截断,堆栈跟踪中给出的 0x...86e20 也不匹配指令的开头。

【问题讨论】:

  • 这可能是你的问题。返回地址被 1 位损坏。硬件内存故障?
  • 我认为运行时代码只是从它保留的值中减去一个以显示异常的源代码行号而不是源代码的下一行(call 指令推送下一条指令的地址,直接读取会给出错误的行号)。但我无法证明这一点,我现在没有在源代码中看到它,所以我不想将它作为答案发布。

标签: assembly d dmd


【解决方案1】:

好的,我刚刚从评论中找到了证明我直觉的部分源代码。

这是添加时的 git blame:https://github.com/dlang/druntime/blame/bc940316b4cd7cf6a76e34b7396de2003867fbef/src/core/runtime.d#L756

唉,提交信息不是超级丰富,但代码本身,加上我的记忆,让我非常确信。

所以这是druntime 库中的文件core/runtime.d。在撰写本文时,它恰好位于第 756 行

enum CALL_INSTRUCTION_SIZE = 1; // it may not be 1 but it is good enough to get
   // in CALL instruction address range for backtrace
callstack[numframes++] = *(stackPtr + 1) - CALL_INSTRUCTION_SIZE;

请注意,当抛出异常时,callstack 变量会复制当前调用。当请求实际将其写出时,跟踪打印机将查看该数组以确定要写入的内容。 (请参阅,查找调试信息以打印文件/行号和函数名称真的很慢,所以它只在必要时才这样做,以保持正常的异常使用 - 当它被抛出并稍后捕获时 - 更快。)

无论如何,我记得当回溯用于打印错误的行时。它将打印包含下一条指令的代码行——这可能与实际的断言/抛出语句有相当大的距离,从而降低了打印的帮助。如果您查看那个 git blame 链接,您会看到用于直接从堆栈中复制地址的旧代码。

call 指令的工作原理是将返回地址压入堆栈,然后跳转到子程序地址。返回地址立即在调用指令之后,因此当CPU 回到那里时,它不会再次运行调用。这就是为什么旧代码会显示错误的行号,错误地将责任归咎于以下指令。

新代码稍微倒回该地址以将其返回到调用指令本身 - 从而将打印的函数放在它所属的行上。但是,在 x86 上,有一些不同的调用指令,我什至不确定是否可能正确地倒带 - 你只能通过查看操作码来确定指令的实际大小,如果您知道指令的大小,或者像 CPU 本身那样以正向顺序读取代码,您只会知道操作码在哪里。此外,在其他处理器架构上,大小会有所不同。

就像那行中的评论所说的那样,我们实际上不必完美无缺。此回溯的目标是让用户查看正确的位置。调试信息使用一种边界框——如果你在这个函数或源代码行的起始地址或之后,但还没有在下一个函数/行的起始地址,它认为你在那里。它不知道也不关心代码的小数行。

因此,它通过假设大小为 1 大大简化了实现 - 足以让它回到那个边界。

我打赌 gdb 在内部做了类似的事情,只是它的打印机隐藏了这一点,直接在其回溯中显示堆栈的返回地址。 (顺便说一句,有趣的提示:在 gdb 中运行程序时将 --DRT-trapExceptions=no 传递给程序的命令行参数。然后它会在程序仍在运行的情况下在抛出点捕获,而不是打印消息并说程序以代码 1 退出!)

druntime 打印代码也可以在打印之前 +1 返回它以隐藏这个内部实现黑客......但是嗯。返回地址也不是实际发生调用的地方,无论如何,您都需要在反汇编程序中查看上面的内容。甚至 gdb 实际上并没有显示调用的地址(至少不是我的旧版本,也许是新版本)。但是,如果它是反汇编中用于 grepping 的值,那可能会很好...如果您想对 druntime 进行 PR,我会支持您(请注意,我在那里没有权限,但可以帮助 cmets)。

但这至少可以明确地解释现状。

【讨论】:

  • 感谢您抽出宝贵的时间,这非常有用!我昨天还提交了一份关于它的错误报告,好消息是看起来已经有 PR 修复了这个问题:issues.dlang.org/show_bug.cgi?id=19653 :)
猜你喜欢
  • 1970-01-01
  • 2015-05-14
  • 2011-01-06
  • 1970-01-01
  • 2011-09-13
  • 2011-11-09
  • 2020-06-26
  • 1970-01-01
相关资源
最近更新 更多