gdb线程调试指南
GDB 的输出中经常出现 "LWP"(Light Weight Process,轻量级进程),查看 LWP 信息的命令是 info threads。
附加到进程
gdb -p <进程号>
或:
gdb
attach <进程号>
detach 进程
在 GDB 中执行 quit 后,会 detach 进程,原进程会继续正常运行
1. 显示线程命令:info threads
使用此命令可以查看所有线程及其对应的 LWP ID。
(gdb) info threads
典型输出示例:
Id Target Id Frame
1 Thread 0x7ffff7fc (LWP 12345) "main" 0x00007ffff... in poll ()
* 2 Thread 0x7ffff6fc (LWP 12346) "worker" 0x00007ffff... in epoll_wait ()
3 Thread 0x7ffff5fc (LWP 12347) "logger" 0x00007ffff... in nanosleep ()
- Id: GDB 内部使用的线程编号(切换线程时用这个,如
thread 2)。 - Target Id: 包含目标系统的线程标识,括号里的
LWP 12346就是操作系统层面的线程 ID(对应top -H或ps -L里的 PID/TID)。 - Frame: 线程当前执行到的函数位置。
- 星号 (*): 表示当前 GDB 正在调试/控制的线程。
2. 常用线程调试命令速查
| 目标 | 命令 | 说明 |
|---|---|---|
| 查看所有线程 | info threads |
显示 GDB ID 和 LWP ID 的对应关系 |
| 切换线程 | thread <ID> |
使用第一列的 GDB ID(例如 thread 2) |
| 查看特定线程堆栈 | thread apply <ID> bt |
查看指定线程的调用堆栈 |
| 查看所有线程堆栈 | thread apply all bt |
最常用,排查死锁或卡顿的神器 |
| 只运行当前线程 | set scheduler-locking on |
防止单步调试时其他线程干扰 |
提示:如果你需要将 GDB 中的线程与 Linux 系统命令(如
top)对应起来,请查看 info threads 输出中括号里的 LWP 数值。在 GDB 中,锁定调试线程的主要命令是
set scheduler-locking。默认情况下,当你单步调试(
step 或 next)某一个线程时,GDB 会允许所有线程同时运行,这可能导致在你调试当前线程时,其他线程也执行了大量代码甚至触发了其他断点。这三个命令是 GDB 进行多线程调试时非常核心的工具,能有效帮助开发者定位线程同步、死锁等问题。
其他常用线程调试说明:
1). break thread_test.c:123 thread all
- 功能:在所有线程中的源文件
thread_test.c的第 123 行设置一个断点。 - 特点:无论哪个线程运行到这一行,GDB 都会暂停下来。默认情况下,如果不加
thread all,断点仅对当前线程有效或仅暂停一个线程。 - 适用场景:想要查看所有线程在特定代码位置的状态时(例如检查一个全局变量在每个线程执行到该行时的值)。
- 特定线程断点:break <location> thread <ID> 只有当指定 ID 的线程运行到该处时才会触发。
2). thread apply id1 id2 ... command
- 功能:让特定的一个或多个线程执行指定的 GDB 命令。
- 参数:
id1 id2是info threads命令显示的 GDB 内部线程 ID(注意:不是系统线程 TID)。 - 示例:
thread apply 2 3 bt(打印线程 2 和线程 3 的堆栈信息)。 - 适用场景:只想要检查或操作部分线程时,避免输出过多信息。
3). thread apply all command
- 功能:让所有正在被调试的线程执行相同的 GDB 命令。
- 示例:
thread apply all bt(查看所有线程的调用堆栈)。 - 适用场景:在死锁发生时,通过
thread apply all bt快速查看所有线程卡在哪个位置。
其他技巧:
- 线程重命名:在代码中使用
pthread_setname_np命名线程,GDB 的info threads会显示这些名称,极大方便识别。 - 实时事件控制:
set print thread-events on/off开启或关闭线程启动/退出的提示信息。
3. gdb锁定调试线程
你可以通过设置以下三种模式来控制线程锁定:
set scheduler-locking on:完全锁定。只有当前选中的线程可以运行,其他所有线程都会被暂停。set scheduler-locking step:单步锁定(推荐)。仅在你执行step或next命令时锁定其他线程。如果你执行continue,所有线程将恢复正常运行。set scheduler-locking off:不锁定(默认值)。任何线程都可以在任何时间运行。
操作步骤
- 查看当前线程:使用
info threads查看所有线程 ID。 - 切换到目标线程:使用
thread <ID>切换到你想锁定的线程。 - 开启锁定:输入
set scheduler-locking on或set scheduler-locking step。 - 验证状态:使用
show scheduler-locking查看当前锁定模式。
注意事项
- 死锁风险:如果当前线程在等待另一个被你锁定的线程释放锁(如
Mutex),程序会发生永久阻塞。 - 恢复运行:如果使用了完全锁定,调试完成后,记得使用
set scheduler-locking off恢复多线程并发,否则continue命令也只会让当前线程运行。
4. 保存线程栈信息到文件
(gdb) set logging file <文件名>
(gdb) set logging on
(gdb) thread apply all bt
(gdb) set logging off
(gdb) quit
.bt上好多# in ?? ()
在
-O0 调试版本下出现 ??(无法解析函数名)且伴随段错误,通常是因为 栈被破坏 (Stack Smashing) 或 程序跳到了非法地址。当 GDB 找不到符号表时,它就无法回溯(Backtrace)出函数名。以下是几种典型场景及排查方法:
1). 栈溢出或覆盖 (Stack Corruption)
这是最常见的原因。如果你在函数内部定义了一个局部变量数组,并且发生了越界写入,可能会覆盖掉函数的 返回地址。
- 现象:函数执行完毕尝试返回时,由于返回地址变成了垃圾值,程序跳到了一个随机内存地址,触发段错误。因为那个地址没有代码,GDB 只能显示
??。 - 排查:检查
bt结果中,最顶层的地址是否看起来像是一个合法的代码地址(通常很小,如0x55...或0x40...),还是像随机数据(如0x30303030)。
2). 坏的函数指针 (Bad Function Pointer)
你可能调用了一个未初始化、已释放或被污染的函数指针。
- 现象:程序直接跳转到了一个不存在代码的内存区域。
- 排查:在 GDB 中看当前指令指针:
p $pc。如果$pc指向 0 或某个奇怪的地址,说明是调用路径出错了。
3). 动态库/插件问题 (Missing Shared Libraries)
如果段错误发生在某个动态链接库(.so)内部,而那个库在编译时没有带调试信息,或者在运行时被
dlclose 掉了。- 解决方法:在 GDB 中输入
info sharedlibrary。检查是否有库的状态显示为No(表示未加载调试符号)。
4). 符号表未正确加载
虽然你用了
-g -O0,但如果可执行文件在运行前被执行了 strip 操作,调试信息会被剔除。- 验证:在 shell 中执行
file your_program,看是否显示with debug_info, not stripped。
快速排查步骤:
- 使用
f 1或f 2尝试切换帧:即使最顶层(Frame 0)是??,往下一层(Frame 1)往往能看到是谁调用了出问题的代码。 - 开启 AddressSanitizer (强烈推荐):这是定位这类问题最强的方法。在编译选项中加入:
运行程序时,它会直接告诉你哪一行代码发生了内存越界访问。
gcc -g -O0 -fsanitize=address your_code.c -o your_program - 检查栈指针:在 GDB 中输入
p $sp,看栈指针是否超出了合理范围。
同一行代码被多次“hit”(命中)
在 GDB 调试中,同一行代码被多次“hit”(命中)通常是由以下几个原因造成的:
1). 多线程并发 (最常见原因)
如果你的程序是多线程的,默认情况下 GDB 会在所有线程上设置该断点。
- 现象:当你点击
continue后,可能立即又停在同一行,但此时其实是另一个线程运行到了这里。 - 验证:查看 GDB 输出,看命中的线程 ID 是否发生了变化(例如从
Thread 1变为Thread 2)。 - 解决方法:使用
set scheduler-locking on锁定当前线程,或者设置线程特定断点:break linespec thread threadno。 [2, 3, 4, 5]
2). 编译器代码优化
如果编译时开启了优化(如
-O2 或 -O3),编译器可能会进行代码重排或指令合并。- 现象:逻辑上看似只执行一次的代码,其汇编指令可能分散在多个位置,或者被多次映射到同一行源码。
- 解决方法:调试时建议使用
-O0(关闭优化)或-Og(针对调试优化的级别)进行编译。
3). 内联函数 (Inline Functions) 或 模板 (Templates)
- 内联函数:如果一个函数被
inline,它在源码中只有一份,但在编译后的二进制文件中会被插入到每一个调用它的地方。GDB 会尝试在所有这些物理位置都设上断点,导致你觉得“反复进入”同一个函数。 - C++ 模板:同一行模板代码会被实例化为多个不同的函数(如
vector<int>和vector<double>),它们在内存中地址不同,但对应同一行源码。
4). 循环结构
这虽然显而易见,但有时在复杂的嵌套循环或递归中,开发者可能会忽略代码逻辑本身就在反复执行该行。
- 技巧:你可以使用
info breakpoints查看该断点的already hit X times计数器,确认它被命中的总次数。
5). 构造函数与析构函数
在 C++ 中,编译器有时会为同一个构造函数生成多个版本的底层代码(例如“In-charge”和“Not-in-charge”构造函数)。GDB 为了保证能拦截到构造行为,会在这些位置都放断点。
排查建议:
下次命中时,请立即输入
下次命中时,请立即输入
info threads 查看当前是哪个线程,并输入 where 或 bt 查看调用堆栈,确认是从哪个路径运行到此处的。其他参考:
1.gdb调试线程
浙公网安备 33010602011771号