C++代码调试:如何不再害怕段错误并爱上GDB
您是否曾花费三个小时寻找一个错误,结果发现它只是switch中忘记的break,或是if后面多余的;?如果是——欢迎加入俱乐部。C++调试是一门近乎自虐的艺术。但和任何艺术一样,它是可以掌握的。
在本文中,我们将讨论的不仅仅是“如何运行GDB”,而是真正的技巧,这些技巧将调试从占卜咖啡渣变成福尔摩斯式的侦探工作。我们将讨论现代消毒器、反向调试,甚至如何捕捉那种“只在周五下午出现”的错误。
1. 经典之作:GDB和LLDB
从基础开始。GNU调试器(GDB)及其竞争对手LLDB是调试器中的瑞士军刀。但大多数人只使用了它们10%的功能。让我们改变这一点。
带条件的断点
您在一个有10,000次迭代的循环中设置断点,然后按continue键9,999次?停下来。使用条件断点:
(gdb) break main.cpp:42 if i == 5000这将节省您数小时的生命。在LLDB中,语法类似:
(lldb) breakpoint set --file main.cpp --line 42 --condition 'i == 5000'监视点——捕捉变量变化
想象一下:您有一个变量,它在未知的地方被修改。设置一个监视点:
(gdb) watch my_variable(gdb) rwatch my_variable # 在读取时触发(gdb) awatch my_variable # 在读取和写入时触发现在,程序会在有人触碰您的变量的那一刻停止。很神奇,对吧?
2. 反向调试:时间旅行
程序员最可怕的噩梦:程序崩溃了,但您不知道它是如何到达这个状态的。反向调试允许您“回退”执行过程。
在GDB中,这需要record支持:
(gdb) record(gdb) continue... 程序崩溃 ...(gdb) reverse-step # 向后单步执行(gdb) reverse-continue # 反向继续执行在商业工具(例如UndoDB或Mozilla的rr)中,这效果更佳。rr(记录和重放)是一个免费工具,它记录程序的执行,然后允许您反复重放:
$ rr record ./my_program$ rr replay(gdb) continue... 找到崩溃位置 ...(gdb) reverse-step提示:使用rr查找数据竞争——它确定性地重放每次执行。
3. 消毒器:针对未定义行为的疫苗
现代C++(从Clang和GCC 4.8+开始)拥有我们前辈梦寐以求的内置工具。AddressSanitizer (ASan)、UndefinedBehaviorSanitizer (UBSan)和ThreadSanitizer (TSan)是必备工具。
示例:捕捉内存泄漏
// 编译:g++ -fsanitize=address -g main.cpp -o main#include <iostream>
void leak() { int* p = new int(42); // 忘记 delete}
int main() { leak(); return 0;}运行——然后看到:
==12345==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 4 byte(s) in 1 object(s) allocated from: #0 0x7f... in operator new(unsigned long) #1 0x400... in leak() main.cpp:5 #2 0x400... in main main.cpp:10ASan将显示精确的调用栈,包括发生分配的行号。再也不会有“某处内存泄漏”的情况。
UBSan对抗未定义行为
// 编译:g++ -fsanitize=undefined -g main.cppint main() { int x = INT_M