1. gdb 概述
GDB是GNU开源组织发布的一个强大的UNIX下的程序调试工具。或许,各位比较喜欢那种图形界面方式的,像VC、BCB等IDE的调试,但如果你是在 UNIX平台下做软件,你会发现GDB这个调试工具有比VC、BCB的图形化调试器更强大的功能。所谓“寸有所长,尺有所短”就是这个道理。
一般来说,GDB主要帮忙你完成下面四个方面的功能:
- 1、启动你的程序,可以按照你的自定义的要求随心所欲的运行程序。
- 2、可让被调试的程序在你所指定的调置的断点处停住。(断点可以是条件表达式)
- 3、当程序被停住时,可以检查此时你的程序中所发生的事。
- 4、动态的改变你程序的执行环境。
从上面看来,GDB和一般的调试工具没有什么两样,基本上也是完成这些功能,不过在细节上,你会发现GDB这个调试工具的强大,大家可能比较习惯了图形化的调试工具,但有时候,命令行的调试工具却有着图形化工具所不能完成的功能。
2.使用gdb
mycode.c
我们先生成一个Debug版本的可执行程序(gcc加上-g选项)
我们可以验证一下它是不是含有debug信息
很好,有
接下来我们就可以使用gdb进行调试了
在此之前要确保安装了gdb
2.1.显示代码
l(list) 行号/函数名
—— 显示对应的code,每次10行
- 首先若是直接【L】的话便会随机显示出该源文件中的随机10行内容,这不是我们想要的
- 若是【l 0】或者是【l 1】的话那就是从第一行开始往下列10行的内容
- 注意这里的L是小写,而且与数字之间要有一个空格
- 我们想要看第二行或者第10行,可以像下面这么做
- 这个也没有显示出全部代码啊!!!只需要多
Enter
几次就可以了,gdb会自动记忆你上次敲入的指令
- 我们还可以查看一个函数
2.2.设置断点
- r或run:运行程序。
因为我们还没有设置断点,所以一直执行到尾了
我们现在来打断点
- break/b 行号:在某一行设置断点
- break/b 函数名:在某个函数开头设置断点
b/break 源文件:行号
—— 在该源文件中的这行加上一个断点
2.3.查看断点信息
我们打了3个断点,我们发现我们的断点不像windows上面的断点,我们现在看不到断点,那我们怎么去查呢?
- info break/b :查看断点信息。
- 若是直接执行【info】的话,出来的就是所有的调试信息
- 但若是我们只想查看一下所打的断点的信息,那就在后面加个
b/break
断点的信息都显示出来了
- 接下来简要介绍一下断点的一些字段信息
- Num —— 编号
- Type —— 类型
- Disp —— 状态
- Enb —— 是否可用
- Address —— 地址
- What —— 在此文件的哪个函数的第几行
- 最后的话就是每个断点信息的下面这块
breakpoint already hit 1 time
即此断点被命中1次
2.4.删除断点
我们想删除断点,该怎么做呢?
我们先看看我们之前设置的断点
d + 当前要删除断点的编号
—— 删除一个断点【不可以d + 行号】d + breakpoints
—— 删除所有的断点- 此时若继续将这个20行的断点打上时,就可以发现其编号为【4】,而并不是从1开始,这是因为我们没有退出过gdb,所以会持续上一次的编号继续往下
2.5.开启/禁用断点
disable b(breakpoints)
—— 使所有断点无效【默认缺省】- 我们看到所有断点的Enb从y变成了n,代表从可用变成了不可用
enable b(breakpoints)
—— 使所有断点有效【默认缺省】- 我们看到所有断点的Enb从n变成了y,代表从不可用变成了可用
disable b(breakpoint) + 编号
—— 使一个断点无效【禁用断点】
enable b(breakpoint) + 编号
—— 使一个断点有效【开启断点】
2.6.运行/调试
r(run)
—— F5【无断点直接运行、有断点从第一个断点处开始运行】
- 首先若是将断点删除掉,使用【r】指令运行的话就会直接运行到程序结束
- 再加上断点去运行的话就会在打的断点处停下来
2.7.逐过程和逐语句
我们先看看我们设置的断点
n(next)
—— 逐过程【相当于F10,为了查找是哪个函数出错了】
- 可以看到,我从第一个断点处也就是17行的位置开始执行,按下【n】之后因为在其后即19行有一个断点,此时就会直接运行到断点处
- 按n,再按n,直到结束
s(step) —— 逐语句【相当于F11,一次走一条代码,可进入函数,同样的库函数也会进入】
- 此时我们按下【s】,也就相当于是【step】,让程序一步一步地走,继而进入了Addtop这个函数(如下图),若是你在printf()语句要执行时按下【s】的话gdb就会进入printf()库函数内部去执行,这里就不展示了
- 接下去我们可以就继续【n】,然后进行逐过程调试,来到for循环中,那么逐过程也就是变量i的累加和计数器count的累加,所以会反复执行(通过图中最左侧可以看出是第8行和第10行在反复执行)
- 可以看到后面我没有再按【n】了,但是依旧会执行上面的步骤,这点上面也有提到过,因为gdb会自动化记忆你上一次执行过的命令,所以若是不想再敲了,直接Enter就可以了
2.8.打印 / 追踪变量
p(print) 变量名
—— 打印变量值
- 都执行了那么多次了,不知道【i】和【count】发生了怎样的变化,将它们打印出来看看吧
- 通过继续执行【n】,然后再去打印就可以发现
i
的值和count
的值发生了变化- 但是你不觉得这样每次去打印会显得很繁琐吗,那一定会的,所以我们有更好的办法💡
display
—— 跟踪查看一个变量,每次停下来都显示它的值【变量/结构体…】- 我们也可以去追踪一下这两个变量的地址,不过可以看到对于地址来说是不会发生改变的
- 但是呢,每次都追踪打印这么多内容又太多了,我想把它们取消了可以吗?答:当然是可以的
- 既然有
display
,那就有undisplay
undisplay + 变量名编号
—— 取消对先前设置的那些变量的跟踪
2.9. 查看函数调用
bt
—— 看到底层函数调用的过程【函数压栈】
- 通过仔细观察刚才追踪的4个变量最左侧的编号,就可以看到它们的排列的顺序是倒着的。因为变量i和变量res是我们先追踪的,它们的地址是我们后追踪的,所以可以看出这很像是一个压栈的过程
- 其实不仅是对于它们,
Addtop
函数和main
函数也呈现这样的关系。此时我们就可以通过【bt】这个指令来查看函数压栈的过程,此时便可以看到因为
2.10.修改变量的值
set var
—— 修改变量的值
- 对于这个修改变量的值,很像是在VS里调试之前设置的那种条件断点,可以使调试开始后直接运行到此断点处。不过对于【set var】而言是在调试过程中进行设置
2.11.gdb指令合集
注:()括号里面是该指令的全称
- 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】
3.最常用指令(指令三剑客)
掌握了上面的这些,你就可以在Linux下调一些简单的代码了,不过想做到高效地进行调试,就需要学习一下【三剑客】
3.1.指定行号跳转
until + 行号 —— 进行指定位置跳转,执行完区间代码
- 可以看到,当前在for循环内容执行累加的逻辑,但若是我们一直这么执行下去,就没有时间排错了,除了上面的哪一种【set var】之外,还有一种方法其实起到直接结束当前循环的作用,那就是进行指定行号跳转
- 通过观察下图可以看到,当我们运行了until 13之后,程序直接就给出了我们最终的结果count,而且即将要执行最后的打印语句,说明我们跳转成功了
- 然后可以看到,在获取到返回值后,也就直接进行了printf打印
3.2.强制执行函数
finish —— 在一个函数内部,执行到当前函数返回,然后停下来等待命令
- 有时候我们会有这样的需求,在初步排查的时候推断可能是某个函数内部的逻辑出了问题,但是呢又不想一步步地进到函数内部进行调试,在VS中其实很简单,只需要在函数下方设个断点,然后F5直接运行到断点处即可
- 但是在Linux下的gdb中,我们可以使用【finsh】指令来直接使一个函数执行完毕。从下图我们可以看到,首先【s】进到函数内部,接下去我直接使用finish,可以看到它直接回到了调用函数的位置,returned了一个返回值
3.3.跳转到下一断点
c(continue) —— 从一个断点处,直接运行至下一个断点处【VS下不断按F5】
- 这点也是我刚才在上面有提到过的,在VS中,我们要直接跳转到下一个断点处只修要按下F5即可,那在gdb中该如何操作呢,你需要敲个【c】就可以了
- 从下图我们可以看出,对于这个指令的用处可谓是非常大,当我处于第一个断点也就是20行的时候,直接敲下【c】,就可以运行到第二个断点处也就是第10行。之后若反复敲【c】,因为这是一个单语句的循环,所以循环的下一次还是会执行到此处。上面的这两个功能就和我们在VS中用的F5是一个道理
4.gdb实战演练
- 本次演示使用的代码是
- 我们取名mycode.c,用gcc编译成带有debug信息的
接下来,我们进入GDB 调试模式。
- 首先我们在程序第16行设置上一个断点,然后【r】从第15行开始运行
- 然后我们使用【s】进入到swap函数中,因为我首先不想调试,想先立马看看运行结果,但是此时又已经进入调试了,那么我们就可以使用到【finish】来立马执行完这个函数,然后观察一下结果
- 可以看到,最后打印出结果的时候a和b的值确实发生了交换
- 既然清楚了二者会进行一个交换,接下去我们就逐语句【n】进行一个单步追踪吧
- 因为提前看了执行结果,所以我们要重新开始调试,按下【r】即可,它会询问你是否需要重新开始调试,选择y之后就可以重新从16行开始进行调试
- 首先通过【display】记录一下两个变量的值和地址
- 接着按【s】进入到swap函数里,追踪一下指针x和指针y的内容,也就是它们所存放的地址,就可以看到,函数内部已经接受到了这两个变量的地址
- 然后对我们要观察的值变化继续做一个追踪
- 并且在执行完第一个语句
t = *x
时,临时变量t中已经存放了变量a的值,也就是指针所指向的那块空间中的值- 接下去执行
*x = *y
,此时*x中的值就发生了变化,因为指针x可以直接找到变量a的地址,所以可以对其中的内容做修改,就变为了20- 接下去执行
*y = t
,同理,指针y可以直接找到变量b的地址,所以可以对其中的内容做修改,将原本保存在临时变量t中的10赋值给到*y,也就修改了其中的内容- 再按【n】的话这个swap函数就执行结束了,回到了main函数,就可以清楚地看到函数内部的修改带动了函数外部值的变化,真正地通过【传址调用】交换了两个数
- 整体的调试结束!!!!