天天看點

GDB 進階調試-多線程、背景調試、多程序、反向調試調試多線程背景執行調試指令多程序調試反向調試handle指令:信号處理檢視棧資訊

目錄

  • 調試多線程
    • GDB調試多線程程式常用指令
      • 檢視所有線程
    • 調整目前線程
    • 為特定線程設定斷點
    • 設定線程鎖
    • GDB non-stop 模式
  • 背景執行調試指令
    • 暫停背景線程執行
  • 多程序調試
    • GDB多程序調試常用指令
    • GDB attach指令調試程序
    • 顯式指定要調試的程序
    • detach-on-fork選項
  • 反向調試
    • GDB反向調試的常用指令
  • handle指令:信号處理
  • 檢視棧資訊
    • frame指令
      • 標明要檢視的棧幀
      • 檢視目前棧幀中存儲的資訊
    • backtrace指令

調試多線程

GDB調試多線程程式常用指令

調試指令 功 能
info threads 檢視目前調試環境中包含多少個線程,并列印出各個線程的相關資訊,包括線程編号(ID)、線程名稱等。
thread id 将線程編号為 id 的線程設定為目前線程。
thread apply id… command id… 表示線程的編号;command 代指 GDB 指令,如 next、continue 等。整個指令的功能是将 command 指令作用于指定編号的線程。當然,如果想将 command 指令作用于所有線程,id… 可以用 all 代替。
break location thread id 在 location 指定的位置建立普通斷點,并且該斷點僅用于暫停編号為 id 的線程。
set scheduler-locking off|on|step 預設情況下,當程式中某一線程暫停執行時,所有執行的線程都會暫停;同樣,當執行 continue 指令時,預設所有暫停的程式都會繼續執行。該指令可以打破此預設設定,即隻繼續執行目前線程,其它線程仍停止執行。

調試的代碼:

#include <stdio.h>
#include <pthread.h>
void* thread_job(void*name)
{
    char * thread_name = (char*)name;
    printf("this is %s\n",thread_name);
    sleep(2);
    printf("http://c.biancheng.net\n");
}
int main()
{
    pthread_t tid1,tid2;
    pthread_create(&tid1, NULL, thread_job, "thread1_job");
    pthread_create(&tid2, NULL, thread_job, "thread2_job");
    pthread_join(tid1,NULL);
    pthread_join(tid2,NULL);
    printf("this is main\n");
    return 0;
}
           

編譯指令:

gcc main.c -o main -g -lpthread
           

檢視所有線程

info threads 指令的功能有 2 個,既可以檢視目前調試環境下存在的線程數以及各線程的具體資訊,也可以通過指定線程的編号檢視某個線程的具體資訊。
(gdb) info threads [id...]
           

其中,參數 id… 作為可選參數,表示要檢視的線程編号,編号個數可以是多個。

(gdb) b 6
Breakpoint 5 at 0x400699: file main.c, line 6.
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /root/learn/c++11/main 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
[New Thread 0x7ffff77f0700 (LWP 16854)]			 <--新線程
[Switching to Thread 0x7ffff77f0700 (LWP 16854)]   <--該線程作為目前線程,因為它最先碰到斷點

Breakpoint 5, thread_job (name=0x4007f3) at main.c:7
7           char * thread_name = (char*)name;
(gdb) 
(gdb) info threads;
[New Thread 0x7ffff6fef700 (LWP 16855)]
Args must be numbers or '$' variables.
(gdb) info threads
  Id   Target Id         Frame 
  3    Thread 0x7ffff6fef700 (LWP 16855) "main" 0x00007ffff78ef9c1 in clone () from /lib64/libc.so.6
* 2    Thread 0x7ffff77f0700 (LWP 16854) "main" thread_job (name=0x4007f3) at main.c:7		<--Id 列前标有 * 号的線程即為目前線程
  1    Thread 0x7ffff7fe6740 (LWP 16853) "main" 0x00007ffff78ef9c1 in clone () from /lib64/libc.so.6
           

其中 Id 清單示各個線程的編号(ID 号);Target Id 清單示各個線程的辨別符;Frame 列列印各個線程執行的有關資訊,例如線程名稱,線程暫停的具體位置等。

我們輸入的調試指令并不僅僅作用于目前線程,例如 continue、next 等,預設情況下它們作用于所有線程。

調整目前線程

将編号為 id 的線程設定為目前線程

(gdb) thead id 
           

為特定線程設定斷點

(gdb) break location thread id
(gdb) break location thread id if...
           

location 表示設定斷點的具體位置;id 表示斷點要作用的線程的編号;if… 參數作用指定斷點激活的條件,即隻有條件符合時,斷點才會發揮作用。

設定線程鎖

使用 GDB 調試多線程程式時,預設的調試模式為:一個線程暫停運作,其它線程也随即暫停;一個線程啟動運作,其它線程也随即啟動。要知道,這種調試機制确實能幫我們更好地監控各個線程的“一舉一動”,但并非适用于所有場景

一些場景中,我們可能隻想讓某一特定線程運作,其它線程仍維持暫停狀态。要想達到這樣的效果,就需要借助 set scheduler-locking 指令。 此指令可以幫我們将其它線程都“鎖起來”,使後續執行的指令隻對目前線程或者指定線程有效,而對其它線程無效

(gdb) set scheduler-locking mode
           

參數 mode 的值有 3 個,分别為 off、on 和 step,它們的含義分别是:

  • off:不鎖定線程,任何線程都可以随時執行;
  • on:鎖定線程,隻有目前線程或指定線程可以運作;
  • step:當單步執行某一線程時,其它線程不會執行,同時保證在調試過程中目前線程不會發生改變。但如果該模式下執行 continue、until、finish 指令,則其它線程也會執行,并且如果某一線程執行過程遇到斷點,則 GDB 調試器會将該線程作為目前線程。

檢視各個線程鎖定的狀态

(gdb) show scheduler-locking
Mode for locking scheduler during execution is "on".
           

GDB non-stop 模式

對于調試多線程程式,GDB 預設采用的是 all-stop 模式,即隻要有一個線程暫停執行,所有線程都随即暫停。這種調試模式可以适用于大部分場景的需要,借助适當數量的斷點,我們可以清楚地監控到各個線程的具體執行過程。

在某些場景中,我們可能需要調試個别的線程,并且不想在調試過程中,影響其它線程的運作。這種情況下,可以将 GDB 的調試模式由 all-stop 模式更改為 non-stop 模式,該模式下調試多線程程式,當某一線程暫停運作時,其它線程仍可以繼續執行

隻有 7.0 版本以上的 GDB 調試器,才支援 non-stop 模式。

  1. non-stop 模式下可以進行 all-stop 模式無法做到的調試工作,例如:
  • 保持其它線程繼續執行的狀态下,單獨調試某個線程;
  • 在所有線程都暫停執行的狀态下,單步調試某個線程;
  • 單獨執行多個線程等等。

all-stop 模式下,continue、next、step 指令的作用對象并不是目前線程,而是所有的線程;但在 non-stop 模式下,continue、next、step 指令隻作用于目前線程。

在 non-stop 模式下,如果想要 continue 指令作用于所有線程,可以為 continue 指令添加一個 -a 選項,即執行 continue -a 或者 c -a 指令,即可實作令所有線程繼續執行的目的。

由 all-stop 模式轉換到 non-stop 模式:未啟動程式前執行如下指令即可

(gdb) set non-stop mode
           

其中,mode 參數的值有 2 種,分别是 on 和 off,on 表示啟用 non-stop 模式;off 表示禁用 non-stop 模式。

show non-stop 指令,可以檢視 non-stop 模式是否開啟

背景執行調試指令

對于某些調試指令,GDB 調試器提供有 2 種執行方式:

  • 同步執行:“一個一個”的執行,即必須等待前一個指令執行完畢,才能執行下一個調試指令。
  • 背景執行:又稱“異步執行”,即當某個調試指令開始執行時,(gdb) 指令提示符會立即出現,我們無需等待前一個指令執行完畢就可以繼續執行下一個調試指令。

    以背景(異步)的方式執行一個調試指令,其文法格式如下:

(gdb) command&
           

其中,command 表示就是要執行的調試指令。command 和 & 之間不需要添加空格。

GDB支援背景執行的調試指令:

調試指令 含 義
run(r) 啟動被調試的程式
attach 調試處于運作着的的程式。
step 單步調試程式
stepi 執行一條機器指令。
next(n) 單步調試程式
nexti 執行一條機器指令,其與 stepi 指令的差別,類似于 step 和 next 指令的差別。
continue 繼續執行程式。
finish 結束目前正在執行的函數
until(u) 快速執行完目前的循環體
背景執行指令異步調試程式的方法,多用于 non-stop 模式中。雖然 all-stop 模式中也可以使用,但在前一個異步指令未執行完畢前,仍舊不能執行其它指令。

暫停背景線程執行

在背景處于執行狀态的線程,可以使用 interrupt 指令将其中斷

注意,在 all-stop 模式下,interrupt 指令作用于所有線程,即該指令可以令整個程式暫停執行;而在 non-stop 模式下,interrupt 指令僅作用于目前線程。 如果想另其作用于所有線程,可以執行 interrupt -a 指令。

多程序調試

GDB多程序調試常用指令

指令文法格式 功 能
(gdb)show detach-on-fork 檢視目前調試環境中 detach-on-fork 選項的值。
(gdb) info inferiors 檢視目前調試環境中有多少個程序。其中,程序 id 号前帶有 * 号的為目前正在調試的程序。
(gdb) inferiors id 切換到指定 ID 編号的程序對其進行調試。
(gdb) detach inferior id 斷開 GDB 與指定 id 編号程序之間的聯系,使該程序可以獨立運作。不過,該程序仍存在 info inferiors 列印的清單中,其 Describution 列為 <null>,并且借助 run 仍可以重新啟用。
(gdb) kill inferior id 斷開 GDB 與指定 id 編号程序之間的聯系,并中斷該程序的執行。不過,該程序仍存在 info inferiors 列印的清單中,其 Describution 列為 <null>,并且借助 run 仍可以重新啟用。
remove-inferior id 徹底删除指令 id 編号的程序(從 info inferiors 列印的清單中消除),不過在執行此操作之前,需先使用 detach inferior id 或者 kill inferior id 指令将該程序與 GDB 分離,同時确認其不是目前程序。

GDB attach指令調試程序

#include <stdio.h>
#include <unistd.h>
int main()
{
    pid_t pid = fork();
    if(pid == 0)
    {
         int num =10;
	    while(num==10){
	        sleep(10);
	    }
	    printf("this is child,pid = %d\n",getpid());
    }
    else
    {
        printf("this is parent,pid = %d\n",getpid());
    }
    return 0;
}
           
[[email protected] demo]# gdb myfork.exe -q
Reading symbols from ~/demo/myfork.exe...done.
(gdb) r
Starting program: ~/demo/myfork.exe
Detaching after fork from child process 5316.  <-- 子程序的 ID 号為 5316
this is parent,pid = 5313               <-- 父程序執行完畢

Program exited normally.
(gdb) attach 5316                          <-- 跳轉調試 ID 号為 5316 的子程序
......
(gdb) n                                           <-- 程式正在運作,所有直接使用 next 指令就可以進行單步調試
Single stepping until exit from function __nanosleep_nocancel,
which has no line number information.
0x00000037ee2acb50 in sleep () from /lib64/libc.so.6
(gdb) n
Single stepping until exit from function sleep,
which has no line number information.
main () at myfork.c:10
10  while(num==10){
(gdb) p num=1
$1 = 1
(gdb) n                                           <-- 跳出循環
13         printf("this is child,pid = %d\n",getpid());
(gdb) c
Continuing.
this is child,pid = 5316

Program exited normally.
(gdb) 
           

顯式指定要調試的程序

GDB 調試多程序程式時預設隻調試父程序。對于核心版本為 2.5.46 甚至更高的 Linux 發行版系統來說,可以通過修改 follow-fork-mode 或者 detach-on-fork 選項的值來調整這一預設設定。
(gdb) set follow-fork-mode mode
           

參數 mode 的可選值有 2 個:

  • parent:選項的預設值,表示 GDB 調試器預設隻調試父程序;
  • child:和 parent 完全相反,它使的 GDB 隻調試子程序。且當程式中包含多個子程序時,我們可以逐一對它們進行調試。
(gdb) show follow-fork-mode
Debugger response to a program call of fork or vfork is "child".
           

detach-on-fork選項

借助 follow-fork-mode 選項,我們隻能選擇調試子程序還是父程序,且一經標明,調試過程中将無法改變。如果既想調試父程序,又想随時切換并調試某個子程序,就需要借助 detach-on-fork 選項。
(gdb) set detach-on-fork mode
           

其中,mode 參數的可選值有 2 個:

  • on:預設值,表明 GDB 隻調試一個程序,可以是父程序,或者某個子程序;
  • off:程式中出現的每個程序都會被 GDB 記錄,我們可以随時切換到任意一個程序進行調試。

反向調試

所謂反向調試,指的是臨時改變程式的執行方向,反向執行指定行數的代碼,此過程中 GDB 調試器可以消除這些代碼所做的工作,将調試環境還原到這些代碼未執行前的狀态。

GDB反向調試的常用指令

命 令 功 能
(gdb) record/(gdb) record btrace 讓程式開始記錄反向調試所必要的資訊,其中包括儲存程式每一步運作的結果等等資訊。進行反向調試之前(啟動程式之後),需執行此指令,否則是無法進行反向調試的。
(gdb) reverse-continue/(gdb) rc 反向運作程式,直到遇到使程式中斷的事件,比如斷點或者已經退回到 record 指令開啟時程式執行到的位置。
(gdb) reverse-step 反向執行一行代碼,并在上一行代碼的開頭處暫停。和 step 指令類似,當反向遇到函數時,該指令會回退到函數内部,并在函數最後一行代碼的開頭處(通常為 return 0; )暫停執行。
(gdb) reverse-next 反向執行一行代碼,并在上一行代碼的開頭處暫停。和 reverse-step 指令不同,該指令不會進入函數内部,而僅将被調用函數視為一行代碼。
(gdb) reverse-finish 當在函數内部進行反向調試時,該指令可以回退到調用目前函數的代碼處。
(gdb) set exec-direction mode mode 參數值可以為 forward (預設值)和 reverse:forward 表示 GDB 以正常的方式執行所有指令;reverse 表示 GDB 将反向執行所有指令,由此我們直接隻用step、next、continue、finish 指令來反向調試程式。注意,return 指令不能在 reverse 模式中使用。

handle指令:信号處理

GDB 調試器提供了info handles指令,用于檢視 GDB 可以處理的信号種類,以及各個信号的具體處理方式

(gdb) info signals
Signal        Stop Print  Pass to program  Description

SIGHUP        Yes  Yes    Yes              Hangup
SIGINT        Yes  Yes    No               Interrupt
SIGQUIT       Yes  Yes    Yes              Quit
SIGILL        Yes  Yes    Yes              Illegal instruction
SIGTRAP       Yes  Yes    No               Trace/breakpoint trap
SIGABRT       Yes  Yes    Yes              Aborted
SIGEMT        Yes  Yes    Yes              Emulation trap
SIGFPE        Yes  Yes    Yes              Arithmetic exception
SIGKILL       Yes  Yes    Yes              Killed
......
           

其中各列的含義分别為:

Signal:各個信号的名稱;

Stop:當信号發生時,是否終止程式執行。Yes 表示終止,No 表示當信号發生時程式認可繼續執行;

Print:當信号發生時,是否要求 GDB 列印出一條提示資訊。Yes 表示列印,No 表示不列印;

Pass:當信号發生時,該信号是否對程式可見。Yes 表示程式可以捕捉到該資訊,No 表示程式不會捕捉到該資訊;

Description:對信号所表示含義的簡單描述。

(gdb) handle signal mode
           

其中,signal 參數表示要設定的目标信号,它通常為某個信号的全名(SIGINT)或者簡稱(去除‘SIG’後的部分,如 INT);如果要指定所有信号,可以用 all 表示。

mode 參數用于明确 GDB 處理該目标資訊的方式,其值可以是如下幾個:

nostop:當信号發生時,GDB 不會暫停程式,其可以繼續執行,但會列印出一條提示資訊,告訴我們信号已經發生;

stop:當信号發生時,GDB 會暫停程式執行。

noprint:當信号發生時,GDB 不會列印出任何提示資訊;

print:當信号發生時,GDB 會列印出必要的提示資訊;

nopass(或者 ignore):GDB 捕獲目标信号的同時,不允許程式自行處理該信号;

pass(或者 noignore):GDB 調試在捕獲目标信号的同時,也允許程式自動處理該信号。

注意,當 GDB 捕獲到信号并暫停程式執行的那一刻,程式是捕獲不到信号的,隻有等到程式繼續執行時,信号才能被程式捕獲。

檢視棧資訊

frame 指令的常用形式有 2 個:

frame指令

標明要檢視的棧幀

(gdb) frame spec
           

該指令可以将 spec 參數指定的棧幀標明為目前棧幀。spec 參數的值,常用的指定方法有 3 種:

  • 通過棧幀的編号指定。0 為目前被調用函數對應的棧幀号,最大編号的棧幀對應的函數通常就是 main() 主函數;
  • 借助棧幀的位址指定。棧幀位址可以通過 info frame 指令(後續會講)列印出的資訊中看到;
  • 通過函數的函數名指定。注意,如果是類似遞歸函數,其對應多個棧幀的話,通過此方法指定的是編号最小的那個棧幀。

除此之外,對于標明一個棧幀作為目前棧幀,GDB 調試器還提供有 up 和 down 兩個指令。其中,up 、down指令的文法格式為:

(gdb) up n
(gdb) down n
           

up—>其中 n 為整數,預設值為 1。該指令表示在目前棧幀編号(假設為 m)的基礎上,標明 m+n 為編号的棧幀作為新的目前棧幀。

down—>其中 n 為整數,預設值為 1。該指令表示在目前棧幀編号(假設為 m)的基礎上,標明 m-n 為編号的棧幀作為新的目前棧幀。

檢視目前棧幀中存儲的資訊

(gdb) info frame
           

該指令會依次列印出目前棧幀的如下資訊:

  • 目前棧幀的編号,以及棧幀的位址;
  • 目前棧幀對應函數的存儲位址,以及該函數被調用時的代碼存儲的位址
  • 目前函數的調用者,對應的棧幀的位址;
  • 編寫此棧幀所用的程式設計語言;
  • 函數參數的存儲位址以及值;
  • 函數中局部變量的存儲位址;
  • 棧幀中存儲的寄存器變量,例如指令寄存器(64位環境中用 rip 表示,32為環境中用 eip 表示)、堆棧基指針寄存器(64位環境用 rbp 表示,32位環境用 ebp 表示)等。
除此之外,還可以使用info args指令檢視目前函數各個參數的值;使用info locals指令檢視目前函數中各局部變量的值

backtrace指令

backtrace 指令用于列印目前調試環境中所有棧幀的資訊

(gdb) backtrace [-full] [n]
           

其中,用 [ ] 括起來的參數為可選項,它們的含義分别為:

  • n:一個整數值,當為正整數時,表示列印最裡層的 n 個棧幀的資訊;n 為負整數時,那麼表示列印最外層 n 個棧幀的資訊;
  • full:列印棧幀資訊的同時,列印出局部變量的值。
注意,當調試多線程程式時,該指令僅用于列印目前線程中所有棧幀的資訊。如果想要列印所有線程的棧幀資訊,應執行thread apply all backtrace指令。
#include <stdio.h>
int func(int num){
    if(num==1){
        return 1;
    }else{
        return num*func(num-1);
    }
}
int main ()
{
    int n = 5;
    int result = func(n);
    printf("%d! = %d",n,result);
    return 0;
}
           
(gdb) b 3
Breakpoint 1 at 0x4004cf: file main.c, line 3.
(gdb) r
Starting program: ~/demo/main.exe

Breakpoint 1, func (num=5) at main.c:3
3     if(num==1){
(gdb) c
Continuing.

Breakpoint 1, func (num=4) at main.c:3
3     if(num==1){
(gdb) p num
$1 = 4
(gdb) backtrace        <-- 列印所有的棧幀資訊
#0  func (num=4) at main.c:3
#1  0x00000000004004e9 in func (num=5) at main.c:6
#2  0x0000000000400508 in main () at main.c:12
(gdb) info frame       <-- 列印目前棧幀的詳細資訊
Stack level 0, frame at 0x7fffffffe240:            <-- 棧幀編号 0,位址 0x7fffffffe240
rip = 0x4004cf in func (main.c:3); saved rip 0x4004e9   <-- 函數的存儲位址 0x4004cf,調用它的函數位址為 0x4004e9
called by frame at 0x7fffffffe260        <-- 目前棧幀的上一級棧幀(編号 1 的棧幀)的位址為 0x7fffffffe260
source language c.
Arglist at 0x7fffffffe230, args: num=4  <-- 函數參數的位址和值
Locals at 0x7fffffffe230, Previous frame's sp is 0x7fffffffe240  <--函數内部局部變量的存儲位址
Saved registers:    <-- 棧幀内部存儲的寄存器
  rbp at 0x7fffffffe230, rip at 0x7fffffffe238
(gdb) info args          <-- 列印目前函數參數的值
num = 4
(gdb) info locals        <-- 列印目前函數内部局部變量的資訊(這裡沒有)
No locals.
(gdb) up   <-- 檢視編号為 1 的棧幀
#1  0x00000000004004e9 in func (num=5) at main.c:6
6         return num*func(num-1);
(gdb) frame 1           <-- 當編号為 1 的棧幀作為目前棧幀
#1  0x00000000004004e9 in func (num=5) at main.c:6
6         return num*func(num-1);
(gdb) info frame      <-- 列印 1 号棧幀的詳細資訊
Stack level 1, frame at 0x7fffffffe260:
rip = 0x4004e9 in func (main.c:6); saved rip 0x400508
called by frame at 0x7fffffffe280, caller of frame at 0x7fffffffe240  <--上一級棧幀位址為 0x7fffffffe280,下一級棧幀位址為 0x7fffffffe240
source language c.
Arglist at 0x7fffffffe250, args: num=5
Locals at 0x7fffffffe250, Previous frame's sp is 0x7fffffffe260
Saved registers:
  rbp at 0x7fffffffe250, rip at 0x7fffffffe258
(gdb)
           

繼續閱讀