程序員在調試時每每分紅兩派,一派用debugger另外一派用print。至於本人嘛,是一個「機會主義者」,有時用print,有時卻改投debugger陣營。python
實話說,print要比用debugger設下斷點更爲簡單粗暴,有時甚至會更有用。不過debugger對比於print有三個優勢:linux
無需從新編譯ios
能夠在調試時改變變量程序員
debugger能夠實現print作不到的複雜操做segmentfault
在本文,我會介紹一些在gdb中自動化操做的技術,保證可讓你大開眼界,見識下gdb真正的力量。less
一般咱們只有在程序出問題纔會啓動gdb,開始調試工做,調試完畢後退出。不過,讓gdb一直開着何嘗不是更好的作法。每一個gdb老司機都懂得,gdb在r
的時候會加載當前程序的最新版本。也便是說,就算不退出gdb,每次運行的也會是當前最新的版本。不退出當前調試會話有兩個好處:編輯器
調試上下文能夠獲得保留。不用每次運行都從新設一輪斷點。函數
一旦core dump了,能夠顯示core dump的位置,無需帶着core從新啓動一次。性能
在開發C/C++項目,我通常是這樣的工做流程:一個窗口開着編輯器,編譯也在這個窗口執行;另外一個窗口開着gdb,這個窗口同時也用來運行程序。一旦要調試了(或者,又segment fault了),隨手就能夠開始幹活。debug
固然了,勞做一天以後,總須要關電腦回家。這時候只能退出gdb。不想明天一早再把斷點設上一遍?gdb提供了保留斷點的功能。輸入save br .gdb_bp
,gdb會把本次會話的斷點存在.gdb_bp
中。明天早上一回來,啓動gdb的時候,加上-x .gdb_bp
,讓gdb把.gdb_bp
當作命令文件逐條從新執行,一切又回到昨晚。
下面是一個帶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
看看。
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
時,表示銷掉對應最近一次借出的還款。把所有輸出解析完後,困擾已久的壞帳狀況就將水落石出,欠錢不還的老賴也將無可遁形。這種「拖網法」真的是簡單粗暴又有效。
咱們還能夠用這種「拖網法」獲取指定函數的調用者比例、調用參數的分佈範圍等等。注意,不要在生產環境撒網,畢竟這麼作對性能有顯著影響。並且要作統計的話,也有更好的方法能夠選。
除了用gdb自身的DSL,咱們還可使用python來給gdb寫腳本。憑藉python的力量,咱們甚至能夠在gdb裏跟外部程序交互,展現更多的可能性。「大家對力量一無所知」。
欲知後事如何,請聽下回分解。