使用 GDB 除錯
開發程式過程中除錯程式碼是開發者經常要做的一件事情,Go 語言不像 PHP、Python 等動態語言,只要修改不需要編譯就可以直接輸出,而且可以動態的在執行環境下列印資料。當然 Go 語言也可以透過 Println 之類別的列印資料來除錯,但是每次都需要重新編譯,這是一件相當麻煩的事情。我們知道在 Python 中有 pdb/ipdb 之類別的工具除錯,Javascript 也有類似工具,這些工具都能夠動態的顯示變數資訊,單步除錯等。不過慶幸的是 Go 也有類似的工具支援:GDB。Go 內部已經內建支援了 GDB,所以,我們可以透過 GDB 來進行除錯,那麼本小節就來介紹一下如何透過 GDB 來除錯 Go 程式。
另外建議純 go 程式碼使用delve可以很好的進行 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 <- 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 <var>=<value>
除錯過程
我們透過下面這個程式碼來示範如何透過 GDB 來除錯 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
上一節: 錯誤處理
下一節: Go 怎麼寫測試案例
Last updated
Was this helpful?