2. 断点基础

调试这件事,说白了就是让程序在关键时刻停下来,让你看看它到底在干什么。而断点,就是那个「关键时刻」的标记。

我记得刚入行那会儿,调试全靠 printf 大法。代码里到处是 printf("here1")、printf("here2")…… 后来被 mentor 骂了一顿:「你这是在写代码还是在写日志?」从那以后,我才老老实实学 GDB。嗯,断点这东西,一旦用熟了,你就再也回不去了。

2.1 设置断点:break 命令

设置断点最常用的命令就是 break,简写为 b。我个人习惯用简写,毕竟调试的时候谁也不想打那么多字。

基本用法有这几种:

命令 说明 示例
break 行号 在指定行设置断点 b 42
break 函数名 在函数入口处设置断点 b main
break 文件名:行号 在指定文件的某行设置断点 b server.c:128
break 文件名:函数名 在指定文件的函数入口设置断点 b utils.c:parse_request
break +偏移量 从当前行往后偏移若干行 b +5

举个例子,假设我们有这么一段代码:

// server.c
1:  #include <stdio.h>
2:  #include <stdlib.h>
3:  
4:  int handle_request(int fd) {
5:      char buffer[1024];
6:      int n = read(fd, buffer, sizeof(buffer));
7:      if (n < 0) {
8:          return -1;
9:      }
10:     // 处理请求...
11:     return 0;
12: }
13: 
14: int main() {
15:     int sock = create_socket();
16:     while (1) {
17:         int client = accept(sock, NULL, NULL);
18:         handle_request(client);
19:     }
20:     return 0;
21: }

在 GDB 里可以这样设断点:

(gdb) b 17              # 在 accept 调用处停下
(gdb) b handle_request  # 在函数入口处停下
(gdb) b server.c:8      # 在错误处理处停下
小技巧: 如果你在调试多线程程序,可以用 break 函数名 thread 线程号 来让断点只对某个线程生效。我在排查一个并发 bug 时用过这招,效果拔群。

2.2 条件断点:让断点更聪明

有时候你不想每次循环都停下来,只想在某个条件满足时才停。这时候就需要条件断点了。

(gdb) b 17 if client == -1
(gdb) b handle_request if fd > 100

我曾经调试过一个内存泄漏问题,程序里有个循环处理了上万个请求。如果每个请求都停下来,那得等到猴年马月。我加了个条件断点 b free_pool if pool->used > 1024,只在内存池使用量超过阈值时才停。嗯,一下子就定位到了问题。

注意: 条件断点每次命中时都会评估条件表达式。如果表达式本身有副作用(比如调用了某个函数),可能会改变程序行为。我曾经见过同事在条件里写了个 malloc,结果程序跑着跑着就崩了…… 别问我怎么知道的。

2.3 查看断点:info breakpoints

断点设多了,难免会忘记哪些设了、设在哪里。这时候 info breakpoints(简写 i b)就派上用场了。

(gdb) info breakpoints
Num     Type           Disp Enb Address    What
1       breakpoint     keep y   0x4005a2   in main at server.c:17
2       breakpoint     keep y   0x4005c0   in handle_request at server.c:5
3       breakpoint     keep n   0x4005d0   in handle_request at server.c:8
        stop only if fd < 0

输出里每一列的含义:

  • Num:断点编号,后续操作都用这个编号
  • Type:断点类型,一般是 breakpoint
  • Disp:命中后的行为,keep 表示保留,del 表示删除
  • Enb:是否启用,y 表示启用,n 表示禁用
  • Address:断点所在的地址
  • What:断点位置描述

你想想看,如果断点编号是 3,但 Enb 列显示的是 n,说明这个断点被禁用了。这在调试时很有用——你暂时不想停在这里,但又不想删掉它。

2.4 删除与禁用断点

断点设多了,该清理的时候就得清理。GDB 提供了两种方式:删除和禁用。

删除断点:delete

(gdb) delete 1          # 删除编号为 1 的断点
(gdb) delete 1 2 3      # 删除多个断点
(gdb) delete            # 删除所有断点(会提示确认)

禁用/启用断点:disable / enable

(gdb) disable 2         # 禁用编号为 2 的断点
(gdb) enable 2          # 重新启用
(gdb) disable           # 禁用所有断点
(gdb) enable            # 启用所有断点

禁用和删除的区别在于:禁用的断点还在,只是暂时不生效;删除了就真的没了,想用还得重新设。我个人习惯是:

  • 如果这个断点后面还可能用到 → 用 disable
  • 如果这个断点已经没用了 → 用 delete
  • 如果调试结束后想保留断点配置 → 用 save breakpoints bp.txt 保存,下次用 source bp.txt 恢复
避坑指南: 我曾经在调试一个网络服务时,设了十几个断点,然后一个个 disable 掉。结果第二天回来忘了哪些是禁用的,哪些是启用的,调试起来一团糟。后来我养成了一个习惯:每次调试只保留 3-5 个活跃断点,其他的要么删掉,要么保存到文件里。清爽多了。

2.5 知识体系总览

下面这张图帮你理清断点操作的核心脉络:

断点操作知识体系 断点 (Breakpoint) 设置断点 (break) break 行号 break 函数名 break 文件:行号 break ... if 条件 查看断点 (info b) 查看编号/类型/状态 查看位置/条件 查看命中次数 删除断点 (delete) delete 编号 delete 范围 delete(全部) 禁用/启用 (disable/enable) disable 编号 enable 编号 disable/enable(全部) 核心原则:设断点要精准,查断点要勤快,删断点要果断

2.6 实战小贴士

最后分享几个我平时用得比较多的技巧:

  1. 临时断点:用 tbreak 设置临时断点,命中一次后自动删除。适合只想停一次的场景。
  2. 硬件断点:用 hbreak 设置硬件断点,适合调试内核代码或 ROM 中的代码。
  3. 断点命令列表:用 commands 编号 给断点绑定一组命令,命中后自动执行。比如自动打印变量值然后继续运行。
(gdb) commands 1
> print fd
> continue
> end

这样每次命中断点 1 时,GDB 会自动打印 fd 的值,然后继续运行。省得你一次次手动敲命令。

断点这东西,说白了就是调试的「眼睛」。设得好,问题一眼就能看出来;设得乱,反而把自己绕进去。多练练,慢慢就有感觉了。