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 等命令,即可随意切换到当前环境中的各个进程,并对它们进行调试。

 

参考:

  1. http://c.biancheng.net/gdb/

 

posted @ 2021-12-30 21:15  烟消00云散  阅读(617)  评论(0编辑  收藏  举报