从 GDB 到 CDB:C/C++ 程序调试的两把“手术刀”

从 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

btbacktrace 的缩写,用于查看当前线程调用栈。调用栈从下往上看,底部通常是程序入口,上面是逐层调用,最顶部是当前崩溃位置。

如果程序崩在 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 类型,例如 QStringQVectorQMap,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 0frame 1frame 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 的命令普遍很短,比如 gptk,看起来像黑客电影,其实只是历史包袱比较重。

3. 查看调用栈

查看当前线程调用栈:

text 复制代码
k

更详细一些:

text 复制代码
kb

带参数和源信息:

text 复制代码
kp

查看所有线程调用栈:

text 复制代码
~* k

~ 在 CDB/WinDbg 中表示线程相关命令,* 表示所有线程,k 表示栈。因此 ~* k 就是"所有线程都打印调用栈"。

Windows 下程序卡死时,这条命令非常关键。比如 UI 不响应,可以 attach 后执行:

text 复制代码
~* k

如果主线程停在 WaitForSingleObjectEnterCriticalSection、数据库访问、网络阻塞或某个耗时函数上,基本就能确定卡死方向。

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。

工程上建议:

  1. 发布包和符号分开保存;
  2. 每个版本保留唯一构建号;
  3. 崩溃时自动生成 core/dmp;
  4. 日志中打印版本号、构建时间、Git 提交号;
  5. 调试符号不要随意覆盖;
  6. 保留对应版本的 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 分析。它们的命令不一定优雅,但足够锋利。熟练掌握之后,你会发现很多曾经"玄学"的问题,其实都有痕迹可循。

程序崩溃并不可怕,可怕的是崩溃后只能说一句:"我这里没复现。"

而调试器的意义,就是让我们在没有复现、没有界面、没有用户配合的情况下,依然有机会接近真相。

相关推荐
SL-staff1 小时前
智慧园区2000+设备统一管理:JVS-IoT如何降低运维成本40%
java·开发语言·物联网·智慧园区·设备管理·运维优化·jvs-iot
2zcode3 小时前
基于MATLAB图像处理的饮料瓶灌装液位检测系统设计与实现
开发语言·图像处理·matlab
必须得开心呀4 小时前
QT解决中文乱码问题
开发语言·qt
熊猫_豆豆4 小时前
QT6 Android C++ 自制美观闹钟
android·c++·qt·闹钟
随意起个昵称5 小时前
状压dp-基础题目2([USACO12MAR] Cows in a Skyscraper G)
c++·算法·动态规划
这不小天嘛5 小时前
JAVA八股——redis篇
java·开发语言·redis
无限的鲜花6 小时前
协程本质是函数加状态机——零基础深入浅出 C++20 协程
c++·算法·c++20
逝水无殇6 小时前
C# 字符串(String)详解
开发语言·后端·c#
精明的身影6 小时前
C++自学之路1:Hello world
开发语言·c++