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 -Hps -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
默认情况下,当你单步调试(stepnext)某一个线程时,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 id2info 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单步锁定(推荐)。仅在你执行 stepnext 命令时锁定其他线程。如果你执行 continue,所有线程将恢复正常运行。
  • set scheduler-locking off:不锁定(默认值)。任何线程都可以在任何时间运行。

操作步骤

  • 查看当前线程:使用 info threads 查看所有线程 ID。
  • 切换到目标线程:使用 thread <ID> 切换到你想锁定的线程。
  • 开启锁定:输入 set scheduler-locking onset 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 1f 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 查看当前是哪个线程,并输入 wherebt 查看调用堆栈,确认是从哪个路径运行到此处的。
 

 
其他参考:
posted @ 2020-03-04 20:34  PKICA  阅读(75)  评论(0)    收藏  举报