Skip to content

LV079-gdb-condition

一、概述

前面我们了解了 GDB 调试器中普通断点、观察断点以及捕捉断点的功能和用法。其中,对于普通断点的建立,可以使用如下格式的 break 命令:

shell
(gdb) break ... if cond

... 参数用于指定生成断点的具体位置;cond 参数用于代指某个表达式。通过此方式建立的普通断点,只有当表达式 cond 成立(值为 True)时,才会发挥它的作用;反之,断点并不会使程序停止执行。

类似上面这种,以某个表达式的是否成立作为条件,从而决定自身是否生效的断点,又称为条件断点。除了普通断点外,观察断点和捕捉断点也可以成为条件断点。

需要说明的是,创建普通条件断点的方式,也同样适用于观察条件断点。通过执行如下命令,即可直接生成一个观察条件断点:

shell
(gdb) watch expr if cond

参数 expr 表示要观察的变量或表达式;参数 cond 用于代指某个表达式。

但是,以上创建条件断点的方法,不适用于捕捉断点。换句话说,捕捉条件断点无法直接生成,需要借助 condition 命令为现有捕捉断点增添一个 cond 表达式,才能使其变成条件断点。

总的来说,借助 condition 命令,我们可以将现有的普通断点、观察断点以及捕捉断点变成条件断点;而普通条件断点和观察条件断点,可以分别通过 break if 命令和 watch if 命令直接生成。

二、condition

1. 语法格式

condition 命令的功能是:既可以为现有的普通断点、观察断点以及捕捉断点添加条件表达式,也可以对条件断点的条件表达式进行修改。condition 命令没有缩写形式,使用方法很简单,语法格式如下:

shell
(gdb) condition bnum expression
(gdb) condition bnum

参数 bnum 用于代指目标断点的编号;参数 expression 表示为断点添加或修改的条件表达式。以上 2 种语法格式中,第 1 种用于为 bnum 编号的断点添加或修改 expression 条件表达式;第 2 种用于删除 bnum 编号断点的条件表达式,使其变成普通的无条件断点。

2. 使用示例

2.1 测试程序

这里用 C++程序来测试,因为 C 语言本身没有异常处理机制,所以捕捉断点不好演示。

c
#include <iostream>
using namespace std;
int main(int argc, const char *argv[]) {
    int num = 1;
    while (num < 20) {
        try {
            throw num;
        }
        catch (int &e) {
            num++;
        }
    }
    cout << num << endl;
    return 0;
}
  • num 初始为 1。
  • 进入 while (num < 20) 循环,一共执行 19 次 循环体(num 为 1 ~ 19)。
  • 每次循环里:throw num 抛出一个 int 异常,然后被 catch(int &e) 捕获,执行 num++
  • 第 19 次抛出 num=19 后,num++ 变成 20,while (20 < 20) 条件为假,退出循环。
  • 最后 cout << num << endl; 输出 20,程序结束。

我们在 ubuntu 中测试,所以直接用下面的命令编译:

shell
cd ~/workspace/c-learning/02-c-basic/21-debug
g++ 030-gdb-condition.cpp -g

2.2 调试示例

shell
 sumu@virtual-machine:~/workspace/c-learning/02-c-basic/21-debug [main  +1 ~0 -0 !]
$ gdb a.out -q
Reading symbols from a.out...
(gdb) l       # <=== 1. 查看源码
1       #include <iostream>
2       using namespace std;
3       int main(int argc, const char *argv[]) {
4           int num = 1;
5           while (num < 20) {
6               try {
7                   throw num;
8               }
9               catch (int &e) {
10                  num++;
(gdb) 
11              }
12          }
13          cout << num << endl;
14          return 0;
15      }
(gdb) b 10   # <=== 2. 在第 10 行打上普通断点
Breakpoint 1 at 0x12d7: file 030-gdb-condition.cpp, line 10.
(gdb) r      # <=== 3. 运行程序到第 10 行
Starting program: /home/sumu/workspace/c-learning/02-c-basic/21-debug/a.out 

Breakpoint 1, main (argc=1, argv=0x7fffffffce28) at 030-gdb-condition.cpp:10
10                  num++;
(gdb) rwatch num      # <=== 4. 添加观察断点,num 的值被读取时,程序暂停
Hardware read watchpoint 2: num
(gdb) catch throw int # <=== 5. 添加捕捉断点,当每次抛出 int 异常时程序暂停,即在 throw num; 时触发
Catchpoint 3 (throw)
(gdb) info b          # <=== 查看当前所有断点
Num     Type            Disp Enb Address            What
1       breakpoint      keep y   0x00005555555552d7 in main(int, char const**) 
                                                    at 030-gdb-condition.cpp:10
        breakpoint already hit 1 time
2       read watchpoint keep y                      num
3       catchpoint      keep y                      exception throw
        matching: int
(gdb) condition 1 num==3 # <=== 6. 为普通断点添加条件表达式
(gdb) condition 2 num==5 # <=== 7. 为观察断点添加条件表达式
(gdb) condition 3 num==7 # <=== 8. 为捕捉断点添加条件表达式
(gdb) info b             # <=== 查看当前所有断点
Num     Type            Disp Enb Address            What
1       breakpoint      keep y   0x00005555555552d7 in main(int, char const**) 
                                                    at 030-gdb-condition.cpp:10
        stop only if num==3
        breakpoint already hit 1 time
2       read watchpoint keep y                      num
        stop only if num==5
3       catchpoint      keep y                      exception throw
        matching: int
        stop only if num==7
(gdb) c                  # < === 9. 继续运行程序,num== 3 肯定最先到来,所以普通断点先触发
Continuing.
                         # <=== 10. 普通条件断点触发,停在第 10 行
Breakpoint 1, main (argc=1, argv=0x7fffffffce28) at 030-gdb-condition.cpp:10
10                  num++;
(gdb) p num              # <=== 11. 查看 num 的值为 3
$1 = 3
(gdb) c                  # < === 12. 继续执行,num 会继续增加,下一个会触发断点的是 num== 5 的时候的观察断点
Continuing.

Hardware read watchpoint 2: num

Value = 5               # <=== 观察断点被触发,循环判断的时候会读取 num 的值,所以这里会暂停
0x0000555555555267 in main (argc=1, argv=0x7fffffffce28) at 030-gdb-condition.cpp:5
5           while (num < 20) {
(gdb) c                 # <=== 13. 继续执行,此时 num 值仍然为 5
Continuing.

Hardware read watchpoint 2: num

Value = 5               # <=== throw num 的时候也会读取 num 的值,所以这里再次触发观察条件断点
0x0000555555555276 in main (argc=1, argv=0x7fffffffce28) at 030-gdb-condition.cpp:7
7                   throw num;
(gdb) c                 # <=== 14. 继续执行,为什么没有在 num++ 的时候触发观察条件断点?
Continuing.

Catchpoint 3 (exception thrown), 0x00007ffff7e7a662 in __cxa_throw ()  # <=== 捕捉条件断点触发
   from /lib/x86_64-linux-gnu/libstdc++.so.6
(gdb) bt                # <=== 15. 查看栈帧情况
#0  0x00007ffff7e7a662 in __cxa_throw () from /lib/x86_64-linux-gnu/libstdc++.so.6
#1  0x000055555555528c in main (argc = 1, argv = 0x7fffffffce28) at 030-gdb-condition.cpp: 7
(gdb) up                # <=== 16. 移动到上一个栈帧,在这里就是回到了源码第 7 行
#1  0x000055555555528c in main (argc = 1, argv = 0x7fffffffce28) at 030-gdb-condition.cpp: 7
7                   throw num;
(gdb) p num             # <=== 17. 打印 num 的值为 7
$2 = 7
(gdb)

上面第 14 个步骤中有一个疑问,rwatch 是观察断点,只要发生读取,都会暂停程序,上面 num++是先读取 num 然后做++操作,为什么没有处罚观察条件段点?下面是 Deepseek 的回答:

txt
在 c 继续后,理论上 catch 块里的 num++ 确实会读取 num,而此时 num 的值还是 5,满足 rwatch 的条件 num==5。之所以没有在那里停下,根本原因在于 C++ 异常抛出与捕获的内部流程,会导致 GDB 在处理多类型断点(尤其是 catch 捕捉点)时,暂时“绕过”了本该触发的读观察点。

观察断点(read watchpoint)在以下情况下触发:当变量被 显式读取 时;在 表达式求值 过程中读取变量时。但是,在 catch 块内的 num++ 操作中,虽然会读取 num 的值,但这个读取操作可能不会触发观察断点,可能的原因:

(1)编译器优化:编译器可能会将 num++ 优化为单个指令,GDB 可能无法捕获到中间的读取操作

(2)断点触发时机:观察断点可能只在特定的执行点触发,比如在 while 条件判断和 throw 语句中

(3)异常处理上下文:在异常处理的特殊上下文中,GDB 的断点机制可能有不同的行为

我们可以在这个 catch 块中显式读取一下 num 看看:

shell
#include <iostream>
using namespace std;
int main(int argc, const char *argv[]) {
    int num = 1;
    while (num < 20) {
        try {
            throw num;
        }
        catch (int &e) {
        	cout << "Before increment: " << num << endl;
            num++;
        }
    }
    cout << num << endl;
    return 0;
}

然后再来按照相同的调试步骤走一遍:

shell
 sumu@virtual-machine:~/workspace/c-learning/02-c-basic/21-debug [main  +1 ~0 -0 !]
$ gdb a.out -q
Reading symbols from a.out...
(gdb) l
1       #include <iostream>
2       using namespace std;
3       int main(int argc, const char *argv[]) {
4           int num = 1;
5           while (num < 20) {
6               try {
7                   throw num;
8               }
9               catch (int &e) {
10                  cout << "Before increment: " << num << endl;
(gdb) 
11                  num++;
12              }
13          }
14          cout << num << endl;
15          return 0;
16      }
(gdb) b 11
Breakpoint 1 at 0x1330: file 031-gdb-condition2.cpp, line 11.
(gdb) r
Starting program: /home/sumu/workspace/c-learning/02-c-basic/21-debug/a.out 
Before increment: 1

Breakpoint 1, main (argc=1, argv=0x7fffffffce28) at 031-gdb-condition2.cpp:11
11                  num++;
(gdb) rwatch num
Hardware read watchpoint 2: num
(gdb) catch throw int
Catchpoint 3 (throw)
(gdb) info b
Num     Type            Disp Enb Address            What
1       breakpoint      keep y   0x0000555555555330 in main(int, char const**) 
                                                    at 031-gdb-condition2.cpp:11
        breakpoint already hit 1 time
2       read watchpoint keep y                      num
3       catchpoint      keep y                      exception throw
        matching: int
(gdb) condition 1 num==3
(gdb) condition 2 num==5
(gdb) condition 3 num==7
(gdb) c
Continuing.
Before increment: 2
Before increment: 3

Breakpoint 1, main (argc=1, argv=0x7fffffffce28) at 031-gdb-condition2.cpp:11
11                  num++;
(gdb) c
Continuing.
Before increment: 4

Hardware read watchpoint 2: num

Value = 5
0x0000555555555288 in main (argc=1, argv=0x7fffffffce28) at 031-gdb-condition2.cpp:5
5           while (num < 20) {
(gdb) c
Continuing.

Hardware read watchpoint 2: num

Value = 5
0x0000555555555297 in main (argc=1, argv=0x7fffffffce28) at 031-gdb-condition2.cpp:7
7                   throw num;
(gdb) c
Continuing.

Hardware read watchpoint 2: num

Value = 5
0x0000555555555311 in main (argc=1, argv=0x7fffffffce28) at 031-gdb-condition2.cpp:10
10                  cout << "Before increment: " << num << endl;
(gdb) c
Continuing.
Before increment: 5
Before increment: 6

Catchpoint 3 (exception thrown), 0x00007ffff7e7a662 in __cxa_throw ()
   from /lib/x86_64-linux-gnu/libstdc++.so.6
(gdb) bt
#0  0x00007ffff7e7a662 in __cxa_throw () from /lib/x86_64-linux-gnu/libstdc++.so.6
#1  0x00005555555552ad in main (argc = 1, argv = 0x7fffffffce28) at 031-gdb-condition2.cpp: 7
(gdb) up
#1  0x00005555555552ad in main (argc = 1, argv = 0x7fffffffce28) at 031-gdb-condition2.cpp: 7
7                   throw num;
(gdb) p num
$1 = 7
(gdb)

这个地方就会发现,显式读取的时候正常触发了。

三、ignore

1. 语法格式

ignore 命令也可以使一个断点成为条件断点,但这里的“条件”并非自定义的表达式,而仅为一个整数,它用来表示该断点失效的次数。也就会说,ignore 命令可以使目标断点暂时失去作用,当断点失效的次数超过指定次数时,断点的功能会自动恢复。ignore 命令也没有缩写形式,其语法格式如下:

shell
(gdb) ignore bnum count

参数 bnum 为某个断点的编号;参数 count 用于指定该断点失效的次数。

2. 使用示例

2.1 测试程序

c
#include <stdio.h>

int main(int argc, const char *argv[]) {
    int i = 1;
    printf("Hello, World! \n");

    while (i < 30) {
        i += 2;
    }
    printf("i = %d\n", i);

    return 0;
}

我们在ubuntu中测试,所以直接用下面的命令编译:

shell
cd ~/workspace/c-learning/02-c-basic/21-debug
gcc 032-gdb-ignore.c -g

2.2 调试示例

shell
 sumu@virtual-machine:~/workspace/c-learning/02-c-basic/21-debug [main  +1 ~0 -0 !]
$ gdb a.out -q
Reading symbols from a.out...
(gdb) l           # <=== 1. 查看源码
1       #include <stdio.h>
2
3       int main(int argc, const char *argv[]) {
4           int i = 1;
5           printf("Hello, World! \n");
6
7           while (i < 30) {
8               i += 2;
9           }
10          printf("i = %d\n", i);
(gdb) b 8         # <=== 2. 在第8行打断点
Breakpoint 1 at 0x1191: file 032-gdb-ignore.c, line 8.
(gdb) r           # <=== 3. 运行程序到第8行停下来
Starting program: /home/sumu/workspace/c-learning/02-c-basic/21-debug/a.out 
Hello, World! 

Breakpoint 1, main (argc=1, argv=0x7fffffffce28) at 032-gdb-ignore.c:8
8               i += 2;
(gdb) p i         # <=== 4. 查看i的值为1
$1 = 1
(gdb) ignore 1 3  # <=== 5. 忽略编号为1的断点3次,接下来三次不会在断点1这里停下来
Will ignore next 3 crossings of breakpoint 1.
(gdb) info b
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x0000555555555191 in main at 032-gdb-ignore.c:8
        breakpoint already hit 1 time
        ignore next 3 hits
(gdb) c           # <=== 6. 继续执行程序
Continuing.

Breakpoint 1, main (argc=1, argv=0x7fffffffce28) at 032-gdb-ignore.c:8
8               i += 2;
(gdb) p i        # <=== 7. 下一次停下来的时候查看i的值为9,上一次停下来i的值为1,后面每次加2,等于3、5、7的未暂停
$2 = 9
(gdb)

执行 ignore 命令之前,num 变量的值为 1。借助 ignore 命令使编号为 1(作用于第 9 行)的断点失效 3 次后,继续执行程序,最终程序仍暂停至第 8 行,此时 num 的值变为 9。这这恰恰证明了 num 从 1 递增至 9 的过程中,编号为 1 的断点失效了 3 次。