通过GDB调试找到程序的bug
请查看位于https://github.com/xmu-Linux101/Linux101/tree/201720182/experiments/gcc-5-gdb的代码
这个是向量加法的程序,但是有一些小bug,请通过GDB调式工具找出具体的bugs。
调式过程请尽量使用截图工具保留下来,便于评判。
提交PDF实验报告。
前情回顾:
编译过程可分为四个阶段:
- 预处理(Pre-Processing)
- 编译(Compiling)
- 汇编(Assembling)
- 链接(Linking)
调试选项
默认情况下,gcc在编译时不会建个调试符号插入到生成的二进制代码中,如果需要生成调试符号信息,可以使用gcc -g选项,一般不加调试选项,否则会使代码增大。
1.设置断点 2.单步执行程序,便于调试 3.查看程序中变量值的变化 4.动态改变程序的执行环境 5.分析崩溃程序产生的core文件
以上这些就是这次实验的前置知识,需要我们采用gdb调试器来找出一些程序的bug
首先看一下这个程序的目录结构:
我们可以看到文件的目录结构是一个典型的C语言项目架构:Makefile,include文件夹下是预先定义好的库函数,粗看文件结构应该可以想到array.c是一个具体实现函数功能的文件,main.c则是总的主函数,进行测试编写的代码功能是否正常执行
在找这个项目的bug之前我们必须确认一下Makefile的内容是否有逻辑错误或者语法错误,这样才能保证我们后期的调试没有问题
输入vi Makefile,我们看到:
这个Makefile中的几条命令大致为:
make clean:清除已经存在的result可执行文件
make/make result:将已经得到的可执行文件main.o与array.o链接成可执行文件result,不开启O2优化或采用O0优化,在此之前将main.c和array.c分别编译成可执行文件main.o和array.o
make_clean:清除已经存在的main.o可执行文件
array_clean:清除已经存在的main.o可执行文件
array:清除已经存在的array.o可执行文件并编译array.c生成array.o文件
main:清除已经存在的main.o可执行文件并编译mian.c生成main.o文件
main_optimize:编译mian.c生成main.o文件,开启O2优化(该优化选项会牺牲部分编译速度,除了执行-O1所执行的所有优化之外,还会采用几乎所有的目标配置支持的优化算法,用以提高目标代码的运行速度.)
array_optimize:编译mian.c生成main.o文件,开启O2优化(该优化选项会牺牲部分编译速度,除了执行-O1所执行的所有优化之外,还会采用几乎所有的目标配置支持的优化算法,用以提高目标代码的运行速度.)
make diff:观察生成的不加优化的版本代码和加入O2优化的release版本代码执行结果,查看区别
观察可得:Makefile并未存在任何语法错误、以及逻辑上的错误,初步排查断定是在代码实现上出了问题
在确定了Makefile没有大问题之后,我们采用gdb调试器来调试,首先gdb ./main启动调试器,list 查看代码:
我们观察可以得到,这是一个实现了创建两个一维向量(长度都为16)、并且将他们相加,最后输出相加结果的程序,更进一步,我们在第11行设置断点,display i和array_a[i]的信息:
我们可以看到,函数入口array_fill_with(int *array, int length, int fillWith)是有这三个参数,但是在实现代码中,length是其定义的数组长度,但是在循环中for(int i=0;i<=length;i )竟然写成了<=length,这样就会导致执行到array[length]=fillWith这条语句,那这是什么意思呢,在C语言中,定义一个数组,array[length],那么我们可以使用的有效元素范围就只有0~length-1,但是在这里的话就属于很严重的数组越界,也就是我们这里常说的未定义行为,但是到这里,我们还不能完全确定是否程序中就只有这个错误,我们还需要检查所有其他的代码才能确定:
查看include文件夹下的预定义函数:
没啥问题,ok,下一个
主要的array.c,启动gdb调试器:
查看完毕,果真和之前初步调试的一样,在array_add和array_fill两个函数里面都涉及到段错误,数组越界,length被取等号,但为什么没有发生报错或者错误终止程序是因为在最后的print函数里面只涉及到了正常的0~array.lenth-1的范围,当然如果将print函数里面也改成length取等号的话,很有可能最后一个元素(即第17个的值会不太一样)
其实,写出这样程序会造成十分严重的错误,但这种错误又非常隐蔽,难以发现以及调试。这里有个简单的例子:
代码语言:javascript复制 #include <stdio.h>
int main()
{
int i, a[10];
for(i = 1; i <= 10; i)
a[i] = 0;
return 0;
}
你看到这个程序,真的会输出是11个0吗,但其实它运行起来是死循环,这就是C语言中数组越界带来的巨大隐患:
数组中的下标从0开始。
那么在上面代码中只能访问:a[1]、a[2]、a[3]、a[4]、a[5]、a[6]、a[7]、a[8]、a[9]
i自加到10时,a[10]属于数组下标越界,在C语言立,它会这样处理,对越界空间进行操作,破坏原有数据。访问之后程序会破坏内存原有数据,导致缓冲区泄露,并且发生不可预知的错误(在这里则是将i的内存地址和a[10]绑定起来,相当于每次修改a[10]的时候就顺便将i置为0,这样就会导致死循环)
总结来说:这个项目运行起来没有问题,看起来让人放心,但是,仔细去调试它的array.c具体实现代码,就会发现其中函数调用时出现的数组越界,这样就会导致缓冲区泄露,可能会修改内存,造成不可知的错误,这样是最可怕的,因为无法准确预料到,后续会产生难以估计的错误
让人放心,但是,仔细去调试它的array.c具体实现代码,就会发现其中函数调用时出现的数组越界,这样就会导致缓冲区泄露,可能会修改内存,造成不可知的错误,这样是最可怕的,因为无法准确预料到,后续会产生难以估计的错误**