簡介
調試程序有很多方法,例如向屏幕上打印消息,使用調試器,或者只需仔細考慮程序如何運行,并對問題進行有根有據的猜測。
在修復 bug 之前,首先要確定在源程序中的位置。例如,當一個程序產生崩潰或生成核心轉儲(core dump)時,您就需要了解是哪行代碼發生了崩潰。在找到有問題的代碼行之后,就可以確定這個函數中變量的值,函數是如何調用的,更具體點說,為什么會發生這種錯誤。使用調試器查找這些信息非常簡單。
本文將簡要介紹幾種用于修復一些很難通過可視化地檢查代碼而發現的 bug 的技術,并闡述了如何使用在 Linux on Power 架構上可用的工具。
回頁首
調試內存問題的工具和技術
動態內存分配看起來似乎非常簡單:您可以根據需要分配內存 —— 使用 malloc() 或其變種 —— 并在不需要時釋放這些內存。實際上,內存管理的問題是軟件中最為常見的 bug,因為通常在程序啟動時這些問題并不明顯。例如,程序中的內存泄漏可能開始并不為人注意,直到經過多天甚至幾個月的運行才會被發現。接下來的幾節將簡要介紹如何使用流行的調試器 Valgrind 來發現并調試這些最常見的內存 bug。
在開始使用任何調試工具之前,請考慮這個工具是否對重新編譯應用程序有益,是否可以支持具有調試信息的庫(-g
選項)。如果沒有啟用調試信息,調試工具可以做的最好的事情也不過是猜測一段特定的代碼是屬于哪個函數的。這使得錯誤消息和概要分析輸出幾乎沒有什么用處。使用 -g
選項,您就有可能獲得一些信息來直接指出相關的代碼行。
Valgrind
Valgrind 已經在 Linux 應用程序開發社區中廣泛用來調試應用程序。它尤其擅長發現內存管理的問題。它可以檢查程序運行時的內存泄漏問題。這個工具目前正由 Julian Seward 進行開發,并由 Paul Mackerras 移植到了 Power 架構上。
要安裝 Valgrind,請從 Valgrind 的 Web 站點上下載源代碼(參閱 參考資料)。切換到 Valgrind 目錄,并執行下面的命令:
# make
# make check
# make install
|
Valgrind 的錯誤報告
Valgrind 的輸出格式如下:
清單 1. Valgrind 的輸出消息
# valgrind du –x –s
.
.
==29404== Address 0x1189AD84 is 0 bytes after a block of size 12 alloc'd
==29404== at 0xFFB9964: malloc (vg_replace_malloc.c:130)
==29404== by 0xFEE1AD0: strdup (in /lib/tls/libc.so.6)
==29404== by 0xFE94D30: setlocale (in /lib/tls/libc.so.6)
==29404== by 0x10001414: main (in /usr/bin/du)
|
==29404==
是進程的 ID。消息 Address 0x1189AD84 is 0 bytes after a block of size 12 alloc'd
說明在這個 12 字節的數組后面沒有存儲空間了。第二行以及后續幾行說明內存是在 130 行(vg_replace_malloc.c)的 strdup()
程序中進行分配的。strdup()
是在 libc.so.6 庫的 setlocale()
中調用的;main()
調用了 setlocale()
。
未初始化的內存
最為常見的一個 bug 是程序使用了未初始化的內存。未初始化的數據可能來源于:
- 未經初始化的變量
- malloc 函數所分配的數據,在寫入值之前使用了
下面這個例子使用了一個未初始化的數組:
清單 2. 使用未初始化的內存
2 {
3 int i[5];
4
5 if (i[0] == 0)
6 i[1]=1;
7 return 0;
8 }
|
在這個例子中,整數數組 i[5] 沒有進行初始化;因此,i[0] 包含的是一個隨機數。因此使用 i[0] 的值來判斷一個條件分支就會導致不可預期的問題。Valgrind 可以很容易捕獲這種錯誤條件。當您使用 Valgrind 運行這個程序時,就會接收到下面的消息:
清單 3. Valgrind 的輸出消息
# gcc –g –o test1 test1.c
# valgrind ./test1
.
.
==31363==
==31363== Conditional jump or move depends on uninitialised value(s)
==31363== at 0x1000041C: main (test1.c:5)
==31363==
==31363== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 7 from 1)
==31363== malloc/free: in use at exit: 0 bytes in 0 blocks.
==31363== malloc/free: 0 allocs, 0 frees, 0 bytes allocated.
==31363== For counts of detected errors, rerun with: -v
==31363== No malloc'd blocks -- no leaks are possible.
|
Valgrind 的輸出說明,有一個條件分支依賴于文件 test1.c 中第 5 行中的一個未初始化的變量。
內存泄漏
內存泄漏是另外一個常見的問題,也是很多程序中最難判斷的問題。內存泄漏的主要表現為:當程序連續運行時,與程序相關的內存(或堆)變得越來越大。結果是,當這個程序所消耗的內存達到系統的上限時,就會自己崩潰;或者會出現更嚴重的情況:掛起或導致系統崩潰。下面是一個有內存泄漏 bug 的示例程序:
清單 4. 內存泄漏示例
1 int main(void)
2 {
3 char *p1;
4 char *p2;
5
6 p1 = (char *) malloc(512);
7 p2 = (char *) malloc(512);
8
9 p1=p2;
10
11 free(p1);
12 free(p2);
13 }
|
上面的代碼分別給字符指針 p1 和 p2 分配了兩個 512 字節的內存塊,然后將指向第一個內存塊的指針設置為指向第二個內存塊。結果是,第二個內存塊的地址丟失了,并導致內存泄漏。在使用 Valgrind 運行這個程序時,會返回如下的消息:
清單 5. Valgrind 的輸出消息
# gcc –g –o test2 test2.c
# valgrind ./test2
.
.
==31468== Invalid free() / delete / delete[]
==31468== at 0xFFB9FF0: free (vg_replace_malloc.c:152)
==31468== by 0x100004B0: main (test2.c:12)
==31468== Address 0x11899258 is 0 bytes inside a block of size 512 free'd
==31468== at 0xFFB9FF0: free (vg_replace_malloc.c:152)
==31468== by 0x100004A4: main (test2.c:11)
==31468==
==31468== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 7 from 1)
==31468== malloc/free: in use at exit: 512 bytes in 1 blocks.
==31468== malloc/free: 2 allocs, 2 frees, 1024 bytes allocated.
==31468== For counts of detected errors, rerun with: -v
==31468== searching for pointers to 1 not-freed blocks.
==31468== checked 167936 bytes.
==31468==
==31468== LEAK SUMMARY:
==31468== definitely lost: 512 bytes in 1 blocks.
==31468== possibly lost: 0 bytes in 0 blocks.
==31468== still reachable: 0 bytes in 0 blocks.
==31468== suppressed: 0 bytes in 0 blocks.
==31468== Use --leak-check=full to see details of leaked memory.
|
正如您可以看到的一樣,Valgrind 報告說這個程序中有 512 字節的內存丟失了。
非法寫/讀
這種情況發生在程序試圖對一個不屬于程序本身的內存地址進行讀寫時。在有些系統上,在發生這種錯誤時,程序會異常結束,并產生一個段錯誤。下面這個例子就是一個常見的 bug,它試圖讀寫一個超出數組邊界的元素。
清單 6. 非法讀寫
1 int main() {
2 int i, *iw, *ir;
3
4 iw = (int *)malloc(10*sizeof(int));
5 ir = (int *)malloc(10*sizeof(int));
6
7
8 for (i=0; i<11; i++)
9 iw[i] = i;
10
11 for (i=0; i<11; i++)
12 ir[i] = iw[i];
13
14 free(iw);
15 free(ir);
16 }
|
從這個程序中我們可以看出,對于 iw[10]
和 ir[10]
的訪問都是非法的,因為 iw
和 ir
都只有 10 個元素,分別是從 0 到 9。請注意int iw[10 ]
和 iw = (int *)malloc(10*sizeof(int))
是等效的 —— 它們都是用來給一個整數數組 iw 分配 10 個元素。
當您使用 Valgrind 運行這個程序時,會返回如下的消息:
清單 7. Valgrind 的輸出消息
# gcc –g –o test3 test3.c
# valgrind ./test3
.
.
==31522== Invalid write of size 4
==31522== at 0x100004C0: main (test3.c:9)
==31522== Address 0x11899050 is 0 bytes after a block of size 40 alloc'd
==31522== at 0xFFB9964: malloc (vg_replace_malloc.c:130)
==31522== by 0x10000474: main (test10.c:4)
==31522==
==31522== Invalid read of size 4
==31522== at 0x1000050C: main (test3.c:12)
==31522== Address 0x11899050 is 0 bytes after a block of size 40 alloc'd
==31522== at 0xFFB9964: malloc (vg_replace_malloc.c:130)
==31522== by 0x10000474: main (test10.c:4)
==31522==
==31522== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 7 from 1)
==31522== malloc/free: in use at exit: 0 bytes in 0 blocks.
==31522== malloc/free: 2 allocs, 2 frees, 84 bytes allocated.
==31522== For counts of detected errors, rerun with: -v
==31522== No malloc'd blocks -- no leaks are possible.
|
在 test3.c 的第 9 行發現一個非法的 4 字節寫操作,在第 12 行發現一個非法的 4 字節讀操作。
Valgrind 也可以幫助判斷內存誤用的問題,例如:
- 讀/寫已經釋放的內存
- C++ 環境中錯誤地使用 malloc/new 與 free/delete 的配對
下面這個列表介紹了 POWER 架構上 Valgrind 的狀態:
- memcheck 和 addrcheck 工具都可以很好地工作。然而,其他工具還沒有進行大量的測試。另外,Helgrind (一個數據競爭的檢測程序)在 POWER 上尚不能使用。
- 所有的 32 位 PowerPC? 用戶模式的指令都可以支持,除了兩條非常少用的指令:lswx 和 stswx。具體來說,所有的浮點和 Altivec(VMX)指令都可以支持。
- Valgrind 可以在 32 位或 64 位 PowerPC/Linux 內核上工作,但是只能用于 32 位的可執行程序。
有關 Valgrind 內存調試的更多信息,請訪問 Valgrind HOW TO 站點。還可以參閱 Steve Best 的“Debugging Memory Problems”(Linux Magazine,2003 年 5 月)。參考資料 中有它們的鏈接
除了 Valgrind 之外,還可以使用其他幾個內存調試工具;例如,Memwatch 和 Electric Fence。
回頁首
調試其他程序問題的工具和技術
除了內存 bug 之外,開發人員通常還會碰到程序雖然能夠成功編譯,但是在運行時卻會產生內核轉儲或段錯誤的問題。有時在程序完成之后,程序的輸出可能與所期望或設計的不同。在這兩種情況中,可能代碼中存在您認為正確而實際上錯誤的情況。接下來的幾節中介紹的調試器將幫助您找到這些情況的原因。
GNU 項目調試器
GDB(GNU 項目調試器)可以讓您了解程序在執行時“內部” 究竟在干些什么,以及在程序發生崩潰的瞬間正在做什么。
GDB 做以下 4 件主要的事情來幫助您捕獲程序中的 bug:
- 在程序啟動之前指定一些可以影響程序行為的變量或條件
- 在某個指定的地方或條件下暫停程序
- 在程序停止時檢查已經發生了什么
- 在程序執行過程中修改程序中的變量或條件,這樣就可以體驗修復一個 bug 的成果,并繼續了解其他 bug
要調試的程序可以是使用 C、C++、Pascal、Objective-C 以及其他很多語言編寫的。GDB 的二進制文件名是 gdb。
gdb 中有很多命令。使用 help
命令可以列出所有的命令,以及關于如何使用這些命令的介紹。下表給出了最常用的 GDB 命令。
表 1. gdb 中最常用的命令命令 | 說明 | 例子 |
---|
help | 顯示命令類別 | help - 顯示命令類別
help breakpoints - 顯示屬于 breakpoints 類別的命令
help break - 顯示 break 命令的解釋 |
run | 啟動所調試的程序 | ? |
kill | 終止正在調試的程序的執行 | 通常這會在要執行的代碼行已經超過了您想要調試的代碼時使用。執行 kill 會重置斷點,并從頭再次運行這個程序 |
| | |
cont | 所調試的程序運行到一個斷點、異常或單步之后,繼續執行 | ? |
info break | 顯示當前的斷點或觀察點 | ? |
break | 在指定的行或函數處設置斷點 | break 93 if i=8 - 當變量 i 等于 8 時,在第 93 行停止程序執行 |
Step | 單步執行程序,直到它到達一個不同的源代碼行。您可以使用 s 來代表 step 命令 | ? |
Next | 與 step 命令類似,只是它不會“單步跟蹤到”子例程中 | ? |
print | 打印一個變量或表達式的值 | print pointer - 打印變量指針的內容
print *pointer - 打印指針所指向的數據結構的內容 |
delete | 刪除某些斷點或自動顯示表達式 | delete 1 - 刪除斷點 1。斷點可以通過 info break 來顯示 |
watch | 為一個表達式設置一個觀察點。當表達式的值發生變化時,這個觀察點就會暫停程序的執行 | ? |
where | 打印所有堆棧幀的棧信息 | where - 不使用參數,輸出當前線程的堆棧信息
where all - 輸出當前線程組中所有線程的堆棧信息
where threadindex - 輸出指定線程的堆棧信息 |
attach | 開始查看一個已經運行的進程 | attach <process_id> - 附加到進程 process_id 上。process_id 可以使用 ps 命令找到 |
info thread | 顯示當前正在運行的線程 | ? |
thread apply threadno command | 對一個線程運行 gdb 命令 | thread apply 3 where - 對線程 3 運行 where 命令 |
Thread threadno | 選擇一個線程作為當前線程 | ? |
如果一個程序崩潰了,并生成了一個 core 文件,您可以查看 core 文件來判斷進程結束時的狀態。使用下面的命令啟動 gdb:
# gdb programname corefilename |
要調試一個 core 文件,您需要可執行程序、源代碼文件以及 core 文件。要對一個 core 文件啟動 gdb,請使用 -c 選項:
# gdb -c core programname |
gdb 會顯示是哪行代碼導致這個程序產生了核心轉儲。
默認情況下,核心轉儲在 Novell 的 SUSE LINUX Enterprise Server 9(SLES 9)和 Red Hat? Enterprise Linux Advanced Server(RHEL AS 4)上都是禁用的。要啟用核心轉儲,請以 root 用戶的身份在命令行中執行 ulimit –c unlimited
。
清單 8 中的例子闡述了如何使用 gdb 來定位程序中的 bug。清單 8 是一段包含 bug 的 C++ 代碼。
清單 8 中的 C++ 程序試圖構建 10 個鏈接在一起的數字框(number box),例如:
圖 1. 一個包含 10 個鏈接在一起的數字框的列表
然后試圖從這個列表中逐個刪除數字框。
編譯并運行這個程序,如下所示:
清單 9. 編譯并運行這個程序
# g++ -g -o gdbtest1 gdbtest1.cpp
# ./gdbtest1
Number Box "0" created
Number Box "1" created
Number Box "2" created
Number Box "3" created
Number Box "4" created
Number Box "5" created
Number Box "6" created
Number Box "7" created
Number Box "8" created
Number Box "9" created
list created
Number Box "9" deleted
Segmentation fault
|
正如您可以看到的一樣,這個程序會導致段錯誤。調用 gdb 來看一下這個問題,如下所示:
清單 10. 調用 gdb
# gdb ./gdbtest1
GNU gdb 6.2.1
Copyright 2004 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 "ppc-suse-linux"...Using host libthread_db
library "/lib/tls/libthread_db.so.1".
(gdb)
|
您知道段錯誤是在數字框 "9" 被刪除之后發生的。執行 run
和 where
命令來精確定位段錯誤發生在程序中的什么位置。
清單 11. 執行 run 和 where 命令
(gdb) run
Starting program: /root/test/gdbtest1
Number Box "0" created
Number Box "1" created
Number Box "2" created
Number Box "3" created
Number Box "4" created
Number Box "5" created
Number Box "6" created
Number Box "7" created
Number Box "8" created
Number Box "9" created
list created
Number Box "9" deleted
Program received signal SIGSEGV, Segmentation fault.
0x10000f74 in NumBox<int>::GetNext (this=0x0) at gdbtest1.cpp:14
14 NumBox<T>*GetNext() const { return Next; }
(gdb) where
#0 0x10000f74 in NumBox<int>::GetNext (this=0x0) at gdbtest1.cpp:14
#1 0x10000d10 in NumChain<int>::RemoveBox (this=0x10012008,
item_to_remove=@0xffffe200) at gdbtest1.cpp:63
#2 0x10000978 in main (argc=1, argv=0xffffe554) at gdbtest1.cpp:94
(gdb)
|
跟蹤信息顯示這個程序在第 14 行 NumBox<int>::GetNext (this=0x0)
接收到一個段錯誤。這個數字框上 Next 指針的地址是 0x0,這對于一個數字框來說是一個無效的地址。從上面的跟蹤信息可以看出,GetNext
函數是由 63 行調用的。看一下在 gdbtest1.cpp 的 63 行附近發生了什么:
清單 12. gdbtest1.cpp
54 } else {
55 temp->SetNext (current->GetNext());
56 delete temp;
57 temp = 0;
58 return 0;
59 }
60 }
61 current = 0;
62 temp = current;
63 current = current->GetNext();
64 }
65
66 return -1;
|
第 61 行 current=0
將這個指針設置為一個無效的地址,這正是產生段錯誤的根源。注釋掉第 61 行,將其保存為 gdbtest2.cpp,然后編譯并重新運行。
清單 13. 再次運行程序(gdbtest2.cpp)
# g++ -g -o gdbtest2 gdbtest2.cpp
# ./gdbtest2
Number Box "0" created
Number Box "1" created
Number Box "2" created
Number Box "3" created
Number Box "4" created
Number Box "5" created
Number Box "6" created
Number Box "7" created
Number Box "8" created
Number Box "9" created
list created
Number Box "9" deleted
Number Box "0" deleted
|
這個程序現在可以成功完成而不會出現段錯誤了。然而,結果并不像我們預期的一樣:程序在刪除 Number Box "9"之后刪除了 Number Box "0",而不像我們期望的一樣刪除 Number Box "8,"。使用 gdb 再次來看一下。
清單 14. 再次使用 gdb 進行查看
# gdb ./gdbtest2
GNU gdb 6.2.1
Copyright 2004 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 "ppc-suse-linux"...Using host libthread_db
library "/lib/tls/libthread_db.so.1".
(gdb) break 94 if i==8
Breakpoint 1 at 0x10000968: file gdbtest2.cpp, line 94.
(gdb) run
Starting program: /root/test/gdbtest2
Number Box "0" created
Number Box "1" created
Number Box "2" created
Number Box "3" created
Number Box "4" created
Number Box "5" created
Number Box "6" created
Number Box "7" created
Number Box "8" created
Number Box "9" created
list created
Number Box "9" deleted
Breakpoint 1, main (argc=1, argv=0xffffe554) at gdbtest2.cpp:94
94 list ->RemoveBox(i);
|
您可能希望找出為什么這個程序刪除的是 Number Box 0,而不是 Number Box 8,因此需要在您認為程序會刪除 Number Box 8 的地方停止程序。設置這個斷點:break 94 if i==8
,可以在 i 等于 8 時在第 94 行處停止程序。然后單步跟蹤
到 RemoveBox()
函數中。
清單 15. 單步跟蹤到 RemoveBox() 函數中
(gdb) s
38 NumBox<T> *temp = 0;
(gdb) s
40 while (current != 0) {
(gdb) print pointer
$1 = (NumBox<int> *) 0x100120a8
(gdb) print *pointer
$2 = {Num = 0, Next = 0x0}
(gdb)
|
指針早已指向了 Number Box "0",因此這個 bug 可能就存在于程序刪除 Number Box "9" 的地方。要在 gdb 中重新啟動這個程序,請使用 kill
刪除原來的斷點,然后添加一個 i 等于 9 時的新斷點,然后再次運行這個程序。
清單 16. 在 gdb 中重新啟動程序
(gdb) kill
Kill the program being debugged? (y or n) y
(gdb) info break
Num Type Disp Enb Address What
1 breakpoint keep y 0x10000968 in main at gdbtest2.cpp:94
stop only if i == 8
breakpoint already hit 1 time
(gdb) delete 1
(gdb) break 94 if i==9
Breakpoint 2 at 0x10000968: file gdbtest2.cpp, line 94.
(gdb) run
Starting program: /root/test/gdbtest2
Number Box "0" created
Number Box "1" created
Number Box "2" created
Number Box "3" created
Number Box "4" created
Number Box "5" created
Number Box "6" created
Number Box "7" created
Number Box "8" created
Number Box "9" created
list created
Breakpoint 2, main (argc=1, argv=0xffffe554) at gdbtest2.cpp:94
94 list ->RemoveBox(i);
(gdb)
|
當這一次單步跟蹤 RemoveBox()
函數時,要特別注意 list->pointer
正在指向哪一個數字框,因為 bug 可能就在于 list->pointer
開始指向 Number Box "0" 的地方。請使用 display *pointer
命令來查看,這會自動顯示這個函數。
清單 17. 使用 display *pointer 命令進行監視
Breakpoint 2, main (argc=1, argv=0xffffe554) at gdbtest2.cpp:94
94 list ->RemoveBox(i);
(gdb) s
NumChain<int>::RemoveBox (this=0x10012008, item_to_remove=@0xffffe200)
at gdbtest2.cpp:37
37 NumBox<T> *current = pointer;
(gdb) display *pointer
1: *this->pointer = {Num = 9, Next = 0x10012098}
(gdb) s
38 NumBox<T> *temp = 0;
1: *this->pointer = {Num = 9, Next = 0x10012098}
(gdb) s
40 while (current != 0) {
1: *this->pointer = {Num = 9, Next = 0x10012098}
(gdb) s
41 if (current->GetValue() == item_to_remove) {
1: *this->pointer = {Num = 9, Next = 0x10012098}
(gdb) s
NumBox<int>::GetValue (this=0x100120a8) at gdbtest2.cpp:16
16 const T& GetValue () const { return Num; }
(gdb) s
NumChain<int>::RemoveBox (this=0x10012008, item_to_remove=@0xffffe200)
at gdbtest2.cpp:42
42 if (temp == 0) {
1: *this->pointer = {Num = 9, Next = 0x10012098}
(gdb) s
44 if (current->GetNext() == 0) {
1: *this->pointer = {Num = 9, Next = 0x10012098}
(gdb) s
NumBox<int>::GetNext (this=0x100120a8) at gdbtest2.cpp:14
14 NumBox<T>*GetNext() const { return Next; }
(gdb) s
NumChain<int>::RemoveBox (this=0x10012008, item_to_remove=@0xffffe200)
at gdbtest2.cpp:50
50 delete current;
1: *this->pointer = {Num = 9, Next = 0x10012098}
(gdb) s
~NumBox (this=0x100120a8) at gdbtest2.cpp:10
10 std::cout << "Number Box " <<"\"" << GetValue()
<<"\"" <<" deleted" << std::endl;
(gdb) s
NumBox<int>::GetValue (this=0x100120a8) at gdbtest2.cpp:16
16 const T& GetValue () const { return Num; }
(gdb) s
Number Box "9" deleted
~NumBox (this=0x100120a8) at gdbtest2.cpp:11
11 Next = 0;
(gdb) s
NumChain<int>::RemoveBox (this=0x10012008, item_to_remove=@0xffffe200)
at gdbtest2.cpp:51
51 current = 0;
1: *this->pointer = {Num = 0, Next = 0x0}
(gdb) s
53 return 0;
1: *this->pointer = {Num = 0, Next = 0x0}
(gdb) s
0x10000d1c 66 return -1;
1: *this->pointer = {Num = 0, Next = 0x0}
|
從上面的跟蹤過程中,您可以看到 list->pointer
在刪除 Number Box "9" 之后指向了 Number Box "0"。這個邏輯并不正確,因為在刪除 Number Box "9" 之后,list->pointer
應該指向的是 Number Box "8"。現在非常顯然我們應該在第 50 行之前添加一條語句pointer = pointer->GetNext();
,如下所示:
清單 18. 在第 50 行之前添加一條 pointer = pointer->GetNext(); 語句
49 } else {
50 pointer = pointer->GetNext();
51 delete current;
52 current = 0;
53 }
54 return 0;
|
將新修改之后的程序保存為 gdbtest3.cpp,然后編譯并再次運行。
清單 19. 再次運行程序(gdbtest3.cpp)
# g++ -g -o gdbtest3 gdbtest3.cpp
# ./gdbtest3
Number Box "0" created
Number Box "1" created
Number Box "2" created
Number Box "3" created
Number Box "4" created
Number Box "5" created
Number Box "6" created
Number Box "7" created
Number Box "8" created
Number Box "9" created
list created
Number Box "9" deleted
Number Box "8" deleted
Number Box "7" deleted
Number Box "6" deleted
Number Box "5" deleted
Number Box "4" deleted
Number Box "3" deleted
Number Box "2" deleted
Number Box "1" deleted
Number Box "0" deleted
|
這才是我們期望的結果。
多線程環境
在 GDB 中有一些特殊的命令可以用于多線程應用程序的調試。下面這個例子給出了一個死鎖情況,并介紹了如何使用這些命令來檢查多線程應用程序中的問題:
清單 20. 多線程的例子
#include <stdio.h>
#include "pthread.h>
pthread_mutex_t AccountA_mutex;
pthread_mutex_t AccountB_mutex;
struct BankAccount {
char account_name[1];
int balance;
};
struct BankAccount accountA = {"A", 10000 };
struct BankAccount accountB = {"B", 20000 };
void * transferAB (void* amount_ptr) {
int amount = *((int*)amount_ptr);
pthread_mutex_lock(&AccountA_mutex);
if (accountA.balance < amount) {
printf("There is not enough memory in Account A!\n");
pthread_mutex_unlock(&AccountA_mutex);
pthread_exit((void *)1);
}
accountA.balance -=amount;
sleep(1);
pthread_mutex_lock(&AccountB_mutex);
accountB.balance +=amount;
pthread_mutex_unlock(&AccountA_mutex);
pthread_mutex_unlock(&AccountB_mutex);
}
void * transferBA (void* amount_ptr) {
int amount = *((int*)amount_ptr);
pthread_mutex_lock(&AccountB_mutex);
if (accountB.balance < amount) {
printf("There is not enough memory in Account B!\n");
pthread_mutex_unlock(&AccountB_mutex);
pthread_exit((void *)1);
}
accountB.balance -=amount;
sleep(1);
pthread_mutex_lock(&AccountA_mutex);
accountA.balance +=amount;
pthread_mutex_unlock(&AccountB_mutex);
pthread_mutex_unlock(&AccountA_mutex);
}
int main(int argc, char* argv[]) {
int threadid[4];
pthread_t pthread[4];
int transfer_amount[4] = {100, 200, 300, 400};
int final_balanceA, final_balanceB;
final_balanceA=accountA.balance-transfer_amount[0]-
transfer_amount[1]+transfer_amount[2]+transfer_amount[3];
final_balanceB=accountB.balance+transfer_amount[0]
+transfer_amount[1]-transfer_amount[2]-transfer_amount[3];
if (threadid[0] = pthread_create(&pthread[0], NULL, transferAB,
(void*)&transfer_amount[0]) " 0) {
perror("Thread #0 creation failed.");
exit (1);
}
if (threadid[1] = pthread_create(&pthread[1], NULL, transferAB,
(void*)&transfer_amount[1]) " 0) {
perror("Thread #1 creation failed.");
exit (1);
}
if (threadid[2] = pthread_create(&pthread[2], NULL, transferBA,
(void*)&transfer_amount[2]) < 0) {
perror("Thread #2 creation failed.");
exit (1);
}
if (threadid[3] = pthread_create(&pthread[3], NULL, transferBA,
(void*)&transfer_amount[3]) < 0) {
perror("Thread #3 creation failed.");
exit (1);
}
printf("Transitions are in progress..");
while ((accountA.balance != final_balanceA) && (accountB.balance
!= final_balanceB)) {
printf("..");
}
printf("\nAll the money is transferred !!\n");
}
|
使用 gcc 來編譯這個程序,如下所示:
# gcc -g -o gdbtest2 gdbtest2.c -L/lib/tls -lpthread
|
程序 gdbtest2 會掛起,不會返回一條 All the money is transferred !!
消息。
將 gdb 附加到正在運行的進程上,從而了解這個進程內部正在發生什么。
清單 21. 將 gdb 附加到正在運行的進程上
# ps -ef |grep gdbtest2
root 9510 8065 1 06:30 pts/1 00:00:00 ./gdbtest2
root 9516 9400 0 06:30 pts/4 00:00:00 grep gdbtest2
# gdb -pid 9510
GNU gdb 6.2.1
Copyright 2004 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 "ppc-suse-linux".
Attaching to process 9510
Reading symbols from /root/test/gdbtest2...done.
Using host libthread_db library "/lib/tls/libthread_db.so.1".
Reading symbols from /lib/tls/libpthread.so.0...done.
[Thread debugging using libthread_db enabled]
[New Thread 1073991712 (LWP 9510)]
[New Thread 1090771744 (LWP 9514)]
[New Thread 1086577440 (LWP 9513)]
[New Thread 1082383136 (LWP 9512)]
[New Thread 1078188832 (LWP 9511)]
Loaded symbols for /lib/tls/libpthread.so.0
Reading symbols from /lib/tls/libc.so.6...done.
Loaded symbols for /lib/tls/libc.so.6
Reading symbols from /lib/ld.so.1...done.
Loaded symbols for /lib/ld.so.1
0x0ff4ac40 in __write_nocancel () from /lib/tls/libc.so.6
(gdb) info thread
5 Thread 1078188832 (LWP 9511) 0x0ffe94ec in __lll_lock_wait ()
from /lib/tls/libpthread.so.0
4 Thread 1082383136 (LWP 9512) 0x0ffe94ec in __lll_lock_wait ()
from /lib/tls/libpthread.so.0
3 Thread 1086577440 (LWP 9513) 0x0ffe94ec in __lll_lock_wait ()
from /lib/tls/libpthread.so.0
2 Thread 1090771744 (LWP 9514) 0x0ffe94ec in __lll_lock_wait ()
from /lib/tls/libpthread.so.0
1 Thread 1073991712 (LWP 9510) 0x0ff4ac40 in __write_nocancel ()
from /lib/tls/libc.so.6
(gdb)
|
從 info thread
命令中,我們可以了解到除了主線程(thread #1)之外的所有線程都在等待函數 __lll_lock_wait ()
完成。
使用 thread apply threadno where
命令來查看每個線程到底運行到了什么地方:
清單 22. 查看每個線程運行到了什么地方
(gdb) thread apply 1 where
Thread 1 (Thread 1073991712 (LWP 9510)):
#0 0x0ff4ac40 in __write_nocancel () from /lib/tls/libc.so.6
#1 0x0ff4ac28 in __write_nocancel () from /lib/tls/libc.so.6
Previous frame identical to this frame (corrupt stack?)
#0 0x0ff4ac40 in __write_nocancel () from /lib/tls/libc.so.6
(gdb) thread apply 2 where
Thread 2 (Thread 1090771744 (LWP 9514)):
#0 0x0ffe94ec in __lll_lock_wait () from /lib/tls/libpthread.so.0
#1 0x0ffe466c in pthread_mutex_lock () from /lib/tls/libpthread.so.0
#2 0x0ffe466c in pthread_mutex_lock () from /lib/tls/libpthread.so.0
#3 0x0ffe466c in pthread_mutex_lock () from /lib/tls/libpthread.so.0
#4 0x0ffe466c in pthread_mutex_lock () from /lib/tls/libpthread.so.0
Previous frame inner to this frame (corrupt stack?)
#0 0x0ff4ac40 in __write_nocancel () from /lib/tls/libc.so.6
(gdb) thread apply 3 where
Thread 3 (Thread 1086577440 (LWP 9513)):
#0 0x0ffe94ec in __lll_lock_wait () from /lib/tls/libpthread.so.0
#1 0x0ffe466c in pthread_mutex_lock () from /lib/tls/libpthread.so.0
#2 0x0ffe466c in pthread_mutex_lock () from /lib/tls/libpthread.so.0
#3 0x0ffe466c in pthread_mutex_lock () from /lib/tls/libpthread.so.0
#4 0x0ffe466c in pthread_mutex_lock () from /lib/tls/libpthread.so.0
Previous frame inner to this frame (corrupt stack?)
#0 0x0ff4ac40 in __write_nocancel () from /lib/tls/libc.so.6
(gdb) thread apply 4 where
Thread 4 (Thread 1082383136 (LWP 9512)):
#0 0x0ffe94ec in __lll_lock_wait () from /lib/tls/libpthread.so.0
#1 0x0ffe466c in pthread_mutex_lock () from /lib/tls/libpthread.so.0
#2 0x0ffe466c in pthread_mutex_lock () from /lib/tls/libpthread.so.0
#3 0x0ffe466c in pthread_mutex_lock () from /lib/tls/libpthread.so.0
#4 0x0ffe466c in pthread_mutex_lock () from /lib/tls/libpthread.so.0
Previous frame inner to this frame (corrupt stack?)
#0 0x0ff4ac40 in __write_nocancel () from /lib/tls/libc.so.6
(gdb) thread apply 5 where
Thread 5 (Thread 1078188832 (LWP 9511)):
#0 0x0ffe94ec in __lll_lock_wait () from /lib/tls/libpthread.so.0
#1 0x0ffe466c in pthread_mutex_lock () from /lib/tls/libpthread.so.0
#2 0x0ffe466c in pthread_mutex_lock () from /lib/tls/libpthread.so.0
#3 0x0ffe466c in pthread_mutex_lock () from /lib/tls/libpthread.so.0
#4 0x0ffe466c in pthread_mutex_lock () from /lib/tls/libpthread.so.0
Previous frame inner to this frame (corrupt stack?)
#0 0x0ff4ac40 in __write_nocancel () from /lib/tls/libc.so.6
|
每個線程都試圖對一個互斥體進行加鎖,但是這個互斥體卻是不可用的,可能是因為有另外一個線程已經對其進行加鎖了。從上面的證據我們可以判斷程序中一定存在死鎖。您還可以看到哪個線程現在擁有這個互斥體。
清單 23. 查看哪個線程擁有互斥體
(gdb) print AccountA_mutex
$1 = {__m_reserved = 2, __m_count = 0, __m_owner = 0x2527,
__m_kind = 0, __m_lock = {__status = 1, __spinlock = 0}}
(gdb) print 0x2527
$2 = 9511
(gdb) print AccountB_mutex
$3 = {__m_reserved = 2, __m_count = 0, __m_owner = 0x2529,
__m_kind = 0, __m_lock = {__status = 1, __spinlock = 0}}
(gdb) print 0x2529
$4 = 9513
(gdb)
|
從上面的命令中,我們可以看出 AccontA_mutex
是被線程 5(LWP 9511)加鎖(擁有)的,而 AccontB_mutex
是被線程 3(LWP 9513)加鎖(擁有)的。
為了解決上面的死鎖情況,可以按照相同的順序對互斥體進行加鎖,如下所示:
清單 24. 按照相同的順序對互斥體進行加鎖
.
.
void * transferAB (void* amount_ptr) {
int amount = *((int*)amount_ptr);
pthread_mutex_lock(&AccountA_mutex);
pthread_mutex_lock(&AccountB_mutex);
if (accountA.balance < amount) {
printf("There is not enough memory in Account A!\n");
pthread_mutex_unlock(&AccountA_mutex);
pthread_exit((void *)1);
}
accountA.balance -=amount;
sleep(1);
accountB.balance +=amount;
pthread_mutex_unlock(&AccountA_mutex);
pthread_mutex_unlock(&AccountB_mutex);
}
void * transferBA (void* amount_ptr) {
int amount = *((int*)amount_ptr);
pthread_mutex_lock(&AccountA_mutex);
pthread_mutex_lock(&AccountB_mutex);
if (accountB.balance < amount) {
printf("There is not enough memory in Account B!\n");
pthread_mutex_unlock(&AccountB_mutex);
pthread_exit((void *)1);
}
accountB.balance -=amount;
sleep(1);
accountA.balance +=amount;
pthread_mutex_unlock(&AccountA_mutex);
pthread_mutex_unlock(&AccountB_mutex);
}
.
.
|
或者對每個帳號單獨進行加鎖,如下所示:
清單 25. 對每個帳號單獨進行加鎖
.
.
void * transferAB (void* amount_ptr) {
int amount = *((int*)amount_ptr);
pthread_mutex_lock(&AccountA_mutex);
if (accountA.balance < amount) {
printf("There is not enough memory in Account A!\n");
pthread_mutex_unlock(&AccountA_mutex);
pthread_exit((void *)1);
}
accountA.balance -=amount;
sleep(1);
pthread_mutex_unlock(&AccountA_mutex);
pthread_mutex_lock(&AccountB_mutex);
accountB.balance +=amount;
pthread_mutex_unlock(&AccountB_mutex);
}
void * transferBA (void* amount_ptr) {
int amount = *((int*)amount_ptr);
pthread_mutex_lock(&AccountB_mutex);
if (accountB.balance < amount) {
printf("There is not enough memory in Account B!\n");
pthread_mutex_unlock(&AccountB_mutex);
pthread_exit((void *)1);
}
accountB.balance -=amount;
sleep(1);
pthread_mutex_unlock(&AccountB_mutex);
pthread_mutex_lock(&AccountA_mutex);
accountA.balance +=amount;
pthread_mutex_unlock(&AccountA_mutex);
}
.
.
.
|
要調試 64 位的應用程序(使用 GCC 的 –m64
選項或 IBM XL C/C++ 編譯器的 –q64
選項編譯),應該使用一個特別的 gdb 版本 gdb64。
Java 調試器
Java? 調試器 JDB 是一個用來調試 Java 類的命令行調試器。它提供了對本地或遠程 Java 虛擬機(JVM)的檢查和調試功能。其二進制文件是 jdb。
JDB 是與 java 編譯器 javac 一起打包的,在 java2 rpm 包中。
有很多方法都可以開始一個 jdb 會話。最常見的方法是讓 jdb 啟動一個新的 Java 虛擬機,其中運行要調試的應用程序的主類。這可以通過在命令行中使用命令 jdb 替換 java 來實現。例如,如果您的應用程序的主類是 appClass,那么就使用下面的命令在 JDB 中調試這個程序:
另外一種使用 jdb 的方法是將其附加到一個早已在運行的 JVM 上。要使用 jdb 進行調試的 VM 必須是使用下面的選項啟動的:
# java -Xdebug -Xnoagent - Xrunjdwp:transport=dt_socket,server=y,suspend=n,
address=8888 -Djava.compiler=NONEappClass |
然后您就可以使用下面的命令將 jdb 附加到這個 VM 上:
jdb 最常用的命令與 gdb 的類似。詳細信息請參考 表 1。
圖形化調試器
使用圖形模式的調試器相對于命令行調試器的一個優點是,在調試器中單步執行程序的同時可以看到對應的每行源代碼。
GNU DDD(Data Display Debugger)就是一個調試器(例如 GDB 和 JDB)的圖形化前端。除了常見的前端特性(例如查看源代碼)之外,DDD 還通過將要顯示的數據結構以交互式的圖形化方式進行顯示而聞名。
對于 SLES 9 來說,用于 PowerPC 的 DDD 二進制文件是在 SUSE Linux SDK CD 中單獨提供的,也可以從 Novell 公司的網站上進行下載(參閱 參考資料)。RedHat 在 RHEL AS 4 CD 中提供了 DDD 的 rpm 包。
圖 2 是在使用 DDD 來調試 清單 19 (gdbtest3.cpp)中的例子時的截圖。
圖 2. DDD 截屏
默認情況下,DDD 使用 gdb 作為后端調試器;要切換到 jdb,請使用 ddd -jdb 來啟動 DDD。
有關 DDD 的更多信息,請參考 GNU 項目 Web 站點上的 DDD 部分(參閱 參考資料)。
strace
strace
命令是可以在 Linux on POWER 架構上使用的一個功能非常強大的工具。它可以顯示用戶空間的應用程序所執行的全部系統調用。strace
可以以符號表的形式顯示這些調用的參數和返回值。strace
從內核接收信息,并不需要采用任何特殊的方式來構建內核。要跟蹤的應用程序也不需要為 strace 重新進行編譯,當我們無法訪問應用程序的源代碼時,這是非常方便的一種方法。
下面的例子使用 strace
來跟蹤一個普通用戶執行 cat /etc/shadow 的過程,然后將跟蹤到的結果打印到 strace.cat.out
中。這個程序依然正常運行,但是在使用 strace
運行時速度稍微有些慢;最后,我們得到一個跟蹤文件。
$ strace -o strace.cat.out cat /etc/shadow
cat: /etc/shadow: Permission denied
$ |
跟蹤文件通常都很大。即使對這個簡單的例子來說,strace.cat.out
也有 111 行長。這個文件的最后包含下面的行:
清單 26. strace.cat.out 文件中的部分行
88 open("/usr/lib/locale/en_US.UTF-8/LC_NUMERIC",
O_RDONLY) = -1 ENOENT (No such file or directory)
89 open("/usr/lib/locale/en_US.utf8/LC_NUMERIC", O_RDONLY) = 3
90 fstat64(3, {st_mode=S_IFREG|0644, st_size=54, ...}) = 0
91 mmap(NULL, 54, PROT_READ, MAP_PRIVATE, 3, 0) = 0x4010f000
92 close(3) = 0
93 open("/usr/lib/locale/en_US.UTF-8/LC_CTYPE",
O_RDONLY) = -1 ENOENT (No such file or directory)
94 open("/usr/lib/locale/en_US.utf8/LC_CTYPE", O_RDONLY) = 3
95 fstat64(3, {st_mode=S_IFREG|0644, st_size=208464, ...}) = 0
96 mmap(NULL, 208464, PROT_READ, MAP_PRIVATE, 3, 0) = 0x40110000
97 close(3) = 0
98 fstat64(1, {st_mode=S_IFCHR|0620,
st_rdev=makedev(136, 4), ...}) = 0
99 open("/etc/shadow", O_RDONLY|O_LARGEFILE) = -1 EACCES
(Permission denied)
100 write(2, "cat: ", 5) = 5
101 write(2, "/etc/shadow", 11) = 11
102 open("/usr/share/locale/en_US.UTF-8/LC_MESSAGES/libc.mo",
O_RDONLY) = -1 ENOENT (No such file or directory)
103 open("/usr/share/locale/en_US.utf8/LC_MESSAGES/libc.mo",
O_RDONLY) = -1 ENOENT (No such file or directory)
104 open("/usr/share/locale/en_US/LC_MESSAGES/libc.mo",
O_RDONLY) = -1 ENOENT (No such file or directory)
105 open("/usr/share/locale/en.UTF-8/LC_MESSAGES/libc.mo",
O_RDONLY) = -1 ENOENT (No such file or directory)
106 open("/usr/share/locale/en.utf8/LC_MESSAGES/libc.mo",
O_RDONLY) = -1 ENOENT (No such file or directory)
107 open("/usr/share/locale/en/LC_MESSAGES/libc.mo",
O_RDONLY) = -1 ENOENT (No such file or directory)
108 write(2, ": Permission denied", 19) = 19
109 write(2, "\n", 1) = 1
110 close(1) = 0
111 exit_group(1)
|
注意在第 99 行處,命令會失效,因為系統調用 open("/etc/shadow", O_RDONLY|O_LARGEFILE)
失效了,返回了一個 EACCESS 錯誤代碼,這說明權限不符合。
在有些情況中,應用程序可能會掛起,并且不能響應諸如 ctrl+c(SIGINT)之類的信號。這說明應用程序正在調用的系統調用在其內核模式下掛起了,一直不會返回用戶模式。strace
可以非常有用地用來判斷是哪一個系統調用,以及傳給這個系統調用的參數是什么。可能性最大的情況是參數的位置不對而導致了問題。
與 gdb64 是用于 64 位應用程序的 gdb 類似,strace64
也用來跟蹤 64 位應用程序所請求的系統調用。
回頁首
結束語
在 Linux on POWER 平臺上可以使用很多工具來幫助調試應用程序。本文中介紹的工具可以幫助您解決很多編碼的問題。諸如 Valgrind 之類的工具可以顯示內存泄漏的位置、非法讀/寫以及類似的內容,這可以解決內存管理的問題。
使用 gdb 和 jdb 有助于解決那些導致應用程序異常結束的問題,以及導致非預期或不想要的結果的 bug。DDD 工具可以幫助簡化調試任務,方法是通過將代碼的執行與源代碼行聯系在一起,并在數據顯示窗口中可視化地顯示數據結構。另外,strace 是一個功能非常強大的工具,它可以用來跟蹤應用程序的所有系統調用。因此,下一次當您要在 Linux 上修復 bug 時,可以試用一下這些工具。
回頁首
致謝
我要感謝 Linda Kinnunen 為我提供文檔模板,并幫我審閱本文;感謝 John Engel 和 Chakarat Skawratananond 所提供的技術幫助以及對本文的審閱。
回頁首
特別說明
本文所介紹的信息僅僅限于本文的環境,并不提供任何擔保。
本文中介紹的所有客戶例子都是用來解釋這些客戶如何使用 IBM 產品的,以及他們達到了什么結果。針對每個客戶的實際環境,成本和性能可能會有很大的差異。
非 IBM 產品中的信息來源于這些產品的供應商所公開發表的資料,或可以公開使用的源代碼,這并不說明 IBM 產品對它們的認可。非 IBM 產品的價格和性能都來源于可公開使用的信息,包括供應商的公告和供應商的全球主頁。IBM 沒有對這些產品進行測試,也不保證性能、能力數據的正確性,以及其他有關非 IBM 產品的聲明的正確性。有關非 IBM 產品的功能的問題應該由這些產品的供應商來解決。
所有有關 IBM 將來方向和興趣的聲明都可能會不加任何通知而發生變化或撤銷,這只是為了表示我們的目標。有關具體的方向聲明的全文,請與您本地的 IBM 辦公室或 IBM 認證的分銷商聯系。
有些信息是為了解決預期將來的功能。這種信息不會作為對將來任何產品所能實現特定級別的性能、功能或上市周期的承諾的最終聲明。這些承諾只在 IBM 產品聲明中做出。本文中提到這些信息是為了說明 IBM 目前的投資和開發活動是真正為了幫助客戶實現將來的計劃。
性能是在一個受控的環境中使用標準的 IBM 基準測試結果而得出的。用戶自己環境中的實際吞吐量或性能可能會有很大的不同,這取決于很多方面,例如用戶作業流中的程序數量,I/O 配置情況,存儲配置,以及所處理的任務負載。因此,我們并不保證每個用戶能獲得的吞吐量和性能方面的提高都與本文中介紹的相同。
參考資料
關于作者
Calvin Sze 是 IBM 的 IBM eServer Solutions Enablement 組織的一名 Linux 顧問。他住在德州奧斯汀。Calvin 的主要角色是幫助解決方案開發人員將自己的應用程序移植到 Linux on POWER 上。Calvin 參與 Linux 和 AIX 平臺上的軟件開發和系統集成已超過 10 年的時間了。您可以通過 calvins@us.ibm.com 與 Calvin 聯系。