7

你调试问题的标准方法是什么?这似乎是一个相当广泛的问题,你们中的一些人回答“这取决于问题”,但我认为我们中的很多人都是凭直觉进行调试的,实际上并没有尝试过对我们的流程进行措辞。这就是我们说“视情况而定”的原因。

我最近有点被迫说出我的过程,因为我和一些开发人员正在解决同样的问题,我们正在以完全不同的方式对其进行调试。我希望他们了解我想要做什么,反之亦然。

经过一番思考,我意识到我的调试方式实际上很单调。我将首先尝试能够可靠地复制问题(尤其是在我的本地机器上)。然后通过一系列消除(我认为这与问题有关)尝试找出问题所在。

其他人试图以完全不同的方式做到这一点。

所以,只是想知道你们那里有什么工作吗?如果您必须用文字形式化它,您会说您的过程用于调试什么?

顺便说一句,我们还没有发现我们的问题 =)

4

11 回答 11

7

我的方法因我对手头系统的熟悉程度而异。通常我会做类似的事情:

  1. 如果可能的话,复制失败。
  2. 检查故障状态以确定故障的直接原因。
  3. 如果我熟悉该系统,我可能会很好地猜测根本原因。如果不是,我开始通过软件机械地追溯数据,同时挑战软件所做的基本假设。
  4. 如果问题似乎有一个一致的触发器,我可能会使用调试器手动向前遍历代码,同时挑战代码所做的隐含假设。

当然,追查根本原因是事情可能会变得棘手。这就是拥有转储(或者更好的是,一个活的、破碎的进程)真正无价的地方。

我认为调试过程中的关键点是挑战先入为主的概念和假设。 我在该组件中发现我或同事发誓工作正常的错误的次数是巨大的。

更直观的朋友和同事告诉我,当他们看着我调试或要求我帮助他们解决问题时,我非常迂腐。:)

于 2009-04-08T03:57:34.050 回答
4

考虑获取David J Agans的《调试》一书。副标题是“寻找最难以捉摸的软件和硬件问题的 9 条不可或缺的规则”。他的调试规则列表——在网站上以海报形式提供(也有这本书的链接)是:

  • 了解系统
  • 让它失败
  • 停止思考,看看
  • 分而治之
  • 一次改变一件事
  • 保留审计线索
  • 检查插头
  • 获得全新的视角
  • 如果你不修复它,它就没有修复

最后一点与软件行业特别相关。

于 2009-04-08T05:52:05.273 回答
3

当我遇到一个我似乎无法弄清楚的错误时,我喜欢为这个问题建立一个模型。制作问题代码部分的副本,并开始从中删除功能,一次一个。每次删除后对代码运行单元测试。通过此过程,您将删除带有错误的功能(并因此定位错误),或者将错误隔离到包含问题本质的核心代码段。一旦你弄清楚问题的本质,解决起来就会容易得多。

于 2009-04-08T03:55:51.840 回答
3

我选择了网上的那些或一些我不记得的书(可能是 CodingHorror ...)

调试 101:

  • 复制
  • 逐渐缩小范围
  • 避免使用调试器
  • 一次只改变一件事

心理方法:

  • 橡皮鸭调试
  • 不要推测
  • 不要急于责备工具
  • 了解问题和解决方案
  • 休息一下
  • 考虑多种原因

错误预防方法:

  • 监控你自己的故障注入习惯
  • 尽早引入调试辅助工具
  • 松散耦合和信息隐藏
  • 编写回归测试以防止 Re 发生

技术方法:

  • 惰性跟踪语句
  • 查阅第三方产品的日志文件
  • 在网络上搜索堆栈跟踪
  • 按合同引入设计
  • 把石板擦干净
  • 间歇性错误
  • 探索局部性
  • 介绍虚拟实现和子类
  • 重新编译/重新链接
  • 探测边界条件和特殊情况
  • 检查版本依赖(第三方)
  • 检查最近更改的代码
  • 不要相信错误信息
  • 图形错误
于 2009-04-08T04:31:59.350 回答
2

我通常会根据手头的信息形成一个假设。一旦完成,我会努力证明它是正确的。如果它被证明是错误的,我会从一个不同的假设开始。

使用这种方法可以很容易地解决大多数多线程同步问题。

此外,您还需要对您正在使用的调试器及其功能有一个很好的了解。我在 Windows 应用程序上工作,发现 windbg 对查找错误非常有帮助。

于 2009-04-08T04:07:03.437 回答
2

将错误减少到最简单的形式通常会导致对问题的更好理解,并增加在必要时能够让其他人参与的好处。

设置一个快速复制场景,以便有效地利用您的时间来测试您选择的任何假设。

创建工具以快速转储环境以进行比较。

使用日志记录创建和重现错误已达到最高级别。

检查系统日志是否有任何警报。

查看文件日期和时间戳以了解问题是否可能是最近的介绍。

查看源存储库以了解相关模块中的最新活动。

应用演绎推理并应用奥卡姆剃刀原则。

愿意退后一步,从问题中解脱出来。

于 2009-04-08T04:07:05.213 回答
1

我也是使用消除过程的忠实粉丝。排除变量极大地简化了调试任务。这通常是应该做的第一件事。

另一个真正有效的技术是尽可能回滚到上一个工作版本并重试。这可能非常强大,因为它为您提供了坚实的基础,可以更加谨慎地进行。对此的一种变体是使代码达到可以工作的程度,但功能较少,而不是不具有更多功能。

当然,重要的是不要只是尝试。这会增加你的绝望,因为它永远不会奏效。我宁愿跑 50 次来收集关于这个 bug 的信息,而不是疯狂地摇摆并希望它有效。

于 2009-04-08T04:05:30.863 回答
1

我发现“调试”的最佳时间是在编写代码时。换句话说,要防守。检查返回值,自由使用断言,使用某种可靠的日志记录机制并记录所有内容。

为了更直接地回答这个问题,我调试问题的最有效方法是阅读代码。拥有日志可以帮助您找到相关代码以快速阅读。没有记录?花时间把它放进去。看起来你似乎没有找到错误,你也可能没有。不过,日志记录可能会帮助您找到另一个错误,最终,一旦您完成了足够多的代码,您就会发现它......比设置调试器并尝试重现问题、单步执行等更快。

在调试时,我尝试考虑可能出现的问题。我提出了一个相当随意的分类系统,但它对我有用:所有错误都属于四个类别之一。请记住,我说的是运行时问题,而不是编译器或链接器错误。这四类是:

  • 动态内存分配
  • 堆栈溢出
  • 未初始化的变量
  • 逻辑错误

这些类别对我来说对 C 和 C++ 最有用,但我希望它们在其他地方也能很好地应用。逻辑错误类别很大(例如,当正确的事情是 a <= b 时放置 a < b),并且可能包括诸如未能在线程之间同步访问之类的事情。

知道我在寻找什么(这四件事之一)有助于找到它。发现错误似乎总是比修复它们要困难得多。

调试的实际机制通常是:

  1. 我有演示问题的自动化测试吗?
    • 如果没有,添加一个失败的测试
  2. 更改代码以使测试通过
  3. 确保所有其他测试仍然通过
  4. 签入更改

您的环境中没有自动化测试?没有时间像现在这样设置它。组织事物太难以至于您可以测试程序的各个部分?花点时间做到这一点。修复此特定错误可能会花费“太长时间”,但您越早开始,其他一切都会越快。同样,您可能无法修复您正在寻找的特定错误,但我敢打赌您会在此过程中找到并修复其他错误。

于 2009-04-08T08:57:15.093 回答
0

我的调试方法不同,可能是因为我还是初学者。

当我遇到逻辑错误时,我似乎最终会添加更多变量以查看哪些值在哪里,然后我在导致问题的代码段中逐行调试。

于 2009-04-08T04:37:09.337 回答
0

复制问题并生成可重复的测试数据集绝对是调试的第一步,也是最重要的一步。

如果我能识别出一个可重复的错误,我通常会尝试隔离所涉及的组件,直到找到问题所在。我经常会花一点时间排除案例,以便明确说明:问题不在于组件 X(或流程 Y 等)。

于 2009-04-08T05:35:41.320 回答
0

首先,我尝试复制错误,如果无法复制错误,那么在非平凡的程序中基本上不可能猜测问题。

然后,如果可能,将代码拆分到一个单独的独立项目中。这有几个原因:如果原始项目很大,第二次调试非常困难,它会消除或突出关于代码的任何假设。

我通常总是有另一个 VS open 副本,用于调试迷你项目中的部分并测试我稍后添加到主项目中的例程。

一旦在单独的模块中重现了错误,战斗几乎就赢了。

有时破解一段代码并不容易,所以在这些情况下,我会根据问题的复杂程度使用不同的方法。在大多数情况下,关于数据的假设似乎来咬我,所以我尝试在代码中添加大量断言,以确保我的假设是正确的。我还使用#ifdef 禁用代码,直到错误消失。消除对其他模块的依赖等......有点像秃鹰一样在错误中缓慢盘旋......

我想我并没有真正有意识的方式来做这件事,它变化很大,但一般原则是消除围绕问题的噪音,直到它非常明显。希望我听起来不会太混乱:)

于 2009-04-08T06:08:31.567 回答