GDB调试命令详解
GDB是什么
调试程序
程序中出现的语法错误可以借助编译器解决;但逻辑错误则只能靠自己解决。实际场景中解决逻辑错误最高效的方法,就是借助调试工具对程序进行调试。
所谓调试(Debug),就是让代码一步一步慢慢执行,跟踪程序的运行过程。比如,可以让程序停在某个地方,查看当前所有变量的值,或者内存中的数据;也可以让程序一次只执行一条或者几条语句,看看程序到底执行了哪些代码。
也就是说,通过调试程序,我们可以监控程序执行的每一个细节,包括变量的值、函数的调用过程、内存中数据、线程的调度等,从而发现隐藏的错误或者低效的代码。
GDB的作用
GDB 全称“GNU symbolic debugger”,从名称上不难看出,它诞生于 GNU 计划(同时诞生的还有 GCC、Emacs 等),是 Linux 下常用的程序调试器。发展至今,GDB 已经迭代了诸多个版本,当下的 GDB 支持调试多种编程语言编写的程序,包括 C、C++、Go、Objective-C、OpenCL、Ada等。实际场景中,GDB 更常用来调试 C 和 C++ 程序。
总的来说,借助 GDB调试器可以实现以下几个功能:
程序启动时,可以按照我们自定义的要求运行程序,例如设置参数和环境变量;
可使被调试程序在指定代码处暂停运行,并查看当前程序的运行状态(例如当前变量的值,函数的执行结果等),即支持断点调试;
程序执行过程中,可以改变某个变量的值,还可以改变代码的执行顺序,从而尝试修改程序中出现的逻辑错误。
GDB安装
1、通过包管理器进行安装
1 | $ yum -y install gdb |
2、源码安装GDB
在gdb源码包上面下载相应的版本进行安装即可。
3、查看GDB版本
输入gdb -v,即可查看当前安装的gdb的版本。
1 | $ gdb -v |
如果显示出gdb的版本,也说明了安装成功。
GDB的用法
常用调试命令
GDB 的主要功能就是监控程序的执行流程。这也就意味着,只有当源程序文件编译为可执行文件并执行时,并且该文件中必须包含必要的调试信息(比如各行代码所在的行号、包含程序中所有变量名称的列表(又称为符号表)等),GDB 才会派上用场。
所以在编译时需要使用 gcc/g++ -g 选项编译源文件,才可生成满足 GDB 要求的可执行文件
| 调试命令 (缩写) | 作用 |
|---|---|
| (gdb) break (b) | 在源代码指定的某一行设置断点,其中xxx用于指定具体打断点位置 |
| (gdb) run (r) | 执行被调试的程序,其会自动在第一个断点处暂停执行。 |
| (gdb) continue (c) | 当程序在某一断点处停止后,用该指令可以继续执行,直至遇到断点或者程序结束。 |
| (gdb) next (n) | 令程序一行代码一行代码的执行。 |
| (gdb) step(s) | 如果有调用函数,进入调用的函数内部;否则,和 next 命令的功能一样。 |
| (gdb) until (u) (gdb) until (u) location |
当你厌倦了在一个循环体内单步跟踪时,单纯使用 until 命令,可以运行程序直到退出循环体。 until n 命令中,n 为某一行代码的行号,该命令会使程序运行至第 n 行代码处停止。 |
| (gdb) print (p) | 打印指定变量的值,其中 xxx 指的就是某一变量名。 |
| (gdb) list (l) | 显示源程序代码的内容,包括各行代码所在的行号。 |
| (gdb) finish(fi) | 结束当前正在执行的函数,并在跳出函数后暂停程序的执行。 |
| (gdb) return(return) | 结束当前调用函数并返回指定值,到上一层函数调用处停止程序执行。 |
| (gdb) jump(j) | 使程序从当前要执行的代码处,直接跳转到指定位置处继续执行后续的代码。 |
| (gdb) quit (q) | 终止调试。 |
示例:
1 | $ ls |
gdb启动时会默认打印一堆免责条款,通过添加 --silent(或者 -q、--quiet)选项,可将这部分信息屏蔽掉。
下面先用个例子运行下上述调试命令
1 | $ gdb test -q <-- 启动gdb进行调试 |
接下来分别介绍下各个命令的用法
GDB 断点调试
启动程序
根据不同场景的需要,GDB 调试器提供了多种方式来启动目标程序,其中最常用的就是 run 指令,其次为 start 指令。也就是说,run 和 start 指令都可以用来在 GDB 调试器中启动程序,它们之间的区别是:
默认情况下,
run指令会一直执行程序,直到执行结束。如果程序中手动设置有断点,则run指令会执行程序至第一个断点处;start指令会执行程序至main()主函数的起始位置,即在main()函数的第一行语句处停止执行(该行代码尚未执行)。
break命令
break 命令(可以用 b 代替)常用的语法格式有以下 2 种。
1 | 1、(gdb) break location // b location |
- 第一种格式中,
location用于指定打断点的具体位置,其表示方式有多种,如表 1 所示。
| location的值 | 含义 |
|---|---|
| linenum | linenum 是一个整数,表示要打断点处代码的行号。要知道,程序中各行代码都有对应的行号,可通过执行 l(小写的 L)命令看到。 |
| filename:linenum | filename 表示源程序文件名;linenum 为整数,表示具体行数。整体的意思是在指令文件 filename 中的第 linenum 行打断点。 |
| + offset - offset |
offset 为整数(假设值为 2),+offset 表示以当前程序暂停位置(例如第 4 行)为准,向后数 offset 行处(第 6 行)打断点;-offset 表示以当前程序暂停位置为准,向前数 offset 行处(第 2 行)打断点 |
| function | function 表示程序中包含的函数的函数名,即 break 命令会在该函数内部的开头位置打断点,程序会执行到该函数第一行代码处暂停。 |
| filename:function | filename 表示远程文件名;function 表示程序中函数的函数名。整体的意思是在指定文件 filename 中 function 函数的开头位置打断点。 |
- 第二种格式中,… 可以是表 1 中所有参数的值,用于指定打断点的具体位置;
cond为某个表达式。整体的含义为:每次程序执行到 … 位置时都计算cond的值,如果为True,则程序在该位置暂停;反之,程序继续执行。另外也可以用condition为断点设置命中条件。
tbreak和rbreak命令
tbreak 命令可以看到是 break 命令的另一个版本,tbreak 和 break 命令的用法和功能都非常相似,唯一的不同在于,使用 tbreak 命令打的断点仅会作用 1 次,即使程序暂停之后,该断点就会自动消失。
和 break 和 tbreak 命令不同,rbreak 命令的作用对象是 C、C++ 程序中的函数,它会在指定函数的开头位置打断点。语法格式
1 | (gdb) tbreak regex |
其中 regex 为一个正则表达式,程序中函数的函数名只要满足 regex 条件,tbreak 命令就会其内部的开头位置打断点。值得一提的是,tbreak 命令打的断点和 break 命令打断点的效果是一样的,会一直存在,不会自动消失。
示例
1 | $ gdb test -q |
删除或禁用断点
删除断点
如果之前建立的断点不再需要或者暂时不需要,该如何删除或者禁用呢?常用的方式有 2 种:
- 使用
quit命令退出调试,然后重新对目标程序启动调试,此方法会将消除上一次调试操作中建立的所有断点; - 使用专门删除或禁用断点的命令,既可以删除某一个断点,也可以删除全部断点。
无论是普通断点、观察断点还是捕捉断点,都可以使用 clear 或者 delete 命令进行删除。
clear 命令可以删除指定位置处的所有断点,常用的语法格式如下所示:
1 | (gdb) clear location |
参数 location 通常为某一行代码的行号或者某个具体的函数名。当 location 参数为某个函数的函数名时,表示删除位于该函数入口处的所有断点。
delete 命令(可以缩写为 d )通常用来删除所有断点,也可以删除指定编号的各类型断点,语法格式如下:
1 | delete [breakpoints] [num] |
其中,breakpoints 参数可有可无,num 参数为指定断点的编号,其可以是 delete 删除某一个断点,而非全部。
如果不指定 num 参数,则 delete 命令会删除当前程序中存在的所有断点。
禁用断点
禁用断点可以使用 disable 命令,语法格式如下:
1 | disable [breakpoints] [num...] |
breakpoints 参数可有可无;num... 表示可以有多个参数,每个参数都为要禁用断点的编号。如果指定 num...,disable 命令会禁用指定编号的断点;反之若不设定 num...,则 disable 会禁用当前程序中所有的断点。
对于禁用的断点,可以使用 enable 命令激活,该命令的语法格式有多种,分别对应有不同的功能:
1 | enable [breakpoints] [num...] 激活用 num... 参数指定的多个断点,如果不设定 num...,表示激活所有禁用的断点 |
其中,breakpoints 参数可有可无;num... 表示可以提供多个断点的编号,enable 命令可以同时激活多个断点。
观察断点监控变量值的变化
观察断点
要知道,GDB 调试器支持在程序中打 3 种断点,分别为普通断点、观察断点和捕捉断点。其中 break 命令打的就是普通断点,而 watch 命令打的为观察断点。
使用 GDB 调试程序的过程中,借助观察断点可以监控程序中某个变量或者表达式的值,只要发生改变,程序就会停止执行。相比普通断点,观察断点不需要我们预测变量(表达式)值发生改变的具体位置
1 | (gdb) watch cond |
和 watch 命令功能相似的,还有 rwatch 和 awatch 命令。其中:
rwatch命令:只要程序中出现读取目标变量(表达式)的值的操作,程序就会停止运行;awatch命令:只要程序中出现读取目标变量(表达式)的值或者改变值的操作,程序就会停止运行。
示例
1 | $ gdb test -q |
查看变量或表达式的值
对于在调试期间查看某个变量或表达式的值,GDB 调试器提供有 2 种方法,即使用 print 命令或者 display 命令。
print 命令
它的功能就是在 GDB 调试程序的过程中,输出或者修改指定变量或者表达式的值。
print 命令可以缩写为 p,最常用的语法格式如下所示:
1 | (gdb) print num |
其中,参数 num 用来代指要查看或者修改的目标变量或者表达式。
当程序中包含多个作用域不同但名称相同的变量或表达式时,可以借助::运算符明确指定要查看的目标变量或表达式。::运算符的语法格式如下:
1 | (gdb) print file::variable |
其中 file 用于指定具体的文件名,funciton 用于指定具体所在函数的函数名,variable 表示要查看的目标变量或表达式。
另外,print也可以打印出类或者结构体变量的值。
display 命令
和 print 命令一样,display 命令也用于调试阶段查看某个变量或表达式的值,它们的区别是,使用 display 命令查看变量或表达式的值,每当程序暂停执行(例如单步执行)时,GDB 调试器都会自动帮我们打印出来,而 print 命令则不会。
也就是说,使用 1 次 print 命令只能查看 1 次某个变量或表达式的值,而同样使用 1 次 display 命令,每次程序暂停执行时都会自动打印出目标变量或表达式的值。因此,当我们想频繁查看某个变量或表达式的值从而观察它的变化情况时,使用 display 命令可以一劳永逸。
display 命令没有缩写形式,常用的语法格式如下 2 种:
1 | (gdb) display expr |
注意,display 命令和 /fmt 之间不要留有空格。以 /x 为例,应写为 (gdb)display/x expr。
GDB单步调试
根据实际场景的需要,GDB 调试器共提供了 3 种可实现单步调试程序的方法,即使用 next、step 和 until 命令。换句话说,这 3 个命令都可以控制 GDB 调试器每次仅执行 1 行代码,但除此之外,它们各自还有不同的功能。
next命令
next 是最常用来进行单步调试的命令,其最大的特点是当遇到包含调用函数的语句时,无论函数内部包含多少行代码,next 指令都会一步执行完。也就是说,对于调用的函数来说,next 命令只会将其视作一行代码。
next 命令可以缩写为 n 命令,使用方法也很简单,语法格式如下:
1 | (gdb) next count |
step命令
通常情况下,step 命令和next命令的功能相同,都是单步执行程序。不同之处在于,当 step 命令所执行的代码行中包含函数时,会进入该函数内部,并在函数第一行代码处停止执行。
step 命令可以缩写为 s 命令,用法和 next 命令相同,语法格式如下:
1 | (gdb) step count |
until命令
until 命令可以简写为 u 命令,有 2 种语法格式,如下所示:
1 | 1、(gdb) until |
其中,参数 location 为某一行代码的行号。
不带参数的 until 命令,可以使 GDB 调试器快速运行完当前的循环体,并运行至循环体外停止。注意,until 命令并非任何情况下都会发挥这个作用,只有当执行至循环体尾部(最后一行代码)时,until 命令才会发生此作用;反之,until 命令和 next 命令的功能一样,只是单步执行程序。
return命令
实际调试时,在某个函数中调试一段时间后,可能不需要再一步步执行到函数返回处,希望直接执行完当前函数,这时可以使用 finish 命令。与 finish 命令类似的还有 return 命令,它们都可以结束当前执行的函数。
finish命令
finish 命令和 return 命令的区别是,finish 命令会执行函数到正常退出;而 return 命令是立即结束执行当前函数并返回,也就是说,如果当前函数还有剩余的代码未执行完毕,也不会执行了。除此之外,return 命令还有一个功能,即可以指定该函数的返回值。
jump命令
jump 命令的功能是直接跳到指定行继续执行程序,其语法格式为:
1 | (gdb) jump location |
其中,location 通常为某一行代码的行号。
也就是说,jump 命令可以略过某些代码,直接跳到 location 处的代码继续执行程序。这意味着,如果你跳过了某个变量(对象)的初始化代码,直接执行操作该变量(对象)的代码,很可能会导致程序崩溃或出现其它 Bug。另外,如果 jump 跳转到的位置后续没有断点,那么 GDB 会直接执行自跳转处开始的后续代码。
GDB search 命令
调试文件时,某些时候可能会去找寻找某一行或者是某一部分的代码。可以使用 list 显示全部的源码,然后进行查看。当源文件的代码量较少时,我们可以使用这种方式搜索。如果源文件的代码量很大,使用这种方式寻找效率会很低。所以 GDB 中提供了相关的源代码搜索的的search命令。
search 命令的语法格式为:
1 | search <regexp> |
第一项命令格式表示从当前行的开始向前搜索,后一项表示从当前行开始向后搜索。其中 regexp 就是正则表达式,正则表达式描述了一种字符串匹配的模式,可以用来检查一个串中是否含有某种子串、将匹配的子串替换或者从某个串中取出符合某个条件的子串。很多的编程语言都支持使用正则表达式。
查看堆栈信息
backtrace 命令
backtrace 命令用于打印当前调试环境中所有栈帧的信息,常用的语法格式如下:
1 | (gdb) backtrace [-full] [n] |
其中,用 [ ] 括起来的参数为可选项,它们的含义分别为:
n:一个整数值,当为正整数时,表示打印最里层的n个栈帧的信息;n为负整数时,那么表示打印最外层n个栈帧的信息;-full:打印栈帧信息的同时,打印出局部变量的值。
注意,当调试多线程程序时,该命令仅用于打印当前线程中所有栈帧的信息。如果想要打印所有线程的栈帧信息,应执行thread apply all backtrace命令。
frame 命令
frame 命令的常用形式有 2 个:
- 根据栈帧编号或者栈帧地址,选定要查看的栈帧,语法格式如下:
1 | (gdb) frame spec |
该命令可以将 spec 参数指定的栈帧选定为当前栈帧。spec 参数的值,常用的指定方法有 3 种:
- 通过栈帧的编号指定。0 为当前被调用函数对应的栈帧号,最大编号的栈帧对应的函数通常就是 main() 主函数;
- 借助栈帧的地址指定。栈帧地址可以通过
info frame命令(后续会讲)打印出的信息中看到; - 通过函数的函数名指定。注意,如果是类似递归函数,其对应多个栈帧的话,通过此方法指定的是编号最小的那个栈帧。
除此之外,对于选定一个栈帧作为当前栈帧,GDB 调试器还提供有 up 和down两个命令。其中,up 命令的语法格式为:
1 | (gdb) up n |
其中 n 为整数,默认值为 1。该命令表示在当前栈帧编号(假设为 m)的基础上,选定 m+n 为编号的栈帧作为新的当前栈帧。
相对地,down 命令的语法格式为:
1 | (gdb) down n |
其中n为整数,默认值为 1。该命令表示在当前栈帧编号(假设为 m)的基础上,选定 m-n 为编号的栈帧作为新的当前栈帧。
- 借助如下命令,我们可以查看当前栈帧中存储的信息:
1 | (gdb) info frame |
该命令会依次打印出当前栈帧的如下信息:
- 当前栈帧的编号,以及栈帧的地址;
- 当前栈帧对应函数的存储地址,以及该函数被调用时的代码存储的地址
- 当前函数的调用者,对应的栈帧的地址;
- 编写此栈帧所用的编程语言;
- 函数参数的存储地址以及值;
- 函数中局部变量的存储地址;
- 栈帧中存储的寄存器变量,例如指令寄存器(64位环境中用 rip 表示,32为环境中用
eip表示)、堆栈基指针寄存器(64位环境用rbp表示,32位环境用ebp表示)等。
除此之外,还可以使用info args命令查看当前函数各个参数的值;使用info locals命令查看当前函数中各局部变量的值。
调试正在执行的程序
如果调试正在执行中的程序,首先需要找到正在运行程序的进程号PID,之后可以用下面三个命令进行调试,进入正常的调试流程。
1 | 1) gdb attach PID |
示例:
1 |
|
注意,当 GDB 调试器成功连接到指定进程上时,程序执行会暂停。如上所示,程序暂停至第 6 行代码num++的位置,此时可以通过断点调试、逐步运行等方式监控程序的执行过程。例如:
1 | (gdb) l |
注意,当调试完成后,如果想令当前程序进行执行,消除调试操作对它的影响,需手动将 GDB 调试器与程序分离,分离过程分为 2 步:
执行
detach指令,使GDB调试器和程序分离;执行
quit(或q)指令,退出GDB调试。
调试执行异常崩溃的程序
在Linux操作系统中,当程序执行发生异常崩溃时,系统可以将发生崩溃时的内存数据、调用堆栈情况等信息自动记录下载,并存储到一个文件中,该文件通常称为 core 文件,Linux 系统所具备的这种功能又称为核心转储(core dump)。幸运的是,GDB 对 core 文件的分析和调试提供有非常强大的功能支持,当程序发生异常崩溃时,通过 GDB 调试产生的 core 文件,往往可以更快速的解决问题。
这里就先不写如何设置core dump文件目录了,可以自行了解。
写个程序验证一下:
1 |
|
编译运行
1 | $ g++ -g -o test core.cpp |
可以根据生成时间查找core dump文件
1 | ls /home/homework/coresave -hl | grep test |
用gdb进行调试
1 | $ gdb test /home/homework/coresave/core.test1.7791.1615620408 -q |
由此可见,程序崩溃了在第五行,定位到了出现问题的代码位置。
本文参考
|
本文作者:zhuyong 原文链接:https://zhuyongchn.github.io 关于博主:欢迎关注左侧公众号,获取更多干货。 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处! |
