1. GDB是什么
- 是 Linux 下常用的程序调试器
- 借助 GDB 调试器可以实现以下几个功能:
- 程序启动时,可以按照自定义的要求运行程序,例如设置参数和环境变量;
- 支持断点调试;
- 程序执行过程中,可以改变某个变量的值、改变代码的执行顺序,从而尝试修改程序中出现的逻辑错误。
2. GDB下载和安装教程
- 判断当前 Linux 发行版是否安装有 GDB 的方法也很简单,就是在命令行窗口中执行
gdb -v
命令,已安装的情况下会显示gdb版本。 - 对于尚未安装 GDB 的 Linux 发行版,安装方法通常有以下 2 种:
- 直接调用该操作系统内拥有的 GDB 安装包,使用包管理器进行安装。此安装方式的好处是速度快,但通常情况下安装的并非 GDB 的最新版本;
- 前往 GDB 官网下载源码包,在本机编译安装。此安装方式的好处是可以任意选择 GDB 的版本,但由于安装过程需要编译源码,因此安装速度较慢。
- 快速安装GDB
对于 RedHat 系列的 Linux 发行版,通过在命令行窗口中执行sudo yum -y install gdb
指令;对于 Debian 系列的 Linux 发行版,通过执行sudo apt -y install gdb
指令,即可实现 GDB 的安装。 - 源码安装GDB
和使用 yum(apt)自动安装 GDB 不同,手动安装需提前到 GDB 官网下载相应的源码包。
3. 使用GDB的前期准备
- 以源文件main.c为例,使用指令
gcc main.c -o main.exe
生成可执行文件main.exe。 - 需要注意的是,这样生成的main.exe不支持使用 GDB 进行调试。原因很简单,使用 GDB 调试某个可执行文件,该文件中必须包含必要的调试信息(比如各行代码所在的行号、包含程序中所有变量名称的列表(又称为符号表)等),而上面生成的 main.exe 则没有。
- 只需要使用 gcc -g 选项编译源文件,即可生成满足 GDB 要求的可执行文件:
gcc main.c -o main.exe -g
。
4. 启动GDB调试器
- 在生成包含调试信息的 main.exe 可执行文件的基础上,启动 GDB 调试器的指令:
gdb main.exe
。 - 注意,该指令在启动 GDB 的同时,会打印出一堆免责条款。通过添加 --silent(或者 -q、–quiet)选项,可将比部分信息屏蔽掉:
gdb main.exe --silent/--quiet/-q
。 - 最直接的启动方式:
gdb
,但这种方式未指定要调试的目标程序。 - 启动成功的标志就是最终输出的 (gdb)。
5. 常见GDB调试指令
指令 | 作用 |
---|---|
break / b xxx | 设置断点,xxx为断点位置 |
run / r | 执行程序,在第一个断点处暂停 |
continue / c | 程序在断点处暂停时,用该指令来继续执行,直到遇到下一个断点或程序结束 |
next / n | 让程序一行行地执行代码 |
print / p xxx | 打印变量xxx的值 |
list / l | 显示源程序代码内容,包括代码行号 |
finish | 结束当前函数运行,返回调用处暂停 |
quit / q | 终止调试 |
6. 启动程序
- GDB 调试器提供了多种方式来启动目标程序,其中最常用的就是 run 指令,其次为 start 指令。它们之间的区别是:
- 默认情况下,run 指令会一直执行程序,直到执行结束。如果程序中手动设置有断点,则 run 指令会执行程序至第一个断点处;
- start 指令会执行程序至 main() 主函数的起始位置,即在 main() 函数的第一行语句处停止执行(该行代码尚未执行)。
- 对于已启动的 GDB 调试器,我们可以先通过 l (小写的 L)指令验证其是否已找到指定的目标程序文件:
(gdb) l
。对于找不到目标程序文件的 GDB 调试器,l 指令的执行结果显示No symbol table is loaded. Use the "file" command.
。这种情况下,我们就必须手动为其指定要调试的目标程序。 - 通过借助 file 命令,则无需重启 GDB 调试器也能指定要调试的目标程序文件:
(gdb) file /tmp/demo/main.exe
。 - 为 GDB 调试器指定的目标程序传递参数,常用的方法有 3 种:
- 启动 GDB 调试器时,可以在指定目标调试程序的同时,使用 --args 选项指定需要传递给该程序的数据:
gdb --args main.exe str
。
整个指令的意思是:启动 GDB 调试器调试 main.exe 程序,并为其传递 “str” 这个字符串(其会被 argv[] 字符串数组接收)。
- GDB 调试器启动后,可以借助 set args 命令指定目标调试程序启动所需要的数据:
(gdb) set args str
。 - 还可以使用 run 或者 start 启动目标程序时,指定其所需要的数据。
(gdb) run str
,(gdb) start str
- 默认情况下,GDB 调试的程序会将输出结果打印到屏幕上。而通过对输入、输出重定向,可以令调试程序接收指定文件或者终端提供的数据,也可以将执行结果输出到文件或者某个终端上。例如,将 main.exe 文件的执行结果输出到 a.txt 文件中,执行命令:
(gdb) run > a.txt
。 - 对于调试 /tmp/demo/ 路径下的 main.exe 文件,将其作为 GDB 调试器的工作目录,一定程度上可以提高调试效率。例如在 ~ 路径下启动的 GDB 调试器,其工作目录就为 ~(当前用户的 home 目录)。GDB 调试器提供有修改工作目录的指令,即 cd 指令:
(gdb) cd /tmp/demo
。
7. 设置断点
- break 命令(可以用 b 代替)常用的语法格式有以下 2 种:
(gdb) break location // b location
(gdb) break location if cond // b .. if cond
- 第一种格式中,location 用于指定打断点的具体位置,其表示方式如下:
location的值 | 含义 |
---|---|
linenum | 一个整数,表示要打断点处代码的行号。程序中各行代码都有对应的行号,可通过执行 l(小写的 L)命令看到。 |
filename:linenum | 在源文件 filename 中的第 linenum 行打断点。 |
+ offset - offset | offset 为整数(假设值为 2),+offset 表示以当前程序暂停位置为准,向后数 offset 行处打断点;-offset 表示以当前程序暂停位置为准,向前数 offset 行处打断点。 |
function | function 表示程序中包含的函数的函数名,会在该函数内部的开头位置打断点,程序会执行到该函数第一行代码处暂停。 |
filename:function | 在指定文件 filename 中 function 函数的开头位置打断点。 |
- 第二种格式中,cond 为某个表达式。含义为:每次程序执行到断点位置时都计算 cond 的值,如果为 True,则程序在该位置暂停;反之,程序继续执行。
- tbreak 命令:tbreak 和 break 命令的用法和功能都非常相似,唯一的不同在于,使用 tbreak 命令打的断点仅会作用 1 次,即使程序暂停之后,该断点就会自动消失。
- rbreak 命令:和 break 和 tbreak 命令不同,rbreak 命令的作用对象是 C、C++ 程序中的函数,它会在指定函数的开头位置打断点。格式为:
(gdb) rbreak regex
。
其中 regex 为一个正则表达式,程序中函数的函数名只要满足 regex 条件,rbreak 命令就会其内部的开头位置打断点。值得一提的是,rbreak 命令打的断点和 break 命令打断点的效果是一样的,会一直存在,不会自动消失。
8. 监控变量值的变化
- GDB 调试器支持在程序中打 3 种断点,分别为普通断点、观察断点和捕捉断点。其中 break 命令打的就是普通断点,而 watch 命令打的为观察断点,关于捕捉断点,后续会做讲解。
- 使用 GDB 调试程序的过程中,借助观察断点可以监控程序中某个变量或者表达式的值,只要发生改变,程序就会暂停执行。相比普通断点,观察断点不需要我们预测变量(表达式)值发生改变的具体位置。
所谓表达式,就是包含多个变量的式子,比如 a+b 就是一个表达式,其中 a、b 为变量。
- watch 命令的语法为:
(gdb) watch cond
,cond指的就是要监控的变量或表达式。 - 和 watch 命令功能相似的,还有 rwatch 和 awatch 命令。其中:
rwatch 命令:只要程序中出现读取目标变量(表达式)的值的操作,程序就会停止运行;
awatch 命令:只要程序中出现读取目标变量(表达式)的值或者改变值的操作,程序就会停止运行。
- (提醒)watch 命令的功能是:只有当被监控变量(表达式)的值发生改变,程序才会暂停运行。
9. 建立捕捉断点
- 捕捉断点的作用是,监控程序中某一事件的发生,例如程序发生某种异常时、某一动态库被加载时等等,一旦目标时间发生,则程序停止执行。用捕捉断点监控某一事件的发生,等同于在程序中该事件发生的位置打普通断点。
- catch 命令的格式:
(gdb) catch event
。 - C、C++ 程序常用的 event 事件类型可到GDB官网了解。
10. 单步调试程序
- GDB 调试器共提供了 3 种可实现单步调试程序的方法,即使用 next、step 和 until 命令。
- next 是最常用来进行单步调试的命令,其最大的特点是当遇到包含调用函数的语句时,无论函数内部包含多少行代码,next 指令都会一步执行完。也就是说next 不会进入函数内部。
- next 命令可以缩写为 n 命令,格式为:
(gdb) next count
。
参数 count 表示单步执行多少行代码,默认为 1 行。
- step 命令所执行的代码行中包含函数时,会进入该函数内部,并在函数第一行代码处停止执行。
- step 命令可以缩写为 s 命令,用法和 next 命令相同:
(gdb) step count
。
参数 count 表示单步执行多少行代码,默认为 1 行。
- until 命令可以简写为 u 命令,有 2 种语法格式:
(gdb) until
(gdb) until location
参数 location 为某一行代码的行号
- 不带参数的 until 命令,可以使 GDB 调试器快速运行完当前的循环体,并运行至循环体外停止。注意:until 命令并非任何情况下都会发挥这个作用,只有当执行至循环体尾部(最后一行代码)时,until 命令才会发生此作用;反之,until 命令和 next 命令的功能一样,只是单步执行程序。
- until 命令还可以后跟某行代码的行号location,以指示 GDB 调试器直接执行至指定位置后停止。
- next 、step 和until 命令都会在终端上显示当前程序运行到了第几行。
11. 查看变量的值
- whatis 命令可以显示某个变量的类型:
(gdb) whatis a
。 - 对于在调试期间查看某个变量或表达式的值,GDB 调试器提供有 2 种方法,即使用 print 命令或者 display 命令。
- print 命令的功能就是在 GDB 调试程序的过程中,输出或者修改指定变量或者表达式的值。
- print 命令可以缩写为 p,最常用的语法格式为:
(gdb) print num
/(gdb) p num
。
参数 num 用来代指要查看或者修改的目标变量或者表达式。
- 如果要查看变量的地址,用命令:
p &变量名
。 - 使用 display 命令查看变量或表达式的值,每当程序暂停执行(例如单步执行)时,GDB 调试器都会自动帮我们打印出来,而 print 命令则不会。因此,当我们想频繁查看某个变量或表达式的值从而观察它的变化情况时,使用 display 命令可以一劳永逸。
- display 命令没有缩写形式,常用的语法格式如下 2 种:
(gdb) display expr
(gdb) display/fmt expr
expr 表示要查看的目标变量或表达式;参数 fmt 用于指定输出变量或表达式的格式,下表罗列了常用的一些 fmt 参数。
/fmt | 功 能 |
---|---|
/x | 以十六进制的形式打印出整数。 |
/d | 以有符号、十进制的形式打印出整数。 |
/u | 以无符号、十进制的形式打印出整数。 |
/o | 以八进制的形式打印出整数。 |
/t | 以二进制的形式打印出整数。 |
/f | 以浮点数的形式打印变量或表达式的值。 |
/c | 以字符形式打印变量或表达式的值。 |
- 注意:display 命令和 /fmt 之间不要留有空格。
- 事实上,对于使用 display 命令查看的目标变量或表达式,都会被记录在一张列表(称为自动显示列表)中。通过执行info dispaly命令,可以打印出这张表:
(gdb) info display
Auto-display expressions now in effect:
Num Enb Expression
2: y /t result
1: y num
各列的含义为:
- Num 列为各变量或表达式的编号,GDB 调试器为每个变量或表达式都分配有唯一的编号.
- Enb 列表示当前各个变量(表达式)是处于激活状态还是禁用状态,如果处于激活状态(用 y >表示),则每次程序停止执行,该变量的值都会被打印出来;反之,如果处于禁用状态(用 n >表示),则该变量(表达式)的值不会被打印。
- Expression 列:表示查看的变量或表达式。
- 对于不需要再打印值的变量或表达式,可以将其删除或者禁用。
- 删除自动显示列表中的变量或表达式:
(gdb) undisplay num...
/(gdb) delete display num...
。
参数 num… 表示目标变量或表达式的编号,编号的个数可以是多个。
- 禁用自动显示列表中处于激活状态下的变量或表达式:
(gdb) disable display num...
。
num… 表示要禁用的变量或表达式的编号,编号的个数可以是多个。
12. 禁用和删除断点
- 常用的方式有 2 种:
- 使用 quit 命令退出调试,然后重新对目标程序启动调试,此方法会将消除上一次调试操作中建立的所有断点;
- 使用专门删除或禁用断点的命令,既可以删除某一个断点,也可以删除全部断点。
- 查看当前已建好的断点
借助指令(gdb) info breakpoint n
/(gdb) info break n
,可以查看当前调试环境中存在的所有断点,包括普通断点、观察断点以及捕捉断点。
参数 n 作为可选参数,为某个断点的编号,表示查看指定断点而非全部断点。
- 无论是普通断点、观察断点还是捕捉断点,都可以使用 clear 或者 delete 命令进行删除。
- clear 命令可以删除指定位置处的所有断点,格式为:
(gdb) clear location
。
参数 location 通常为某一行代码的行号或者某个具体的函数名。当 location 参数为某个函数的函数名时,表示删除位于该函数入口处的所有断点。
- delete 命令(可以缩写为 d )通常用来删除所有断点,也可以删除指定编号的各类型断点。格式为:
delete breakpoints num
。
breakpoints 参数可有可无,num 参数为指定断点的编号,其可以是 delete 删除某一个断点,而非全部。
- GDB禁用断点
所谓禁用,就是使目标断点暂时失去作用,必要时可以再将其激活,恢复断点原有的功能。 - 禁用断点可以使用 disable 命令,格式:
disable breakpoints num...
。
breakpoints 参数可有可无;num… 表示可以有多个参数,每个参数都为要禁用断点的编号。如果指定 num…,disable 命令会禁用指定编号的断点;反之若不设定 num…,则 disable 会禁用当前程序中所有的断点。
13. 反汇编相关命令
- 这部分命令能够以操作汇编语言程序的方式操作可执行程序。
- 利用gdb的反汇编命令调试程序方法:将可执行程序用
gdb program_name
调用到内存中。 - 反汇编命令disas / disass / disassemble
功能:将内存中的机器码程序以指令助记符的形式显示出来。格式:disas / disass / disassemble 函数名/起始地址[,结束地址]
。 - 要查看某个line的相关信息,可用命令:
info line 函数名 / *内存地址
。
- 如果参数为函数名,则显示该函数在源文件中的行号及在内存中的起始地址和结束地址。
- 如果参数为*内存地址,则显示该内存地址的指令对应的语句所在的函数在源文件中的行号及在内存中的起始地址和结束地址。
- 查看内存单元用命令:
x /nfu 内存地址
。
说明:
n 表示要显示的内存单元的个数;
u 表示一个地址单元的长度,b表示单字节,h表示双字节,w表示四字节,g表示八字节;
f 表示显示方式, 可取如下值:
取值 | 含义 |
---|---|
x / a | 按十六进制格式显示变量 |
d | 按十进制格式显示变量 |
u | 按十进制格式显示无符号整型 |
o | 按八进制格式显示变量 |
t | 按二进制格式显示变量 |
i | 指令地址格式 |
c | 按字符格式显示变量 |
s | 按字符串格式显示变量 |
f | 按浮点数格式显示变量 |
- si 命令类似于s命令,ni 命令类似于n命令。所不同的是,这两个命令(si/ni)所针对的是汇编指令,而s/n针对的是源代码。作用是单步执行。
14. 其他
info reg
命令可以查看程序使用到的寄存器以及这些寄存器的值。bt
命令可以查看堆栈。