什么是调试器,它如何帮助我诊断问题?


107

这旨在作为一个通用问题,以帮助遇到程序问题但不知道如何使用调试器诊断问题原因的新程序员。

该问题涵盖三类更具体的问题:

  • 当我运行程序时,它不会产生我期望输入的输出。
  • 当我运行程序时,它崩溃并给了我堆栈跟踪。我已经检查了堆栈跟踪,但是我仍然不知道问题的原因,因为堆栈跟踪没有为我提供足够的信息。
  • 当我运行程序时,它由于分段错误(SEGV)而崩溃。

3
尼斯的工作-这也将是很好的有一个相关的“去” Q&A的调试技术,例如使用调试器,其他调试工具(如Valgrind的),战略printfs输出,压力测试,分而治之,等等
保罗R

1
我同意@PaulR,常见问题解答应包含此类内容。
Nicu Stiurca,2014年

Answers:


73

调试器是可以在程序运行时检查程序状态的程序。它用于执行此操作技术手段对于理解如何使用调试器的基础并不重要。您可以使用调试器在程序到达代码中的特定位置时暂停程序的执行,然后检查程序中变量的值。在检查其变量的值时,可以使用调试器非常缓慢地运行程序,一次只运行一行代码(称为单步执行)。

使用调试器是预期的基本技能

调试器是一种非常强大的工具,可帮助诊断程序问题。调试器可用于所有实用的编程语言。因此,能够使用调试器被认为是任何专业或发烧级程序员的基本技能。并使用调试器自己 被认为是基本的工作,你应该做你自己问别人的帮助了。由于此站点是针对专业和发烧级程序员的站点,而不是帮助台或指导站点,因此,如果您对特定程序的问题有疑问,但没有使用调试器,那么很可能会关闭并拒绝您的问题。如果您仍然遇到类似的问题,最终将阻止您发布更多信息。

调试器如何帮助您

通过使用调试器,您可以发现变量是否具有错误的值,以及在程序中变量的值更改为错误的值的位置。

使用单步执行,您还可以发现控制流是否符合您的期望。例如,if分支是否应按预期执行。

有关使用调试器的一般说明

使用调试器的细节取决于调试器,并且在较小程度上取决于您使用的编程语言。

  • 您可以调试器附加到已经在运行程序的进程中。如果程序卡住,您可能会这样做。

  • 实际上,从一开始,在调试器的控制下运行程序通常会更容易。

  • 您可以通过指示应停止执行的行的源代码文件和行号,或通过指示程序应停止执行的方法/函数的名称,来指示程序应在何处停止执行(如果要以一旦执行进入方法。调试器用来导致​​程序停止的技术手段称为断点,而此过程称为设置断点

  • 大多数现代调试器都是IDE的一部分,并为您提供了一个方便的GUI,用于检查程序的源代码和变量,并带有单击界面,用于设置断点,运行程序并单步执行。

  • 除非程序可执行文件或字节码文件包含调试符号信息和对源代码的交叉引用,否则使用调试器可能非常困难。您可能必须稍微不同编译(或重新编译)程序,以确保存在信息。如果编译器执行了广泛的优化,则这些交叉引用可能会造成混淆。因此,您可能不得不在关闭优化的情况下重新编译程序


4
这是不完整的,因为它错过了所有最重要的调试器,有可能极大地减少Stackoverflow上的问题数量(我预计至少减少20%)-javascript调试器:firebug,Chrome,Firefox,IE9 +集成调试器,IE8- Visual Studio等
slebetman 2014年

1
同样适用于node.js-节点检查器。但是,node.js程序员所提出的基本和/或“修复我的代码”问题并不像一般的javascript程序员那样多。
slebetman 2014年

40

我想补充一点,调试器并不总是完美的解决方案,也不应该总是调试的首选解决方案。在某些情况下,调试器可能对您不起作用:

  • 程序中失败的部分确实很大(也许是模块化不佳?),您不确定要从哪里开始逐步执​​行代码。逐步执行所有操作可能会非常耗时。
  • 您的程序使用许多回调和其他非线性流控制方法,这使调试器在逐步调试时感到困惑。
  • 您的程序是多线程的。甚至更糟的是,您的问题是由竞争状况引起的。
  • 包含错误的代码在错误发布之前会运行多次。这在主循环中可能尤其成问题,或者在物理引擎中更为严重,因为物理引擎中的问题可能是数字。在这种情况下,即使设置一个断点,也会使您多次击中它,而不会出现该错误。
  • 您的程序必须实时运行。对于连接到网络的程序来说,这是一个大问题。如果您在网络代码中设置了断点,则另一端不会等待您逐步通过,而只是超时了。依赖于系统时钟的程序,例如带有跳帧的游戏,也都不会更好。
  • 您的程序执行某种形式的破坏性操作,例如写入文件或发送电子邮件,并且您希望限制运行该程序的次数。
  • 您可以说出您的错误是由错误的值到达函数X引起的,但您不知道这些值来自何处。必须一次又一次地运行程序,将断点设置得越来越远,这可能是一个巨大的麻烦。尤其是在整个程序的许多地方都调用了函数X的情况下。

在所有这些情况下,要么突然停止程序可能会导致最终结果有所不同,要么手动单步查找导致错误的一行会很麻烦。无论您的错误是错误的行为还是崩溃,都可能同样发生。例如,如果内存损坏导致崩溃,那么到崩溃发生时,它与第一次发生内存损坏的位置相距太远,并且没有留下有用的信息。

那么,有哪些替代方案?

最简单的就是记录和断言。在各个时间点将日志添加到程序中,然后将您得到的与期望的进行比较。例如,看看是否首先调用了您认为存在错误的函数。看看方法开始时的变量是否符合您的想法。与断点不同,没有很多特殊情况的日志行是可以的。您可以随后简单地搜索日志。一旦您遇到了与预期不同的日志行,请在同一区域中添加更多内容。使其越来越窄,直到足够小以至于无法记录错误区域中的每一行。

断言可用于在出现不正确的值时捕获它们,而不是一旦它们对最终用户具有可见的影响。越早发现不正确的值,您就越接近产生该值的行。

重构和单元测试。如果您的程序太大,则可能值得一次测试一个类或一个函数。给它输入,看一下输出,看看哪个不是您期望的。能够将错误从整个程序缩小到单个功能,可以大大缩短调试时间。

如果出现内存泄漏或内存重载,请使用能够在运行时进行分析和检测的适当工具。能够检测到实际损坏发生的位置是第一步。此后,您可以使用日志将您的方法返回到引入错误值的位置。

请记住,调试是一个倒退的过程。您得到了最终结果-一个错误-并且找到了导致该错误的原因。这是关于向后工作的方法,不幸的是,调试器只能向前走。在这里,良好的日志记录和事后分析可以为您提供更好的结果。


10
这将是一个很好的答案……另一个问题。这个问题的答案很糟糕。也许您应该问这个问题并将其发布为对它的答复。
Raedwald

10
实际问题被描述为“协助有程序问题的新程序员”,“它不会产生我期望的输出”和“我已经检查了堆栈跟踪,但我仍然不知道问题的原因” 。所有这些都得到了这个答案的帮助。此外,解释调试器做什么时,解释什么是同样重要的去做。
SlugFiller 2015年

5
好答案。我一直使用调试器作为查找错误的主要工具。但是现在我在一个项目中工作,在该项目中,一个巨大的基础架构组件正在使用许多线程和许多网络代码(客户端/服务器),并且注意到调试器是最后对我有帮助的东西。您提到了很多事情,您应该真正使用其他工具,而不是依赖于良好的旧调试器(最重要的是:日志记录)。
蒂姆·施密特

“您可以说出您的错误是由到达函数X的不正确值引起的,但是您不知道这些值来自何处。”这尤其难以调试。您通常如何解决这样的问题?
Ayxan Haqverdili

3
@Ayxan在某种程度上,如果您设法使函数在断言上中断,则可以使用调用堆栈来获取调用方。但这本身并不能为您提供价值的来源,因为价值很可能来自较早的那一行。基本上,您必须通过值所经过的各种变量来跟踪该值。如果您对数据采用的路径有一个很好的了解,则可以只创建一堆日志记录,然后尝试缩小“错误”的范围。如果没有,则基本上每步返回都需要单独运行该程序(重现错误)。
SlugFiller
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.