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-----------------------------------------