3. GDB断点基础

断点,说白了就是调试器里的「暂停键」。你让程序跑着,它跑到某一行就自动停下来,等你检查变量、看堆栈、分析逻辑。我刚开始学GDB时,觉得断点不就是个暂停嘛,有啥好学的?后来在Qt项目里追一个信号槽死循环,才发现断点用得好不好,直接决定你是一小时解决问题,还是一整天原地打转。

这一节,咱们就把断点的基本功打扎实。你想想看,如果连断点都不会设,后面的条件断点、观察点、捕获点就更别谈了。

3.1 设置断点:break 命令

break 是GDB里最常用的命令,缩写是 b。我个人习惯直接用 b,少打几个字,调试时效率更高。

基本用法有这几种:

命令 说明 示例
b 行号 在指定行设置断点 b 42
b 函数名 在函数入口设置断点 b main
b 文件名:行号 在指定文件的某行设断点 b main.cpp:42
b 文件名:函数名 在指定文件的函数入口设断点 b main.cpp:main
b +偏移量 从当前行往后偏移N行 b +5

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

// main.cpp
#include <iostream>
#include <QApplication>
#include <QPushButton>

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);  // 第8行

    QPushButton btn("Click me");   // 第10行
    btn.show();                    // 第11行

    return app.exec();             // 第13行
}

在GDB里,你可以这样设断点:

(gdb) b 10
Breakpoint 1 at 0x4007f0: file main.cpp, line 10.
(gdb) b main
Breakpoint 2 at 0x4007e0: file main.cpp, line 8.
(gdb) b main.cpp:13
Breakpoint 3 at 0x400810: file main.cpp, line 13.
我的小技巧: 在Qt项目里,我经常用 b QPushButton::setText 这种形式来断住某个Qt方法。但要注意,如果函数是内联的或者被编译器优化掉了,可能断不住。这时候我会加 -O0 编译选项重新编译。

3.2 查看断点:info break

断点设多了,难免会忘记自己设了哪些。这时候 info break 就派上用场了。缩写是 i b。

(gdb) info break
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x00000000004007f0 in main at main.cpp:10
2       breakpoint     keep y   0x00000000004007e0 in main at main.cpp:8
3       breakpoint     keep y   0x0000000000400810 in main at main.cpp:13

每一列的含义:

  • Num:断点编号,后面删除、启用禁用都靠它
  • Type:断点类型,breakpoint 是普通断点,还有 watchpoint、catchpoint 等
  • Disp:处置方式,keep 表示保留,del 表示命中后自动删除
  • Enb:是否启用,y 表示启用,n 表示禁用
  • Address:断点的内存地址
  • What:断点所在位置

我记得有一次追一个Qt信号槽的bug,一口气设了十几个断点。结果跑起来后,程序到处停,根本分不清哪个是哪个。后来我养成了一个习惯:每设一个断点,就用 comment 给它加个备注。

(gdb) b main.cpp:42
Breakpoint 4 at 0x400850: file main.cpp, line 42.
(gdb) comment 4
> 这里是处理按钮点击后的回调函数入口
> 

这样再看 info break 时,最后一列会显示你的备注,一目了然。

3.3 删除断点:delete 命令

断点不用了,就该删掉。用 delete 命令,缩写是 d。

(gdb) d 1          # 删除编号为1的断点
(gdb) d 1 2 3      # 删除编号1、2、3的断点
(gdb) d            # 删除所有断点(会提示确认)
注意: 我曾经在调试一个线上问题时,手快打了个 d 没加编号,结果把所有断点都删了。当时那个项目编译一次要5分钟,我整个人都麻了。所以建议你:删除时一定带上编号。如果真想清空所有断点,用 d 后GDB会问你是否确认,看清楚再回答。

3.4 启用/禁用断点:enable / disable

有时候你不想删断点,只是暂时不想让它生效。比如你设了一个断点在循环里,每次循环都停,烦得很。这时候用 disable 把它禁掉,等需要时再 enable 启用。

(gdb) disable 2       # 禁用编号2的断点
(gdb) enable 2        # 启用编号2的断点
(gdb) disable 1 2 3   # 禁用多个
(gdb) enable 1 2 3    # 启用多个

禁用后,info break 里对应的 Enb 列会变成 n:

(gdb) disable 2
(gdb) info break
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x00000000004007f0 in main at main.cpp:10
2       breakpoint     keep n   0x00000000004007e0 in main at main.cpp:8
3       breakpoint     keep y   0x0000000000400810 in main at main.cpp:13

嗯,这里要注意:禁用断点后,程序跑到那一行不会停,但断点本身还在,编号也不会变。你随时可以重新启用。

3.5 实战场景:在Qt信号槽中设置断点

Qt的信号槽机制,说白了就是函数调用。你可以在槽函数里设断点,也可以在发射信号的代码行设断点。但有个坑:如果你在 emit 那一行设断点,程序停住时,信号还没真正发出去。如果你想在信号发出后、槽函数执行前停住,那就得在槽函数入口设断点。

举个例子:

// widget.cpp
void Widget::onButtonClicked()
{
    emit mySignal("hello");  // 第15行
}

void Widget::onMySignal(const QString &msg)
{
    qDebug() << msg;        // 第20行
}

如果你在15行设断点,停住时信号还没发。如果你在20行设断点,停住时信号已经发了,槽函数正在执行。我个人习惯是在槽函数入口设断点,因为我想看的是槽函数接收到的参数对不对。

核心要点: 断点不是越多越好,而是越精准越好。设断点前先想清楚:我想在哪个时刻停下来?我想看什么数据?想清楚了再设,别盲目。

3.6 本章知识体系

下面这张图帮你梳理了断点操作的核心流程:

GDB断点操作核心流程 设置断点 (break / b) b 行号 | b 函数名 | b 文件名:行号 | b 文件名:函数名 查看断点 (info break / i b) 查看编号、类型、启用状态、位置 管理断点 删除 (delete / d) 禁用 (disable) 启用 (enable) 核心原则:精准设断,及时清理,善用启用/禁用

这张图把断点的生命周期串起来了:先设置,再查看,然后根据需要进行删除、禁用或启用。你调试时就在这四个操作之间来回切换,熟练了以后,效率会高很多。

我的习惯: 调试Qt信号槽时,我通常先在槽函数入口设断点,然后用 disable 禁掉。等程序跑到发射信号的地方,我再手动 enable 那个断点。这样既不会漏掉,也不会被无关的断点干扰。