在这里,在OkC188bet金宝搏官网upid我们最近花了很多时间试图调试好像它不可能发生的应用程序的恐慌。那么,事实证明,恐慌发生的事情,但我们认为它是从比实际的,因为调试看似骗了我们有关在恐慌的时候调用堆栈不同的代码路径触发!然而,他说,调试器骗了我们会比说,编译器骗调试不太准确。为了弄清楚这是怎么发生,让我们来看看导致此问题的代码的简化版本:

1#包括中2的#include 中3的#include 的4 5静态INT项[10];6 7静态内嵌INT 8 get_nonzero_item(为size_t I){9如果(项[I] == 0){10的printf( “请求的项为0 \ N!”);11中止();12} 13 14返回项[I];15} 16 17静态内嵌INT 18 get_nonzero_three(无效){19返回get_nonzero_item(3);20} 21 22静态内嵌INT 23 get_nonzero_five(无效){24返回get_nonzero_item(5);25} 26 27 28 INT主(无效){29 INT I;30 INT项;31 32如果(GETENV( “填充”)){33(I = 0; I'的sizeof(项目)/的sizeof(项[0]); ++ⅰ){34项[I] I =;35} 36}其他{37 memset的(项目,0,的sizeof项目); 38 } 39 40 if (getenv("FOO")) { 41 item = get_nonzero_three(); 42 } else { 43 item = get_nonzero_five(); 44 } 45 46 printf("item = %d\n", item); 47 48 return 0; 49 }

这个代码导致错误的关键部分是两个内嵌代码路径导致完全相同的打印/中止代码,编译器将结合在一起代码优化的一部分。这种优化使得它不可能为调试,以确定哪些代码路径导致了失败。让我们尝试了这一点为自己通过得到两个可能的恐慌路径运行。我们将使用铛在这个例子中,虽然我证实,用gcc 5.4.0发生等效行为。

$铛铛--version版本3.8.0-2ubuntu4(标签/ RELEASE_380 /决赛)$铛-std = C99 -O3 -g -o inline_merge inline_merge.c $ ./inline_merge要求的项目为0!中止(核心转储)$ FOO = 1 ./inline_merge要求的项目为0!中止(核心转储)

我们的测试程序的第一个调用应在有恐慌get_nonzero_five因为环境变量FOO没有设置,并且第二次调用应在有恐慌get_nonzero_three。让我们来看看导致核心转储,看看这是否真的是这样。

$ GDB ./inline_merge core_with_foo ...(GDB)BT在__GI_raise#0 0x00007f1453115428在../sysdeps/unix/sysv/linux/raise.c:54#1 0x00007f145311702a在__GI_abort(SIG = SIG @条目= 6)()在abort.c:11#3 get_nonzero_three()在inline_merge.c:89#2 0x00000000004006ac在get_nonzero_item(ⅰ在inline_merge.c = 3)19#4的main()在inline_merge.c:41

这看起来correct--环境变量FOO定,所以我们打过电话get_nonzero_three和恐慌。如何在其他核心文件?

$ GDB ./inline_merge core_without_foo ...(GDB)BT在__GI_raise#0 0x00007f07be73f428在../sysdeps/unix/sysv/linux/raise.c:54#1 0x00007f07be74102a在__GI_abort(SIG = SIG @条目= 6)()在abort.c:11#3 get_nonzero_three()在inline_merge.c:89#2 0x00000000004006ac在get_nonzero_item(ⅰ在inline_merge.c = 3)19#4的main()在inline_merge.c:41

嗯,这不可能是正确的!我们没有设置FOO所以程序应该叫get_nonzero_five代替get_nonzero_three。这怎么发生的?让我们来看看这上面的代码编译器生成的指令。因为一切都得到了内联,我们只要看看拆装主要

$ objdump的-d inline_merge |少... 0000000000400600 <主>:400600:55推%RBP 400601:53推%RBX 400602:50推%RAX 400603:BF 60 07 40 00 MOV $ 0x400760,%EDI 400608:E8 FE 93 FF FF callq 4004a0  40060d:48 85 C0试验%RAX,%RAX 400610:74 39 JE 40064b <主+ 0x4b> 400612:0F 28 05 27 01 00 00 MOVAPS 0x127(%RIP),%XMM0#400740 <_IO_stdin_used + 0x10的> 400619:0F 29 05 40 0A 20 00 MOVAPS%xmm0,0x200a40(%RIP)#601060 <项> 400620:0F 28 05 29 01 00 00 MOVAPS 0x129(%RIP),%XMM0#400750 <_IO_stdin_used + 0×20> 400627:0F 29 05 42 0A 20 00 MOVAPS%xmm0,0x200a42(%RIP)#601070 <项+ 0×10> 40062e:48 B8 08 00 00 00 09 movabs $ 0x900000008,%RAX 400635:00 00 00 400638 48 89 05 410A 20 00 MOV%RAX,0x200a41(%RIP)#601080 <项+ 0×20> 40063f:BB 03 00 00 00 MOV $ 0x3,%EBX 400644:BD 05 00 00 00 MOV $ 0x5的,为%ebp 400649:EB 20 JMP40066b <主+ 0x6b> 40064b:0F 57 C0 xorps%XMM0,%XMM0 40064e:0F 29 05 1B 20个0A 00 MOVAPS%xmm0,0x200a1b(%RIP)#601070 <项+ 0×10> 400655:0F 29 05 04 0A 2000 MOVAPS%xmm0,0x200a04(%RIP)#601060 <项> 40065c:48 C7 05 19 0A 20 00 MOVQ $ 0x0,0x200a19(%RIP)#601080 <项+ 0×20> 400663:00 00 00 00 400667:31编XOR的%ebp,%EBP400669:31分贝XOR%EBX,%EBX 40066b:BF 65 07 40 00 MOV $ 0x400765,%EDI 400670:E8 2B FE FF FF callq 4004a0  400675:48 85 C0试验%RAX,%RAX 400678:74 06 JE ​​400680 <主+ 0x80的> 40067a:85分贝试验%EBX,%EBX 40067c:75 08 JNE 400686 <主+ 0x86可以> 40067e:EB 1D JMP 40069d <主+ 0x9d> 400680:85版测试的%ebp,%EBP 400682:89 EB MOV的%ebp,%EBX 400684:74 17 JE 40069d <主+ 0x9d> 400686:BF 69 07 40 00 MOV $ 0x400769,%EDI 40068b:31 C0 XOR%eax中,%eax中40068d:89日MOV%EBX,%ESI 40068f:E8 3C FE FF FF callq 4004d0  400694:31 C0 XOR%eax中,%eax中400696:48 83 C 4 08加$ 0x8中,%RSP 40069a:5b中弹出%RBX 40069b:5D pop %rbp 40069c: c3 retq 40069d: bf 80 07 40 00 mov $0x400780,%edi 4006a2: e8 19 fe ff ff callq 4004c0  4006a7: e8 04 fe ff ff callq 4004b0  4006ac: 0f 1f 40 00 nopl 0x0(%rax)

环境变量检查,以确定哪个函数调用指令时发生400670。然后,如果FOO设置,我们执行这些指令:

40067a:85分贝试验%EBX,%EBX 40067c:75 08 JNE 400686 <主+ 0x86可以> 40067e:EB 1D JMP 40069d <主+ 0x9d>

否则,我们执行这些:

400680:85版测试的%ebp,%EBP 400682:89 EB MOV的%ebp,%EBX 400684:74 17 JE 40069d <主+ 0x9d>

在功能早些时候,我们分配值项[I]EBX%%EBP对应于呼叫的可能性get_nonzero_item分别与图3和5,;在检查FOO确定我们最终使用这两个值。然而,这两个片段中最重要的部分是,他们跳到地址40069d如果结果是零。什么是在这个地址怎么回事?

40069d:BF 80 07 40 00 MOV $ 0x400780,%EDI 4006a2:E8 19 FE FF FF callq 4004c0 <放@ PLT> 4006a7:E8 04 FE FF FF callq 4004b0 <中止@ PLT> 4006ac:0F 1F 40 00 nopl为0x0(RAX%)

这些指令实现从印刷和恐慌码get_nonzero_item上线10-11。内联函数后get_nonzero_threeget_nonzero_five时,编译器注意到,这个代码块被两个内联的函数之间复制和组合它们作为优化。让我们来看看在指令指针主要在恐慌(它在两个核心转储相同的值)的时间:

(GDB)框架4#4的main()在inline_merge.c:41 41项= get_nonzero_three();(GDB)P / X $ $翻录1 = 0x4006ac

该方案是执行该块这是两个联函数之间共享。一旦执行已经达到了这一点,怎么能这内联代码路径调试器知道领导呢?在某些情况下,这可能会使用动态分析,但一般这个问题是非常困难的,如果不是不可能的,可靠地解决是不可能的。那么,为什么GDB选择告诉我们,我们已经执行get_nonzero_three在这两种情况下?为什么不选择它get_nonzero_five两次呢?让我们来看看调试信息生成的编译:

$ readelf --debug =信息inline_merge |少... <1> <9B>:缩略号:11(DW_TAG_subprogram)<9C> DW_AT_low_pc:0x400600  DW_AT_high_pc:0xac  DW_AT_name:(间接串,偏移:0xb6):主... <2> :缩略号:13(DW_TAG_inlined_subroutine) DW_AT_abstract_origin:<0×83>  DW_AT_ranges:为0x0  DW_AT_call_file:1  DW_AT_call_line:41 ... <2> :缩略号: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

这是矮人调试信息(看看这两项资源用于表示介绍DWARF)主要功能和功能的内联实例get_nonzero_threeget_nonzero_five, 分别。该DW_TAG_inlined_subroutine块提供有关哪些指令对应于每个联函数,使得调试器知道在回溯显示哪些信息。让我们来看一看每一个内联函数的相关说明。该DW_AT_ranges第一联函数块属性告诉我们,通过该内联函数所覆盖的范围指令在另一个称为DWARF部分中定义.debug_ranges在偏移为0x0让我们来看看那里的东西:

$ readelf --debug =范围inline_merge |少...偏移开始结束00000000 000000000040067a 0000000000400680 00000000 000000000040069d 00000000004006ac 00000000 <列表结尾> ...

这意味着,内联函数get_nonzero_three占地面积指令两大块:40067a-40068040069d-4006ac。请注意,这些完全对应的指令两大块,我们看了上面:的检查如果(项[3] == 0)和在结束对数/恐慌块。最重要的是,该第二块包括在指令指针主要在恐慌的时候。现在怎么样get_nonzero_five?它有矮属性DW_AT_low_pcDW_AT_high_pc,它告诉我们,它的范围的开始和结束的指令。这实际上是在范围表上方的单排的缩写版本。该范围get_nonzero_five400680-400686。这恰好对应于实现上述指令块如果(项[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从直接内嵌调用主要为暧昧代码块;否则会从堆栈跟踪也被省略。

完全省略这些呼叫是优选潜在地包括在堆栈跟踪不正确的呼叫,因为后者可以导致开发沿着错误的调试路径。在我们的例子中,如果调试器已经告诉我们,有可能会导致恐慌两个执行路径,我们会知道去探索这两种可能性,并会很快找出了真正的根本原因。相反,我们花了很多时间调试根源,从来没有在第一时间发生的事情。

可开发商做些什么呢?

由于编译器的当前行为,有几个外卖,可以帮助这些情况开发商。首先是简单地意识到,这些种类的优化是可能的,堆栈跟踪可在这种情况下会产生误导。为什么我们花了一段时间来调试我们的问题的部分原因是,我们假设堆栈跟踪我们看到的是准确的。我们知道,变量值经常优化掉了,有时是不正确的优化版本,但我们认为堆栈跟踪应该仍然是可靠的。

第二个外卖是错误处理应该始终提供有关数据分解到错误的信息。例如,如果在这个程序中的错误信息已经包含导致错误的指标,我们可以调试仅基于错误信息的问题;我们也将有一个明确的迹象表明堆栈跟踪是不正确的。通过上面写的代码,故障堆栈跟踪仍然和日志的输出一致,我们所看到的,所以没有一个明确的理由怀疑堆栈跟踪。此外,在这种特殊情况下,使得该错误消息取决于可能阻止编译器执行导致摆在首位这个bug优化输入数据,因为这两个失败的案例将会共享更少的代码,但韩元”吨可靠地在所有情况下的情况。这个外卖的更一般的表达式为:编写错误处理代码的时候,我们应该始终致力于使尽可能容易的多种原因,或投入,一个问题区别开来。

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