10

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

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

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

4

6 回答 6

10

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

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

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

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

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

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

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

于 2012-09-01T17:07:37.910 回答
3

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

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

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

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

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

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

于 2012-09-01T17:35:03.897 回答
2

诉诸 没有错printf。事实上,它有很多优点:

  • printf已被注释掉的语句清楚地表明了过去的调试尝试,这表明该代码区域可能存在可疑之处。

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

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

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

让我在这里引用Rob Pike的话:

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

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

于 2012-09-01T21:15:19.717 回答
1

您可能想阅读一本关于“测试驱动开发”(TDD) 的书,例如 Kent Beck 的书。

于 2012-09-01T17:03:40.643 回答
1

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

  • 溢出和未运行的堆块
  • 使用未定义的值
  • 使用已经释放的对象
于 2012-09-02T06:40:59.003 回答