Linux下GDB调试程序
1. 什么是GDB
GDB(全称:GNU Debugger)是GNU工程师为GNU操作系统开发的调试器。它可以用于调试C、C++、Objective-C、Pascal、Ada等语言编写的程序。
2. GDB的使用条件
在程序编译的时候,添加响应的调试信息,才能使程序使用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学习笔记》
注:
- 通常在为调试而编译程序时,必须关掉编译器的优化现象(-0n),并打开调试选项 -g,另外,’-Wall‘在尽量不影响程序行为的情况下打开,提示所有的warning,优化选项的具体含义可参考 《CMAKE学习笔记》
- -g选项的作用是在可执行文件中加入调试需要的信息GDB中查看的时源文件的代码)
3. GDB启动方法
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命令实现变量监视机制的方式有两种
- 为变量设置硬件观察点 Hardware watchpoint
- 为变量设置软件观察点 Software watchpoint
软件观察点: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参数可以指定为:
- 栈帧编号,0 为当前被调用函数对应的栈帧号,最大编号的栈帧对应的函数通常就是 main() 主函数;
- 栈帧的地址
- 通过函数的函数名指定。如果是类似递归函数,其对应多个栈帧的话,通过此方法指定的是编号最小的那个栈帧。
栈帧的编号以及栈帧的地址,都可以通过如下命令进行查询:
info frame
通过info frame可以查看到栈帧的如下信息:
- 当前栈帧的编号
- 当前栈帧的地址
- 当前栈帧对应函数的存储地址,以及该函数被调用时的代码存储的地址
- 当前函数的调用者,以及对应的栈帧地址
- 编写此栈帧所使用的编程语言 (source language c++)
- 函数参数的存储地址以及参数值
- 栈帧中局部变量的存储地址
- 栈帧中存储的寄存器变量,例如指令寄存器(64位环境中用 rip 表示,32为环境中用 eip 表示)、堆栈基指针寄存器(64位环境用 rbp 表示,32位环境用 ebp 表示)等。
-----------------------------------------------分割线------------------------------------------------
4. GDB调试多进程程序
多进程程序调试,首先启动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 <unistd.h> #include <pthread.h> #include <stdio.h> #include <stdlib.h> // 编写多线程测试程序 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 可取值为:
- on 锁定线程,调试当前线程的时候,其他的线程会暂时被GDB挂起
- off 不锁定线程,在调试当前线程的时候,其他的线程都会继续运行
- step 当单步执行当前线程时,其它线程不会执行。但如果该模式下执行 continue、until、 finish 命令,则其它线程也会继续执行,并且如果某一线程执行过程遇到断点,则 GDB 调试器会将该线程作为当前线程。
可通过如下命令查看线程锁的设置值:
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-----------------------------------------
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)