# 11.2 使用 GDB 除錯 開發程式過程中除錯程式碼是開發者經常要做的一件事情,Go 語言不像 PHP、Python 等動態語言,只要修改不需要編譯就可以直接輸出,而且可以動態的在執行環境下列印資料。當然 Go 語言也可以透過 Println 之類別的列印資料來除錯,但是每次都需要重新編譯,這是一件相當麻煩的事情。我們知道在 Python 中有 pdb/ipdb 之類別的工具除錯,Javascript 也有類似工具,這些工具都能夠動態的顯示變數資訊,單步除錯等。不過慶幸的是 Go 也有類似的工具支援:GDB。Go 內部已經內建支援了 GDB,所以,我們可以透過 GDB 來進行除錯,那麼本小節就來介紹一下如何透過 GDB 來除錯 Go 程式。 另外建議純 go 程式碼使用[delve](https://github.com/derekparker/delve)可以很好的進行 Go 程式碼除錯 ## GDB 除錯簡介 GDB 是 FSF(自由軟體基金會)釋出的一個強大的類別 UNIX 系統下的程式除錯工具。使用 GDB 可以做如下事情: 1. 啟動程式,可以按照開發者的自訂要求執行程式。 2. 可讓被除錯的程式在開發者設定的調置的斷點處停住。(斷點可以是條件表示式) 3. 當程式被停住時,可以檢查此時程式中所發生的事。 4. 動態的改變當前程式的執行環境。 目前支援除錯 Go 程式的 GDB 版本必須大於 7.1。 編譯 Go 程式的時候需要注意以下幾點 1. 傳遞參數-ldflags "-s",忽略 debug 的列印資訊 2. 傳遞-gcflags "-N -l" 參數,這樣可以忽略 Go 內部做的一些優化,聚合變數和函式等優化,這樣對於 GDB 除錯來說非常困難,所以在編譯的時候加入這兩個參數避免這些優化。 ## 常用命令 GDB 的一些常用命令如下所示 - list 簡寫命令`l`,用來顯示原始碼,預設顯示十行程式碼,後面可以帶上參數顯示的具體行,例如:`list 15`,顯示十行程式碼,其中第 15 行在顯示的十行裡面的中間,如下所示。 10 time.Sleep(2 * time.Second) 11 c <- i 12 } 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:23 breakpoint already hit 1 time - backtrace 簡寫命令 `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 ?? () - info info 命令用來顯示資訊,後面有幾種參數,我們常用的有如下幾種: - `info locals` 顯示當前執行的程式中的變數值 - `info breakpoints` 顯示當前設定的斷點列表 - `info goroutines` 顯示當前執行的 goroutine 列表,如下程式碼所示,帶*的表示當前執行的 * 1 running runtime.gosched * 2 syscall runtime.entersyscall 3 waiting runtime.gosched 4 runnable runtime.gosched - print 簡寫命令`p`,用來列印變數或者其他資訊,後面跟上需要列印的變數名,當然還有一些很有用的函式$len()和$cap(),用來回傳當前 string、slices 或者 maps 的長度和容量。 - whatis 用來顯示當前變數的型別,後面跟上變數名,例如`whatis msg`,顯示如下: type = struct string - next 簡寫命令 `n`,用來單步除錯,跳到下一步,當有斷點之後,可以輸入 `n` 跳轉到下一步繼續執行 - continue 簡稱命令 `c`,用來跳出當前斷點處,後面可以跟參數 N,跳過多少次斷點 - set variable 該命令用來改變執行過程中的變數值,格式如:`set variable =` ## 除錯過程 我們透過下面這個程式碼來示範如何透過 GDB 來除錯 Go 程式,下面是將要示範的程式碼: ```Go package main import ( "fmt" "time" ) func counting(c chan<- 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` 命令 Enter 後程序就開始執行,程式正常的話可以看到程式輸出如下,和我們在命令列直接執行程式輸出是一樣的: (gdb) run Starting program: /home/xiemengjun/gdbfile Starting main count: 0 count: 1 count: 2 count: 3 count: 4 count: 5 count: 6 count: 7 count: 8 count: 9 [LWP 2771 exited] [Inferior 1 (process 2771) exited normally] 好了,現在我們已經知道怎麼讓程式跑起來了,接下來開始給程式碼設定斷點: (gdb) b 23 Breakpoint 1 at 0x400d8d: file /home/xiemengjun/gdbfile.go, line 23. (gdb) run Starting program: /home/xiemengjun/gdbfile Starting main [New LWP 3284] [Switching to LWP 3284] Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23 23 fmt.Println("count:", count) 上面例子`b 23`表示在第 23 行設定了斷點,之後輸入 `run` 開始執行程式。現在程式在前面設定斷點的地方停住了,我們需要檢視斷點相應上下文的原始碼,輸入 `list` 就可以看到原始碼顯示從當前停止行的前五行開始: (gdb) list 18 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) info locals count = 0 bus = 0xf840001a50 (gdb) p count $1 = 0 (gdb) p bus $2 = (chan int) 0xf840001a50 (gdb) whatis bus type = chan int 接下來該讓程式繼續往下執行,請繼續看下面的命令 (gdb) c Continuing. count: 0 [New LWP 3303] [Switching to LWP 3303] Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23 23 fmt.Println("count:", count) (gdb) c Continuing. count: 1 [Switching to LWP 3302] Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23 23 fmt.Println("count:", count) 每次輸入 `c` 之後都會執行一次程式碼,又跳到下一次 for 迴圈,繼續顯示出來相應的資訊。 設想目前需要改變上下文相關變數的資訊,跳過一些過程,並繼續執行下一步,得出修改後想要的結果: (gdb) info locals count = 2 bus = 0xf840001a50 (gdb) set variable count=9 (gdb) info locals count = 9 bus = 0xf840001a50 (gdb) c Continuing. count: 9 [Switching to LWP 3302] Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23 23 fmt.Println("count:", count) 最後稍微思考一下,前面整個程式執行的過程中到底建立了多少個 goroutine,每個 goroutine 都在做什麼: (gdb) info goroutines * 1 running runtime.gosched * 2 syscall runtime.entersyscall 3 waiting runtime.gosched 4 runnable runtime.gosched (gdb) goroutine 1 bt #0 0x000000000040e33b in runtime.gosched () at /home/xiemengjun/go/src/pkg/runtime/proc.c:927 #1 0x0000000000403091 in runtime.chanrecv (c=void, ep=void, selected=void, received=void) at /home/xiemengjun/go/src/pkg/runtime/chan.c:327 #2 0x000000000040316f in runtime.chanrecv2 (t=void, c=void) at /home/xiemengjun/go/src/pkg/runtime/chan.c:420 #3 0x0000000000400d6f in main.main () at /home/xiemengjun/gdbfile.go:22 #4 0x000000000040d0c7 in runtime.main () at /home/xiemengjun/go/src/pkg/runtime/proc.c:244 #5 0x000000000040d16a in schedunlock () at /home/xiemengjun/go/src/pkg/runtime/proc.c:267 #6 0x0000000000000000 in ?? () 透過檢視 goroutines 的命令我們可以清楚地了解 goruntine 內部是怎麼執行的,每個函式的呼叫順序已經明明白白地顯示出來了。 ## 小結 本小節我們介紹了 GDB 除錯 Go 程式的一些基本命令,包括`run`、`print`、`info`、`set variable`、`coutinue`、`list`、`break` 等經常用到的除錯命令,透過上面的例子示範,我相信讀者已經對於透過 GDB 除錯 Go 程式有了基本的理解,如果你想取得更多的除錯技巧請參考官方網站的 GDB 除錯手冊,還有 GDB 官方網站的手冊。 ## links * [目錄]() * 上一節:[錯誤處理](<11.1.md>) * 下一節:[Go 怎麼寫測試案例](<11.3.md>)