如何在优化后的C程序中找到bug的最佳实践

5
我的程序使用了一个第三方库,在某些时候会抛出分段错误。我尝试使用带有调试符号且没有编译器优化的库进行编译,这样崩溃就消失了。我的猜测是编译器优化揭示了这个 bug。在这种情况下,如何进行调试是最佳实践?
编辑 -(更正上述陈述:“揭示”而不是“导致”)
我想我被误解了。我没有意图责怪编译器或类似的东西。我只是询问在这种情况下找到 bug 的最佳实践,即在第三方库中没有调试符号的情况下进行调试(崩溃回溯指向第三方库)。

9
通常情况下,错误的假设是——问题往往只有在启用优化时才会显现出来,但实际上这个问题很可能是你代码中潜在的错误。相比于用户代码中的错误,编译器错误相对较少见。请注意,此翻译力求简洁明了,不改变原意。 - Paul R
2
错误不在第三方库中。如果我编写一个程序并在执行printf时崩溃,我的第一反应不会是认为printf存在错误。 - pmg
如果你正在使用C或Objective-C,那么使用LLVM/Clang的静态分析器来发现未定义行为(如使用未初始化的变量等)是值得的。 - DarkDust
1
@Paul Spektom说这个bug是在一个C程序中,并且由编译器优化引起的...我想是这样。 - Ben
3
您已经为问题打了[gdb]标签。您只是进行了调试构建并且崩溃没有发生,还是在使用gdb运行时崩溃已经消失了?这是一个重要的区别。如果崩溃在使用gdb运行时消失了,那么很有可能是一个未初始化的指针。调试器会将所有指针都初始化为零,这会导致这个 bug "神奇地消失",因为它被捕获在其中一个常见的 if(ptr != 0) 子句中。 - Damon
显示剩余3条评论
7个回答

8

您怀疑优化导致了一个错误。我的怀疑是您的代码存在构造不当,导致未定义的行为,在优化器开启时,这些未定义的行为会表现为错误的行为或崩溃。不要责怪优化器。找到代码中的未定义行为可能有点棘手。可能的罪魁祸首:

  • 越界索引
  • 返回临时地址
  • 其他无数问题

2
当新手程序员用三个printf语句时,却认为自己发现了一个编译器中的错误,这总是让我感到困惑。毕竟这个编译器已经不断改进了15年之久。 - Blindy
@spektom:最佳实践是一开始编写具有定义行为的安全代码。很抱歉,我无法为您提供查找代码错误的算法。 - Armen Tsirunyan
最佳实践不是算法。 最佳实践可以包括:工具,编译设置等。 即使建议“放置printf”语句也是某种最佳实践。 - Michael Spector
@spektom:查找所有数组索引,确保它们是安全的。查看所有返回指针和引用的函数。确保它们是安全的。目前想不到其他的了。 - Armen Tsirunyan

8
您所描述的情况非常普遍。这几乎从来不是编译器优化中的错误。优化会对您的代码进行许多操作,例如重新排序/优化变量等。如果您有一个缓冲区溢出,它可能只会在调试版本中溢出内存,但该内存在优化版本中非常重要。
使用valgrind来跟踪内存错误-它们几乎总是导致您看到的症状的原因。

+1 for valgrind。在手动调试之前使用它。此外,如果代码出现段错误,从GDB中执行应用程序应该会给出引用,指出段错误发生的位置。即使没有调试符号,您也可以从函数+汇编偏移量推断出位置。 - gravitron

4
使用带有调试符号和编译器优化编译,它也会“希望”失败。允许系统生成一个核心文件( ulimit-c无限制,然后重新运行程序)。将核心文件加载到gdb中以查看发生了什么。
另一个强大的工具是valgrind,在选项--db-attatch=yes下运行程序,当检测到无效读取或写入时,它将停止并运行调试器。无效读取/写入很可能会引起段错误,即使它们没有,它们也应该被删除。
祝你好运!

是的 - 调试符号和优化并不互斥(尽管调试优化构建仍然可能具有挑战性)。调试符号的存在或缺失不会改变代码生成。 - caf

2
保持在你认为代码崩溃的位置放置调试语句或消息框。只要代码没有改变太多,崩溃将发生在两个消息框之间,这将帮助您定位有问题的代码。
还可以注释掉代码块,直到崩溃不再出现。继续注释回来,直到崩溃重新出现。您最后注释回来的代码必须直接或间接地导致崩溃。
这些方法都对一般调试很有用,如果能够可靠地重现崩溃,则已经完成了一半的工作。
我没有给出关于调试编译器优化的具体建议,因为崩溃很可能不是由此引起的。通常会进行非常严格的测试以确保优化不会以任何方式改变代码的功能或语义。

2
如果回溯引导到第三方库,请使用在调用库之前中断。请验证您传递给库的参数是否有效(即,不是未初始化指针,不是指向已释放内存的指针,不超出范围等)。
您可以使用跟踪函数调用,然后尝试确定第三方库中的执行路径吗?在失败的库调用之前使用或其他系统调用,以便在strace输出中有一个起点。
如果您真的认为这是第三方库中的错误,则必须使用优化编译它,以便您可以重现故障。您是否表示您的编译器只能为非优化构建包含调试符号?仍应适用于优化构建。

0

嗯,浏览已编译的二进制文件是没用的。

所以只能查找代码中哪个部分导致了段错误。我建议你手动检查代码并开始注释掉一些东西。一旦找到导致错误的原因,就可以确定如何解决它了。值得尝试的是在某些位置添加 printf 以确切地知道程序失败的位置。

想象一下这是在寻找错误的二分搜索;)


在我的工作中,我们在面试中会问类似的问题,而你的答案是常见的,但被认为是一个警告信号。这并不是说你的解决方案最终不能起作用,而是有更快的方法来查找segfault(如valgrind和在调试器中执行)。 - gravitron
这确实取决于你的程序的大小和复杂性,以及你在手动调试方面的能力。对我来说,这就像使用算盘和计算器之间的区别。计算器用户通常可以很快得到正确答案,但在大多数情况下会被有经验的算盘使用者击败。:P - tskuzzy
你用gdb调试过段错误吗? gdb myprog; run; <它会产生段错误>; bt <-- 它会打印出错误所在的行。没有什么注释/printf的解决方案比这更快了。 - gravitron

0

如果只有在开启优化时出现问题,那么这就是你在某个地方调用未定义行为的强烈提示。不幸的是,这种 UB 可能与实际生成 segfault 的代码毫不相关(正如我以前发现的几次那样)。

每次这种情况发生在我身上时(这种情况并不经常发生),原因都是代码中其他地方存在缓冲区溢出。但是,我从来没有开发出一种可重复、普遍适用的技术来找到这个问题(除非你想称呼花费几个小时通过调试器和诅咒来实现的方法是普遍适用的技术)。


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