使用GDB debug
GDB是Linux环境使用的一款debug工具,详细介绍请看官网。
GDB入门
下载以及安装
许多Linux发行版默认安装了GDB,查看安装的GDB版本命令
gdb -v
若结果显示没有安装GDB,需要手动进行安装
# CentOS
yum -y install gdb
# Ubuntu
apt -y install gdb
也可从源码手动编译进行安装
- 从相关网站上手动下载源码
gdb-xxx.tar.gz
- 解压
tar -zxvf gdb-xxx.tar.gz
- cd
gdb-xxx.tar.gz
- mkdir
gdb-build.tar.gz
- cd
gdb-build.tar.gz
../configure
make
make install
牛刀小试
GDB调试的可执行文件需要拥有一些调试信息,所以用gcc编译过程时需要添加额外参数。
gcc main.c -o main -g
gdb main.exe
之后命令行出现以下提示
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-120.el7
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /root/main...done.
(gdb)
接下来罗列几个常用的调试指令
调试指令 | 简写 | 作用 |
---|---|---|
break xxx | b | 设置断点 |
run | r | 执行debug,会停止在第一个断点处 |
continue | c | 继续直到下一个断点或程序结束 |
next | n | 令代码往下执行一行 |
print xxx | p | 打印指定变量的值 |
list | l | 显示源程序代码的内容 |
quit | q | 终止 |
(gdb) l
1 #include <stdio.h>
2 int main()
3 {
4 unsigned long long int n, sum;
5 n = 1;
6 sum = 0;
7 while (n <= 100)
8 {
9 sum = sum + n;
10 n = n + 1;
(gdb)
11 }
12 return 0;
13 }(gdb)
Line number 14 out of range; broken.cpp has 13 lines.
(gdb) b 7
Breakpoint 1 at 0x400501: file broken.cpp, line 7.
(gdb) r
Starting program: /root/main
Breakpoint 1, main () at broken.cpp:7
7 while (n <= 100)
Missing separate debuginfos, use: debuginfo-install glibc-2.17-292.el7.x86_64
(gdb) p n
$1 = 1
(gdb) b 12
Breakpoint 2 at 0x400517: file broken.cpp, line 12.
(gdb) c
Continuing.
Breakpoint 2, main () at broken.cpp:12
12 return 0;
(gdb) p n
$2 = 101
(gdb) q
A debugging session is active.
Inferior 1 [process 12474] will be killed.
Quit anyway? (y or n) y
GDB调试进程
除了GDB可执行文件的方式,GDB也能使用进程号的方式来进行。 查看进程号
pidof xxx
调试
gdb -p 进程号
注意,当GDB调试器成功连接到指定进程上时,程序执行会暂停。
注意,当调试完成后,如果想令当前程序进行执行,消除调试操作对它的影响,需手动将GDB调试器与程序分离,分离过程分为 2 步:
- 执行
detach
指令,使GDB调试器和程序分离; - 执行
quit
指令,退出GDB调试。
除了以上 3 种情况外,C或者C++程序运行过程中常常会因为各种异常或者Bug而崩溃,比如内存访问越界(例如数组下标越界、输出字符串时该字符串没有\0 结束符等)、非法使用空指针等,此时就需要调试程序。
值得一提的是,在Linux操作系统中,当程序执行发生异常崩溃时,系统可以将发生崩溃时的内存数据、调用堆栈情况等信息自动记录下载,并存储到一个文件中,该文件通常称为core
文件,Linux系统所具备的这种功能又称为核心转储(core dump)。幸运的是,GDB对core
文件的分析和调试提供有非常强大的功能支持,当程序发生异常崩溃时,通过GDB调试产生的core
文件,往往可以更快速的解决问题。
默认情况下,Linux 系统是不开启core dump
这一功能的,读者可以借助执行ulimit -a
指令来查看当前系统是否开启此功能:
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 381159
max locked memory (kbytes, -l) 64
max memory size (kbytes, -m) unlimited
open files (-n) 1024
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 381159
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
如果 core file size(core 文件大小)对应的值为 0,表示当前系统未开启 core dump 功能。这种情况下,可以通过执行如下指令改变 core 文件的大小:
ulimit -c unlimited
# unlimited 表示不限制 core 文件的大小。
当程序执行发生异常崩溃时,系统就可以自动生成相应的 core 文件。
默认情况下,core 文件的生成位置同该程序所在的目录相同。当然我们也可以指定 core 文件的生成的位置
/proc/sys/kernel/core_pattern
# 如把所有core生成至tmp目录下,且命名为core_[program].[pid]
echo '/tmp/core_%e.%p' | sudo tee /proc/sys/kernel/core_pattern
# 或者
sudo sysctl -w kernel.core_pattern="/tmp/%e_core.%p"
对于 core 文件的调试,其调用 GDB 调试器的指令为:
gdb main.exe core
GDB run
根据不同场景的需要,GDB 调试器提供了多种方式来启动目标程序,其中最常用的就是 run 指令,其次为 start 指令。也就是说,run 和 start 指令都可以用来在 GDB 调试器中启动程序,它们之间的区别是:
默认情况下,run 指令会一直执行程序,直到执行结束。如果程序中手动设置有断点,则 run 指令会执行程序至第一个断点处;
start 指令会执行程序至 main() 主函数的起始位置,即在 main() 函数的第一行语句处停止执行(该行代码尚未执行)。
GDB break
break
在 GDB 调试器中对 C、C++ 程序打断点,最常用的就是 break 命令,有些场景中还会用到 tbreak 或者 rbreak 命令
tbreak
tbreak 命令可以看到是 break 命令的另一个版本,tbreak 和 break 命令的用法和功能都非常相似,唯一的不同在于,使用 tbreak 命令打的断点仅会作用 1 次,即使程序暂停之后,该断点就会自动消失。
rbreak
和 break 和 tbreak 命令不同,rbreak 命令的作用对象是 C、C++ 程序中的函数,它会在指定函数的开头位置打断点。
rbreak regex
其中 regex 为一个正则表达式,程序中函数的函数名只要满足 regex 条件,rbreak 命令就会其内部的开头位置打断点。
监控变量值的变化
有一些场景,我们需要监控某个变量或者表达式的值,通过值的变化情况判断程序的执行过程是否存在异常或者 Bug。这种情况下,break 命令显然不再适用,推荐大家使用 watch 命令。
GDB 调试器支持在程序中打 3 种断点,分别为普通断点、观察断点和捕捉断点。其中 break 命令打的就是普通断点,而 watch 命令打的为观察断点
使用 GDB 调试程序的过程中,借助观察断点可以监控程序中某个变量或者表达式的值,只要发生改变,程序就会停止执行。相比普通断点,观察断点不需要我们预测变量(表达式)值发生改变的具体位置。
watch cond
其中,conde 指的就是要监控的变量或表达式。
和 watch 命令功能相似的,还有 rwatch 和 awatch 命令。其中:
- rwatch 命令:只要程序中出现读取目标变量(表达式)的值的操作,程序就会停止运行;
- awatch 命令:只要程序中出现读取目标变量(表达式)的值或者改变值的操作,程序就会停止运行。
如果我们想查看当前建立的观察点的数量,借助如下指令即可:
info watchpoints
GDB catch
捕捉断点的作用是,监控程序中某一事件的发生,例如程序发生某种异常时、某一动态库被加载时等等,一旦目标时间发生,则程序停止执行。
catch event
throw [exception]
,catch [exception]
,load [regexp]
unload [regexp]
注意,当前 GDB 调试器对监控 C++ 程序中异常的支持还有待完善,使用 catch 命令时,有以下几点需要说明:
- 对于使用 catch 监控指定的 event 事件,其匹配过程需要借助 libstdc++ 库中的一些 SDT 探针,而这些探针最早出现在 GCC 4.8 版本中。也就是说,想使用 catch 监控指定类型的 event 事件,系统中 GCC 编译器的版本最低为 4.8,但即便如此,catch 命令是否能正常发挥作用,还可能受到系统中其它因素的影响。
- 当 catch 命令捕获到指定的 event 事件时,程序暂停执行的位置往往位于某个系统库(例如 libstdc++)中。这种情况下,通过执行 up 命令,即可返回发生 event 事件的源代码处。
- catch 无法捕获以交互方式引发的异常。
GDB单步调试程序
GDB 调试器共提供了 3 种可实现单步调试程序的方法,即使用 next、step 和 until 命令
next
next 是最常用来进行单步调试的命令,其最大的特点是当遇到包含调用函数的语句时,无论函数内部包含多少行代码,next 指令都会一步执行完。也就是说,对于调用的函数来说,next 命令只会将其视作一行代码。
step
通常情况下,step 命令和 next 命令的功能相同,都是单步执行程序。不同之处在于,当 step 命令所执行的代码行中包含函数时,会进入该函数内部,并在函数第一行代码处停止执行。
until
until 命令可以简写为 u 命令,有 2 种语法格式,如下所示:
until until location
其中,参数 location 为某一行代码的行号。
不带参数的 until 命令,可以使 GDB 调试器快速运行完当前的循环体,并运行至循环体外停止。注意,until 命令并非任何情况下都会发挥这个作用,只有当执行至循环体尾部(最后一行代码)时,until 命令才会发生此作用;反之,until 命令和 next 命令的功能一样,只是单步执行程序。
GDB finish和return
实际调试时,在某个函数中调试一段时间后,可能不需要再一步步执行到函数返回处,希望直接执行完当前函数,这时可以使用 finish 命令。与 finish 命令类似的还有 return 命令,它们都可以结束当前执行的函数。
finish 命令和 return 命令的区别是,finish 命令会执行函数到正常退出;而 return 命令是立即结束执行当前函数并返回,也就是说,如果当前函数还有剩余的代码未执行完毕,也不会执行了。除此之外,return 命令还有一个功能,即可以指定该函数的返回值。
GDB jump
jump 命令的功能是直接跳到指定行继续执行程序,其语法格式为:
jump location
其中,location 通常为某一行代码的行号。
GDB print和display
对于在调试期间查看某个变量或表达式的值,GDB 调试器提供有 2 种方法,即使用 print 命令或者 display 命令。
print 命令,它的功能就是在 GDB 调试程序的过程中,输出或者修改指定变量或者表达式的值。
和print 命令一样,display命令也用于调试阶段查看某个变量或表达式的值,它们的区别是,使用 display命令查看变量或表达式的值,每当程序暂停执行(例如单步执行)时,GDB 调试器都会自动帮我们打印出来,而 print 命令则不会。
也就是说,使用 1 次 print 命令只能查看 1 次某个变量或表达式的值,而同样使用 1 次 display 命令,每次程序暂停执行时都会自动打印出目标变量或表达式的值。因此,当我们想频繁查看某个变量或表达式的值从而观察它的变化情况时,使用 display 命令可以一劳永逸。
(gdb) display expr
(gdb) display/fmt expr
/fmt | 功 能 |
---|---|
/x | 以十六进制的形式打印出整数。 |
/d | 以有符号、十进制的形式打印出整数。 |
/u | 以无符号、十进制的形式打印出整数。 |
/o | 以八进制的形式打印出整数。 |
/t | 以二进制的形式打印出整数。 |
/f | 以浮点数的形式打印变量或表达式的值。 |
/c | 以字符形式打印变量或表达式的值。 |
禁用和删除断点
常用的方式有 2 种:
- 使用 quit 命令退出调试,然后重新对目标程序启动调试,此方法将会消除上一次调试操作中建立的所有断点;
- 使用专门删除或禁用断点的命令,既可以删除某一个断点,也可以删除全部断点。
查看当前已有的断点
info break
clear 命令可以删除指定位置处的所有断点
(gdb) clear location
参数 location 通常为某一行代码的行号或者某个具体的函数名。当 location 参数为某个函数的函数名时,表示删除位于该函数入口处的所有断点。
delete 命令(可以缩写为 d )通常用来删除所有断点,也可以删除指定编号的各类型断点,语法格式如下:
delete [breakpoints] [num]
其中,breakpoints 参数可有可无,num 参数为指定断点的编号,其可以是 delete 删除某一个断点,而非全部。
禁用断点
disable [breakpoints] [num...]
对于禁用的断点,可以使用 enable 命令激活
enable [breakpoints] [num...] 激活用 num... 参数指定的多个断点,如果不设定 num...,表示激活所有禁用的断点
enable [breakpoints] once num… 临时激活以 num... 为编号的多个断点,但断点只能使用 1 次,之后会自动回到禁用状态
enable [breakpoints] count num... 临时激活以 num... 为编号的多个断点,断点可以使用 count 次,之后进入禁用状态
enable [breakpoints] delete num… 激活 num.. 为编号的多个断点,但断点只能使用 1 次,之后会被永久删除。
GDB调试多线程程序
调试命令 | 功 能 |
---|---|
info threads | 查看当前调试环境中包含多少个线程,并打印出各个线程的相关信息,包括线程编号(ID)、线程名称等。 |
thread id | 将线程编号为 id 的线程设置为当前线程。 |
thread apply id… command | id… 表示线程的编号;command 代指 GDB 命令,如 next、continue 等。整个命令的功能是将 command 命令作用于指定编号的线程。当然,如果想将 command 命令作用于所有线程,id… 可以用 all 代替。 |
break location thread id | 在 location 指定的位置建立普通断点,并且该断点仅用于暂停编号为 id 的线程。 |
set scheduler-locking off \on\step | 默认情况下,当程序中某一线程暂停执行时,所有执行的线程都会暂停;同样,当执行 continue 命令时,默认所有暂停的程序都会继续执行。该命令可以打破此默认设置,即只继续执行当前线程,其它线程仍停止执行。 |
要知道,使用 GDB 调试多线程程序时,同一时刻我们调试的焦点都只能是某个线程,被称为当前线程。整个调试过程中,GDB 调试器总是会从当前线程的角度为我们打印调试信息。如上所示,当执行 r 启动程序后,GDB 编译器自行选择标识号为 LWP 54283(编号为 2)的线程作为当前线程,则随后打印的暂停运行的信息就与该线程有关,而没有打印出编号为 1 和 3 的暂停信息。
GDB 调试器为了方便用户快速识别出当前线程,执行 info thread 命令后,Id 列前标有 * 号的线程即为当前线程。
thread id 命令用于将编号为 id 的线程设定为当前线程
如果想单独控制某一线程进行指定的操作,可以借助 thread apply id… command 命令实现:
(gdb) thread apply id... command
参数 id… 表示要控制的目标线程的编号,编号个数可以是多个。如果想控制所有线程,可以用 all 代替书写所有线程的编号;参数 command 表示要目标线程执行的操作,例如 next、continue 等。
默认情况下,无论哪个线程暂停执行,其它线程都会随即暂停;反之,一旦某个线程启动(借助 next、step、continue 命令),其它线程也随即启动。GDB 调试默认的这种调试模式(称为全停止模式),一定程序上可以帮助我们更好地监控程序中各个线程的执行。
当调试环境中拥有多个线程时,我们可以选择为特定的线程设置断点,该断点仅对指定线程有效。
(gdb) break location thread id
(gdb) break location thread id if...
前面提到,使用 GDB 调试多线程程序时,默认的调试模式为:一个线程暂停运行,其它线程也随即暂停;一个线程启动运行,其它线程也随即启动。要知道,这种调试机制确实能帮我们更好地监控各个线程的“一举一动”,但并非适用于所有场景。
一些场景中,我们可能只想让某一特定线程运行,其它线程仍维持暂停状态。要想达到这样的效果,就需要借助 set scheduler-locking 命令。 此命令可以帮我们将其它线程都“锁起来”,使后续执行的命令只对当前线程或者指定线程有效,而对其它线程无效。
GDB frame和backtrace命令
当程序因某种异常停止运行时,我们要做的就是找到程序停止的具体位置,分析导致程序停止的原因。
对于 C、C++ 程序而言,异常往往出现在某个函数体内,例如 main() 主函数、调用的系统库函数或者自定义的函数等。要知道,程序中每个被调用的函数在执行时,都会生成一些必要的信息,包括:
- 函数调用发生在程序中的具体位置;
- 调用函数时的参数;
- 函数体内部各局部变量的值等等。
这些信息会集中存储在一块称为“栈帧”的内存空间中。也就是说,程序执行时调用了多少个函数,就会相应产生多少个栈帧,其中每个栈帧自函数调用时生成,函数调用结束后自动销毁。
注意,这些栈帧所在的位置也不是随意的,它们集中位于一个大的内存区域里,我们通常将其称为栈区或者栈。
这也就意味着,当程序因某种异常暂停执行时,如果其发生在某个函数内部,我们可以尝试借助该函数对应栈帧中记录的信息,找到程序发生异常的原因。
GDB frame
通过阅读上文我们知道,任何一个被调用的函数,执行时都会生成一个存储必要信息的栈帧。对于 C、C++ 程序而言,其至少也要包含一个函数,即 main() 主函数,这意味着程序执行时至少会生成一个栈帧。
main() 主函数对应的栈帧,又称为初始帧或者最外层的帧。
除此之外,每当程序中多调用一个函数,执行过程中就会生成一个新的栈帧。更甚者,如果该函数是一个递归函数,则会生成多个栈帧。
在程序内部,各个栈帧用地址作为它们的标识符,注意这里的地址并不一定为栈帧的起始地址。我们知道,每个栈帧往往是由连续的多个字节构成,每个字节都有自己的地址,不同操作系统为栈帧选定地址标识符的规则不同,它们会选择其中一个字节的地址作为栈帧的标识符。
然而,GDB 调试器并没有套用地址标识符的方式来管理栈帧。对于当前调试环境中存在的栈帧,GDB 调试器会按照既定规则对它们进行编号:当前正被调用函数对应的栈帧的编号为 0,调用它的函数对应栈帧的编号为 1,以此类推。
frame 命令的常用形式有 2 个: 1) 根据栈帧编号或者栈帧地址,选定要查看的栈帧,语法格式如下:
(gdb) frame spec
该命令可以将 spec 参数指定的栈帧选定为当前栈帧。spec 参数的值,常用的指定方法有 3 种:
- 通过栈帧的编号指定。0 为当前被调用函数对应的栈帧号,最大编号的栈帧对应的函数通常就是 main() 主函数;
- 借助栈帧的地址指定。栈帧地址可以通过 info frame 命令(后续会讲)打印出的信息中看到;
- 通过函数的函数名指定。注意,如果是类似递归函数,其对应多个栈帧的话,通过此方法指定的是编号最小的那个栈帧。
除此之外,对于选定一个栈帧作为当前栈帧,GDB 调试器还提供有 up 和 down 两个命令。其中,up 命令的语法格式为:
(gdb) up n
其中 n 为整数,默认值为 1。该命令表示在当前栈帧编号(假设为 m)的基础上,选定 m+n 为编号的栈帧作为新的当前栈帧。
相对地,down 命令的语法格式为:
(gdb) down n
其中 n 为整数,默认值为 1。该命令表示在当前栈帧编号(假设为 m)的基础上,选定 m-n 为编号的栈帧作为新的当前栈帧。
2) 借助如下命令,我们可以查看当前栈帧中存储的信息:
(gdb) info frame
该命令会依次打印出当前栈帧的如下信息:
- 当前栈帧的编号,以及栈帧的地址;
- 当前栈帧对应函数的存储地址,以及该函数被调用时的代码存储的地址
- 当前函数的调用者,对应的栈帧的地址;
- 编写此栈帧所用的编程语言;
- 函数参数的存储地址以及值;
- 函数中局部变量的存储地址;
- 栈帧中存储的寄存器变量,例如指令寄存器(64位环境中用 rip 表示,32为环境中用 eip 表示)、堆栈基指针寄存器(64位环境用 rbp 表示,32位环境用 ebp 表示)等。
- 除此之外,还可以使用
info args
命令查看当前函数各个参数的值;使用info locals
命令查看当前函数中各局部变量的值。GDB backtrace
backtrace 命令用于打印当前调试环境中所有栈帧的信息,常用的语法格式如下:
(gdb) backtrace [-full] [n]
GDB搜索源码
在调试文件时,某些时候可能会去找寻找某一行或者是某一部分的代码。可以使用 list 显示全部的源码,然后进行查看。当源文件的代码量较少时,我们可以使用这种方式搜索。如果源文件的代码量很大,使用这种方式寻找效率会很低。所以 GDB 中提供了相关的源代码搜索的的 search 命令。
search <regexp> reverse-search <regexp>
参考