GDB调试学习总结
GDB调试
1. GDB调试的背景和功能
(1)为什么要用GDB调试
程序不按照预期逻辑走,需要找到错误发生的起始位置;程序异常挂掉,通过调试core文件,找到挂掉的最后位置。
GDB是类unix下代码调试工具,是开发必须具备的技能,通过调试理清代码的逻辑走向,可以改变变量值测试不同的分置逻辑。
(2)GDB调试的功能
GDB 的主要功能是监控程序的执行流程,具体主要有:
1)程序启动时,可以按照我们自定义的要求运行程序,例如设置参数和环境变量;
2)可使被调试程序在指定代码处暂停运行,并查看当前程序的运行状态(例如当前变量的值,函数的执行结果等),即支持断点调试;
3)程序执行过程中,可以改变某个变量的值,还可以改变代码的执行顺序,从而尝试修改程序中出现的逻辑错误。
2. GDB调试的准备和步骤
(1)生成符合GDB要求的可执行文件
仅使用 gcc(或 g++)命令编译生成的可执行文件,是无法借助 GDB 进行调试的。因为使用 GDB 调试某个可执行文件,该文件中必须包含必要的调试信息(比如各行代码所在的行号、包含程序中所有变量名称的列表(又称为符号表)等)。
使用 gcc -g 选项编译源文件,即可生成满足 GDB 要求的可执行文件。 例如:
gcc main.c -o main.exe -g
(2)启动GDB调试
-
调试可执行程序:
gdb xxx.exe
或者gdbtui xxx.exe
-
调试进程:
gdb -p 进程号
或者gdbtui -p 进程号
调试指令 | 作 用 |
---|---|
(gdb) break xxx (gdb) b xxx | 在源代码指定的某一行设置断点,其中 xxx 用于指定具体打断点的位置。 |
(gdb) run (gdb) r | 执行被调试的程序,其会自动在第一个断点处暂停执行。 |
(gdb) continue (gdb) c | 当程序在某一断点处停止运行后,使用该指令可以继续执行,直至遇到下一个断点或者程序结束。 |
(gdb) next (gdb) n | 令程序一行代码一行代码的执行。 |
(gdb) print xxx (gdb) p xxx | 打印指定变量的值,其中 xxx 指的就是某一变量名。 |
(gdb) list (gdb) l | 显示源程序代码的内容,包括各行代码所在的行号。 |
(gdb) quit (gdb) q | 终止调试。 |
3. 调用GDB调试器的几种方式
(1)直接使用GDB指令启动GDB调试器
此方式启动的 GDB 调试器,由于事先未指定要调试的具体程序,因此需启动后借助 file 或者 exec-file 命令指定。
(2)调试尚未执行的程序
格式:gdb program
(3)调试正在执行的程序
1)gdb attach PID
2)gdb 文件名 PID
3)gdb -p PID (PID表示进程号)
(4)调试异常崩溃的程序
首先执行指令 ulimit -c unlimited 来开启系统的core dump功能,这时当程序执行发生异常崩溃时,系统就可以自动生成相应的core文件。
GDB调试core文件:
gdb 可执行文件 -c core文件
查看堆栈信息可知道程序挂哪一行代码处。
4. GDB断点
(1)普通断点
2)b 函数名
例如:b Func
2)b 文件名:函数名
例如:b test.c:Func
3)b 文件名:行号
例如:b test.c:47
4)条件断点 b [断点处]
条件 :
-
如果断点处判断的是整型变量:
b test.c:125 if n==1
-
如果断点处判断的是字符型的变量 :
b test.c:175 if strcmp(str,"0000000001")==0
(此处的断点处最好以文件名:行号来表示更明确)
(2)观察断点
1)观察断点可以用来监控某变量或表达式的值是否发生变化, 只有当被监控变量(表达式)的值发生改变,程序才会停止运行。
语法格式:watch [变量或表达式]
和 watch 命令功能相似的,还有 rwatch 和 awatch 命令。其中:
-
rwatch 命令:只要程序中出现读取目标变量(表达式)的值的操作,程序就会停止运行;
awatch 命令:只要程序中出现读取目标变量(表达式)的值或者改变值的操作,程序就会停止运行。
2)查看观察断点数量
info watchpoints
(3)捕捉断点
1)普通断点作用于程序中的某一行,当程序运行至此行时停止执行,观察断点作用于某一变量或表达式,当该变量(表达式)的值发生改变时,程序暂停。而捕捉断点的作用是,监控程序中某一事件的发生,例如程序发生某种异常时、某一动态库被加载时等等,一旦目标事件发生,则程序停止执行。用捕捉断点监控某一事件的发生,等同于在程序中该事件发生的位置打普通断点。
语法格式:catch event
常用的event事件类型如表所示:
event 事件 | 含 义 |
---|---|
throw [exception] | 当程序中抛出 exception 指定类型异常时,程序停止执行。如果不指定异常类型(即省略 exception),则表示只要程序发生异常,程序就停止执行。 |
catch [exception] | 当程序中捕获到 exception 异常时,程序停止执行。exception 参数也可以省略,表示无论程序中捕获到哪种异常,程序都暂停执行。 |
load [regexp] unload [regexp] | 其中,regexp 表示目标动态库的名称,load 命令表示当 regexp 动态库加载时程序停止执行;unload 命令表示当 regexp 动态库被卸载时,程序暂停执行。regexp 参数也可以省略,此时只要程序中某一动态库被加载或卸载,程序就会暂停执行 |
(4)查看断点
语法格式:info b(break) [n]
(break也可以是watchpoint、catchpoint)
参数n为可选参数,为某个断点的编号,表示查看指定断点而非全部断点。
(5)删除断点
1)clear
语法格式:clear location
其中,location 通常为某一行代码的行号或者某个具体的函数名。当 location 参数为某个函数的函数名时,表示删除位于该函数入口处的所有断点。
2)delete
语法格式:delete [breakpoints] [n]
其中,breakpoints 参数可有可无,n参数为指定断点的编号,其可以是 delete 删除某一个断点,而非全部。
(6)禁用断点
语法格式:disable [breakpoints] [n...]
其中,breakpoints 参数可有可无,n...表示可以有多个参数,每个参数都为要禁用的断点编号, 如果指定 n...,disable 命令会禁用指定编号的断点;反之若不设定 n...,则 disable 会禁用当前程序中所有的断点。
(7)输入c(contiune)然后等待程序跑到断点处
5. GDB常用命令
(1)打印变量
1)print命令
print命令可以缩写为p,语法格式为:p [变量或表达式]
如果变量是结构体或者字符串太长,显示的时候会被截断,这时候需要设置参数。
set print element 0
这样再p 一次就能显示全部信息
2)display命令
display命令和print命令的区别是,display命令在每次程序暂停执行时都会自动打印出目标变量或表达式的值,从而可以观察变量的变化。
语法格式为:display [变量或表达式](没有缩写形式)
(2)单步调试
1)next命令
next命令可以缩写为n命令,语法格式为:next count
其中,count表示单步执行多少行代码,默认为1行。
2)step命令
step命令可以缩写为s命令,用法和next相同,语法格式为:step count
不同之处在于,当step命令执行的代码中包含函数时,会进入该函数内部,并在函数第一行代码处停止执行。
3)until命令
until命令可以简写为u命令,语法格式为:until或until location
其中,location为某一行代码的行号。 不带参数的 until 命令,可以使 GDB 调试器快速运行完当前的循环体,并运行至循环体外停止。注意,until 命令并非任何情况下都会发挥这个作用,只有当执行至循环体尾部(最后一行代码)时,until 命令才会发生此作用;反之,until 命令和 next 命令的功能一样,只是单步执行程序。
(3)退出函数:finish
(4)查看函数堆栈:bt
(5)退出调试:quit
6. GDB调试多线程程序
GDB 调试器不仅仅支持调试单线程程序,还支持调试多线程程序。本质上讲,使用 GDB 调试多线程程序的过程和调试单线程程序类似,不同之处在于,调试多线程程序需要监控多个线程的执行过程,进而找到导致程序出现问题的异常或 Bug,而调试单线程程序只需要监控 1 个线程。
(1)常用命令
调试命令 | 功 能 |
---|---|
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 命令时,默认所有暂停的程序都会继续执行。该命令可以打破此默认设置,即只继续执行当前线程,其它线程仍停止执行。 |
调试程序实例如下:
#include <stdio.h>
#include <pthread.h>
void* thread_job(void*name)
{
char * thread_name = (char*)name;
printf("this is %s\n",thread_name);
}
int main()
{
pthread_t tid1,tid2;
pthread_create(&tid1, NULL, thread_job, "thread1_job");
pthread_create(&tid2, NULL, thread_job, "thread2_job");
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
printf("this is main\n");
return 0;
}
1)GDB查看所有线程
info threads 命令的功能有 2 个,既可以查看当前调试环境下存在的线程数以及各线程的具体信息,也可以通过指定线程的编号查看某个线程的具体信息。
info threads 命令的完整语法格式如下:
(gdb) info threads [id...]
以 main.exe 程序为例,如图所示:
其中,[Switching to Thread 0x7ffff7d9f700 (LWP 54283)]
表示该线程作为当前线程,因为它最先碰到断点, 执行info threads命令后,Id 列表示各个线程的编号(ID 号),Id 列前标有 * 号的线程即为当前线程;Target Id 列表示各个线程的标识符;Frame 列打印各个线程执行的有关信息,例如线程名称,线程暂停的具体位置等。
使用 GDB 调试多线程程序时,同一时刻我们调试的焦点都只能是某个线程,被称为当前线程。整个调试过程中,GDB 调试器总是会从当前线程的角度为我们打印调试信息。如上所示,当执行 r 启动程序后,GDB 编译器自行选择标识号为 LWP 54283(编号为 2)的线程作为当前线程,则随后打印的暂停运行的信息就与该线程有关,而没有打印出编号为 1 和 3 的暂停信息。
2)GDB调整当前线程
用 GDB 调试多线程程序的过程中,根据需要可以随时对当前线程进行调整,这就需要用到 thead id 命令。如下图所示:
3)GDB执行特定线程
如果想单独控制某一线程进行指定的操作,可以借助 thread apply id... command 命令实现:
(gdb) thread apply id... command
参数 id... 表示要控制的目标线程的编号,编号个数可以是多个。如果想控制所有线程,可以用 all 代替书写所有线程的编号;参数 command 表示要目标线程执行的操作,例如 next、continue 等。如下所示:
当调用 thread apply 2 next 命令对 2 号线程进行逐步调试时,3 号线程也会运行,因为这和 GDB 调试器的调试机制有关。
默认情况下,无论哪个线程暂停执行,其它线程都会随即暂停;反之,一旦某个线程启动(借助 next、step、continue 命令),其它线程也随即启动。GDB 调试默认的这种调试模式(称为全停止模式),一定程序上可以帮助我们更好地监控程序中各个线程的执行。
4)GDB为特定线程设置断点
当调试环境中拥有多个线程时,我们可以选择为特定的线程设置断点,该断点仅对指定线程有效。命令如下所示:
(gdb) break location thread id
(gdb) break location thread id if...
location 表示设置断点的具体位置;id 表示断点要作用的线程的编号;if... 参数作用指定断点激活的条件,即只有条件符合时,断点才会发挥作用。
默认情况下,当某个线程执行遇到断点时,GDB 调试器会自动将该线程作为当前线程,并提示用户 "[Switching to Thread n]",其中 n 即为新的当前线程。
例如:
可以看到,我们在第 7 行代码处为 2 号线程单独设置了一个普通断点,该断点仅对 2 号线程有效。
5)GDB设置线程锁
一些场景中,我们可能只想让某一特定线程运行,其它线程仍维持暂停状态。要想达到这样的效果,就需要借助 set scheduler-locking 命令。 此命令可以帮我们将其它线程都“锁起来”,使后续执行的命令只对当前线程或者指定线程有效,而对其它线程无效。语法格式如下:
(gdb) set scheduler-locking mode
其中,参数 mode 的值有 3 个,分别为 off、on 和 step(如果为空,则查看当前各个线程的状态),它们的含义分别是:
-
off:不锁定线程,任何线程都可以随时执行;
-
on:锁定线程,只有当前线程或指定线程可以运行;
-
step:当单步执行某一线程时,其它线程不会执行,同时保证在调试过程中当前线程不会发生改变。但如果该模式下执行 continue、until、finish 命令,则其它线程也会执行,并且如果某一线程执行过程遇到断点,则 GDB 调试器会将该线程作为当前线程。
示例如下:
(2)GDB non-stop模式
对于调试多线程程序,GDB 默认采用的是 all-stop 模式,即只要有一个线程暂停执行,所有线程都随即暂停。这种调试模式可以适用于大部分场景的需要,借助适当数量的断点,我们可以清楚地监控到各个线程的具体执行过程。
但在某些场景中,我们可能需要调试个别的线程,并且不想在调试过程中,影响其它线程的运行。这种情况下,可以将 GDB 的调试模式由 all-stop 模式更改为 non-stop 模式,该模式下调试多线程程序,当某一线程暂停运行时,其它线程仍可以继续执行。
也就是说,non-stop 模式下可以进行 all-stop 模式无法做到的调试工作,例如:
-
保持其它线程继续执行的状态下,单独调试某个线程;
-
在所有线程都暂停执行的状态下,单独调试某个线程;
-
单独执行多个线程等等。
另外还有一点和 all-stop 模式不同的是,在 all-stop 模式下,continue、next、step 命令的作用对象并不是当前线程,而是所有的线程;但在 non-stop 模式下,continue、next、step 命令只作用于当前线程。在 non-stop 模式下,如果想要 continue 命令作用于所有线程,可以为 continue 命令添加一个 -a 选项,即执行 continue -a 或者 c -a 命令,即可实现令所有线程继续执行的目的。
转换到 non-stop 模式命令如下:
(gdb) set non-stop mode
其中,mode 参数的值有 2 种,分别是 on 和 off,on 表示启用 non-stop 模式;off 表示禁用 non-stop 模式。
以调试如下程序为例:
#include <stdio.h>
#include <pthread.h>
static void *thread1_job()
{
printf("this is 1\n");
}
static void *thread2_job()
{
sleep(1);
printf("this is 2\n");
}
int main()
{
pthread_t tid1,tid2;
pthread_create(&tid1, NULL, thread1_job, NULL);
pthread_create(&tid2, NULL, thread2_job, NULL);
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
printf("this is main\n");
return 0;
}
在程序第5行打断点,调试结果如下所示:
可以看到,如果在 all-stop 模式下,thread1_job 线程的暂停执行势必会导致 main 主线程和 thread2_job 线程暂停执行;但在 non-stop 模式下却完全相反,thread1_job 线程的暂停,并未影响到 main 主线程和 thread2_job 线程,其中 thread2_job 线程执行完毕后自动退出,而 main 主线程一直在运行(等待 thread1_job 线程执行结束)。此外,在 all-stop 模式下,当某一线程暂停执行时,GDB 调试器会自行将其切换为当前线程;而在 non-stop 模式下不会。
7. GDB后台执行调试命令
GDB 调试器其实提供有 2 种执行方式:
-
同步执行:“一个一个”的执行,即必须等待前一个命令执行完毕,才能执行下一个调试命令。
-
后台执行:又称“异步执行”,即当某个调试命令开始执行时,(gdb) 命令提示符会立即出现,我们无需等待前一个命令执行完毕就可以继续执行下一个调试命令。
以后台(异步)的方式执行一个调试命令,其语法格式如下:
(gdb) command&
其中,command 表示就是要执行的调试命令。command 和 & 之间不需要添加空格。通过在目标命令的后面添加一个 '&' 字符,即可使该命令以后台的方式执行。例如,continue 命令的异步执行版本为continue&
或者c&
。后台执行命令异步调试程序的方法,多用于 non-stop 模式中。
支持后台执行的一些常用的调试命令如下:
调试命令 | 含 义 |
---|---|
run(r) | 启动被调试的程序。 |
attach | 调试处于运行着的的程序。 |
step | 单步调试程序。 |
stepi | 执行一条机器指令。 |
next(n) | 单步调试程序。 |
nexti | 执行一条机器指令,其与 stepi 命令的区别,类似于 step 和 next 命令的区别。 |
continue | 继续执行程序。 |
finish | 结束当前正在执行的函数。 |
until(u) | 快速执行完当前的循环体。 |
GDB interrupt命令:暂停后台线程执行。
在 all-stop 模式下,interrupt 命令作用于所有线程,即该命令可以令整个程序暂停执行;而在 non-stop 模式下,interrupt 命令仅作用于当前线程。 如果想另其作用于所有线程,可以执行 interrupt -a 命令。
8. GDB调试多进程程序
GDB可以调试多进程程序。无论父进程还是子进程,都可以借助 attach 命令启动 GDB 调试它。attach 命令用于调试正在运行的进程,要知道对于每个运行的进程,操作系统都会为其配备一个独一无二的 ID 号。在得知目标子进程 ID 号的前提下,就可以借助 attach 命令来启动 GDB 对其进行调试。
1)GDB显示指定要调试的进程
GDB follow-fork-mode选项
对于使用 fork() 或者 vfork() 函数构建的多进程程序,借助 follow-fork-mode 选项可以设定 GDB 调试父进程还是子进程。该选项的使用语法格式为:
(gdb) set follow-fork-mode mode
参数 mode 的可选值有 2 个:
-
parent:选项的默认值,表示 GDB 调试器默认只调试父进程;
-
child:和 parent 完全相反,它使的 GDB 只调试子进程。且当程序中包含多个子进程时,我们可以逐一对它们进行调试。
GDB detach-on-fork选项
借助 follow-fork-mode 选项,我们只能选择调试子进程还是父进程,且一经选定,调试过程中将无法改变。如果既想调试父进程,又想随时切换并调试某个子进程,就需要借助 detach-on-fork 选项。语法格式如下:
(gdb) set detach-on-fork mode
其中,mode 参数的可选值有 2 个:
-
on:默认值,表明 GDB 只调试一个进程,可以是父进程,或者某个子进程;
-
off:程序中出现的每个进程都会被 GDB 记录,我们可以随时切换到任意一个进程进行调试。
和 detach-on-fork 搭配使用的,还有如表 1 所示的几个命令。
命令语法格式 | 功 能 |
---|---|
(gdb)show detach-on-fork | 查看当前调试环境中 detach-on-fork 选项的值。 |
(gdb) info inferiors | 查看当前调试环境中有多少个进程。其中,进程 id 号前带有 * 号的为当前正在调试的进程。 |
(gdb) inferiors id | 切换到指定 ID 编号的进程对其进行调试。 |
(gdb) detach inferior id | 断开 GDB 与指定 id 编号进程之间的联系,使该进程可以独立运行。不过,该进程仍存在 info inferiors 打印的列表中,其 Describution 列为 <null>,并且借助 run 仍可以重新启用。 |
(gdb) kill inferior id | 断开 GDB 与指定 id 编号进程之间的联系,并中断该进程的执行。不过,该进程仍存在 info inferiors 打印的列表中,其 Describution 列为 <null>,并且借助 run 仍可以重新启用。 |
remove-inferior id | 彻底删除指令 id 编号的进程(从 info inferiors 打印的列表中消除),不过在执行此操作之前,需先使用 detach inferior id 或者 kill inferior id 命令将该进程与 GDB 分离,同时确认其不是当前进程。 |
2)调试多进程程序示例
调试的代码如下:
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t pid = fork();
if(pid == 0)
{
printf("this is child,pid = %d\n",getpid());
int num = 10;
while(num == 10)
{
sleep(10);
}
printf("this is child,pid = %d\n",getpid());
}
else
{
sleep(1);
printf("this is parent,pid = %d\n",getpid());
}
return 0;
}
无论父进程还是子进程,都可以借助 attach 命令启动 GDB 调试它。attach 命令用于调试正在运行的进程,要知道对于每个运行的进程,操作系统都会为其配备一个独一无二的 ID 号。在得知目标子进程 ID 号的前提下,就可以借助 attach 命令来启动 GDB 对其进行调试。但是如果 set follow-fork-mode 命令,可以直接设置调试子进程。
首先用 set follow-fork-mode child 命令设置只调试子进程,调试结果如下图:
然后将父进程的代码也写成死循环,再用 set detach-on-fork off 命令设置GDB可以调试多个进程,调试结果如下图:
上图中,使用info inferiors查看子进程id为2,然后用 inferiors 2 命令切换到子进程调试子进程。这说明,通过设置 detach-on-fork 选项值为 off,再配合使用 info inferiors 等命令,即可随意切换到当前环境中的各个进程,并对它们进行调试。
参考: