弗雷德里克·布鲁克斯(Frederick P. Brooks)博士在他那篇著名的《没有银弹——软件工程中的根本和次要问题》一文中,将软件项目比作可怕的人狼(werewolves),并大胆地预言十年内不会找到特别有效的银弹。该论文发表的时间是1986年,如今整整20年过去了,尽管不时有人惊呼找到了神奇的银弹,但是冷静的人们很快发现那只是美好的愿望。
如果说软件工业中与人狼的战斗还在持续,那么在这些战役中一定会有程序员的身影,笔者也是其中的一个。我的编程生涯是从使用汇编语言编写DOS下的TSR程序开始的。今天DOS操作系统已经成为历史,在那个年代最值得炫耀的TSR技术也早已经过时了。十几年中,OWL、VFW、VDX、ISAPI、Active Movie等技术也被时间淘汰……然而,在这漫长的时间当中,我最看重的是软件调试技术。它是十几年中我学到的最有用、一直受用、而且日久弥新的一项技术。 其实就是: "软件工程"与"软件工艺"之间门派之争的问题
从软件工程的角度来讲,软件调试是软件工程的一个重要部分,软件调试过程出现在软件工程的各个阶段。从最初的可行性分析、原型验证、到开发和测试阶段、再到发布后的维护与支持,都有软件调试过程的参与。通常认为,一个完整的软件调试过程由以下几个步骤组成:
● 重现故障,通常是在用于调试的系统上重复导致故障的步骤,使要解决的问题出现在被调试的系统中。
● 定位根源,即综合利用各种调试工具,使用各种调试手段寻找导致软件故障的根源(root cause)。通常测试人员报告和描述的是软件界面或工作行为中所表现出的异常,或者是与软件需求和功能规约不符的地方,泛指软件缺欠(defect)或者故障(failure)。而这些表面的缺欠总是由于一或多个内在因素所导致的。这些内因要么是代码的行为错误,要么是不行为错误(该作而未作)。
● 探索和实现解决方案,即根据寻找到的故障根源、和资源情况、紧迫程度等要求设计和实现解决方案。
● 验证方案,在目标环境中测试方案的有效性,又称为回归(regress)测试。如果问题已经解决,那么就可以关闭问题。如果没有解决则回到第3步调整和修改解决方案。
这些步骤中,定位根源常常是最困难也是最关键的步骤,它是软件调试过程的核心和灵魂。如果没有找到故障根源,那么解决方案便很是隔靴搔痒,或者头痛医脚,白白浪费了时间。
对软件调试的另一种更通俗的解释是指使用调试工具求解各种软件问题的过程,例如跟踪软件的执行过程,探索软件本身或者与其配套的其它软件或者硬件系统的工作原理等,这些过程的目的有可能是为了去除软件缺欠,也可能不是。
在了解了软件调试技术的基本概念以后,下面我们来看一下支撑软件调试技术的几种基本机制。
● 断点:即当被调试程序执行到某一空间或时间点时将其中断到调试器中。根据中断条件分为如下几种:
○ 代码断点:当程序执行到指定内存地址的代码时中断到调试器。
○ 数据断点:当程序访问指定内存地址的数据时中断到调试器。
○ I/O断点:当程序访问指定I/O地址的端口时中断到调试器。
根据断点的设置方法,断点又分为软件断点和硬件断点。软件断点通常是通过向指定的代码位置插入专用的断点指令来实现的,比如IA32 CPU的INT 3指令(机器码为0xCC)就是断点指令。硬件断点通常是通过设置CPU的调试寄存器来设置的。IA32 CPU定义了8个调试寄存器,DR0~DR7,可以最多同时设置4个硬件断点(对于一个调试会话)。通过调试寄存器可以设置以上三种断点中的任一种,但是通过断点指令只可以设置代码断点。
● 单步跟踪:即让应用程序按照某单位一步步执行。根据单位,又分几种:
○ 每次执行一条汇编指令,称为汇编语言一级的单步跟踪。设置IA32 CPU标志寄存器的TF(Trap Flag,即陷阱标志位)位,便可以让CPU每执行完一条指令便产生一个调试异常(INT 1),中断到调试器。
○ 每次执行源代码(比汇编语言更高级的程序语言,如C/C++)的一条语句,又称为源代码级的单步跟踪。通常高级语言的单步跟踪是通过反复设置CPU的陷阱标志位来实现的,如果当前源代码行还没有执行完,那么调试器重新设置陷阱标志并让程序继续执行,直到该语句结束(EIP指向另一语句)才中断给用户。
○ 每次执行一个程序分支,又称为分支到分支单步跟踪。设置IA32 CPU的DbgCtl MSR寄存器的BTF(Branch Trap Flag)标志后,便可以启用分支到分支单步跟踪。
○ 每次执行一个任务(线程),即当一个任务(线程)被调度执行时中断到调试器。IA32架构所定义的任务状态段(TSS)中的T标志为实现这一功能提供了硬件一级的支持,但是很多调试器还有提供这项功能。
● 栈回溯(stack backtrace):即通过记录在栈中的函数返回地址显示(追溯)函数调用过程。在将返回地址翻译成函数名时需要有调试符号(debug symbol)的支持。大多数编译器都支持在编译时生成调试符号。微软的调试符号服务器(http://msdl.microsoft.com/download/symbols)提供了大多数Windows系统文件的调试符号,是调试和学习Windows操作系统的宝贵资源。
● 调试信息输出(debug output/print):即将程序运行的位置、变量状态等信息输出到调试器、窗口、文件或者其它可以观察到的地方。这种方法的优点是简单方便、不依赖于调试器,但也有明显的缺点,如效率低,安全性差,通常不可以动态开启,且难以管理等。在Windows操作系统中,驱动程序可以使用DbgPrint/DbgPrintEx来输出调试信息,应用程序可以调用OutputDebugString API。
● 日志(log):将程序运行的状态信息写入到特定的文件或者数据库中。Windows操作系统提供了记录、观察和管理(删除和备份)日志的功能。Windows Vista新引入了名为Common Log File System(CLFS.SYS)的内核模块,用于进一步加强日志功能。
● 事件追踪(event trace):通常用来监视频繁的复杂的软件过程,满足普通日志机制难以胜任的需求。比如监视大信息量的文件操作、网络通信等。ETW(Event Trace for Windows)是Windows操作系统内建的事件追踪机制,Windows内核本身和很多Windows下的软件工具(如Bootvis,TCP/IP View)都使用了该机制。
在以上机制中,断点和单步跟踪通常必须在有调试器参与的情况下才能使用。调试器(software debugger)是综合提供各种调试功能的软件工具。除了处理断点、单步跟踪、模块映射等调试事件外,调试器通常还提供如下功能:
● 观察和编辑被调试程序的内存和数据,如全局变量、局部变量、以及程序的栈和堆等重要数据结构。
● 观察和反汇编被调试程序的代码。
● 显示线程栈中的函数调用信息。
● 管理调试符号。
● 控制进程和线程,例如将被调试程序中断到调试器中,和恢复其执行等。
根据调试器所调试目标程序的工作模式,可以把调试器分为用户态调试器和内核态调试器,前者用于调试用户态下的各种程序(应用程序、系统服务、或者用户态的DLL模块),后者用于调试工作在内核模式的程序,如驱动程序和操作系统的内核部分。WinDbg是微软开发的一个免费调试器,它既可以用作用户态调试器,也可以用作内核态调试器,是调试Windows操作系统下的各种软件的一个强有力工具。我几乎每天都使用WinDbg,它是我的计算机中使用频率最高的软件之一。
最后,简要地描述一下软件调试技术的几个特征。
系统性——很多看似简单的调试机制都是依靠系统内的多个部件协同工作而完成的。以软件断点为例,CPU提供了指令支持和硬件级的异常机制,操作系统将异常以调试事件的形式分发给调试器,调试器响应调试事件并与用户交互。如果在做源代码级的调试,那么调试器又需要编译器所产生的调试符号来帮忙。
全局性——对于一个软件项目,应该在项目的设计和架构阶段就制定出全局的调试支持机制,并贯彻实施。比如,所有模块都应该使用统一的方法来输出调试信息、记录日志、报告错误,并公开统一的接口用做单元测试和故障诊断。这样不仅可以避免重复工作,而且增加了软件的可调适性(debuggability),有利于保证产品的质量和进度。
困难性——《C语言编程》一书的作者Brian Kernighan曾经说过,“调试天生就比编写代码难上一倍,如果你写出了最聪明的代码,那么你的智商就不足以调试这个代码。”因为,要调试一个程序,就必须深刻理解它的工作原理,不仅要知道how和表层的东西,还要知道why和深层次的内幕。另外,调试需要锲而不舍的探索精神和坚韧的耐力,这也让很多人望而却步。
综上所述,软件调试技术是与软件开发密不可分的一门技术,其初衷是为了定位和去除软件故障,但因为调试技术所具有的对软件的强大控制力和观察力,其应用早已延伸到了很多其它领域,比如逆向工程、计算机安全等等。
1 大前提:软件活动包含根本任务和次要任务
布鲁克斯指出:所有软件活动包括:
根本任务——打造构成抽象软件实体的复杂概念结构;
次要任务——使用编程语言表达这些抽象实体,并在时间和空间内将它们映射成机器语言。
2 小前提:现有解决方案致力于解决次要任务
考察和评估几乎现有所有的软件工程解决方案,布鲁克斯指出:现有所有方案全都在致力于解决软件工程中的次要问题。
3 结论:没有银弹
无论这些方案多么完善,都不可能在根本上解决问题,即使将全部次要任务的时间缩减到零,也不会带来生产率数量级上的提高。
应用孤岛问题:管理软件如何实施、应用的问题
由于基础架构的先天不足,现有的信息系统,无论是独立的工具软件还是集成的解决方案,大多是孤岛型的应用,或者是业务功能模块的简单累加。这些孤岛系统,相互之间是孤立封闭的
IT黑洞问题:管理软件如何设计、开发和维护的问题
现有信息系统的建设,是在底层的技术平台上直接构建业务系统,并且也只能采用面向技术的、业务无关的“原始”编程工具来开发管理系统 。这种低层次的软件开发模式,使信息系统的开发、维护和扩展困难重重,导致IT黑洞现象的普遍发生