使用 GDB 调试 C++ 程序

GDB(GNU Debugger)是 Linux 环境下强大的调试工具,可用于调试程序崩溃、多线程程序以及检测内存泄漏。以下是详细的 GDB 调试方法,涵盖程序崩溃、多线程调试和内存泄漏检测,格式为 Markdown。


1. 基本 GDB 使用

编译程序以启用调试

要使用 GDB 调试,需在编译时添加 -g 标志以包含调试信息:

1
g++ -g -o myprogram myprogram.cpp

启动 GDB

启动 GDB 并加载程序:

1
gdb ./myprogram

常用 GDB 命令

  • run(或 r):运行程序。
  • break <位置>(或 b):设置断点,如 break mainbreak myprogram.cpp:10
  • next(或 n):单步执行(不进入函数)。
  • step(或 s):单步执行(进入函数)。
  • continue(或 c):继续运行至下一个断点。
  • print <变量>(或 p):打印变量值,如 print x
  • backtrace(或 bt):显示调用栈。
  • quit(或 q):退出 GDB。

2. 调试程序崩溃

当程序崩溃(例如段错误,Segmentation Fault),GDB 可帮助定位问题。

步骤

  1. 编译带调试信息

    1
    g++ -g -o myprogram myprogram.cpp
  2. 启动 GDB 并运行程序

    1
    2
    gdb ./myprogram
    (gdb) run

    如果程序崩溃,GDB 会停在崩溃点,并显示错误信息,如:

    1
    2
    Program received signal SIGSEGV, Segmentation fault.
    0x00005555555551c3 in main () at myprogram.cpp:10
  3. 分析崩溃原因

    • 使用 backtrace 查看调用栈:
      1
      (gdb) bt
      输出调用栈,显示崩溃前的函数调用链。
    • 使用 frame <n> 切换到特定栈帧,检查变量:
      1
      2
      (gdb) frame 0
      (gdb) print *ptr
      检查指针是否为空(如 ptr = nullptr)或访问非法内存。
    • 使用 info localsinfo args 查看局部变量和函数参数。
  4. 设置断点定位问题

    • 在可疑代码处设置断点:
      1
      (gdb) break myprogram.cpp:10
    • 运行程序并逐步检查变量状态。
  5. 启用核心转储(Core Dump)
    如果程序崩溃但未在 GDB 中运行,可分析核心转储文件:

    • 启用核心转储:
      1
      ulimit -c unlimited
    • 运行程序,崩溃后生成 core 文件。
    • 使用 GDB 加载核心转储:
      1
      gdb ./myprogram core
    • 使用 bt 查看崩溃时的调用栈。

示例:调试段错误

1
2
3
4
5
6
7
#include <iostream>
int main() {
int* ptr = nullptr;
*ptr = 42; // 段错误
std::cout << "Never reached\n";
return 0;
}

调试过程:

1
2
3
4
5
6
7
8
9
10
g++ -g -o crash crash.cpp
gdb ./crash
(gdb) run
Program received signal SIGSEGV, Segmentation fault.
0x00005555555551c3 in main () at crash.cpp:4
4 *ptr = 42;
(gdb) print ptr
$1 = (int *) 0x0
(gdb) bt
#0 main () at crash.cpp:4

问题:ptr 为空导致解引用失败。


3. 多线程 GDB 调试

多线程程序可能因竞争条件、死锁或线程特定错误导致问题。GDB 提供多线程调试支持。

步骤

  1. 编译带调试信息和线程支持

    1
    g++ -g -pthread -o myprogram myprogram.cpp
  2. 启动 GDB 并运行

    1
    2
    gdb ./myprogram
    (gdb) run
  3. 线程相关命令

    • info threads:列出所有线程及其状态。
      1
      2
      3
      4
      (gdb) info threads
      Id Target Id Frame
      * 1 Thread 0x7ffff7fc4740 (LWP 1234) main () at myprogram.cpp:10
      2 Thread 0x7ffff75c3700 (LWP 1235) worker () at myprogram.cpp:20
    • thread <id>:切换到指定线程(如 thread 2)。
    • break <位置> thread <id>:为特定线程设置断点,如:
      1
      (gdb) break myprogram.cpp:20 thread 2
    • thread apply all <命令>:对所有线程执行命令,如:
      1
      (gdb) thread apply all bt
      显示所有线程的调用栈。
    • set scheduler-locking on:锁定调度,仅运行当前线程(便于调试特定线程)。
  4. 调试死锁或竞争条件

    • 设置断点在共享资源访问点(如锁操作)。
    • 使用 watch <变量> 设置观察点,监控共享变量变化:
      1
      (gdb) watch shared_data
    • 检查线程状态,分析是否因锁未释放导致死锁。

示例:调试多线程程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;
int shared_data = 0;

void worker(int id) {
mtx.lock();
shared_data++; // 可能引发竞争
mtx.unlock();
std::cout << "Thread " << id << " incremented to " << shared_data << '\n';
}

int main() {
std::thread t1(worker, 1);
std::thread t2(worker, 2);
t1.join();
t2.join();
return 0;
}

调试:

1
2
3
4
5
6
7
8
g++ -g -pthread -o threads threads.cpp
gdb ./threads
(gdb) break worker
(gdb) run
(gdb) info threads
(gdb) thread 2
(gdb) next
(gdb) print shared_data

4. 检测内存泄漏

GDB 本身不直接检测内存泄漏,但可结合工具(如 ValgrindAddressSanitizer)分析内存问题。以下是方法:

使用 GDB 辅助分析

  1. 检查指针和分配

    • 在分配内存处设置断点(如 mallocnew):
      1
      2
      (gdb) break malloc
      (gdb) break operator new
    • 使用 print 检查分配的指针是否被正确释放。
    • 使用 watch 监控指针是否被覆盖或未释放。
  2. 调用栈分析

    • 如果怀疑内存泄漏,使用 bt 检查分配和释放的调用路径。

使用 Valgrind(推荐)

Valgrind 的 memcheck 工具可检测内存泄漏:

  1. 编译带调试信息:
    1
    g++ -g -o myprogram myprogram.cpp
  2. 运行 Valgrind:
    1
    valgrind --leak-check=full ./myprogram
  3. 分析输出:
    • Valgrind 报告未释放的内存块及其分配位置。
    • 示例输出:
      1
      2
      3
      ==1234== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
      ==1234== at 0x4C2DB8D: malloc (vg_replace_malloc.c:299)
      ==1234== by 0x4005B3: main (myprogram.cpp:10)

使用 AddressSanitizer(ASan)

  1. 编译时启用 ASan:
    1
    g++ -g -fsanitize=address -o myprogram myprogram.cpp
  2. 运行程序,ASan 自动报告内存泄漏:
    1
    2
    3
    4
    ==5678==ERROR: LeakSanitizer: detected memory leaks
    Direct leak of 40 byte(s) in 1 object(s) allocated from:
    #0 0x7f8b6c4b7a90 in operator new
    #1 0x4005b3 in main myprogram.cpp:10

示例:检测内存泄漏

1
2
3
4
5
6
#include <iostream>
int main() {
int* ptr = new int[10]; // 未释放
std::cout << "Allocated memory\n";
return 0;
}

使用 Valgrind:

1
2
g++ -g -o leak leak.cpp
valgrind --leak-check=full ./leak

输出显示 40 字节未释放,定位到 new 调用。


注意事项

  1. 调试信息:始终使用 -g 编译,确保 GDB 能显示行号和变量信息。
  2. 优化级别:避免高优化(如 -O2),可能导致调试信息不准确。
  3. 多线程调试
    • 使用 info threadsthread apply all 管理复杂线程场景。
    • 注意竞争条件,结合锁或 scheduler-locking 隔离线程。
  4. 内存泄漏
    • Valgrind 和 ASan 比 GDB 更适合检测内存问题。
    • 对于复杂程序,定期检查分配和释放匹配。
  5. 性能
    • GDB 调试可能影响程序性能,生产环境中谨慎使用。
    • Valgrind 和 ASan 运行时开销较大,适合开发阶段。

数学表示

  • 时间复杂度
    • GDB 断点设置/单步执行:( O(1) )(依赖硬件支持)。
    • Valgrind 内存检查:( O(n) ),其中 ( n ) 为内存操作数,运行时显著慢于原程序。
  • 空间复杂度
    • GDB:仅占用调试符号表,约为 ( O(1) )。
    • Valgrind/ASan:额外内存开销,约为 ( O(m) ),其中 ( m ) 为分配的内存大小。

总结

  • 程序崩溃:用 GDB 的 btprint 和核心转储定位问题。
  • 多线程调试:用 info threadsthreadwatch 分析线程行为。
  • 内存泄漏:结合 Valgrind 或 ASan 检测未释放内存,GDB 辅助定位分配点。
  • 建议:熟悉 GDB 基本命令,结合 Valgrind/ASan 提高调试效率。