gdb指令碼編寫

查志強發表於2014-06-08

【原文:http://blog.chinaunix.net/uid-21977330-id-3707554.html

關於本主題的前一篇文章“Fun with strace and GDB”提供了相關基礎知識,介紹瞭如何使用這些工具來探索您的系統並附加到已在執行的程式,以瞭解它們正在做的工作。本文繼續討論該偵錯程式的自定義,以使其使用體驗更加個性化和更加高效。

當 GDB(即 GNU Project Debugger)啟動時,它在當前使用者的主目錄中尋找一個名為 .gdbinit 的檔案;如果該檔案存在,則 GDB 就執行該檔案中的所有命令。通常,該檔案用於簡單的配置命令,如設定所需的預設彙編程式格式(Intel? 或 Motorola)或用於顯示輸入和輸出資料的預設基數(十進位制或十六進位制)。它還可以讀取巨集編碼語言,從而允許實現更強大的自定義。該語言遵循如下基本 格式:

define <command>

end
document <command>
<help text>
end

該命令稱為使用者命令。可以將所有其他標準 GDB 命令與流控制指令結合使用並向其傳遞引數,從而建立一種語言,以允許為正在除錯的特定應用程式而自定義偵錯程式的行為。

從簡單開始:清屏

從簡單開始並在此基礎上逐步發展始終是個好主意。啟動 xterm,調出您最喜歡的編輯器,讓我們開始建立一個有用的 .gdbinit 檔案吧!偵錯程式產生的輸出可能非常零亂,根據個人偏好,在使用任何可能產生混亂的工具時,許多人都希望能夠清屏。GDB 沒有用於清屏的內建命令,但它可以呼叫 shell 函式;下面的程式碼跳到偵錯程式之外以使用 cls 命令來清除 xterm 控制檯:

define cls
shell clear
end
document cls
Clears the screen with a simple command.
end

此定義的上半部分(在 define ... end 動詞所界定的範圍內)構成了在呼叫該命令時所執行的程式碼。

此定義的下半部分(在 document ... end 所界定的範圍內)由 GDB 命令直譯器使用,用於在您鍵入 help cls 時顯示與 cls 命令關聯的文字。

在將該程式碼鍵入 .gdbinit 檔案以後,調出 GDB 並輸入 cls 命令。此時螢幕被清除,您所看到的就只有 GDB 提示符。您的 GDB 自定義之旅已經開始了!

文件的重要性

如果輸入 help user 命令,您會看到已在 .gdbinit 檔案中輸入的所有使用者命令的摘要。.gdbinit 使用者定義命令的設計者提供了一個重要特性,您在編寫自己的命令時不應忽略該特性:document ... end 子句。隨著這些命令數量的增加,維護有關命令如何工作的功能文件將變得非常關鍵。

您可能已經遇到過此問題。假設您在若干年前編寫了一些程式碼;後來當您重新處理它(也許是為了修正錯誤或通過新增新特性來修改它)時,您發現自己很難理解您自己的程式碼。優秀的程式設計師習慣將程式碼保持得簡短、簡單和具有良好的文件記錄,以便使其可維護。

適用於程式設計程式碼的一般規則也適用於偵錯程式程式碼。當您在這個最有價值的職業中披荊斬棘地拼搏時,保留仔細的註釋和有良好文件記錄的程式碼將為您帶來豐厚的回報。

GDB 的社群使用

人類通過多種方式學習新知識,包括研究其他人已做的工作。初學的汽車工程師首先開啟他們第一輛車的發動機罩,拔出他們的工具,開始拆卸部件以進行清理和研究。此類活動使他們能夠在保持汽車清潔的同時,還了解了汽車引擎是如何工作的。

初學的電腦科學家也沒有什麼不同,因為他們想了解程式究竟是如何工作的——它們如何與動態庫和本機作業系統互動。用於查 看這些程式如何工作的工具就是偵錯程式。計算機程式設計是一項複雜的活動,通過與志趣相投的人群社群交流、提問並獲得答案,新的電腦科學家能夠滿足他們對知識 的需要。

在全球程式設計社群中,始終存在大量渴求知識的人。他們不再滿足於在計算機上執行程式——他們還想知道得更多。他們想知道這些程式是如何執行的,並樂此不疲地使用最適當的可用工具來探索系統的功能:偵錯程式。通過逆向工程(一種在偵錯程式下執行程式並密切注意它們如何完成所做的工作,從而瞭解程式工作原理的方法),您可以從所研究程式的創作者已完成的工作中學到大量的知識。程式設計中涉及的大量底層詳細資訊沒有相關的文件記錄;瞭解它們的唯一方法就是在它們的實際工作中觀察它們。

逆向工程背上了不應有的壞名聲,彷彿那只是黑客和犯罪分子企圖破壞副本保護系統和編寫蠕蟲及病毒來對計算機世界造成損害才 會幹的事情。雖然存在這樣的人,但絕大多數使用偵錯程式和逆向工程來研究程式如何工作的人都是當前和將來的軟體工程師,他們希望並需要知道這些程式是如何工 作的。他們已形成了線上社群以共享他們的知識和發現;抵制該活動是非建設性的,會阻礙電腦科學的未來發展。

本文中定義的許多使用者函式就來自於此類知識渴求者的社群。如果希望瞭解有關他們的更多資訊,建議您研究本文的參考資料部分所提到的網站。

斷點別名

許多 GDB 命令太繁瑣,這是眾所周知的事實。儘管可以對它們進行縮寫,但是 GDB 巨集語言允許實現進一步的簡化。諸如 info breakpoints 這樣的命令可以變得像 bpl 一樣簡單。清單 1 顯示了一組此類簡單和高度有用的斷點別名 使用者命令,您可以將它們新增到不斷增長的 .gdbinit 檔案中。


清單 1:斷點別名命令
                
define bpl
info breakpoints
end
document bpl
List breakpoints
end

define bp
set $SHOW_CONTEXT = 1
break * $arg0
end
document bp
Set a breakpoint on address
Usage: bp addr
end

define bpc
clear $arg0
end
document bpc
Clear breakpoint at function/address
Usage: bpc addr
end

define bpe
enable $arg0
end
document bpe
Enable breakpoint #
Usage: bpe num
end

define bpd
disable $arg0
end
document bpd
Disable breakpoint #
Usage: bpd num
end

define bpt
set $SHOW_CONTEXT = 1
tbreak $arg0
end
document bpt
Set a temporary breakpoint on address
Usage: bpt addr
end

define bpm
set $SHOW_CONTEXT = 1
awatch $arg0
end
document bpm
Set a read/write breakpoint on address
Usage: bpm addr
end

一旦您習慣了使用斷點別名命令,除錯會話就變得更有價值了;這些命令極大地提高了偵錯程式的效率,因為它能使您事半功倍。

顯示程式資訊

使用者定義的 GDB 命令可由其他使用者定義的命令呼叫,從而為各方都帶來更高的效率。這就是程式語言的遞增性質——編寫底層函式,逐漸由更高層的函式呼叫,直到您只需最少的工 作即可讓那些工具方便地完成您想要它們完成的任務。要整合到 .gdbinit 檔案中的下一組 GDB 定義將在程式被呼叫時顯示有用的程式資訊,如清單 2 所示。


清單 2: 程式資訊命令
                
define argv
show args
end
document argv
Print program arguments
end

define stack
info stack
end
document stack
Print call stack
end

define frame
info frame
info args
info locals
end
document frame
Print stack frame
end

define flags
if (($eflags >> 0xB) & 1 )
printf "O "
else
printf "o "
end
if (($eflags >> 0xA) & 1 )
printf "D "
else
printf "d "
end
if (($eflags >> 9) & 1 )
printf "I "
else
printf "i "
end
if (($eflags >> 8) & 1 )
printf "T "
else
printf "t "
end
if (($eflags >> 7) & 1 )
printf "S "
else
printf "s "
end
if (($eflags >> 6) & 1 )
printf "Z "
else
printf "z "
end
if (($eflags >> 4) & 1 )
printf "A "
else
printf "a "
end
if (($eflags >> 2) & 1 )
printf "P "
else
printf "p "
end
if ($eflags & 1)
printf "C "
else
printf "c "
end
printf "\n"
end
document flags
Print flags register
end

define eflags
printf "     OF <%d>  DF <%d>  IF <%d>  TF <%d>",\
        (($eflags >> 0xB) & 1 ), (($eflags >> 0xA) & 1 ), \
        (($eflags >> 9) & 1 ), (($eflags >> 8) & 1 )
printf "  SF <%d>  ZF <%d>  AF <%d>  PF <%d>  CF <%d>\n",\
        (($eflags >> 7) & 1 ), (($eflags >> 6) & 1 ),\
        (($eflags >> 4) & 1 ), (($eflags >> 2) & 1 ), ($eflags & 1)
printf "     ID <%d>  VIP <%d> VIF <%d> AC <%d>",\
        (($eflags >> 0x15) & 1 ), (($eflags >> 0x14) & 1 ), \
        (($eflags >> 0x13) & 1 ), (($eflags >> 0x12) & 1 )
printf "  VM <%d>  RF <%d>  NT <%d>  IOPL <%d>\n",\
        (($eflags >> 0x11) & 1 ), (($eflags >> 0x10) & 1 ),\
        (($eflags >> 0xE) & 1 ), (($eflags >> 0xC) & 3 )
end
document eflags
Print entire eflags register
end

define reg
printf "     eax:%08X ebx:%08X  ecx:%08X ",  $eax, $ebx, $ecx
printf " edx:%08X     eflags:%08X\n",  $edx, $eflags
printf "     esi:%08X edi:%08X  esp:%08X ",  $esi, $edi, $esp
printf " ebp:%08X     eip:%08X\n", $ebp, $eip
printf "     cs:%04X  ds:%04X  es:%04X", $cs, $ds, $es
printf "  fs:%04X  gs:%04X  ss:%04X    ", $fs, $gs, $ss
flags
end
document reg
Print CPU registers
end

define func
info functions
end
document func
Print functions in target
end

define var
info variables
end
document var
Print variables (symbols) in target
end

define lib
info sharedlibrary
end
document lib
Print shared libraries linked to target
end

define sig
info signals
end
document sig
Print signal actions for target
end

define thread
info threads
end
document thread
Print threads in target
end

define u
info udot
end
document u
Print kernel 'user' struct for target
end

define dis
disassemble $arg0
end
document dis
Disassemble address
Usage: dis addr
end

十六進位制和 ASCII 轉儲命令

要整合進 .gdbinit 檔案中的下一組定義包括增強的十六進位制和 ASCII 轉儲函式,如清單 3 所示。程式設計師注意:若想建立卓越的軟體,則應新增巨集程式設計功能,從而允許使用者社群能夠增強您的工具以適應他們自己的偏好。GDB 就是一個卓越的軟體!


清單 3: 十六進位制和 ASCII 轉儲命令
                
define ascii_char
set $_c=*(unsigned char *)($arg0)
if ( $_c < 0x20 || $_c > 0x7E )
printf "."
else
printf "%c", $_c
end
end
document ascii_char
Print the ASCII value of arg0 or '.' if value is unprintable
end

define hex_quad
printf "%02X %02X %02X %02X  %02X %02X %02X %02X",                          \
               *(unsigned char*)($arg0), *(unsigned char*)($arg0 + 1),      \
               *(unsigned char*)($arg0 + 2), *(unsigned char*)($arg0 + 3),  \
               *(unsigned char*)($arg0 + 4), *(unsigned char*)($arg0 + 5),  \
               *(unsigned char*)($arg0 + 6), *(unsigned char*)($arg0 + 7)
end
document hex_quad
Print eight hexadecimal bytes starting at arg0
end

define hexdump
printf "%08X : ", $arg0
hex_quad $arg0
printf " - "
hex_quad ($arg0+8)
printf " "

ascii_char ($arg0)
ascii_char ($arg0+1)
ascii_char ($arg0+2)
ascii_char ($arg0+3)
ascii_char ($arg0+4)
ascii_char ($arg0+5)
ascii_char ($arg0+6)
ascii_char ($arg0+7)
ascii_char ($arg0+8)
ascii_char ($arg0+9)
ascii_char ($arg0+0xA)
ascii_char ($arg0+0xB)
ascii_char ($arg0+0xC)
ascii_char ($arg0+0xD)
ascii_char ($arg0+0xE)
ascii_char ($arg0+0xF)

printf "\n"
end
document hexdump
Display a 16-byte hex/ASCII dump of arg0
end

define ddump
printf "[%04X:%08X]------------------------", $ds, $data_addr
printf "---------------------------------[ data]\n"
set $_count=0
while ( $_count < $arg0 )
set $_i=($_count*0x10)
hexdump ($data_addr+$_i)
set $_count++
end
end
document ddump
Display $arg0 lines of hexdump for address $data_addr
end

define dd
if ( ($arg0 & 0x40000000) || ($arg0 & 0x08000000) || ($arg0 & 0xBF000000) )
set $data_addr=$arg0
ddump 0x10
else
printf "Invalid address: %08X\n", $arg0
end
end
document dd
Display 16 lines of a hex dump for $arg0
end

define datawin
if ( ($esi & 0x40000000) || ($esi & 0x08000000) || ($esi & 0xBF000000) )
set $data_addr=$esi
else
if ( ($edi & 0x40000000) || ($edi & 0x08000000) || ($edi & 0xBF000000) )
set $data_addr=$edi
else
if ( ($eax & 0x40000000) || ($eax & 0x08000000) || \
      ($eax & 0xBF000000) )
set $data_addr=$eax
else
set $data_addr=$esp
end
end
end
 ddump 2
end
document datawin
Display esi, edi, eax, or esp in the data window
end

流程上下文命令

最後,當您除錯正在執行的程式時,獲得程式上下文的總體檢視通常是必要的。清單 4 中有用的程式上下文命令是使用前面定義的資料轉儲函式來構建的。


清單 4: 程式上下文命令
                
define context
printf "_______________________________________"
printf "________________________________________\n"
reg
printf "[%04X:%08X]------------------------", $ss, $esp
printf "---------------------------------[stack]\n"
hexdump $sp+0x30
hexdump $sp+0x20
hexdump $sp+0x10
hexdump $sp
datawin
printf "[%04X:%08X]------------------------", $cs, $eip
printf "---------------------------------[ code]\n"
x /6i $pc
printf "---------------------------------------"
printf "---------------------------------------\n"
end
document context
Print regs, stack, ds:esi, and disassemble cs:eip
end

define context-on
set $SHOW_CONTEXT = 1
end
document context-on
Enable display of context on every program stop
end

define context-off
set $SHOW_CONTEXT = 1
end
document context-on
Disable display of context on every program stop
end

# Calls "context" at every breakpoint.
define hook-stop
  context
end

# Init parameters
set output-radix 0x10
set input-radix 0x10
set disassembly-flavor intel

hook-stop 是 GDB 在每次發生斷點事件時呼叫的特殊定義。此例中生成了 context 清單,以便您能清楚看到處理器執行每條指令的結果。

具有新功能的除錯會話

讓我們試驗一下這組新工具,以瞭解它們在除錯我們的“老朋友”(由 IBM developerWorks 供稿作家 Nigel Griffiths 編寫的 nweb 伺服器程式碼)時的工作情況。(請參見參考資料部分以獲得指向 Nigel 的文章“nweb: a tiny, safe Web server (static pages only)”的連結。)

在將 es-nweb.zip 檔案下載到 $HOME/downloads 目錄後,鍵入如下命令以提取、編譯和執行 nweb。(請注意,這裡假設您是將該程式編譯到中央處理單元 (CPU) 為 Intel Pentium 的 Linux? 工作站——該 .gdbinit 程式碼是僅為 Intel Pentium 型別的處理器和相容處理器而編寫的。)

$ cd src
$ mkdir nweb
$ cd nweb
$ unzip $HOME/downloads/es-nweb.zip
$ gcc -ggdb -O -DLINUX nweb.c -o nweb
$ ./nweb 9090 $HOME/src/nweb &

注意:此示例中的 -ggdb 選項與 Nigel 文章中所述的選項不同,因為它告訴 GNU 編譯器集 (GCC) 優化該程式,以便於使用 GDB 來進行除錯。

接下來,為驗證 nweb 伺服器正在執行,可使用 ps 命令來對它進行檢查。

$ ps
  PID TTY          TIME CMD
 2913 pts/5    00:00:00 bash
 4009 pts/5    00:00:00 nweb
 4011 pts/5    00:00:00 ps

最後,在您的計算機上啟動 Web 瀏覽器,並在位址列鍵入:http://localhost:9090。

下一步是啟動 GDB,並與以前一樣附加到當前執行的 nweb 例項,如 清單 5 所示。


清單 5:執行 GDB
                
$ gdb --quiet
(gdb) attach 4009
Attaching to process 4009
Reading symbols from /home/bill/src/nweb/nweb...done.
Reading symbols from /lib/tls/libc.so.6...done.
Loaded symbols for /lib/tls/libc.so.6
Reading symbols from /lib/ld-linux.so.2...done.
Loaded symbols for /lib/ld-linux.so.2
_______________________________________________________________________________
     eax:FFFFFE00 ebx:00000005  ecx:BFFFF680  edx:00000001     eflags:00000246
     esi:00000005 edi:00000000  esp:BFFFF66C  ebp:BFFFF6A8     eip:FFFFE410
     cs:0073  ds:007B  es:007B  fs:0000  gs:0033  ss:007B    o d I t s Z a P c
[007B:BFFFF66C]---------------------------------------------------------[stack]
BFFFF69C : 14 0A 13 42  60 53 01 40 - 24 8F 04 08  C8 F6 FF BF ...B`S.@$.......
BFFFF68C : A6 8E 04 08  14 0A 13 42 - 70 C6 00 40  10 00 00 00 .......Bp..@....
BFFFF67C : 82 8E 04 08  00 00 00 00 - C4 C6 04 08  98 F6 FF BF ................
BFFFF66C : A8 F6 FF BF  01 00 00 00 - 80 F6 FF BF  81 EA 0D 42 ...............B
[007B:FFFFFE00]---------------------------------------------------------[ data]
FFFFFE00 : Error while running hook_stop:
Cannot access memory at address 0xfffffe00
0xffffe410 in ?? ()
(gdb)

-quiet 選項告訴 GDB 偵錯程式僅顯示其提示符,而不要顯示所有其他通常顯示的啟動資訊。如果需要顯示額外的文字資訊,可以去掉 -quiet 選項。

attach 4009 命令開始對當前正在執行的 nweb 伺服器的除錯,並且 GDB 偵錯程式通過讀取有關該程式的所有符號資訊來做出同樣方式的響應。

您將會注意到 context 程式碼執行並顯示大量有關當前程式的有用資訊,但它不能訪問資料段中的記憶體。這不是個嚴重問題,並且應該忽略它。有時,保護模式處理器的保護方案不允許您看到您可能希望看到的所有內容。在此情況下,該問題並不重要。

下一步,使用 info 命令來列出有關您所研究的程式的資訊(請參見清單 6)。


清單 6:info 命令列出程式資訊
                
(gdb) info proc
process 4009
cmdline = './nweb'
cwd = '/home/bill/src/nweb'
exe = '/home/bill/src/nweb/nweb'
(gdb)

在其執行過程中進行觀察

由於您所觀察的是一個實際執行的程式,所以可以設定相應的斷點,然後在它響應瀏覽器請求並向發出請求的瀏覽器傳輸 .html 和 .jpg 檔案時,對該程式進行觀察清單 7 表明瞭如何完成該任務。


清單 7:設定斷點
                
(gdb) b 188
Breakpoint 1 at 0x8048e70: file nweb.c, line 188.
(gdb) commands 1
Type commands for when breakpoint 1 is hit, one per line.
End with a line saying just "end".
>continue
>end
(gdb) c
Continuing.

此時,GDB 除錯工具已設定為在 nweb 伺服器接受 瀏覽器請求時所在的行中斷,偵錯程式將簡單地顯示請求並繼續處理其他請求,而不會中斷正在執行的程式。重新整理幾次瀏覽器中的 http://localhost:9090/ 頁面,可以觀察到,GDB 偵錯程式顯示了斷點並繼續執行。

在重新整理瀏覽器頁面的同時,您應該看到如清單 8 所示的斷點資訊,在 GDB 偵錯程式 xterm 中滾動輸出。還可以通過按 Ctrl+C 來停止在 nweb 伺服器中的除錯。停止跟蹤以後,可以通過鍵入 quit 命令來退出 GDB 偵錯程式。


清單 8: GDB 偵錯程式 xterm 中的斷點資訊
                
_______________________________________________________________________________
     eax:00000000 ebx:00000001  ecx:00000000  edx:00000001     eflags:00000206
     esi:00000006 edi:00000000  esp:BFFFF690  ebp:BFFFF6A8     eip:08048E70
     cs:0073  ds:007B  es:007B  fs:0000  gs:0033  ss:007B    o d I t s z a P c
[007B:BFFFF690]---------------------------------------------------------[stack]
BFFFF6C0 : 03 00 00 00  D4 86 04 08 - 00 00 00 00  F5 86 04 08 ................
BFFFF6B0 : 03 00 00 00  F4 F6 FF BF - 04 F7 FF BF  2C 58 01 40 ............,X.@
BFFFF6A0 : 60 53 01 40  24 8F 04 08 - C8 F6 FF BF  04 55 01 42 `S.@$........U.B
BFFFF690 : 14 0A 13 42  70 C6 00 40 - 10 00 00 00  14 0A 13 42 ...Bp..@.......B
[007B:BFFFF690]---------------------------------------------------------[ data]
BFFFF690 : 14 0A 13 42  70 C6 00 40 - 10 00 00 00  14 0A 13 42 ...Bp..@.......B
BFFFF6A0 : 60 53 01 40  24 8F 04 08 - C8 F6 FF BF  04 55 01 42 `S.@$........U.B
[0073:08048E70]---------------------------------------------------------[ code]
0x8048e70 <main+718>:   sub    esp,0x4
0x8048e73 <main+721>:   lea    eax,[ebp-16]
0x8048e76 <main+724>:   push   eax
0x8048e77 <main+725>:   push   0x804c6c4
0x8048e7c <main+730>:   push   edi
0x8048e7d <main+731>:   call   0x80485e4 <accept>
------------------------------------------------------------------------------
Breakpoint 1, main (argc=3, argv=0x1) at nweb.c:188
188           if((socketfd = accept(listenfd, (struct sockaddr *)&cli_addr, &length)) < 0)
Program received signal SIGINT, Interrupt.
0xffffe410 in ?? ()
(gdb) quit
The program is running.  Quit anyway (and detach it)? (y or n) y
Detaching from program: /home/bill/src/nweb/nweb, process 4009
$

可以看到,context 函式所顯示的資訊遠比您通常使用預設 GDB hook_stop 函式所看到的資訊更詳細。(您還會注意到,現在也可以訪問資料段了。)使用這些 GDB 增強,您可以看到每次到達斷點和執行每步操作時的確切 CPU 狀態。單步執行每個命令並觀察暫存器和記憶體值如何受影響,這也是學習 Intel 機器語言命令基礎知識的理想方法。

與所有程式一樣,.gdbinit 檔案中的程式碼提供了無窮無盡的增強和改進機會。無論如何,這都不是結束!強烈建議您使用這裡描述的命令,併為不斷增長的 .gdbinit 自定義集新增更多命令。在您研究和使用這些工具時,請與更廣泛的社群分享它們,以使每個人都能增長知識。

結束語

應該有更多的人來從事有關程式設計工具如何工作的深入研究,各抒己見、博採眾長,為尋求此類知識的整個使用者社群作出貢獻。訪問一些此類線上社群,甚至考慮組建您自己的社群,以便能夠更快地實現將來的技術創新。


相關文章