将C++程序源代码与汇编代码清单相关联

5
零售版本的核心转储分析通常需要将特定模块的objdump和源代码进行关联。如果函数非常复杂,通常将汇编转储与源代码进行关联会变得非常麻烦。 今天我尝试创建一个特定模块的汇编程序清单(使用编译选项-S),期望看到源代码与汇编程序的交错或某些关联。不幸的是,清单没有足够友好的关联,所以我在想:
  • 假设我可以从其中确定崩溃位置的核心转储
  • 失败模块的objdump通过重新编译为
  • 选项的模块的汇编程序清单。

是否可能与源代码进行一对一的对应?

例如, 我看到汇编清单如下:

.LBE7923:
        .loc 2 4863 0
        movq    %rdi, %r14
        movl    %esi, %r12d
        movl    696(%rsp), %r15d
        movq    704(%rsp), %rbp
.LBB7924:
        .loc 2 4880 0
        testq   %rdx, %rdx
        je      .L2680
.LVL2123:
        testl   %ecx, %ecx
        jle     .L2680
        movslq  %ecx,%rax
        .loc 2 4882 0
        testl   %r15d, %r15d
        .loc 2 4880 0
        leaq    (%rax,%rax,4), %rax
        leaq    -40(%rdx,%rax,8), %rdx
        movq    %rdx, 64(%rsp)

但是我不知道如何解释类似.LVL2123和指令.loc 2 4863 0这样的标签。

注意: 正如答案所示,通过阅读汇编源代码并根据符号(如函数调用、分支、返回语句)直观地确定模式是我通常做的。我不否认它不起作用,但当一个函数很复杂时,阅读页数的汇编清单是一种痛苦,经常会出现清单不匹配的情况,因为函数被内联或者优化器随意扔掉了代码。我有一种感觉,看到Valgrind如何高效处理优化后的二进制文件,以及在Windows中WinDBG如何处理优化后的二进制文件,我错过了什么。所以我想从编译器输出开始使用它来进行相关性分析。如果我的编译器负责搞乱二进制文件,它将是最好的人来说明如何与源代码相关联,但不幸的是,这对我没有帮助,.loc实际上是非常误导人的。 不幸的是,我经常不得不阅读各种平台上无法重现的转储,并且我在调试Windows Mini-dumps上花费的时间最少,而在调试Linux Coredumps上花费的时间相当长。我认为可能是我做事情不正确,所以我提出了这个问题。


这不是对你问题的回答,但可能仍然有用:http://msdn.microsoft.com/en-us/library/aa238730%28v=vs.60%29.aspx - João Fernandes
1
核心转储应该包含地址信息。因此,请尝试使用addr2line程序将其翻译成源代码位置。当然,这需要具备调试符号的可执行文件(即使您的分发版本已经剥离了符号信息,只需与未剥离版本进行比较也可以运行)。 - edA-qa mort-ora-y
@edA-qamort-ora-y:我会试一下并告诉您我的结果。顺便说一句,这不应该是一个回答而不是一个评论吗? - Abhijit
@Abhijit,我不确定这是否正是您所寻求的。我也没有做过这个确切的场景,所以我不能确定它是否有效。 - edA-qa mort-ora-y
3个回答

4

.loc指令是您要查找的内容。它们表示第4863行、4880行等。源代码和优化汇编代码之间没有完美的映射(这就是为什么您会看到4880多次)。但是,.loc 指令可以让您知道在文件中的位置。语法如下:

.loc <file> <line> <column>

这里还有什么我可以推断的吗?例如,.loc 还有另外两个数字,如 20。这些是什么?还有这些标签 .LVL2123.LBE7923 是什么? - Abhijit
1
这些通常是编译器发出的goto目标。例如,你有je .L2680,那么应该有一行以.L2680:开头的代码。 - Useless

4

能否与源代码进行一对一的对应关系?

A: 不行,除非禁用所有优化。 编译器可能最初会每行发出一些指令(或类似指令的东西),但优化器会重新排序、拆分、融合并完全改变它们。


如果我正在反汇编发布代码,我会查看那些应该与代码有明显逻辑关系的指令。例如:

.LBB7924:
        .loc 2 4880 0
        testq   %rdx, %rdx
        je      .L2680

如果%rdx为零,则它看起来像是一个分支,它来自第4880行。找到该行,确定正在测试的变量,并注意它当前被分配给%rdx

.LVL2123:
        testl   %ecx, %ecx
        jle     .L2680

好的,所以这个测试和分支有相同的目标,接下来的任何操作都知道 %rdx%ecx 都是非零的。原始代码可能结构如下:

if (a && b) {

也许是这样的:
if (!a || !b) {

优化器重新排序了这两个分支...

现在你有了一些结构,希望能够匹配原始代码,也可以弄清寄存器的赋值。例如,如果您知道正在测试的东西是某些结构的数据成员,请向后阅读以查看%rdx从内存中加载时来自哪里:它是否从其他寄存器的固定偏移量加载?如果是这样,那么该寄存器很可能是对象地址。

祝好运!


+1 对于详细的教程。真的很棒。但不幸的是,这和@Chris大多数提到的内容都是我阅读转储的方式。但您会同意,阅读几页转储有时可能会变得非常痛苦,尤其是当函数被内联时。例如,今天我花了将近一个小时阅读了将近1000行汇编代码。我有一种预感,考虑到Valgrind使用-g选项(带符号)编译时如何高效地并几乎可以预测地映射完全优化的二进制文件与源清单,我应该会错过某些东西。 - Abhijit
好的,.loc 标签可以将一组指令映射到一行代码,并且据推测,valgrind 可以使用它来将近似指令成本归因于源代码行。然而,这并不意味着能够找出程序崩溃的原因:你正在试图重构优化器转换和丢弃的 逻辑 程序状态。这更难,而且对于 valgrind 的计算并非必需。 - Useless

1

除非您静态链接到系统库,即使没有调试符号,二进制文件中也会有符号名称 - 链接到系统库函数的符号名称。

这些通常可以帮助缩小代码位置。例如,如果您看到在函数foo()中调用open(),然后调用ioctl(),然后在调用read()之前崩溃,您可能可以很容易地在foo的源代码中找到该点。(说实话,在Linux上,您甚至可能不需要转储 - 您可以使用ltrace或strace获取与库和系统函数相关的崩溃发生记录)

请注意,在某些二进制格式中,可能会通过二进制文件中的其他小包装器间接引用库函数。通常,转储仍将在程序流中调用地址处具有相关的符号名称信息。但是,即使没有,您也可以通过它们在二进制文件中的地址范围识别这些外部链接包装器,并且当您看到其中一个时,可以找到其代码并确定它链接到哪个外部函数。

但正如其他人所提到的,如果您拥有源代码和经常崩溃以便有所帮助的系统,则通常最快的方法是重新构建具有调试符号的版本,或插入日志输出并获取更有用的崩溃记录。


很不幸,我经常需要在各种系统上调试无法重现的转储,并且你提到的方法是我所采用的,它确实有效,但我必须投入大量的精力和时间。我估计调试Windows Mini-dump(通过WinDBG)所需的时间要比调试Linux coredump少得多。更痛苦的是调试Solaris/AIX/HP-UX dumps。看到Valgrind如何高效地将优化的二进制文件与源代码映射,我变得雄心勃勃并充满了乐观主义,希望能找到更有效的方法。@edA-qa mort-ora-y提出了一个建议,我还没有尝试过,等我尝试后再告诉你结果 :-) - Abhijit

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接