第十章 调试
所有的软件都会存在缺陷,通常每100行代码就会存在2到5个缺陷。这些错误通常会使得程序和库并不会预期的表现,通常会使得一个程序的行为并不会如预想的那样。Bug跟踪,标识以及修复会占用程序软件开发过程中的大量时间。
在这一章,我们讨论软件缺陷,并且会考虑一些工具与技术用于跟踪特定的错误行为。这不同于测试(在各种条件下验证程序行为的任务),尽管测试与调试是相关联的,而且许多bug就是在测试过程中发现的。
我们会讨论下列主题:
错误类型
通常的调试技术
使用GDB与其他工具进行调试
断言
内存使用调试
错误类型
bug通常是由下列一些原因引起的,而其中的每一个都指出一个检测与修复的方法:
规范错误:如果一个程序没有进行正确的规范,毫无疑问,这个程序并不会表现出预期的行为。即使是世界上时优秀的程序员有时也会编写出错误的程序。在我们开始编程(或是设计)之前,要保证我们清楚的知道与了解我们的程序需要做什么。我们可以通过查看需求和与使用程序的用户所达成的协议来检测与修复许多(如果不是所有)的规范错误。
设计错误:任何规模的程序都需要创建之前进行设计。通常坐在电脑前,直接输入源码,并且希望程序第一次就正确工作,这样是不够的。我们需要花些时间来考虑如何组织我们的程序,我们需要使用哪些数据结构,以及如何使用他们。试着进行详细的设计,因为这样以后就可以省去许多重新编写的痛苦。
编码错误:当然,每个人都会出输入错误。由我们的设计创建源代码的过程是一个不完美的过程。这也是许多bug滋生的地方。当我们在程序中遇到一个bug时,不要忽视简单重读源代码或是请其他人来阅读源代码从而修复bug的可能性。令人惊奇的一件事是就通守与其他人讨论实现我们可以检测并修复许多bug。
试着在纸上执行程序核心,这个过程被称之为干运行(dry running)。对于许多重要的例程,一步步写下输入的值并且计算输出。我们并不必须使用计算机进行调试,而且有时就是计算机引起的问题。即使是那些编写库,编译器,以及操作系统的人也会出错。另一方面,不要急于责备工具;很有可能是在一个新的程序中存在bug,而不是存在于编译器中。
通常的调试技术
有许多不的方法可以用来调试与测试一个通常的Linux程序。我们通常运行程序并且查看发生了什么。如果程序不能工作,我们需要决定对其做些什么。我们可以修改程序并且再次运行,我们可以尝试获得程序内部运行的更多信息,或是我们可以直接监视程序的运行。调试的五个步骤为:
测试:发现存在哪些缺陷或是bug
稳定化:使得bug重新出现
本地化:标识相关的代码行
修正:修正代码
验证:保证修正正常工作
一个带有bug的程序
下面我们来看一下带有bug的程序。在本章的讨论中,我们将会尝试对其进行调试。这个程序是在一个大型软件系统的开发过程中编写的。其目的就是测试一个函数,sort,其作用是在一个item类型的结构数组上实现一个冒泡排序算法。这些项目以其成员key升序的顺序进行排列。这个程序在一个例子数组上调用sort进行测试。在实际的工作中我们绝不会使用这种排序算法,因为其效率实在是太低了。我们在这里使用他是因为他很短小,理解相对简单,而且很容易出错。事实上,标准C库具有一个名为qsort的函数可以实现所要求的任务。
不幸的是,代码很难阅读,没有注释,而且原始程序也不可得了。我们不得不自己与其挣扎,我们由基本的例程debug1.c开始。
/* 1 */ typedef struct {
/* 2 */ char *data;
/* 3 */ int key;
/* 4 */ } item;
/* 5 */
/* 6 */ item array[] = {
/* 7 */ {“bill”, 3},
/* 8 */ {“neil”, 4},
/* 9 */ {“john”, 2},
/* 10 */ {“rick”, 5},
/* 11 */ {“alex”, 1},
/* 12 */ };
/* 13 */
/* 14 */ sort(a,n)
/* 15 */ item *a;
/* 16 */ {
/* 17 */ int i = 0, j = 0;
/* 18 */ int s = 1;
/* 19 */
/* 20 */ for(; i < n && s != 0; i++) {
/* 21 */ s = 0;
/* 22 */ for(j = 0; j < n; j++) {
/* 23 */ if(a[j].key > a[j+1].key) {
/* 24 */ item t = a[j];
/* 25 */ a[j] = a[j+1];
/* 26 */ a[j+1] = t;
/* 27 */ s++;
/* 28 */ }
/* 29 */ }
/* 30 */ n--;
/* 31 */ }
/* 32 */ }
/* 33 */
/* 34 */ main()
/* 35 */ {
/* 36 */ sort(array,5);
/* 37 */ }
我们试着编译这个程序:
$ cc -o debug1 debug1.c
编译成功,没有错误或是警告报告。
在我们运行这个程序之前,我们需要添加一些代码来输出结果。否则,我们就不知道程序是否进行了工作。我们会添加一些额外的代码在排序结束之后显示数组。我们称这个新版本为debug2.c。
/* 34 */ main()
/* 35 */ {
/* 36 */ int i;
/* 37 */ sort(array,5);
/* 38 */ for(i = 0; i < 5; i++)
/* 39 */ printf(“array[%d] = {%s, %d}\n”,
/* 40 */ i, array[i].data, array[i].key);
/* 41 */ }
/* 34 */ main()
/* 35 */ {
/* 36 */ int i;
/* 37 */ sort(array,5);
/* 38 */ for(i = 0; i < 5; i++)
/* 39 */ printf(“array[%d] = {%s, %d}\n”,
/* 40 */ i, array[i].data, array[i].key);
/* 41 */ }
严格来说这些额外的代码并不算是程序修正的一部分。我们添加这些代码仅是为测试。我们必须非常小心不要在我们的测试代码中引入额外的bug。现在再次编译并且运行程序。
$ cc -o debug2 debug2.c
$ ./debug2
当我们这样做时发生了什么依赖于我们的Linux平台以及我们所进行的设置。在作者的系统上,我们会得到下面的输出信息:
array[0] = {john, 2}
array[1] = {alex, 1}
array[2] = {(null), -1}
array[3] = {bill, 3}
array[4] = {neil, 4}
但是在另一个作者的系统(运行一个不同的内核),我们会得到下面的信息:
Segmentation fault
在我们的Linux系统上,我们会看到其中的一个信息或是另一上不同的结果。我们希望得看到下面的信息:
array[0] = {alex, 1}
array[1] = {john, 2}
array[2] = {bill, 3}
array[3] = {neil, 4}
array[4] = {rick, 5}
很明显,在代码中存在一个严重的问题。如果这个程序可以运行,那么他就不能对数组进行正确的排序,而如果程序结束并提示内存错误,那么是系统向程序发送了一个信号表明系统已经检测到一个非法的内存访问并且提前结束了程序的运行以防止内存被破坏。
操作系统检测非法内存访问的能力依赖于其硬件的配置以及内存管理系统的精巧实现。在大多数系统上,由操作系统分配给程序的内存远大于实际正在使用的内存。如果非法内存访问发生了这块内存区域,硬件也许就不能检测非法访问。这就是为什么并不是所有的Linux版本以及Unix产生内存错误的原因。
注:一些库函数,例如printf,也会阻止某些条件下的非法访问,例如使用一个空指针。
当我们跟踪数组访问问题时,通常增加数组元素的数量是一个好主意,因为这会增加错误数。如果我们读取超过数组字节结束处一个字节,我们也许就会消耗掉这些内存,因为分配给程序的内存将会达到操作系统特定的边界,通常为8K。
如果我们增加数组元素的数量,在这个例子中可以通过修改item成员data为一个4096字符的数组来做到,对于不存在的数组元素的访问也许就会是超出已分配的内存地址。每一个数组元素为4K大小,所以我们非正常使用的内存可以为0到4K。
如果我们这样修改,并将其结果称之为debug3.c,我们就会在两个作者的Linux版本上得到内存错误的信息。
/* 2 */ char data[4096];
$ cc -o debug3 debug3.c
$ ./debug3
Segmentation fault (core dumped)
也有可能某些Linux或是Unix版本仍然不会产生内存错误信息。当ANSI C标准检测到未定义行为时,他会允许程序执行任何动作。当然看上去似乎是我们编写了一个非正常的C程序,而一个非正常的C程序可以执行任何奇怪的行为。正如我们将会看到的,错误类型就落入了未定义行为的类别。
分享到:
相关推荐
用高级语言编写和调试一个简单的文件系统,模拟文件管理的工作过程。
1.编写并调试一个模拟的进程调度程序,采用“最高优先数优先”调度算法对五个进程进行调度。 2、用“简单轮转法调度算法”实现第一题
sd卡调试一册通
编写并调试一个模拟的进程调度程序,采用“轮转法”调度算法对五个进程进行调度。 轮转法是简单轮转法。 简单轮转法的基本思想是:所有就绪进程按 FCFS排成一个队列,总是把处理机分配给队首的进程,各进程占用CPU...
为了提高工作效率,一款名为“二合一串口网络调试助手”的工具应运而生,它巧妙地将串口(Serial Port)调试功能与网络(Network)调试功能融合在一个软件之中,极大地简化了工程师的工作流程。 首先,让我们深入...
调试一个程序通常涉及两个主要步骤: 1. **使用调试标志启动进程**:通过指定`DEBUG_ONLY_THIS_PROCESS`或`DEBUG_PROCESS`标志来启动一个进程。 2. **设置调试器循环**:处理调试事件的循环。 ##### A. 使用调试...
例如,如果你正在调试一个Web服务的崩溃,可以利用WinDbg分析服务进程,查看HTTP请求处理时的堆栈信息,找出导致问题的代码行。 WinDbg的强大之处还在于其可扩展性,通过编写扩展脚本(如C++或Python),我们可以...
例如,在调试一个可能抛出NullPointerException的程序时,可以设置一个异常断点,只有当程序抛出NullPointerException时,程序才会停止。 3. 观察点 观察点是Eclipse中的一种断点类型,它可以在程序访问或修改某个...
编写并调试一个单道处理系统的作业等待模拟程序。 作业等待算法:分别采用先来先服务(FCFS),先来先服务(FCFS),响应比高者优先(HRN)的调度算法。 对每种调度算法都要求打印每个作业开始运行时刻、完成...
例如,如果你正在调试一个UDP服务,可以输入目标主机和端口,发送测试数据包,然后观察返回的结果。对于TCP服务,可以尝试建立连接,查看连接是否稳定,传输数据是否正常。 总的来说,`mNetAssist`是Ubuntu用户在...
VC调试入门.doc 调试Release版本应用程序.doc 调试技巧之调用堆栈 - Call stack.doc 如何查看MFC源码.doc Release调试.doc VC++中使用Disassembly查看代码.doc 细谈VC程序调试的若干方法.doc
【标题】:“利达调试码助手一天” 【描述】:“利达调试码助手一天”这款工具,正如其名,是专为程序员设计的一款高效、实用的调试辅助软件,旨在优化和提升开发人员在一天工作中的代码调试效率。通过提供一系列...
- 要调试一个C语言程序,首先要创建或附加到目标进程。使用CreateProcess函数可以创建一个新的进程,AttachToProcess则可以附加到已存在的进程。 4. **设置断点**: - 在C语言代码中,断点通常是在特定指令地址处...
然而,随着处理器核心数量的增加,众核处理器中每个核心能够设置的硬件断点数量有限,这就意味着当程序员需要调试一个大规模或多线程的程序时,可能会遇到断点数量不足的问题。为了解决这一问题,作者们提出了一种...
- **调试新进程**:当需要调试一个新创建的进程时,可以直接使用GDB启动程序,并在其后执行`run`命令来运行该程序。例如,在终端中输入`gdb your_program`启动GDB,然后输入`run`开始运行程序。这种方式适用于调试...
传统的调试方法存在诸多问题,如每次只能调试一个板卡上的芯片,需反复拔插物理链路,效率低下,且无法实时监控整个系统状态。 针对这些问题,文中提出的源代码级调试系统旨在支持处理器集群中的所有计算核心同时...
特别是在没有IDE的情况下,或者你需要调试一个不包含源代码的二进制文件时,DebugView能提供一个便捷的解决方案。 总结来说,DebugView是一个强大的辅助调试工具,通过捕获`OuputDebugString`函数输出的调试信息,...
7.0之后的版本支持线程non-stop模式,允许其他线程在调试一个线程时继续运行。 - 设置断点对所有线程有效,但如果只想针对特定线程设置断点,可以使用“break ... thread all”或者“break ... if thread==n”。 6....
例如,当需要调试一个温湿度传感器时,可以通过串口助手发送命令获取数据,然后在界面上查看返回的温度和湿度值,从而判断传感器工作是否正常。同样,如果在设计一个遥控系统,串口助手也可以帮助验证控制指令的正确...
例如,在调试一个嵌入式设备的串口通信功能时,开发者可以通过该工具发送特定指令,观察设备响应,从而判断设备功能是否正常。 总结,友善串口调试助手以其强大而实用的功能,成为串口和网口调试的得力助手。通过...