GDB 自動化操做的技術

程序員在調試時每每分紅兩派,一派用debugger另外一派用print。至於本人嘛,是一個「機會主義者」,有時用print,有時卻改投debugger陣營。python

實話說,print要比用debugger設下斷點更爲簡單粗暴,有時甚至會更有用。不過debugger對比於print有三個優勢:linux

  1. 無需從新編譯ios

  2. 能夠在調試時改變變量程序員

  3. debugger能夠實現print作不到的複雜操做segmentfault

在本文,我會介紹一些在gdb中自動化操做的技術,保證可讓你大開眼界,見識下gdb真正的力量。less

會話/歷史/命令文件

一般咱們只有在程序出問題纔會啓動gdb,開始調試工做,調試完畢後退出。不過,讓gdb一直開着何嘗不是更好的作法。每一個gdb老司機都懂得,gdb在r的時候會加載當前程序的最新版本。也便是說,就算不退出gdb,每次運行的也會是當前最新的版本。不退出當前調試會話有兩個好處:編輯器

  1. 調試上下文能夠獲得保留。不用每次運行都從新設一輪斷點。函數

  2. 一旦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當作命令文件逐條從新執行,一切又回到昨晚。

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

如今就只有第三次調用會觸發斷點。

問題看上去跟floorceiling值的變化有關。要想觀察它們的值,能夠p floorp 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 localsi args命令會被觸發,列出當前上下文內的變量。這個功能挺廢的,由於你徹底能夠在斷點被觸發後才敲入這幾個命令。要不是有definecommands就真成擺設了。接下來咱們要介紹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裏跟外部程序交互,展現更多的可能性。「大家對力量一無所知」。

欲知後事如何,請聽下回分解。

相關文章
相關標籤/搜索