GDB(全称:GNU Debugger)是GNU工程师为GNU操作系统开发的调试器。它可以用于调试C、C++、Objective-C、Pascal、Ada等语言编写的程序。
在程序编译的时候,添加响应的调试信息,才能使程序使用GDB进行调试,以CMake为例,示范添加调试信息的方法:
- SET(CMAKE_BUILD_TYPE "Debug") # 使得生成的程序包含调试信息
-
- SET(CMAKE_CXX_FLAGS_DEBUG "$ENV{CXXFLAGS} -O0 -Wall -g -ggdb")
-
- SET(CMAKE_CXX_FLAGS_RELEASE "$ENV{CXXFLAGS} -O3 -Wall")
设置的具体含义可参考《CMAKE学习笔记》
注:
3.1 GDB启动方法
gdb xxx // xxx表示需要调试的程序名
3.2 GDB常用命令
| 命令 | 简写 | 功能 |
| file | 载入需要调试的可执行文件 | |
| kill | 终止正在调试的程序 | |
| list | l | 列出部分源代码,可列出的是正在执行的位置附近的源代码。 1. 输入list后,每次列出大概10行左右的代码,重复enter可不断 列出后续的源代码 2. list linenum,可列出特定行linenum附近的源代码 3. list filename:linenum 列出特定文件的特定行的源代码 |
| next | n | 执行一行源代码,但是不进入函数内部 |
| step | 执行一行源代码,且可以进入函数内部 | |
| run | r | 开始执行当前被调试的程序,遇到第一个断点的时候停下来,如果没有断点,会直接往下运行。 |
| continue | c | 继续运行程序到下一个断点的位置 |
| start | 运行程序并停在主函数开始的地方(即使没有断点) | |
| break | b | 设置断点 1. b linenum 在当前文件的linenum行设置断点 2. b filename:linename 在特定文件的特定行设置断点 3. b func 在函数func处设置断点 4. b filename:func 在特定文件func函数处设置断点 5. b linenum if i==x 在某行设置条件断点 |
| info b | 查看算有断点 | |
| watch | 监视一个变量的值,在调试过程中,变量的值发生变化的时候程序会停在变量值发生变化的位置,watch的优点是不需要提前预知到变量的值在哪里会发生变化而去打断点,在变量被watch之后,在那个地方变量的值发生了改变,程序就会停在这个地方(相当于到了断点) | |
| rwatch | 只要程序中出现读取目标变量的值,则程序就会停在读取的位置处 | |
| awatch | 只要程序中出现读取目标变量的值或者修改目标变量的值,则程序就会停在读取或者修改值的位置处 | |
| p | 查看一个变量的值 | |
| display | 与print类似,也是用于在调试过程中查看变量或者表达式的值,但使用display不仅在执行该命令的同时会看到目标变量的值,后续每次程序停止执行时(停在断点处),GDB 调试器都会将目标变量的值打印出来。 | |
| whatis | 显示变量或者函数的类型 | |
| ptype | 显示结构的定义,如结构体类型的具体定义 | |
| make | 不退出gdb,重新生成可执行文件 | |
| shell | 再不退出GDB的情况下,可执行Linux shell命令 | |
| info b | 打印输出所有设置的断点 | |
| info watchpoints | 打印输出所有的观察点 | |
| info files | 显示被调试文件的详细信息 | |
| info args | 查看传入当前函数的参数值 | |
| info func | 显示所有的函数名称 | |
| info prog | 显示被调试程序的执行状态 | |
| info locals | 打印函数内所有的变量值 | |
| info inferiors | 显示当前调试程序的所有进程 (父进程和子进程) | |
| inferior n | 切换到进程n(多进程程序调试) | |
| info frame | 查看所有的栈帧信息 | |
| frame n | 查看栈信息, n为栈帧的编号 | |
| up n | 在当前栈帧编号(假设为t)的基础上,将编号t+n的栈帧作为新的栈帧,n的默认值为1 (可理解为在栈中进行上下移动) | |
| down n | 在当前栈帧编号(假设为t)的基础上,将编号t-n的栈帧作为新的栈帧,n的默认值为1 (可理解为在栈中进行上下移动) | |
| backtrace | bt | 查看栈信息: backtrace n :打印最里层的n的栈帧的信息 backtrace -n: 打印最外层的n个栈帧的信息 backtrace -full 打印栈帧信息的同时打印局部变量的值 |
| where | 显示当前程序运行到哪一个文件的哪一行 | |
| enable n | 使能断点n | |
| disabke n | 禁用断点n | |
| del n | 删除断点n del m n t 删除多个断点 | |
| finish | 终止当前函数并返回到函数调用点 | |
| set variable | 设置变量的值,当程序运行到某个地方停住,如果想改变这个位置前某一个变量的值,则可以使用set variable来实现修改: set variable data=1 set variable buffer="testcon" | |
| call name(args) | 调用并执行函数name,传递的参数是args | |
| return val | 停止当前函数,并将值val返回给函数调用者 | |
| quit | q | 退出GDB |
关于watch命令的补充:
watch命令实现变量监视机制的方式有两种
软件观察点:watch命令监视目标变量或者表达式之后,GDB调试器会以单步执行的方式运行程序,在运行完每一行代码之后,都会区检测目标变量或者表达式的值是否发生了变化,如果改变,则程序会停止在值发生变化的位置。这种机制会降低程序的调试效率,但是调试程序的目的是为了查找到其中的bug,所以一定程度的效率降低并不是关注的重点。
硬件观察点:系统会为GDB提供少量的寄存器(Intel x86 提供4个调试寄存器),每个寄存器可以作为一个观察点,协助GDB完成变量监视,这种机制在同样实现变量监视的同时,不会影响程序的调试效率。
因为系统提供的调试寄存器数量有限,因此如果在程序中设置过多的硬件观察点,则可能会导致观察点失效,此时GDB会提示:
Hardware watchpoint num: Could not insert watchpoint
此时需要删除或者禁用一些观察点。
此外,调试寄存器的大小固定,因此不能用硬件观察点来监视占用字节数较多的变量(比如一些操作系统中,GDB只能监视4字节长度的数据,如 long 类型监视不了,可以尝试转换为 int 类型)。目前大多数系统都支持建立硬件观察点,所以GDB调试在建立观察点的时候,会优先建立硬件观察点,只有当系统不支持硬件观察点的时候,才会去建立软件观察点。使用如下命令,可强制GDB只建立软件观察点:
set can-use-hw-watchpoints 0
注:awatch 和 rwatch 命令只能设置硬件观察点,当系统不支持硬件观察点的时候,GDB会打印输出如下信息:
Expression cannot be implemented with read/access watchpoint.
关于display命令的补充:
display命令还支持将变量值通过特定的格式进行输出:
display/fmt variable
| /fmt | 描述 |
| /d | 以有符号、十进制的形式打印出整数。 |
| /x | 以十六进制的形式打印出整数。 |
| /u | 以无符号、十进制的形式打印出整数。 |
| /t | 以二进制的形式打印出整数。 |
| /o | 以八进制的形式打印出整数。 |
| /f | 以浮点数的形式打印变量或表达式的值。 |
| /c | 以字符形式打印变量或表达式的值。 |
通过display显示的变量或者表达式,都会被记录在自动显示列表中,可通过执行如下命令,查看列表中记录的所有变量或者表达式:
info dispaly

Num: GDB为列表中的变量或者表达式提供的唯一编号
Enb: 列表中的变量是处于激活状态还是禁止状态(y/n)
Expression: 列表中的变量或者表达式
可使用如下命令删除自动显示列表中的某个变量:
- undisplay n
- delete display n
可使用如下命令使能或者禁用自动显示列表中的某个变量:
- enable display n // 使能
- disable display n // 禁止
关于frame命令的补充
在程序中每个被调用的函数在执行的时候,都会生成与此函数相关的一些基本信息,这些基本信息包括:
这些基础信息会存储在一块称栈帧的内存空间中,即程序运行时,每调用一个函数,就会生成一个对应的栈帧,程序调用结束的时候,栈帧会自动销毁。而这些栈帧的存储位置集中在一块特定的内存区域,称之为栈或者栈区。(在程序执行的时候都会占用一整块内存空间,且这块内存空间会被细分为多个不同的区域,例如栈区、堆区、全局数据区、常量区等,用以存储程序中不同的资源)。
因此当程序因在某个函数中存在某种错误而停止执行的时候,可以通过程序的栈帧记录的信息,查找程序异常停止的原因(C,C++程序中至少存在一个函数,即main函数,因此也会至少生成一个栈帧)。
frame命令的用法:
frame spec
通过上述命令,可以将指定的栈帧选定为当前的栈帧,spec参数可以指定为:
栈帧的编号以及栈帧的地址,都可以通过如下命令进行查询:
info frame

通过info frame可以查看到栈帧的如下信息:
-----------------------------------------------分割线------------------------------------------------
多进程程序调试,首先启动GDB调试,接着需要做两个设置:
- set follow-fork-mode child
- set detach-on-fork off
follow-fork-mode: 可取值为:child , parent, 用于设置GDB跟踪子进程还是父进程,在进行多进程程序调试的时候,可设置为跟踪子进程。
detach-on-fork: 可取值为off 或者 on, 表示调试当前进程的时候,其他进程是否继续运行,当设置为off的时候,调试当前进程,其他进程会被GDB挂起。当设置为on,调试当前进程的时候,其他进程会继续运行,
可以通过如下语句查看设置值:
- show follow-fork-mode
- show detach-on-fork
在设置上述的两个选项之后,即可开始调试多进程程序,在遇到fork()进程之后,GDB会自动切换新fork出的进程里面,原来的进程则被GDB挂起,可通过如下语句查看目前程序的所有进程:
info inferiors

可看到当前程序共有两个进程,可通过如下命令在不同进程之间进行切换:
inferior n
其中n表示info输出的进程的Num号,而不是进程号
使用如下命令可使进程脱离GDB调试:
detach inferiors n
5. GDB调试多线程程序
Linux环境下的线程本质上依然是进程,称之为轻量级进程(Light Weight Process, LWP),计算机是以进程作为资源分配的最小单位。而线程是操作系统调度执行的最小单位。
测试代码如下所示:
- #include
- #include
- #include
- #include
-
- // 编写多线程测试程序
- pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
- pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
- pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;
-
- void* worker1(void* args)
- {
-
- pthread_mutex_lock(&mutex);
-
- int* arrs = (int*)args;
-
- pthread_t tid = pthread_self(); // 获取线程ID
-
- for (size_t i = 0; i < 10; i++)
- {
- /* code */
- arrs[0]++;
-
- sleep(1);
-
- printf("Thread %d current cnt value is %d\n", tid, arrs[0]);
- }
-
-
- pthread_mutex_unlock(&mutex);
-
- return NULL;
- }
-
- void* worker2(void* args)
- {
- pthread_mutex_lock(&mutex1);
-
- int* arrs = (int*)args;
-
- pthread_t tid = pthread_self();
-
- for (size_t i = 0; i < 10; i++)
- {
- /* code */
- arrs[1]++;
-
- sleep(1);
-
- printf("Thread %d current cnt value is %d\n", tid, arrs[1]);
- }
-
- pthread_mutex_unlock(&mutex1);
-
- return NULL;
- }
-
- int main(int argc, char* argv[])
- {
- int array[2] = {0};
-
- pthread_t thread1, thread2;
-
- pthread_create(&thread1, NULL, worker1, array);
- pthread_create(&thread2, NULL, worker2, array);
-
- pthread_detach(thread1);
- pthread_detach(thread2);
-
- //pthread_join(thread1, NULL);
- //pthread_join(thread2, NULL);
-
- while (true)
- {
- // 等待两个子线程运行结束,主线程才能结束
- // 否则会由于主线程的提前推出而导致子线程执行失败
- // printf("Waiting for child thread to terminate...\n");
- pthread_mutex_lock(&mutex2);
-
- if (array[0] >= 10 && array[1] >=10)
- {
- break;
- }
-
- pthread_mutex_unlock(&mutex2);
- }
-
- printf("The Main thread terminate!\n");
-
- return 0;
- }
-
运行结果:

开始调试:
在进程多线程调试的时候,我们需要设置,让调试当前线程的时候,其他的线程能够被GDB挂起,可通过如下命令设置命令设置线程锁:
set scheduler-locking off
scheduler-locking 可取值为:
可通过如下命令查看线程锁的设置值:
show scheduler-locking
注:set scheduler-locking要处于线程运行环境下才能生效,也就是程序已经运行并且暂停在某个断点处,否则会出现 “Target 'exec' cannot support this command.” 这样的错误;而且设置后的scheduler-locking值在整个进程内有效,不属于某个线程。

运行至创建线程之后,可通过如下的方式查看所有的线程(主线程和子线程)

可看到线程ID前面带有星号,表示此线程是当前正在被调试的线程,可通过thread id去切换到不同的线程进行调试。
thread n
可在循环中设置条件断点,来调试程序:
线程2中,当循环进行到i=6的时候,会触发断点

此时切换到线程3进行,按照相同的方式进行调试,此外在调试过程中,可指定某个或者所有的线程执行GDB命令
- thread apply id GDB_CMD
- thread apply all GDB_CMD
tread apply all detach 所有被挂起的线程进行释放,开始运行
-------------------------------------to be continued-----------------------------------------