第4章 断点设置:让程序在你想停的地方停下来

调试这件事,说白了就是让程序按你的意愿运行。

你想让它跑,它就跑。你想让它停,它就停。而断点,就是那个让程序停下来的开关。

我个人习惯把断点比作「路障」。程序执行到路障前,会自动刹车。这时候你就可以检查变量、查看堆栈、分析状态。嗯,今天我们就来聊聊怎么设置这些路障。

4.1 break命令:最基础的断点

GDB里设置断点的命令是 break,简写为 b。用法很简单:

(gdb) break main.c:42
(gdb) b main.c:42   // 简写形式

这表示在 main.c 文件的第42行设置一个断点。程序执行到这一行时,就会停下来。

我记得刚学GDB那会儿,总觉得记命令很麻烦。后来发现,其实常用的就那几个。break、continue、next、step、print,五个命令能解决80%的问题。

小技巧: 如果你不确定当前在哪个文件,可以用 info source 查看。我经常用这个命令确认自己没跑偏。

4.2 设置行号断点

行号断点是最直观的。你打开源码,看到某一行有问题,直接在那行设个断点就行。

// example.c
#include <stdio.h>

int main() {
    int a = 10;
    int b = 20;
    int c = a + b;    // 我想看看这行执行后c的值
    printf("c = %d\n", c);
    return 0;
}

在GDB里:

(gdb) b example.c:6
Breakpoint 1 at 0x4004f6: file example.c, line 6.
(gdb) run
Starting program: /home/user/example

Breakpoint 1, main () at example.c:6
6           int c = a + b;

程序停在了第6行。这时候你可以用 print a、print b 看看它们的值。

注意: 行号断点依赖源码文件。如果你修改了源码但没有重新编译,行号可能会对不上。我曾经因为这个浪费了半天时间,后来养成了「改完代码必重新编译」的习惯。

4.3 设置函数断点

有时候你不想关心具体在哪一行,只想在某个函数被调用时停下来。这时候用函数断点就方便了。

(gdb) b main
Breakpoint 1 at 0x4004e6: file example.c, line 4.
(gdb) b printf
Breakpoint 2 at 0x4004d0: file printf.c, line 45.

第一个断点设在 main 函数入口。第二个断点设在 printf 函数入口。

函数断点有个好处:你不需要知道函数定义在哪一行。只要知道函数名就行。我在调试大型项目时特别喜欢用这个,因为项目文件太多,找行号太费劲了。

你想想看,一个项目几百个源文件,你记得住每个函数在第几行吗?反正我记不住。用函数名断点,省心多了。

4.4 设置条件断点

条件断点,说白了就是带条件的断点。程序只有在满足条件时才会停下来。

举个例子:你有一个循环,循环1000次。你只想知道当 i == 500 时发生了什么。如果设普通断点,你得按500次 continue,手都按酸了。

条件断点就是来解决这个问题的:

// loop.c
#include <stdio.h>

int main() {
    for (int i = 0; i < 1000; i++) {
        // 一些处理逻辑
        printf("i = %d\n", i);
    }
    return 0;
}

在GDB里:

(gdb) b loop.c:5 if i == 500
Breakpoint 1 at 0x4004f6: file loop.c, line 5.
(gdb) run
Starting program: /home/user/loop
...
Breakpoint 1, main () at loop.c:5
5               printf("i = %d\n", i);
(gdb) print i
$1 = 500

看到了吗?程序直接停在了 i == 500 的那一次。不用你手动按500次。

核心要点: 条件断点的语法是 break [位置] if [条件]。位置可以是行号,也可以是函数名。条件可以是任何合法的C/C++表达式。

条件断点还有更高级的用法。比如你想监控一个指针是否为空:

(gdb) b my_function if ptr == NULL

或者你想监控一个变量是否超过某个阈值:

(gdb) b process_data if data_size > 1024

我曾经在调试一个网络协议栈时,遇到一个诡异的问题:数据包偶尔会丢失。用条件断点监控 packet->length == 0,一下子就找到了问题根源——某个分支忘记给长度赋值了。

4.5 断点的管理

断点设多了,有时候会忘记自己设了哪些。这时候可以用 info breakpoints 查看:

(gdb) info breakpoints
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x00000000004004f6 in main at example.c:6
        breakpoint already hit 1 time
2       breakpoint     keep y   0x00000000004004d0 in printf at printf.c:45
3       breakpoint     keep y   0x00000000004004f6 in main at loop.c:5
        stop only if i == 500
        breakpoint already hit 1 time

表格里每一列的含义:

列名 含义
Num 断点编号,从1开始
Type 断点类型,一般是breakpoint
Disp 断点命中后的行为,keep表示保留,del表示删除
Enb 是否启用,y表示启用,n表示禁用
Address 断点的内存地址
What 断点的位置和条件

删除断点用 delete 命令:

(gdb) delete 1    // 删除编号为1的断点
(gdb) delete      // 删除所有断点(会提示确认)

禁用断点用 disable,启用用 enable:

(gdb) disable 2   // 禁用编号为2的断点
(gdb) enable 2    // 重新启用
实用技巧: 有时候你不想删除断点,只是暂时不想让它生效。用 disable 比 delete 更合适。我经常在调试时临时禁用一些断点,等需要时再启用,省得重新设置。

4.6 本章知识体系

下面这张图总结了断点设置的核心内容:

断点设置知识体系 break 命令 行号断点 函数断点 条件断点 断点管理 b file.c:行号 依赖源码文件 b 函数名 无需知道行号 b 位置 if 条件 info breakpoints delete/disable

4.7 避坑指南

最后分享几个我踩过的坑:

  • 优化编译导致断点不准:用 -O2 编译时,编译器可能会重排代码。断点设在第10行,实际停在第8行。我建议调试时用 -O0 -g 编译。
  • 条件断点中的副作用:条件表达式里不要写有副作用的代码,比如 if i++ == 500。这会导致程序逻辑改变,我吃过这个亏。
  • 断点太多影响性能:设太多断点,程序会变慢。尤其是条件断点,每次执行都要检查条件。我一般控制在10个以内。
  • 共享库的断点:给共享库设断点,需要等库加载后才能设。可以用 break 加函数名,GDB会自动处理。

好了,断点设置就讲到这里。记住一句话:断点是你和程序对话的窗口。设好断点,你就掌握了调试的主动权。

本章总结:
  • break命令是设置断点的核心命令
  • 行号断点精确到行,函数断点精确到函数入口
  • 条件断点用 if 关键字,只在条件满足时触发
  • 用 info breakpoints 管理断点,用 delete 和 disable 控制断点状态

可可精品资料库