第十章 调试
所有的软件都会存在缺陷,通常每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程序可以执行任何奇怪的行为。正如我们将会看到的,错误类型就落入了未定义行为的类别。
分享到:
相关推荐
实验目的:设计、编制并调试一个词法分析程序,加深对词法分析程序的理解。 2、实验要求: (1)能识别关键字、运算符号、界符、标识符、数字(无符号整数、实数(4分)、科学计数法表示的数(5分)) (2)要求建立...
1.编写并调试一个模拟的进程调度程序,采用“最高优先数优先”调度算法对五个进程进行调度。 2、用“简单轮转法调度算法”实现第一题
标题提到的是“java远程调试一个朋友推荐”,这表明文章主要关注的是如何通过特定的技术或工具来进行Java应用的远程调试。描述部分进一步强调了这一主题,并表达了作者希望将这些信息分享给需要帮助的人的愿望。 ##...
sd卡调试一册通
例如,如果你正在调试一个Web服务的崩溃,可以利用WinDbg分析服务进程,查看HTTP请求处理时的堆栈信息,找出导致问题的代码行。 WinDbg的强大之处还在于其可扩展性,通过编写扩展脚本(如C++或Python),我们可以...
例如,可以使用 GDB 调试一个简单的问候程序,展示了 GDB 的典型应用。该程序使用 `greeting` 名称,显示一个简单的问候,并将其反序列化列出。 GDB 是 Linux 系统中的一款强大调试工具,提供了多种功能和命令,...
例如,在调试一个可能抛出NullPointerException的程序时,可以设置一个异常断点,只有当程序抛出NullPointerException时,程序才会停止。 3. 观察点 观察点是Eclipse中的一种断点类型,它可以在程序访问或修改某个...
例如,如果你正在调试一个复杂的通信协议,可能需要Commix或UartAssist这样功能全面的助手;如果只是简单地测试串口连接,dnw串口调试助手或SScom32就足够了。无论选择哪一款,都应确保了解其基本操作,以充分利用其...
例如,如果你正在调试一个UDP服务,可以输入目标主机和端口,发送测试数据包,然后观察返回的结果。对于TCP服务,可以尝试建立连接,查看连接是否稳定,传输数据是否正常。 总的来说,`mNetAssist`是Ubuntu用户在...
首先,为了调试一个程序,必须确保它包含调试信息。在VC环境下,通常使用Debug Configuration来自动包含这些信息。然而,调试信息不仅限于Debug版本,开发者可以在任何Configuration中添加,包括Release版本。添加...
- **调试新进程**:当需要调试一个新创建的进程时,可以直接使用GDB启动程序,并在其后执行`run`命令来运行该程序。例如,在终端中输入`gdb your_program`启动GDB,然后输入`run`开始运行程序。这种方式适用于调试...
传统的调试方法存在诸多问题,如每次只能调试一个板卡上的芯片,需反复拔插物理链路,效率低下,且无法实时监控整个系统状态。 针对这些问题,文中提出的源代码级调试系统旨在支持处理器集群中的所有计算核心同时...
例如,当需要调试一个温湿度传感器时,可以通过串口助手发送命令获取数据,然后在界面上查看返回的温度和湿度值,从而判断传感器工作是否正常。同样,如果在设计一个遥控系统,串口助手也可以帮助验证控制指令的正确...
例如,在调试一个嵌入式设备的串口通信功能时,开发者可以通过该工具发送特定指令,观察设备响应,从而判断设备功能是否正常。 总结,友善串口调试助手以其强大而实用的功能,成为串口和网口调试的得力助手。通过...
例如,为了调试一个USB设备,开发者可能需要编写或修改相应的USB驱动,然后利用像GDB这样的工具来调试驱动代码,同时使用"SimpleHIDWrite.exe"来测试设备的输入和输出功能。 总的来说,"里仁教育"提供的这两种调试...
通过列举linux平台下的例子,并结合gdb描述了堆栈溢的过程。
matlab 调试技巧1.直接调试: (1)去掉句末的分号;...(2)单独调试一个函数:将第一行的函数声明注释掉,并定义输入量,以脚本方式执行 M 文件; (3)适当地方添加输出变量值的语句; (4)添加keyboard命令;
有人串口调试助手和网络调试助手合二为一,特别适合调试网络设备。 2. 支持中文和英文双语言,再也不用愁找不到合适的串口调试软件给国际客户用了。 3. 最小化时停留在右下角,不占用任务栏位置,需要时一键调入。 4...
当我们想要使用 VS Code 来调试一个 Node.js 应用程序时,我们需要设置以下内容: 一个调试配置对象。 配置对象的类型必须是 node。 配置对象的请求类型必须是 launch,表示我们要启动一个新的程序来进行调试。 ...
21ic侃单片机有奖征文活动《我的一次调试经历》合集中,汇集了众多工程师的真实调试故事,这些经验分享为我们提供了宝贵的实战参考。 首先,单片机调试的基本方法包括使用串口通信、仿真器、JTAG接口、ISP在线编程...