這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
11.2 使用GDB調試開發程式過程中調試代碼是開發人員經常要做的一件事情,Go語言不像PHP、Python等動態語言,只要修改不需要編譯就可以直接輸出,而且可以動態在運行環境下列印資料。當然Go語言也可以通過Println之類的列印資料來調試,但是每次都需要重新編譯,這是一件相當麻煩的事情。我們知道在Python中有pdb/ipdb之類的工具調試,Javascript也有類似工具,這些工具都能夠動態顯示變數資訊,單步調試等。不過慶幸的是Go也有類似的工具支援:GDB。Go內部已經內建支援了GDB,所以,我們可以通過GDB來進行調試,那麼本小節就來介紹一下如何通過GDB來調試Go程式。GDB調試簡介GDB是FSF(自由軟體基金會)發布的一個強大的類UNIX系統下的程式調試工具。使用GDB可以做如下事情:啟動程式,可以按照開發人員的自訂要求運行程式。可讓被調試的程式在開發人員設定的調置的斷點處停住。(斷點可以是條件運算式)當程式被停住時,可以檢查此時程式中所發生的事。動態改變當前程式的執行環境。目前支援調試Go程式的GDB版本必須大於7.1。編譯Go程式的時候需要注意以下幾點傳遞參數-ldflags "-s",忽略debug的列印資訊傳遞-gcflags "-N -l" 參數,這樣可以忽略Go內部做的一些最佳化,彙總變數和函數等最佳化,這樣對於GDB調試來說非常困難,所以在編譯的時候加入這兩個參數避免這些最佳化。常用命令GDB的一些常用命令如下所示list簡寫命令l,用來顯示原始碼,預設顯示十行代碼,後面可以帶上參數顯示的具體行,例如:list 15,顯示十行代碼,其中第15行在顯示的十行裡面的中間,如下所示。10 time.Sleep(2 * time.Second)11 c <- i12 }13 close(c)14 }15 16 func main() {17 msg := "Starting main"18 fmt.Println(msg)19 bus := make(chan int)break簡寫命令 b,用來設定斷點,後面跟上參數設定斷點的行數,例如b 10在第十行設定斷點。delete 簡寫命令 d,用來刪除斷點,後面跟上斷點設定的序號,這個序號可以通過info breakpoints擷取相應的設定的斷點序號,如下是顯示的設定斷點序號。· Num Type Disp Enb Address What· 2 breakpoint keep y 0x0000000000400dc3 in main.main at /home/xiemengjun/gdb.go:23breakpointalready hit 1 timebacktrace簡寫命令 bt,用來列印執行的代碼過程,如下所示:#0 main.main () at /home/xiemengjun/gdb.go:23#1 0x000000000040d61e in runtime.main () at/home/xiemengjun/go/src/pkg/runtime/proc.c:244#2 0x000000000040d6c1 in schedunlock () at/home/xiemengjun/go/src/pkg/runtime/proc.c:267#3 0x0000000000000000 in ?? ()infoinfo命令用來顯示資訊,後面有幾種參數,我們常用的有如下幾種:info locals顯示當前執行的程式中的變數值info breakpoints顯示當前設定的斷點列表info goroutines顯示當前執行的goroutine列表,如下代碼所示,帶*的表示當前執行的* 1 running runtime.gosched* 2 syscall runtime.entersyscall 3 waiting runtime.gosched 4 runnable runtime.goschedprint簡寫命令p,用來列印變數或者其他資訊,後面跟上需要列印的變數名,當然還有一些很有用的函數$len()和$cap(),用來返回當前string、slices或者maps的長度和容量。whatis用來顯示當前變數的類型,後面跟上變數名,例如whatis msg,顯示如下:type = structstringnext簡寫命令 n,用來單步調試,跳到下一步,當有斷點之後,可以輸入n跳轉到下一步繼續執行coutinue簡稱命令 c,用來跳出當前斷點處,後面可以跟參數N,跳過多少次斷點set variable該命令用來改變運行過程中的變數值,格式如:set variable <var>=<value>調試過程我們通過下面這個代碼來示範如何通過GDB來調試Go程式,下面是將要示範的代碼:package main import ( "fmt" "time") func counting(cchan<- int) { for i := 0; i < 10; i++ { time.Sleep(2 * time.Second) c <- i } close(c)} func main() { msg := "Starting main" fmt.Println(msg) bus := make(chan int) msg = "starting a gofunc" go counting(bus) for count := range bus { fmt.Println("count:", count) }}編譯檔案,產生可執行檔gdbfile:go build-gcflags "-N -l" gdbfile.go通過gdb命令啟動調試:gdb gdbfile啟動之後首先看看這個程式是不是可以運行起來,只要輸入run命令斷行符號後程式就開始運行,程式正常的話可以看到程式輸出如下,和我們在命令列直接執行程式輸出是一樣的:(gdb) runStartingprogram: /home/xiemengjun/gdbfileStarting maincount: 0count: 1count: 2count: 3count: 4count: 5count: 6count: 7count: 8count: 9[LWP 2771exited][Inferior 1(process 2771) exited normally]好了,現在我們已經知道怎麼讓程式跑起來了,接下來開始給代碼設定斷點:(gdb) br main.mainBreakpoint 1 at0x4010b0: file /data/code/gowork/src/my_code/d5.go, line 13.(gdb) br main.countingBreakpoint 2 at0x401000: file /data/code/gowork/src/my_code/d5.go, line 6.(gdb) list main.main time.Sleep(2 * time.Second) c <- i } close(c) } func main() { msg := "Starting main" fmt.Println(msg) bus := make(chan int) msg = "Starting a fo gofunction" (gdb) list main.counting package main import ( "fmt" "time" ) func counting(c chan<- int){ for i:=0;i < 10;i++ { time.Sleep(2 * time.Second) c <- i }(gdb) runStartingprogram: /home/xiemengjun/gdbfileStarting main[New LWP 3284][Switching toLWP 3284] Breakpoint 1,main.main () at /home/xiemengjun/gdbfile.go:2323 fmt.Println("count:",count)上面例子b 23表示在第23行設定了斷點,之後輸入run開始運行程式。現在程式在前面設定斷點的地方停住了,我們需要查看斷點相應內容相關的源碼,輸入list就可以看到源碼顯示從當前停止行的前五行開始:(gdb) list18 fmt.Println(msg)19 bus := make(chan int)20 msg = "starting a gofunc"21 go counting(bus)22 for count := range bus {23 fmt.Println("count:",count)24 }25 }現在GDB在運行當前的程式的環境中已經保留了一些有用的調試資訊,我們只需列印出相應的變數,查看相應變數的類型及值:(gdb) infolocalscount = 0bus =0xf840001a50(gdb) p count$1 = 0(gdb) p bus$2 = (chan int)0xf840001a50(gdb) whatis bustype = chan int接下來該讓程式繼續往下執行,請繼續看下面的命令(gdb) cContinuing.count: 0[New LWP 3303][Switching toLWP 3303] Breakpoint 1,main.main () at /home/xiemengjun/gdbfile.go:2323fmt.Println("count:", count)(gdb) cContinuing.count: 1[Switching toLWP 3302] Breakpoint 1,main.main () at /home/xiemengjun/gdbfile.go:2323fmt.Println("count:", count)每次輸入c之後都會執行一次代碼,又跳到下一次for迴圈,繼續列印出來相應的資訊。設想目前需要改變上下文相關變數的資訊,跳過一些過程,並繼續執行下一步,得出修改後想要的結果:(gdb) info localscount = 2bus =0xf840001a50(gdb) set variable count=9(gdb) info localscount = 9bus =0xf840001a50(gdb) cContinuing.count: 9[Switching toLWP 3302] Breakpoint 1,main.main () at /home/xiemengjun/gdbfile.go:2323fmt.Println("count:", count) 最後稍微思考一下,前面整個程式啟動並執行過程中到底建立了多少個goroutine,每個goroutine都在做什麼:(gdb) info goroutines* 1 runningruntime.gosched* 2 syscallruntime.entersyscall3 waitingruntime.gosched4 runnableruntime.gosched(gdb) goroutine 1 bt#00x000000000040e33b in runtime.gosched () at/home/xiemengjun/go/src/pkg/runtime/proc.c:927#10x0000000000403091 in runtime.chanrecv (c=void, ep=void, selected=void,received=void)at/home/xiemengjun/go/src/pkg/runtime/chan.c:327#20x000000000040316f in runtime.chanrecv2 (t=void, c=void)at/home/xiemengjun/go/src/pkg/runtime/chan.c:420#30x0000000000400d6f in main.main () at /home/xiemengjun/gdbfile.go:22#40x000000000040d0c7 in runtime.main () at/home/xiemengjun/go/src/pkg/runtime/proc.c:244#5 0x000000000040d16ain schedunlock () at /home/xiemengjun/go/src/pkg/runtime/proc.c:267#60x0000000000000000 in ?? ()通過查看goroutines的命令我們可以清楚地瞭解goruntine內部是怎麼執行的,每個函數的調用順序已經明明白白地顯示出來了。小結本小節我們介紹了GDB調試Go程式的一些基本命令,包括run、print、info、set variable、coutinue、list、break等經常用到的調試命令,通過上面的例子示範,我相信讀者已經對於通過GDB調試Go程式有了基本的理解,如果你想擷取更多的調試技巧請參考官方網站的GDB調試手冊,還有GDB官方網站的手冊