linux进阶17——GDB(三):观察断点和捕捉断点

GDB 调试器支持在程序中打 3 种断点,分别为普通断点(break)、观察断点(watch)和捕捉断点(catch)。普通断点(break)前文已经介绍,本文主要介绍观察断点和捕捉断点。

1. watch 

1.1 语法

(gdb) watch cond

其中,cond 指的就是要监控的变量或表达式。

和 watch 命令功能相似的,还有 rwatch 和 awatch 命令。其中:

  • rwatch 命令:只要程序中出现读取目标变量(表达式)的值的操作,程序就会停止运行;
  • awatch 命令:只要程序中出现读取目标变量(表达式)的值或者改变值的操作,程序就会停止运行。

1.2 功能

只有当被监控变量(表达式)的值发生改变,程序才会停止运行。

1.3 实例

[root@localhost day3]# ls
test1  test1.c
[root@localhost day3]# gdb test1 -silent
Reading symbols from /home/gdb/day3/test1...done.
(gdb) l 0
1	#include <stdio.h>
2	
3	int main(int argc,char* argv[])
4	{
5		int num = 1;
6		while(num<=50)
7		{
8			num *= 2;
9	     	}
10	     	printf("%d",num);
(gdb) 
11	     	return 0;
12	}
(gdb) b 5
Breakpoint 1 at 0x400511: file test1.c, line 5.
(gdb) r
Starting program: /home/gdb/day3/test1 

Breakpoint 1, main (argc=1, argv=0x7fffffffe578) at test1.c:5
5		int num = 1;
Missing separate debuginfos, use: debuginfo-install glibc-2.17-323.el7_9.x86_64
(gdb) watch num     <---监控程序中num的值
Hardware watchpoint 2: num
(gdb) c
Continuing.
Hardware watchpoint 2: num

Old value = 0
New value = 2
main (argc=1, argv=0x7fffffffe578) at test1.c:6
6		while(num<=50)
(gdb) c
Continuing.
Hardware watchpoint 2: num

Old value = 2
New value = 4
main (argc=1, argv=0x7fffffffe578) at test1.c:6
6		while(num<=50)
(gdb) c
Continuing.
Hardware watchpoint 2: num

Old value = 4
New value = 8
main (argc=1, argv=0x7fffffffe578) at test1.c:6
6		while(num<=50)
(gdb) c
Continuing.
Hardware watchpoint 2: num

Old value = 8
New value = 16
main (argc=1, argv=0x7fffffffe578) at test1.c:6
6		while(num<=50)
(gdb) c
Continuing.
Hardware watchpoint 2: num

Old value = 16
New value = 32
main (argc=1, argv=0x7fffffffe578) at test1.c:6
6		while(num<=50)
(gdb) c
Continuing.
Hardware watchpoint 2: num

Old value = 32
New value = 64
main (argc=1, argv=0x7fffffffe578) at test1.c:6
6		while(num<=50)
(gdb) c
Continuing.

Watchpoint 2 deleted because the program has left the block in
which its expression is valid.
0x00007ffff7a2f555 in __libc_start_main () from /lib64/libc.so.6
(gdb) c
Continuing.
64[Inferior 1 (process 1817) exited normally]
(gdb) c
The program is not being run.
(gdb) 

注意:

  • 当监控的变量(表达式)为局部变量(表达式)时,一旦局部变量(表达式)失效,则监控操作也随即失效;
  • 如果监控的是一个指针变量(例如 *p),则 watch *p 和 watch p 是有区别的,前者监控的是 p 所指数据的变化情况,而后者监控的是 p 指针本身有没有改变指向;
  • 这 3 个监控命令还可以用于监控数组中元素值的变化情况,例如对于 a[10] 这个数组,watch a 表示只要 a 数组中存储的数据发生改变,程序就会停止执行。

1.4 原理

watch 命令实现监控机制的方式有 2 种,一种是为目标变量(表达式)设置硬件观察点,另一种是为目标变量(表达式)设置软件观察点。

1.4.1 软件观察点(software watchpoint)

用 watch 命令监控目标变量(表达式)后,GDB 调试器会以单步执行的方式运行程序,并且每行代码执行完毕后,都会检测该目标变量(表达式)的值是否发生改变,如果改变则程序执行停止。

可想而知,这种“实时”的判别方式,一定程度上会影响程序的执行效率。但从另一个角度看,调试程序的目的并非是为了获得运行结果,而是查找导致程序异常或 Bug 的代码,因此即便软件观察点会影响执行效率,一定程度上也是可以接受的。

1.4.2 硬件观察点(Hardware watchpoint)

和前者最大的不同是,它在实现监控机制的同时不影响程序的执行效率。简单的理解,系统会为 GDB 调试器提供少量的寄存器(例如 32 位的 Intel x86 处理器提供有 4 个调试寄存器),每个寄存器都可以作为一个观察点协助 GDB 完成监控任务。

需要注意的是,基于寄存器个数的限制,如果调试环境中设立的硬件观察点太多,则有些可能会失去作用,这种情况下,GDB 调试器会发出如下警告:

Hardware watchpoint num: Could not insert watchpoint

解决方案也很简单,就是删除或者禁用一部分硬件观察点。

除此之外,受到寄存器数量的限制,可能会出现:无法使用硬件观察点监控数据类型占用字节数较多的变量(表达式)。比如说,某些操作系统中,GDB 调试器最多只能监控 4 个字节长度的数据,这意味着 C、C++ 中 double 类型的数据是无法使用硬件观察点监测的。这种情况下,可以考虑将其换成占用字符串少的 float 类型。

目前,大多数 PowerPC 或者基于 x86 的操作系统,都支持采用硬件观点。并且 GDB 调试器在建立观察断点时,会优先尝试建立硬件观察点,只有当前环境不支持硬件观察点时,才会建立软件观察点。借助如下指令,即可强制 GDB 调试器只建立软件观察点:

set can-use-hw-watchpoints 0

注意,awatch 和 rwatch 命令只能设置硬件观察点,如果系统不支持或者借助如上命令禁用,则 GDB 调试器会打印如下信息:

Expression cannot be implemented with read/access watchpoint.

2. catch

普通断点:作用于程序中的某一行,当程序运行至此行时停止执行;

观察断点:作用于某一变量或表达式,当该变量(表达式)的值发生改变时,程序暂停;

捕捉断点:监控程序中某一事件的发生,例如程序发生某种异常时、某一动态库被加载时等等,一旦目标时间发生,则程序停止执行。

2.1 语法

(gdb) catch event

其中,event 参数表示要监控的具体事件。常用的 event 事件类型如表 1 所示。

说明:

  • 对于使用 catch 监控指定的 event 事件,其匹配过程需要借助 libstdc++ 库中的一些 SDT 探针,而这些探针最早出现在 GCC 4.8 版本中。也就是说,想使用 catch 监控指定类型的 event 事件,系统中 GCC 编译器的版本最低为 4.8,但即便如此,catch 命令是否能正常发挥作用,还可能受到系统中其它因素的影响。
  • 当 catch 命令捕获到指定的 event 事件时,程序暂停执行的位置往往位于某个系统库(例如 libstdc++)中。这种情况下,通过执行 up 命令,即可返回发生 event 事件的源代码处。
  • catch 无法捕获以交互方式引发的异常。

catch 命令也有另一个版本,即 tcatch 命令。tcatch 命令和 catch 命令的用法完全相同,唯一不同之处在于,对于目标事件,catch 命令的监控是永久的,而 tcatch 命令只监控一次,也就是说,只有目标时间第一次触发时,tcath 命令才会捕获并使程序暂停,之后将失效。

2.2 功能

用捕捉断点监控某一事件的发生,等同于在程序中该事件发生的位置打普通断点。

2.3 实例

一段程序test2.cpp:

#include <iostream>
using namespace std;

int main(){
    int num = 1;
    while(num <= 5){
        try{
            throw 100;
        }catch(int e){
            num++;
            cout << "next" << endl;
        }
    }
    cout << "over" << endl;
    return 0;
}

对该程序进行编译调试:

[root@localhost day3]# g++ test2.cpp -o test2 -g
[root@localhost day3]# gdb test2 -silent
Reading symbols from /home/gdb/day3/test2...done.
(gdb) 

通过观察程序可以看出,当前程序中通过 throw 手动抛出了 int 异常,此异常能够被 catch 成功捕获。假设我们使用 catch 命令监控:只要程序中引发 int 异常,程序就停止执行:

[root@localhost day3]# gdb test2 -silent
Reading symbols from /home/gdb/day3/test2...done.
(gdb) catch throw     <------指定捕获throw事件
Catchpoint 1 (throw)
(gdb) r
Starting program: /home/gdb/day3/test2 
Catchpoint 1 (exception thrown), __cxxabiv1::__cxa_throw (obj=0x613ca0, tinfo=0x6010a0 <_ZTIi@@CXXABI_1.3>, dest=0x0) at ../../.././libstdc++-v3/libsupc++/eh_throw.cc:80
80	  __cxa_eh_globals *globals = __cxa_get_globals ();
Missing separate debuginfos, use: debuginfo-install glibc-2.17-323.el7_9.x86_64 libgcc-4.8.5-44.el7.x86_64    <------程序暂停执行
(gdb) up           <------回到源码
#1  0x0000000000400a6a in main () at test2.cpp:8
8	            throw 100;
(gdb) c   <-------程序继续执行
Continuing.
next
Catchpoint 1 (exception thrown), __cxxabiv1::__cxa_throw (obj=0x613ca0, tinfo=0x6010a0 <_ZTIi@@CXXABI_1.3>, dest=0x0) at ../../.././libstdc++-v3/libsupc++/eh_throw.cc:80
80	  __cxa_eh_globals *globals = __cxa_get_globals ();
(gdb) up
#1  0x0000000000400a6a in main () at test2.cpp:8
8	            throw 100;
(gdb) c
Continuing.
next
Catchpoint 1 (exception thrown), __cxxabiv1::__cxa_throw (obj=0x613ca0, tinfo=0x6010a0 <_ZTIi@@CXXABI_1.3>, dest=0x0) at ../../.././libstdc++-v3/libsupc++/eh_throw.cc:80
80	  __cxa_eh_globals *globals = __cxa_get_globals ();
(gdb) 

如上所示,借助 catch 命令设置了一个捕获断点,该断点用于监控 throw int 事件,只要发生程序就会暂停执行。由此当程序执行时,其会暂停至 libstdc++ 库中的某个位置,借助 up 指令我们可以得知该异常发生在源代码文件中的位置。

同理,我们也可以监控 main.cpp 程序中发生的  catch event 事件:

[root@localhost day3]# gdb test2 -silent
Reading symbols from /home/gdb/day3/test2...done.
(gdb) catch catch 
Catchpoint 1 (catch)
(gdb) r
Starting program: /home/gdb/day3/test2 
Catchpoint 1 (exception caught), __cxxabiv1::__cxa_begin_catch (exc_obj_in=0x613c80) at ../../.././libstdc++-v3/libsupc++/eh_catch.cc:42
42	  _Unwind_Exception *exceptionObject
Missing separate debuginfos, use: debuginfo-install glibc-2.17-323.el7_9.x86_64 libgcc-4.8.5-44.el7.x86_64
(gdb) up
#1  0x0000000000400aa3 in main () at test2.cpp:9
9	        }catch(int e){
(gdb) c
Continuing.
next
Catchpoint 1 (exception caught), __cxxabiv1::__cxa_begin_catch (exc_obj_in=0x613c80) at ../../.././libstdc++-v3/libsupc++/eh_catch.cc:42
42	  _Unwind_Exception *exceptionObject
(gdb) up
#1  0x0000000000400aa3 in main () at test2.cpp:9
9	        }catch(int e){
(gdb) 

甚至于,在个别场景中,还可以使用 catch 命令监控 C、C++ 程序动态库的加载和卸载。就以 main.exe 为例,其运行所需加载的动态库可以使用 ldd 命令查看,例如:

[root@localhost day3]# ldd test2
	linux-vdso.so.1 =>  (0x00007ffebf7f6000)
	libstdc++.so.6 => /lib/libstdc++.so.6 (0x00007faf1512f000)
	libm.so.6 => /lib64/libm.so.6 (0x00007faf14e2d000)
	libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007faf14c17000)
	libc.so.6 => /lib64/libc.so.6 (0x00007faf14849000)
	/lib64/ld-linux-x86-64.so.2 (0x00007faf154fc000)
[root@localhost day3]# 

就以监控 libstdc++.so.6 为例,在 GDB 调试器中,通过执行如下指令,即可监控该动态库的加载:

(gdb) catch load libstdc++.so.6
Catchpoint 1 (load)
(gdb) r
Starting program: ~/demo/main.exe

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
0x00007ffff7fd37a5 in ?? () from /lib64/ld-linux-x86-64.so.2

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值