掌握 Linux 調試技術 使用 GDB 調試 Linux 軟件

簡介:?您可以用各種方法來監控運行著的用戶空間程序:可以為其運行調試器并單步調試該程序,添加打印語句,或者添加工具來分析程序。本文描述了幾種可以用來調試在 Linux 上運行的程序的方法。我們將回顧四種調試問題的情況,這些問題包括段錯誤,內存溢出和泄漏,還有掛起。


本文討論了四種調試 Linux 程序的情況。在第 1 種情況中,我們使用了兩個有內存分配問題的樣本程序,使用 MEMWATCH 和 Yet Another Malloc Debugger(YAMD)工具來調試它們。在第 2 種情況中,我們使用了 Linux 中的 strace 實用程序,它能夠跟蹤系統調用和信號,從而找出程序發生錯誤的地方。在第 3 種情況中,我們使用 Linux 內核的 Oops 功能來解決程序的段錯誤,并向您展示如何設置內核源代碼級調試器(kernel source level debugger,kgdb),以使用 GNU 調試器(GNU debugger,gdb)來解決相同的問題;kgdb 程序是使用串行連接的 Linux 內核遠程 gdb。在第 4 種情況中,我們使用 Linux 上提供的魔術鍵控順序(magic key sequence)來顯示引發掛起問題的組件的信息。

常見調試方法

當您的程序中包含錯誤時,很可能在代碼中某處有一個條件,您認為它為真(true),但實際上是假(false)。找出錯誤的過程也就是在找出錯誤后推翻以前一直確信為真的某個條件過程。

以下幾個示例是您可能確信成立的條件的一些類型:

  • 在源代碼中的某處,某變量有特定的值。
  • 在給定的地方,某個結構已被正確設置。
  • 對于給定的 if-then-else 語句, if 部分就是被執行的路徑。
  • 當子例程被調用時,該例程正確地接收到了它的參數。

找出錯誤也就是要確定上述所有情況是否存在。如果您確信在子例程被調用時某變量應該有特定的值,那么就檢查一下情況是否如此。如果您相信 if 結構會被執行,那么也檢查一下情況是否如此。通常,您的假設都會是正確的,但最終您會找到與假設不符的情況。結果,您就會找出發生錯誤的地方。

調試是您無法逃避的任務。進行調試有很多種方法,比如將消息打印到屏幕上、使用調試器,或只是考慮程序執行的情況并仔細地揣摩問題所在。

在修正問題之前,您必須找出它的源頭。舉例來說,對于段錯誤,您需要了解段錯誤發生在代碼的哪一行。一旦您發現了代碼中出錯的行,請確定該方法中變量的值、方法被調用的方式以及關于錯誤如何發生的詳細情況。使用調試器將使找出所有這些信息變得很簡單。如果沒有調試器可用,您還可以使用其它的工具。(請注意,產品環境中可能并不提供調試器,而且 Linux 內核沒有內建的調試器。)

實用的內存和內核工具

您可以使用 Linux 上的調試工具,通過各種方式跟蹤用戶空間和內核問題。請使用下面的工具和技術來構建和調試您的源代碼:
用戶空間工具

  • 內存工具:MEMWATCH 和 YAMD
  • strace
  • GNU 調試器(gdb)
  • 魔術鍵控順序

內核工具

  • 內核源代碼級調試器(kgdb)
  • 內建內核調試器(kdb)
  • Oops

本文將討論一類通過人工檢查代碼不容易找到的問題,而且此類問題只在很少見的情況下存在。內存錯誤通常在多種情況同時存在時出現,而且您有時只能在部署程序之后才能發現內存錯誤。

第 1 種情況:內存調試工具

C 語言作為 Linux 系統上標準的編程語言給予了我們對動態內存分配很大的控制權。然而,這種自由可能會導致嚴重的內存管理問題,而這些問題可能導致程序崩潰或隨時間的推移導致性能降級。

內存泄漏(即 malloc() 內存在對應的 free() 調用執行后永不被釋放)和緩沖區溢出(例如對以前分配到某數組的內存進行寫操作)是一些常見的問題,它們可能很難檢測到。這一部分將討論幾個調試工具,它們極大地簡化了檢測和找出內存問題的過程。

MEMWATCH

MEMWATCH 由 Johan Lindh 編寫,是一個開放源代碼 C 語言內存錯誤檢測工具,您可以自己下載它(請參閱本文后面部分的 參考資料)。只要在代碼中添加一個頭文件并在 gcc 語句中定義了 MEMWATCH 之后,您就可以跟蹤程序中的內存泄漏和錯誤了。MEMWATCH 支持 ANSI C,它提供結果日志紀錄,能檢測雙重釋放(double-free)、錯誤釋放(erroneous free)、沒有釋放的內存(unfreed memory)、溢出和下溢等等。

清單 1. 內存樣本(test1.c)

#include <stdlib.h>
#include <stdio.h>
#include "memwatch.h"
int main(void)
{char *ptr1;char *ptr2;ptr1 = malloc(512);ptr2 = malloc(512);ptr2 = ptr1;free(ptr2);free(ptr1);
}

清單 1 中的代碼將分配兩個 512 字節的內存塊,然后指向第一個內存塊的指針被設定為指向第二個內存塊。結果,第二個內存塊的地址丟失,從而產生了內存泄漏。

現在我們編譯清單 1 的 memwatch.c。下面是一個 makefile 示例:

test1

gcc -DMEMWATCH -DMW_STDIO test1.c memwatch
c -o test1

當您運行 test1 程序后,它會生成一個關于泄漏的內存的報告。清單 2 展示了示例 memwatch.log 輸出文件。

清單 2. test1 memwatch.log 文件

  MEMWATCH 2.67 Copyright (C) 1992-1999 Johan Lindh
...
double-free: <4> test1.c(15), 0x80517b4 was freed from test1.c(14)
...
unfreed: <2> test1.c(11), 512 bytes at 0x80519e4
{FE FE FE FE FE FE FE FE FE FE FE FE ..............}
Memory usage statistics (global):N)umber of allocations made: 	2L)argest memory usage : 	1024T)otal of all alloc() calls: 	1024U)nfreed bytes totals : 	512

MEMWATCH 為您顯示真正導致問題的行。如果您釋放一個已經釋放過的指針,它會告訴您。對于沒有釋放的內存也一樣。日志結尾部分顯示統計信息,包括泄漏了多少內存,使用了多少內存,以及總共分配了多少內存。

YAMD

YAMD 軟件包由 Nate Eldredge 編寫,可以查找 C 和 C++ 中動態的、與內存分配有關的問題。在撰寫本文時,YAMD 的最新版本為 0.32。請下載 yamd-0.32.tar.gz(請參閱參考資料)。執行make 命令來構建程序;然后執行 make install 命令安裝程序并設置工具。

一旦您下載了 YAMD 之后,請在 test1.c 上使用它。請刪除 #include memwatch.h 并對 makefile 進行如下小小的修改:

使用 YAMD 的 test1

gcc -g test1.c -o test1

清單 3 展示了來自 test1 上的 YAMD 的輸出。

清單 3. 使用 YAMD 的 test1 輸出

YAMD version 0.32
Executable: /usr/src/test/yamd-0.32/test1
...
INFO: Normal allocation of this block
Address 0x40025e00, size 512
...
INFO: Normal allocation of this block
Address 0x40028e00, size 512
...
INFO: Normal deallocation of this block
Address 0x40025e00, size 512
...
ERROR: Multiple freeing At
free of pointer already freed
Address 0x40025e00, size 512
...
WARNING: Memory leak
Address 0x40028e00, size 512
WARNING: Total memory leaks:
1 unfreed allocations totaling 512 bytes
*** Finished at Tue ... 10:07:15 2002
Allocated a grand total of 1024 bytes 2 allocations
Average of 512 bytes per allocation
Max bytes allocated at one time: 1024
24 K alloced internally / 12 K mapped now / 8 K max
Virtual program size is 1416 K
End.

YAMD 顯示我們已經釋放了內存,而且存在內存泄漏。讓我們在清單 4 中另一個樣本程序上試試 YAMD。

清單 4. 內存代碼(test2.c)

#include <stdlib.h>
#include <stdio.h>
int main(void)
{char *ptr1;char *ptr2;char *chptr;int i = 1;ptr1 = malloc(512);ptr2 = malloc(512);chptr = (char *)malloc(512);for (i; i <= 512; i++) {chptr[i] = 'S';}	ptr2 = ptr1;free(ptr2);free(ptr1);free(chptr);
}

您可以使用下面的命令來啟動 YAMD:

./run-yamd /usr/src/test/test2/test2

清單 5 顯示了在樣本程序 test2 上使用 YAMD 得到的輸出。YAMD 告訴我們在 for 循環中有“越界(out-of-bounds)”的情況。

清單 5. 使用 YAMD 的 test2 輸出

Running /usr/src/test/test2/test2
Temp output to /tmp/yamd-out.1243
*********
./run-yamd: line 101: 1248 Segmentation fault (core dumped)
YAMD version 0.32
Starting run: /usr/src/test/test2/test2
Executable: /usr/src/test/test2/test2
Virtual program size is 1380 K
...
INFO: Normal allocation of this block
Address 0x40025e00, size 512
...
INFO: Normal allocation of this block
Address 0x40028e00, size 512
...
INFO: Normal allocation of this block
Address 0x4002be00, size 512
ERROR: Crash
...
Tried to write address 0x4002c000
Seems to be part of this block:
Address 0x4002be00, size 512
...
Address in question is at offset 512 (out of bounds)
Will dump core after checking heap.
Done.

MEMWATCH 和 YAMD 都是很有用的調試工具,它們的使用方法有所不同。對于 MEMWATCH,您需要添加包含文件 memwatch.h 并打開兩個編譯時間標記。對于鏈接(link)語句,YAMD 只需要-g 選項。

Electric Fence

多數 Linux 分發版包含一個 Electric Fence 包,不過您也可以選擇下載它。Electric Fence 是一個由 Bruce Perens 編寫的malloc() 調試庫。它就在您分配內存后分配受保護的內存。如果存在 fencepost 錯誤(超過數組末尾運行),程序就會產生保護錯誤,并立即結束。通過結合 Electric Fence 和 gdb,您可以精確地跟蹤到哪一行試圖訪問受保護內存。Electric Fence 的另一個功能就是能夠檢測內存泄漏。

第 2 種情況:使用 strace

strace 命令是一種強大的工具,它能夠顯示所有由用戶空間程序發出的系統調用。strace 顯示這些調用的參數并返回符號形式的值。strace 從內核接收信息,而且不需要以任何特殊的方式來構建內核。將跟蹤信息發送到應用程序及內核開發者都很有用。在清單 6 中,分區的一種格式有錯誤,清單顯示了 strace 的開頭部分,內容是關于調出創建文件系統操作(mkfs )的。strace 確定哪個調用導致問題出現。

清單 6. mkfs 上 strace 的開頭部分

  execve("/sbin/mkfs.jfs", ["mkfs.jfs", "-f", "/dev/test1"], &...open("/dev/test1", O_RDWR|O_LARGEFILE) = 4stat64("/dev/test1", {st_mode=&, st_rdev=makedev(63, 255), ...}) = 0ioctl(4, 0x40041271, 0xbfffe128) = -1 EINVAL (Invalid argument)write(2, "mkfs.jfs: warning - cannot setb" ..., 98mkfs.jfs: warning -cannot set blocksize on block device /dev/test1: Invalid argument )= 98stat64("/dev/test1", {st_mode=&, st_rdev=makedev(63, 255), ...}) = 0open("/dev/test1", O_RDONLY|O_LARGEFILE) = 5ioctl(5, 0x80041272, 0xbfffe124) = -1 EINVAL (Invalid argument)write(2, "mkfs.jfs: can\'t determine device"..., ..._exit(1)= ?

清單 6 顯示 ioctl 調用導致用來格式化分區的 mkfs 程序失敗。 ioctl BLKGETSIZE64 失敗。(BLKGET-SIZE64 在調用ioctl 的源代碼中定義。) BLKGETSIZE64 ioctl 將被添加到 Linux 中所有的設備,而在這里,邏輯卷管理器還不支持它。因此,如果BLKGETSIZE64 ioctl 調用失敗,mkfs 代碼將改為調用較早的ioctl 調用;這使得 mkfs 適用于邏輯卷管理器。

第 3 種情況:使用 gdb 和 Oops

您可以從命令行使用 gdb 程序(Free Software Foundation 的調試器)來找出錯誤,也可以從諸如 Data Display Debugger(DDD)這樣的幾個圖形工具之一使用 gdb 程序來找出錯誤。您可以使用 gdb 來調試用戶空間程序或 Linux 內核。這一部分只討論從命令行運行 gdb 的情況。

使用 gdb program name 命令啟動 gdb。gdb 將載入可執行程序符號并顯示輸入提示符,讓您可以開始使用調試器。您可以通過三種方式用 gdb 查看進程:

  • 使用 attach 命令開始查看一個已經運行的進程;attach 將停止進程。

  • 使用 run 命令執行程序并從頭開始調試程序。

  • 查看已有的核心文件來確定進程終止時的狀態。要查看核心文件,請用下面的命令啟動 gdb。 gdb programname corefilename

    要用核心文件進行調試,您不僅需要程序的可執行文件和源文件,還需要核心文件本身。要用核心文件啟動 gdb,請使用 -c 選項: gdb -c core programname

    gdb 顯示哪行代碼導致程序發生核心轉儲。

在運行程序或連接到已經運行的程序之前,請列出您覺得有錯誤的源代碼,設置斷點,然后開始調試程序。您可以使用 help 命令查看全面的 gdb 在線幫助和詳細的教程。

kgdb

kgdb 程序(使用 gdb 的遠程主機 Linux 內核調試器)提供了一種使用 gdb 調試 Linux 內核的機制。kgdb 程序是內核的擴展,它讓您能夠在遠程主機上運行 gdb 時連接到運行用 kgdb 擴展的內核機器。您可以接著深入到內核中、設置斷點、檢查數據并進行其它操作(類似于您在應用程序上使用 gdb 的方式)。這個補丁的主要特點之一就是運行 gdb 的主機在引導過程中連接到目標機器(運行要被調試的內核)。這讓您能夠盡早開始調試。請注意,補丁為 Linux 內核添加了功能,所以 gdb 可以用來調試 Linux 內核。

使用 kgdb 需要兩臺機器:一臺是開發機器,另一臺是測試機器。一條串行線(空調制解調器電纜)將通過機器的串口連接它們。您希望調試的內核在測試機器上運行;gdb 在開發機器上運行。gdb 使用串行線與您要調試的內核通信。

請遵循下面的步驟來設置 kgdb 調試環境:

  1. 下載您的 Linux 內核版本適用的補丁。

  2. 將組件構建到內核,因為這是使用 kgdb 最簡單的方法。(請注意,有兩種方法可以構建多數內核組件,比如作為模塊或直接構建到內核中。舉例來說,日志紀錄文件系統(Journaled File System,JFS)可以作為模塊構建,或直接構建到內核中。通過使用 gdb 補丁,我們就可以將 JFS 直接構建到內核中。)

  3. 應用內核補丁并重新構建內核。

  4. 創建一個名為 .gdbinit 的文件,并將其保存在內核源文件子目錄中(換句話說就是 /usr/src/linux)。文件 .gdbinit 中有下面四行代碼:
    • set remotebaud 115200
    • symbol-file vmlinux
    • target remote /dev/ttyS0
    • set output-radix 16

  5. 將 append=gdb 這一行添加到 lilo,lilo 是用來在引導內核時選擇使用哪個內核的引導載入程序。
    • image=/boot/bzImage-2.4.17
    • label=gdb2417
    • read-only
    • root=/dev/sda8
    • append="gdb gdbttyS=1 gdb-baud=115200 nmi_watchdog=0"

清單 7 是一個腳本示例,它將您在開發機器上構建的內核和模塊引入測試機器。您需要修改下面幾項:

  • best@sfb :用戶標識和機器名。
  • /usr/src/linux-2.4.17 :內核源代碼樹的目錄。
  • bzImage-2.4.17 :測試機器上將引導的內核名。
  • rcprsync :必須允許它在構建內核的機器上運行。

清單 7. 引入測試機器的內核和模塊的腳本

set -x
rcp best@sfb: /usr/src/linux-2.4.17/arch/i386/boot/bzImage /boot/bzImage-2.4.17
rcp best@sfb:/usr/src/linux-2.4.17/System.map /boot/System.map-2.4.17
rm -rf /lib/modules/2.4.17
rsync -a best@sfb:/lib/modules/2.4.17 /lib/modules
chown -R root /lib/modules/2.4.17
lilo

現在我們可以通過改為使用內核源代碼樹開始的目錄來啟動開發機器上的 gdb 程序了。在本示例中,內核源代碼樹位于 /usr/src/linux-2.4.17。輸入gdb 啟動程序。

如果一切正常,測試機器將在啟動過程中停止。輸入 gdb 命令 cont 以繼續啟動過程。一個常見的問題是,空調制解調器電纜可能會被連接到錯誤的串口。如果 gdb 不啟動,將端口改為第二個串口,這會使 gdb 啟動。

使用 kgdb 調試內核問題

清單 8 列出了 jfs_mount.c 文件的源代碼中被修改過的代碼,我們在代碼中創建了一個空指針異常,從而使代碼在第 109 行產生段錯誤。

清單 8. 修改過后的 jfs_mount.c 代碼

int jfs_mount(struct super_block *sb)
{
...
int ptr; 			/* line 1 added */
jFYI(1, ("\nMount JFS\n"));
/ *
* read/validate superblock
* (initialize mount inode from the superblock)
* /
if ((rc = chkSuper(sb))) {goto errout20;}
108 	ptr=0; 			/* line 2 added */
109 	printk("%d\n",*ptr); 	/* line 3 added */

清單 9 在向文件系統發出 mount 命令之后顯示一個 gdb 異常。kgdb 提供了幾條命令,如顯示數據結構和變量值以及顯示系統中的所有任務處于什么狀態、它們駐留在何處、它們在哪些地方使用了 CPU 等等。清單 9 將顯示回溯跟蹤為該問題提供的信息;where 命令用來執行反跟蹤,它將告訴被執行的調用在代碼中的什么地方停止。

清單 9. gdb 異常和反跟蹤

mount -t jfs /dev/sdb /jfs
Program received signal SIGSEGV, Segmentation fault.
jfs_mount (sb=0xf78a3800) at jfs_mount.c:109
109 		printk("%d\n",*ptr);
(gdb)where
#0 jfs_mount (sb=0xf78a3800) at jfs_mount.c:109
#1 0xc01a0dbb in jfs_read_super ... at super.c:280
#2 0xc0149ff5 in get_sb_bdev ... at super.c:620
#3 0xc014a89f in do_kern_mount ... at super.c:849
#4 0xc0160e66 in do_add_mount ... at namespace.c:569
#5 0xc01610f4 in do_mount ... at namespace.c:683
#6 0xc01611ea in sys_mount ... at namespace.c:716
#7 0xc01074a7 in system_call () at af_packet.c:1891
#8 0x0 in -- ()
(gdb)

下一部分還將討論這個相同的 JFS 段錯誤問題,但不設置調試器,如果您在非 kgdb 內核環境中執行清單 8 中的代碼,那么它使用內核可能生成的 Oops 消息。

Oops 分析

Oops(也稱 panic,慌張)消息包含系統錯誤的細節,如 CPU 寄存器的內容。在 Linux 中,調試系統崩潰的傳統方法是分析在發生崩潰時發送到系統控制臺的 Oops 消息。一旦您掌握了細節,就可以將消息發送到 ksymoops 實用程序,它將試圖將代碼轉換為指令并將堆棧值映射到內核符號。在很多情況下,這些信息就足夠您確定錯誤的可能原因是什么了。請注意,Oops 消息并不包括核心文件。

讓我們假設系統剛剛創建了一條 Oops 消息。作為編寫代碼的人,您希望解決問題并確定什么導致了 Oops 消息的產生,或者您希望向顯示了 Oops 消息的代碼的開發者提供有關您的問題的大部分信息,從而及時地解決問題。Oops 消息是等式的一部分,但如果不通過 ksymoops 程序運行它也于事無補。下面的圖顯示了格式化 Oops 消息的過程。


格式化 Oops 消息
格式化 Oops 消息

ksymoops 需要幾項內容:Oops 消息輸出、來自正在運行的內核的 System.map 文件,還有 /proc/ksyms、vmlinux 和 /proc/modules。關于如何使用 ksymoops,內核源代碼 /usr/src/linux/Documentation/oops-tracing.txt 中或 ksymoops 手冊頁上有完整的說明可以參考。Ksymoops 反匯編代碼部分,指出發生錯誤的指令,并顯示一個跟蹤部分表明代碼如何被調用。

首先,將 Oops 消息保存在一個文件中以便通過 ksymoops 實用程序運行它。清單 10 顯示了由安裝 JFS 文件系統的 mount 命令創建的 Oops 消息,問題是由清單 8 中添加到 JFS 安裝代碼的那三行代碼產生的。

清單 10. ksymoops 處理后的 Oops 消息

   ksymoops 2.4.0 on i686 2.4.17. Options used
... 15:59:37 sfb1 kernel: Unable to handle kernel NULL pointer dereference at
virtual address 0000000
... 15:59:37 sfb1 kernel: c01588fc
... 15:59:37 sfb1 kernel: *pde = 0000000
... 15:59:37 sfb1 kernel: Oops: 0000
... 15:59:37 sfb1 kernel: CPU:    0
... 15:59:37 sfb1 kernel: EIP:    0010:[jfs_mount+60/704]
... 15:59:37 sfb1 kernel: Call Trace: [jfs_read_super+287/688] 
[get_sb_bdev+563/736] [do_kern_mount+189/336] [do_add_mount+35/208]
[do_page_fault+0/1264]
... 15:59:37 sfb1 kernel: Call Trace: [<c0155d4f>]...
... 15:59:37 sfb1 kernel: [<c0106e04 ...
... 15:59:37 sfb1 kernel: Code: 8b 2d 00 00 00 00 55 ...
>>EIP; c01588fc <jfs_mount+3c/2c0> <=====
...
Trace; c0106cf3 <system_call+33/40>
Code; c01588fc <jfs_mount+3c/2c0>
00000000 <_EIP>:
Code; c01588fc <jfs_mount+3c/2c0>  <=====0: 8b 2d 00 00 00 00 	mov 	0x0,%ebp    <=====
Code; c0158902 <jfs_mount+42/2c0>6:  55 			push 	%ebp

接下來,您要確定 jfs_mount 中的哪一行代碼引起了這個問題。Oops 消息告訴我們問題是由位于偏移地址 3c 的指令引起的。做這件事的辦法之一是對 jfs_mount.o 文件使用 objdump 實用程序,然后查看偏移地址 3c。Objdump 用來反匯編模塊函數,看看您的 C 源代碼會產生什么匯編指令。清單 11 顯示了使用 objdump 后您將看到的內容,接著,我們查看 jfs_mount 的 C 代碼,可以看到空值是第 109 行引起的。偏移地址 3c 之所以很重要,是因為 Oops 消息將該處標識為引起問題的位置。

清單 11. jfs_mount 的匯編程序清單

  109	printk("%d\n",*ptr);
objdump jfs_mount.o
jfs_mount.o: 	file format elf32-i386
Disassembly of section .text:
00000000 <jfs_mount>:0:55 			push %ebp...2c:	e8 cf 03 00 00	   call	   400 <chkSuper>31:	89 c3 	  	    	mov     %eax,%ebx33:	58		    	pop     %eax34:	85 db 	  	    	test 	%ebx,%ebx36:	0f 85 55 02 00 00 jne 	291 <jfs_mount+0x291>3c:	8b 2d 00 00 00 00 mov 	0x0,%ebp << problem line above42:	55			push 	%ebp

kdb

Linux 內核調試器(Linux kernel debugger,kdb)是 Linux 內核的補丁,它提供了一種在系統能運行時對內核內存和數據結構進行檢查的辦法。請注意,kdb 不需要兩臺機器,不過它也不允許您像 kgdb 那樣進行源代碼級別上的調試。您可以添加額外的命令,給出該數據結構的標識或地址,這些命令便可以格式化和顯示基本的系統數據結構。目前的命令集允許您控制包括以下操作在內的內核操作:

  • 處理器單步執行
  • 執行到某條特定指令時停止
  • 當存取(或修改)某個特定的虛擬內存位置時停止
  • 當存取輸入/輸出地址空間中的寄存器時停止
  • 對當前活動的任務和所有其它任務進行堆棧回溯跟蹤(通過進程 ID)
  • 對指令進行反匯編

追擊內存溢出

您肯定不想陷入類似在幾千次調用之后發生分配溢出這樣的情形。

我們的小組花了許許多多時間來跟蹤稀奇古怪的內存錯誤問題。應用程序在我們的開發工作站上能運行,但在新的產品工作站上,這個應用程序在調用 malloc() 兩百萬次之后就不能運行了。真正的問題是在大約一百萬次調用之后發生了溢出。新系統之所有存在這個問題,是因為被保留的malloc() 區域的布局有所不同,從而這些零散內存被放置在了不同的地方,在發生溢出時破壞了一些不同的內容。

我們用多種不同技術來解決這個問題,其中一種是使用調試器,另一種是在源代碼中添加跟蹤功能。在我職業生涯的大概也是這個時候,我便開始關注內存調試工具,希望能更快更有效地解決這些類型的問題。在開始一個新項目時,我最先做的事情之一就是運行 MEMWATCH 和 YAMD,看看它們是不是會指出內存管理方面的問題。

內存泄漏是應用程序中常見的問題,不過您可以使用本文所講述的工具來解決這些問題。

第 4 種情況:使用魔術鍵控順序進行回溯跟蹤

如果在 Linux 掛起時您的鍵盤仍然能用,那請您使用以下方法來幫助解決掛起問題的根源。遵循這些步驟,您便可以顯示當前運行的進程和所有使用魔術鍵控順序的進程的回溯跟蹤。

  1. 您正在運行的內核必須是在啟用 CONFIG_MAGIC_SYS-REQ 的情況下構建的。您還必須處在文本模式。CLTR+ALT+F1 會使您進入文本模式,CLTR+ALT+F7 會使您回到 X Windows。
  2. 當在文本模式時,請按 <ALT+ScrollLock>,然后按 <Ctrl+ScrollLock>。上述魔術的擊鍵會分別給出當前運行的進程和所有進程的堆棧跟蹤。
  3. 請查找 /var/log/messages。如果一切設置正確,則系統應該已經為您轉換了內核的符號地址。回溯跟蹤將被寫到 /var/log/messages 文件中。

結束語

幫助調試 Linux 上的程序有許多不同的工具可供使用。本文講述的工具可以幫助您解決許多編碼問題。能顯示內存泄漏、溢出等等的位置的工具可以解決內存管理問題,我發現 MEMWATCH 和 YAMD 很有幫助。

使用 Linux 內核補丁會使 gdb 能在 Linux 內核上工作,這對解決我工作中使用的 Linux 的文件系統方面的問題很有幫助。此外,跟蹤實用程序能幫助確定在系統調用期間文件系統實用程序什么地方出了故障。下次當您要擺平 Linux 中的錯誤時,請試試這些工具中的某一個。


參考資料

  • 您可以參閱本文在 developerWorks 全球站點上的 英文原文.

  • 下載 MEMWATCH。



  • 請查看 Dynamic Probes 調試功能程序。



  • 請閱讀文章“ Linux software debugging with GDB”。( developerWorks,2001 年 2 月)



  • 請訪問 IBM Linux Technology Center。



  • developerWorksLinux 專區可以找到 更多的 Linux 文章。

關于作者

Steve Best 目前在做 Linux 項目的日志紀錄文件系統(Journaled File System,JFS)的工作。Steve 在操作系統方面有豐富的從業經驗,他的著重的領域是文件系統、國際化和安全性。




簡介:?Linux 的大部分特色源自于 shell 的 GNU 調試器,也稱作 gdb。gdb 可以讓您查看程序的內部結構、打印變量值、設置斷點,以及單步調試源代碼。它是功能極其強大的工具,適用于修復程序代碼中的問題。在本文中,David Seager 將嘗試說明 gdb 有多棒,多實用。


編譯

開始調試之前,必須用程序中的調試信息編譯要調試的程序。這樣,gdb 才能夠調試所使用的變量、代碼行和函數。如果要進行編譯,請在 gcc(或 g++)下使用額外的 '-g' 選項來編譯程序:

gcc -g eg.c -o eg

運行 gdb

在 shell 中,可以使用 'gdb' 命令并指定程序名作為參數來運行 gdb,例如 'gdb eg';或者在 gdb 中,可以使用 file 命令來裝入要調試的程序,例如 'file eg'。這兩種方式都假設您是在包含程序的目錄中執行命令。裝入程序之后,可以用 gdb 命令 'run' 來啟動程序。

調試會話示例

如果一切正常,程序將執行到結束,此時 gdb 將重新獲得控制。但如果有錯誤將會怎么樣?這種情況下,gdb 會獲得控制并中斷程序,從而可以讓您檢查所有事物的狀態,如果運氣好的話,可以找出原因。為了引發這種情況,我們將使用一個示例程序:


代碼示例 eg1.c
#include 
int wib(int no1, int no2)
{int result, diff;diff = no1 - no2;result = no1 / diff;return result;
}
int main(int argc, char *argv[])
{int value, div, result, i, total;value = 10;div = 6;total = 0;for(i = 0; i < 10; i++){result = wib(value, div);total += result;div++;value--;}printf("%d wibed by %d equals %d\n", value, div, total);return 0;
}

這個程序將運行 10 次 for 循環,使用 'wib()" 函數計算出累積值,最后打印出結果。

在您喜歡的文本編輯器中輸入這個程序(要保持相同的行距),保存為 'eg1.c',使用 'gcc -g eg1.c -o eg1' 進行編譯,并用 'gdb eg1' 啟動 gdb。使用 'run' 運行程序可能會產生以下消息:

Program received signal SIGFPE, Arithmetic exception.
0x80483ea in wib (no1=8, no2=8) at eg1.c:7
7         result = no1 / diff;
(gdb)

gdb 指出在程序第 7 行發生一個算術異常,通常它會打印這一行以及 wib() 函數的自變量值。要查看第 7 行前后的源代碼,請使用 'list' 命令,它通常會打印 10 行。再次輸入 'list'(或者按回車重復上一條命令)將列出程序的下 10 行。從 gdb 消息中可以看出,第 7 行中的除法運算出了錯,程序在這一行中將變量 "no1" 除以 "diff"。

要查看變量的值,使用 gdb 'print' 命令并指定變量名。輸入 'print no1' 和 'print diff',可以相應看到 "no1" 和 "diff" 的值,結果如下:

(gdb) print no1
$5 = 8
(gdb) print diff
$2 = 0

gdb 指出 "no1" 等于 8,"diff" 等于 0。根據這些值和第 7 行中的語句,我們可以推斷出算術異常是由除數為 0 的除法運算造成的。清單顯示了第 6 行計算的變量 "diff",我們可以打印 "diff" 表達式(使用 'print no1 - no2' 命令),來重新估計這個變量。gdb 告訴我們 wib 函數的這兩個自變量都等于 8,于是我們要檢查調用 wib() 函數的 main() 函數,以查看這是在什么時候發生的。在允許程序自然終止的同時,我們使用 'continue' 命令告訴 gdb 繼續執行。

(gdb) continue
Continuing.
Program terminated with signal SIGFPE, Arithmetic exception.
The program no longer exists.

使用斷點

為了查看在 main() 中發生了什么情況,可以在程序代碼中的某一特定行或函數中設置斷點,這樣 gdb 會在遇到斷點時中斷執行。可以使用命令 'break main' 在進入 main() 函數時設置斷點,或者可以指定其它任何感興趣的函數名來設置斷點。然而,我們只希望在調用 wib() 函數之前中斷執行。輸入 'list main' 將打印從 main() 函數開始的源碼清單,再次按回車將顯示第 21 行上的 wib() 函數調用。要在那一行上設置斷點,只需輸入 'break 21'。gdb 將發出以下響應:

(gdb) break 21
Breakpoint 1 at 0x8048428: file eg1.c, line 21.

以顯示它已在我們請求的行上設置了 1 號斷點。'run' 命令將從頭重新運行程序,直到 gdb 中斷為止。發生這種情況時,gdb 會生成一條消息,指出它在哪個斷點上中斷,以及程序運行到何處:

Breakpoint 1, main (argc=1, argv=0xbffff954) at eg1.c:21
21          result = wib(value, div);

發出 'print value' 和 'print div' 將會顯示在第一次調用 wib() 時,變量分別等于 10 和 6,而 'print i' 將會顯示 0。幸好,gdb 將顯示所有局部變量的值,并使用 'info locals' 命令保存大量輸入信息。

從以上的調查中可以看出,當 "value" 和 "div" 相等時就會出現問題,因此輸入 'continue' 繼續執行,直到下一次遇到 1 號斷點。對于這次迭代,'info locals' 顯示了 value=9 和 div=7。

與其再次繼續,還不如使用 'next' 命令單步調試程序,以查看 "value" 和 "div" 是如何改變的。gdb 將響應:

(gdb) next
22          total += result;

再按兩次回車將顯示加法和減法表達式:

(gdb)
23          div++;
(gdb)
24          value--;

再按兩次回車將顯示第 21 行,wib() 調用。'info locals' 將顯示目前 "div" 等于 "value",這就意味著將發生問題。如果有興趣,可以使用 'step' 命令(與 'next' 形成對比,'next' 將跳過函數調用)來繼續執行 wib() 函數,以再次查看除法錯誤,然后使用 'next' 來計算 "result"。

現在已完成了調試,可以使用 'quit' 命令退出 gdb。由于程序仍在運行,這個操作會終止它,gdb 將提示您確認。

更多斷點和觀察點

由于我們想要知道在調用 wib() 函數之前 "value" 什么時候等于 "div",因此在上一示例中我們在第 21 行中設置斷點。我們必須繼續執行兩次程序才會發生這種情況,但是只要在斷點上設置一個條件就可以使 gdb 只在 "value" 與 "div" 真正相等時暫停。要設置條件,可以在定義斷點時指定 "break <line number> if <conditional expression>"。將 eg1 再次裝入 gdb,并輸入:

(gdb) break 21 if value==div
Breakpoint 1 at 0x8048428: file eg1.c, line 21.

如果已經在第 21 行中設置了斷點,如 1 號斷點,則可以使用 'condition' 命令來代替在斷點上設置條件:

(gdb) condition 1 value==div

使用 'run' 運行 eg1.c 時,如果 "value" 等于 "div",gdb 將中斷,從而避免了在它們相等之前必須手工執行 'continue'。調試 C 程序時,斷點條件可以是任何有效的 C 表達式,一定要是程序所使用語言的任意有效表達式。條件中指定的變量必須在設置了斷點的行中,否則表達式就沒有什么意義!

使用 'condition' 命令時,如果指定斷點編號但又不指定表達式,可以將斷點設置成無條件斷點,例如,'condition 1' 就將 1 號斷點設置成無條件斷點。

要查看當前定義了什么斷點及其條件,請發出命令 'info break':

(gdb) info break
Num Type           Disp Enb Address    What
1   breakpoint     keep y   0x08048428 in main at eg1.c:21stop only if value == divbreakpoint already hit 1 time

除了所有條件和已經遇到斷點多少次之外,斷點信息還在 'Enb' 列中指定了是否啟用該斷點。可以使用命令 'disable <breakpoint number>'、'enable <breakpoint number>' 或 'delete <breakpoint number>' 來禁用、啟用和徹底刪除斷點,例如 'disable 1' 將阻止在 1 號斷點處中斷。

如果我們對 "value" 什么時候變得與 "div" 相等更感興趣,那么可以使用另一種斷點,稱作監視。當指定表達式的值改變時,監視點將中斷程序執行,但必須在表達式中所使用的變量在作用域中時設置監視點。要獲取作用域中的 "value" 和 "div",可以在 main 函數上設置斷點,然后運行程序,當遇到 main() 斷點時設置監視點。重新啟動 gdb,并裝入 eg1,然后輸入:

(gdb) break main
Breakpoint 1 at 0x8048402: file eg1.c, line 15.
(gdb) run
...
Breakpoint 1, main (argc=1, argv=0xbffff954) at eg1.c:15
15        value = 10;

要了解 "div" 何時更改,可以使用 'watch div',但由于要在 "div" 等于 "value" 時中斷,那么應輸入:

(gdb) watch div==value
Hardware watchpoint 2: div == value

如果繼續執行,那么當表達式 "div==value" 的值從 0(假)變成 1(真)時,gdb 將中斷:

(gdb) continue
Continuing.
Hardware watchpoint 2: div == value
Old value = 0
New value = 1
main (argc=1, argv=0xbffff954) at eg1.c:19
19        for(i = 0; i < 10; i++)

'info locals' 命令將驗證 "value" 是否確實等于 "div"(再次聲明,是 8)。

'info watch' 命令將列出已定義的監視點和斷點(此命令等價于 'info break'),而且可以使用與斷點相同的語法來啟用、禁用和刪除監視點。

core 文件

在 gdb 下運行程序可以使俘獲錯誤變得更容易,但在調試器外運行的程序通常會中止而只留下一個 core 文件。gdb 可以裝入 core 文件,并讓您檢查程序中止之前的狀態。

在 gdb 外運行示例程序 eg1 將會導致核心信息轉儲:

$ ./eg1
Floating point exception (core dumped)

要使用 core 文件啟動 gdb,在 shell 中發出命令 'gdb eg1 core' 或 'gdb eg1 -c core'。gdb 將裝入 core 文件,eg1 的程序清單,顯示程序是如何終止的,并顯示非常類似于我們剛才在 gdb 下運行程序時看到的消息:

...
Core was generated by `./eg1'.
Program terminated with signal 8, Floating point exception.
...
#0  0x80483ea in wib (no1=8, no2=8) at eg1.c:7
7         result = no1 / diff;

此時,可以發出 'info locals'、'print'、'info args' 和 'list' 命令來查看引起除數為零的值。'info variables' 命令將打印出所有程序變量的值,但這要進行很長時間,因為 gdb 將打印 C 庫和程序代碼中的變量。為了更容易地查明在調用 wib() 的函數中發生了什么情況,可以使用 gdb 的堆棧命令。

堆棧跟蹤

程序“調用堆棧”是當前函數之前的所有已調用函數的列表(包括當前函數)。每個函數及其變量都被分配了一個“幀”,最近調用的函數在 0 號幀中(“底部”幀)。要打印堆棧,發出命令 'bt'('backtrace' [回溯] 的縮寫):

(gdb) bt
#0  0x80483ea in wib (no1=8, no2=8) at eg1.c:7
#1  0x8048435 in main (argc=1, argv=0xbffff9c4) at eg1.c:21

此結果顯示了在 main() 的第 21 行中調用了函數 wib()(只要使用 'list 21' 就能證實這一點),而且 wib() 在 0 號幀中,main() 在 1 號幀中。由于 wib() 在 0 號幀中,那么它就是執行程序時發生算術錯誤的函數。

實際上,發出 'info locals' 命令時,gdb 會打印出當前幀中的局部變量,缺省情況下,這個幀中的函數就是被中斷的函數(0 號幀)。可以使用命令 'frame' 打印當前幀。要查看 main 函數(在 1 號幀中)中的變量,可以發出 'frame 1' 切換到 1 號幀,然后發出 'info locals' 命令:

(gdb) frame 1
#1  0x8048435 in main (argc=1, argv=0xbffff9c4) at eg1.c:21
21          result = wib(value, div);
(gdb) info locals
value = 8
div = 8
result = 4
i = 2
total = 6

此信息顯示了在第三次執行 "for" 循環時(i 等于 2)發生了錯誤,此時 "value" 等于 "div"。

可以通過如上所示在 'frame' 命令中明確指定號碼,或者使用 'up' 命令在堆棧中上移以及 'down' 命令在堆棧中下移來切換幀。要獲取有關幀的進一步信息,如它的地址和程序語言,可以使用命令 'info frame'。

gdb 堆棧命令可以在程序執行期間使用,也可以在 core 文件中使用,因此對于復雜的程序,可以在程序運行時跟蹤它是如何轉到函數的。

連接到其它進程

除了調試 core 文件或程序之外,gdb 還可以連接到已經運行的進程(它的程序已經過編譯,并加入了調試信息),并中斷該進程。只需用希望 gdb 連接的進程標識替換 core 文件名就可以執行此操作。以下是一個執行循環并睡眠的示例程序:


eg2 示例代碼
#include 
int main(int argc, char *argv[])
{int i;for(i = 0; i < 60; i++){sleep(1);}return 0;
}

使用 'gcc -g eg2.c -o eg2' 編譯該程序并使用 './eg2 &' 運行該程序。請留意在啟動該程序時在背景上打印的進程標識,在本例中是 1283:

./eg2 &
[3] 1283

啟動 gdb 并指定進程標識,在我舉的這個例子中是 'gdb eg2 1283'。gdb 會查找一個叫作 "1283" 的 core 文件。如果沒有找到,那么只要進程 1283 正在運行(在本例中可能在 sleep() 中),gdb 就會連接并中斷該進程:

...
/home/seager/gdb/1283: No such file or directory.
Attaching to program: /home/seager/gdb/eg2, Pid 1283
...
0x400a87f1 in __libc_nanosleep () from /lib/libc.so.6
(gdb)

此時,可以發出所有常用 gdb 命令。可以使用 'backtrace' 來查看當前位置與 main() 的相對關系,以及 mian() 的幀號是什么,然后切換到 main() 所在的幀,查看已經在 "for" 循環中運行了多少次:

(gdb) backtrace
#0  0x400a87f1 in __libc_nanosleep () from /lib/libc.so.6
#1  0x400a877d in __sleep (seconds=1) at ../sysdeps/unix/sysv/linux/sleep.c:78
#2  0x80483ef in main (argc=1, argv=0xbffff9c4) at eg2.c:7
(gdb) frame 2
#2  0x80483ef in main (argc=1, argv=0xbffff9c4) at eg2.c:7
7           sleep(1);
(gdb) print i
$1 = 50

如果已經完成了對程序的修改,可以 'detach' 命令繼續執行程序,或者 'kill' 命令殺死進程。還可以首先使用 'file eg2' 裝入文件,然后發出 'attach 1283' 命令連接到進程標識 1283 下的 eg2。

其它小技巧

gdb 可以讓您通過使用 shell 命令在不退出調試環境的情況下運行 shell 命令,調用形式是 'shell [commandline]',這有助于在調試時更改源代碼。

最后,在程序運行時,可以使用 'set ' 命令修改變量的值。在 gdb 下再次運行 eg1,使用命令 'break 7 if diff==0' 在第 7 行(將在此處計算結果)設置條件斷點,然后運行程序。當 gdb 中斷執行時,可以將 "diff" 設置成非零值,使程序繼續運行直至結束:

Breakpoint 1, wib (no1=8, no2=8) at eg1.c:7
7         result = no1 / diff;
(gdb) print diff
$1 = 0
(gdb) set diff=1
(gdb) continue
Continuing.
0 wibed by 16 equals 10
Program exited normally.

結束語

GNU 調試器是所有程序員工具庫中的一個功能非常強大的工具。在本文中,我只介紹了 gdb 的一小部分功能。要了解更多知識,建議您閱讀 GNU 調試器手冊。


參考資料

  • 您可以參閱本文在 developerWorks 全球站點上的 英文原文.

  • GNU 調試器手冊

  • 調試會話示例的 源代碼。

  • 連接示例的 源代碼。

關于作者

David Seager 是 IBM 的軟件開發人員,他從事 Linux 和基于 Web 的應用工作已有兩年時間了。


gdb (GNU 調試器): 基礎

簡介:?這是由兩部分組成的關于調試 zSeries* 上的 Linux 應用程序的系列文章中的第 2 部分。請參閱 第 1 部分。

最后,set args 為程序設置命令行參數。您也可以在執行 run 時指定命令行參數,但是 set args 將使參數在 run 的多次執行中都有效。

gdb Post Mortem

當程序意外地終止時,內核會嘗試產生一個核心文件,以圖判斷發生了什么錯誤。然而,核心文件通常不是在默認設置值下產生的。這可以使用 ulimit 命令來改變。ulimit -c unlimited 幫助確保您獲得應用程序的完整核心文件。

雖然核心文件當前僅提供多線程應用程序中的有限的值,不過 2.5 版的開發內核已開始處理這個問題。預計 2.6 版的內核中會提供一些理想的線程改進。

圖 2突出顯示了一系列便利的 post mortem 命令。圖 3簡要顯示了一個核心程序的完整運行過程。同樣,我們使用了 simple 程序。 但不是手動加載程序和核心文件,而是從命令行調入:

gdb simple core

在加載符號之后,gdb 將指出程序在何處終止。注意當前幀 #0 包含前一節中計算的地址。gdb 將在 31 位系統上截去高位,僅顯示指令地址。 還要注意幀 #1 包含 gpr14 中的返回地址。

接著往下看,i f 提供了關于當前堆棧幀的信息。在堆棧幀中往上移到 main,這就是我們離開該幀的地方(即調用 memcpy 的地方)。簡單的 i locals 提供了傳遞給 memcpy 的變量的值,其中一個變量 boink.boik 的值為 0x0。使用 ptype 來檢查變量類型,這樣將確認它是一個整型指針,并且如果目的是為了拷貝內容到其中,它就不應該是 0x0。最后一個選項是使用 print,通過一個星號(*)來解除指針引用,以便接收值。

處理優化過的代碼

先前,我曾提到當您在源代碼級調試優化過的代碼時,gdb 可能變得有點棘手。編譯器優化一些代碼的執行順序以最大化性能。 圖4顯示了這樣一個例子。您可以看到行號如何從 32 切換到 30 然后又切換回 32。

如何處理這種情況呢?使用 si 和 ni(next instruction;它類似 si,但是會跳過子例程調用)將非常有幫助。 在這個層次上,很好理解 zArchitecture 是有所幫助的。

圖 5顯示了為調試而對程序進行的設置。首先在 main()的地址處設置一個斷點,然后設置一個 display。display 是一個表達式,它在每次代碼停止執行時打印有關信息。在此例中,display 被設置為顯示當前指令地址處的指令。/i 是打印為反匯編代碼的格式,而當前指令指針在值/寄存器(value/register)$pswa 中。

單步調試代碼,可以明顯看出每條機器指令都與一行 c 代碼相關聯。 前四行與第 27 行(即函數 main 的開頭)相關聯。 前四行是典型的函數引入操作,它們保存寄存器、堆棧指針并調整堆棧。當關聯的行號變為32 時,我們就設置好了對 do_one_thing() 的函數調用。

當 display 在工作時,它顯示 x /i 作為實際數據顯示之前的命令。x 是檢查內存的命令。/i 是以指令格式來格式化;/x 將以 16 進制格式來格式化;而 /a 將以 16 進制來格式化。然而,您應該在盡可能的地方把該值看作是地址,并解析符號名稱。

當在指令級工作時,設置一些顯示可能是有所幫助的。您可以將所有 display 命令放在一個文件中,并在命令行上使用 -x 選項來指定它。 圖 6包含了工作在匯編程序級時通常使用的 display 命令。

這個命令打印全部 PSW 值、所有通用寄存器和從當前指令地址開始的下 10 行機器代碼。 圖 7顯示了當我們在 main() 處中斷時的結果。可以看到,在其中一些寄存器所指向的地方,/a 格式解析是如何使得理解正在發生的事情更加容易的。

結束語

對于一些可用于 Linux 應用程序調試的基本工具以及調試過程本身,本文中的信息應該為您提供了有用的入門信息。


關于作者

Mike Grundy:MikeGrundy 在 IBM 負責 S/390 Linux 應用程序開發工具,您可以通過電子郵件grundym@us.ibm.com聯系他。

調試 make

讓 make 為我們工作而不是為我們制造麻煩

簡介:?make 工具如 GNU make、System V make 和 Berkeley make 是用來組織應用程序編譯過程的基本工具,但是每個 make 工具之間又有所不同。本文將介紹 makefile 的結構,避免如何在創建 makefile 時出現一些共同的錯誤,并探索如何修復或解決可移植性問題,還為解決突發的問題提供了一些技巧。

大部分 UNIX? 和 Linux? 程序都是通過運行 make 來編譯的。make 工具會讀取一個包含指令的文件(這個文件的名字通常都是 makefile 或 Makefile,不過后文中我們統一稱之為 “makefile”),并執行各種操作來編譯程序。在很多編譯過程中,makefile 自己完全是由其他軟件生成的;例如,autoconf/automake 程序就用來開發編譯程序。其他程序可能會要求我們直接編輯 makefile,當然,新的開發還可能需要我們自己編寫 makefile。

“make 工具”這個短語可能有些容易引起誤解。經常使用的 make 工具至少有 3 個變種:GNU make、System V make 和 Berkeley make。它們都是從早期 UNIX 的一個核心規范發展而來的,每個變種都增加了一些新特性。這就導致出現了一種復雜的情況:很常用的一些特性,例如在 makefile 中通過引用來包含其他文件,都不能很好地移植!簡單編寫程序來創建 makefile 就是一種解決方案。由于 GNU make 是免費的,并且可以廣泛地發布,因此有些開發人員就簡單地為它來編寫代碼;類似地,有很多起源于 BSD 的項目都要求我們使用 Berkeley make(這也是免費的)。

稍微遜色一點但依然相關的 make 工具是 J?rg Schilling 的 smake 和 make 家族中的第五位(已不再使用) —— 早先的 make,后者定義了與其他 make 工具共享的一些公共特性的子集。盡管 smake 在任何系統上都不是默認的 make 工具,但是它也是一個很好的 make 實現,有些程序(尤其是 Schilling 的程序)都喜歡使用它。

下面先來回顧一下在使用 makefile 時所遇到的最常見的一些問題。

理解 makefile

要調試 make,需要讀取 makefile。正如所了解的那樣,makefile 的目標就是為編譯程序提供一些指令。make 的主要特性之一就是 依賴性管理:只有在程序源碼發生更新必須要重新編譯程序時,make 才會真正重新編譯程序。通常,這是通過一系列依賴性規則來表示的。其中一種依賴性規則如下所示:


清單 1. 依賴性規則的格式
target: dependenciesinstructions

人們在編寫自己的第一個 makefile 時所碰到的主要問題在這個結構中可能看得出來,也可能看不出來:縮進使用的是制表符,而不是多少個空格。由于在這種格式中使用空格所產生的 Berkeley make 錯誤消息對人們也沒什么幫助:


清單 2. Berkeley make 錯誤消息
make: "Makefile" line 2: Need an operator
make: Fatal errors encountered -- cannot continue

GNU make,盡管不能對這個文件進行處理,但卻會給出一個更有用的建議:


清單 3. GNU make 錯誤消息
Makefile::2: *** missing separator (did you mean TAB instead of 8 spaces?).  Stop.

請注意依賴性和指令都是可選的;只有目標和冒號才是必須的。那么既然語法是這樣,語義又該如何呢?其語義是:如果 make 希望編譯 target ,那它就會首先查看依賴關系。實際上,它會遞歸地嘗試編譯目標;如果所依賴的內容碰巧又依賴其他內容,那么在這條規則繼續之前,必須對所依賴的內容進行處理。如果target 存在,并且至少比 dependencies 中所列出的所有內容都要新,那么就不會執行任何操作。如果target 不存在,或者有一個或多個依賴內容更新,那么 make 就會執行 instructions 操作。依賴性是按照指定的順序進行處理的。如果沒有指定依賴性,那就總會執行 instructions。所依賴的內容也稱為源(source)

如果在命令行中給出了一個目標(例如 make foo),那么 make 就會試圖編譯這個目標。否則,它就試圖編譯文件中列出的第一個目標。一些開發人員采用的約定是讓第一個目標看起來如下所示:


清單 4. 通常使用的第一個目標約定
default: all

有些人會假設之所以使用這條規則是因為它是 “默認的”。但實際上并非如此;它之所以這樣使用是因為這是該文件中的第一條規則。可以按照自己希望的方式對其進行命名,不過名字 “default” 是一個很好的選擇,因為這對于讀者來說意義是顯而易見的。記住 makefile 是會由人來閱讀的,而不是只由 make 程序來使用的。

偽目標

通常我們可以說,目標的功能是從其他文件中創建一個文件。實際上并非總是如此。大部分 makefile 都至少有兩條規則,它們從來都不會創建目標。請考慮下面的示例規則:


清單 5. 示例偽目標
all: hello goodbye fibonacci

這條規則會告訴 make —— 如果希望編譯目標 all —— 首先要確保 hello、goodbye 和 fibonacci 都是最新的。然后,就什么也不做了。下面并沒有提供指令。在這條規則完成之后,并不會創建名為 all 的文件。這個目標是一種假目標。在某些 make 變種中使用的技術術語稱之為 “偽目標”。

偽目標是為了組織結構的目的而設計的,這在編寫一個清晰的 makefile 時是種非常不錯的技術。舉例來說,我們可能會經常看到下面的規則:


清單 6. 偽目標的靈活用法
build: clean all install

這指定了編譯過程執行的操作順序。

特殊的目標和源

系統還定義了幾個特殊的目標,它們對 make 可以產生一些特別的影響,提供一種可配置的機制。具體的目標集對于每個實現來說都是不同的;其中最通用的一個是 .SUFFIXES 目標,它使用的源是一系列模式,添加在可識別的文件后綴列表中。這些特殊目標并不會用作通用規則來把編譯作為 makefile 中默認的第一條目標。

有些版本的 make 允許將特殊源與給定目標的依賴性一起指定,例如 .IGNORE,它說明從編譯這個目標所使用的命令中生成的錯誤都應該忽略,仿佛它們前面都有一個短線一樣。這些標記的可移植性并不好,但是對于理解 makefile 來說卻是必須的。

通用規則

在 make 中有一些隱式規則用來根據文件名后綴執行通用轉換。舉例來說,如果現在沒有 makefile,可以創建一個名為 “hello.c” 的文件,并運行make hello 命令:


清單 7. C 文件的隱式規則的例子
$ make hello
cc -O2   -o hello hello.c

大型程序使用的 makefile 可能會簡單地指定自己需要的對象模塊清單(hello.o、world.o 等),然后為如何將 .c 文件轉換成 .o 文件提供一條規則:


清單 8. 將 .c 文件轉換成 .o 文件的規則
.c.o:cc $(CFLAGS) -c $<

實際上,大部分 make 工具都有一個早已內嵌到系統中的與此類似的規則;如果請求 make 來編譯 file.o,而且現在已經有 file.c 文件了,那么它就可以正確地完成編譯過程。術語 "$<" 是一個特殊的預定義的 make 變量,代表某條規則的 “源”。這使我們可以使用一些 make 變量。

通用規則取決于 “后綴” 的聲明,它然后會被識別為文件擴展名,而不是文件名的一部分。

變量

make 程序使用了一些變量來簡化通用值的重用。最常見的值可能是 CFLAGS。有關 make 變量有一些東西應該澄清一下。它們不一定必須是環境變量。如果所給出的名字沒有對應的 make 變量,那么 make 就會去檢查環境變量;然而,這并意味著 make 變量會被導出為環境變量。優先規則非常神秘;通常,它們的順序從高到低依次為:

  1. 命令行變量設置
  2. 父 make 進程的 makefile 中的變量設置
  3. 本 make 進程的 makefile 中的變量設置
  4. 環境變量

因此,一個變量只有在沒有在任何 makefile 或命令行中指定時,才會使用環境變量的設置(注意:父進程 makefile 變量有時候會傳遞下來,但不總會這樣。正如可能已經猜測到的一樣,這些規則在各個 make 工具中會有所不同)。

人們在使用 make 時常常碰到的一個問題是變量被變量名的一部分替換掉了:舉例來說,$CFLAGS 就被替換成了 “FLAGS”。因此要引用一個 make 變量,就請將它的名字放到括號中:$(CFLAGS)。否則,所得到的將是$C,后面加上一個 FLAGS

很多變量都有一些特殊的意義,這是正在使用它們的規則的一種功能。最常見的用法有:

  • $< —— 用來構建目標所使用的源文件
  • $* —— 目標名中基本的部分(不包含擴展名或目錄)
  • $@ —— 目標的完整名

雖然 Berkeley make 沒有使用這些變量,但是它們(到現在)都是可移植的。至少,是部分可移植的;其確切定義在不同的 make 實現中可能會有所不同。使用這些變量編寫的任何復雜規則都可能到某個特定的實現就不能用了。

Shell 腳本

有時候可能還需要執行一些 make 中沒法移植的內容。由于 make 是通過 shell 來運行所有操作的,因此常見的解決方案是編寫一個內嵌的 shell 腳本來實現。下面是如何實現的過程。

首先,要知道 shell 腳本傳統上來講是在多行中編寫的,它們可以使用分號來分割語句,從而將整個腳本壓縮成一行。其次,要注意這樣做可讀性不好。解決方案是一種折衷:使用常見的縮進格式來編寫腳本,但是在每行后面都加上一個 “;\” 符號。這在語法上使用分號結束了一個 shell 命令,但卻會把一個 make 命令的文本部分一次傳遞給 shell。舉例來說,下面的代碼就可能會在某個最上層的 makefile 中出現:


清單 9. shell 腳本中的換行
all:for i in $(ALLDIRS) ; \do      ( cd $$i ; $(MAKE) all ) ; \done

其中給出了需要注意的 3 件事情。首先是分號和反斜線的用法。其次是 make 變量的用法 $(VARIABLE)。再次是使用 $$ 向 shell 傳遞一個 $ 符號。就是這樣,這實際上都非常簡單。

前綴

默認情況下,make 會打印出它所運行的每個命令,如果有任何命令失敗,make 就會停止執行。在某些情況中,可能會出現某個命令看起來失敗了,但是我們卻希望整個編譯過程繼續進行。如果一個命令的第一個字符是連字符(-),那么該行中剩余的命令都會執行,不過其退出狀態會被忽略。

如果并不希望回顯命令,可以在前面加上 @ 符號作為前綴。這是顯示消息最常用的方法:


清單 10. 禁止回顯
all:@echo "Beginning build at:"@date@echo "--------"

如果沒有 @ 符號,這就會產生下面的輸出:


清單 11. 沒有 @ 的命令
echo "Beginning build at:"
Beginning build at:
date
Sun Jun 18 01:13:21 CDT 2006
echo "--------"
--------

盡管 @ 符號不會真正改變 make 所做的事情,但是這卻是一種非常受歡迎的特性。

不可移植的功能

有些人們非常希望實現的事情卻不可移植。但是這些問題也有一些解決辦法。

包含文件

歷史上最難解決的一個兼容性問題是在 makefile 中對包含的處理。早先的 make 實現通常都沒有提供方法來實現這種功能,但是現代的一些 make 變種似乎看起來都對這個問題進行了妥善處理。GNU make 語法非常簡單,即include file。傳統的 Berkeley 語法是 .include "file"。至少有一種 Berkeley make 現在也可以支持 GNU 的符號了,但是目前還尚未全部支持。autoconfImake 所提供的可移植解決方案只是將所希望使用的每個變量的賦值都包含進來。

有些程序可能會簡單地要求使用 GNU make,有些則可能要求使用 Berkeley make,還有些可能要求使用 smake。如果需要包含的文件非常多,可以嘗試簡單指定一個 make 工具,用這個工具編譯一個樹(在這 3 種以源代碼形式發布的可移植 make 工具中,我最喜歡的是 Berkeley make)。

使用變量進行嵌套編譯

實際上并沒有什么好方法來做這件事情。如果使用了一個包含文件,就可能會遇到此文件是否被干凈地包含這樣的移植性問題。如果在每個文件中都設置了變量,那么就很難全部重載這些變量。如果只在一個頂層文件中設置這些變量,那么子目錄中一些獨立的編譯就會失敗,因為還沒有設置變量!

根據所使用的 make 版本的不同,一個理想的解決方案是在每個文件中都有條件地設置變量:只有在還沒有設置這些變量時才需要進行設置;然后頂層文件中的變化在完全編譯時就會影響到所有的子目錄。當然,此時如果單獨進入一個子目錄并運行 make 會產生不同的并且不兼容的結果。

如果所包含的文件不存在,這樣做的負面影響就會被放大,那些曾經在 Imake 數千行 makefile 中掙扎過的人都可以證明這點。

有些人提倡另外一種簡單的解決方案:根本就不要遞歸使用 make。對于大部分項目來說,這是絕對可行的,可以急劇簡化(并加速)整個編譯過程。 Peter Miller 撰寫的文章 “Recursive Make Considered Harmful”(請參閱參考資料)就是一個非常規范的例子。

當出現問題時應該怎樣做

首先,不要恐慌。開發人員在編寫出一個完整的版本之前,可能需要解決很多怪異的 make 問題。隱式規則、沒想到的變量替換以及嵌入式 shell 腳本中的語法錯誤,都可能會引發這種痛苦的享受。

此時需要仔細閱讀錯誤消息。這是 make 自己產生的消息么?還是 make 所調用的東西產生的消息?如果有一個嵌套的編譯,可能會需要通過對一組錯誤消息來仔細進行分析,才能找到確切的錯誤。

如果一個程序沒有找到,首先要檢查它是否已經安裝了。如果已經安裝了,那么就要檢查路徑設置是否正確;有些開發人員的習慣是在 makefile 中使用絕對路徑,這在其他系統上可能會失敗。如果將某些東西安裝到 /opt 中,而 makefile 引用的卻是 /usr/local/bin,那么編譯就會失敗。此時就需要修改路徑的設置。

檢查系統時鐘;更重要的是,要檢查編譯樹中文件的日期、系統中其他文件的日期以及系統的時鐘。在面臨輸入數據的時間順序不一致的情況時,make 的行為可能是無害的,也可能是不現實的。如果碰到了時鐘問題(例如有些 “新” 文件被標記成 1970 年的),那么就需要修整這個問題了。 “touch” 工具是一個很好的幫手。在時鐘問題中產生的錯誤消息通常都不太明顯。

如果看到的錯誤消息顯示有一些語法錯誤,或者有很多變量沒有設置,或設置得不正確,那么可以嘗試試驗一下其他版本的 make;舉例來說,有些程序在使用 gmake 編譯時會產生一些非常含糊的錯誤,而使用 smake 時就能很好地進行編譯。有些非常怪異的錯誤會說明正在使用 GNU make 來運行一個 Berkeley 的 makefile,反之亦然。Linux 特有的程序通常會假設使用 GNU make,使用其他 make 工具可能會碰到莫名其妙的錯誤,有些甚至在文檔中都沒有任何提示。

調試標記可能會非常有用。對于 GNU make ,-d 標記會提供大量的信息,其中有些是非常有用的。對于 Berkeley make ,-d 標記有一組標記;-d A 表示完整的集合,或者可以使用其中的一些子集;舉例來說,-d vx 會給出有關變量賦值(v)的調試信息,這會導致通過sh -x 來運行所有的命令,這樣 shell 就會精確地回顯自己接收到的命令。-n 調試標記會導致 make 打印它認為需要做的事情的一個列表;這并不總是正確的,不過通常可以為思考哪些地方出現了問題而提供一些思路。

在調試 makefile 時,目標是找到 make 正在試圖編譯什么東西,以及它認為哪些命令可以用來編譯。如果 make 使用了正確的命令,但命令卻出現了故障,那么這可能意味著完成了 make 調試 —— 但也許并不完全是。舉例來說,如果試圖編譯程序時由于存在無法解析的符號而失敗了,那么就可能是編譯過程前面某個步驟出現了問題!如果不能定位命令中哪兒出現了問題,并且它看起來應該正常工作,那么很可能是 make 前面創建的某個文件沒有被正確創建。

文檔

通常情況下, GNU make 的主要文檔都沒有以 man 格式提供,這一點非常不幸;我們只好使用 info 系統,而且不能運行 man make 來查找有關的信息。不過這些文檔還是非常齊全的。

要找到有關所有實現都能支持的特性的一個 “安全子集” 的文檔非常難。Berkeley 和 GNU make 文檔在描述擴展時都試圖提及這個問題,不過多做些測試總是個好事,這樣就不會全靠猜測去定義每個 make 工具的確切界限。

經過一段時間的發展,BSD 系列之間的微小偏移已經在 make 實現之間產生了一些差異。在三者之中,NetBSD 是 make 在其他系統上支持最為廣泛的;NetBSD pkgsrc 系統現在還在其他平臺上使用,它就嚴重依賴于 NetBSD 的 make 實現。


參考資料

學習

  • 您可以參閱本文在 developerWorks 全球站點上的 英文原文 。

  • “Recursive Make Considered Harmful” 介紹了與使用遞歸 make 有關的問題,將要解決的問題回溯到第一條規則,并提供了一種直觀的解決方案。

  • Peter 早期的文章 “調試 configure”(developerWorks,2003 年 12 月)為那些已經飽受配置腳本問題之苦的人們提供了幫助,并為開發人員提供了有關如何將故障最小化的建議。

  • 在 developerWorks Linux 專區 中可以找到為 Linux 開發人員準備的更多資源。

  • 隨時關注 developerWorks 技術事件和網絡廣播 。

獲得產品和技術

  • 訂購免費的 SEK for Linux,這有兩張 DVD,包括最新的 IBM for Linux 的試用版軟件,包括 DB2?、Lotus?、Rational?、Tivoli? 和 WebSphere?。

  • 使用 IBM 試用軟件 構建您的下一個 Linux 開發項目,這些軟件可以從 developerWorks 上直接下載。

討論

  • 通過參與 developerWorks blogs 加入 developerWorks 社區。

關于作者

作者照片

Peter Seebach 有多年使用計算機的經驗,已經逐漸變成了計算機高手。但是他仍然不知道為什么需要如此頻繁地清理鼠標。

Linux 系統內核的調試

樹雷 李 (lisl03@mails.tsinghua.edu.cn), 清華大學計算機系碩士研究生
渝 陳 (yuchen@tsinghua.edu.cn), 清華大學

簡介:?本文將首先介紹 Linux 內核上的一些內核代碼監視和錯誤跟蹤技術,這些調試和跟蹤方法因所要求的使用環境和使用方法而各有不同,然后重點介紹三種 Linux 內核的源代碼級的調試方法。

調試是軟件開發過程中一個必不可少的環節,在 Linux 內核開發的過程中也不可避免地會面對如何調試內核的問題。但是,Linux 系統的開發者出于保證內核代碼正確性的考慮,不愿意在 Linux 內核源代碼樹中加入一個調試器。他們認為內核中的調試器會誤導開發者,從而引入不良的修正[1]。所以對 Linux 內核進行調試一直是個令內核程序員感到棘手的問題,調試工作的艱苦性是內核級的開發區別于用戶級開發的一個顯著特點。

盡管缺乏一種內置的調試內核的有效方法,但是 Linux 系統在內核發展的過程中也逐漸形成了一些監視內核代碼和錯誤跟蹤的技術。同時,許多的補丁程序應運而生,它們為標準內核附加了內核調試的支持。盡管這些補丁有些并不被 Linux 官方組織認可,但他們確實功能完善,十分強大。調試內核問題時,利用這些工具與方法跟蹤內核執行情況,并查看其內存和數據結構將是非常有用的。

本文將首先介紹 Linux 內核上的一些內核代碼監視和錯誤跟蹤技術,這些調試和跟蹤方法因所要求的使用環境和使用方法而各有不同,然后重點介紹三種 Linux 內核的源代碼級的調試方法。

1. Linux 系統內核級軟件的調試技術

printk() 是調試內核代碼時最常用的一種技術。在內核代碼中的特定位置加入printk() 調試調用,可以直接把所關心的信息打打印到屏幕上,從而可以觀察程序的執行路徑和所關心的變量、指針等信息。 Linux 內核調試器(Linux kernel debugger,kdb)是 Linux 內核的補丁,它提供了一種在系統能運行時對內核內存和數據結構進行檢查的辦法。Oops、KDB在文章掌握Linux 調試技術有詳細介紹,大家可以參考。 Kprobes 提供了一個強行進入任何內核例程,并從中斷處理器無干擾地收集信息的接口。使用 Kprobes 可以輕松地收集處理器寄存器和全局數據結構等調試信息,而無需對Linux內核頻繁編譯和啟動,具體使用方法,請參考使用 Kprobes 調試內核。

以上介紹了進行Linux內核調試和跟蹤時的常用技術和方法。當然,內核調試與跟蹤的方法還不止以上提到的這些。這些調試技術的一個共同的特點在于,他們都不能提供源代碼級的有效的內核調試手段,有些只能稱之為錯誤跟蹤技術,因此這些方法都只能提供有限的調試能力。下面將介紹三種實用的源代碼級的內核調試方法。

2. 使用KGDB構建Linux內核調試環境

kgdb提供了一種使用 gdb調試 Linux 內核的機制。使用KGDB可以象調試普通的應用程序那樣,在內核中進行設置斷點、檢查變量值、單步跟蹤程序運行等操作。使用KGDB調試時需要兩臺機器,一臺作為開發機(Development Machine),另一臺作為目標機(Target Machine),兩臺機器之間通過串口或者以太網口相連。串口連接線是一根RS-232接口的電纜,在其內部兩端的第2腳(TXD)與第3腳(RXD)交叉相連,第7腳(接地腳)直接相連。調試過程中,被調試的內核運行在目標機上,gdb調試器運行在開發機上。

目前,kgdb發布支持i386、x86_64、32-bit PPC、SPARC等幾種體系結構的調試器。有關kgdb補丁的下載地址見參考資料[4]。

2.1 kgdb的調試原理

安裝kgdb調試環境需要為Linux內核應用kgdb補丁,補丁實現的gdb遠程調試所需要的功能包括命令處理、陷阱處理及串口通訊3個主要的部分。kgdb補丁的主要作用是在Linux內核中添加了一個調試Stub。調試Stub是Linux內核中的一小段代碼,提供了運行gdb的開發機和所調試內核之間的一個媒介。gdb和調試stub之間通過gdb串行協議進行通訊。gdb串行協議是一種基于消息的ASCII碼協議,包含了各種調試命令。當設置斷點時,kgdb負責在設置斷點的指令前增加一條trap指令,當執行到斷點時控制權就轉移到調試stub中去。此時,調試stub的任務就是使用遠程串行通信協議將當前環境傳送給gdb,然后從gdb處接受命令。gdb命令告訴stub下一步該做什么,當stub收到繼續執行的命令時,將恢復程序的運行環境,把對CPU的控制權重新交還給內核。



2.2 Kgdb的安裝與設置

下面我們將以Linux 2.6.7內核為例詳細介紹kgdb調試環境的建立過程。

2.2.1軟硬件準備

以下軟硬件配置取自筆者進行試驗的系統配置情況:



kgdb補丁的版本遵循如下命名模式:Linux-A-kgdb-B,其中A表示Linux的內核版本號,B為kgdb的版本號。以試驗使用的kgdb補丁為例,linux內核的版本為linux-2.6.7,補丁版本為kgdb-2.2。

物理連接好串口線后,使用以下命令來測試兩臺機器之間串口連接情況,stty命令可以對串口參數進行設置:

在development機上執行:


stty ispeed 115200 ospeed 115200 -F /dev/ttyS0

在target機上執行:


stty ispeed 115200 ospeed 115200 -F /dev/ttyS0

在developement機上執行:


echo hello > /dev/ttyS0

在target機上執行:


cat /dev/ttyS0

如果串口連接沒問題的話在將在target機的屏幕上顯示"hello"。

2.2.2 安裝與配置

下面我們需要應用kgdb補丁到Linux內核,設置內核選項并編譯內核。這方面的資料相對較少,筆者這里給出詳細的介紹。下面的工作在開發機(developement)上進行,以上面介紹的試驗環境為例,某些具體步驟在實際的環境中可能要做適當的改動:

I、內核的配置與編譯


[root@lisl tmp]# tar -jxvf linux-2.6.7.tar.bz2
[root@lisl tmp]#tar -jxvf linux-2.6.7-kgdb-2.2.tar.tar
[root@lisl tmp]#cd inux-2.6.7

請參照目錄補丁包中文件README給出的說明,執行對應體系結構的補丁程序。由于試驗在i386體系結構上完成,所以只需要安裝一下補丁:core-lite.patch、i386-lite.patch、8250.patch、eth.patch、core.patch、i386.patch。應用補丁文件時,請遵循kgdb軟件包內series文件所指定的順序,否則可能會帶來預想不到的問題。eth.patch文件是選擇以太網口作為調試的連接端口時需要運用的補丁

應用補丁的命令如下所示:


[root@lisl tmp]#patch -p1 <../linux-2.6.7-kgdb-2.2/core-lite.patch 

如果內核正確,那么應用補丁時應該不會出現任何問題(不會產生*.rej文件)。為Linux內核添加了補丁之后,需要進行內核的配置。內核的配置可以按照你的習慣選擇配置Linux內核的任意一種方式。


[root@lisl tmp]#make menuconfig

在內核配置菜單的Kernel hacking選項中選擇kgdb調試項,例如:


  [*] KGDB: kernel debugging with remote gdbMethod for KGDB communication (KGDB: On generic serial port (8250))  --->  [*] KGDB: Thread analysis [*] KGDB: Console messages through gdb
[root@lisl tmp]#make

編譯內核之前請注意Linux目錄下Makefile中的優化選項,默認的Linux內核的編譯都以-O2的優化級別進行。在這個優化級別之下,編譯器要對內核中的某些代碼的執行順序進行改動,所以在調試時會出現程序運行與代碼順序不一致的情況。可以把Makefile中的-O2選項改為-O,但不可去掉-O,否則編譯會出問題。為了使編譯后的內核帶有調試信息,注意在編譯內核的時候需要加上-g選項。

不過,當選擇"Kernel debugging->Compile the kernel with debug info"選項后配置系統將自動打開調試選項。另外,選擇"kernel debugging with remote gdb"后,配置系統將自動打開"Compile the kernel with debug info"選項。

內核編譯完成后,使用scp命令進行將相關文件拷貝到target機上(當然也可以使用其它的網絡工具,如rcp)。


[root@lisl tmp]#scp arch/i386/boot/bzImage root@192.168.6.13:/boot/vmlinuz-2.6.7-kgdb
[root@lisl tmp]#scp System.map root@192.168.6.13:/boot/System.map-2.6.7-kgdb

如果系統啟動使所需要的某些設備驅動沒有編譯進內核的情況下,那么還需要執行如下操作:


[root@lisl tmp]#mkinitrd /boot/initrd-2.6.7-kgdb 2.6.7
[root@lisl tmp]#scp initrd-2.6.7-kgdb root@192.168.6.13:/boot/ initrd-2.6.7-kgdb

II、kgdb的啟動

在將編譯出的內核拷貝的到target機器之后,需要配置系統引導程序,加入內核的啟動選項。以下是kgdb內核引導參數的說明:



如表中所述,在kgdb 2.0版本之后內核的引導參數已經與以前的版本有所不同。使用grub引導程序時,直接將kgdb參數作為內核vmlinuz的引導參數。下面給出引導器的配置示例。


title 2.6.7 kgdb
root (hd0,0)
kernel /boot/vmlinuz-2.6.7-kgdb ro root=/dev/hda1 kgdbwait kgdb8250=1,115200

在使用lilo作為引導程序時,需要把kgdb參放在由append修飾的語句中。下面給出使用lilo作為引導器時的配置示例。


image=/boot/vmlinuz-2.6.7-kgdb
label=kgdbread-onlyroot=/dev/hda3
append="gdb gdbttyS=1 gdbbaud=115200"

保存好以上配置后重新啟動計算機,選擇啟動帶調試信息的內核,內核將在短暫的運行后在創建init內核線程之前停下來,打印出以下信息,并等待開發機的連接。

Waiting for connection from remote gdb...

在開發機上執行:


gdb
file vmlinux
set remotebaud 115200
target remote /dev/ttyS0

其中vmlinux是指向源代碼目錄下編譯出來的Linux內核文件的鏈接,它是沒有經過壓縮的內核文件,gdb程序從該文件中得到各種符號地址信息。

這樣,就與目標機上的kgdb調試接口建立了聯系。一旦建立聯接之后,對Linux內的調試工作與對普通的運用程序的調試就沒有什么區別了。任何時候都可以通過鍵入ctrl+c打斷目標機的執行,進行具體的調試工作。

在kgdb 2.0之前的版本中,編譯內核后在arch/i386/kernel目錄下還會生成可執行文件gdbstart。將該文件拷貝到target機器的/boot目錄下,此時無需更改內核的啟動配置文件,直接使用命令:


[root@lisl boot]#gdbstart -s 115200 -t /dev/ttyS0

可以在KGDB內核引導啟動完成后建立開發機與目標機之間的調試聯系。

2.2.3 通過網絡接口進行調試

kgdb也支持使用以太網接口作為調試器的連接端口。在對Linux內核應用補丁包時,需應用eth.patch補丁文件。配置內核時在Kernel hacking中選擇kgdb調試項,配置kgdb調試端口為以太網接口,例如:


[*]KGDB: kernel debugging with remote gdb
Method for KGDB communication (KGDB: On ethernet)  ---> 
( ) KGDB: On generic serial port (8250)
(X) KGDB: On ethernet

另外使用eth0網口作為調試端口時,grub.list的配置如下:


title 2.6.7 kgdb
root (hd0,0)
kernel /boot/vmlinuz-2.6.7-kgdb ro root=/dev/hda1 kgdbwait kgdboe=@192.168.
5.13/,@192.168. 6.13/ 

其他的過程與使用串口作為連接端口時的設置過程相同。

注意:盡管可以使用以太網口作為kgdb的調試端口,使用串口作為連接端口更加簡單易行,kgdb項目組推薦使用串口作為調試端口。

2.2.4 模塊的調試方法

內核可加載模塊的調試具有其特殊性。由于內核模塊中各段的地址是在模塊加載進內核的時候才最終確定的,所以develop機的gdb無法得到各種符號地址信息。所以,使用kgdb調試模塊所需要解決的一個問題是,需要通過某種方法獲得可加載模塊的最終加載地址信息,并把這些信息加入到gdb環境中。

I、在Linux 2.4內核中的內核模塊調試方法

在Linux2.4.x內核中,可以使用insmod -m命令輸出模塊的加載信息,例如:


[root@lisl tmp]# insmod -m hello.ko >modaddr

查看模塊加載信息文件modaddr如下:


.this           00000060  c88d8000  2**2
.text           00000035  c88d8060  2**2
.rodata         00000069  c88d80a0  2**5
……
.data           00000000  c88d833c  2**2
.bss            00000000  c88d833c  2**2
……

在這些信息中,我們關心的只有4個段的地址:.text、.rodata、.data、.bss。在development機上將以上地址信息加入到gdb中,這樣就可以進行模塊功能的測試了。


(gdb) Add-symbol-file hello.o 0xc88d8060 -s .data 0xc88d80a0 -s 
.rodata 0xc88d80a0 -s .bss 0x c88d833c

這種方法也存在一定的不足,它不能調試模塊初始化的代碼,因為此時模塊初始化代碼已經執行過了。而如果不執行模塊的加載又無法獲得模塊插入地址,更不可能在模塊初始化之前設置斷點了。對于這種調試要求可以采用以下替代方法。

在target機上用上述方法得到模塊加載的地址信息,然后再用rmmod卸載模塊。在development機上將得到的模塊地址信息導入到gdb環境中,在內核代碼的調用初始化代碼之前設置斷點。這樣,在target機上再次插入模塊時,代碼將在執行模塊初始化之前停下來,這樣就可以使用gdb命令調試模塊初始化代碼了。

另外一種調試模塊初始化函數的方法是:當插入內核模塊時,內核模塊機制將調用函數sys_init_module(kernel/modle.c)執行對內核模塊的初始化,該函數將調用所插入模塊的初始化函數。程序代碼片斷如下:


……	……if (mod->init != NULL)ret = mod->init();
……	……

在該語句上設置斷點,也能在執行模塊初始化之前停下來。

II、在Linux 2.6.x內核中的內核模塊調試方法

Linux 2.6之后的內核中,由于module-init-tools工具的更改,insmod命令不再支持-m參數,只有采取其他的方法來獲取模塊加載到內核的地址。通過分析ELF文件格式,我們知道程序中各段的意義如下:

.text(代碼段):用來存放可執行文件的操作指令,也就是說是它是可執行程序在內存種的鏡像。

.data(數據段):數據段用來存放可執行文件中已初始化全局變量,也就是存放程序靜態分配的變量和全局變量。

.bss(BSS段):BSS段包含了程序中未初始化全局變量,在內存中 bss段全部置零。

.rodata(只讀段):該段保存著只讀數據,在進程映象中構造不可寫的段。

通過在模塊初始化函數中放置一下代碼,我們可以很容易地獲得模塊加載到內存中的地址。


……
int bss_var;
static int hello_init(void)
{
printk(KERN_ALERT "Text location .text(Code Segment):%p\n",hello_init);
static int data_var=0;
printk(KERN_ALERT "Data Location .data(Data Segment):%p\n",&data_var);
printk(KERN_ALERT "BSS Location: .bss(BSS Segment):%p\n",&bss_var);
……
}
Module_init(hello_init);

這里,通過在模塊的初始化函數中添加一段簡單的程序,使模塊在加載時打印出在內核中的加載地址。.rodata段的地址可以通過執行命令readelf -e hello.ko,取得.rodata在文件中的偏移量并加上段的align值得出。

為了使讀者能夠更好地進行模塊的調試,kgdb項目還發布了一些腳本程序能夠自動探測模塊的插入并自動更新gdb中模塊的符號信息。這些腳本程序的工作原理與前面解釋的工作過程相似,更多的信息請閱讀參考資料[4]。

2.2.5 硬件斷點

kgdb提供對硬件調試寄存器的支持。在kgdb中可以設置三種硬件斷點:執行斷點(Execution Breakpoint)、寫斷點(Write Breakpoint)、訪問斷點(Access Breakpoint)但不支持I/O訪問的斷點。目前,kgdb對硬件斷點的支持是通過宏來實現的,最多可以設置4個硬件斷點,這些宏的用法如下:



在有些情況下,硬件斷點的使用對于內核的調試是非常方便的。有關硬件斷點的定義和具體的使用說明見參考資料[4]

2.3.在VMware中搭建調試環境

kgdb調試環境需要使用兩臺微機分別充當development機和target機,使用VMware后我們只使用一臺計算機就可以順利完成kgdb調試環境的搭建。以windows下的環境為例,創建兩臺虛擬機,一臺作為開發機,一臺作為目標機。

2.3.1虛擬機之間的串口連接

虛擬機中的串口連接可以采用兩種方法。一種是指定虛擬機的串口連接到實際的COM上,例如開發機連接到COM1,目標機連接到COM2,然后把兩個串口通過串口線相連接。另一種更為簡便的方法是:在較高一些版本的VMware中都支持把串口映射到命名管道,把兩個虛擬機的串口映射到同一個命名管道。例如,在兩個虛擬機中都選定同一個命名管道\\.\pipe\com_1,指定target機的COM口為server端,并選擇"The other end is a virtual machine"屬性;指定development機的COM口端為client端,同樣指定COM口的"The other end is a virtual machine"屬性。對于IO mode屬性,在target上選中"Yield CPU on poll"復選擇框,development機不選。這樣,可以無需附加任何硬件,利用虛擬機就可以搭建kgdb調試環境。即降低了使用kgdb進行調試的硬件要求,也簡化了建立調試環境的過程。



2.3.2 VMware的使用技巧

VMware虛擬機是比較占用資源的,尤其是象上面那樣在Windows中使用兩臺虛擬機。因此,最好為系統配備512M以上的內存,每臺虛擬機至少分配128M的內存。這樣的硬件要求,對目前主流配置的PC而言并不是過高的要求。出于系統性能的考慮,在VMware中盡量使用字符界面進行調試工作。同時,Linux系統默認情況下開啟了sshd服務,建議使用SecureCRT登陸到Linux進行操作,這樣可以有較好的用戶使用界面。

2.3.3 在Linux下的虛擬機中使用kgdb

對于在Linux下面使用VMware虛擬機的情況,筆者沒有做過實際的探索。從原理上而言,只需要在Linux下只要創建一臺虛擬機作為target機,開發機的工作可以在實際的Linux環境中進行,搭建調試環境的過程與上面所述的過程類似。由于只需要創建一臺虛擬機,所以使用Linux下的虛擬機搭建kgdb調試環境對系統性能的要求較低。(vmware已經推出了Linux下的版本)還可以在development機上配合使用一些其他的調試工具,例如功能更強大的cgdb、圖形界面的DDD調試器等,以方便內核的調試工作。



2.4 kgdb的一些特點和不足

使用kgdb作為內核調試環境最大的不足在于對kgdb硬件環境的要求較高,必須使用兩臺計算機分別作為target和development機。盡管使用虛擬機的方法可以只用一臺PC即能搭建調試環境,但是對系統其他方面的性能也提出了一定的要求,同時也增加了搭建調試環境時復雜程度。另外,kgdb內核的編譯、配置也比較復雜,需要一定的技巧,筆者當時做的時候也是費了很多周折。當調試過程結束后時,還需要重新制作所要發布的內核。使用kgdb并不能進行全程調試,也就是說kgdb并不能用于調試系統一開始的初始化引導過程。

不過,kgdb是一個不錯的內核調試工具,使用它可以進行對內核的全面調試,甚至可以調試內核的中斷處理程序。如果在一些圖形化的開發工具的幫助下,對內核的調試將更方便。

3. 使用SkyEye構建Linux內核調試環境

SkyEye是一個開源軟件項目(OPenSource Software),SkyEye項目的目標是在通用的Linux和Windows平臺上模擬常見的嵌入式計算機系統。SkyEye實現了一個指令級的硬件模擬平臺,可以模擬多種嵌入式開發板,支持多種CPU指令集。SkyEye 的核心是 GNU 的 gdb 項目,它把gdb和 ARM Simulator很好地結合在了一起。加入ARMulator 的功能之后,它就可以來仿真嵌入式開發板,在它上面不僅可以調試硬件驅動,還可以調試操作系統。Skyeye項目目前已經在嵌入式系統開發領域得到了很大的推廣。

3.1 SkyEye的安裝和μcLinux內核編譯

3.1.1 SkyEye的安裝

SkyEye的安裝不是本文要介紹的重點,目前已經有大量的資料對此進行了介紹。有關SkyEye的安裝與使用的內容請查閱參考資料[11]。由于skyeye面目主要用于嵌入式系統領域,所以在skyeye上經常使用的是μcLinux系統,當然使用Linux作為skyeye上運行的系統也是可以的。由于介紹μcLinux 2.6在skyeye上編譯的相關資料并不多,所以下面進行詳細介紹。

3.1.2 μcLinux 2.6.x的編譯

要在SkyEye中調試操作系統內核,首先必須使被調試內核能在SkyEye所模擬的開發板上正確運行。因此,正確編譯待調試操作系統內核并配置SkyEye是進行內核調試的第一步。下面我們以SkyEye模擬基于Atmel AT91X40的開發板,并運行μcLinux 2.6為例介紹SkyEye的具體調試方法。

I、安裝交叉編譯環境

先安裝交叉編譯器。盡管在一些資料中說明使用工具鏈arm-elf-tools-20040427.sh ,但是由于arm-elf-xxx與arm-linux-xxx對宏及鏈接處理的不同,經驗證明使用arm-elf-xxx工具鏈在鏈接vmlinux的最后階段將會出錯。所以這里我們使用的交叉編譯工具鏈是:arm-uclinux-tools-base-gcc3.4.0-20040713.sh,關于該交叉編譯工具鏈的下載地址請參見[6]。注意以下步驟最好用root用戶來執行。


[root@lisl tmp]#chmod +x  arm-uclinux-tools-base-gcc3.4.0-20040713.sh
[root@lisl tmp]#./arm-uclinux-tools-base-gcc3.4.0-20040713.sh

安裝交叉編譯工具鏈之后,請確保工具鏈安裝路徑存在于系統PATH變量中。

II、制作μcLinux內核

得到μcLinux發布包的一個最容易的方法是直接訪問uClinux.org站點[7]。該站點發布的內核版本可能不是最新的,但你能找到一個最新的μcLinux補丁以及找一個對應的Linux內核版本來制作一個最新的μcLinux內核。這里,將使用這種方法來制作最新的μcLinux內核。目前(筆者記錄編寫此文章時),所能得到的發布包的最新版本是uClinux-dist.20041215.tar.gz。

下載uClinux-dist.20041215.tar.gz,文件的下載地址請參見[7]。

下載linux-2.6.9-hsc0.patch.gz,文件的下載地址請參見[8]。

下載linux-2.6.9.tar.bz2,文件的下載地址請參見[9]。

現在我們得到了整個的linux-2.6.9源代碼,以及所需的內核補丁。請準備一個有2GB空間的目錄里來完成以下制作μcLinux內核的過程。


[root@lisl tmp]# tar -jxvf uClinux-dist-20041215.tar.bz2
[root@lisl uClinux-dist]# tar -jxvf  linux-2.6.9.tar.bz2
[root@lisl uClinux-dist]# gzip -dc linux-2.6.9-hsc0.patch.gz | patch -p0 

或者使用:


[root@lisl uClinux-dist]# gunzip linux-2.6.9-hsc0.patch.gz 
[root@lisl uClinux-dist]patch -p0 < linux-2.6.9-hsc0.patch

執行以上過程后,將在linux-2.6.9/arch目錄下生成一個補丁目錄-armnommu。刪除原來μcLinux目錄里的linux-2.6.x(即那個linux-2.6.9-uc0),并將我們打好補丁的Linux內核目錄更名為linux-2.6.x。


[root@lisl uClinux-dist]# rm -rf linux-2.6.x/
[root@lisl uClinux-dist]# mv linux-2.6.9 linux-2.6.x

III、配置和編譯μcLinux內核

因為只是出于調試μcLinux內核的目的,這里沒有生成uClibc庫文件及romfs.img文件。在發布μcLinux時,已經預置了某些常用嵌入式開發板的配置文件,因此這里直接使用這些配置文件,過程如下:


[root@lisl uClinux-dist]# cd linux-2.6.x
[root@lisl linux-2.6.x]#make ARCH=armnommu CROSS_COMPILE=arm-uclinux- atmel_
deconfig

atmel_deconfig文件是μcLinux發布時提供的一個配置文件,存放于目錄linux-2.6.x /arch/armnommu/configs/中。


[root@lisl linux-2.6.x]#make ARCH=armnommu CROSS_COMPILE=arm-uclinux-
oldconfig

下面編譯配置好的內核:


[root@lisl linux-2.6.x]# make ARCH=armnommu CROSS_COMPILE=arm-uclinux- v=1

一般情況下,編譯將順利結束并在Linux-2.6.x/目錄下生成未經壓縮的μcLinux內核文件vmlinux。需要注意的是為了調試μcLinux內核,需要打開內核編譯的調試選項-g,使編譯后的內核帶有調試信息。打開編譯選項的方法可以選擇:

"Kernel debugging->Compile the kernel with debug info"后將自動打開調試選項。也可以直接修改linux-2.6.x目錄下的Makefile文件,為其打開調試開關。方法如下:。


CFLAGS  += -g 

最容易出現的問題是找不到arm-uclinux-gcc命令的錯誤,主要原因是PATH變量中沒有包含arm-uclinux-gcc命令所在目錄。在arm-linux-gcc的缺省安裝情況下,它的安裝目錄是/root/bin/arm-linux-tool/,使用以下命令將路徑加到PATH環境變量中。


Export PATH=$PATH:/root/bin/arm-linux-tool/bin

IV、根文件系統的制作

Linux內核在啟動的時的最后操作之一是加載根文件系統。根文件系統中存放了嵌入式系統使用的所有應用程序、庫文件及其他一些需要用到的服務。出于文章篇幅的考慮,這里不打算介紹根文件系統的制作方法,讀者可以查閱一些其他的相關資料。值得注意的是,由配置文件skyeye.conf指定了裝載到內核中的根文件系統。

3.2 使用SkyEye調試

編譯完μcLinux內核后,就可以在SkyEye中調試該ELF執行文件格式的內核了。前面已經說過利用SkyEye調試內核與使用gdb調試運用程序的方法相同。

需要提醒讀者的是,SkyEye的配置文件-skyeye.conf記錄了模擬的硬件配置和模擬執行行為。該配置文件是SkyEye系統中一個及其重要的文件,很多錯誤和異常情況的發生都和該文件有關。在安裝配置SkyEye出錯時,請首先檢查該配置文件然后再進行其他的工作。此時,所有的準備工作已經完成,就可以進行內核的調試工作了。

3.3使用SkyEye調試內核的特點和不足

在SkyEye中可以進行對Linux系統內核的全程調試。由于SkyEye目前主要支持基于ARM內核的CPU,因此一般而言需要使用交叉編譯工具編譯待調試的Linux系統內核。另外,制作SkyEye中使用的內核編譯、配置過程比較復雜、繁瑣。不過,當調試過程結束后無需重新制作所要發布的內核。

SkyEye只是對系統硬件進行了一定程度上的模擬,所以在SkyEye與真實硬件環境相比較而言還是有一定的差距,這對一些與硬件緊密相關的調試可能會有一定的影響,例如驅動程序的調試。不過對于大部分軟件的調試,SkyEye已經提供了精度足夠的模擬了。

SkyEye的下一個目標是和eclipse結合,有了圖形界面,能為調試和查看源碼提供一些方便。

4. 使用UML調試Linux內核

User-mode Linux(UML)簡單說來就是在Linux內運行的Linux。該項目是使Linux內核成為一個運行在 Linux 系統之上單獨的、用戶空間的進程。UML并不是運行在某種新的硬件體系結構之上,而是運行在基于 Linux 系統調用接口所實現的虛擬機。正是由于UML是一個將Linux作為用戶空間進程運行的特性,可以使用UML來進行操作系統內核的調試。有關UML的介紹請查閱參考資料[10]、[12]。

4.1 UML的安裝與調試

UML的安裝需要一臺運行Linux 2.2.15以上,或者2.3.22以上的I386機器。對于2.6.8及其以前版本的UML,采用兩種形式發布:一種是以RPM包的形式發布,一種是以源代碼的形式提供UML的安裝。按照UML的說明,以RPM形式提供的安裝包比較陳舊且會有許多問題。以二進制形式發布的UML包并不包含所需要的調試信息,這些代碼在發布時已經做了程度不同的優化。所以,要想利用UML調試Linux系統內核,需要使用最新的UML patch代碼和對應版本的Linux內核編譯、安裝UML。完成UML的補丁之后,會在arch目錄下產生一個um目錄,主要的UML代碼都放在該目錄下。

從2.6.9版本之后(包含2.6.9版本的Linux),User-Mode Linux已經隨Linux內核源代碼樹一起發布,它存放于arch/um目錄下。

編譯好UML的內核之后,直接使用gdb運行已經編譯好的內核即可進行調試。

4.2使用UML調試系統內核的特點和不足

目前,用戶模式 Linux 虛擬機也存在一定的局限性。由于UML虛擬機是基于Linux系統調用接口的方式實現的虛擬機,所以用戶模式內核不能訪問主機系統上的硬件設備。因此,UML并不適合于調試那些處理實際硬件的驅動程序。不過,如果所編寫的內核程序不是硬件驅動,例如Linux文件系統、協議棧等情況,使用UML作為調試工具還是一個不錯的選擇。

5. 內核調試配置選項

為了方便調試和測試代碼,內核提供了許多與內核調試相關的配置選項。這些選項大部分都在內核配置編輯器的內核開發(kernel hacking)菜單項中。在內核配置目錄樹菜單的其他地方也還有一些可配置的調試選項,下面將對他們作一定的介紹。

Page alloc debugging :CONFIG_DEBUG_PAGEALLOC:

不使用該選項時,釋放的內存頁將從內核地址空間中移出。使用該選項后,內核推遲移出內存頁的過程,因此能夠發現內存泄漏的錯誤。

Debug memory allocations :CONFIG_DEBUG_SLAB:

該打開該選項時,在內核執行內存分配之前將執行多種類型檢查,通過這些類型檢查可以發現諸如內核過量分配或者未初始化等錯誤。內核將會在每次分配內存前后時設置一些警戒值,如果這些值發生了變化那么內核就會知道內存已經被操作過并給出明確的提示,從而使各種隱晦的錯誤變得容易被跟蹤。

Spinlock debugging :CONFIG_DEBUG_SPINLOCK:

打開此選項時,內核將能夠發現spinlock未初始化及各種其他的錯誤,能用于排除一些死鎖引起的錯誤。

Sleep-inside-spinlock checking:CONFIG_DEBUG_SPINLOCK_SLEEP:

打開該選項時,當spinlock的持有者要睡眠時會執行相應的檢查。實際上即使調用者目前沒有睡眠,而只是存在睡眠的可能性時也會給出提示。

Compile the kernel with debug info :CONFIG_DEBUG_INFO:

打開該選項時,編譯出的內核將會包含全部的調試信息,使用gdb時需要這些調試信息。

Stack utilization instrumentation :CONFIG_DEBUG_STACK_USAGE:

該選項用于跟蹤內核棧的溢出錯誤,一個內核棧溢出錯誤的明顯的現象是產生oops錯誤卻沒有列出系統的調用棧信息。該選項將使內核進行棧溢出檢查,并使內核進行棧使用的統計。

Driver Core verbose debug messages:CONFIG_DEBUG_DRIVER:

該選項位于"Device drivers-> Generic Driver Options"下,打開該選項使得內核驅動核心產生大量的調試信息,并將他們記錄到系統日志中。

Verbose SCSI error reporting (kernel size +=12K) :CONFIG_SCSI_CONSTANTS:

該選項位于"Device drivers/SCSI device support"下。當SCSI設備出錯時內核將給出詳細的出錯信息。

Event debugging:CONFIG_INPUT_EVBUG:

打開該選項時,會將輸入子系統的錯誤及所有事件都輸出到系統日志中。該選項在產生了詳細的輸入報告的同時,也會導致一定的安全問題。

以上內核編譯選項需要讀者根據自己所進行的內核編程的實際情況,靈活選取。在使用以上介紹的三種源代碼級的內核調試工具時,一般需要選取CONFIG_DEBUG_INFO選項,以使編譯的內核包含調試信息。

6. 總結

上面介紹了一些調試Linux內核的方法,特別是詳細介紹了三種源代碼級的內核調試工具,以及搭建這些內核調試環境的方法,讀者可以根據自己的情況從中作出選擇。

調試工具(例如gdb)的運行都需要操作系統的支持,而此時內核由于一些錯誤的代碼而不能正確執行對系統的管理功能,所以對內核的調試必須采取一些特殊的方法進行。以上介紹的三種源代碼級的調試方法,可以歸納為以下兩種策略:

I、為內核增加調試Stub,利用調試Stub進行遠程調試,這種調試策略需要target及development機器才能完成調試任務。

II、將虛擬機技術與調試工具相結合,使Linux內核在虛擬機中運行從而利用調試器對內核進行調試。這種策略需要制作適合在虛擬機中運行的系統內核。

由不同的調試策略決定了進行調試時不同的工作原理,同時也形成了各種調試方法不同的軟硬件需求和各自的特點。

另外,需要說明的是內核調試能力的掌握很大程度上取決于經驗和對整個操作系統的深入理解。對系統內核的全面深入的理解,將能在很大程度上加快對Linux系統內核的開發和調試。

對系統內核的調試技術和方法絕不止上面介紹所涉及的內容,這里只是介紹了一些經常看到和聽到方法。在Linux內核向前發展的同時,內核的調試技術也在不斷的進步。希望以上介紹的一些方法能對讀者開發和學習Linux有所幫助。

參考資料

[1] http://oss.sgi.com/projects/kdb/

[2] http://www.ibm.com/developerworks/cn/linux/sdk/l-debug/index.html

[3] http://www.ibm.com/developerworks/cn/linux/l-kdbug/

[4] http://www.ibm.com/developerworks/cn/linux/l-kprobes.html

[5] http://kgdb.linsyssoft.com/downloads.htm

[6] ftp://166.111.68.183

[8] http://www.uclinux.org/pub/uClinux/dist/

[9] http://opensrc.sec.samsung.com/download/linux-2.6.9-hsc0.patch.gz

[10] http:// www.kernel.org

[11] http://user-mode-linux.sourceforge.net/

[12] http://www.ibm.com/developerworks/cn/linux/l-skyeye/part1/

[13] http://www.ibm.com/developerworks/cn/views/linux/tutorials.jsp?cv_doc_id=84978

參考文獻

[1]Robert Love Linux kernel development機械工業出版社

[2]陳渝 源代碼開發的嵌入式系統軟件分析與實踐 北京航空航天大學出版社

[3]Alessandro Rubini Linux device driver 2se Edition O'Reilly

[4]Jonathan Corbet Linux device driver 3rd Edition O'Reilly

[5]李善平 Linux內核源代碼分析大全 機械工業出版社


作者簡介

李樹雷,清華大學計算機系碩士研究生,主要從事操作系統與中間件的研究。通過lisl03@mails.tsinghua.edu.cn 可以跟他聯系


Shell腳本調試技術

曹 羽中 (caoyuz@cn.ibm.com), 軟件工程師, IBM中國開發中心

簡介:?本文全面系統地介紹了shell腳本調試技術,包括使用echo, tee, trap等命令輸出關鍵信息,跟蹤變量的值,在腳本中植入調試鉤子,使用“-n”選項進行shell腳本的語法檢查, 使用“-x”選項實現shell腳本逐條語句的跟蹤,巧妙地利用shell的內置變量增強“-x”選項的輸出信息等。

一. 前言

shell編程在unix/linux世界中使用得非常廣泛,熟練掌握shell編程也是成為一名優秀的unix/linux開發者和系統管理員的必經之路。腳本調試的主要工作就是發現引發腳本錯誤的原因以及在腳本源代碼中定位發生錯誤的行,常用的手段包括分析輸出的錯誤信息,通過在腳本中加入調試語句,輸出調試信息來輔助診斷錯誤,利用調試工具等。但與其它高級語言相比,shell解釋器缺乏相應的調試機制和調試工具的支持,其輸出的錯誤信息又往往很不明確,初學者在調試腳本時,除了知道用echo語句輸出一些信息外,別無它法,而僅僅依賴于大量的加入echo語句來診斷錯誤,確實令人不勝其繁,故常見初學者抱怨shell腳本太難調試了。本文將系統地介紹一些重要的shell腳本調試技術,希望能對shell的初學者有所裨益。

本文的目標讀者是unix/linux環境下的開發人員,測試人員和系統管理員,要求讀者具有基本的shell編程知識。本文所使用范例在Bash3.1+Redhat Enterprise Server 4.0下測試通過,但所述調試技巧應也同樣適用于其它shell。

二. 在shell腳本中輸出調試信息

通過在程序中加入調試語句把一些關鍵地方或出錯的地方的相關信息顯示出來是最常見的調試手段。Shell程序員通常使用echo(ksh程序員常使用print)語句輸出信息,但僅僅依賴echo語句的輸出跟蹤信息很麻煩,調試階段在腳本中加入的大量的echo語句在產品交付時還得再費力一一刪除。針對這個問題,本節主要介紹一些如何方便有效的輸出調試信息的方法。

1. 使用trap命令

trap命令用于捕獲指定的信號并執行預定義的命令。
其基本的語法是:
trap 'command' signal
其中signal是要捕獲的信號,command是捕獲到指定的信號之后,所要執行的命令。可以用kill –l命令看到系統中全部可用的信號名,捕獲信號后所執行的命令可以是任何一條或多條合法的shell語句,也可以是一個函數名。
shell腳本在執行時,會產生三個所謂的“偽信號”,(之所以稱之為“偽信號”是因為這三個信號是由shell產生的,而其它的信號是由操作系統產生的),通過使用trap命令捕獲這三個“偽信號”并輸出相關信息對調試非常有幫助。


表 1. shell偽信號
信號名何時產生
EXIT從一個函數中退出或整個腳本執行完畢
ERR當一條命令返回非零狀態時(代表命令執行不成功)
DEBUG腳本中每一條命令執行之前

通過捕獲EXIT信號,我們可以在shell腳本中止執行或從函數中退出時,輸出某些想要跟蹤的變量的值,并由此來判斷腳本的執行狀態以及出錯原因,其使用方法是:
trap 'command' EXIT 或 trap 'command' 0

通過捕獲ERR信號,我們可以方便的追蹤執行不成功的命令或函數,并輸出相關的調試信息,以下是一個捕獲ERR信號的示例程序,其中的$LINENO是一個shell的內置變量,代表shell腳本的當前行號。

$ cat -n exp1.sh1  ERRTRAP()2  {3    echo "[LINE:$1] Error: Command or function exited with status $?"4  }5  foo()6  {7    return 1;8  }9  trap 'ERRTRAP $LINENO' ERR10  abc11  foo

其輸出結果如下:

$ sh exp1.sh
exp1.sh: line 10: abc: command not found
[LINE:10] Error: Command or function exited with status 127
[LINE:11] Error: Command or function exited with status 1

在調試過程中,為了跟蹤某些變量的值,我們常常需要在shell腳本的許多地方插入相同的echo語句來打印相關變量的值,這種做法顯得煩瑣而笨拙。而通過捕獲DEBUG信號,我們只需要一條trap語句就可以完成對相關變量的全程跟蹤。

以下是一個通過捕獲DEBUG信號來跟蹤變量的示例程序:

$ cat –n exp2.sh1  #!/bin/bash2  trap 'echo “before execute line:$LINENO, a=$a,b=$b,c=$c”' DEBUG3  a=14  if [ "$a" -eq 1 ]5  then6     b=27  else8     b=19  fi10  c=311  echo "end"

其輸出結果如下:

$ sh exp2.sh
before execute line:3, a=,b=,c=
before execute line:4, a=1,b=,c=
before execute line:6, a=1,b=,c=
before execute line:10, a=1,b=2,c=
before execute line:11, a=1,b=2,c=3
end

從運行結果中可以清晰的看到每執行一條命令之后,相關變量的值的變化。同時,從運行結果中打印出來的行號來分析,可以看到整個腳本的執行軌跡,能夠判斷出哪些條件分支執行了,哪些條件分支沒有執行。

2. 使用tee命令

在shell腳本中管道以及輸入輸出重定向使用得非常多,在管道的作用下,一些命令的執行結果直接成為了下一條命令的輸入。如果我們發現由管道連接起來的一批命令的執行結果并非如預期的那樣,就需要逐步檢查各條命令的執行結果來判斷問題出在哪兒,但因為使用了管道,這些中間結果并不會顯示在屏幕上,給調試帶來了困難,此時我們就可以借助于tee命令了。

tee命令會從標準輸入讀取數據,將其內容輸出到標準輸出設備,同時又可將內容保存成文件。例如有如下的腳本片段,其作用是獲取本機的ip地址:

ipaddr=`/sbin/ifconfig | grep 'inet addr:' | grep -v '127.0.0.1'
| cut -d : -f3 | awk '{print $1}'` 
#注意=號后面的整句是用反引號(數字1鍵的左邊那個鍵)括起來的。
echo $ipaddr

運行這個腳本,實際輸出的卻不是本機的ip地址,而是廣播地址,這時我們可以借助tee命令,輸出某些中間結果,將上述腳本片段修改為:

ipaddr=`/sbin/ifconfig | grep 'inet addr:' | grep -v '127.0.0.1'
| tee temp.txt | cut -d : -f3 | awk '{print $1}'`
echo $ipaddr

之后,將這段腳本再執行一遍,然后查看temp.txt文件的內容:

$ cat temp.txt
inet addr:192.168.0.1  Bcast:192.168.0.255  Mask:255.255.255.0

我們可以發現中間結果的第二列(列之間以:號分隔)才包含了IP地址,而在上面的腳本中使用cut命令截取了第三列,故我們只需將腳本中的cut -d : -f3改為cut -d : -f2即可得到正確的結果。

具體到上述的script例子,我們也許并不需要tee命令的幫助,比如我們可以分段執行由管道連接起來的各條命令并查看各命令的輸出結果來診斷錯誤,但在一些復雜的shell腳本中,這些由管道連接起來的命令可能又依賴于腳本中定義的一些其它變量,這時我們想要在提示符下來分段運行各條命令就會非常麻煩了,簡單地在管道之間插入一條tee命令來查看中間結果會更方便一些。

3. 使用"調試鉤子"

在C語言程序中,我們經常使用DEBUG宏來控制是否要輸出調試信息,在shell腳本中我們同樣可以使用這樣的機制,如下列代碼所示:

if [ “$DEBUG” = “true” ]; then
echo “debugging”  #此處可以輸出調試信息
fi

這樣的代碼塊通常稱之為“調試鉤子”或“調試塊”。在調試鉤子內部可以輸出任何您想輸出的調試信息,使用調試鉤子的好處是它是可以通過DEBUG變量來控制的,在腳本的開發調試階段,可以先執行export DEBUG=true命令打開調試鉤子,使其輸出調試信息,而在把腳本交付使用時,也無需再費事把腳本中的調試語句一一刪除。

如果在每一處需要輸出調試信息的地方均使用if語句來判斷DEBUG變量的值,還是顯得比較繁瑣,通過定義一個DEBUG函數可以使植入調試鉤子的過程更簡潔方便,如下面代碼所示:

$ cat –n exp3.sh1  DEBUG()2  {3  if [ "$DEBUG" = "true" ]; then4      $@  5  fi6  }7  a=18  DEBUG echo "a=$a"9  if [ "$a" -eq 1 ]10  then11       b=212  else13       b=114  fi15  DEBUG echo "b=$b"16  c=317  DEBUG echo "c=$c"

在上面所示的DEBUG函數中,會執行任何傳給它的命令,并且這個執行過程是可以通過DEBUG變量的值來控制的,我們可以把所有跟調試有關的命令都作為DEBUG函數的參數來調用,非常的方便。

三. 使用shell的執行選項

上一節所述的調試手段是通過修改shell腳本的源代碼,令其輸出相關的調試信息來定位錯誤的,那有沒有不修改源代碼來調試shell腳本的方法呢?答案就是使用shell的執行選項,本節將介紹一些常用選項的用法:

-n 只讀取shell腳本,但不實際執行
-x 進入跟蹤方式,顯示所執行的每一條命令
-c "string" 從strings中讀取命令

“-n”可用于測試shell腳本是否存在語法錯誤,但不會實際執行命令。在shell腳本編寫完成之后,實際執行之前,首先使用“-n”選項來測試腳本是否存在語法錯誤是一個很好的習慣。因為某些shell腳本在執行時會對系統環境產生影響,比如生成或移動文件等,如果在實際執行才發現語法錯誤,您不得不手工做一些系統環境的恢復工作才能繼續測試這個腳本。

“-c”選項使shell解釋器從一個字符串中而不是從一個文件中讀取并執行shell命令。當需要臨時測試一小段腳本的執行結果時,可以使用這個選項,如下所示:
sh -c 'a=1;b=2;let c=$a+$b;echo "c=$c"'

"-x"選項可用來跟蹤腳本的執行,是調試shell腳本的強有力工具。“-x”選項使shell在執行腳本的過程中把它實際執行的每一個命令行顯示出來,并且在行首顯示一個"+"號。"+"號后面顯示的是經過了變量替換之后的命令行的內容,有助于分析實際執行的是什么命令。 “-x”選項使用起來簡單方便,可以輕松對付大多數的shell調試任務,應把其當作首選的調試手段。

如果把本文前面所述的trap ‘command’ DEBUG機制與“-x”選項結合起來,我們就可以既輸出實際執行的每一條命令,又逐行跟蹤相關變量的值,對調試相當有幫助。

仍以前面所述的exp2.sh為例,現在加上“-x”選項來執行它:

$ sh –x exp2.sh
+ trap 'echo "before execute line:$LINENO, a=$a,b=$b,c=$c"' DEBUG
++ echo 'before execute line:3, a=,b=,c='
before execute line:3, a=,b=,c=
+ a=1
++ echo 'before execute line:4, a=1,b=,c='
before execute line:4, a=1,b=,c=
+ '[' 1 -eq 1 ']'
++ echo 'before execute line:6, a=1,b=,c='
before execute line:6, a=1,b=,c=
+ b=2
++ echo 'before execute line:10, a=1,b=2,c='
before execute line:10, a=1,b=2,c=
+ c=3
++ echo 'before execute line:11, a=1,b=2,c=3'
before execute line:11, a=1,b=2,c=3
+ echo end
end

在上面的結果中,前面有“+”號的行是shell腳本實際執行的命令,前面有“++”號的行是執行trap機制中指定的命令,其它的行則是輸出信息。

shell的執行選項除了可以在啟動shell時指定外,亦可在腳本中用set命令來指定。"set -參數"表示啟用某選項,"set +參數"表示關閉某選項。有時候我們并不需要在啟動時用"-x"選項來跟蹤所有的命令行,這時我們可以在腳本中使用set命令,如以下腳本片段所示:

set -x    #啟動"-x"選項 
要跟蹤的程序段 
set +x     #關閉"-x"選項

set命令同樣可以使用上一節中介紹的調試鉤子—DEBUG函數來調用,這樣可以避免腳本交付使用時刪除這些調試語句的麻煩,如以下腳本片段所示:

DEBUG set -x    #啟動"-x"選項 
要跟蹤的程序段 
DEBUG set +x    #關閉"-x"選項

四. 對"-x"選項的增強

"-x"執行選項是目前最常用的跟蹤和調試shell腳本的手段,但其輸出的調試信息僅限于進行變量替換之后的每一條實際執行的命令以及行首的一個"+"號提示符,居然連行號這樣的重要信息都沒有,對于復雜的shell腳本的調試來說,還是非常的不方便。幸運的是,我們可以巧妙地利用shell內置的一些環境變量來增強"-x"選項的輸出信息,下面先介紹幾個shell內置的環境變量:

$LINENO
代表shell腳本的當前行號,類似于C語言中的內置宏__LINE__

$FUNCNAME
函數的名字,類似于C語言中的內置宏__func__,但宏__func__只能代表當前所在的函數名,而$FUNCNAME的功能更強大,它是一個數組變量,其中包含了整個調用鏈上所有的函數的名字,故變量${FUNCNAME[0]}代表shell腳本當前正在執行的函數的名字,而變量${FUNCNAME[1]}則代表調用函數${FUNCNAME[0]}的函數的名字,余者可以依此類推。

$PS4
主提示符變量$PS1和第二級提示符變量$PS2比較常見,但很少有人注意到第四級提示符變量$PS4的作用。我們知道使用“-x”執行選項將會顯示shell腳本中每一條實際執行過的命令,而$PS4的值將被顯示在“-x”選項輸出的每一條命令的前面。在Bash Shell中,缺省的$PS4的值是"+"號。(現在知道為什么使用"-x"選項時,輸出的命令前面有一個"+"號了吧?)。

利用$PS4這一特性,通過使用一些內置變量來重定義$PS4的值,我們就可以增強"-x"選項的輸出信息。例如先執行export PS4='+{$LINENO:${FUNCNAME[0]}} ', 然后再使用“-x”選項來執行腳本,就能在每一條實際執行的命令前面顯示其行號以及所屬的函數名。

以下是一個存在bug的shell腳本的示例,本文將用此腳本來示范如何用“-n”以及增強的“-x”執行選項來調試shell腳本。這個腳本中定義了一個函數isRoot(),用于判斷當前用戶是不是root用戶,如果不是,則中止腳本的執行

$ cat –n exp4.sh1  #!/bin/bash2  isRoot()3  {4          if [ "$UID" -ne 0 ]5                  return 16          else7                  return 08          fi9  }10  isRoot11  if ["$?" -ne 0 ]12  then13          echo "Must be root to run this script"14          exit 115  else16          echo "welcome root user"17          #do something18  fi

首先執行sh –n exp4.sh來進行語法檢查,輸出如下:

$ sh –n exp4.sh
exp4.sh: line 6: syntax error near unexpected token `else'
exp4.sh: line 6: `      else'

發現了一個語法錯誤,通過仔細檢查第6行前后的命令,我們發現是第4行的if語句缺少then關鍵字引起的(寫慣了C程序的人很容易犯這個錯誤)。我們可以把第4行修改為if [ "$UID" -ne 0 ]; then來修正這個錯誤。再次運行sh –n exp4.sh來進行語法檢查,沒有再報告錯誤。接下來就可以實際執行這個腳本了,執行結果如下:

$ sh exp4.sh
exp2.sh: line 11: [1: command not found
welcome root user

盡管腳本沒有語法錯誤了,在執行時卻又報告了錯誤。錯誤信息還非常奇怪“[1: command not found”。現在我們可以試試定制$PS4的值,并使用“-x”選項來跟蹤:

$ export PS4='+{$LINENO:${FUNCNAME[0]}} '
$ sh –x exp4.sh
+{10:} isRoot
+{4:isRoot} '[' 503 -ne 0 ']'
+{5:isRoot} return 1
+{11:} '[1' -ne 0 ']'
exp4.sh: line 11: [1: command not found
+{16:} echo 'welcome root user'
welcome root user

從輸出結果中,我們可以看到腳本實際被執行的語句,該語句的行號以及所屬的函數名也被打印出來,從中可以清楚的分析出腳本的執行軌跡以及所調用的函數的內部執行情況。由于執行時是第11行報錯,這是一個if語句,我們對比分析一下同為if語句的第4行的跟蹤結果:

+{4:isRoot} '[' 503 -ne 0 ']'
+{11:} '[1' -ne 0 ']'

可知由于第11行的[號后面缺少了一個空格,導致[號與緊挨它的變量$?的值1被shell解釋器看作了一個整體,并試著把這個整體視為一個命令來執行,故有“[1: command not found”這樣的錯誤提示。只需在[號后面插入一個空格就一切正常了。

shell中還有其它一些對調試有幫助的內置變量,比如在Bash Shell中還有BASH_SOURCE, BASH_SUBSHELL等一批對調試有幫助的內置變量,您可以通過man sh或man bash來查看,然后根據您的調試目的,使用這些內置變量來定制$PS4,從而達到增強“-x”選項的輸出信息的目的。

五. 總結

現在讓我們來總結一下調試shell腳本的過程:
首先使用“-n”選項檢查語法錯誤,然后使用“-x”選項跟蹤腳本的執行,使用“-x”選項之前,別忘了先定制PS4變量的值來增強“-x”選項的輸出信息,至少應該令其輸出行號信息(先執行export PS4='+[$LINENO]',更一勞永逸的辦法是將這條語句加到您用戶主目錄的.bash_profile文件中去),這將使你的調試之旅更輕松。也可以利用trap,調試鉤子等手段輸出關鍵調試信息,快速縮小排查錯誤的范圍,并在腳本中使用“set -x”及“set +x”對某些代碼塊進行重點跟蹤。這樣多種手段齊下,相信您已經可以比較輕松地抓出您的shell腳本中的臭蟲了。如果您的腳本足夠復雜,還需要更強的調試能力,可以使用shell調試器bashdb,這是一個類似于GDB的調試工具,可以完成對shell腳本的斷點設置,單步執行,變量觀察等許多功能,使用bashdb對閱讀和理解復雜的shell腳本也會大有裨益。關于bashdb的安裝和使用,不屬于本文范圍,您可參閱http://bashdb.sourceforge.net/上的文檔并下載試用。


參考資料

  • 請訪問: GNU 的 bash 主頁

  • 請下載和試用 Shell調試器bashdb

關于作者

曹羽中,在北京航空航天大學獲得計算機軟件與理論專業的碩士學位,具有數年的 unix 環境下的 C 語言,Java,數據庫以及電信計費軟件的開發經驗,他的技術興趣還包括 OSGi 和搜索技術。他目前在IBM中國系統與科技實驗室從事系統管理軟件的開發工作,可以通過caoyuz@cn.ibm.com與他聯系。


使用 GDB 調試多進程程序

田 強 (tianq@cn.ibm.com), 軟件工程師, IBM中國軟件開發中心

簡介:?GDB 是 linux 系統上常用的調試工具,本文介紹了使用 GDB 調試多進程程序的幾種方法,并對各種方法進行比較。

GDB 是 linux 系統上常用的 c/c++ 調試工具,功能十分強大。對于較為復雜的系統,比如多進程系統,如何使用 GDB 調試呢?考慮下面這個三進程系統:


進程
進程

Proc2 是 Proc1 的子進程,Proc3 又是 Proc2 的子進程。如何使用 GDB 調試 proc2 或者 proc3 呢?

實際上,GDB 沒有對多進程程序調試提供直接支持。例如,使用GDB調試某個進程,如果該進程fork了子進程,GDB會繼續調試該進程,子進程會不受干擾地運行下去。如果你事先在子進程代碼里設定了斷點,子進程會收到SIGTRAP信號并終止。那么該如何調試子進程呢?其實我們可以利用GDB的特點或者其他一些輔助手段來達到目的。此外,GDB 也在較新內核上加入一些多進程調試支持。

接下來我們詳細介紹幾種方法,分別是 follow-fork-mode 方法,attach 子進程方法和 GDB wrapper 方法。

follow-fork-mode

在2.5.60版Linux內核及以后,GDB對使用fork/vfork創建子進程的程序提供了follow-fork-mode選項來支持多進程調試。

follow-fork-mode的用法為:

set follow-fork-mode [parent|child]

  • parent: fork之后繼續調試父進程,子進程不受影響。
  • child: fork之后調試子進程,父進程不受影響。

因此如果需要調試子進程,在啟動gdb后:

(gdb) set follow-fork-mode child

并在子進程代碼設置斷點。

此外還有detach-on-fork參數,指示GDB在fork之后是否斷開(detach)某個進程的調試,或者都交由GDB控制:

set detach-on-fork [on|off]

  • on: 斷開調試follow-fork-mode指定的進程。
  • off: gdb將控制父進程和子進程。follow-fork-mode指定的進程將被調試,另一個進程置于暫停(suspended)狀態。

注意,最好使用GDB 6.6或以上版本,如果你使用的是GDB6.4,就只有follow-fork-mode模式。

follow-fork-mode/detach-on-fork的使用還是比較簡單的,但由于其系統內核/gdb版本限制,我們只能在符合要求的系統上才能使用。而且,由于follow-fork-mode的調試必然是從父進程開始的,對于fork多次,以至于出現孫進程或曾孫進程的系統,例如上圖3進程系統,調試起來并不方便。

Attach子進程

眾所周知,GDB有附著(attach)到正在運行的進程的功能,即attach <pid>命令。因此我們可以利用該命令attach到子進程然后進行調試。

例如我們要調試某個進程RIM_Oracle_Agent.9i,首先得到該進程的pid

[root@tivf09 tianq]# ps -ef|grep RIM_Oracle_Agent.9i
nobody    6722  6721  0 05:57 ?        00:00:00 RIM_Oracle_Agent.9i
root      7541 27816  0 06:10 pts/3    00:00:00 grep -i rim_oracle_agent.9i

通過pstree可以看到,這是一個三進程系統,oserv是RIM_Oracle_prog的父進程,RIM_Oracle_prog又是RIM_Oracle_Agent.9i的父進程。

[root@tivf09 root]# pstree -H 6722


通過 pstree 察看進程
通過 pstree 察看進程

啟動GDB,attach到該進程


用 GDB 連接進程
用 GDB 連接進程

現在就可以調試了。一個新的問題是,子進程一直在運行,attach上去后都不知道運行到哪里了。有沒有辦法解決呢?

一個辦法是,在要調試的子進程初始代碼中,比如main函數開始處,加入一段特殊代碼,使子進程在某個條件成立時便循環睡眠等待,attach到進程后在該代碼段后設上斷點,再把成立的條件取消,使代碼可以繼續執行下去。

至于這段代碼所采用的條件,看你的偏好了。比如我們可以檢查一個指定的環境變量的值,或者檢查一個特定的文件存不存在。以文件為例,其形式可以如下:

void debug_wait(char *tag_file)
{while(1){if (tag_file存在)睡眠一段時間;elsebreak;}
}

當attach到進程后,在該段代碼之后設上斷點,再把該文件刪除就OK了。當然你也可以采用其他的條件或形式,只要這個條件可以設置/檢測即可。

Attach進程方法還是很方便的,它能夠應付各種各樣復雜的進程系統,比如孫子/曾孫進程,比如守護進程(daemon process),唯一需要的就是加入一小段代碼。

GDB wrapper

很多時候,父進程 fork 出子進程,子進程會緊接著調用 exec族函數來執行新的代碼。對于這種情況,我們也可以使用gdb wrapper 方法。它的優點是不用添加額外代碼。

其基本原理是以gdb調用待執行代碼作為一個新的整體來被exec函數執行,使得待執行代碼始終處于gdb的控制中,這樣我們自然能夠調試該子進程代碼。

還是上面那個例子,RIM_Oracle_prog fork出子進程后將緊接著執行RIM_Oracle_Agent.9i的二進制代碼文件。我們將該文件重命名為RIM_Oracle_Agent.9i.binary,并新建一個名為RIM_Oracle_Agent.9i的shell腳本文件,其內容如下:

[root@tivf09 bin]# mv RIM_Oracle_Agent.9i RIM_Oracle_Agent.9i.binary
[root@tivf09 bin]# cat RIM_Oracle_Agent.9i
#!/bin/sh
gdb RIM_Oracle_Agent.binary

當fork的子進程執行名為RIM_Oracle_Agent.9i的文件時,gdb會被首先啟動,使得要調試的代碼處于gdb控制之下。

新的問題來了。子進程是在gdb的控制下了,但還是不能調試:如何與gdb交互呢?我們必須以某種方式啟動gdb,以便能在某個窗口/終端與gdb交互。具體來說,可以使用xterm生成這個窗口。

xterm是X window系統下的模擬終端程序。比如我們在Linux桌面環境GNOME中敲入xterm命令:


xterm
xterm

就會跳出一個終端窗口:


終端
終端

如果你是在一臺遠程linux服務器上調試,那么可以使用VNC(Virtual Network Computing) viewer從本地機器連接到服務器上使用xterm。在此之前,需要在你的本地機器上安裝VNC viewer,在服務器上安裝并啟動VNC server。大多數linux發行版都預裝了vnc-server軟件包,所以我們可以直接運行vncserver命令。注意,第一次運行vncserver時會提示輸入密碼,用作VNC viewer從客戶端連接時的密碼。可以在VNC server機器上使用vncpasswd命令修改密碼。

[root@tivf09 root]# vncserver New 'tivf09:1 (root)' desktop is tivf09:1Starting applications specified in /root/.vnc/xstartup
Log file is /root/.vnc/tivf09:1.log[root@tivf09 root]#
[root@tivf09 root]# ps -ef|grep -i vnc
root     19609     1  0 Jun05 ?        00:08:46 Xvnc :1 -desktop tivf09:1 (root) -httpd /usr/share/vnc/classes -auth /root/.Xauthority -geometry 1024x768 -depth 16 -rfbwait 30000 -rfbauth /root/.vnc/passwd -rfbport 5901 -pn
root     19627     1  0 Jun05 ?        00:00:00 vncconfig -iconic
root     12714 10599  0 01:23 pts/0    00:00:00 grep -i vnc
[root@tivf09 root]#

Vncserver是一個Perl腳本,用來啟動Xvnc(X VNC server)。X client應用,比如xterm,VNC viewer都是和它通信的。如上所示,我們可以使用的DISPLAY值為tivf09:1。現在就可以從本地機器使用VNC viewer連接過去:


VNC viewer:輸入服務器
VNC viewer:輸入服務器

輸入密碼:


VNC viewer:輸入密碼
VNC viewer:輸入密碼

登錄成功,界面和服務器本地桌面上一樣:


VNC viewer
VNC viewer

下面我們來修改RIM_Oracle_Agent.9i腳本,使它看起來像下面這樣:

#!/bin/sh
export DISPLAY=tivf09:1.0; xterm -e gdb RIM_Oracle_Agent.binary

如果你的程序在exec的時候還傳入了參數,可以改成:

#!/bin/sh
export DISPLAY=tivf09:1.0; xterm -e gdb --args RIM_Oracle_Agent.binary $@ 

最后加上執行權限

[root@tivf09 bin]# chmod 755 RIM_Oracle_Agent.9i

現在就可以調試了。運行啟動子進程的程序:

[root@tivf09 root]# wrimtest -l 9i_linux
Resource Type  : RIM
Resource Label : 9i_linux
Host Name      : tivf09
User Name      : mdstatus
Vendor         : Oracle
Database       : rim
Database Home  : /data/oracle9i/920
Server ID      : rim
Instance Home  : 
Instance Name  : 
Opening Regular Session...

程序停住了。從VNC viewer中可以看到,一個新的gdb xterm窗口在服務器端打開了


gdb xterm 窗口
gdb xterm窗口
[root@tivf09 root]# ps -ef|grep gdb
nobody   24312 24311  0 04:30 ?        00:00:00 xterm -e gdb RIM_Oracle_Agent.binary
nobody   24314 24312  0 04:30 pts/2    00:00:00 gdb RIM_Oracle_Agent.binary
root     24326 10599  0 04:30 pts/0    00:00:00 grep gdb

運行的正是要調試的程序。設置好斷點,開始調試吧!

注意,下面的錯誤一般是權限的問題,使用 xhost 命令來修改權限:


xterm 錯誤
xterm 錯誤
[root@tivf09 bin]# export DISPLAY=tivf09:1.0
[root@tivf09 bin]# xhost +
access control disabled, clients can connect from any host

xhost + 禁止了訪問控制,從任何機器都可以連接過來。考慮到安全問題,你也可以使用xhost + <你的機器名>。

小結

上述三種方法各有特點和優劣,因此適應于不同的場合和環境:

  • follow-fork-mode方法:方便易用,對系統內核和GDB版本有限制,適合于較為簡單的多進程系統
  • attach子進程方法:靈活強大,但需要添加額外代碼,適合于各種復雜情況,特別是守護進程
  • GDB wrapper方法:專用于fork+exec模式,不用添加額外代碼,但需要X環境支持(xterm/VNC)。

參考資料

  • GDB 官方參考資料:http://sourceware.org/gdb/documentation/

  • 更多 VNC 信息:http://www.realvnc.com/

關于作者

田強,中國軟件開發中心 Tivoli 部門軟件工程師,負責 IBM 產品TMF(Tivoli Management Framework)的維護和客戶支持工作,熱愛 Linux。


本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/453640.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/453640.shtml
英文地址,請注明出處:http://en.pswp.cn/news/453640.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

集合之二:迭代器

迭代器的簡單使用 在遍歷容器時&#xff0c;我們可以使用for循環或者是增強for循環&#xff0c;但是不同的集合結構在遍歷時&#xff0c;我們要針對集合特點采取不同的方式&#xff0c;比如List是鏈表&#xff0c;我們可以直接當做數組處理&#xff0c;但Map是Key—Value的形式…

簡單使用ansible-playbook

1.使用以下命令給客戶端安裝httpd服務&#xff1a; [rootserver ~]# ansible testhost -m yum -a "namehttpd" 192.168.77.128 | SUCCESS > {"changed": true, "msg": "", "rc": 0, "results": ["Loaded …

原則

昨天例會上&#xff0c;領導分享了他最近看過的一本書《原則》。試想&#xff0c;工作上&#xff0c;生活中我的原則是什么呢&#xff1f;關于技術學習的原則。一開始的時候&#xff0c;一般都是遇到不會的再去學習&#xff0c;我一直比較喜歡帶著問題&#xff0c;這樣會學習效…

Python內置函數簡記

一、數學運算類 abs(x)求絕對值 1、參數可以是整型&#xff0c;也可以是復數 2、若參數是復數&#xff0c;則返回復數的模complex([real[, imag]])創建一個復數divmod(a, b)分別取商和余數 注意&#xff1a;整型、浮點型都可以float([x])將一個字符串或數轉換為浮點數。如果無參…

開源Java反編譯工具

Java 反編譯器 1. JD-GUI JD-GUI 是一個用 C 開發的 Java 反編譯工具&#xff0c;由 Pavel Kouznetsov開發&#xff0c;支持Windows、Linux和蘋果Mac Os三個平臺。 而且提供了Eclipse平臺下的插件JD-Eclipse。JD-GUI不需要安裝&#xff0c;直接點擊運行&#xff0c;可以反編譯j…

基于MPI的H.264并行編碼代碼移植與優化

2010 03 25基于MPI的H.264并行編碼代碼移植與優化范 文洛陽理工學院計算機信息工程系 洛陽 471023摘 要 H.264獲得出色壓縮效果和質量的代價是壓縮編碼算法復雜度的增加。為了尋求更高的編碼速度&#xff0c;集群并行計算被運用到H.264的視頻編碼計算中。分析H.264可實現并行計…

python自動取款機程序_python ATM取款機----運維開發初學(上篇)

自動取款機基本功能&#xff1a;可以存取轉賬&#xff0c;刷卡信息查詢&#xff0c;銀行卡號歷史信息查詢&#xff0c;消費記錄查詢&#xff0c;修改密碼。思維導圖如下&#xff1a;數據庫設計&#xff1a;mysql> desc balan_list; #保存賬號交易記錄option_type-----------…

java的運行參數

貼個java的運行參數&#xff1a; Usage: java [-options] class [args...] (to execute a class) or java [-options] -jar jarfile [args...] (to execute a jar file) where options include: -client to select the "client" VM -server to select t…

阿里服務器+Centos7.4+Tomcat+JDK部署

適用對象 本文檔介紹如何使用一臺基本配置的云服務器 ECS 實例部署 Java web 項目。適用于剛開始使用阿里云進行建站的個人用戶。 配置要求 這里列出的軟件版本僅代表寫作本文檔使用的版本。操作時&#xff0c;請您以實際軟件版本為準。 操作系統&#xff1a;CentOS 7.4Tomcat …

php輸出mysqli查詢出來的結果

php連接mysql我有文章已經寫過了&#xff0c;這篇文章主要是介紹從mysql中查詢出結果之后怎么輸出的問題。 一&#xff1a;mysqli_fetch_row(); 查詢結果&#xff1a;array([0]>小王) 查詢&#xff1a; [php] view plaincopy while ($row mysqli_fetch_assoc($result)) …

rhel mysql安裝_RHEL6.4下MySQL安裝方法及簡單配置

1.MySQL安裝方法簡介 1.rpm包yum安裝 2.通用二進制包安裝 3.源碼編譯安裝 注意&#xff1a;實驗所采用的系統平臺為&#xff1a;RHEL6.4 2.rpm ins首頁 → 數據庫技術背景&#xff1a;閱讀新聞RHEL6.4下MySQL安裝方法及簡單配置[日期&#xff1a;2014-04-08]來源&#xff1a;Li…

H.264算法的DSP移植與優化

摘要&#xff1a;在TMS320DM643平臺上實現H&#xff0e;264基檔次編碼器的移植與優化顯得格外實用和必要。基于對DSP平臺的結構特性和H&#xff0e;264的計算復雜度分析&#xff0c;主要從核心算法、數據傳輸和存儲器&#xff0f;Cache使用幾方面對H&#xff0e;264編碼器進行了…

IDA*與A*

我實在懶得寫博客了&#xff0c;直接放上來之前講課做的的PPT得了。 PPT_Source Code.zip 轉載于:https://www.cnblogs.com/zzzc18/p/8323927.html

java 子類 父類 轉換_Java子類與父類之間的類型轉換

1.向上轉換父類的引用變量指向子類變量時&#xff0c;子類對象向父類對象向上轉換。從子類向父類的轉換不需要什么限制&#xff0c;只需直接蔣子類實例賦值給父類變量即可&#xff0c;這也是Java中多態的實現機制。2.向下轉換在父類變量調用子類特有的、不是從父類繼承來的方法…

H.264視頻編解碼的代碼移植和優化

基于DSP系統開發的視頻編解碼系統&#xff0c;國內幾乎都是走的移植&#xff0c;優化的路線&#xff0c;并且移植的代碼&#xff0c;都是開源的。畢竟花費大量的人力&#xff0c;物力去開發一套自己的代碼&#xff0c;并不見得比一些成熟的開源代碼效率更高&#xff0c;健壯性更…

SublimeText2 快捷鍵

SublimeText2 快捷鍵&#xff0c;與對應功能一覽表&#xff1a; 快捷鍵功能ctrlshiftn打開新Sublimectrlshiftw關閉Sublime&#xff0c;關閉所有打開文件ctrlshiftt重新打開最近關閉文件ctrln新建文件ctrls保存ctrlshifts另存為ctrlf4關閉文件ctrlw關閉ctrlk, ctrlb切換側邊欄顯…

java-linux-eclipse配置

轉載于:https://www.cnblogs.com/sheying/p/8327517.html

n皇后問題java_經典n皇后問題java代碼實現

問題描述&#xff1a;在n*n的二維表格&#xff0c;把n個皇后在表格上&#xff0c;要求同一行、同一列或同一斜線上不能有2個以上的皇后。例如八皇后有92種解決方案&#xff0c;五皇后有10種解決方案。public class TestQueen {int n; //皇后的個數int num 0; // 記錄方案數int…

ffmpeg mplayer x264 代碼重點詳解 詳細分析

ffmpeg和mplayer中求平均值得方法 1 ordinary c language level #define avg2(a,b) ((ab1)>>1) #define avg4(a,b,c,d) ((abcd2)>>2) 顯而易見&#xff0e;&#xff0e;&#xff0e;&#xff0c;注意a&#xff0c;b宏表達式可能引出的副作用 2 SIMD by software…

nagios監控服務器的搭建

nagios 概述&#xff1a; 開源的免費的網絡監視工具。 監控&#xff1a; windows, Linux,Unix,交換機和路由器。報警。 Nagios是插件式的結構&#xff0c;它本身沒有任何監控功能&#xff0c;所有的監控都是通過插件進行的&#xff0c;因此其是高度模塊化和富于彈性的。Nagios…