C/C++ 中的调试原则/核心主题 [关闭]

Posted

技术标签:

【中文标题】C/C++ 中的调试原则/核心主题 [关闭]【英文标题】:Debugging principles/core topics in C/C++ [closed] 【发布时间】:2012-09-01 16:59:50 【问题描述】:

我有兴趣投入大量时间来提高我的调试能力,并且正在寻找我需要涵盖的核心主题列表,以便精通常用和高级调试/测试技术的原理。

最初,我想我会通读 gdb 文档并从其功能中收集调试技术;然而,除了跳入其中获取段错误的行号并可能运行bt,几个月后我仍然采用大量printf作为我的默认策略。我觉得这是因为我没有任何明确的策略可以通过更复杂的方式来实现。

尽管我的问题与 C/C++ 相关,而且虽然我在 UNIX 环境中操作,但如果它们能提高我对关键概念的理解,我愿意查看通用材料,甚至其他语言涵盖的主题。

【问题讨论】:

我同意H2CO3,他们会给你一个程序流程的概念。但我也很好奇这个问题的任何答案。 ***.com/questions/1325853/… @pauluss86 我不同意,如果您还不知道程序流程,那么您不应该尝试修改它。首先你明白,然后才应该采取行动。我更喜欢好的 ol' 断点 -> 降级 -> 同时观看本地方法。 @Christian:你就在那里,但我的意思是用它作为支持来检查预期的程序流是否确实正在执行,而不是改变它。 Afaic 添加打印语句不会改变流程。另外我在这里交换printlog 【参考方案1】:

您应该考虑多种直接策略:

大量 printfs 迫切需要 日志记录 解决方案。您在这里有很多选择,但广泛记录并不是一个特别糟糕的策略,事实上它对于任何形式的客户端调试都至关重要。 广泛使用断言(永远不要禁用它们,即使在“发布”代码中也是如此)。始终为所有潜在错误编写检查并尽快失败(在 C++ 中使用异常——始终抛出,从不捕获)。 学习在 emacs 中掌握 gdb 很有用。学习如何单步执行程序、如何设置断点以及如何检查局部变量通常就足够了。 单元测试也是需要考虑的事情。特别是因为小型测试更容易调试,因为它们不会被功能齐全的程序的噪音所包围。在代码之前编写测试,或者更好的是让其他人编写测试。

更一般地说,以下几点虽然与调试没有直接关系,但会让您受益:

了解程序是如何执行的(例如了解堆栈帧和汇编的小介绍)在某些错误正在破坏内存的情况下可能很有用。更一般地说,永远不要停止学习有关您环境的知识。 在 C++ 中,使用良好实践:RAII、标准库、尽快失败等。这具有减少调试工作量的强烈趋势,尤其是。因为调试器可以很好地打印标准库中的东西。此外,尽可能简单地编写代码对调试时间有积极影响。 使用(分布式)版本控制。总是。一旦你习惯了它,你就会看到它的好处(例如,结合单元测试,你可以使用全能的git bisect)。

【讨论】:

人们真的使用git bisect吗? @arxanas:在有很多人/提交的项目中,是的(我的意思是在 linux 内核错误发现之外)。这甚至可以在某种程度上实现自动化。【参考方案2】:

如果你想成为一个更高级的 C/C++ 调试器,学习一些汇编(你不需要成为专家,只要一些基础知识就可以了),学习机器寄存器,学习你平台的 ABI(应用程序二进制接口,特别是函数参数和堆栈如何工作),这样您就不必到处依赖printf 来查看您的程序在做什么。学习如何检查内存也是一个好主意,并且知道您在内存地址中寻找什么,一旦您在汇编方面做得不错并了解机器指令如何与寄存器集交互,您将很快知道在哪里查找指针地址或内存位置以设置为观察点或查找为块并查看给定内存位置中的数据结构发生了什么。

例如,如果某些事情发生在几级以下,则调试递归算法可能会很困难......取决于多少级,您最终可能会输出大量需要永远筛选的数据,或者您可能会发现自己永远停在断点上。但是,如果您了解堆栈如何与递归算法一起工作,则可以设置条件断点来监视堆栈指针寄存器以及堆栈上的其他内存地址,以在递归算法中的错误点正确停止程序发生,而不必筛选无意义的数据。所以你运行你的程序直到它停止,检查回溯,查看堆栈上的一些变量以及堆栈指针寄存器,然后设置一个条件断点,让你在正确的递归点停止在算法中。

顺便说一句,请注意,不要使用printf ...它在stdout 上缓冲,这意味着当您在输出中看到错误时,您的错误可能已经传播到其他东西,例如一个奇怪的内存损坏错误等。即使您通过放置结束行字符等来刷新stdout 的缓冲区,您仍然会最终将错误消息与程序的正常输出混合。代替printf 使用fprintf,并输出到stderr。这不会被缓冲,它会立即打印到输出,如果您希望保存程序的输出,错误消息不会与程序的输出混合。

【讨论】:

【参考方案3】:

我想多介绍一下使用断言的技术。

断言让您可以在代码中的任何位置指定您期望为真的内容。在函数的开头,您可以使用断言来确保参数具有合理的值。这些被称为先决条件。而函数结束时,你可以检查以确保你即将返回的内容与函数的目的一致。这些被称为后置条件。在函数的中间,您可以确保任何中间计算都是合理的(尽管您通常应该尽量使您的函数足够小,以便没有很多中间计算)。

使用类,您可以检查以确保将正确的值传递给构造函数。在其他方法中,您可以在方法返回之前确保类的一般状态是合理的。这些称为不变量。

我在调试时,通常会发现 bug 很难找到,因为我错过了一些断言,让崩溃离问题的根源越来越远。我使用调试过程来帮助解决这个问题。我从崩溃实际发生的地方开始,然后思考“在这个抽象级别,出了什么问题?”。如果它在函数中间崩溃,我可能会意识到传递给函数的参数不正确,因此我在函数开头附近添加了额外的断言来捕获这些断言。当我下次运行它并且它崩溃时,崩溃发生得更快。如果崩溃现在发生在函数的顶部,我会上一层堆栈并询问“为什么调用者没有传递正确的值?”。然后我可能会意识到一些中间计算是错误的,所以我在那里添加了一个断言来更早地捕获它。中间计算可能是由于另一个函数返回了错误的值,在这种情况下,我将添加一个后置条件断言来更早地捕获它。可能是因为当前函数没有传递正确的参数,所以我给这个函数添加了一个前置条件断言。

每次我添加一个断言,我都会让崩溃发生在更接近问题的真正根源的地方。最终,我到达了崩溃发生在真正的逻辑错误处的地步,并且修复是显而易见的。但是通过这个过程,我也让未来的问题更容易被发现。

您可以在进行单元测试时应用类似的推理。问“我的测试有什么问题导致这个问题没有被更早发现?”

【讨论】:

【参考方案4】:

求助于printf 没有错。事实上,它有很多优点:

已被注释掉的printf 语句清楚地表明过去调试尝试表明该代码区域可能有问题.

printf 陈述易于理解,如果它们冗长且精心设计。即使是初学者也能理解它们的含义以及如何使用它们。

printf 声明实际上迫使您思考问题以及导致它的原因。您必须尝试遵循逻辑控制流和数据流来了解问题。这会加强您对整个系统的理解。

事实上,我认为那些被教导依赖工具来调试他们的代码的人倾向于在真正尝试找出错误行为的原因之前先查阅他们的工具他们的代码!当然,在许多情况下,调试器是首选,但很明显,首先思考而不是盲目转向调试器的人是天生就能更好地理解问题,更好地理解错误状态的人。程序并最终更好地了解问题的原因

Let me quote Rob Pike here:

“当出现问题时,我会本能地开始挖掘问题,检查堆栈跟踪,坚持打印语句,调用调试器等等。但肯只是站着思考,无视我和我们刚写的代码。过了一会儿,我注意到一个模式:肯经常比我理解问题,然后突然宣布,“我知道哪里错了。”他通常是正确的。我意识到肯正在建立一个心理代码的模型,当出现问题时,它就是模型中的错误。通过考虑如何这个问题可能会发生,他会直觉模型错误的地方或者我们的代码不能满足型号。

Ken 告诉我,调试前的思考非常重要。如果你深入研究 bug,你倾向于修复代码中的局部问题,但如果你首先考虑 bug,bug 是如何产生的,你经常会发现并纠正代码中更高级别的问题,这将改进设计并防止进一步的错误。”

【讨论】:

printf 上使用fprintf(stderr, ...)【参考方案5】:

您可能想阅读一本关于“测试驱动开发”(TDD)的书,例如肯特贝克的那个。

【讨论】:

【参考方案6】:

了解如何使用 Valgrind,至少它是默认工具 Memcheck。在调试各种内存管理问题时,它将为您节省大量时间,例如:

溢出和运行不足的堆块 使用未定义的值 使用已解除分配的对象

【讨论】:

以上是关于C/C++ 中的调试原则/核心主题 [关闭]的主要内容,如果未能解决你的问题,请参考以下文章

面向(嵌入式 C/C++)开发人员的 Eclipse IDE 2020-12:经典深色主题深黑色背景和菜单中的文本

证书中的“主题”是啥意思? [关闭]

7. 如何构建主题域模型原则之站在巨人的肩上IBM-FSDM主题域模型划分

断言与 C/C++ 中的调试有啥关系? [关闭]

启用主题的 Windows XP/Vista 中的 TAnimate 将不起作用 [关闭]

win7下怎样关闭任务栏中的进度条,但不关掉窗口预览