本文在 “Linux基础知识(14)- GDB 调试器(二)| 普通断点、单步调试和查看变量” 的基础上,继续演示实时监控变量值、捕捉断点和条件断点。
1. 实时监控变量值
使用 GDB 调试程序的过程中,借助观察断点可以监控程序中某个变量或者表达式的值,只要发生改变,程序就会停止执行。相比普通断点,观察断点不需要我们预测变量(表达式)值发生改变的具体位置。
所谓表达式,就是包含多个变量的式子,比如 a+b 就是一个表达式,其中 a、b 为变量。
对于监控 C、C++ 程序中某变量或表达式的值是否发生改变,watch 命令的语法非常简单,如下所示:
(gdb) watch num
其中,num 指的就是要监控的变量或表达式。
通过借助 watch 命令监控 num 的值,后续只要 num 的值发生改变,程序都会停止。watch 命令的功能是:只有当被监控变量(表达式)的值发生改变,程序才会停止运行。
和 watch 命令功能相似的,还有 rwatch 和 awatch 命令。其中:
rwatch 命令:只要程序中出现读取目标变量(表达式)的值的操作,程序就会停止运行;
awatch 命令:只要程序中出现读取目标变量(表达式)的值或者改变值的操作,程序就会停止运行。
如下指令查看当前建立的观察点的数量:
(gdb) info watchpoints
1) 创建 C 程序
$ cd ~/
$ vim test3.c
#include <stdio.h>
int main(int argc, char* argv[]) {
int num = 1;
while (num<=100) {
num *= 2;
}
printf("%d\n", num);
return 0;
}
2)编译并设置调试信息
$ gcc -g test3.c -o test3
$ ./test3
128
3) 使用 watch 命令
# GDB 启动 test3 程序
$ gdb -q test3
Reading symbols from test3 ...
(gdb) l
1 #include <stdio.h>
2
3 int main(int argc, char* argv[]) {
4
5 int num = 1;
6 while (num<=100) {
7 num *= 2;
8 }
9 printf("%d\n", num);
10 return 0;
(gdb)
11 }
12
(gdb) b 5
Breakpoint 1 at 0x115c: file test3.c, line 5.
(gdb) r
Starting program: /home/xxx/test3
Breakpoint 1, main (argc=1, argv=0x7fffffffe228) at test3.c:5
5 int num = 1;
(gdb) watch num
Hardware watchpoint 2: num
(gdb) c
Continuing.
Hardware watchpoint 2: num
Old value = 0
New value = 1
main (argc=1, argv=0x7fffffffe228) at test3.c:6
6 while (num<=100) {
(gdb) c
Continuing.
Hardware watchpoint 2: num
Old value = 1
New value = 2
main (argc=1, argv=0x7fffffffe228) at test3.c:6
6 while (num<=100) {
2. 建立捕捉断点
GDB 调试器支持在被调试程序中打 3 种断点,分别为普通断点、观察断点和捕捉断点,其中普通断点用 break 命令建立,观察断点用 watch 命令建立,本节将讲解如何使用 catch 命令建立捕捉断点。
普通断点作用于程序中的某一行,当程序运行至此行时停止执行,观察断点作用于某一变量或表达式,当该变量(表达式)的值发生改变时,程序暂停。而捕捉断点的作用是,监控程序中某一事件的发生,例如程序发生某种异常时、某一动态库被加载时等等,一旦目标时间发生,则程序停止执行。
用捕捉断点监控某一事件的发生,等同于在程序中该事件发生的位置打普通断点。
建立捕捉断点的方式很简单,就是使用 catch 命令,其基本格式为:
(gdb) catch event
其中,event 参数表示要监控的具体事件。对于使用 GDB 调试 C、C++ 程序,常用的 event 事件类型如下表所示。
event 事件 | 描述 |
throw [exception] | 当程序中抛出 exception 指定类型异常时,程序停止执行。如果不指定异常类型(即省略 exception),则表示只要程序发生异常,程序就停止执行。 |
catch [exception] | 当程序中捕获到 exception 异常时,程序停止执行。exception 参数也可以省略,表示无论程序中捕获到哪种异常,程序都暂停执行。 |
load/unload [regexp] | 其中,regexp 表示目标动态库的名称,load 命令表示当 regexp 动态库加载时程序停止执行;unload 命令表示当 regexp 动态库被卸载时,程序暂停执行。regexp 参数也可以省略,此时只要程序中某一动态库被加载或卸载,程序就会暂停执行。 |
注意,当前 GDB 调试器对监控 C++ 程序中异常的支持还有待完善,使用 catch 命令时,有以下几点需要说明:
(1)对于使用 catch 监控指定的 event 事件,其匹配过程需要借助 libstdc++ 库中的一些 SDT 探针,而这些探针最早出现在 GCC 4.8 版本中。也就是说,想使用 catch 监控指定类型的 event 事件,系统中 GCC 编译器的版本最低为 4.8,但即便如此,catch 命令是否能正常发挥作用,还可能受到系统中其它因素的影响。
(2)当 catch 命令捕获到指定的 event 事件时,程序暂停执行的位置往往位于某个系统库(例如 libstdc++)中。这种情况下,通过执行 up 命令,即可返回发生 event 事件的源代码处。
(3)catch 无法捕获以交互方式引发的异常。
catch 命令也有另一个版本,即 tcatch 命令。tcatch 命令和 catch 命令的用法完全相同,唯一不同之处在于,对于目标事件,catch 命令的监控是永久的,而 tcatch 命令只监控一次,也就是说,只有目标时间第一次触发时,tcath 命令才会捕获并使程序暂停,之后将失效。
1) 创建 C++ 程序
$ cd ~/
$ vim test4.cpp
#include <iostream>
using namespace std;
int main() {
int num = 1;
while(num <= 5) {
try {
throw num;
} catch (int e) {
cout << num << endl;
num++;
}
}
cout << "finish" << endl;
return 0;
}
2)编译并设置调试信息
$ g++ -g test4.cpp -o test4
$ ./test4
1
2
3
4
5
finish
3) 处理 throw 事件
# GDB 启动 test4 程序
$ gdb -q test4
Reading symbols from test4 ...
(gdb) catch throw int
Catchpoint 1 (throw)
(gdb) r
Starting program: /home/xxx/test4
Catchpoint 1 (exception thrown), 0x00007ffff7e7e672 in __cxa_throw ()
from /lib/x86_64-linux-gnu/libstdc++.so.6 # 程序暂停执行
(gdb) up # 运行 up 命令回到源码点
#1 0x00005555555552a6 in main () at test4.cpp:9
9 throw num;
(gdb) c # 继续执行程序
Continuing.
1
Catchpoint 1 (exception thrown), 0x00007ffff7e7e672 in __cxa_throw ()
from /lib/x86_64-linux-gnu/libstdc++.so.6
...
(gdb) c # 继续执行程序
Continuing.
5
finish
[Inferior 1 (process 25566) exited normally]
4) 处理 catch 事件
# GDB 启动 test4 程序
$ gdb -q test4
Reading symbols from test4 ...
(gdb) catch catch int
Catchpoint 1 (catch)
(gdb) r
Starting program: /home/xxx/test4
Catchpoint 1 (exception caught), 0x00007ffff7e7d3e3 in __cxa_begin_catch ()
from /lib/x86_64-linux-gnu/libstdc++.so.6 # 程序暂停执行
(gdb) up # 运行 up 命令回到源码点
#1 0x00005555555552ef in main () at test4.cpp:10
10 } catch (int e) {
(gdb) c
Continuing.
1
Catchpoint 1 (exception caught), 0x00007ffff7e7d3e3 in __cxa_begin_catch ()
from /lib/x86_64-linux-gnu/libstdc++.so.6
...
(gdb) c
Continuing.
5
finish
[Inferior 1 (process 25658) exited normally]
5) 处理 load 事件
在个别场景中,还可以使用 catch 命令监控 C、C++ 程序动态库的加载和卸载。就以 test4 为例,其运行所需加载的动态库可以使用 ldd 命令查看,例如:
$ ldd test4
linux-vdso.so.1 (0x00007fff5d757000)
libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f1176ef9000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f1176ede000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f1176cec000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f1176b9d000)
/lib64/ld-linux-x86-64.so.2 (0x00007f11770f3000)
就以监控 libstdc++.so.6 为例。
# GDB 启动 test4 程序
$ gdb -q test4
Reading symbols from test4 ...
(gdb) catch load libstdc++.so.6
Catchpoint 1 (load)
(gdb) r
Starting program: /home/xxx/test4
Catchpoint 1
Inferior loaded /lib/x86_64-linux-gnu/libstdc++.so.6
/lib/x86_64-linux-gnu/libgcc_s.so.1
/lib/x86_64-linux-gnu/libc.so.6
/lib/x86_64-linux-gnu/libm.so.6
dl_main (phdr=<optimized out>, phnum=<optimized out>, user_entry=<optimized out>,
auxv=<optimized out>) at rtld.c:2358
2358 rtld.c: No such file or directory.
(gdb) up
#1 0x00007ffff7febc4b in _dl_sysdep_start (start_argptr=start_argptr@entry=0x7fffffffe220,
dl_main=dl_main@entry=0x7ffff7fd15e0 <dl_main>) at ../elf/dl-sysdep.c:252
252 ../elf/dl-sysdep.c: No such file or directory.
(gdb) c
Continuing.
1
2
3
4
5
finish
[Inferior 1 (process 25678) exited normally]
3. 条件断点
对于普通断点的建立,可以使用如下格式的 break 命令:
(gdb) break ... if cond
参数 ... 用于指定生成断点的具体位置;cond 参数用于代指某个表达式。通过此方式建立的普通断点,只有当表达式 cond 成立(值为 True)时,才会发挥它的作用;反之,断点并不会使程序停止执行。
类似上面这种,以某个表达式的是否成立作为条件,从而决定自身是否生效的断点,又称为条件断点。除了普通断点外,观察断点和捕捉断点也可以成为条件断点。
通过执行如下命令,即可直接生成一个观察条件断点:
(gdb) watch expr if cond
参数 expr 表示要观察的变量或表达式;参数 cond 用于代指某个表达式。
但是,以上创建条件断点的方法,不适用于捕捉断点。换句话说,捕捉条件断点无法直接生成,需要借助 condition 命令为现有捕捉断点增添一个 cond 表达式,才能使其变成条件断点。
condition 命令既可以为现有的普通断点、观察断点以及捕捉断点添加条件表达式,也可以对条件断点的条件表达式进行修改。语法格式如下:
(gdb) condition bnum expression
(gdb) condition bnum
参数 bnum 用于代指目标断点的编号;参数 expression 表示为断点添加或修改的条件表达式。
以上 2 种语法格式中,第 1 种用于为 bnum 编号的断点添加或修改 expression 条件表达式;第 2 种用于删除 bnum 编号断点的条件表达式,使其变成普通的无条件断点。
1) 创建 C++ 程序
$ cd ~/
$ vim test5.cpp
#include <iostream>
using namespace std;
int main() {
int num = 1;
while (num<10) {
try {
throw num;
} catch (int &e) {
num++;
}
}
cout << num << endl;
return 0;
}
2)编译并设置调试信息
$ g++ -g test5.cpp -o test5
$ ./test5
10
3)使用 condition 命令
# GDB 启动 test5 程序
$ gdb -q test5
Reading symbols from test5 ...
(gdb) l
1 #include <iostream>
2 using namespace std;
3
4 int main() {
5 int num = 1;
6 while (num<10) {
7 try {
8 throw num;
9 } catch (int &e) {
10 num++;
(gdb)
11 }
12 }
13
14 cout << num << endl;
15 return 0;
16 }
(gdb) b 10 # 添加普通断点
Breakpoint 1 at 0x12d0: file test5.cpp, line 10.
(gdb) r
Starting program: /home/xxx/test5
1
Breakpoint 1, main () at test5.cpp:10
10 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 0x00005555555552d0 in main() at test5.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 # 为普通断点添加条件表达式
(gdb) condition 2 num==5 # 为观察断点添加条件表达式
(gdb) condition 3 num==7 # 为捕捉断点添加条件表达式
(gdb) c
Continuing.
Breakpoint 1, main () at test5.cpp:10 # 普通条件断点触发
10 num++;
(gdb) p num
$1 = 3
(gdb) c
Continuing.
Hardware read watchpoint 2: num # 观察条件断点触发
Value = 5
0x0000555555555260 in main () at test5.cpp:6
6 while (num<10) {
(gdb) p num
$2 = 5
...
(gdb) c
Continuing.
Catchpoint 3 (exception thrown), 0x00007ffff7e7e672 in __cxa_throw ()
from /lib/x86_64-linux-gnu/libstdc++.so.6 # 捕捉条件断点触发
(gdb) up
#1 0x0000555555555285 in main () at test5.cpp:8
8 throw num;
(gdb) p num
$5 = 7