coredumpctl
创建 coredump 后,该实用程序会显示程序的堆栈跟踪。
例如,在 Firefox 核心转储上:
Stack trace of thread 14469:
#0 0x00007f0ac652d3bd pthread_cond_wait@@GLIBC_2.3.2 (libpthread.so.0)
#1 0x0000560f2ab95488 _ZN7mozilla6detail21ConditionVariableImpl4waitERNS0_9MutexImplE (firefox)
#2 0x0000560f2ab95646 _ZN7mozilla6detail21ConditionVariableImpl8wait_forERNS0_9MutexImplERKNS_16BaseTimeDurationINS_27TimeDurati>
#3 0x00007f0aba9799f9 n/a (libxul.so)
#4 0x00007f0aba96eb9a n/a (libxul.so)
#5 0x00007f0ac652708c start_thread (libpthread.so.0)
#6 0x00007f0ac5abce7f __clone (libc.so.6)
考虑到这是 C 代码,因此它是编译的,符号并没有直接嵌入到二进制文件中:那么这怎么可能呢?
而且,readelf 在实践中是如何做到的?
(我的猜测,这与嵌入在 ELF 文件中的符号表有关)
正如您正确猜测的那样,符号来自嵌入在 ELF 文件中的符号信息。即使不存在完整的符号表,也需要一些符号信息才能进行动态链接。
就实际的堆栈跟踪而言,当调用函数时,会保存 cpu 返回的位置。对于像 x86 这样的 cpu,它被压入堆栈。对于 RISC 机器,它通常被放入寄存器。如果函数想要调用任何其他函数(即它不是
leaf
函数)然后这个寄存器被压入堆栈。堆栈跟踪代码在堆栈上找到这些地址,在其前面的符号中查找最近的地址并报告它。一些堆栈跟踪代码会同时打印符号名称和距离,这可以让您对它的准确性更有信心。例如,如果符号在返回地址之前只有 40 个字节,那么与之前的 40,000 个字节相比,人们对它在该代码中的信心要大得多。在后一种情况下,人们可能会怀疑返回地址指向不同的函数,但该函数在符号表中没有条目。很多事情都会使这不准确。如果编译器将函数 a 内联到函数 b 中,那么您可能在函数 a 中,但堆栈跟踪会报告您在 b 中。
如果编译器执行“尾调用优化”,其中函数 a 以类似的结尾
return b();
并且函数 c 调用函数 a,您可能希望跟踪显示 c->a->b,但您只会看到 c->b。如果您查看 c 的源代码并且您发现它从不直接调用 b,这可能会令人困惑。