从 GDB 到 CDB:C/C++ 程序调试的两把"手术刀"
很多程序员l老铁刚开始写 C/C++ 时,遇到程序崩溃,第一反应是加日志、加 qDebug()、加 printf()。这当然有用,但当程序进入复杂阶段,比如多线程、网络通信、Qt 事件循环、指针越界、内存破坏、现场崩溃转储分析时,单纯靠日志就像拿着手电筒在黑屋子里找针:不是不能找,而是太慢。
真正进入 C/C++ 工程化开发后,调试器就是必修课。
在 Linux、国产化系统、Kylin、Ubuntu 等环境下,我们常用 GDB ;在 Windows + MSVC 环境下,我们常用 CDB,它是 Debugging Tools for Windows 里的一员,也可以理解为 WinDbg 的命令行版本。二者虽然平台不同、命令风格不同,但本质目标一致:让你在程序运行时"冻结现场",查看调用栈、变量、寄存器、内存、线程、模块、异常,并逐步还原问题发生的真实路径。
这篇博客不把 GDB 和 CDB 当作命令手册来堆砌,而是从实际工程出发,讲清楚它们分别适合解决什么问题、常用命令怎么用,以及如何把它们放进 Qt/C++ 项目的排障流程里。
一、为什么需要调试器?
调试器最有价值的地方,不是"单步执行"这么简单,而是它能回答几个日志很难准确回答的问题:
程序到底崩在哪一行?
崩溃时调用栈是什么?
当前线程在干什么?其他线程是不是死锁了?
某个变量什么时候被改坏的?
指针指向的内存到底是什么内容?
发布到现场的程序崩溃了,没有复现环境,能不能通过 core dump 或 dmp 文件还原现场?
在 C++ 项目中,很多问题并不会在错误发生的位置立刻暴露。例如数组越界可能在 A 函数发生,但程序在 B 函数释放内存时才崩;空指针可能在某次网络回调中产生,但直到 UI 定时刷新时才访问;跨线程错误可能不稳定,有时跑一天才出现一次。这些问题最怕"猜",而调试器的价值就是让我们少猜一点,多看一点。
可以把调试器理解成程序世界里的"法医":它不一定能阻止事故发生,但它能最大限度还原案发现场。
二、GDB 与 CDB 的定位
1. GDB 是 Linux 世界的主力调试器
GDB,全称 GNU Debugger,常用于 Linux、Ubuntu、Kylin、嵌入式 Linux 等环境。它支持 C、C++、Fortran、Rust 等多种语言,常用于调试 ELF 可执行文件、动态库、core dump,也可以 attach 到正在运行的进程。
常见使用场景包括:
- Linux 下调试 C/C++ 控制台程序;
- Qt 程序在 Kylin 或 Ubuntu 上崩溃;
- 程序现场生成 core 文件后离线分析;
- 嵌入式设备远程调试;
- 多线程死锁、卡死、段错误分析;
- 查看内存、寄存器、汇编指令。
GDB 的风格比较"Unix":命令短、强大、灵活,刚开始看起来有点硬核,但熟悉以后非常顺手。
2. CDB 是 Windows 世界的命令行调试器
CDB,全称 Console Debugger,是 Microsoft Debugging Tools for Windows 的一部分。很多人熟悉 WinDbg,但不一定熟悉 CDB。实际上,CDB 和 WinDbg 使用的是同一套调试引擎,命令体系基本一致,只是 CDB 是命令行界面,更适合脚本化、自动化、CI 环境以及 Qt Creator 调试器后端调用。
常见使用场景包括:
- Windows 下调试 MSVC 编译的 C/C++ 程序;
- 分析
.dmp崩溃转储文件; - 配合 Qt Creator 调试 MSVC Kit;
- 查看 Windows 异常、模块、线程、调用栈;
- 分析发布版本程序崩溃原因;
- 配置符号服务器,定位系统库和自身模块问题。
简单来说,GDB 更像 Linux 下的瑞士军刀,CDB 更像 Windows 下的工业内窥镜。一个擅长开源生态,一个深度绑定 Windows 调试体系。
三、调试前最重要的一件事:符号
无论 GDB 还是 CDB,真正让调试器"看懂程序"的关键都是 调试符号。
调试符号记录了函数名、变量名、源代码行号、类型信息等内容。没有符号,调试器看到的只是地址,比如:
text
0x00000000004012af
有了符号,调试器才能告诉你:
text
MainWindow::onStartButtonClicked() at mainwindow.cpp:128
这两者的差别,就像地图上一个是经纬度坐标,一个是"重庆市某某路某某大厦 12 楼"。
1. GCC/Clang 下生成 GDB 符号
Linux 下使用 GCC 或 Clang 编译时,一般加上 -g:
bash
g++ -g main.cpp -o app
如果是 CMake 项目,可以使用 Debug 构建类型:
bash
cmake -DCMAKE_BUILD_TYPE=Debug ..
make
或者在 CMakeLists.txt 中加入:
cmake
set(CMAKE_CXX_FLAGS_DEBUG "-g -O0")
-O0 表示关闭优化。初学调试时建议关闭优化,因为编译器优化可能会导致变量被优化掉、代码执行顺序和源码看起来不完全一致。
如果是 Qt 项目,Debug 模式下通常会自动带调试符号。使用 qmake 时,.pro 中可确认:
pro
CONFIG += debug
2. MSVC 下生成 CDB 符号
Windows + MSVC 使用的是 PDB 文件。PDB 文件相当于 Windows 调试体系里的符号文件。
在 Visual Studio 或 CMake + MSVC 中,Debug 模式通常会生成 .pdb 文件。发布程序时,如果希望后续能分析崩溃 dump,强烈建议保留对应版本的 PDB 文件。
一个常见的工程化习惯是:
text
release/
MyApp.exe
MyApp.pdb
Qt5Core.dll
Qt5Widgets.dll
或者至少把 PDB 按版本归档,例如:
text
symbols/
v1.0.3/
MyApp.pdb
v1.0.4/
MyApp.pdb
没有 PDB 的 dump 分析就像拿着监控录像但没有清晰度,只能看见有人摔倒,却看不清是谁推的。
四、GDB 基础使用
假设有一个简单程序:
cpp
#include <iostream>
#include <vector>
int main()
{
std::vector<int> nums = {1, 2, 3};
for (int i = 0; i <= 3; ++i) {
std::cout << nums[i] << std::endl;
}
return 0;
}
编译:
bash
g++ -g -O0 main.cpp -o app
启动 GDB:
bash
gdb ./app
进入 GDB 后,常用命令如下。
1. 运行程序
gdb
run
也可以带参数:
gdb
run arg1 arg2
如果程序崩溃,GDB 会停在崩溃位置,例如:
text
Program received signal SIGSEGV, Segmentation fault.
这时候不要急着退出,真正的调查才刚开始。
2. 查看调用栈
gdb
bt
bt 是 backtrace 的缩写,用于查看当前线程调用栈。调用栈从下往上看,底部通常是程序入口,上面是逐层调用,最顶部是当前崩溃位置。
如果程序崩在 Qt 程序里,调用栈可能长这样:
text
#0 MyWidget::updateData() at mywidget.cpp:85
#1 MainWindow::refreshUi() at mainwindow.cpp:132
#2 QMetaObject::activate(...)
#3 QTimer::timeout()
#4 ...
这说明问题并不是"Qt 崩了",而是你的 updateData() 中访问了非法数据。很多初学者看到调用栈里有 Qt 内部函数,就误以为是 Qt 的问题,其实多数情况下是自己的槽函数或回调里出错。
3. 设置断点
按函数名设置断点:
gdb
break main
按文件行号设置断点:
gdb
break main.cpp:8
按类成员函数设置断点:
gdb
break MainWindow::on_pushButton_clicked
查看断点:
gdb
info breakpoints
删除断点:
gdb
delete 1
禁用断点:
gdb
disable 1
启用断点:
gdb
enable 1
断点是调试器最基本的能力,但真正有用的是条件断点。
4. 条件断点
如果一个循环执行一万次,但只在第 9999 次出错,不可能手动点一万次继续。这时可以使用条件断点:
gdb
break main.cpp:8 if i == 9999
或者对业务字段设置条件:
gdb
break TargetManager::updateTarget if targetId == 1024
这在处理雷达目标、网络数据、资源台账批量导入时非常有用。你不需要在海量数据里肉眼找异常,让调试器帮你筛选。
5. 单步调试
继续运行:
gdb
continue
单步进入函数:
gdb
step
单步跳过函数:
gdb
next
跳出当前函数:
gdb
finish
执行到当前函数返回:
gdb
return
一般来说,step 用于进入你关心的函数,next 用于跳过库函数或你暂时不关心的逻辑。不要对 Qt、STL、系统库一直 step,否则很容易迷路。调试器里迷路,比在代码里迷路更容易怀疑人生。
6. 查看变量
打印变量:
gdb
print i
打印对象成员:
gdb
print target.id
print target.position.x
打印指针指向的内容:
gdb
print *ptr
查看局部变量:
gdb
info locals
查看函数参数:
gdb
info args
持续显示某个变量:
gdb
display i
取消显示:
gdb
undisplay 1
对于 Qt 类型,例如 QString、QVector、QMap,GDB 原生显示可能不够友好,Qt Creator 通常会加载 pretty printer,让这些类型显示得更直观。如果在纯命令行 GDB 下查看 Qt 类型比较痛苦,可以优先通过 Qt Creator 调试,或者配置 Qt 的 Python pretty printer。
7. 观察点:变量什么时候被改坏?
断点解决的是"程序运行到哪里停下",观察点解决的是"谁改了这个变量"。
例如你发现 state 某个时刻变成了异常值,但不知道是谁改的,可以使用:
gdb
watch state
当 state 被写入时,GDB 会自动停下。
如果是指针指向的内存:
gdb
watch *ptr
观察点在排查内存破坏、状态机异常、跨线程修改共享变量时非常有用。不过硬件观察点数量有限,不能无限设置。
8. 多线程调试
查看线程:
gdb
info threads
切换线程:
gdb
thread 3
查看所有线程调用栈:
gdb
thread apply all bt
这是排查卡死、死锁、线程阻塞的神命令。
例如 Qt 程序界面卡死,attach 进去后执行:
gdb
thread apply all bt
如果主线程栈里停在:
text
QMutex::lock()
SomeService::query()
MainWindow::onSearchClicked()
说明 UI 线程可能在等待锁或者执行耗时查询。此时优化方向就不是"界面刷新慢",而是"主线程不该做阻塞操作"。
9. Attach 到正在运行的进程
有些程序不是从 GDB 启动的,而是已经在运行。可以先查进程号:
bash
ps -ef | grep app
然后 attach:
bash
gdb -p 12345
或者进入 GDB 后:
gdb
attach 12345
attach 后程序会暂停。分析完可以继续:
gdb
continue
如果想退出 GDB 但不杀掉程序:
gdb
detach
quit
这是现场排查时常用的方法。特别是服务端程序、后台采集程序、长时间运行的测试工具,不一定方便重新启动,用 attach 可以直接进入现场。
五、GDB 分析 core dump
线上或现场程序崩溃时,最理想的情况是生成 core dump。core dump 是程序崩溃瞬间的内存快照,包含调用栈、寄存器、部分内存等信息。
1. 开启 core dump
查看限制:
bash
ulimit -c
如果输出是 0,说明不会生成 core。可以临时开启:
bash
ulimit -c unlimited
也可以配置 core 文件命名:
bash
cat /proc/sys/kernel/core_pattern
临时修改:
bash
sudo sh -c 'echo "core.%e.%p.%t" > /proc/sys/kernel/core_pattern'
程序崩溃后,可能生成类似文件:
text
core.app.12345.1710000000
2. 使用 GDB 打开 core
bash
gdb ./app core.app.12345.1710000000
进入后第一件事:
gdb
bt
然后查看局部变量:
gdb
frame 0
info locals
info args
切换到上一层调用:
gdb
frame 1
frame 命令很重要。崩溃点不一定是根因,上一层、上上层的业务调用才可能暴露真正问题。
3. core 分析的典型流程
我的建议流程是:
第一步,bt 看崩溃线程调用栈。
第二步,thread apply all bt 看其他线程是否参与问题。
第三步,frame 0、frame 1、frame 2 逐层查看参数和局部变量。
第四步,确认崩溃类型:空指针、野指针、数组越界、重复释放、栈溢出、断言失败,还是业务主动 abort。
第五步,对照源码和版本,确认 core 对应的可执行文件、动态库、符号是否匹配。
core 分析最怕符号不匹配。你拿 v1.0.5 的程序去分析 v1.0.3 的 core,调用栈可能完全不可信。工程上一定要把"可执行文件 + 动态库 + 符号 + 版本号"作为一个整体归档。
六、CDB 基础使用
CDB 在 Windows 下使用,通常安装在 Debugging Tools for Windows 中。它的命令风格和 GDB 完全不同,一开始看起来像神秘咒语,但熟悉后非常适合分析 dump。
1. 启动程序
bat
cdb.exe MyApp.exe
带参数:
bat
cdb.exe MyApp.exe arg1 arg2
启动后输入:
text
g
g 表示 go,也就是继续运行。
2. 常用执行命令
继续运行:
text
g
单步进入:
text
t
单步跳过:
text
p
运行到当前函数返回:
text
gu
中断程序:
text
Ctrl + Break
CDB 的命令普遍很短,比如 g、p、t、k,看起来像黑客电影,其实只是历史包袱比较重。
3. 查看调用栈
查看当前线程调用栈:
text
k
更详细一些:
text
kb
带参数和源信息:
text
kp
查看所有线程调用栈:
text
~* k
~ 在 CDB/WinDbg 中表示线程相关命令,* 表示所有线程,k 表示栈。因此 ~* k 就是"所有线程都打印调用栈"。
Windows 下程序卡死时,这条命令非常关键。比如 UI 不响应,可以 attach 后执行:
text
~* k
如果主线程停在 WaitForSingleObject、EnterCriticalSection、数据库访问、网络阻塞或某个耗时函数上,基本就能确定卡死方向。
4. 设置断点
按函数设置断点:
text
bp MyApp!MainWindow::onStartClicked
设置未加载模块的延迟断点:
text
bu MyApp!SomeClass::someFunction
列出断点:
text
bl
删除断点:
text
bc 0
禁用断点:
text
bd 0
启用断点:
text
be 0
CDB 中模块名很重要。MyApp!FunctionName 的意思是某个模块里的某个函数。对于大型程序,多个 DLL 可能有同名函数,因此带模块名更准确。
5. 查看变量和内存
查看局部变量:
text
dv
查看类型:
text
dt MyApp!SomeStruct
查看某个对象:
text
dt MyApp!SomeClass 00000012`34567890
查看内存:
text
db address
按 DWORD 查看:
text
dd address
按 QWORD 查看:
text
dq address
查看 Unicode 字符串:
text
du address
CDB 的内存查看能力非常强,但它不像 GDB 那样对 C++ 表达式求值直观。做 Windows dump 分析时,通常是"调用栈 + 异常上下文 + 模块 + 符号 + 内存"组合判断。
6. 线程操作
列出线程:
text
~
切换到 2 号线程:
text
~2s
查看当前线程栈:
text
k
查看所有线程栈:
text
~* k
如果你分析 Qt Windows 程序,经常会看到主线程在消息循环中,例如:
text
user32!GetMessageW
Qt5Core!QEventDispatcherWin32::processEvents
Qt5Widgets!QApplicationPrivate::notify_helper
这并不代表错误。要结合异常线程或卡死线程判断。调试时不要看到系统库、Qt 库就紧张,它们很多时候只是"案发现场附近的路人"。
七、CDB 分析 dump 文件
Windows 程序发布后,现场最常见的不是让你远程连上去单步调试,而是丢给你一个 .dmp 文件。此时 CDB 就非常有用。
1. 打开 dump
bat
cdb.exe -z crash.dmp
进入后先执行:
text
!analyze -v
这是 Windows 调试器的经典命令,用于自动分析异常原因。它会给出异常码、崩溃地址、调用栈、可疑模块等信息。
常见异常码包括:
text
0xC0000005
这是访问违规,也就是常说的 Access Violation。通常对应空指针、野指针、越界访问等问题。
text
0xC0000409
可能与栈缓冲区溢出、安全检查失败有关。
text
0x80000003
通常是断点异常。
!analyze -v 很有用,但不能盲信。它像一个经验丰富但偶尔自信过头的侦探,能给方向,但最终还是要结合调用栈和源码判断。
2. 查看异常上下文
打开 dump 后,常用:
text
.ecxr
.ecxr 用于切换到异常发生时的上下文。执行后再看调用栈:
text
k
很多时候,不执行 .ecxr 看到的栈不是最关键的异常栈。分析 dump 的基本动作可以记成:
text
!analyze -v
.ecxr
k
dv
也就是先自动分析,再切异常上下文,再看栈,再看局部变量。
3. 配置符号路径
Windows dump 分析高度依赖符号。常见配置方式:
text
.symfix
.reload
.symfix 会设置 Microsoft 公共符号服务器。更完整的方式是:
text
.sympath srv*C:\Symbols*https://msdl.microsoft.com/download/symbols
.reload /f
如果你有自己的 PDB 目录,可以加进去:
text
.sympath C:\MyAppSymbols;srv*C:\Symbols*https://msdl.microsoft.com/download/symbols
.reload /f
如果符号加载不正确,可以查看模块:
text
lm
查看某个模块详细信息:
text
lmvm MyApp
重点关注 PDB 是否匹配。PDB 不匹配时,调用栈可能缺函数名、行号错乱,甚至把你带到错误方向。
八、GDB 与 CDB 命令对照
| 目的 | GDB | CDB |
|---|---|---|
| 启动程序 | gdb ./app |
cdb app.exe |
| 运行/继续 | run / continue |
g |
| 单步进入 | step |
t |
| 单步跳过 | next |
p |
| 跳出函数 | finish |
gu |
| 查看调用栈 | bt |
k / kb / kp |
| 查看所有线程栈 | thread apply all bt |
~* k |
| 查看线程 | info threads |
~ |
| 切换线程 | thread 3 |
~3s |
| 设置断点 | break func |
bp module!func |
| 查看断点 | info breakpoints |
bl |
| 删除断点 | delete 1 |
bc 1 |
| 查看局部变量 | info locals |
dv |
| 查看参数 | info args |
dv / 栈参数 |
| 查看模块 | info sharedlibrary |
lm |
| 分析 core/dump | gdb app core |
cdb -z crash.dmp |
| 自动分析 | 无完全等价 | !analyze -v |
从使用感受看,GDB 更适合源码级交互调试;CDB 更适合 Windows 异常、dump、符号、线程、模块层面的深度分析。当然,这不是绝对的。GDB 也能分析 core 非常强,CDB 也能源码调试。只是各自的舒适区不同。
九、Qt/C++ 工程中的典型场景
场景一:Qt 程序点击按钮后崩溃
假设按钮槽函数如下:
cpp
void MainWindow::onQueryClicked()
{
ResourceInfo *info = service->query(currentCode);
ui->nameLabel->setText(info->name);
}
如果 query() 返回了空指针,程序会在 info->name 处崩溃。
GDB 中:
gdb
bt
frame 0
print info
如果看到:
text
$1 = (ResourceInfo *) 0x0
就可以确认是空指针。
CDB 中:
text
.ecxr
k
dv
如果局部变量里 info = 00000000,同样可以确认。
正确修复不是"加一个 try catch",而是梳理业务边界:
cpp
void MainWindow::onQueryClicked()
{
ResourceInfo *info = service->query(currentCode);
if (!info) {
QMessageBox::warning(this, "提示", "未查询到资源信息");
return;
}
ui->nameLabel->setText(info->name);
}
调试器帮我们找到崩溃点,但修复要回到业务逻辑。
场景二:程序不崩,但界面卡死
界面卡死时,很多人第一反应是"Qt 卡了"。实际上,Qt 主线程只要被阻塞,界面就无法响应。
Linux 下:
bash
gdb -p 进程号
然后:
gdb
thread apply all bt
Windows 下:
bat
cdb -p 进程号
然后:
text
~* k
如果主线程正在执行数据库查询、文件扫描、网络阻塞、等待锁,那就说明问题在主线程耗时操作。
典型错误:
cpp
void MainWindow::onSearchClicked()
{
auto result = repository->queryLargeData(); // 耗时 5 秒
updateTable(result);
}
更合理的方式是把耗时任务放到工作线程,完成后通过信号槽通知 UI 更新。
调试器在这里的作用不是直接修复卡顿,而是告诉你:主线程到底卡在哪里。
场景三:多线程共享数据偶发崩溃
偶发崩溃通常最折磨人。比如一个线程写 QVector,另一个线程读 QVector,没有加锁,某次扩容时读线程访问了失效内存。
GDB 中可以查看所有线程栈:
gdb
thread apply all bt
如果一个线程在写容器,另一个线程在读容器,且没有同步机制,就要警惕数据竞争。
CDB 中:
text
~* k
再结合崩溃地址、异常码、源码判断。
这类问题的修复通常不是"在崩溃位置加判断",而是建立线程模型:
- UI 线程只更新界面;
- 数据接收线程只负责接收;
- 数据模型更新需要加锁或使用消息队列;
- 跨线程通信尽量使用 queued connection;
- 不要让多个线程同时直接读写非线程安全容器。
调试器只能告诉你哪个线程摔倒了,架构设计要保证大家不要在同一条狭窄楼梯上互相撞。
十、调试发布版本程序的注意事项
Debug 版本好调,但现场运行的往往是 Release 版本。Release 版本调试有几个问题:
第一,优化会改变代码结构。变量可能被优化掉,函数可能被内联,调用栈可能不完整。
第二,符号可能不在现场。没有符号就很难定位源码行。
第三,现场环境不一定可复现。你只能依赖 core 或 dump。
工程上建议:
- 发布包和符号分开保存;
- 每个版本保留唯一构建号;
- 崩溃时自动生成 core/dmp;
- 日志中打印版本号、构建时间、Git 提交号;
- 调试符号不要随意覆盖;
- 保留对应版本的 Qt 库、业务 DLL、插件。
对于 Windows 程序,可以集成 MiniDumpWriteDump 生成 dmp。对于 Linux 程序,可以配置 core dump 或使用 systemd-coredump。这样现场问题就不会只剩一句"程序崩了"。
十一、一些实用调试技巧
1. 不要一上来就单步
新手喜欢从 main 函数一路单步,这种方式很容易把自己调晕。更好的方式是:
先复现问题,再在关键函数设置断点,再看调用栈和变量。
调试不是散步,是抓嫌疑人。不要从城市入口一步一步走到案发现场,要先看监控、查路线、锁定重点区域。
2. 调用栈比当前行更重要
当前崩溃行只告诉你"哪里爆炸",调用栈告诉你"炸弹是怎么被送来的"。很多问题根因在上一层调用,甚至更上层的状态设置。
看到崩溃后,第一反应应该是:
gdb
bt
或者 CDB:
text
k
不要只盯着当前行。
3. 先确认符号是否可信
如果调用栈看起来很奇怪,先别急着分析业务。检查符号、版本、可执行文件是否匹配。
GDB 中可以看共享库:
gdb
info sharedlibrary
CDB 中可以看模块:
text
lm
lmvm MyApp
符号错了,后面的推理都可能是空中楼阁。
4. 对偶发问题使用观察点和条件断点
偶发问题靠肉眼盯日志很痛苦。GDB 的 watch、条件断点,CDB 的数据断点 ba 都可以在变量被写入时停下。
CDB 中设置写入断点示例:
text
ba w4 address
表示当某地址开始的 4 字节被写入时中断。
这类能力在排查"某个值不知道什么时候被改坏"时非常有价值。
5. 日志和调试器不是敌人
很多人会问:有了调试器还要日志吗?当然要。
日志适合记录长时间运行过程、业务路径、输入输出、现场上下文;调试器适合冻结瞬间、查看栈和内存。
优秀的排障方式往往是:
先用日志缩小时间和业务范围,再用调试器精准进入现场。
日志像监控录像,调试器像显微镜。一个看过程,一个看细节。
十二、推荐的学习路线
如果你刚开始学习 GDB 和 CDB,不建议一开始就背命令大全。可以按下面路线走。
第一阶段:掌握断点、运行、单步、调用栈、变量查看。
GDB 重点命令:
gdb
run
break
continue
next
step
finish
bt
print
info locals
CDB 重点命令:
text
g
bp
p
t
gu
k
dv
第二阶段:掌握线程和崩溃分析。
GDB:
gdb
info threads
thread 线程号
thread apply all bt
CDB:
text
~
~线程号s
~* k
!analyze -v
.ecxr
第三阶段:掌握 core/dump 离线分析。
GDB:
bash
gdb ./app core.xxx
CDB:
bat
cdb -z crash.dmp
第四阶段:掌握符号、模块、内存、观察点。
GDB:
gdb
watch
x
info sharedlibrary
CDB:
text
lm
dt
db
dd
dq
ba
.sympath
.reload
第五阶段:结合项目建立排障流程。
比如在 Qt/C++ 项目中,形成固定套路:
程序崩溃:先看调用栈。
界面卡死:先看所有线程栈。
变量异常:设置观察点。
现场崩溃:分析 core/dmp。
版本混乱:先查符号和构建号。
只要这个流程稳定下来,调试效率会明显提升。
十三、GDB 与 CDB 的"性格差异"
如果把 GDB 和 CDB 拟人化,GDB 像一个 Linux 老工程师,话不多,但工具箱里什么都有。你问它变量是什么,它直接告诉你;你让它停在某个条件,它照办;你让它 attach 一个进程,它也不废话。
CDB 则像一个 Windows 内核侦探,语法有点古怪,但查 dump、看异常、找模块、配符号非常专业。它常常不是陪你从 main 函数一步步走,而是在事故发生后打开黑匣子,告诉你飞机最后几秒发生了什么。
GDB 更偏源码级调试的舒适感,CDB 更偏系统级和转储分析的专业性。对于跨平台 Qt/C++ 开发者来说,这两把工具都值得掌握。
因为你迟早会遇到这样的场景:
Linux 现场说程序段错误了,给你一个 core。
Windows 用户说软件闪退了,给你一个 dmp。
国产化环境上 Qt 程序卡死了,只能远程 SSH。
MSVC Debug 模式没问题,Release 偶现崩溃。
这时候,谁会用调试器,谁就更接近真相。
十四、结语:调试器不是高级技巧,而是基本功
很多人把 GDB、CDB、WinDbg 看成"高手才用的工具"。其实恰恰相反,调试器应该是 C/C++ 程序员的基本功。因为 C++ 给了我们极高的性能和控制力,也给了我们足够多把自己绊倒的机会:空指针、野指针、越界、悬空引用、数据竞争、ABI 不匹配、库版本错误、栈破坏、堆破坏......
日志能告诉你"我走到了这里",调试器能告诉你"我为什么死在这里"。
真正有效的调试,不是疯狂单步,也不是到处加打印,而是建立一种调查思维:
先确认现象,再冻结现场;
先看调用栈,再看变量;
先判断线程,再判断业务;
先确认符号,再相信结果;
先缩小范围,再精准下刀。
GDB 和 CDB 就是两把手术刀。GDB 适合 Linux/Kylin/嵌入式现场,CDB 适合 Windows/MSVC/dump 分析。它们的命令不一定优雅,但足够锋利。熟练掌握之后,你会发现很多曾经"玄学"的问题,其实都有痕迹可循。
程序崩溃并不可怕,可怕的是崩溃后只能说一句:"我这里没复现。"
而调试器的意义,就是让我们在没有复现、没有界面、没有用户配合的情况下,依然有机会接近真相。