0 调试之前
- 环境配置
- 代码断点

1 调试工具栏
调试工具栏中可用的操作
| 操作 | 快捷键 | 描述 |
|---|---|---|
| 继续 / 暂停 | F5 | 继续:恢复正常的程序/脚本执行(直到下一个断点)。 暂停:检查当前行执行的代码并逐行调试。 |
| 单步跳过 | F10 | 执行下一个方法作为单个命令,而不检查或跟随其组成步骤。 |
| 单步调试 | F11 | 进入下一个方法以逐行跟踪其执行。 |
| 单步跳出 | Shift+F11 | 当位于方法或子例程内部时,通过完成当前方法的剩余行(如同将其视为单个命令)来返回到先前的执行上下文。 |
| 重启 | Ctrl+Shift+F5 | 终止当前程序执行并使用当前的运行配置重新开始调试。 |
| 停止 | Shift+F5 | 终止当前程序执行。 |
另外,可以通过右键某一行,“运行到光标处”。

多进程调试:在一个调试过程中,启动另一个调试,调试工具栏会当前活动的进程,在下拉菜单中选择其他进程:

同时 “调用堆栈” 窗口中可以切换不同进程进行调试:

2 调试控制台
调试控制台主要用于在程序暂停时查看变量、计算表达式,以及直接调用调试器命令。表达式会在当前选中的调用栈帧中求值。

- 常用 C++ 表达式示例:
i // 直接查看变量
vec.size() // 向量长度
vec[3] // 向量索引
*ptr // 查看指针的值
obj.member // 访问对象成员
this->member // 指针访问对象成员
static_cast<int>(status) // 类型转换
- GDB 常用命令:
-exec print vec \\ 打印变量
-exec whatis vec \\ 显示变量基本类型
-exec break test.cpp:50 \\ 设置断点
-exec watch count \\ 变量被写入时暂停
-exec info breakpoints \\ 断点信息
-exec info threads \\ 线程信息
-exec where \\ 查看当前执行位置
3 调试侧边栏
3.1 变量
3.1.1 Locals
局部变量名后面的 = 表示“该变量当前的值或摘要”。

对于复杂对象,调试器通常不会直接显示全部内容,而是给出一行概括。部分显示结果说明:
| 调试显示 | 含义 |
|---|---|
{...} | 复杂对象,内部内容被折叠 |
1 | 当前标量值为 1,具体含义取决于类型 |
std::vector of length 20, capacity 20 | vector 有 20 个元素,已分配 20 个元素的空间 |
std::map with 3 elements | map 中有 3 个键值对 |
OptFu::ElemOrder::First | 当前值是 ElemOrder 枚举中的 First |
Eigen::Matrix<double,3,1344,ColMajor> | 3×1344 的 double 矩阵,按列存储 |
data ptr: 0x7fff3db00140 | 数据所在内存的起始地址 |
std::unique_ptr<OptFu::ds::bvh::BVHTree> | 管理 BVHTree 对象的独占智能指针 |
注意: _M_ 开头的字段属于标准库内部实现。
“this“是由编译器自动提供的指针,指向“调用当前成员函数的那个对象”。
3.1.2 Registers
寄存器是 CPU 内部的高速存储单元,直接保存执行指令所需的数据。调试时通过寄存器窗口可以观察 CPU 的实时状态。

寄存器窗口通常包含以下内容:
- CPU / 通用寄存器
这类寄存器是最常见的,主要用于普通算术、逻辑运算、地址运算和函数调用传参。它们的作用可以理解为“程序当前正在处理什么数据”。常见有:
rax/rbx/rcx/rdx:通用数据寄存器;rsp:栈指针,决定当前函数调用栈的位置;rbp:栈基址指针,帮助访问当前函数的局部变量;rip:指令指针,表示下一条将执行的机器指令;rsi/rdi:常用于内存操作和函数参数传递。
它们的用途主要是:看程序“现在在算什么、用的是什么地址、当前函数调用栈是不是正常”。
- Segs(段寄存器)
这类寄存器用于告诉 CPU 当前代码、数据、栈分别位于哪个内存段。它们的作用更多是“定位内存区域”。常见有:
cs:代码段;ds/es/ss:数据段、额外段、栈段;fs/gs:在现代系统中常被用于线程本地存储(TLS)等特殊用途。
在调试时它们通常不是最主要的关注点,但遇到异常访问、线程切换、TLS 相关问题时,仍然可能很有帮助。
- FPU(浮点协处理器)
FPU 寄存器用于浮点运算,尤其是老式的 x87 浮点计算。它们的作用是帮助 CPU 处理小数、科学计算、三角函数等运算。常见是 st0 ~ st7。
在调试时,如果程序出现浮点异常、NaN、Inf,或者数值计算结果异常,通常会查看这些寄存器。
- SSE / AVX(向量寄存器)
这类寄存器用于 SIMD(单指令多数据)并行计算,常用于图形、数值计算、视频处理等场景。它们能一次处理多个数据,提高性能。常见有:
xmm0~xmm15:SSE 寄存器,通常 128 位;ymm0~ymm15:AVX 寄存器,通常 256 位;zmm0~zmm31:AVX-512 寄存器,通常 512 位。
调试时,若发现程序在做向量化计算、矩阵运算、图像处理等任务,SSE/AVX 寄存器可以帮助你看“当前是不是在批量处理多个数据”。
- Other Registers(其他寄存器)
除了上面几类,还有一些很重要的寄存器:
rflags/eflags:标志寄存器,记录比较结果、溢出、进位等状态;mxcsr:控制和状态寄存器,影响浮点异常处理和 SIMD 浮点精度;- 调试寄存器、控制寄存器等:用于断点、异常处理和系统级调试。
这些寄存器的作用是帮助判断“当前这一步运算是否成功、是否发生了异常、控制流是否按预期进行”。
3.2 监视

可以输入符合当前调试上下文的 C++ 表达式,例如:
a
a * b
vec.size()
vec[0]
ptr->value
object.member
this->member
也可以在变量窗口中复制变量的表达式,粘贴在监视窗口中,或者直接添加到监视。
但是变量名称是否能直接使用,仍然取决于它在当前代码位置是否可见,常见查看变量的方式如下:
| 变量类型 | 典型查看方式 |
|---|---|
| 当前函数局部变量 | value |
| 全局变量 | global_value 或 ::global_value |
| 命名空间变量 | Namespace::value |
| 类静态成员 | ClassName::value |
| 对象普通成员 | object.value |
| 指针指向对象的成员 | ptr->value |
| 当前对象成员 | value 或 this->value |
| 调用者函数的局部变量 | 切换到调用者的调用栈帧 |
| 已离开作用域的局部变量 | 通常无法查看 |
3.3 调用堆栈
调用堆栈展示程序从入口函数到当前暂停位置的函数调用链。

核心思路是“从当前执行点,向上追溯它是怎么被调用到这里的”。例如:
main()
└─ Solver::run()
└─ run_analysis()
└─ generate_mesh() ← 当前帧
这表示当前停在 generate_mesh(),它是由 run_analysis() 调用的,而 run_analysis() 又是由 Solver::run() 调用的,最终由 main() 发起。
在多进程和多线程的场景中,可以把调用堆栈理解成一个分层结构:
[进程 A]
├─ [线程 1]
│ └─ main()
│ └─ Solver::run()
│ └─ run_analysis()
│ └─ generate_mesh() ← 当前帧
├─ [线程 2]
│ └─ worker_thread()
│ └─ process_data()
[进程 B]
└─ [线程 1]
└─ server_loop()
└─ handle_request()
也就是说,调试时可以先从“进程”一级选择要看的目标,再从“线程”一级选择具体执行流,最后再挑选某个线程下面的若干“栈帧”来查看对应函数的局部变量和调用关系。
单击任意栈帧,可以切换到该函数的暂停位置,并查看当时可见的局部变量。
3.4 断点
常用断点包括:
| 类型 | 用途 | 示例 |
|---|---|---|
| 普通断点 | 运行到这里时暂停 | 直接停在某一行代码 |
| 条件断点 | 仅在表达式为真时暂停 | i == 999、ptr == nullptr |
| 命中次数断点 | 命中指定次数后暂停 | 第 100 次进入循环时暂停 |
| 日志点 | 调试控制台输出信息但不暂停程序 | 输出 i、对象状态或执行路径 |
| 触发断点 | 选定断点触发后启用 | src/RodLattice/rod_lattice.cpp:925 |
| 函数断点 | 进入指定函数时暂停 | Solver::run |
| 内联断点 | 一行内多个可执行语句分别设置断点 | int a = foo(); int b = bar(); |
| 数据断点 | 某块内存被读取或写入时暂停 | -exec watch count , 追踪成员变量何时被意外修改 |
| 异常断点 | 抛出或捕获异常时暂停 | 定位异常最初抛出的位置 |
命中次数断点在gdb中不支持
一个断点不仅可以设置一种属性,还可以可以叠加多个属性。可以把它理解成:一个断点“既能判断条件,还能记录日志”。
例如:


这类组合使用非常实用,尤其适合高频循环、复杂条件判断或需要观察多次执行结果的场景。右键行号旁的断点,可以编辑条件、命中次数,或者把它改为日志点。日志点适合高频循环:它不会频繁中断程序,比反复点击“继续”更容易观察变量变化。
4 常见调试问题
4.1 变量显示为 <optimized out>
使用带调试符号的低优化构建。例如 CMake 项目可以选择 Debug 配置,并确认实际编译参数中包含 -g。仅在 launch.json 中修改设置,不能替代重新编译。
4.2 断点停在错误的源码行,单步跳过/单步调试不按预期
使用 F10/F11 进行单步操作时,如果没有跳到预期源码行,常见原因包括:
- 编译器优化导致代码行被重排、内联或合并,源码行与实际执行位置不完全匹配;
- 当前调试符号已过期,源码改动后未重新编译;
- 宏展开、模板实例化或复杂表达式造成行号映射不准确;
- 代码被内联或尾调用优化,F10/F11 的“进入/跳过”行为与源码显示不同;
黄色高亮行是当前 CPU 指令地址所对应的源码行
4.3 多线程程序单步时跳转混乱
单步期间其他线程仍可能运行并触发断点。可以先把线程数设为 1,以判断问题是否与并发有关。
Tips:
- 通过启用
debug.showBreakpointsInOverviewRuler设置,在编辑器右侧的概览标尺中显示断点。 - 如果更喜欢在树状视图中查看按文件分组的断点,请将
debug.breakpointsView.presentation设置配置为tree。 - 在聚焦于变量部分时使用键盘快捷键 Ctrl+Alt+F,然后输入搜索词,可按名称或值过滤变量
- 调用堆栈可以通过关键词,如blas、omp、mkl、tbb等,判断是哪个库开启的线程
调试优化级别
- gcc调试信息等级:
| 选项 | 描述 |
|---|---|
-g | 默认级别,通常等同于 -g2。 |
-g1 | 最少量的调试信息,仅包含行号和最基本的变量/函数信息。 |
-g2 | 推荐。包含标准调试信息,包括局部变量、函数参数、完整的类型信息等。 |
-g3 | 包含 -g2 的所有信息,以及宏定义。这会生成最大的调试文件。 |
- gcc 编译优化等级:
| 选项 | 作用 | 适用场景 |
|---|---|---|
-O0 | 默认优化等级,不进行主动优化,保证代码逻辑与源码一致,最适合调试阶段 | 调试、排查逻辑错误 |
-O1 | 基础优化,适度提高执行效率,同时保留较好的调试能力 | 轻量性能优化 |
-O2 | 通用优化,平衡执行速度和代码大小,适合大多数场景 | 大多数生产环境 |
-O3 | 激进优化,追求极致性能,可能增加代码体积,需实测验证 | 高性能场景 |
-Os | 优化代码尺寸,适合嵌入式或存储受限场景 | 嵌入式、资源受限 |
-Ofast | 在 -O3 基础上进一步放宽标准兼容性以追求性能,慎用 | 只在确认兼容性可接受时使用 |
-Og | 专为调试设计,在提供优化的同时保持较好的调试友好性 | 调试与性能兼顾 |
在调试阶段,推荐使用 -O0 -g 组合,以便完整生成调试信息并方便断点调试。生产阶段可根据目标平台和性能需求选择 -O2 或 -O3,并结合链接器优化(如 --gc-sections)进一步减少未使用代码。
带参调试
在launch.json的”args” : 中添加值,通过F5或者 “运行和调试” 窗口上方的蓝色三角启动运行,可以传入int mainint argc, char *argv [ ]的argv中,可以根据实际情况调用。