在OkCupid188bet金宝搏官网这里,我们最近花了很多时间试图调试一个应用程序的恐慌,似乎它不可能发生。事实证明,恐慌发生了,但我们认为它是从不同的代码路径触发的,而不是实际触发的,因为调试器在恐慌时似乎对调用堆栈撒谎了!但是,说调试器对我们说谎比说编译器对调试器说谎更不准确。为了弄清楚这是如何发生的,让我们来看看导致这个问题的代码的简化版本:

1 #include  2 #include  3 #include  4 5静态int项目[10];6 7静态内联Int 8 get_nonzero_item(size_t i){9 if(项目[i] == 0){10 printf(“请求的项目为0!\ n”);11 abort();12} 13 14退货[i];15} 16 17静态内联Int18 get_nonzero_three(void){19 return get_nonzero_item(3);20} 21 22静态内联int 23 get_nonzero_five(void){24 return get_nonzere_item(5);25} 26 27 int 28 main(空白){29 int i;30 int项目;31 32如果(getEnv(“填充”)){33 for(i = 0; i 

这段代码中导致bug的关键部分是两个内联代码路径导致了完全相同的打印/中止代码,编译器将把它们组合在一起作为代码优化的一部分。这种优化使得调试器无法确定哪条代码路径导致了失败。让我们通过运行两种可能的恐慌路径来自己尝试一下。我们将在这个示例中使用clang,尽管我已经确认在gcc 5.4.0中也会出现相同的行为。

$ clang -version 3.8.0-2ubuntu4 (tags/RELEASE_380/final) $ clang -std=c99 -O3 -g -o$ FOO=1 ./inline_merge请求的项为0!流产(信息转储)

首次调用我们的测试计划应该在内部恐慌get_nonzero_five因为环境变量Foo,而第二次调用应该在get_nonzero_three.。让我们来看看产生的核心转储是否真的如此。

$ gdb ./inline_merge core_with_foo…(gdb) bt #0 0x00007f1453115428在其余gi_raise (sig=sig@entry=6)在../sysdeps/unix/sysv/linux/raise。c:54 #1 0x00007f145311702a在剩余的gi_abort()中。在inline_merge中的get_nonzero_item (i=3)中的c:89 #2 0x00000000004006ac。get_nonzero_three()在inline_merge。在inline_merge.c:41的main ()

这看起来是正确的——环境变量Foo设定好了,我们试着打过电话get_nonzero_three.并恐慌。其他核心文件怎么样?

$ gdb ./inline_merge core_without_foo…(gdb) bt #0 0x00007f07be73f428在gi_raise (sig=sig@entry=6)在../sysdeps/unix/sysv/linux/raisec:54 #1 0x00007f07be74102a剩余gi_abort()在abort。在inline_merge中的get_nonzero_item (i=3)中的c:89 #2 0x00000000004006ac。get_nonzero_three()在inline_merge。在inline_merge.c:41的main ()

那不可能是对的!我们没有设置Foo程序应该调用get_nonzero_five代替get_nonzero_three.。这是怎么发生的?让我们看一看编译器为上述代码生成的指令。因为所有的东西都被内联了,我们可以只看反汇编主要

$ objdump -d inline_merge | less…主要0000000000400600 < >:400600:55推动% rbp 400601: 53推动% rbx 400602: 50推动%伸展400603:男朋友60 07年40 00 mov $ 0 x400760 % edi 400608: e8 93菲ff ff callq 4004 a0 < getenv@plt > 40060 d: 48 85 c0测试%伸展,%伸展400610:74年39我40064 b <主要+ 0 x4b > 400612: 0 f 28日05年27日01 00 00 movaps 0 x127(%撕裂),% xmm0 # 400740 < _IO_stdin_used + 0 x10 > 400619: 20 0 f 29日05 40 0 00 movaps % xmm0, 0 x200a40 (% rip) # 601060 <物品> 400620:0 f 28日05 29日01 00 00 movaps 0 x129(%撕裂),% xmm0 # 400750 < _IO_stdin_used + 0 x20 > 400627:20 0 f 29日05 42 0 00 movaps % xmm0, 0 x200a42 (% rip) # 601070 + 0 x10 > <项目40062 e: 48 b8 08年00 00 00 09 movabs $ 0 x900000008 %伸展400635:00 00 00 400638:48 89 05 41 0 20 00 mov %伸展,0 x200a41 (% rip) # 601080 + 0 x20的> <项目40063 f: bb 03 00 00 00 mov $ 0 x3, % ebx 400644: bd 05 00 00 00 mov $ 0 x5, % ebp 400649: eb 20人民币40066 b <主要+ 0 x6b > 40064 b: 0 f 57 c0 xorps % xmm0, % xmm0 40064 e: 0 f 29日05年1 b 0 20 00 movaps % xmm0, 0 x200a1b (% rip) # 601070 + 0 <项目x10 > 400655:20 0 f 29日05年04 0 00 movaps % xmm0, 0 x200a04 (% rip) # 601060 < >项目40065 c: 48 c7 05年19 20美元00 movq 0 x0, 0 x200a19 (% rip) # 601080 <物品+ 0 x20 > 400663: 00 00 00 00 400667: 31 ed xor % ebp, % ebp 400669: 31 db xor % ebx, % ebx 40066 b:男朋友07 40 00 mov 65 0 x400765 % edi 400670: e8 2 b菲4004 ff ff callq a0 < getenv@plt > 400675: 48 85 c0测试%伸展,%伸展400678:74年06年我400680主+ 0 x80 > < 40067: 85分贝测试% ebx, % ebx 40067 c: 75年08年jne 400686 x86 > <主要+ 0 40067 e: eb 1 d人民币40069 d <主要+ 0 x9d > 400680:85 ed测试% ebp % ebp 400682: 89 eb mov % ebp, % ebx 400684: 74年17我40069 d <主要+ 0 x9d > 400686:男朋友07 40 00 mov 69 0 x400769 % edi 40068 b: 31 c0 xor % eax, % eax 40068 d: 89 de mov % ebx % esi 40068 f: e8 3 c菲4004 ff ff callq d0 < printf@plt > 400694: 31 c0 xor % eax, % eax 400696: 48 83 c4 08年添加0×8美元,%负责40069:5 b流行% rbx 40069 b: 5 d流行% rbp 40069 c: c3 retq 40069 d:男朋友07 40 00 mov 80 0 x400780 % edi 4006 a2: 4004年e8 19日菲ff ff callq c0 < puts@plt > 4006 a7: e8 04菲4004 ff ff callq b0 < abort@plt > 4006交流:0x0(%rax)

环境变量检查以确定要在指令中进行调用哪个函数400670。然后,如果Foo设置时,执行以下指令:

40067a: 85 db测试%ebx,%ebx 40067c: 75 08 jne 400686  40067e: eb 1d jmp 40069d 

否则,我们执行以下命令:

400680:85 ED Test%EBP,%EB​​P 400682:89 EB POV%EBP,%EB​​X 400684:74 17 JE 40069D 

在该函数中早期,我们分配了值物品[i]% ebx% ebp对应于打电话的可能性get_nonzero_item分别为3和5;检查Foo决定我们最终使用这两个值中的哪一个。然而,这两个片段中最重要的部分是其中一个跳到那个地址40069 d如果结果为零。该地址发生了什么?

40069d: bf 80 07 40 00 mov $0x400780,%edi 4006a2: e8 19 fe ff callq 4004c0  4006a7: e8 04 fe ff callq 4004b0  4006ac: 0f 1f 40 00 nopl 0x0(%rax)

这些指令从而实现了打印和恐慌代码get_nonzero_item在第10-11线上。在内含函数后get_nonzero_three.get_nonzero_five,编译器注意到,在两个内划线的函数之间复制了该代码块并将其组合为优化。让我们看看指示指针主要在恐慌时(它在Coredumps中具有相同的价值):

(gdb) frame 4 #4 main()在inline_merge。get_nonzero_three();(gdb) p/x $rip $1 = 0x4006ac

该程序正在执行此块,该块是在两个内联函数之间共享的块。一旦执行达到这一点,调试器如何知道哪个内联的代码路径在那里得到了?在某些情况下,这可能使用动态分析,但通常情况下,如果不是不可能的话,问题是非常困难的,可以可靠地解决。所以为什么GDB选择告诉我们我们已经执行了get_nonzero_three.在这两种情况下?为什么不选择get_nonzero_five两次吗?让我们来看看编译器生成的调试信息:

$简单--debug = Info Inline_Merge |少... <1> <9b>:abbref number:11(dw_tag_subprogram)<9c> dw_at_low_pc:0x400600  dw_at_high_pc:0xac  dw_at_name :(间接字符串,偏移量:0xb6):main ... <2> :abbrev编号:13(dw_tag_inlined_subroutine) dw_at_abstract_origin:<0x83>  dw_at_ranges:0x0  dw_at_call_file:1  dw_at_call_line:41 ... <2> :abbrev编号:15(dw_tag_inlined_subroutine) dw_at_abstract_origin:<0x8f>  dw_at_low_pc:0x400680  dw_at_high_pc:0x6  dw_at_call_file:1  dw_at_call_line:43

这是调试信息(检出这两个)伟大的资源为矮人的介绍)代表主要函数和函数的内联实例get_nonzero_three.get_nonzero_five,分别。的DW_TAG_inlined_subroutine块提供关于哪些指令对应于每个内联函数的信息,以便调试器知道在回溯中显示哪些指令。让我们检查一下每个内联函数的相关说明。的DW_AT_ranges属性的第一个内联函数块告诉我们内联函数所覆盖的指令范围是在另一个称为DWARF的部分中定义的.debug_ranges.在偏移0x0.让我们来看看这是什么:

$ readelf—debug=Ranges inline_merge | less…Offset Begin 00000000 000000000040067a 0000000000400680 00000000 00000000004006d 00000000004006ac 00000000 <列表结束>…

这意味着内联的功能get_nonzero_three.包含两个指令块:40067A-40068040069D-4006AC.。注意,这些与我们上面看到的两个指令块完全对应:of的检查if(项目[3] == 0)最后的日志/恐慌块。最重要的是,该第二块包括指令指针主要在恐慌时期。现在如何get_nonzero_five吗?它具有DWARF属性dw_at_low_pc.dw_at_high_pc.,这告诉我们其范围的开始和结束说明。这有效地是上面范围表中单行的速记版本。范围get_nonzero_five400680 - 400686。这恰好对应于上面实现的指令块if(项目[5] == 0)。然而,它明显地做到了包括的范围40069D-4006AC.,表示日志/恐慌块!因此,当调试器检查哪些内联函数与指令指针相关时0x4006ac.,它匹配get_nonzero_three.但不是get_nonzero_five,不管实际调用了哪个函数,都会发生这种情况!

如何正确显示它?

在这种情况下,调试器可以在这种情况下进行调试器,给出了调试信息。该信息表明当前指令指针在范围内get_nonzero_three.但不是get_nonzero_five,因此调试器实际上正在根据这些信息做正确的事情。编译器可以提供什么信息来代替调试器向开发人员提供有用的反馈?一旦执行了这种优化,调试器在这种情况下可能提供的最准确的信息是主要可以执行要么内联的功能get_nonzero_three.要么内联的功能get_nonzero_five但它不能肯定地说。另一种比现状更好的选择是完全省略不明确的内联函数调用,只显示根据当前状态确定发生的调用。gdb已经通过具有多个可能执行路径的内联调用链实现了这一点,但是在这种情况下,调试信息使执行路径看起来似乎没有歧义。

编译器可能通过包括块表示此模糊性40069D-4006AC.的调试项中get_nonzero_five同样,这将允许调试器选择上述任何一个显示方法。基于奇妙的阅读矮规范对于内联函数的具体实例,我没有看到任何禁止重叠指令范围的内容。然而,如果这是不允许的,那么我认为最好的替代方案将包括重叠的范围既不是内联实例,这将导致调试器在堆栈跟踪中不显示这两个函数。这种方法也需要标记get_nonzero_item作为直接印章的电话主要对于模糊的代码块;否则,也将从堆栈轨迹中省略。

与可能在堆栈跟踪中包含错误调用相比,完全省略这些调用更可取,因为后者可能会导致开发人员走上错误的调试路径。在我们的例子中,如果调试器告诉我们有两种可能导致恐慌的执行路径,我们就会知道探索这两种可能性,并迅速确定真正的根本原因。相反,我们花了很多时间调试根本原因,而根本没有发生。

开发者对此能做些什么呢?

考虑到编译器的当前行为,有几个要点可以帮助开发人员处理这些情况。首先要知道,这类优化是可能的,堆栈跟踪在这种情况下可能会产生误导。我们花了一段时间来调试我们的问题的部分原因是我们假设我们看到的堆栈跟踪是准确的。我们知道,可变值经常被优化出来,有时在优化的构建中可能不正确,但我们认为堆栈跟踪仍然是可靠的。

第二外带是错误处理应始终提供有关数据分解的信息进入错误。例如,如果此程序中的错误消息已包含导致故障的索引,我们本可以仅根据错误消息调试问题;我们还有一个明确的指示堆栈迹线不正确。通过上面写的代码,错误的堆栈跟踪仍然与我们看到的日志输出一致,因此没有明确的原因怀疑堆栈跟踪。另外,在这种特殊情况下,使错误消息取决于输入数据可能已经阻止了编译器在首先执行导致此错误的优化,因为这两个故障情况会分享更小的代码,但赢得了在所有情况下都是可靠的。此外一般的表达式是:当写入错误处理代码时,我们应该始终旨在使其尽可能简单地区分多个原因或输入到一个问题。

188bet金宝搏官网OkCupid正在招聘点击这里了解更多