English
Debugging explains a mismatch between expected and observed behavior, then checks a change intended to remove it. The expectation must be explicit: a crash is visible, but a silently wrong total needs an independent reason to call it wrong.
The core activity is gathering evidence that distinguishes possible explanations. A debugger, logs, tests, and version history expose different evidence. Skill consists in choosing an observation that will change what you believe, rather than repeatedly editing nearby code until the symptom disappears.
Prerequisites and connections: Basic programming (§2.1). Operating systems (§4.2) and observability (§4.10) help with failures involving processes, memory, concurrency, and running services.
The Shape of the Skill: From Staring to Systematic Search
A defect, an incorrect internal state, and an observed failure can occur at different places and times. An invalid value may be created during parsing, stored successfully, and fail only when displayed later. The line where execution stops is therefore a starting point, not necessarily the place to repair.
Keep a brief investigation record: the expected result, actual result, relevant environment, current hypotheses, and results of attempted checks. This prevents repeated experiments and makes it possible for someone else to continue the investigation.
Debugging is empirical, but it uses knowledge of the system to make experiments informative. If a value is already wrong before serialization, changing the serializer cannot explain its origin. If the value is correct there and wrong immediately after decoding, inspect that boundary. The quality of the question matters as much as the quantity of logging.
The Core Debugging Workflow
A useful sequence is to describe the failure, reproduce or characterize it, narrow the relevant conditions, compare hypotheses, and verify a repair. These steps can overlap: an observation may force you to revise what counts as the failure or how it is triggered.
Make the Bug Reproducible
Record the smallest set of conditions you currently know: program revision, input, configuration, dependencies, and sequence of actions. Preserve the actual error and expected result. Reproduce from a fresh run where possible so that hidden state from an earlier attempt is less likely to mislead you.
Reduce an example while checking that the same failure remains. Removing unrelated records, dependencies, or interface layers can expose the condition that matters. Do not change many aspects at once and assume the smaller example has the same cause simply because its error message looks similar.
Intermittent failures need a different standard of evidence. Record how often they occur and under what load, timing, or scheduling conditions. A failure that does not recur in a few runs has not thereby been disproved or fixed. Logs and breakpoints can change timing; record-replay tools can help for executions and platforms they support, but are not a universal recorder of every external system.
A reduced reproducer is useful, not a prerequisite for asking for help. When reduction is blocked by hardware, scale, or inaccessible state, provide the observations, attempted experiments, and remaining uncertainty.
Bisection: When Halving the Search Is Valid
Binary search is powerful when the investigation has a suitable ordering and a reliable predicate. In a linear history where one change introduces a persistent regression, classifying a midpoint as good or bad can discard roughly half the remaining candidates. Distinguishing among 200 possible introduction points takes at most eight binary decisions under these ideal assumptions. Real histories can contain merges, untestable revisions, flaky tests, and fixes followed by reintroductions.
git bisect automates this search through revision history. Keep the test and environment suitable for the revisions being compared. In git bisect run, zero marks a good revision, one can mark a bad revision, and 125 skips an untestable revision. Unexpected harness failures must be handled deliberately: a missing test command must not be mistaken for the regression you are seeking. Skips can leave several possible introduction points.
Reducing an input is a related but different task. Suppose a bug requires both a header H and an option X. An input containing both fails, while either half containing only one passes. The failure has not been localized to one half. Delta debugging tests subsets and complements, preserving the failure as it reduces the example; a result minimal under the chosen removal rule need not be the smallest possible failing input.
You can also inspect state at intermediate boundaries, but choose a property meaningful at that point. “The final answer is not present yet” is not a defect halfway through a computation. Divide the search where a value or invariant should already hold.
The Hypothesis-Verification Loop
Write a hypothesis with a prediction before changing code. If a stale cache causes an old value, bypassing that cache should change the result while holding the input and relevant state fixed. If both cached and uncached reads produce the same old value, inspect the data source or update path instead. An experiment is useful when plausible explanations predict different outcomes.
Change one relevant factor where practical, and record what else could have changed. Restarting a service changes many things at once: memory, connections, caches, and timing. Recovery after restart is evidence, but does not by itself identify which of those changes mattered.
Use the observation tool appropriate to the question. A conditional breakpoint can stop on a particular input; a watchpoint can identify a write where supported; a trace can connect events across components. Optimized code may omit variables or reorder operations, and a debugger’s view is limited to what it can observe. Check that instrumentation measures the value or event your hypothesis concerns.
Distinguish Symptom from Root Cause
A repair should address a demonstrated mechanism at an appropriate boundary. If a missing value is legitimate, handling it at the caller may be the correct repair. If the interface promises that it cannot be missing, silently substituting a default can hide an upstream defect. Determine the contract before deciding whether a guard is a fix or a workaround.
Failures often have several contributing conditions. A configuration error, absent validation, and delayed detection may all matter. Describe the chain you have evidence for instead of forcing every incident into a single ultimate cause. Immediate containment can be justified while a more complete repair is prepared.
Verify the repair with the original failing case and nearby cases that exercise the intended rule. Check that the regression test fails against the old behavior for the expected reason. For intermittent failures, report the conditions and amount of observation after the change; do not turn “not seen again yet” into certainty.
AI-Assisted Debugging
AI assistance can propose hypotheses, interpret an error, or suggest a smaller example. Provide the exact observations and relevant code, and ask what evidence would distinguish its explanations. Verify API and version claims in the appropriate documentation.
Avoid a sequence of unrelated generated patches. Keep a hypothesis, prediction, observation, and conclusion for each attempt. A proposed fix is worth applying when its mechanism is understood and its effects can be checked; a confident explanation alone adds no evidence.
What Changes With Mastery
You can explain why the next experiment is informative and what each possible result would imply. You can also stop an unproductive line of investigation when its predictions fail.
Practice on a small known defect without looking at its fix. Keep an investigation record, reduce the example where possible, and compare your explanation with the actual change afterward. Judge the process by the evidence it produced, not by whether the first guess happened to be right.
Resources
Books and Texts
Agans’s Debugging organizes practical investigation habits. Zeller’s Why Programs Fail develops systematic methods, and The Debugging Book’s input-reduction chapter gives executable examples of reducing failures and explains minimality. Kernighan and Pike connect debugging to testing and program design. Apply a method to an actual defect and record where its assumptions hold.
| Book | Role | Type |
|---|---|---|
| Agans, Debugging: The 9 Indispensable Rules | Practical investigation habits | Entry |
| Zeller, Why Programs Fail (2nd ed.) | Systematic, scientific debugging methodology | Depth |
| Zeller, The Debugging Book (free) | Executable text on automated debugging | Depth |
| Debugging chapters in Kernighan & Pike, The Practice of Programming | Distilled practical wisdom | Practice |
Practice, Tools, and Projects
Learn the debugger for the language you use, then add tools for the observed failure class. Sanitizers detect selected runtime violations in instrumented executions; they do not establish the absence of every error. Record-replay and tracing tools have platform and workload constraints. The git bisect manual documents classification, skipped revisions, and automation; practice those cases in a disposable repository.
| Resource | Platform | Type |
|---|---|---|
| Interactive debugger (gdb/lldb/pdb/DevTools) | Language-specific | Practice |
| git bisect (including bisect run) | Git | Practice |
| rr time-travel debugger (free) | rr-project.org | Practice |
| AddressSanitizer / ThreadSanitizer (free) | GCC / Clang | Practice |
| Valgrind (free) | valgrind.org | Practice |
| Perfetto (free) | perfetto.dev | Practice |
| Julia Evans debugging and profiling zines (paid; some free articles) | wizardzines.com | Auxiliary |
Traps
| Trap | Why it misleads | Better response |
|---|---|---|
| Editing before stating the failure | There is no stable criterion for success. | Record expected and observed behavior first. |
| Treating the crash site as the cause | Invalid state can originate much earlier. | Trace where the relevant invariant first fails. |
| Assuming every problem can be halved | Failures can depend on interacting parts. | Check the ordering and predicate; preserve the failure during reduction. |
| Misclassifying a broken test harness | Tool failure can be mistaken for a bad revision. | Separate regression, untestable revision, and harness error. |
| Changing many conditions without a prediction | Recovery does not identify which change mattered. | Choose discriminating experiments and record confounders. |
| Declaring an intermittent bug fixed after a quiet run | A rare failure can remain unseen by chance. | Report observation conditions and test the proposed mechanism. |
| Replacing investigation with generated patches | A plausible repair can hide the same faulty assumption. | Require a mechanism and a regression check. |
中文
调试解释预期行为与实际行为之间的差异,再检查用于消除差异的修改。预期必须明确:崩溃容易看见,但一个悄悄算错的总额,需要独立依据才能判断它错了。
核心活动是收集能够区分不同解释的证据。调试器、日志、测试和版本历史提供不同证据。调试能力在于选择能改变自己判断的观察,而不是反复修改附近代码,直到症状消失。
前置知识与联系:基础编程(§2.1)。操作系统(§4.2)与可观测性(§4.10)有助于处理涉及进程、内存、并发和运行中服务的故障。
这项能力的形状:从盯代码到系统化搜索
缺陷、错误的内部状态和可观察的失败,可能发生在不同地点与时间。解析时产生的无效值,可能顺利存储,直到之后显示时才失败。因此,程序停止的那一行只是起点,不一定是应该修复的位置。
保留简短调查记录:预期结果、实际结果、相关环境、当前假设,以及已经做过的检查和结果。这样既能避免重复实验,也方便别人接着调查。
调试依靠经验性证据,同时需要利用系统知识,让实验有区分力。如果一个值在序列化之前就错了,修改序列化器无法解释错误的来源;如果之前正确,解码后立即错误,就检查这个边界。提出什么问题,与记录多少日志同样重要。
核心调试工作流
一个实用顺序是:描述失败,复现或确定其特征,缩小相关条件,比较假设,再验证修复。这些步骤可以交叉进行:新的观察可能迫使你重新定义失败是什么,或它怎样触发。
让 bug 可复现
记录当前已知的最小条件集合:程序版本、输入、配置、依赖和操作顺序。保留实际错误与预期结果。尽可能从新启动的程序复现,减少之前尝试遗留的隐藏状态造成的干扰。
缩小例子时,持续检查同一种失败是否仍然存在。删除无关记录、依赖或界面层,可能暴露真正重要的条件。不要一次改变许多方面,再仅凭错误信息相似,就认定缩小后的例子具有相同原因。
间歇性故障需要不同的证据标准。记录发生频率,以及相关的负载、时序或调度条件。几次运行没有重现,不代表故障已被排除或修好。日志和断点可能改变时序;记录回放工具可以帮助处理它们支持的平台与执行过程,却不能普遍记录所有外部系统。
缩小后的复现案例很有用,但不是求助的前提。如果硬件、规模或不可访问的状态妨碍缩减,就提供观察结果、已做实验和仍不确定的部分。
二分法:何时能够排除一半候选
当调查具有合适的顺序和可靠的判断条件时,二分搜索很有效。在线性历史中,如果某次修改引入此后持续存在的回归,那么判断中间版本好坏,就能排除约一半剩余候选。在这些理想假设下,从 200 个可能的引入点中确定一个,至多需要八次二元判断。真实历史可能包含合并、无法测试的版本、不稳定测试,以及修复后再次引入的故障。
git bisect 自动执行版本历史中的这种搜索。测试与环境必须适用于被比较的版本。在 git bisect run 中,零表示好版本,一可以表示坏版本,125 跳过无法测试的版本。必须明确处理测试脚本的意外失败:测试命令缺失,不能被误判为正在寻找的回归。跳过版本可能使最终仍有多个候选引入点。
缩减输入是相关但不同的任务。假设一个缺陷同时需要头部 H 和选项 X:两者都在时失败,各自只保留一个的两半却都通过。这并没有把故障定位到其中一半。Delta debugging 会测试子集及其补集,在保留失败的同时缩减例子;在所选删除规则下已经无法进一步缩小的结果,不一定是所有失败输入中最小的一个。
也可以在中间边界检查状态,但必须选择在该位置有意义的性质。“最终答案还没有产生”,并不表示执行到一半就出了错。应在某个值或不变量已经应该成立的位置划分搜索范围。
假设—验证循环
修改代码之前,写出带预测的假设。如果旧缓存导致读到旧值,那么在输入与相关状态保持不变时,绕过缓存应该改变结果。如果使用缓存和绕过缓存都读到相同旧值,就转而检查数据源或更新路径。当不同合理解释预测不同结果时,实验才有区分力。
实际可行时,每次改变一个相关因素,并记录还有哪些条件可能变化。重启服务会同时改变许多东西:内存、连接、缓存和时序。重启后恢复是证据,但不能单独确定其中哪种变化起了作用。
根据问题选择观察工具。条件断点可以在特定输入时暂停;工具支持时,监视点可以找到一次写入;追踪信息可以连接不同组件中的事件。优化后的代码可能省略变量或重排操作,调试器也只能呈现它能观察到的内容。应确认观测手段测量的正是当前假设涉及的值或事件。
区分症状和根因
修复应在合适的边界处理已经证实的机制。如果缺失值是合法情况,调用者处理它可能就是正确修复;如果接口保证它不会缺失,悄悄替换默认值可能掩盖上游缺陷。先确定约定,才能判断防护检查是修复还是临时绕过。
失败往往有多个促成条件。配置错误、缺少验证和发现过晚,都可能相关。描述证据支持的因果过程,不必把每起事故强行归结为唯一最终原因。准备更完整的修复时,先控制影响也可能是合理选择。
用原始失败案例,以及检验目标规则的邻近案例验证修复。确认回归测试在旧行为下会因预期原因失败。对于间歇性故障,应报告修改后的观察条件和观察量,不要把“暂时没有再看到”说成确定无误。
AI 辅助调试
AI 可以提出假设、解释错误或建议缩小案例。提供确切观察和相关代码,并要求说明什么证据能够区分不同解释。API 和版本方面的说法,应查阅对应文档验证。
避免接连应用互不相关的生成补丁。每次尝试都保留假设、预测、观察和结论。当修改机制已被理解、影响能够检查时,才值得应用建议修复;肯定的解释本身并不增加证据。
熟练之后会发生什么变化
你能够解释下一次实验为何有用,以及不同结果分别意味着什么;预测失败时,也能够停止一条没有进展的调查路线。
选择一个已知的小缺陷练习,先不看修复。保留调查记录,尽可能缩小例子,最后将自己的解释与实际修改比较。根据过程产生的证据评价它,而不是看第一次猜测是否碰巧正确。
资源
书籍与文本
Agans 的《Debugging》整理了实际调查习惯;Zeller 的《Why Programs Fail》发展系统方法;The Debugging Book 的输入缩减章节提供可执行例子,并解释最小性。《The Practice of Programming》则联系调试、测试与程序设计。将方法用于具体缺陷,并记录它的假设在哪里成立。
| 书籍 | 作用 | 类型 |
|---|---|---|
| Agans,Debugging: The 9 Indispensable Rules | 实际调查习惯 | 入门 |
| Zeller,Why Programs Fail(第 2 版) | 系统化、科学化的调试方法论 | 深入 |
| Zeller,The Debugging Book(免费) | 关于自动化调试的可执行文本 | 深入 |
| Kernighan & Pike,The Practice of Programming 中的调试章节 | 提炼过的实践智慧 | 实践 |
实践、工具与项目
先学习所用语言的调试器,再根据观察到的故障类型增加工具。Sanitizer 通过插桩执行发现特定运行时违规,不能证明所有错误都不存在。记录回放与追踪工具也有平台、负载限制。git bisect 手册说明分类、跳过版本和自动化;可以在练习仓库中尝试这些情况。
| 资源 | 平台 | 类型 |
|---|---|---|
| 交互式调试器(gdb/lldb/pdb/DevTools) | 各语言专用 | 实践 |
| git bisect(包括 bisect run) | Git | 实践 |
| rr 时间旅行调试器(免费) | rr-project.org | 实践 |
| AddressSanitizer / ThreadSanitizer(免费) | GCC / Clang | 实践 |
| Valgrind(免费) | valgrind.org | 实践 |
| Perfetto(免费) | perfetto.dev | 实践 |
| Julia Evans 调试和性能分析 zine(付费;部分免费文章) | wizardzines.com | 辅助 |
陷阱
| 误区 | 为什么会误导 | 更好的做法 |
|---|---|---|
| 尚未描述失败就修改 | 缺少稳定的成功标准。 | 先记录预期与实际行为。 |
| 把崩溃位置当成原因 | 无效状态可能更早就已产生。 | 追踪相关不变量最初在哪里失效。 |
| 认为所有问题都能二分 | 失败可能依赖多个部分相互作用。 | 检查顺序与判断条件,缩减时保留失败。 |
| 错误分类测试脚本故障 | 工具故障可能被当成坏版本。 | 区分回归、无法测试的版本和测试脚本错误。 |
| 没有预测就同时改变多个条件 | 恢复并不能确定哪种变化起作用。 | 选择有区分力的实验,记录干扰因素。 |
| 一次未复现就宣布修好偶发故障 | 低频故障可能只是碰巧未出现。 | 报告观察条件,并检验提出的机制。 |
| 用生成补丁代替调查 | 看似合理的修复可能掩盖同一错误假设。 | 要求说明机制,并进行回归检查。 |