style="display:inline-block;width:300px;height:250px"
data-ad-client="ca-pub-5935214489160196"
data-ad-slot="8007533899">

GDB

用GDB偵錯工具
http://blog.chinaunix.net/uid-20398463-id-1950783.html
GDB是一個強大的命令列調試工具。大家知道命令列的強大就是在於,其可以形成執行序列,形成腳本。UNIX下的軟體全是命令列的,這給程式開發提代供了極大的便利,命令列軟體的優勢在於,它們可以非常容易的集成在一起,使用幾個簡單的已有工具的命令,就可以做出一個非常強大的功能。

於是UNIX下的軟體比Windows下的軟體更能有機地結合,各自發揮各自的長處,組合成更為強勁的功能。而Windows下的圖形軟體基本上是各自為營,互相不能調用,很不利於各種軟體的相互集成。在這裡並不是要和Windows做個什麼比較,所謂“寸有所長,尺有所短”,圖形化工具還是有不如命令列的地方。
用GDB偵錯工具
GDB概述
————
GDB是GNU開源組織發佈的一個強大的UNIX下的程式調試工具。或許,各位比較喜歡那種圖形介面方式的,像VC、BCB等IDE的調試,但如果你是在UNIX平臺下做軟體,你會發現GDB這個調試工具有比VC、BCB的圖形化調試器更強大的功能。所謂“寸有所長,尺有所短”就是這個道理。
一般來說,GDB主要幫忙你完成下面四個方面的功能:
1、啟動你的程式,可以按照你的自訂的要求隨心所欲的運行程式。
2、可讓被調試的程式在你所指定的調置的中斷點處停住。(中斷點可以是條件運算式)
3、當程式被停住時,可以檢查此時你的程式中所發生的事。
4、動態的改變你程式的執行環境。
從上面看來,GDB和一般的調試工具沒有什麼兩樣,基本上也是完成這些功能,不過在細節上,你會發現GDB這個調試工具的強大,大家可能比較習慣了圖形化的調試工具,但有時候,命令列的調試工具卻有著圖形化工具所不能完成的功能。讓我們一一看來。
一個調試示例
——————
來源程式:tst.c
1 #include
2
3 int func(int n)
4 {
5 int sum=0,i;
6 for(i=0; i
7 {
8 sum+=i;
9 }
10 return sum;
11 }
12
13
14 main()
15 {
16 int i;
17 long result = 0;
18 for(i=1; i<=100; i++)
19 {
20 result += i;
21 }
22
23 printf("result[1-100] = %d \n", result );
24 printf("result[1-250] = %d \n", func(250) );
25 }
編譯生成執行檔:(Linux下)
hchen/test> cc -g tst.c -o tst
使用GDB調試:
hchen/test> gdb tst <---------- 啟動GDB
GNU gdb 5.1.1
Copyright 2002 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-suse-linux"...
(gdb) l <-------------------- l命令相當於list,從第一行開始例出原碼。
1 #include
2
3 int func(int n)
4 {
5 int sum=0,i;
6 for(i=0; i
7 {
8 sum+=i;
9 }
10 return sum;
(gdb) <-------------------- 直接回車表示,重複上一次命令
11 }
12
13
14 main()
15 {
16 int i;
17 long result = 0;
18 for(i=1; i<=100; i++)
19 {
20 result += i;
(gdb) break 16 <-------------------- 設置中斷點,在來源程式第16行處。
Breakpoint 1 at 0x8048496: file tst.c, line 16.
(gdb) break func <-------------------- 設置中斷點,在函數func()入口處。
Breakpoint 2 at 0x8048456: file tst.c, line 5.
(gdb) info break <-------------------- 查看中斷點信息。
Num Type Disp Enb Address What
1 breakpoint keep y 0x08048496 in main at tst.c:16
2 breakpoint keep y 0x08048456 in func at tst.c:5
(gdb) r <--------------------- 運行程式,run命令簡寫
Starting program: /home/hchen/test/tst
Breakpoint 1, main () at tst.c:17 <---------- 在中斷點處停住。
17 long result = 0;
(gdb) n <--------------------- 單條語句執行,next命令簡寫。
18 for(i=1; i<=100; i++)
(gdb) n
20 result += i;
(gdb) n
18 for(i=1; i<=100; i++)
(gdb) n
20 result += i;
(gdb) c <--------------------- 繼續運行程式,continue命令簡寫。
Continuing.
result[1-100] = 5050 <----------程式輸出。
Breakpoint 2, func (n=250) at tst.c:5
5 int sum=0,i;
(gdb) n
6 for(i=1; i<=n; i++)
(gdb) p i <--------------------- 列印變數i的值,print命令簡寫。
$1 = 134513808
(gdb) n
8 sum+=i;
(gdb) n
6 for(i=1; i<=n; i++)
(gdb) p sum
$2 = 1
(gdb) n
8 sum+=i;
(gdb) p i
$3 = 2
(gdb) n
6 for(i=1; i<=n; i++)
(gdb) p sum
$4 = 3
(gdb) bt <--------------------- 查看函數堆疊。
#0 func (n=250) at tst.c:5
#1 0x080484e4 in main () at tst.c:24
#2 0x400409ed in __libc_start_main () from /lib/libc.so.6
(gdb) finish <--------------------- 退出函數。
Run till exit from #0 func (n=250) at tst.c:5
0x080484e4 in main () at tst.c:24
24 printf("result[1-250] = %d \n", func(250) );
Value returned is $6 = 31375
(gdb) c <--------------------- 繼續運行。
Continuing.
result[1-250] = 31375 <----------程式輸出。
Program exited with code 027. <--------程式退出,調試結束。
(gdb) q <--------------------- 退出gdb。
hchen/test>
好了,有了以上的感性認識,還是讓我們來系統地認識一下gdb吧。
使用GDB
————
一般來說GDB主要調試的是C/C++的程式。要調試C/C++的程式,首先在編譯時,我們必須要把調試資訊加到可執行檔中。使用編譯器(cc/gcc/g++)的 -g 參數可以做到這一點。如:
> cc -g hello.c -o hello
> g++ -g hello.cpp -o hello
如果沒有-g,你將看不見程式的函數名、變數名,所代替的全是運行時的記憶體位址。當你用-g把調試資訊加入之後,並成功編譯目標代碼以後,讓我們來看看如何用gdb來調試他。
啟動GDB的方法有以下幾種:
1、gdb
program也就是你的執行檔,一般在當然目錄下。
2、gdb core
用gdb同時調試一個運行程式和core檔,core是程式非法執行後core dump後產生的檔。
3、gdb
如果你的程式是一個服務程式,那麼你可以指定這個服務程式運行時的進程ID。gdb會自動attach上去,並調試他。program應該在PATH環境變數中搜索得到。
GDB啟動時,可以加上一些GDB的啟動開關,詳細的開關可以用gdb -help查看。我在下面只例舉一些比較常用的參數:
-symbols
-s
從指定檔中讀取符號表。
-se file
從指定檔中讀取符號表資訊,並把他用在可執行檔中。
-core
-c
調試時core dump的core文件。
-directory
-d
加入一個原始檔案的搜索路徑。預設搜索路徑是環境變數中PATH所定義的路徑。
GDB的命令概貌
———————
啟動gdb後,就你被帶入gdb的調試環境中,就可以使用gdb的命令開始偵錯工具了,gdb的命令可以使用help命令來查看,如下所示:
/home/hchen> gdb
GNU gdb 5.1.1
Copyright 2002 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-suse-linux”.
(gdb) help
List of classes of commands:
aliases — Aliases of other commands
breakpoints — Making program stop at certain points
data — Examining data
files — Specifying and examining files
internals — Maintenance commands
obscure — Obscure features
running — Running the program
stack — Examining the stack
status — Status inquiries
support — Support facilities
tracepoints — Tracing of program execution without stopping the program
user-defined — User-defined commands
Type “help” followed by a class name for a list of commands in that class.
Type “help” followed by command name for full documentation.
Command name abbreviations are allowed if unambiguous.
(gdb)
gdb的命令很多,gdb把之分成許多個種類。help命令只是例出gdb的命令種類,如果要看種類中的命令,可以使用help 命令,如:help breakpoints,查看設置中斷點的所有命令。也可以直接help 來查看命令的幫助。

gdb中,輸入命令時,可以不用打全命令,只用打命令的前幾個字元就可以了,當然,命令的前幾個字元應該要標誌著一個唯一的命令,在Linux下,你可以敲擊兩次TAB鍵來補齊命令的全稱,如果有重複的,那麼gdb會把其例出來。

示例一:在進入函數func時,設置一個中斷點。可以敲入break func,或是直接就是b func
(gdb) b func
Breakpoint 1 at 0x8048458: file hello.c, line 10.

示例二:敲入b按兩次TAB鍵,你會看到所有b打頭的命令:
(gdb) b
backtrace break bt
(gdb)
示例三:只記得函數的首碼,可以這樣:
(gdb) b make_ <按TAB鍵>
(再按下一次TAB鍵,你會看到:)
make_a_section_from_file make_environ
make_abs_section make_function_type
make_blockvector make_pointer_type
make_cleanup make_reference_type
make_command make_symbol_completion_list
(gdb) b make_
GDB把所有make開頭的函數全部例出來給你查看。
示例四:調試C++的程式時,有可以函數名一樣。如:
(gdb) b ‘bubble( M-?
bubble(double,double) bubble(int,int)
(gdb) b ‘bubble(
你可以查看到C++中的所有的重載函數及參數。(注:M-?和“按兩次TAB鍵”是一個意思)
要退出gdb時,只用發quit或命令簡稱q就行了。
GDB中運行UNIX的shell程式
————————————
在gdb環境中,你可以執行UNIX的shell的命令,使用gdb的shell命令來完成:
shell
調用UNIX的shell來執行,環境變數SHELL中定義的UNIX的shell將會被用來執行,如果SHELL沒有定義,那就使用UNIX的標準shell:/bin/sh。(在Windows中使用Command.com或cmd.exe)
還有一個gdb命令是make:
make
可以在gdb中執行make命令來重新build自己的程式。這個命令等價於“shell make ”。
在GDB中運行程式
————————
當以gdb 方式啟動gdb後,gdb會在PATH路徑和目前的目錄中搜索的原始檔案。如要確認gdb是否讀到原始檔案,可使用l或list命令,看看gdb是否能列出原始程式碼。
在gdb中,運行程式使用r或是run命令。程式的運行,你有可能需要設置下面四方面的事。
1、程式運行參數。
set args 可指定運行時參數。(如:set args 10 20 30 40 50)
show args 命令可以查看設置好的運行參數。
2、運行環境。
path
可設定程式的運行路徑。
show paths 查看程式的運行路徑。
set environment varname [=value] 設置環境變數。如:set env USER=hchen
show environment [varname] 查看環境變數。
3、工作目錄。
cd
相當於shell的cd命令。
pwd 顯示當前的所在目錄。
4、程式的輸入輸出。
info terminal 顯示你程式用到的終端的模式。
使用重定向控制程式輸出。如:run > outfile
tty命令可以指寫輸入輸出的終端設備。如:tty /dev/ttyb

調試已運行的程式
————————
兩種方法:
1、在UNIX下用ps查看正在運行的程式的PID(進程ID),然後用gdb PID格式掛接正在運行的程式。
2、先用gdb 關聯上原始程式碼,並進行gdb,在gdb中用attach命令來掛接進程的PID。並用detach來取消掛接的進程。
暫停 / 恢復程式運行
—————————
偵錯工具中,暫停程式運行是必須的,GDB可以方便地暫停程式的運行。你可以設置程式的在哪行停住,在什麼條件下停住,在收到什麼信號時停往等等。以便於你查看運行時的變數,以及運行時的流程。
當進程被gdb停住時,你可以使用info program 來查看程式的是否在運行,進程號,被暫停的原因。
在gdb中,我們可以有以下幾種暫停方式:中斷點(BreakPoint)、觀察點(WatchPoint)、捕捉點(CatchPoint)、信號(Signals)、執行緒停止(Thread Stops)。如果要恢復程式運行,可以使用c或是continue命令。
一、設置中斷點(BreakPoint)

我們用break命令來設置中斷點。正面有幾點設置中斷點的方法:

break
在進入指定函數時停住。C++中可以使用class::function或function(type,type)格式來指定函數名。
break
在指定行號停住。
break +offset
break -offset
在當前行號的前面或後面的offset行停住。offiset為自然數。
break filename:linenum
在原始檔案filename的linenum行處停住。
break filename:function
在原始檔案filename的function函數的入口處停住。
break *address
在程式運行的記憶體位址處停住。
break
break命令沒有參數時,表示在下一條指令處停住。
break … if
…可以是上述的參數,condition表示條件,在條件成立時停住。比如在迴圈境體中,可以設置break if i=100,表示當i為100時停住程式。
查看中斷點時,可使用info命令,如下所示:(注:n表示中斷點號)
info breakpoints [n]
info break [n]

二、設置觀察點(WatchPoint)

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

watch
為運算式(變數)expr設置一個觀察點。一量運算式值有變化時,馬上停住程式。

rwatch
當運算式(變數)expr被讀時,停住程式。

awatch
當運算式(變數)的值被讀或被寫時,停住程式。

info watchpoints
列出當前所設置了的所有觀察點。
三、設置捕捉點(CatchPoint)
你可設置捕捉點來補捉程式運行時的一些事件。如:載入共用庫(動態連結程式庫)或是C++的異常。設置捕捉點的格式為:

catch
當event發生時,停住程式。event可以是下面的內容:
1、throw 一個C++拋出的異常。(throw為關鍵字)
2、catch 一個C++捕捉到的異常。(catch為關鍵字)
3、exec 調用系統調用exec時。(exec為關鍵字,目前此功能只在HP-UX下有用)
4、fork 調用系統調用fork時。(fork為關鍵字,目前此功能只在HP-UX下有用)
5、vfork 調用系統調用vfork時。(vfork為關鍵字,目前此功能只在HP-UX下有用)
6、load 或 load 載入共用庫(動態連結程式庫)時。(load為關鍵字,目前此功能只在HP-UX下有用)
7、unload 或 unload 卸載共用庫(動態連結程式庫)時。(unload為關鍵字,目前此功能只在HP-UX下有用)
tcatch
只設置一次捕捉點,當程式停住以後,應點被自動刪除。
四、維護停止點
上面說了如何設置程式的停止點,GDB中的停止點也就是上述的三類。在GDB中,如果你覺得已定義好的停止點沒有用了,你可以使用delete、clear、disable、enable這幾個命令來進行維護。
clear
清除所有的已定義的停止點。
clear
clear
清除所有設置在函數上的停止點。
clear
clear
清除所有設置在指定行上的停止點。
delete [breakpoints] [range…]
刪除指定的中斷點,breakpoints為中斷點號。如果不指定中斷點號,則表示刪除所有的中斷點。range 表示中斷點號的範圍(如:3-7)。其簡寫命令為d。
比刪除更好的一種方法是disable停止點,disable了的停止點,GDB不會刪除,當你還需要時,enable即可,就好像回收站一樣。
disable [breakpoints] [range…]
disable所指定的停止點,breakpoints為停止點號。如果什麼都不指定,表示disable所有的停止點。簡寫命令是dis.
enable [breakpoints] [range…]
enable所指定的停止點,breakpoints為停止點號。
enable [breakpoints] once range…
enable所指定的停止點一次,當程式停止後,該停止點馬上被GDB自動disable。
enable [breakpoints] delete range…
enable所指定的停止點一次,當程式停止後,該停止點馬上被GDB自動刪除。
五、停止條件維護
前面在說到設置中斷點時,我們提到過可以設置一個條件,當條件成立時,程式自動停止,這是一個非常強大的功能,這裡,我想專門說說這個條件的相關維護命令。一般來說,為中斷點設置一個條件,我們使用if關鍵字,後面跟其中斷點條件。並且,條件設置好後,我們可以用condition命令來修改中斷點的條件。(只有break和watch命令支持if,catch目前暫不支持if)
condition
修改中斷點號為bnum的停止條件為expression。
condition
清除中斷點號為bnum的停止條件。

還有一個比較特殊的維護命令ignore,你可以指定程式運行時,忽略停止條件幾次。
ignore
表示忽略中斷點號為bnum的停止條件count次。
六、為停止點設定運行命令
我們可以使用GDB提供的command命令來設置停止點的運行命令。也就是說,當運行的程式在被停止住時,我們可以讓其自動運行一些別的命令,這很有利行自動化調試。對基於GDB的自動化調試是一個強大的支持。

commands [bnum]
… command-list …
end
為中斷點號bnum指寫一個命令列表。當程式被該中斷點停住時,gdb會依次運行命令列表中的命令。
例如:
break foo if x>0
commands
printf “x is %d\n”,x
continue
end
中斷點設置在函數foo中,中斷點條件是x>0,如果程式被斷住後,也就是,一旦x的值在foo函數中大於0,GDB會自動列印出x的值,並繼續運行程式。
如果你要清除中斷點上的命令序列,那麼只要簡單的執行一下commands命令,並直接在打個end就行了。
七、中斷點菜單
在C++中,可能會重複出現同一個名字的函數若干次(函數重載),在這種情況下,break 不能告訴GDB要停在哪個函數的入口。當然,你可以使用break 也就是把函數的參數類型告訴GDB,以指定一個函數。否則的話,GDB會給你列出一個中斷點功能表供你選擇你所需要的中斷點。你只要輸入你功能表清單中的編號就可以了。如:
(gdb) b String::after
[0] cancel
[1] all
[2] file:String.cc; line number:867
[3] file:String.cc; line number:860
[4] file:String.cc; line number:875
[5] file:String.cc; line number:853
[6] file:String.cc; line number:846
[7] file:String.cc; line number:735
> 2 4 6
Breakpoint 1 at 0xb26c: file String.cc, line 867.
Breakpoint 2 at 0xb344: file String.cc, line 875.
Breakpoint 3 at 0xafcc: file String.cc, line 846.
Multiple breakpoints were set.
Use the “delete” command to delete unwanted
breakpoints.
(gdb)
可見,GDB列出了所有after的重載函數,你可以選一下列表編號就行了。0表示放棄設置中斷點,1表示所有函數都設置中斷點。
八、恢復程式運行和單步調試
當程式被停住了,你可以用continue命令恢復程式的運行直到程式結束,或下一個中斷點到來。也可以使用step或next命令單步跟蹤程式。
continue [ignore-count]
c [ignore-count]
fg [ignore-count]
恢復程式運行,直到程式結束,或是下一個中斷點到來。ignore-count表示忽略其後的中斷點次數。continue,c,fg三個命令都是一樣的意思。

step
單步跟蹤,如果有函式呼叫,他會進入該函數。進入函數的前提是,此函數被編譯有debug資訊。很像VC等工具中的step in。後面可以加count也可以不加,不加表示一條條地執行,加表示執行後面的count條指令,然後再停住。
next
同樣單步跟蹤,如果有函式呼叫,他不會進入該函數。很像VC等工具中的step over。後面可以加count也可以不加,不加表示一條條地執行,加表示執行後面的count條指令,然後再停住。
set step-mode
set step-mode on
打開step-mode模式,於是,在進行單步跟蹤時,程式不會因為沒有debug資訊而不停住。這個參數有很利於查看機器碼。
set step-mod off
關閉step-mode模式。
finish
運行程式,直到當前函數完成返回。並列印函數返回時的堆疊位址和返回值及參數值等資訊。
until 或 u
當你厭倦了在一個循環體內單步跟蹤時,這個命令可以運行程式直到退出循環體。
stepi 或 si
nexti 或 ni
單步跟蹤一條機器指令!一條程式碼有可能由數條機器指令完成,stepi和nexti可以單步執行機器指令。與之一樣有相同功能的命令是 “display/i $pc” ,當運行完這個命令後,單步跟蹤會在打出程式碼的同時打出機器指令(也就是彙編代碼)
九、信號(Signals)
信號是一種軟中斷,是一種處理非同步事件的方法。一般來說,作業系統都支援許多信號。尤其是UNIX,比較重要應用程式一般都會處理信號。UNIX定義了許多信號,比如SIGINT表示中斷字元信號,也就是Ctrl+C的信號,SIGBUS表示硬體故障的信號;SIGCHLD表示子進程狀態改變信號; SIGKILL表示終止程式運行的信號,等等。信號量程式設計是UNIX下非常重要的一種技術。
GDB有能力在你偵錯工具的時候處理任何一種信號,你可以告訴GDB需要處理哪一種信號。你可以要求GDB收到你所指定的信號時,馬上停住正在運行的程式,以供你進行調試。你可以用GDB的handle命令來完成這一功能。
handle
在GDB中定義一個信號處理。信號可以以SIG開頭或不以SIG開頭,可以用定義一個要處理信號的範圍(如:SIGIO-SIGKILL,表示處理從SIGIO信號到SIGKILL的信號,其中包括SIGIO,SIGIOT,SIGKILL三個信號),也可以使用關鍵字all來標明要處理所有的信號。一旦被調試的程式接收到信號,運行程式馬上會被GDB停住,以供調試。其可以是以下幾種關鍵字的一個或多個。
nostop
當被調試的程式收到信號時,GDB不會停住程式的運行,但會打出消息告訴你收到這種信號。
stop
當被調試的程式收到信號時,GDB會停住你的程式。
print
當被調試的程式收到信號時,GDB會顯示出一條資訊。
noprint
當被調試的程式收到信號時,GDB不會告訴你收到信號的資訊。
pass
noignore
當被調試的程式收到信號時,GDB不處理信號。這表示,GDB會把這個信號交給被偵錯工具會處理。
nopass
ignore
當被調試的程式收到信號時,GDB不會讓被偵錯工具來處理這個信號。

info signals
info handle
查看有哪些信號在被GDB檢測中。
十、執行緒(Thread Stops)
如果你程式是多執行緒的話,你可以定義你的中斷點是否在所有的執行緒上,或是在某個特定的執行緒。GDB很容易幫你完成這一工作。
break thread
break thread if …
linespec指定了中斷點設置在的來源程式的行號。threadno指定了執行緒的ID,注意,這個ID是GDB分配的,你可以通過“info threads”命令來查看正在運行程式中的執行緒資訊。如果你不指定thread 則表示你的中斷點設在所有執行緒上面。你還可以為某執行緒指定中斷點條件。如:

(gdb) break frik.c:13 thread 28 if bartab > lim
當你的程式被GDB停住時,所有的運行執行緒都會被停住。這方便你你查看運行程式的總體情況。而在你恢復程式運行時,所有的執行緒也會被恢復運行。那怕是主進程在被單步調試時。
查看棧信息
—————
當程式被停住了,你需要做的第一件事就是查看程式是在哪裡停住的。當你的程式調用了一個函數,函數的位址,函數參數,函數內的區域變數都會被壓入“棧”(Stack)中。你可以用GDB命令來查看當前的棧中的資訊。
下面是一些查看函式呼叫棧資訊的GDB命令:
backtrace
bt
列印當前的函式呼叫棧的所有資訊。如:

(gdb) bt
#0 func (n=250) at tst.c:6
#1 0x08048524 in main (argc=1, argv=0xbffff674) at tst.c:30
#2 0x400409ed in __libc_start_main () from /lib/libc.so.6

從上可以看出函數的調用棧資訊:__libc_start_main –> main() –> func()

backtrace
bt
n是一個正整數,表示只列印棧頂上n層的棧資訊。
backtrace <-n>
bt <-n>
-n表一個負整數,表示只列印棧底下n層的棧資訊。

如果你要查看某一層的資訊,你需要在切換當前的棧,一般來說,程式停止時,最頂層的棧就是當前棧,如果你要查看棧下面層的詳細資訊,首先要做的是切換當前棧。
frame
f
n是一個從0開始的整數,是棧中的層編號。比如:frame 0,表示棧頂,frame 1,表示棧的第二層。

up
表示向棧的上面移動n層,可以不打n,表示向上移動一層。

down
表示向棧的下面移動n層,可以不打n,表示向下移動一層。

上面的命令,都會列印出移動到的棧層的資訊。如果你不想讓其打出資訊。你可以使用這三個命令:

select-frame 對應於 frame 命令。
up-silently 對應於 up 命令。
down-silently 對應於 down 命令。

查看當前棧層的資訊,你可以用以下GDB命令:
frame 或 f
會列印出這些資訊:棧的層編號,當前的函數名,函數參數值,函數所在檔及行號,函數執行到的語句。

info frame
info f
這個命令會列印出更為詳細的當前棧層的資訊,只不過,大多數都是運行時的內內位址。比如:函數位址,調用函數的位址,被調用函數的位址,目前的函數是由什麼樣的程式語言寫成的、函數參數位址及值、區域變數的地址等等。如:
(gdb) info f
Stack level 0, frame at 0xbffff5d4:
eip = 0x804845d in func (tst.c:6); saved eip 0x8048524
called by frame at 0xbffff60c
source language c.
Arglist at 0xbffff5d4, args: n=250
Locals at 0xbffff5d4, Previous frame’s sp is 0x0
Saved registers:
ebp at 0xbffff5d4, eip at 0xbffff5d8

info args
列印出當前函數的參數名及其值。

info locals
列印出當前函數中所有區域變數及其值。

info catch
列印出當前的函數中的異常處理資訊。

查看來源程式
—————
一、顯示原始程式碼
GDB 可以列印出所偵錯工具的原始程式碼,當然,在程式編譯時一定要加上-g的參數,把來源程式資訊編譯到執行檔中。不然就看不到來源程式了。當程式停下來以後, GDB會報告程式停在了那個檔的第幾行上。你可以用list命令來列印程式的原始程式碼。還是來看一看查看原始程式碼的GDB命令吧。

list
顯示程式第linenum行的周圍的來源程式。

list
顯示函數名為function的函數的來源程式。

list
顯示當前行後面的來源程式。

list –
顯示當前行前面的來源程式。
一般是列印當前行的上5行和下5行,如果顯示函數是是上2行下8行,默認是10行,當然,你也可以定制顯示的範圍,使用下面命令可以設置一次顯示來源程式的行數。
set listsize
設置一次顯示原始程式碼的行數。

show listsize
查看當前listsize的設置。

list命令還有下麵的用法:
list ,
顯示從first行到last行之間的原始程式碼。

list ,
顯示從當前行到last行之間的原始程式碼。

list +
往後顯示原始程式碼。

一般來說在list後面可以跟以下這們的參數:
行號。
<+offset> 當前行號的正偏移量。
<-offset> 當前行號的負偏移量。
哪個文件的哪一行。
函數名。
哪個檔中的哪個函數。
<*address> 程式運行時的語句在記憶體中的位址。

二、搜索原始程式碼
不僅如此,GDB還提供了原始程式碼搜索的命令:
forward-search
search
向前面搜索。
reverse-search
全部搜索。

其中,就是規則運算式,也主一個字串的匹配模式,關於規則運算式,我就不在這裡講了,還請各位查看相關資料。

三、指定原始檔案的路徑
某些時候,用-g編譯過後的執行程式中只是包括了原始檔案的名字,沒有路徑名。GDB提供了可以讓你指定原始檔案的路徑的命令,以便GDB進行搜索。
directory
dir
加一個原始檔案路徑到當前路徑的前面。如果你要指定多個路徑,UNIX下你可以使用“:”,Windows下你可以使用“;”。
directory
清除所有的自訂的原始檔案搜索路徑資訊。

show directories
顯示定義了的原始檔案搜索路徑。

四、原始程式碼的記憶體
你可以使用info line命令來查看原始程式碼在記憶體中的位址。info line後面可以跟“行號”,“函數名”,“檔案名:行號”,“檔案名:函數名”,這個命令會列印出所指定的源碼在運行時的記憶體位址,如:
(gdb) info line tst.c:func
Line 5 of “tst.c” starts at address 0x8048456 and ends at 0x804845d .
還有一個命令(disassemble)你可以查看來源程式的當前執行時的機器碼,這個命令會把目前記憶體中的指令dump出來。如下面的示例表示查看函數func的彙編代碼。
(gdb) disassemble func
Dump of assembler code for function func:
0x8048450 : push %ebp
0x8048451 : mov %esp,%ebp
0x8048453 : sub $0x18,%esp
0x8048456 : movl $0x0,0xfffffffc(%ebp)
0x804845d : movl $0x1,0xfffffff8(%ebp)
0x8048464 : mov 0xfffffff8(%ebp),%eax
0x8048467 : cmp 0x8(%ebp),%eax
0x804846a : jle 0x8048470
0x804846c : jmp 0x8048480
0x804846e : mov %esi,%esi
0x8048470 : mov 0xfffffff8(%ebp),%eax
0x8048473 : add %eax,0xfffffffc(%ebp)
0x8048476 : incl 0xfffffff8(%ebp)
0x8048479 : jmp 0x8048464
0x804847b : nop
0x804847c : lea 0x0(%esi,1),%esi
0x8048480 : mov 0xfffffffc(%ebp),%edx
0x8048483 : mov %edx,%eax
0x8048485 : jmp 0x8048487
0x8048487 : mov %ebp,%esp
0x8048489 : pop %ebp
0x804848a : ret
End of assembler dump.

查看運行時資料
———————

在你偵錯工具時,當程式被停住時,你可以使用print命令(簡寫命令為p),或是同義命令inspect來查看當前程式的運行資料。print命令的格式是:

print
print /
是運算式,是你所調試的程式的語言的運算式(GDB可以調試多種程式設計語言),是輸出的格式,比如,如果要把運算式按16進制的格式輸出,那麼就是/x。

一、運算式
print和許多GDB的命令一樣,可以接受一個運算式,GDB會根據當前的程式運行的資料來計算這個運算式,既然是運算式,那麼就可以是當前程式運行中的const常量、變數、函數等內容。可惜的是GDB不能使用你在程式中所定義的巨集。

運算式的語法應該是當前所調試的語言的語法,由於C/C++是一種大眾型的語言,所以,本文中的例子都是關於C/C++的。(而關於用GDB調試其它語言的章節,我將在後面介紹)

在運算式中,有幾種GDB所支援的操作符,它們可以用在任何一種語言中。

@
是一個和陣列有關的操作符,在後面會有更詳細的說明。

::
指定一個在檔或是一個函數中的變數。

{}
表示一個指向記憶體位址的類型為type的一個物件。

二、程式變數
在GDB中,你可以隨時查看以下三種變數的值:
1、全域變數(所有檔可見的)
2、靜態全域變數(當前檔可見的)
3、區域變數(當前Scope可見的)

如果你的區域變數和全域變數發生衝突(也就是重名),一般情況下是區域變數會隱藏全域變數,也就是說,如果一個全域變數和一個函數中的區域變數同名時,如果當前停止點在函數中,用print顯示出的變數的值會是函數中的區域變數的值。如果此時你想查看全域變數的值時,你可以使用“::”操作符:

file::variable
function::variable
可以通過這種形式指定你所想查看的變數,是哪個檔中的或是哪個函數中的。例如,查看文件f2.c中的全域變數x的值:

gdb) p ‘f2.c'::x

當然,“::”操作符會和C++中的發生衝突,GDB能自動識別“::” 是否C++的操作符,所以你不必擔心在調試C++程式時會出現異常。

另外,需要注意的是,如果你的程式編譯時開啟了優化選項,那麼在用GDB調試被優化過的程式時,可能會發生某些變數不能訪問,或是取值錯誤碼的情況。這個是很正常的,因為優化程式會刪改你的程式,整理你程式的語句順序,剔除一些無意義的變數等,所以在GDB調試這種程式時,運行時的指令和你所編寫指令就有不一樣,也就會出現你所想像不到的結果。對付這種情況時,需要在編譯器時關閉編譯優化。一般來說,幾乎所有的編譯器都支援編譯優化的開關,例如,GNU 的C/C++編譯器GCC,你可以使用“-gstabs”選項來解決這個問題。關於編譯器的參數,還請查看編譯器的使用說明文檔。
三、陣列
有時候,你需要查看一段連續的記憶體空間的值。比如陣列的一段,或是動態分配的資料的大小。你可以使用GDB的“@”操作符,“@”的左邊是第一個記憶體的位址的值,“@”的右邊則你你想查看記憶體的長度。例如,你的程式中有這樣的語句:

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

於是,在GDB調試過程中,你可以以如下命令顯示出這個動態陣列的取值:
p *array@len
@的左邊是陣列的首位址的值,也就是變數array所指向的內容,右邊則是資料的長度,其保存在變數len中,其輸出結果,大約是下麵這個樣子的:

(gdb) p *array@len
$1 = {2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40}
如果是靜態陣列的話,可以直接用print陣列名稱,就可以顯示陣列中所有資料的內容了。

四、輸出格式
一般來說,GDB會根據變數的類型輸出變數的值。但你也可以自訂GDB的輸出的格式。例如,你想輸出一個整數的十六進位,或是二進位來查看這個整型變數的中的位元的情況。要做到這樣,你可以使用GDB的資料顯示格式:

x 按十六進位格式顯示變數。
d 按十進位格式顯示變數。
u 按十六進位格式顯示無符號整型。
o 按八進制格式顯示變數。
t 按二進位格式顯示變數。
a 按十六進位格式顯示變數。
c 按字元格式顯示變數。
f 按浮點數格式顯示變數。
(gdb) p i
$21 = 101

(gdb) p/a i
$22 = 0x65

(gdb) p/c i
$23 = 101 ‘e’

(gdb) p/f i
$24 = 1.41531145e-43

(gdb) p/x i
$25 = 0x65

(gdb) p/t i
$26 = 1100101

五、查看記憶體
你可以使用examine命令(簡寫是x)來查看記憶體位址中的值。x命令的語法如下所示:

x/

n、f、u是可選的參數。

n 是一個正整數,表示顯示記憶體的長度,也就是說從當前位址向後顯示幾個位址的內容。
f 表示顯示的格式,參見上面。如果位址所指的是字串,那麼格式可以是s,如果地十是指令位元址,那麼格式可以是i。
u 表示從當前位址往後請求的位元組數,如果不指定的話,GDB默認是4個bytes。u參數可以用下面的字元來代替,b表示單字節,h表示雙位元組,w表示四位元組,g表示八位元組。當我們指定了位元組長度後,GDB會從指記憶體定的記憶體位址開始,讀寫指定位元組,並把其當作一個值取出來。

表示一個記憶體位址。
n/f/u三個參數可以一起使用。例如:

命令:x/3uh 0x54320 表示,從記憶體位址0x54320讀取內容,h表示以雙位元組為一個單位,3表示三個單位,u表示按十六進位顯示。

六、自動顯示
你可以設置一些自動顯示的變數,當程式停住時,或是在你單步跟蹤時,這些變數會自動顯示。相關的GDB命令是display。

display
display/
display/

expr是一個運算式,fmt表示顯示的格式,addr表示記憶體位址,當你用display設定好了一個或多個運算式後,只要你的程式被停下來,GDB會自動顯示你所設置的這些運算式的值。

格式i和s同樣被display支持,一個非常有用的命令是:

display/i $pc

$pc是GDB的環境變數,表示著指令的位元址,/i則表示輸出格式為機器指令碼,也就是彙編。於是當程式停下後,就會出現原始程式碼和機器指令碼相對應的情形,這是一個很有意思的功能。

下面是一些和display相關的GDB命令:

undisplay
delete display
刪除自動顯示,dnums意為所設置好了的自動顯式的編號。如果要同時刪除幾個,編號可以用空格分隔,如果要刪除一個範圍內的編號,可以用減號表示(如:2-5)

disable display
enable display
disable和enalbe不刪除自動顯示的設置,而只是讓其失效和恢復。

info display
查看display設置的自動顯示的資訊。GDB會打出一張表格,向你報告當然調試中設置了多少個自動顯示設定,其中包括,設置的編號,運算式,是否enable。
七、設置顯示選項
GDB中關於顯示的選項比較多,這裡我只例舉大多數常用的選項。
set print address
set print address on
打開位址輸出,當程式顯示函數資訊時,GDB會顯出函數的參數位址。系統預設為打開的,如:

(gdb) f
#0 set_quotes (lq=0x34c78 “<<", rq=0x34c88 ">>”)
at input.c:530
530 if (lquote != def_lquote)

set print address off
關閉函數的參數位址顯示,如:

(gdb) set print addr off
(gdb) f
#0 set_quotes (lq=”<<", rq=">>”) at input.c:530
530 if (lquote != def_lquote)
show print address
查看當前位址顯示選項是否打開。

set print array
set print array on
打開陣列顯示,打開後當陣列顯示時,每個元素占一行,如果不打開的話,每個元素則以逗號分隔。這個選項預設是關閉的。與之相關的兩個命令如下,我就不再多說了。

set print array off
show print array
set print elements
這個選項主要是設置陣列的,如果你的陣列太大了,那麼就可以指定一個來指定資料顯示的最大長度,當到達這個長度時,GDB就不再往下顯示了。如果設置為0,則表示不限制。

show print elements
查看print elements的選項資訊。

set print null-stop
如果打開了這個選項,那麼當顯示字串時,遇到結束符則停止顯示。這個選項預設為off。

set print pretty on
如果打開printf pretty這個選項,那麼當GDB顯示結構體時會比較漂亮。如:
$1 = {
next = 0x0,
flags = {
sweet = 1,
sour = 1
},
meat = 0x54 “Pork”
}
set print pretty off
關閉printf pretty這個選項,GDB顯示結構體時會如下顯示:

$1 = {next = 0x0, flags = {sweet = 1, sour = 1}, meat = 0x54 “Pork”}

show print pretty
查看GDB是如何顯示結構體的。

set print sevenbit-strings
設置字元顯示,是否按“\nnn”的格式顯示,如果打開,則字串或字元資料按\nnn顯示,如“\065”。

show print sevenbit-strings
查看字元顯示開關是否打開。

set print union
設置顯示結構體時,是否顯式其內的聯合體資料。例如有以下資料結構:

typedef enum {Tree, Bug} Species;
typedef enum {Big_tree, Acorn, Seedling} Tree_forms;
typedef enum {Caterpillar, Cocoon, Butterfly}
Bug_forms;

struct thing {
Species it;
union {
Tree_forms tree;
Bug_forms bug;
} form;
};

struct thing foo = {Tree, {Acorn}};
當打開這個開關時,執行 p foo 命令後,會如下顯示:
$1 = {it = Tree, form = {tree = Acorn, bug = Cocoon}}

當關閉這個開關時,執行 p foo 命令後,會如下顯示:
$1 = {it = Tree, form = {…}}
show print union
查看聯合體資料的顯示方式

set print object
在C++中,如果一個物件指標指向其派生類,如果打開這個選項,GDB會自動按照虛方法調用的規則顯示輸出,如果關閉這個選項的話,GDB就不管虛函數表了。這個選項預設是off。

show print object
查看物件選項的設置。

set print static-members
這個選項表示,當顯示一個C++物件中的內容是,是否顯示其中的靜態資料成員。默認是on。

show print static-members
查看靜態資料成員選項設置。

set print vtbl
當此選項打開時,GDB將用比較規整的格式來顯示虛函數表時。其默認是關閉的。

show print vtbl
查看虛函數顯示格式的選項。

八、歷史記錄
當你用GDB的print查看程式運行時的資料時,你每一個print都會被GDB記錄下來。GDB會以$1, $2, $3 …..這樣的方式為你每一個print命令編上號。於是,你可以使用這個編號訪問以前的運算式,如$1。這個功能所帶來的好處是,如果你先前輸入了一個比較長的運算式,如果你還想查看這個運算式的值,你可以使用歷史記錄來訪問,省去了重複輸入。

九、GDB環境變數
你可以在GDB的調試環境中定義自己的變數,用來保存一些偵錯工具中的運行資料。要定義一個GDB的變數很簡單只需。使用GDB的set命令。GDB的環境變數和UNIX一樣,也是以$起頭。如:

set $foo = *object_ptr

使用環境變數時,GDB會在你第一次使用時創建這個變數,而在以後的使用中,則直接對其賦值。環境變數沒有類型,你可以給環境變數定義任一的類型。包括結構體和陣列。

show convenience
該命令查看當前所設置的所有的環境變數。

這是一個比較強大的功能,環境變數和程式變數的交互使用,將使得程式調試更為靈活便捷。例如:

set $i = 0
print bar[$i++]->contents

於是,當你就不必,print bar[0]->contents, print bar[1]->contents地輸入命令了。輸入這樣的命令後,只用敲回車,重複執行上一條語句,環境變數會自動累加,從而完成逐個輸出的功能。

十、查看寄存器
要查看寄存器的值,很簡單,可以使用如下命令:

info registers
查看寄存器的情況。(除了浮點寄存器)

info all-registers
查看所有寄存器的情況。(包括浮點寄存器)

info registers
查看所指定的寄存器的情況。

寄存器中放置了程式運行時的資料,比如程式當前運行的指令位元址(ip),程式的當前堆疊位址(sp)等等。你同樣可以使用print命令來訪問寄存器的情況,只需要在寄存器名字前加一個$符號就可以了。如:p $eip。

改變程式的執行
———————
一旦使用GDB掛上被偵錯工具,當程式運行起來後,你可以根據自己的調試思路來動態地在GDB中更改當前被偵錯工具的運行線路或是其變數的值,這個強大的功能能夠讓你更好的調試你的程式,比如,你可以在程式的一次運行中走遍程式的所有分支。

一、修改變數值
修改被偵錯工具運行時的變數值,在GDB中很容易實現,使用GDB的print命令即可完成。如:

(gdb) print x=4

x=4這個運算式是C/C++的語法,意為把變數x的值修改為4,如果你當前調試的語言是Pascal,那麼你可以使用Pascal的語法:x:=4。

在某些時候,很有可能你的變數和GDB中的參數衝突,如:

(gdb) whatis width
type = double
(gdb) p width
$4 = 13
(gdb) set width=47
Invalid syntax in expression.
因為,set width是GDB的命令,所以,出現了“Invalid syntax in expression”的設置錯誤,此時,你可以使用set var命令來告訴GDB,width不是你GDB的參數,而是程式的變數名,如:

(gdb) set var width=47

另外,還可能有些情況,GDB並不報告這種錯誤,所以保險起見,在你改變程式變數取值時,最好都使用set var格式的GDB命令。
二、跳轉執行
一般來說,被偵錯工具會按照程式碼的運行順序依次執行。GDB提供了亂序執行的功能,也就是說,GDB可以修改程式的執行順序,可以讓程式執行隨意跳躍。這個功能可以由GDB的jump命令來完:

jump
指定下一條語句的運行點。可以是檔的行號,可以是file:line格式,可以是+num這種偏移量格式。表式著下一條運行語句從哪裡開始。

jump

這裡的
是代碼行的記憶體位址。

注意,jump命令不會改變當前的程式棧中的內容,所以,當你從一個函數跳到另一個函數時,當函數運行完返回時進行彈棧操作時必然會發生錯誤,可能結果還是非常奇怪的,甚至於產生程式Core Dump。所以最好是同一個函數中進行跳轉。

熟悉彙編的人都知道,程式運行時,有一個寄存器用於保存當前代碼所在的記憶體位址。所以,jump命令也就是改變了這個寄存器中的值。於是,你可以使用“set $pc”來更改跳轉執行的位址。如:

set $pc = 0x485

三、產生信號量
使用singal命令,可以產生一個信號量給被調試的程式。如:中斷信號Ctrl+C。這非常方便於程式的調試,可以在程式運行的任意位置設置中斷點,並在該中斷點用GDB產生一個信號量,這種精確地在某處產生信號非常有利程式的調試。

語法是:signal ,UNIX的系統信號量通常從1到15。所以取值也在這個範圍。

single命令和shell的kill命令不同,系統的kill命令發信號給被偵錯工具時,是由GDB截獲的,而single命令所發出一信號則是直接發給被偵錯工具的。

四、強制函數返回
如果你的調試中斷點在某個函數中,並還有語句沒有執行完。你可以使用return命令強制函數忽略還沒有執行的語句並返回。

return
return
使用return命令取消當前函數的執行,並立即返回,如果指定了,那麼該運算式的值會被認作函數的返回值。

五、強制調用函數
call
運算式中可以一是函數,以此達到強制調用函數的目的。並顯示函數的返回值,如果函數返回值是void,那麼就不顯示。

另一個相似的命令也可以完成這一功能——print,print後面可以跟運算式,所以也可以用他來調用函數,print和call的不同是,如果函數返回void,call則不顯示,print則顯示函數返回值,並把該值存入歷史資料中。
在不同語言中使用GDB
——————————
GDB支援下列語言:C, C++, Fortran, PASCAL, Java, Chill, assembly, 和 Modula-2。一般說來,GDB會根據你所調試的程式來確定當然的調試語言,比如:發現檔案名尾碼為“.c”的,GDB會認為是C程式。檔案名尾碼為 “.C, .cc, .cp, .cpp, .cxx, .c++”的,GDB會認為是C++程式。而尾碼是“.f, .F”的,GDB會認為是Fortran程式,還有,尾碼為如果是“.s, .S”的會認為是組合語言。
也就是說,GDB會根據你所調試的程式的語言,來設置自己的語言環境,並讓GDB的命令跟著語言環境的改變而改變。比如一些GDB命令需要用到運算式或變數時,這些運算式或變數的語法,完全是根據當前的語言環境而改變的。例如C/C++中對指標的語法是*p,而在Modula-2中則是p^。並且,如果你當前的程式是由幾種不同語言一同編譯成的,那到在調試過程中,GDB也能根據不同的語言自動地切換語言環境。這種跟著語言環境而改變的功能,真是體貼開發人員的一種設計。

下麵是幾個相關於GDB語言環境的命令:
show language
查看當前的語言環境。如果GDB不能識為你所調試的程式設計語言,那麼,C語言被認為是預設的環境。

info frame
查看當前函數的程式語言。

info source
查看當前文件的程式語言。

如果GDB沒有檢測出當前的程式語言,那麼你也可以手動設置當前的程式語言。使用set language命令即可做到。
當set language命令後什麼也不跟的話,你可以查看GDB所支援的語言種類:

(gdb) set language
The currently understood settings are:

local or auto Automatic setting based on source file
c Use the C language
c++ Use the C++ language
asm Use the Asm language
chill Use the Chill language
fortran Use the Fortran language
java Use the Java language
modula-2 Use the Modula-2 language
pascal Use the Pascal language
scheme Use the Scheme language

於是你可以在set language後跟上被列出來的程式語言名,來設置當前的語言環境。
後記
——
GDB是一個強大的命令列調試工具。大家知道命令列的強大就是在於,其可以形成執行序列,形成腳本。UNIX下的軟體全是命令列的,這給程式開發提代供了極大的便利,命令列軟體的優勢在於,它們可以非常容易的集成在一起,使用幾個簡單的已有工具的命令,就可以做出一個非常強大的功能。

於是UNIX下的軟體比Windows下的軟體更能有機地結合,各自發揮各自的長處,組合成更為強勁的功能。而Windows下的圖形軟體基本上是各自為營,互相不能調用,很不利於各種軟體的相互集成。在這裡並不是要和Windows做個什麼比較,所謂“寸有所長,尺有所短”,圖形化工具還是有不如命令列的地方。(看到這句話時,希望各位千萬再也不要認為我就是“鄙視圖形介面”,和我抬杠了 )

我是根據版本為5.1.1的GDB所寫的這篇文章,所以可能有些功能已被修改,或是又有更為強勁的功能。而且,我寫得非常倉促,寫得比較簡略,並且,其中我已經看到有許多錯別字了(我用五筆,所以錯字讓你看不懂),所以,我在這裡對我文中的差錯表示萬分的歉意。

文中所羅列的GDB的功能時,我只是羅列了一些帶用的GDB的命令和使用方法,其實,我這裡只講述的功能大約只占GDB所有功能的60%吧,詳細的文檔,還是請查看GDB的幫助和使用手冊吧,或許,過段時間,如果我有空,我再寫一篇GDB的高級使用。

我個人非常喜歡GDB的自動調試的功能,這個功能真的很強大,試想,我在UNIX下寫個腳本,讓腳本自動編譯我的程式,被自動調試,並把結果報告出來,調試成功,自動checkin源碼庫。一個命令,編譯帶著調試帶著checkin,多爽啊。只是GDB對自動化調試目前支持還不是很成熟,只能實現半自動化,真心期望著GDB的自動化調試功能的成熟。

如果各位對GDB或是別的技術問題有興趣的話,歡迎和我討論交流。本人目前主要在UNIX下做產品軟體的開發,所以,對UNIX下的軟體發展比較熟悉,當然,不單單是技術,對軟體工程實施,軟體設計,系統分析,專案管理我也略有心得。歡迎大家找我交流,

紅帽Linux上的故障定位技術
http://linux.ctocio.com.cn/484/12168484.shtml
作者:計世網出處:博客
為便於描述問題,將Linux上各種軟體故障定位的情形分成兩類……
  1. 故障定位(Debugging)場景分類
  為便於描述問題,將Linux上各種軟體故障定位的情形分成兩類
  A) 線上故障故障定位
  線上故障定位(online-debugging)就是在故障發生時, 故障所處的作業系統環境仍然可以訪問,故障處理人員可通過console, ssh等方式登錄到作業系統上,在shell上執行各種操作命令或測試程式的方式對故障環境進行觀察,分析,測試,以定位出故障發生的原因
  B) 離線故障定位
  離線故障定位(offline-debugging)就是在故障發生時,故障所處的作業系統環境已經無法正常訪問,但故障發生時系統的全部或部分狀態已經被系統本身所固有或事先設定的方式收集起來,故障處理人員可通過對收集到的故障狀態資訊進行分析,定位出故障發生的原因
  2. 應用進程故障情形及處理
  應用進程的故障一般不會影響作業系統運行環境的正常使用(如果應用代碼的bug導致了內核的crash或hang,則屬於內核存在漏洞),所以可採用線上故障定位的方法,靈活的進行分析. 應用代碼故障的情形有如下幾種:
  A) 進程異常終止
  很多使用者認為進程異常終止情況無從分析,但實際上進程異常終止情況都是有跡可尋的. 所有的進程異常終止行為,都是通過內核發信號給特定進程或進程組實現的. 可分成幾個類型進行描述:
  l SIGKILL. SIGKILL最特殊,因為該信號不可被捕獲,同時SIGKILL不會導致被終止的進程產生core檔, 但如果真正的是由內核中發出的SIGKILL,則內核一定會在dmesg中記錄下資訊. 另外在內核中使用SIGKILL的地方屈指可數,如oom_kill_process()中, 所以通過dmesg記錄並且分析內核中使用SIGKILL的代碼,並不難分析原因
  l SIGQUIT, SIGILL, SIGABRT, SIGBUS, SIGFPE, SIGSEGV. 這幾個信號在保留情況下會終止進程並會產生core檔, 使用者根據core中的stack trace資訊,能直接定位出導致終止信號的代碼位置. 另外, SIGQUIT,SIGABRT一般是由用戶代碼自己使用的,好的代碼一般會記錄日誌. SIGILL, SIGBUS, SIGFPE, SIGSEGV, 都是由內核中產生的,搜索內核源碼,不難列出內核中使用這幾個信號的地方, 如SIGILL 是非法指令,可能是浮點運算產生的代碼被corrupted或文本區域的實體記憶體corruption; SIGBUS多由MCE故障導致; SIGSEGV多由應用代碼的指標變數被corrupted導致. 對於應用的heap或stack的記憶體被corrupted, 可用valgrind工具對應用進行profile, 通常能直接發現導致corruption的代碼
  l SIGINT, SIGPIPE, SIGALRM, SIGTERM. 這幾個信號在保留情況下終止進程但不會產生core檔. 對這幾個信號,建議使用者一定要定義一個handler,以記錄產生問題的上下文. 比較容易忽略的是SIGPIPE, 很多使用者程式在使用select()或poll()時只監聽read/write描述符,不監聽exception描述符,在對方TCP已經關閉的情況下,仍然向socket中寫入,導致SIGPIPE.
  l 對於惡意的代嗎產生的進程終止行為,如合作的一些進程中,A向B發SIGKILL, 而沒做日誌記錄,或者B直接判斷某條件而調用exit(), 也沒有做日誌記錄.在應用代碼量很大的情況下,通過分析代碼定位這種情形也許很難. SystemTap提供了解決這個問題的一個比較好的方法,就是寫用戶層的probes, 追蹤進程對signal(), exit() 等系統調用的使用
  B) 進程阻塞,應用無法正常推進
  這種情況,對於單個被阻塞的進程而言,屬於正常狀態, 但對於包含多個進程的應用整體而言,屬於異常. 應用無法推進,說明其中某一個進程推進的因素出現了問題,導致其他依賴於它的進程也要等待. 分析這種情形需要分析清楚進程或事件之間的依賴關係,及資料的處理流. 首先要用gdb -p 的back trace功能查出各進程阻塞的執行路徑, 以確定每個進程所處在的狀態機的位置. 通常而言,如果只考慮各個進程的狀態,則進程之間可能形成了一種互相依賴的環形關係,如(P1發請求=>P2處理=>P2發反應=>P1再請求=>P2處理=>P2再發反應), 但應用對workload, 一般是按一個個的transaction 或 session的方式進行處理的,每個transaction都有起點和終點, 我們需要用strace, tcpdump 等工具以及應用的執行日誌進行觀察,分析出當前正被處理的transaction所被阻滯的位置,從而找出全部狀態機被阻塞的原因. 導致這種狀態機停止運轉的原因有多個:如和應用通信的遠端出現了問題,後端資料庫/目錄等出現了問題,應用的某個進程或執行緒處於非正常的blocking位置或直接終止,不再正常工作.
  C) 使用者進程形成鎖死
  使用者進程形成鎖死,如果沒有記憶體上的故障,則完全是應用自身的邏輯問題. 鎖死的進程或執行緒之間由於鎖的互相佔有形成了環路。 這種情況發生時,用gdb -p 的back trace的功能能直接確定鎖死的進程全部阻塞在futex()等和鎖相關的系統調用上, 這些調用futex()的路徑可能是mutex, semaphore, conditional variable 等鎖函數. 通過分析call trace 的代碼,能直接確定各進程在執行到該位置時,可能已經持有的全部鎖, 根據這個修改程式的代碼,消除鎖死環路,就可解決問題.
  注意,記憶體故障也可導致假的鎖死的,如實體記憶體故障可直接導致鎖變數的值為-1,所以使用該鎖的進程都會阻塞. 如果是代碼的bug導致的記憶體corruption,可用valgrind工具檢查程式來發現. 但如果是實體記憶體的故障導致的corruption, 則需要硬體的支援,對於高端的PC, 如MCE功能的機器,當實體記憶體故障時能直接產生異常或報告, 但對於低端PC伺服器,除了運行memtest工具進行檢測外,沒有其他方法
  D) 進程長期處於 ‘D’ (UnInterruptible)狀態沒法退出
  這種多是由內核中的故障引起的. 內核在很多執行路徑中會將進程至於’D’的狀態,以確保關鍵的執行路徑不被外部的信號中斷, 導致不必要的內核中資料結構狀態的不一致性. 但一般而言,進程處於 ‘D’ 狀態的時間不會太久, 因為狀態結束的條件(如timer觸發,IO操作完成等)很快會將進程喚醒. 當進程長期處於 ‘D’,關鍵是要找出其阻塞的代碼位置,用 sysrq 的t鍵功能可直接列印出系統中全部睡眠進程的內核執行堆疊,如 echo ‘t’ > /proc/sysrq-trigger, 其中包括出現 ‘D’狀態的進程的內核態堆疊. 找出代碼位置後,一般可直接分析出 ‘D’ 狀態不能退出的原因, 如IO read操作因硬體或nfs故障而不能完成.
  有可能導致 ‘D’ 狀態的原因比較複雜,如‘D’的退出依賴於某變數的值,而該變數的值因某種原因被永久corrupted掉了.
  3. 內核故障情形及處理
  A) 內核panic
  panic是內核最直接的故障報告,發生panic時,內核已經認為故障已經導致作業系統不再具備正常運行的條件了. 當發生panic時,Linux會將所有CPU的中斷和進程調度功能都關掉,所以這時系統是沒有任何反應的,如果使用者啟動的是圖形介面,則在螢幕上也看不到任何關於panic的資訊. 我們通常遇到的,機器沒反應,ping不通的情況,絕大部分都是panic. Panic發生時,內核直接在console上列印導致panic的代碼位置的調用堆疊, 傳統的使用者用串口連接到機器上來收集console上的列印資訊, 但串口這種方式,顯然用起來不方便, 現在的Linux, 如RHEL5,RHEL6, 都採用kdump的方法來收集panic時的資訊. 在配置好kdump的情況下,panic時系統會用kexec載入並切換到一個新的內核上(放置在預先分配的記憶體位置),並用磁片或網路等將系統的全部或部分記憶體資料保存起來.
  用kdump收集到panic的資料後,使用者用crash工具就能直接查看導致panic的代碼路徑.
  panic一般是很直觀的,panic的堆疊資訊能直接反映出導致bug的原因,如MCE故障,NMI故障, 資料結構分配失敗等. 但有時panic是因為內核主動發現了關鍵的資料結構不一致性,這種不一致性是什麼時候,什麼代碼導致的,並不清楚,可能還需要多次測試用SystemTap這樣的工具進行捕捉
  B) 多處理機環境內核執行路徑產生的鎖死
  內核鎖死和panic不一樣,產生鎖死時,內核並不主動的使自己處於掛起狀態. 但內核鎖死發生時,兩個以上的CPU的執行路徑在內核態不能推進了,處於互相阻塞狀態, 而且是100%的佔用CPU(用的spin-lock),直接或間接的導致全部CPU上的進程無法調度. 內核鎖死又分兩種情況:
  l 涉及到中斷上下文的鎖死. 這種情況的鎖死,最少一個CPU上的中斷被遮罩了.系統可能沒法回應ping請求. 由於有一個CPU已經沒法回應中斷,其上的local APIC定時中斷沒法工作,可以用NMI Watchdog的方法來檢測出來(檢查local APIC handler維護的計數器變數),NMI Watchdog可以在其處理常式中調用panic(), 用戶就可以用kdump收集記憶體資訊,從而分析各鎖死CPU上的調用堆疊,查處導致鎖死的邏輯原因.
  l 不涉及中斷上下文的鎖死. 這種情況的鎖死,各CPU上的中斷都是正常的,系統能對ping請求作出反應,這時NMI Watchdog無法被觸發. 在 2.6.16之前的內核中,並沒有一種很好的方法來處理這種情形. 在RHEL5, RHEL6 內核中, 每個CPU上提供了一個watchdog內核執行緒,在鎖死出現的情況下,鎖死CPU上的watchdog內核執行緒沒法被調度(即使它是最高優先順序的即時進程),它就沒法update相應的counter變數,各CPU的NMI Watchdog中斷會週期性的檢查其CPU對應的counter, 發現沒有updated, 會調用panic(),使用者就可用kdump收集記憶體資訊,分析各鎖死CPU上的調用堆疊,查處導致鎖死的邏輯原因.
  C) 內核的oops或warning
  oops 和warning和panic類似的地方是,他們都是因內核發現了不一致而主動報告的異常. 但oops和warning導致的問題嚴重程度要比panic輕很多,以致於內核處理該問題時不需要使系統掛起. 產生oops和warning, 內核通常已經在dmesg中記錄了相當的資訊,特別是oops, 至少會列印出現故障的地方的call trace. Oops也可轉換成panic/kdump來進行offline-debugging, 只要將/proc/sys/kernel下的panic_on_oops變數設置為1就行了.
  產生oops和warning的直接原因有很多,如內核中的segment fault, 或內核發現的某資料結構的counter值不對, 而segment fault 和counter值的變化還有更深層次的原因,通常並不能從內核dmesg的資訊中看出來,解決這種問題的是要用SystemTap進行probe, 如發現某counter的值不對,就用SystemTap做一個probe來記錄所有代碼對該counter的訪問, 然後再進行分析.
  定位oops和warning會比定位應用程式的記憶體訪問故障困難很多,因為在內核並不能象用valgrind去trace應用程式一樣跟蹤資料結構的分配和使用情況.
  2. 其他(硬體相關)故障
  機器自動重啟是一種常見的故障情形,一般是由硬體如實體記憶體故障引起的,軟體的故障只會導致鎖死或panic, 內核中幾乎沒有代碼在發現問題的情況下去reboot機器. 在/proc/sys/kernel目錄下有個參數“panic”, 其值如果設置為非0,則在panic發生若干秒後,內核會重啟機器. 現在高端的PC伺服器,都在努力用軟體來處理實體記憶體故障,如MCA的 “HWPoison”方法會將故障的物理頁隔離起來,Kill掉故障頁所在的進程就可以了,RHEL6現在已經支持 “HWPoison”. 那些不具備MCA能力的機器,實體記憶體故障時,不會產生MCE異常,直接由硬體機制reboot機器
  3. RHEL6 上的Debugging技術介紹
  A) Kdump故障收集和crash分析
  kdump就是用來在內核panic的情況下收集系統記憶體資訊的, 使用者也可在online情況下用sysrq的’c’鍵觸發. Kdump 採用沒有污染的內核來執行dump工作,所以其比以前的diskdump, lkcd方法更可靠. 使用kdump,使用者可選擇將資料dump到本地盤或網路上,也可通過定義makedumpfile的參數過濾要收集的記憶體資訊,已減少kdump所需要的停機時間
  Crash就是對kdump的資訊進行分析的工具.其實際就是gdb的一個wrapper. 使用crash時,最好安裝kernel-debuginfo包,這樣能解析kdump收集的內核資料的符號資訊. 用crash來定位問題的能力,完全取決於用戶對內核代碼的理解和分析能力
  參考 “#>man kdump.conf”, “#>man crash”, “#>man makedumpfile”學習怎樣使用kdump和crash. 訪問 http://ftp.redhat.com可下載debuginfo文件
  B) 用systemTap定位bug
  systemtap 屬於probe類的定位工具,它能對內核或用戶代碼的指定位置進行probe, 當執行到指定位置或訪問指定位置的資料時,使用者定義的probe函數自動執行,可列印出該位置的調用堆疊,參數值,變數值等資訊. systemtap選擇進行probe的位置很靈活,這是systemtap的強大功能所在. Systemtap的probe點可包括如下幾個方面:
  l 內核中全部系統調用,內核及模組中全部函數的入口或出口點
  l 自訂的計時器probe點
  l 內核中任意指定的代碼或資料訪問位置
  l 特定使用者進程中任意制定的代碼或資料訪問位置
  l 各個功能子系統預先設置的若干probe點,如tcp,udp,nfs,signal各子系統都預先設置了很多探測點
  systemTap的腳本用stap指令碼語言來編寫,腳本代碼中調用stap提供的API進行統計,列印資料等工作,關於stap語言提供的API函數,參考 “#> man stapfuncs”. 關於systemTap的功能和使用可參考 “#> man stap”, “#> man stapprobes”
  C) ftrace
  ftrace 是linux內核中利用tracepoints基礎設施實現的事件追蹤機制,它的作用在於能比較清楚的給出在一定時間內系統或進程所執行的活動,如函式呼叫路徑,進程切換流等. Ftrace可用於觀察系統各部分的latency,以便進行即時應用的優化;它也可以通過記錄一段時間內的內核活動來説明定位故障. 如用以下方法可trace某個進程在一端時間的函式呼叫情況
  #> echo “function” > /sys/kernel/debug/tracing/current_tracer
  #> echo “xxx” > /sys/kernel/debug/tracing/set_ftrace_pid
  #> echo 1 > /sys/kernel/debug/tracing/tracing_enabled
  除tracing函式呼叫外,ftrace還可tracing系統的進程切換,喚醒,塊設備訪問,內核資料結構分配等活動. 注意,tracing和profile是不同的,tracing記錄的是一段時間內的全部活動,而不是統計資訊,使用者可以通過/sys/kernel/debug/tracing下的buffer_size_kb設置緩衝區的大小, 以記錄更長時間的資料.