Linux环境下通过GDB调试C项目实战
通过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个的值会不太一样)
其实,写出这样程序会造成十分严重的错误,但这种错误又非常隐蔽,难以发现以及调试。这里有个简单的例子:
#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具体实现代码,就会发现其中函数调用时出现的数组越界,这样就会导致缓冲区泄露,可能会修改内存,造成不可知的错误,这样是最可怕的,因为无法准确预料到,后续会产生难以估计的错误**
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 企业是如何从头开发一个商业项目的?
- 基于Haproxy的高可用实战
- 组复制常规操作-分布式恢复 | 全方位认识 MySQL 8.0 Group Replication
- 赞!7000 字学习笔记,MySQL 从入到放弃
- 面试官问我Volatile的原理?从操作系统层面的设计怼回去!
- 设计原则之单一职责
- 设计原则之开闭原则
- SpringBoot执行跨域处理
- SpringBoot对全局异常的处理封装
- 自定义springboot-starter揭秘自动配置骚操作
- 【大厂面试题】Redis中是如何实现分布式锁的?
- 最近公司招人,研发组商量了下,暂时定下这么多java面试题!
- 市面上数据库种类那么多,如何选择?
- 玩转正则!推荐一个速查、调试、验证、可视化工具
- 当一个http请求来临时,SpringMVC究竟偷偷帮你做了什么?