GDB 自動化操作的技術
程序員在調試時往往分成兩派,一派用debugger另一派用print。至于本人嘛,是一個“機會主義者”,有時用print,有時卻改投debugger陣營。
實話說,print要比用debugger設下斷點更為簡單粗暴,有時甚至會更有用。不過debugger對比于print有三個優點:
-
無需重新編譯
-
可以在調試時改變變量
-
debugger可以實現print做不到的復雜操作
在本文,我會介紹一些在gdb中自動化操作的技術,保證可以讓你大開眼界,見識下gdb真正的力量。
會話/歷史/命令文件
通常我們只有在程序出問題才會啟動gdb,開始調試工作,調試完畢后退出。不過,讓gdb一直開著未嘗不是更好的做法。每個gdb老司機都懂得,gdb在 r 的時候會加載當前程序的最新版本。也即是說,就算不退出gdb,每次運行的也會是當前最新的版本。不退出當前調試會話有兩個好處:
-
調試上下文可以得到保留。不用每次運行都重新設一輪斷點。
-
一旦core dump了,可以顯示core dump的位置,無需帶著core重新啟動一次。
在開發C/C++項目,我一般是這樣的工作流程:一個窗口開著編輯器,編譯也在這個窗口執行;另一個窗口開著gdb,這個窗口同時也用來運行程序。一旦要調試了(或者,又segment fault了),隨手就可以開始干活。
當然了,勞作一天之后,總需要關電腦回家。這時候只能退出gdb。不想明天一早再把斷點設上一遍?gdb提供了保留斷點的功能。輸入 save br .gdb_bp ,gdb會把本次會話的斷點存在 .gdb_bp 中。明天早上一回來,啟動gdb的時候,加上 -x .gdb_bp ,讓gdb把 .gdb_bp 當做命令文件逐條重新執行,一切又回到昨晚。
condition break/watch/catch
下面是一個帶bug的二分查找實現:
#include <iostream>
using std::cout;
using std::endl;
int binary_search(int *ary, unsigned int ceiling, int target)
{
unsigned int floor = 0;
while (ceiling > floor) {
unsigned int pivot = (ceiling + floor) / 2;
if (ary[pivot] < target)
floor = pivot + 1;
else if (ary[pivot] > target)
ceiling = pivot - 1;
else
return pivot;
}
return -1;
}
int main()
{
int a[] = {1, 2, 4, 5, 6};
cout << binary_search(a, 5, 7) << endl; // -1
cout << binary_search(a, 5, 6) << endl; // 4
cout << binary_search(a, 5, 5) << endl; // 期望3,實際運行結果是-1
return 0;
}
你打算調試下 binary_search(a, 5, 5) 這個組合。若如果用print大法,就在 binary_search 中插入幾個print,運行后掃一眼,看看 target=5 的時候運行流是怎樣的。
debugger大法看似會復雜一點,如果在 binary_search 中插斷點,那么前兩次調用只能連按 c 跳過。其實沒那么復雜,gdb允許用戶設置條件斷點。你可以這么設置:
b binary_search if target == 5
現在就只有第三次調用會觸發斷點。
問題看上去跟 floor 和 ceiling 值的變化有關。要想觀察它們的值,可以 p floor 和 p ceiling 。不過有個簡單的方法,你可以對它們設置watch斷點: wa floor if target == 5 。當 floor 的值變化時,就會觸發斷點。
對于我們的示例程序來說,靠腦補也能算出這兩個值的變化,專門設置斷點似乎小題大做。不過在調試真正的程序時,watch斷點非常實用,尤其當你對相關代碼不熟悉時。使用watch斷點可以更好地幫助你理解程序流程,有時甚至會有意外驚喜。另外結合debugger運行時修改值的能力,你可以在值變化的下一刻設置目標值,觀察走不同路徑會不會出現類似的問題。如果有需要的話,還可以給某個內存地址設斷點: wa *0x7fffffffda40 。
除了watch之外,gdb還有一類catch斷點,可以用來捕獲異常/系統調用/信號。因為用途不大(我從沒實際用過),就不介紹了,感興趣的話在gdb里面 help catch 看看。
commands/define
gdb提供名為 commands 的機制,可以給某個斷點掛上待觸發的命令。舉個例子, b binary_search if target == 5 之后,輸入:
comm
i locals
i args
end
這樣當上面的斷點被觸發時, i locals 和 i args 命令會被觸發,列出當前上下文內的變量。這個功能挺廢的,因為你完全可以在斷點被觸發后才敲入這幾個命令。要不是有 define , commands 就真成擺設了。接下來我們要介紹 commands 的好基友、最強大的gdb命令之一, define 命令。
一如unix世界里面的許多程序一樣,gdb內部實現了一門DSL(領域特定語言)。用戶可以通過這門DSL來編寫自定義的宏,甚至編寫調試用的自動化腳本。我們可以用 define 命令編寫自定義的宏。
繼續上面的例子,你可以自定義一個命令代替 b xxx comm ... :
(gdb) define br_info
Type commands for definition of "br_info".
End with a line saying just "end".
>b $arg0
>comm
>i locals
>i args
>end
(gdb) br_info binary_search if target == 5
當 if target == 5 條件滿足時, br_info binary_search 會被執行。 br_info 展開成為一系列命令,并用 binary_search 替換掉 $arg0 。一行頂過去五行!
除了在會話內創建自定義宏外,我們還可以用gdb的DSL編寫宏文件,并導入到gdb中。
舉個有實際意義的例子。由于源代碼的改變,我們需要更新斷點的位置。通常的做法是刪掉原來的斷點,并新設一個。讓我們現學現用,用宏把這兩步合成一步:
# gdb_macro
define mv
if $argc == 2
delete $arg0
# 注意新創建的斷點編號和被刪除斷點的編號不同
break $arg1
else
print "輸入參數數目不對,help mv以獲得用法"
end
end
# (gdb) help mv 會輸出以下幫助文檔
document mv
Move breakpoint.
Usage: mv old_breakpoint_num new_breakpoint
Example:
(gdb) mv 1 binary_search -- move breakpoint 1 to `b binary_search`
end
# vi:set ft=gdb ts=4 sw=4 et
使用方法:
(gdb) b binary_search
Breakpoint 1 at 0x40083b: file binary_search.cpp, line 7.
(gdb) source ~/gdb_macro
(gdb) help mv
Move breakpoint.
Usage: mv old_breakpoint_num new_breakpoint
Example:
(gdb) mv 1 binary_search -- move breakpoint 1 to `b binary_search`
(gdb) mv 1 binary_search.cpp:18
Breakpoint 2 at 0x4008ab: file binary_search.cpp, line 18.
還可以進一步,把 source ~/gdb_macro 也省掉。你可以創建gdb配置文件 ~/.gdbinit ,讓gdb啟動時自動執行里面的指令。如果把自己常用的宏寫在該文件中,就能直接在gdb里面使用了,用起來如內置命令一般順滑。
調試腳本
在第一節 會話/歷史/命令文件 結尾,我提到用 -x 指定命令文件來回放斷點。那時的命令文件也算是一種用gdb的DSL編寫的調試腳本。由于調試是件交互性的活,需要事先寫好調試腳本的場景不多。即使如此,除了讓gdb自動設置斷點,依然有不少場景下可以用上調試腳本。其中之一,就是讓gdb自動采集特定函數調用的上下文數據。我把這種方法稱為“拖網法”,因為它就像拖網捕魚一樣,把逮到的東西都一股腦帶上來。
設想如下的情景:某個項目出現內存泄露的跡象。事先分配好的內存池用著用著就滿了,一再地吞噬系統的內存。內存管理是自己實現的,所以無法用valgrind來分析。鑒于內存管理部分代碼最近幾個版本都沒有改動過,猜測是業務邏輯代碼里面有誰借了內存又不還。現在你需要把它揪出來。一個辦法是給內存的分配和釋放加上日志,再編譯,然后重新運行程序,謀求復現內存泄露的場景。不過更快的辦法是,敲上這一段代碼:
(假設分配內存的接口是 my_malloc(char *p, size_t size) ,釋放內存的接口是 free(char *p) )
# /tmp/malloc_free
# 設置輸出不要分屏
set pagination off
b my_malloc
comm
silent
printf "malloc 0x%x %lu\n", p, size
bt
c
end
b my_free
comm
silent
printf "free 0x%x\n", p
bt
c
end
c
直接讓gdb執行它:
sudo gdb -q -p $(pidof $your_project) -x /tmp/malloc_free > log
運行一段時間后kill掉gdb,打開log看看里面的內容:
$ less log
Attaching to process 8738
Reading symbols from ...done.
Reading symbols from /lib/x86_64-linux-gnu/libc.so.6...Reading symbols from /usr/
lib/debug//lib/x86_64-linux-gnu/libc-2.19.so...done.
done.
Loaded symbols for /lib/x86_64-linux-gnu/libc.so.6
......
malloc 0x0 82
#0 my_malloc (p=0x0, size=82) at memory.cpp:8
#1 0x0000000000400657 in write_buffer (p=0x0, size=82) at memory.cpp:17
#2 0x00000000004006b6 in main () at memory.cpp:25
malloc 0x852c39c0 13
#0 my_malloc (p=0x7ffd852c39c0 "\001", size=13) at memory.cpp:8
#1 0x0000000000400657 in write_buffer (p=0x7ffd852c39c0 "\001", size=13) at memory.cpp:17
#2 0x00000000004006b6 in main () at memory.cpp:25
free 0x400780
#0 my_free (p=0x400780 <__libc_csu_init> "AWA\211\377AVI\211\366AUI\211\325ATL\215%x\006 ") at memory.cpp:14
#1 0x0000000000400632 in read_buffer (p=0x400780 <__libc_csu_init> "AWA\211\377AVI\211\366AUI\211\325ATL\215%x\006 ") at memory.cpp:16
#2 0x00000000004006fe in main () at memory.cpp:28
free 0x0
......
現在我們可以寫個腳本對下帳。每次解析到 malloc 時,在對應指針的名下記下一項借出。解析到 free 時,表示銷掉對應最近一次借出的還款。把全部輸出解析完后,困擾已久的壞賬情況就將水落石出,欠錢不還的老賴也將無可遁形。這種“拖網法”真的是簡單粗暴又有效。
我們還可以用這種“拖網法”獲取指定函數的調用者比例、調用參數的分布范圍等等。注意,不要在生產環境撒網,畢竟這么做對性能有顯著影響。而且要做統計的話,也有更好的方法可以選。
用python拓展gdb
除了用gdb自身的DSL,我們還可以使用python來給gdb寫腳本。憑借python的力量,我們甚至可以在gdb里跟外部程序交互,展示更多的可能性。“你們對力量一無所知”。
欲知后事如何,請聽下回分解。
來自: https://segmentfault.com/a/1190000005367875