拓展阅读
Devops-02-Jpom 简而轻的低侵入式在线构建、自动部署、日常运维、项目监控软件
项目管理平台-01-jira 入门介绍 缺陷跟踪管理系统,为针对缺陷管理、任务追踪和项目管理的商业性应用软件
项目管理平台-01-Phabricator 入门介绍 一套集成的强大工具,帮助公司构建更高质量的软件
调试
在工程领域,调试是寻找根本原因、解决方法和可能的修复措施的过程。
对于软件,调试策略可以包括交互式调试、控制流分析、日志文件分析、应用程序或系统级别的监控、内存转储和性能分析。
许多编程语言和软件开发工具也提供用于调试的程序,称为调试器。
Etymology 词源学
参见:Bug(工程学)§ 历史
一张来自Mark II的计算机日志条目,页面上粘着一只蛾子
“Bug”这一术语,作为缺陷的意思,至少可以追溯到1878年,当时托马斯·爱迪生将他发明中的“小故障和困难”称为“Bugs”。
一个流行的故事来自1940年代的格雷斯·霍普海军上将。当时她在哈佛大学的Mark II计算机上工作,她的同事发现一只蛾子卡在继电器中,影响了计算机的正常运行,便在日志本中写下了“首次发现Bug”。尽管这很可能是个玩笑,将“Bug”这一词的生物学意义和缺陷意义混淆在一起,但这个故事表明,那个时期“Bug”一词已经在计算机领域被使用。
类似地,调试(debugging)这一术语在进入计算机领域之前,已在航空领域使用。二战时期洛斯阿拉莫斯曼哈顿计划的原子弹项目负责人J.罗伯特·奥本海默在1944年10月27日写给加利福尼亚大学伯克利分校的厄内斯特·劳伦斯博士的一封信中提到了这一术语,信中讨论了招募额外技术人员的问题。牛津英语词典对“debug”一词的定义中,提到了在1945年《皇家航空学会期刊》上的一篇文章,使用了调试(debugging)一词,指的是飞机发动机的测试。在《空军》杂志(1945年6月,第50页)中,也提到过调试飞机相机的问题。
吉尔(Gill)于1951年发表的开创性文章是最早深入讨论编程错误的文章,但它并没有使用“Bug”或“调试”这一术语。
在ACM的数字图书馆中,调试(debugging)一词首次出现在1952年三篇ACM全国会议的论文中。两篇论文使用了引号标注这一术语。
到1963年,调试(debugging)已成为一个足够常见的术语,以至于在《CTSS手册》的第一页中提到时无需进一步解释。
scope 范围
随着软件和电子系统变得越来越复杂,各种常见的调试技术也得到了扩展,增加了更多的方法来检测异常、评估影响,并安排软件补丁或系统的全面更新。
在这种情况下,“异常”和“差异”这两个词可以作为更中立的术语使用,以避免使用“错误”、“缺陷”或“Bug”这类词汇,因这些词可能暗示所有所谓的错误、缺陷或Bug必须修复(无论成本如何)。
相反,可以进行影响评估,以确定去除异常(或差异)的更改是否对系统具有成本效益,或者可能通过安排一个新的版本发布来使这些更改变得不必要。并非所有问题在系统中都是安全关键或任务关键的。
此外,避免出现更改可能会比处理已知问题(即“治病不如养病”)对用户造成更长远的困扰的情况也很重要。基于某些异常是否可接受来做出决策,可以避免形成“零缺陷”要求的文化,因为这种文化可能会诱使人们否认问题的存在,从而使结果看起来没有缺陷。
考虑到附带问题,例如成本与收益的影响评估,调试技术将进一步扩展,以确定异常的发生频率(即相同的“Bug”发生的次数),从而帮助评估其对整体系统的影响。
tools 工具
主条目:调试器
在视频游戏主机上的调试通常通过专用硬件进行,如这款为开发人员设计的Xbox调试单元。
调试的复杂性从修复简单错误到执行冗长且繁琐的数据收集、分析和更新调度任务不等。
程序员的调试技能是调试问题能力的重要因素,但软件调试的难度在很大程度上取决于系统的复杂性,也在某种程度上依赖于使用的编程语言以及可用的工具,如调试器。调试器是软件工具,使程序员能够监控程序的执行、暂停程序、重新启动程序、设置断点并更改内存中的值。调试器一词也可以指正在进行调试的人员。
通常,高级编程语言,如Java,使调试变得更容易,因为它们具备异常处理和类型检查等特性,这使得更容易发现不正常行为的根源。在像C或汇编这样的编程语言中,Bug可能导致沉默的问题,如内存损坏,而往往很难看到初始问题发生的位置。在这些情况下,可能需要使用内存调试工具。
在某些情况下,专门为某种语言设计的通用软件工具可以非常有用。这些工具通常以静态代码分析工具的形式出现。它们在源代码中查找一组非常特定的已知问题,其中一些是常见的,另一些则是罕见的,重点关注语义(例如数据流)而非语法,因为编译器和解释器更多关注语法。
各种语言都有商业版和免费版工具;一些工具声称能够检测数百种不同的问题。这些工具在检查非常大的源代码树时尤其有用,因为手动代码审查在这种情况下并不实际。一个典型的被检测到的问题可能是一个在变量赋值之前就被取消引用的变量。另一个例子是,一些工具在语言本身不要求时执行强类型检查。因此,它们在查找语法正确但可能有错误的代码时更加有效。但这些工具有误报的名声,即将正确的代码标记为可疑。旧版Unix的lint程序是一个早期的例子。
对于调试电子硬件(例如计算机硬件)以及低级软件(例如BIOS、设备驱动程序)和固件,通常使用示波器、逻辑分析仪或在回路仿真器(ICE)等仪器,单独使用或结合使用。ICE可以执行许多典型软件调试器在低级软件和固件中的任务。
调试过程
调试过程通常从识别重现问题的步骤开始。这可能是一个非平凡的任务,特别是在并行处理和某些Heisenbug(无法稳定重现的Bug)情况下。
例如,特定的用户环境和使用历史也可能使问题难以重现。
在Bug被重现之后,可能需要简化程序的输入,以便更容易调试。例如,编译器中的一个Bug可能会导致它在解析一个大型源文件时崩溃。然而,通过简化测试用例后,原始源文件中的少数几行代码就足以重现相同的崩溃。简化过程可以通过手动使用分治法进行,即程序员尝试去除原始测试用例中的某些部分,然后检查问题是否仍然存在。当在GUI中调试时,程序员可以尝试跳过原始问题描述中的一些用户交互,检查剩下的操作是否足以导致Bug发生。
在测试用例被充分简化后,程序员可以使用调试器工具检查程序状态(变量的值以及调用栈),追踪问题的来源。
或者,也可以使用追踪技术。在简单的情况下,追踪只是一些打印语句,用于输出程序执行过程中某些特定时刻变量的值。
技术
交互式调试
交互式调试使用调试器工具,允许程序的执行逐步处理并在必要时暂停以检查或修改程序状态。子程序或函数调用通常可以以全速执行,执行完毕后暂停或单步执行,或者这两者的混合方式。可以设置断点,允许在不怀疑有问题的代码部分以全速执行,然后在怀疑出错的地方暂停。在程序循环结束后设置断点是一种方便评估重复代码的方式。监视点通常可以使用,它允许程序执行直到某个特定变量发生变化,捕获点则使调试器在某些程序事件发生时暂停,例如异常或共享库加载。
打印调试/追踪
打印调试或追踪是通过观察(实时或记录的)追踪语句或打印语句来了解进程的执行流和数据变化。追踪可以通过专门的工具(如GDB的追踪功能)或通过将追踪语句插入源代码中来完成。后者有时称为printf调试,因为C语言中的printf函数经常用于此类调试。这种调试方式在面向初学者的BASIC编程语言的原始版本中通过TRON命令开启,TRON表示“Trace On”,它使得每条BASIC命令行的行号在程序运行时被打印出来。
活动追踪
活动追踪类似于追踪,但它关注的是程序在执行过程中,处理特定代码段的时间占总执行时间的比例,而不是逐条指令或函数地跟踪程序执行。通常它会以程序在特定内存地址(机器代码程序)或某些程序模块(高级语言或编译程序)中的执行时间占比的形式呈现。如果调试的程序在追踪区域内的执行时间占比过高,这可能表明处理器时间分配不当,可能是由于程序逻辑故障或处理器时间分配低效,需要优化。
远程调试
远程调试是调试运行在与调试器不同的系统上的程序。开始远程调试时,调试器通过通信链路(如局域网)连接到远程系统。调试器可以控制远程系统上程序的执行,并获取程序状态信息。
事后调试
事后调试是指程序崩溃后对其进行调试。相关技术通常包括各种追踪技术,如检查日志文件、输出崩溃时的调用栈,以及分析崩溃进程的内存转储(或核心转储)。进程的转储可以由系统自动获取(例如,当进程因未处理的异常而终止时),也可以由程序员插入指令或由交互用户手动获取。
“狼栅栏”算法
Edward Gauss在1982年为《ACM通讯》撰写的一篇文章中描述了这个简单但非常有用的算法:“阿拉斯加有一只狼;你如何找到它?首先,在州中间建一座围栏,等着狼嚎叫,确定它在哪一边。只在这边重复这个过程,直到你能看到狼。” 该算法已经在Git版本控制系统中实现为命令 git bisect
,用来确定引入特定Bug的提交。
记录与重放调试
记录与重放调试是一种创建程序执行记录的技术(例如使用Mozilla的免费调试工具rr;启用可逆调试/执行),可以重放并进行交互式调试。这对远程调试和调试间歇性、非确定性以及其他难以重现的缺陷非常有用。
时间旅行调试
时间旅行调试是通过源代码回溯到过去的过程(例如使用Undo LiveRecorder),以理解程序执行过程中发生了什么;允许用户与程序交互;如果需要,可以更改历史并观察程序如何响应。
Delta调试
Delta调试是一种自动化测试用例简化的技术。
Saff挤压
Saff挤压是一种通过逐步内联故障测试的部分来隔离故障的技术。
因果追踪
因果追踪是一种跟踪计算中因果关系链的技术。这些技术可以针对特定的Bug进行调整,例如空指针取消引用。
自动修复Bug
自动修复Bug是指在不需要人类程序员干预的情况下自动修复软件Bug。
这也通常被称为自动补丁生成、自动Bug修复或自动程序修复。
这类技术的典型目标是自动生成正确的补丁,以消除软件程序中的Bug,同时避免软件回归。
嵌入式系统调试
与通用计算机软件设计环境不同,嵌入式环境的一个主要特点是可供开发人员选择的不同平台数量极多(包括CPU架构、供应商、操作系统及其变种)。嵌入式系统本质上并不是通用设计:它们通常是为单一任务(或小范围任务)开发的,平台的选择是专门为优化该应用而定制的。
这一特点不仅使嵌入式系统开发人员的工作变得更加艰难,也使得这些系统的调试和测试更加困难,因为不同的平台需要不同的调试工具。
尽管如上所述存在异构性问题,已经有一些商业调试器和研究原型工具被开发出来。商业解决方案的例子包括Green Hills Software、Lauterbach GmbH和Microchip的MPLAB-ICD(用于电路内调试器)。研究原型工具的例子包括Aveksha和Flocklab。
这些工具都利用了低成本嵌入式处理器上可用的功能,即片上调试模块(OCDM),其信号通过标准JTAG接口暴露出来。它们的评估通常基于对应用程序所需的修改量以及它们能跟踪的事件速率。
除了典型的识别系统Bug的任务,嵌入式系统调试还旨在收集系统操作状态的信息,这些信息可以用来分析系统,寻找提高性能或优化其他重要特性(如能效、可靠性、实时响应等)的方法。
反调试
反调试是“在计算机代码中实现一种或多种技术,以阻碍逆向工程或调试目标进程的尝试”。
它在副本保护机制中被广泛使用,但也被恶意软件用来增加检测和消除的难度。常见的反调试技术包括:
- 基于API:通过系统信息检查调试器的存在
- 基于异常:检查异常是否受到干扰
- 进程和线程阻塞:检查进程和线程阻塞是否被篡改
- 修改代码:检查由调试器处理软件断点所做的代码修改
- 基于硬件和寄存器:检查硬件断点和CPU寄存器
- 时间和延迟:检查执行指令所花费的时间
- 检测并惩罚调试器:主动检测调试器并对其进行惩罚
早期的反调试例子出现在Microsoft Word的早期版本中,如果检测到调试器,程序会显示一条消息:“邪恶的树结出苦果。现在正在销毁程序磁盘。”
随后,它会让软盘驱动器发出警告声,目的是吓唬用户不要再尝试调试。
参考资料
https://zh.wikipedia.org/wiki/%E8%B0%83%E8%AF%95