调试的第一步永远是「让程序停下来」。断点(breakpoint)就是你插在代码里的一面小旗:程序跑到这里就暂停,把现场交给你检查。这一篇把断点的各种用法讲全:按函数、按行号、条件断点、临时断点、忽略次数,以及启用、禁用、删除,最后再介绍两种「停下来但不打扰你」的断点——命中时自动执行命令的断点和 dprintf。

这是「C++ 调试实战」系列的第 1 篇。示例程序沿用第 00 篇的 orders.cpp,Linux 版用 g++ -std=c++20 -g -O0 orders.cpp -o orders 编译,macOS 版编译为 orders.mac。

先回顾一下 orders.cpp 的关键行号:

行号 代码
11–17 double line_total(const Order& o):12 行计算小计,14 行打九折,16 行 return total;
19–25 double sum_orders(const std::vector<Order>& orders):22 行 sum += line_total(o);
27–36 main():33 行 double total = sum_orders(orders);

一、先看代码:list

下断点之前,往往想先看看代码、确认行号。不用切回编辑器,调试器里就能看:

1
2
3
4
5
6
7
8
9
10
11
(gdb) list line_total
6 std::string name;
7 int quantity;
8 double price;
9 };
10
11 double line_total(const Order& o) {
12 double total = o.quantity * o.price;
13 if (o.quantity >= 10) {
14 total *= 0.9; // 满 10 件打九折
15 }

LLDB 的写法一样(list line_total,完整形式是 source list -n line_total)。常见用法:

命令 作用
list 函数名 显示函数附近的代码
list 20 / list orders.cpp:20 显示第 20 行附近
list 接着上次的位置往下显示

二、三种最常用的下断点方式

2.1 按函数名

1
2
(gdb) break line_total
Breakpoint 1 at 0x19b0: file orders.cpp, line 12.
1
2
(lldb) b line_total
Breakpoint 1: where = orders.mac`line_total(Order const&) + 8 at orders.cpp:12:20, address = 0x00000001000004f0

注意断点落在第 12 行,而不是函数声明所在的第 11 行:调试器会自动跳过函数的「序言」(prologue,保存寄存器、分配栈空间的那几条指令),停在函数体第一行真正的代码上。

函数名下断点最省心:不怕你改代码导致行号变化,只要函数名不变就能停住。

2.2 按文件名 + 行号

1
2
(gdb) break orders.cpp:22
Breakpoint 2 at 0x1a54: file orders.cpp, line 22.
1
2
(lldb) b orders.cpp:22
Breakpoint 3: where = orders.mac`sum_orders(std::__1::vector<Order, std::__1::allocator<Order>> const&) + 92 at orders.cpp:22:27, address = 0x00000001000005b0

LLDB 的完整写法是 breakpoint set --file orders.cpp --line 22(缩写 br s -f orders.cpp -l 22)。

2.3 只写行号

程序停下来之后,**只写行号就表示「当前文件」**的那一行:

1
2
(lldb) b 16
Breakpoint 2: where = orders.mac`line_total(Order const&) + 96 at orders.cpp:16:12, address = 0x0000000100000548

GDB 同样支持 break 16。

2.4 C++ 函数名的写法

C++ 有命名空间、类成员、重载,函数名写法需要多说一句:

场景 写法示例
普通函数 break line_total
类成员函数 break Account::deposit
命名空间里的函数 break school_db::open
重载函数只停某一个 break parse(const std::string&)(GDB)

对于重载函数,只写名字时调试器会在所有同名函数上都下断点,info breakpoints 里会看到一个断点带多个位置(1.1、1.2……)。想只停其中一个,GDB 里写完整参数列表;LLDB 里可以下完之后用 breakpoint disable 1.2 禁掉不需要的位置。

三、查看断点列表

3.1 GDB:info breakpoints

1
2
3
4
(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0x00000000000019b0 in line_total(Order const&) at orders.cpp:12
2 breakpoint keep y 0x0000000000001a54 in sum_orders(std::vector<Order, std::allocator<Order> > const&) at orders.cpp:22

每一列的含义:

列 含义
Num 断点编号,后续启用 / 禁用 / 删除都用它
Type breakpoint 普通断点;还有 hw watchpoint(观察点)、dprintf 等
Disp 命中后的处置:keep 保留;del 命中一次就删除(临时断点)
Enb 是否启用:y / n
Address 断点所在的机器指令地址
What 对应的函数和源码位置

程序运行、断点命中后再看,会多出命中次数,地址也变成了真实加载地址(程序启动前 GDB 只知道相对偏移):

1
2
3
4
5
6
(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000aaaaaaaa19b0 in line_total(Order const&) at orders.cpp:12
breakpoint already hit 1 time
2 breakpoint keep y 0x0000aaaaaaaa1a54 in sum_orders(std::vector<Order, std::allocator<Order> > const&) at orders.cpp:22
breakpoint already hit 1 time

3.2 LLDB:breakpoint list

1
2
3
4
5
6
7
8
9
10
11
12
(lldb) breakpoint list
Current breakpoints:
1: name = 'line_total', locations = 1
Condition: o.quantity >= 10

1.1: where = orders.mac`line_total(Order const&) + 8 at orders.cpp:12:20, address = orders.mac[0x00000001000004f0], unresolved, hit count = 0

2: name = 'main', locations = 1 Options: enabled one-shot
2.1: where = orders.mac`main + 40 at orders.cpp:28:33, address = orders.mac[0x000000010000070c], unresolved, hit count = 0

3: file = 'orders.cpp', line = 22, exact_match = 0, locations = 1
3.1: where = orders.mac`sum_orders(std::__1::vector<Order, std::__1::allocator<Order>> const&) + 92 at orders.cpp:22:27, address = orders.mac[0x00000001000005b0], unresolved, hit count = 0

(这份列表里的条件断点和 one-shot 断点,下面马上会讲到。)LLDB 的每个断点可以有多个位置(location),编号写作 1.1、3.1;unresolved 表示程序还没运行、地址还没最终确定,hit count 是命中次数。缩写:br l。

四、运行到断点

断点下好之后:

命令 作用
run / r 从头启动程序,跑到第一个命中的断点
run arg1 arg2 带命令行参数启动(也可以启动调试器时写 gdb --args ./prog arg1 arg2,LLDB 是 lldb -- ./prog arg1 arg2)
continue / c 从当前位置继续,跑到下一个命中的断点
1
2
3
4
5
6
7
8
9
10
11
(gdb) run

Breakpoint 2, sum_orders (orders=std::vector of length 3, capacity 3 = {...}) at orders.cpp:22
22 sum += line_total(o);
(gdb) bt
#0 sum_orders (orders=std::vector of length 3, capacity 3 = {...}) at orders.cpp:22
#1 0x0000aaaaaaaa1cac in main () at orders.cpp:33
(gdb) continue

Breakpoint 1, line_total (o=...) at orders.cpp:12
12 double total = o.quantity * o.price;

run 之后先停在断点 2(sum_orders 的第 22 行),bt 看一眼调用栈(第 04 篇细讲),continue 之后进入 line_total,停在断点 1。

停下来的那一行还没有执行。 这是初学者最容易搞混的地方:「停在第 12 行」的意思是「第 12 行即将执行」,此时 total 还是未初始化的值。

五、临时断点:只停一次

有时只想停一次,比如「先停在 main 开头,然后再说」。临时断点命中一次后自动删除:

1
2
3
4
5
6
7
(gdb) tbreak main
Temporary breakpoint 2 at 0x1b10: file orders.cpp, line 27.
...
(gdb) run

Temporary breakpoint 2, main () at orders.cpp:27
27 int main() {

info breakpoints 里它的 Disp 列是 del。LLDB 同样有 tbreak,完整写法是 breakpoint set --one-shot true --name main:

1
2
3
4
(lldb) tbreak main
Breakpoint 2: where = orders.mac`main + 40 at orders.cpp:28:33, address = 0x000000010000070c
...
* thread #1, queue = 'com.apple.main-thread', stop reason = one-shot breakpoint 2

GDB 还有一个更顺手的 start 命令:等价于 tbreak main + run,下一篇会用到。

六、条件断点:只在满足条件时停

line_total 每笔订单都会调用一次。如果只关心「满 10 件打折」的那笔,每次都停就太烦了。条件断点只在表达式为真时才停:

1
2
3
4
5
6
7
8
(gdb) break line_total if o.quantity >= 10
Breakpoint 1 at 0x19b0: file orders.cpp, line 12.
(gdb) run
...
Breakpoint 1, line_total (o=...) at orders.cpp:12
12 double total = o.quantity * o.price;
(gdb) print o
$1 = (const Order &) @0xaaaaaaad3050: {name = "banana", quantity = 12, price = 1}

直接跳过了 apple,停在 banana(数量 12)。info breakpoints 里会显示条件:

1
2
3
1       breakpoint     keep y   0x0000aaaaaaaa19b0 in line_total(Order const&) at orders.cpp:12
stop only if o.quantity >= 10
breakpoint already hit 1 time

给已有断点加条件、改条件、去掉条件:

1
2
(gdb) condition 1 o.quantity >= 10    # 设置 / 修改条件
(gdb) condition 1 # 去掉条件

LLDB 用 -c 选项:

1
2
(lldb) br set -n factorial -c 'n == 1'           # 下断点时就带条件
(lldb) breakpoint modify -c 'o.quantity >= 10' 1 # 给已有的 1 号断点加条件

几个实用的条件写法:

场景 条件
某个循环变量到特定值 i == 999
指针为空 p == nullptr 或 p == 0
比较 std::string GDB:$_streq(o.name.c_str(), "banana")`(`$_streq 是 GDB 内置的便利函数,本文环境实测可用)

每次经过断点,调试器都要暂停程序、算一遍条件、再恢复运行,放在执行几百万次的热循环里会明显变慢。条件里调用函数(比如上面的 c_str())还有另一个风险:模板库里的小函数经常被内联,程序里根本没有这个函数的实体,调试器调用不了,第 03 篇会专门讲这个坑。能比较普通字段(例如 o.quantity == 12)时,优先比较字段。

七、忽略次数:跳过前 N 次

另一个常见需求是「前两次别停,第三次再停」:

1
2
3
4
5
6
7
8
9
10
11
12
13
(gdb) break line_total
Breakpoint 1 at 0x19b0: file orders.cpp, line 12.
(gdb) ignore 1 2
(gdb) run

Breakpoint 1, line_total (o=...) at orders.cpp:12
12 double total = o.quantity * o.price;
(gdb) print o.name
$1 = "cherry"
(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000aaaaaaaa19b0 in line_total(Order const&) at orders.cpp:12
breakpoint already hit 3 times

前两次(apple、banana)被忽略,第三次(cherry)才停。注意命中次数是 3——被忽略的也算命中。

LLDB 用 -i:

1
2
3
4
5
6
(lldb) b line_total
(lldb) breakpoint modify -i 2 1
(lldb) run
...
(lldb) p o.name
(std::string) "cherry"

一个排查技巧:程序在循环第 N 次时出错但不知道 N 是多少,可以先 ignore 1 100000(一个足够大的数),让程序一路跑到出错,然后 info breakpoints 看命中了多少次,下次就能精确地停在出错前一次。

八、禁用、启用、删除

断点多了之后要管理。暂时不想让它停,就禁用;彻底不要了,就删除:

1
2
3
4
5
6
7
8
9
10
(gdb) disable 1
(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep n 0x0000aaaaaaaa19b0 in line_total(Order const&) at orders.cpp:12
stop only if o.quantity >= 10
breakpoint already hit 1 time
(gdb) enable 1
(gdb) delete 1
(gdb) info breakpoints
No breakpoints, watchpoints, tracepoints, or catchpoints.

Enb 列从 y 变成 n,条件和命中次数都保留着;delete 之后就彻底没了。

LLDB:

1
2
3
4
(lldb) breakpoint disable 1
1 breakpoints disabled.
(lldb) breakpoint delete 1
1 breakpoints deleted; 0 breakpoint locations disabled.
操作 GDB LLDB
禁用 1 号 disable 1 breakpoint disable 1(br dis 1)
启用 1 号 enable 1 breakpoint enable 1(br en 1)
删除 1 号 delete 1(d 1) breakpoint delete 1(br del 1)
删除全部 delete breakpoint delete(会要求确认,加 -f 跳过)
只启用一次 enable once 1 ——
禁用 / 删除多个 disable 1 3、delete 2-4 br dis 1 3

禁用比删除更常用:你辛辛苦苦配好的条件断点,删掉就得重新敲,禁用随时还能恢复。

九、不停下来的断点

有时候你并不想停,只是想知道「程序经过这里时,某个变量是多少」——也就是在不改代码的情况下临时加一行打印。

9.1 断点命令(commands)

GDB 可以给断点挂一串命令,命中时自动执行。配合 silent(不打印「Breakpoint 1, …」那一行)和 continue(执行完继续跑),就成了一个「打印点」:

1
2
3
4
5
6
7
(gdb) break line_total
Breakpoint 1 at 0x19b0: file orders.cpp, line 12.
(gdb) commands 1
>silent
>print o.name
>continue
>end

9.2 dprintf:动态 printf

更简洁的是 dprintf,语法和 C 的 printf 一样:

1
2
(gdb) dprintf orders.cpp:16,"total=%f\n", total
Dprintf 2 at 0x19f0: file orders.cpp, line 16.

两个一起用,跑一遍:

1
2
3
4
5
6
7
8
9
(gdb) run
$1 = "apple"
total=7.500000
$2 = "banana"
total=10.800000
$3 = "cherry"
total=20.000000
total = 38.3
[Inferior 1 (process 16) exited normally]

程序全程没有停,但每笔订单的名字和小计都打印出来了。info breakpoints 里能看到挂在断点上的命令:

1
2
3
4
5
6
1       breakpoint     keep y   0x00000000000019b0 in line_total(Order const&) at orders.cpp:12
silent
print o.name
continue
2 dprintf keep y 0x00000000000019f0 in line_total(Order const&) at orders.cpp:16
printf "total=%f\n", total

9.3 LLDB 的写法

LLDB 用 breakpoint command add 挂命令,用 -G true(--auto-continue)让断点执行完命令后自动继续:

1
2
3
4
5
6
7
8
9
(lldb) b 16
Breakpoint 2: where = orders.mac`line_total(Order const&) + 96 at orders.cpp:16:12, address = 0x0000000100000548
(lldb) breakpoint command add -o 'p total' 2
(lldb) breakpoint modify -G true 2
(lldb) run
(lldb) p total
(double) 7.5
(lldb) p total
(double) 10.800000000000001

这就是不用改代码、不用重新编译的「printf 调试」。

十、小结

需求 GDB LLDB
按函数下断点 break line_total b line_total
按文件行号 break orders.cpp:22 b orders.cpp:22
当前文件行号 break 16 b 16
临时断点 tbreak main tbreak main
条件断点 break f if x > 0 / condition 1 x > 0 br set -n f -c 'x > 0' / br modify -c 'x > 0' 1
忽略前 N 次 ignore 1 N br modify -i N 1
查看断点 info breakpoints breakpoint list
禁用 / 启用 / 删除 disable / enable / delete br disable / br enable / br delete
打印点 dprintf 文件:行,"格式",变量 br command add -o '命令' + br modify -G true

C++ 调试实战系列第 1 篇完。下一篇:运行与单步——next、step、finish、until,一行一行走、钻进函数、跑到函数返回。