一、前言
- 学习了【vim】知道了如何编辑一个代码文本
- 学习了【gcc】知道了如何编译一个代码文本
- 学习了【make/Makefile】知道了如何自动化构建一个代码文本
但是如何对一段代码去进行调试呢,此时就要使用到Linux下的调试器gdb
了。对于这个调试器来说,不像是VS中那样的图形化界面形式,而是采用纯命令行的形式进行调试。可能我的讲解会比较晦涩难懂,在学习的过程中主要是会一些gdb下基本的操作即可
二、调试版本与发布版本
1、见见gdb
下面是本次调试所要使用到的代码
1 #include <stdio.h> 2 3 int AddToTop(int top) 4 { 5 printf("Enter AddToTop\n"); 6 7 int count = 0; 8 for(int i = 1;i <= top; ++i) 9 { 10 count += i; 11 } 12 13 printf("Quit AddToTop\n"); 14 return count; 15 } 16 17 int main(void) 18 { 19 int top = 100; 20 int ret = AddToTop(top); 21 22 printf("ret = %d\n", ret); 23 return 0; 24 }
下面是Makefile中的内容,用于自动化编译
1 mytest:test.c 2 gcc -o mytest test.c -std=c99 3 .PHONY:clean 4 clean: 5 rm -rf mytest
注:-std=c99
表示以c99的标准来编译代码
- 如果要进入gdb开始调试,那直接
gdb + 可执行程序
即可 - 不过进去之后发现似乎有一些奇怪的内容,【no debugging symbols found】,翻译过来就是没有调试信息。那这是为何呢?是gdb出问题了吗?
- 先不要着急,如果有经常调试的通过就可以知道只有在【DeBug】的环境下才会有我们想要的调试信息,所以可以初步推断这可能不是一个【DeBug】版本的可执行程序
- 先使用
q(quit)
退出gdb
让我们先看下去,了解一下其他的知识再来解决这个问题
2、程序员与测试人员
接下去我们就来说说有关【DeBug】和【Release】版本的不同之处
📚【Debug】—— 调试版本📚【Release】—— 发布版本
- 在使用VS的时候我们可以直接使用鼠标来进行操作,当前程序以DeBug或者是Release的形式进行运行,那么运行出来的可执行程序版本也是不同的,我们程序员在编写代码后运行一般是使用【DeBug】环境进行运行。因为在企业里写软件项目,将代码写完后==程序员自己要做简单的测试,保证代码没有问题==
- 当程序员自己测试完没有问题之后,就会将这个可执行程序给到
测试人员
进行测试,而且会给出自己的单元测试报告。对于测试人员来说所处的模式是【Release】,也就是将来客户要使用的这款软件的发布版本 - 当测试在测的过程中,一定会发现一些问题。此时测试人员就会把报告再打回研发部。研发部做修改重新生成Release版本的可行性程序给到测试人员继续测试
- 最后只有当测试通过了,再将生成的【单元测试报告】与产品经理进行核对之后没有问题,那这个软件才可以真正地面向市场👉
3、为什么Release不能调试但DeBug可以调试❓
其实对于我们刚才直接make自动化生成的可执行程序是通过gcc直接编译产生得到的,它是一个【Release】版本的可执行程序,因此无法进行调试
- 若是我们想要使用
gcc/g++
去生成一个可执行程序时,默认是【Release】版本的,而不是【DeBug】 - 但若是我们想要去生成一个【DeBug】版本的可执行程序也是可以的,只需要修改一下我们的Makefile即可,给gcc后面带上一个
-g
的命令选项,此时再去make一下的话生成的就是【DeBug】版本的了
- 为了之前的【Release】版本不被覆盖,我们将其重命名一下为
mytest-release
- 在生成【DeBug】版本后一样对其进行一个重命名为
mytest-debug
- 通过观察上图中两个可执行文件的大小便可以发现虽然它们都是可执行程序,但是容量大小却不一样,这是为什么呢❓
- 因为以Release版本发布的软件是给客户的,客户是不需要调试信息的
- 往可执行程序里添加很多的调试信息意味着软件的体积会变大
- 一方面,用户下载需要时间了
- 另一方面,用户下载好之后将软件启动、运行都需要更多的时间,体验不好。一般能不加就不加
- 但是对于DeBug来说会自动加调试信息,容量体积比Release大
但是就这么说说太抽象了,我们得看看这个可执行文件里的调试信息究竟是怎样的💻
readelf -S mytest-debug
- 在C生万物 | 程序环境和预处理中,我有说到过对于【可执行文件】它是一个二进制文件,若是查看它的源码就可以发现里面都是一堆乱码
当我们面对一堆二进制乱码措手不及的时候,给大家提到过一个东西叫做readelf
,其实它是Linux中的一个指令,可以用来读取【elf】格式的文件
- 加上
-S
的命令选项以==符号表==的形式来进行读取这个文件,就以一个列表的形式展现出了这个带调试信息的可执行程序 - 下面展现几个比较常见的。例如这里的
.rodata
就是read only data即只读全局数据区.data
就是已初始化全局数据区.bss
就是未初始化全局数据区
- 不过这些呢是这个可执行文件中的所有内容,若是我们只是先要查看一些debug的调试信息,就要对这些东西进行一个筛选才行
- 此时就可以使用到
grep
命令来进行一个筛选。便可以查看到所有的debug调试信息了
- 上面是查看【DeBug】版本下的调试信息,在【Release】版本有没有呢
- 我们也是读取并搜寻一下这个文件便可以发现对于【Release】版本来说是不存在调试信息的,所以什么都没有被打印出来
【总结一下】
- 程序的发布方式有两种,debug模式和release模式
- Linux gcc/g++出来的二进制程序,默认是release模式
- 要使用gdb调试,必须在源代码生成二进制程序的时候, 加上
-g
选项
好,说到这里,对于调试相关背景就全部讲完了,接下去我们正式进入【gdb】的学习:keyboard:
三、使用gdb调试代码
1、指令集汇总
因为这个调试器是在Linux环境下的,是纯命令行模式,所以会有很多的指令,做好心里准备:cry:
注:()括号里面是该指令的全称
l(list) 行号/函数名
—— 显示对应的code,每次10行r(run)
—— F5【无断点直接运行、有断点从第一个断点处开始运行】b(breakpoint) + 行号
—— 在那一行打断点b 源文件:函数名
—— 在该函数的第一行打上断点b 源文件:行号
—— 在该源文件中的这行加上一个断点吧info b
—— 查看断点的信息
breakpoint already hit 1 time【此断点被命中一次】d(delete) + 当前要删除断点的编号
—— 删除一个断点【不可以d + 行号】
- 若当前没有跳出过gdb,则断点的编号会持续累加
d + breakpoints
—— 删除所有的断点disable b(breakpoints)
—— 使所有断点无效【默认缺省】enable b(breakpoints)
—— 使所有断点有效【默认缺省】disable b(breakpoint) + 编号
—— 使一个断点无效【禁用断点】enable b(breakpoint) + 编号
—— 使一个断点有效【开启断点】
- 相当于VS中的空断点
enable breakpount
—— 使一个断点有效【开启断电】n(next)
—— 逐过程【相当于F10,为了查找是哪个函数出错了】s(step)
—— 逐语句【相当于F11,】bt
—— 看到底层函数调用的过程【函数压栈】set var
—— 修改变量的值p(print) 变量名
—— 打印变量值display
—— 跟踪查看一个变量,每次停下来都显示它的值【变量/结构体..】undisplay + 变量名编号
—— 取消对先前设置的那些变量的跟踪排查问题三剑客🗡
until + 行号
—— 进行指定位置跳转,执行完区间代码finish
—— 在一个函数内部,执行到当前函数返回,然后停下来等待命令c(continue)
—— 从一个断点处,直接运行至下一个断点处【VS下不断按F5】
2、命令演示
看了上面的这些命令后,相信你一定回到了刚开始学习Linux指令的时候那种恐惧感,不过没关系,我会一一地演示这些指令,让你在看完本文后有一个基本的调试能力💪
- 首先我们进入到gdb,然后它会等待我们输入指令
⌨ 行号显示
l(list) 行号/函数名
—— 显示对应的code,每次10行
- 首先若是直接【L】的话便会随机显示出该源文件中的随机10行内容,这不是我们想要的
- 若是【L 0】或者是【L 1】的话那就是从第一行开始往下列10行的内容
- 注意这里的L是小写,而且与数字之间要有一个空格
- 接下去若是想要看到我们所写的全部代码,只需要多
Enter
几次就可以了,gdb会自动记忆你上次敲入的指令
⌨ 断点设置
b + 行号
—— 在那一行打断点
b 源文件:函数名
—— 在该函数的第一行打上断点
b 源文件:行号
—— 在该源文件中的这行加上一个断点
⌨ 查看断点信息
info b
—— 查看断点的信息
- 若是直接执行【info】的话,出来的就是所有的调试信息
- 但若是我们只想查看一下所打的断点的信息,那就在后面加个
b/breakpoint
来简要介绍一下断点的一些字段信息
- Num —— 编号
- Type —— 类型
- Disp —— 状态
- Enb —— 是否可用
- Address —— 地址
- What —— 在此文件的哪个函数的第几行
- 最后的话就是每个断点信息的下面这块
breakpoint already hit 1 time
即此断点被命中1次
⌨ 删除断点
d + 当前要删除断点的编号
—— 删除一个断点【不可以d + 行号】
d + breakpoints
—— 删除所有的断点
- 此时若继续将这个20行的断点打上时,就可以发现其编号为【4】,而并不是从1开始,这是因为我们没有退出过gdb,所以会持续上一次的编号继续往下
⌨ 开启 / 禁用断点
disable b(breakpoints)
—— 使所有断点无效【默认缺省】
enable b(breakpoints)
—— 使所有断点有效【默认缺省】
disable b(breakpoint) + 编号
—— 使一个断点无效【禁用断点】
enable b(breakpoint) + 编号
—— 使一个断点有效【开启断点】
其实对于禁用断点和启用断点,VS中也是有的,它叫做【空断点】。我们一起来看看