Linux gdb偵錯程式用法全面解析

xumenger發表於2019-05-14

轉載自:Linux gdb偵錯程式用法全面解析

GDB是GNU開源組織釋出的一個強大的UNIX下的程式除錯工具,GDB主要可幫助工程師完成下面4個方面的功能:

  • 啟動程式,可以按照工程師自定義的要求隨心所欲的執行程式。

  • 讓被除錯的程式在工程師指定的斷點處停住,斷點可以是條件表示式。

  • 當程式被停住時,可以檢查此時程式中所發生的事,並追索上文。

  • 動態地改變程式的執行環境。

不管是除錯Linux核心空間的驅動還是除錯使用者空間的應用程式,掌握gdb的用法都是必須。而且,除錯核心和除錯應用程式時使用的gdb命令是完全相同的,下面以程式碼清單22.2的應用程式為例演示gdb偵錯程式的用法。

1  int add(int a, int b)  
2  {  
3    return a + b;  
4  }  
5    
6  main()  
7  {  
8    int sum[10] =   
9    {  
10     0, 0, 0, 0, 0, 0, 0, 0, 0, 0       
11   }  ;  
12   int i;  
13     
14   int array1[10] =  
15   {  
16     48, 56, 77, 33, 33, 11, 226, 544, 78, 90  
17   };  
18   int array2[10] =  
19   {  
20     85, 99, 66, 0x199, 393, 11, 1, 2, 3, 4  
21   };  
22   
23   for (i = 0; i < 10; i++)  
24   {  
25     sum[i] = add(array1[i], array2[i]);  
26   }  
27 }  

使用命令gcc –g gdb_example.c –o gdb_example編譯上述程式,得到包含除錯資訊的二進位制檔案example,執行gdb gdb_example命令進入除錯狀態:

[root@localhost driver_study]# gdb gdb_example  
GNU gdb Red Hat Linux (5.3post-0.20021129.18rh)  
Copyright 2003 Free Software Foundation, Inc.  
GDB is free software, covered by the GNU General Public License, and you are  
welcome to change it and/or distribute copies of it under certain conditions.  
Type "show copying" to see the conditions.  
There is absolutely no warranty for GDB.  Type "show warranty" for details.  
This GDB was configured as "i386-redhat-linux-gnu"...  
(gdb)  

list命令

在gdb中執行list命令(縮寫l)可以列出程式碼,list的具體形式包括:

  • list <linenum> ,顯示程式第linenum行周圍的源程式,如:

    (gdb) list 15  
    10          
    11        int array1[10] =  
    12        {  
    13          48, 56, 77, 33, 33, 11, 226, 544, 78, 90  
    14        };  
    15        int array2[10] =  
    16        {  
    17          85, 99, 66, 0x199, 393, 11, 1, 2, 3, 4  
    18        };  
    19  
  • list <function>,顯示函式名為function的函式的源程式,如:

    (gdb) list main  
    2       {  
    3         return a + b;  
    4       }  
    5  
    6       main()  
    7       {  
    8         int sum[10];  
    9         int i;  
    10          
    11        int array1[10] =  
  • list,顯示當前行後面的源程式。

  • list - ,顯示當前行前面的源程式。

下面演示了使用gdb中的run(縮寫r)、break(縮寫b)、next(縮寫n)命令控制程式的執行,並使用print(縮寫p)命令列印程式中的變數sum的過程:

(gdb) break add
Breakpoint 1 at 0x80482f7: file gdb_example.c, line 3.
(gdb) run  
Starting program: /driver_study/gdb_example 

Breakpoint 1, add (a=48, b=85) at gdb_example.c:3
warning: Source file is more recent than executable.

3         return a + b;
(gdb) next
4       }
(gdb) next
main () at gdb_example.c:23
23        for (i = 0; i < 10; i++)
(gdb) next
25          sum[i] = add(array1[i], array2[i]);
(gdb) print sum
$1 = {133, 0, 0, 0, 0, 0, 0, 0, 0, 0}

run命令

在gdb中,執行程式使用run命令。在程式執行前,我們可以設定如下4方面的工作環境:

  • 程式執行引數

set args 可指定執行時引數,如:set args 10 20 30 40 50show args 命令可以檢視設定好的執行引數。

  • 執行環境

path <dir> 可設定程式的執行路徑;how paths可檢視程式的執行路徑;set environment varname [=value]用於設定環境變數,如set env USER=baohua;

show environment [varname]則用於檢視環境變數。

  • 工作目錄

cd <dir> 相當於shell的cd命令;pwd 顯示當前所在的目錄。

  • 程式的輸入輸出

info terminal 用於顯示程式用到的終端的模式;gdb中也可以使用重定向控制程式輸出,如run > outfile

tty命令可以指定輸入輸出的終端裝置,如:tty /dev/ttyS1

break命令

在gdb中用break命令來設定斷點,設定斷點的方法包括:

  • break <function>

在進入指定函式時停住,C++中可以使用class::function或function(type, type)格式來指定函式名。

  • break <linenum>

在指定行號停住。

  • break +offset / break -offset

在當前行號的前面或後面的offset行停住,offiset為自然數。

  • break filename:linenum

在原始檔filename的linenum行處停住。

  • break filename:function

在原始檔filename的function函式的入口處停住。

  • break *address

在程式執行的記憶體地址處停住。

  • break

break命令沒有引數時,表示在下一條指令處停住。

  • break ... if <condition>

“…”可以是上述的break <linenum>break +offsetbreak –offset中的引數,condition表示條件,在條件成立時停住。比如在迴圈體中,可以設定break if i=100,表示當i為100時停住程式。

檢視斷點時,可使用info命令,如info breakpoints [n]info break [n](n表示斷點號)。

單步命令

在除錯過程中,next命令用於單步執行,類似VC++中的step over。next的單步不會進入函式的內部,與next對應的step(縮寫s)命令則在單步執行一個函式時,會進入其內部,類似VC++中的step into。下面演示了step命令的執行情況,在23行的add()函式呼叫處執行step會進入其內部的“return a+b;”語句:

(gdb) break 25  
Breakpoint 1 at 0x8048362: file gdb_example.c, line 25.  
(gdb) run  
Starting program: /driver_study/gdb_example   
  
Breakpoint 1, main () at gdb_example.c:25  
25          sum[i] = add(array1[i], array2[i]);  
(gdb) step  
add (a=48, b=85) at gdb_example.c:3  
3         return a + b;  

單步執行的更復雜用法包括:

  • step <count>

單步跟蹤,如果有函式呼叫,則進入該函式(進入函式的前提是,此函式被編譯有debug資訊)。step後面不加count表示一條條地執行,加表示執行後面的count條指令,然後再停住。

  • next <count>

單步跟蹤,如果有函式呼叫,它不會進入該函式。同樣地,next後面不加count表示一條條地執行,加表示執行後面的count條指令,然後再停住。

  • set step-mode

set step-mode on用於開啟step-mode模式,這樣,在進行單步跟蹤時,程式不會因為沒有debug資訊而不停住,這個引數的設定可便於檢視機器碼。set step-mod off用於關閉step-mode模式。

  • finish

執行程式,直到當前函式完成返回,並列印函式返回時的堆疊地址和返回值及引數值等資訊。

  • until (縮寫u)

一直在迴圈體內執行單步,退不出來是一件令人煩惱的事情,until命令可以執行程式直到退出迴圈體。

  • stepi(縮寫si)和nexti(縮寫ni

stepi和nexti用於單步跟蹤一條機器指令,一條程式程式碼有可能由數條機器指令完成,stepi和nexti可以單步執行機器指令。 另外,執行“display/i $pc”命令後,單步跟蹤會在打出程式程式碼的同時打出機器指令,即彙編程式碼。

continue命令

當程式被停住後,可以使用continue命令(縮寫c,fg命令同continue命令)恢復程式的執行直到程式結束,或到達下一個斷點,命令格式為:

continue [ignore-count]  
c [ignore-count]  
fg [ignore-count]  

ignore-count表示忽略其後多少次斷點。 假設我們設定了函式斷點add(),並watch i,則在continue過程中,每次遇到add()函式或i發生變化,程式就會停住,如:

(gdb) continue  
Continuing.  
Hardware watchpoint 3: i  
  
Old value = 2  
New value = 3  
0x0804838d in main () at gdb_example.c:23  
23        for (i = 0; i < 10; i++)  
(gdb) continue  
Continuing.  
  
Breakpoint 1, main () at gdb_example.c:25  
25          sum[i] = add(array1[i], array2[i]);  
(gdb) continue  
Continuing.  
Hardware watchpoint 3: i  
  
Old value = 3  
New value = 4  
0x0804838d in main () at gdb_example.c:23  
23        for (i = 0; i < 10; i++)  

print命令

在除錯程式時,當程式被停住時,可以使用print命令(縮寫為p),或是同義命令inspect來檢視當前程式的執行資料。print命令的格式是:

print <expr>  
print /<f> <expr>  

<expr>是表示式,是被除錯的程式中的表示式,<f>是輸出的格式,比如,如果要把表示式按16進位制的格式輸出,那麼就是/x。在表示式中,有幾種GDB所支援的操作符,它們可以用在任何一種語言中,“@”是一個和陣列有關的操作符,“::”指定一個在檔案或是函式中的變數,“{<type>} <addr>”表示一個指向記憶體地址<addr>的型別為type的一個物件。

下面演示了檢視sum[]陣列的值的過程:

(gdb) print sum  
$2 = {133, 155, 0, 0, 0, 0, 0, 0, 0, 0}  
(gdb) next  
  
Breakpoint 1, main () at gdb_example.c:25  
25          sum[i] = add(array1[i], array2[i]);  
(gdb) next  
23        for (i = 0; i < 10; i++)  
(gdb) print sum  
$3 = {133, 155, 143, 0, 0, 0, 0, 0, 0, 0}  

當需要檢視一段連續記憶體空間的值的時間,可以使用GDB的“@”操作符,“@”的左邊是第一個記憶體地址,“@”的右邊則是想檢視記憶體的長度。例如如下動態申請的記憶體:

int *array = (int *) malloc (len * sizeof (int));

在GDB除錯過程中這樣顯示出這個動態陣列的值:p *array@len

print的輸出格式包括:

  • x 按十六進位制格式顯示變數。

  • d 按十進位制格式顯示變數。

  • u 按十六進位制格式顯示無符號整型。

  • o 按八進位制格式顯示變數。

  • t 按二進位制格式顯示變數。

  • a 按十六進位制格式顯示變數。

  • c 按字元格式顯示變數。

  • f 按浮點數格式顯示變數。

我們可用display命令設定一些自動顯示的變數,當程式停住時,或是單步跟蹤時,這些變數會自動顯示。 如果要修改變數,如x的值,可使用如下命令:print x=4

當用GDB的print檢視程式執行時的資料時,每一個print都會被GDB記錄下來。GDB會以$1,$2,$3 …這樣的方式為每一個print命令編號。我們可以使用這個編號訪問以前的表示式,如$1。

watch命令

watch一般來觀察某個表示式(變數也是一種表示式)的值是否有變化了,如果有變化,馬上停住程式。我們有下面的幾種方法來設定觀察點:

  • watch <expr>:為表示式(變數)expr設定一個觀察點。一量表示式值有變化時,馬上停住程式。

  • rwatch <expr>:當表示式(變數)expr被讀時,停住程式。

  • awatch <expr>:當表示式(變數)的值被讀或被寫時,停住程式。info watchpoints:列出當前所設定了的所有觀察點。

下面演示了觀察i並在連續執行next時一旦發現i變化,i值就會顯示出來的過程:

(gdb) watch i  
Hardware watchpoint 3: i  
(gdb) next  
23        for (i = 0; i < 10; i++)  
(gdb) next  
Hardware watchpoint 3: i  
  
Old value = 0  
New value = 1  
0x0804838d in main () at gdb_example.c:23  
23        for (i = 0; i < 10; i++)  
(gdb) next  
  
Breakpoint 1, main () at gdb_example.c:25  
25          sum[i] = add(array1[i], array2[i]);  
(gdb) next  
23        for (i = 0; i < 10; i++)  
(gdb) next  
Hardware watchpoint 3: i  
  
Old value = 1  
New value = 2  
0x0804838d in main () at gdb_example.c:23  
23        for (i = 0; i < 10; i++)  

examine命令

我們可以使用examine命令(縮寫為x)來檢視記憶體地址中的值。examine命令的語法如下所示:

x/<n/f/u> <addr>

<addr>表示一個記憶體地址。“x/”後的n、f、u都是可選的引數,n 是一個正整數,表示顯示記憶體的長度,也就是說從當前地址向後顯示幾個地址的內容;f 表示顯示的格式,如果地址所指的是字串,那麼格式可以是s,如果地址是指令地址,那麼格式可以是i;u 表示從當前地址往後請求的位元組數,如果不指定的話,GDB預設是4位元組。u引數可以被一些字元代替:b表示單位元組,h表示雙位元組,w表示四位元組,g表示八位元組。當我們指定了位元組長度後,GDB會從指定的記憶體地址開始,讀寫指定位元組,並把其當作一個值取出來。n、f、u這3個引數可以一起使用,例如命令“x/3uh 0x54320”表示從記憶體地址0x54320開始以雙位元組為1個單位(h)、16進位制方式(u)顯示3個單位(3)的記憶體。 ==

譬如下面的例子:

main()  
{  
        char *c = "hello world";  
        printf("%s
", c);  
}  

我們在

char *c = "hello world";  

下一行設定斷點後:

(gdb) l  
1    main()  
2    {  
3        char *c = "hello world";  
4        printf("%s
", c);  
5    }  
(gdb) b 4  
Breakpoint 1 at 0x100000f17: file main.c, line 4.  
(gdb) r  
Starting program: /Users/songbarry/main  
Reading symbols for shared libraries +. done  
  
Breakpoint 1, main () at main.c:4  
4        printf("%s
", c);  

可以通過多種方式看C指向的字串:

  • 方法1:

    (gdb) p c  
    $1 = 0x100000f2e "hello world"  
    
  • 方法2:

    (gdb) x/s 0x100000f2e  
    0x100000f2e:     "hello world"  
    
  • 方法3:

    (gdb) p (char *)0x100000f2e  
    $3 = 0x100000f2e "hello world"  
    
  • 將第一個字元改為大寫:

    (gdb) p *(char *)0x100000f2e=`H`  
    $4 = 72 `H`  
    
  • 再看看C:

    (gdb) p c  
    $5 = 0x100000f2e "Hello world"  
    

set命令

  • 修改暫存器:

    (gdb) set $v0 = 0x004000000  
    (gdb) set $epc = 0xbfc00000   
    
  • 修改記憶體:

    (gdb) set {unsigned int}0x8048a51=0x0  
    
  • 譬如對於第8節的例子:

    (gdb) set {unsigned int}0x100000f2e=0x0         
    (gdb) x/10cb 0x100000f2e  
    0x100000f2e:    0 ` `  0 ` `  0 ` `  0 ` `  111 `o` 32 ` `  119 `w` 111 `o`  
    0x100000f36:    114 `r` 108 `l`  
    (gdb) p c  
    $10 = 0x100000f2e ""  
    

jump命令

一般來說,被除錯程式會按照程式程式碼的執行順序依次執行,但是GDB也提供了亂序執行的功能,也就是說,GDB可以修改程式的執行順序,從而讓程式隨意跳躍。這個功能可以由GDB的jump命令:jump <linespec> 來指定下一條語句的執行點。<linespec>可以是檔案的行號,可以是file:line格式,也可以是+num這種偏移量格式,表示下一條執行語句從哪裡開始。jump <address> 這裡的<address>是程式碼行的記憶體地址。 注意,jump命令不會改變當前的程式棧中的內容,所以,如果使用jump從一個函式跳轉到另一個函式,當跳轉到的函式執行完返回,進行出棧操作時必然會發生錯誤,這可能導致意想不到的結果,所以最好只用jump在同一個函式中進行跳轉。

signal命令

使用singal命令,可以產生一個訊號量給被除錯的程式,如中斷訊號“Ctrl+C”。這非常方便於程式的除錯,可以在程式執行的任意位置設定斷點,並在該斷點用GDB產生一個訊號量,這種精確地在某處產生訊號的方法非常有利於程式的除錯。 signal命令的語法是:signal <signal>,UNIX的系統訊號量通常從1到15,所以<signal>取值也在這個範圍。

return命令

如果在函式中設定了除錯斷點,在斷點後還有語句沒有執行完,這時候我們可以使用return命令強制函式忽略還沒有執行的語句並返回。

return  
return <expression>  

上述return命令用於取消當前函式的執行,並立即返回,如果指定了<expression>,那麼該表示式的值會被作為函式的返回值。

call命令

call命令用於強制呼叫某函式: call <expr> 表示式中可以一是函式,以此達到強制呼叫函式的目的,它會顯示函式的返回值(如果函式返回值不是void)。 其實,前面介紹的print命令也可以完成強制呼叫函式的功能。

info命令

info命令可以在除錯時用來檢視暫存器、斷點、觀察點和訊號等資訊。要檢視暫存器的值,可以使用如下命令:

  • info registers (檢視除了浮點暫存器以外的暫存器)

  • info all-registers (檢視所有暫存器,包括浮點暫存器)

  • info registers <regname ...> (檢視所指定的暫存器)

要檢視斷點資訊,可以使用如下命令:

  • info break 列出當前所設定的所有觀察點

  • 使用如下命令:info watchpoints 檢視有哪些訊號正在被GDB檢測

  • 使用如下命令:info signals info handle 也可以使用info line命令來檢視原始碼在記憶體中的地址。

  • info threads可以看多執行緒。info line後面可以跟行號、函式名、檔名:行號、檔名:函式名等多種形式,例如下面的命令會列印出所指定的原始碼在執行時的記憶體地址:

    info line tst.c:func

set scheduler-locking off|on|step

  • off 不鎖定任何執行緒,也就是所有執行緒都執行,這是預設值。

  • on 只有當前被除錯程式會執行。

  • step 在單步的時候,除了next過一個函式的情況以外,只有當前執行緒會執行。

與多執行緒除錯相關的命令還包括:

  • thread ID,切換當前除錯的執行緒為指定ID的執行緒。

  • break thread_test.c:123 thread all,在所有執行緒中相應的行上設定斷點

  • thread apply ID1 ID2 command,讓一個或者多個執行緒執行GDB命令command。

  • thread apply all command,讓所有被除錯執行緒執行GDB命令command。

disassemble

disassemble命令用於反彙編,它可被用來檢視當前執行時的原始碼的機器碼,其實際上只是把目前記憶體中的指令dump出來。下面的示例用於檢視函式func的彙編程式碼:

(gdb) disassemble func  
Dump of assembler code for function func:  
0x8048450 <func>:       push   %ebp  
0x8048451 <func+1>:     mov    %esp,%ebp  
0x8048453 <func+3>:     sub    $0x18,%esp  
0x8048456 <func+6>:     movl   $0x0,0xfffffffc(%ebp)  
...  
End of assembler dump.  

相關文章