天天看點

linux進階17——GDB(三):觀察斷點和捕捉斷點1. watch 2. catch

GDB 調試器支援在程式中打 3 種斷點,分别為普通斷點(break)、觀察斷點(watch)和捕捉斷點(catch)。普通斷點(break)前文已經介紹,本文主要介紹觀察斷點和捕捉斷點。

1. watch 

1.1 文法

(gdb) watch cond
           

其中,cond 指的就是要監控的變量或表達式。

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

  • rwatch 指令:隻要程式中出現讀取目标變量(表達式)的值的操作,程式就會停止運作;
  • awatch 指令:隻要程式中出現讀取目标變量(表達式)的值或者改變值的操作,程式就會停止運作。

1.2 功能

隻有當被監控變量(表達式)的值發生改變,程式才會停止運作。

1.3 執行個體

[[email protected] day3]# ls
test1  test1.c
[[email protected] 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 所示。

linux進階17——GDB(三):觀察斷點和捕捉斷點1. watch 2. catch
說明:
  • 對于使用 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;
}
           

對該程式進行編譯調試:

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

通過觀察程式可以看出,目前程式中通過 throw 手動抛出了 int 異常,此異常能夠被 catch 成功捕獲。假設我們使用 catch 指令監控:隻要程式中引發 int 異常,程式就停止執行:

[[email protected] 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 事件:

[[email protected] 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 指令檢視,例如:

[[email protected] 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)
[[email protected] 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
           

繼續閱讀