使用 GDB 偵錯 Go 程式碼
下列說明適用於標準工具鏈(gc
Go 編譯器和工具)。Gccgo 有原生 gdb 支援。
請注意,Delve 是使用標準工具鏈建置 Go 程式時,比 GDB 更好的替代方案。它比 GDB 更了解 Go 執行時期、資料結構和表達式。Delve 目前支援 amd64
上的 Linux、OSX 和 Windows。如需最新支援平台清單,請參閱 Delve 文件。
GDB 不太了解 Go 程式。堆疊管理、執行緒和執行時期包含許多與 GDB 預期的執行模型不同的面向,這些面向可能會混淆除錯器,並在使用 gccgo 編譯程式時造成不正確的結果。因此,儘管 GDB 在某些情況下可能很有用(例如,除錯 Cgo 程式碼或除錯執行時期本身),但它並非 Go 程式的可靠除錯器,特別是高度並行的程式。此外,解決這些困難的問題並非 Go 專案的優先事項。
簡而言之,以下說明僅應視為 GDB 在運作時如何使用的指南,而非保證成功。除了這個概觀之外,您可能需要參閱 GDB 手冊。
簡介
當您在 Linux、macOS、FreeBSD 或 NetBSD 上使用 gc
工具鏈編譯和連結 Go 程式時,產生的二進位檔會包含 DWARFv4 除錯資訊,GDB 除錯器的最新版本(≥7.5)可以使用這些資訊來檢查即時處理或核心傾印。
傳遞 '-w'
旗標給連結器以省略除錯資訊(例如,go
build
-ldflags=-w
prog.go
)。
gc
編譯器產生的程式碼包含函式呼叫的內嵌和變數的暫存器化。這些最佳化有時會讓使用 gdb
除錯變得更困難。如果您發現您需要停用這些最佳化,請使用 go
build
-gcflags=all="-N -l"
來建置您的程式。
如果您想使用 gdb 來檢查核心傾印,您可以在允許的系統上,透過在環境中設定 GOTRACEBACK=crash
來觸發程式崩潰時的傾印(請參閱 執行時期套件文件 以取得更多資訊)。
常見操作
-
顯示程式碼的檔案和行號,設定中斷點並反組譯
(gdb) list (gdb) list line (gdb) list file.go:line (gdb) break line (gdb) break file.go:line (gdb) disas
-
顯示回溯並展開堆疊框架
(gdb) bt (gdb) frame n
-
顯示堆疊框架中局部變數、引數和傳回值的變數名稱、類型和位置
(gdb) info locals (gdb) info args (gdb) p variable (gdb) whatis variable
-
顯示全域變數的名稱、類型和位置
(gdb) info variables regexp
Go 擴充功能
最近的 GDB 擴充機制允許它為指定的二進位檔載入擴充指令碼。工具鏈使用此機制來擴充 GDB,以使用一些指令檢查執行時期程式碼的內部結構(例如 goroutine),並以美觀的方式印出內建的 map、slice 和 channel 類型。
-
以美觀的方式印出字串、slice、map、channel 或介面
(gdb) p var
-
針對字串、slice 和 map 的 $len() 和 $cap() 函式
(gdb) p $len(var)
-
將介面轉換為其動態類型的函式
(gdb) p $dtype(var) (gdb) iface var
已知問題:如果介面值的長名稱與其短名稱不同,GDB 無法自動找出其動態類型(在印出堆疊追蹤時很惱人,美觀印表機會改為印出短類型名稱和指標)。
-
檢查 goroutine
(gdb) info goroutines (gdb) goroutine n cmd (gdb) help goroutine
例如(gdb) goroutine 12 bt
您可以傳遞all
(而非特定 goroutine 的 ID)來檢查所有 goroutine。例如(gdb) goroutine all bt
如果您想了解這項功能如何運作,或想進一步擴充,請參閱 Go 原始碼發行版中的 src/runtime/runtime-gdb.py。它仰賴一些特殊的魔法類型(hash<T,U>
)和變數(runtime.m
和 runtime.g
),連結器 (src/cmd/link/internal/ld/dwarf.go) 會確保這些變數在 DWARF 程式碼中都有說明。
如果您有興趣了解除錯資訊的樣貌,請執行 objdump
-W
a.out
並瀏覽 .debug_*
區段。
已知問題
- 字串美觀印表僅會觸發字串類型,不會觸發衍生自字串類型的類型。
- 執行時期函式庫的 C 部分缺少型別資訊。
- GDB 不了解 Go 的名稱限定,並將
"fmt.Print"
視為非結構化文字,其中包含一個需要加上引號的"."
。它更強烈地反對pkg.(*MyType).Meth
形式的方法名稱。 - 自 Go 1.11 起,偵錯資訊預設會被壓縮。較舊版本的 gdb(例如 macOS 上預設提供的版本)不了解壓縮。你可以使用
go build -ldflags=-compressdwarf=false
來產生未壓縮的偵錯資訊。(為方便起見,你可以將-ldflags
選項放入GOFLAGS
環境變數 中,這樣就不必每次都指定它。)
教學
在本教學中,我們將檢查 regexp 套件單元測試的二進位檔。若要建立二進位檔,請變更為 $GOROOT/src/regexp
並執行 go
test
-c
。這應該會產生一個名為 regexp.test
的可執行檔。
入門
啟動 GDB,偵錯 regexp.test
$ gdb regexp.test GNU gdb (GDB) 7.2-gg8 Copyright (C) 2010 Free Software Foundation, Inc. License GPLv 3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> Type "show copying" and "show warranty" for licensing/warranty details. This GDB was configured as "x86_64-linux". Reading symbols from /home/user/go/src/regexp/regexp.test... done. Loading Go Runtime support. (gdb)
訊息「載入 Go 執行時期支援」表示 GDB 已從 $GOROOT/src/runtime/runtime-gdb.py
載入擴充功能。
若要協助 GDB 找出 Go 執行時期來源和隨附的支援腳本,請使用 '-d'
旗標傳遞你的 $GOROOT
$ gdb regexp.test -d $GOROOT
如果出於某種原因,GDB 仍然找不到該目錄或該腳本,你可以手動載入它,方法是告知 gdb(假設你的 go 來源在 ~/go/
中)
(gdb) source ~/go/src/runtime/runtime-gdb.py Loading Go Runtime support.
檢查來源
使用 "l"
或 "list"
命令檢查原始碼。
(gdb) l
列出原始碼的特定部分,並使用函式名稱參數化 "list"
(它必須使用其套件名稱限定)。
(gdb) l main.main
列出特定檔案和行號
(gdb) l regexp.go:1 (gdb) # Hit enter to repeat last command. Here, this lists next 10 lines.
命名
變數和函式名稱必須使用它們所屬套件的名稱限定。regexp
套件中的 Compile
函式在 GDB 中稱為 'regexp.Compile'
。
方法必須使用接收者類型的名稱進行限定。例如,*Regexp
類型的 String
方法稱為 'regexp.(*Regexp).String'
。
在偵錯資訊中,會自動將遮蔽其他變數的變數加上數字字尾。封閉函式所引用的變數會自動加上「&」字首,顯示為指標。
設定中斷點
在 TestFind
函式中設定中斷點
(gdb) b 'regexp.TestFind' Breakpoint 1 at 0x424908: file /home/user/go/src/regexp/find_test.go, line 148.
執行程式
(gdb) run Starting program: /home/user/go/src/regexp/regexp.test Breakpoint 1, regexp.TestFind (t=0xf8404a89c0) at /home/user/go/src/regexp/find_test.go:148 148 func TestFind(t *testing.T) {
執行已在中斷點暫停。查看哪些 goroutine 正在執行,以及它們在做什麼
(gdb) info goroutines 1 waiting runtime.gosched * 13 running runtime.goexit
標記為 *
的那個是目前的 goroutine。
檢查堆疊
查看我們暫停程式的堆疊追蹤
(gdb) bt # backtrace #0 regexp.TestFind (t=0xf8404a89c0) at /home/user/go/src/regexp/find_test.go:148 #1 0x000000000042f60b in testing.tRunner (t=0xf8404a89c0, test=0x573720) at /home/user/go/src/testing/testing.go:156 #2 0x000000000040df64 in runtime.initdone () at /home/user/go/src/runtime/proc.c:242 #3 0x000000f8404a89c0 in ?? () #4 0x0000000000573720 in ?? () #5 0x0000000000000000 in ?? ()
另一個 goroutine,編號 1,卡在 runtime.gosched
中,封鎖在通道接收上
(gdb) goroutine 1 bt #0 0x000000000040facb in runtime.gosched () at /home/user/go/src/runtime/proc.c:873 #1 0x00000000004031c9 in runtime.chanrecv (c=void, ep=void, selected=void, received=void) at /home/user/go/src/runtime/chan.c:342 #2 0x0000000000403299 in runtime.chanrecv1 (t=void, c=void) at/home/user/go/src/runtime/chan.c:423 #3 0x000000000043075b in testing.RunTests (matchString={void (struct string, struct string, bool *, error *)} 0x7ffff7f9ef60, tests= []testing.InternalTest = {...}) at /home/user/go/src/testing/testing.go:201 #4 0x00000000004302b1 in testing.Main (matchString={void (struct string, struct string, bool *, error *)} 0x7ffff7f9ef80, tests= []testing.InternalTest = {...}, benchmarks= []testing.InternalBenchmark = {...}) at /home/user/go/src/testing/testing.go:168 #5 0x0000000000400dc1 in main.main () at /home/user/go/src/regexp/_testmain.go:98 #6 0x00000000004022e7 in runtime.mainstart () at /home/user/go/src/runtime/amd64/asm.s:78 #7 0x000000000040ea6f in runtime.initdone () at /home/user/go/src/runtime/proc.c:243 #8 0x0000000000000000 in ?? ()
堆疊框架顯示我們目前正在執行 regexp.TestFind
函式,正如預期。
(gdb) info frame Stack level 0, frame at 0x7ffff7f9ff88: rip = 0x425530 in regexp.TestFind (/home/user/go/src/regexp/find_test.go:148); saved rip 0x430233 called by frame at 0x7ffff7f9ffa8 source language minimal. Arglist at 0x7ffff7f9ff78, args: t=0xf840688b60 Locals at 0x7ffff7f9ff78, Previous frame's sp is 0x7ffff7f9ff88 Saved registers: rip at 0x7ffff7f9ff80
info
locals
指令會列出函式中所有區域變數及其值,但使用時有點危險,因為它也會嘗試列印未初始化的變數。未初始化的切片可能會導致 gdb 嘗試列印任意大的陣列。
函式的引數
(gdb) info args t = 0xf840688b60
列印引數時,請注意它是指向 Regexp
值的指標。請注意,GDB 已不正確地將 *
放在類型名稱的右側,並使用傳統 C 風格編造了一個「struct」關鍵字。
(gdb) p re (gdb) p t $1 = (struct testing.T *) 0xf840688b60 (gdb) p t $1 = (struct testing.T *) 0xf840688b60 (gdb) p *t $2 = {errors = "", failed = false, ch = 0xf8406f5690} (gdb) p *t->ch $3 = struct hchan<*testing.T>
那個 struct
hchan<*testing.T>
是通道的執行時期內部表示形式。它目前是空的,否則 gdb 會漂亮地列印其內容。
向前執行
(gdb) n # execute next line 149 for _, test := range findTests { (gdb) # enter is repeat 150 re := MustCompile(test.pat) (gdb) p test.pat $4 = "" (gdb) p re $5 = (struct regexp.Regexp *) 0xf84068d070 (gdb) p *re $6 = {expr = "", prog = 0xf840688b80, prefix = "", prefixBytes = []uint8, prefixComplete = true, prefixRune = 0, cond = 0 '\000', numSubexp = 0, longest = false, mu = {state = 0, sema = 0}, machine = []*regexp.machine} (gdb) p *re->prog $7 = {Inst = []regexp/syntax.Inst = {{Op = 5 '\005', Out = 0, Arg = 0, Rune = []int}, {Op = 6 '\006', Out = 2, Arg = 0, Rune = []int}, {Op = 4 '\004', Out = 0, Arg = 0, Rune = []int}}, Start = 1, NumCap = 2}
我們可以使用 "s"
踏入 String
函式呼叫
(gdb) s regexp.(*Regexp).String (re=0xf84068d070, noname=void) at /home/user/go/src/regexp/regexp.go:97 97 func (re *Regexp) String() string {
取得堆疊追蹤以查看我們在哪裡
(gdb) bt #0 regexp.(*Regexp).String (re=0xf84068d070, noname=void) at /home/user/go/src/regexp/regexp.go:97 #1 0x0000000000425615 in regexp.TestFind (t=0xf840688b60) at /home/user/go/src/regexp/find_test.go:151 #2 0x0000000000430233 in testing.tRunner (t=0xf840688b60, test=0x5747b8) at /home/user/go/src/testing/testing.go:156 #3 0x000000000040ea6f in runtime.initdone () at /home/user/go/src/runtime/proc.c:243 ....
查看原始碼
(gdb) l 92 mu sync.Mutex 93 machine []*machine 94 } 95 96 // String returns the source text used to compile the regular expression. 97 func (re *Regexp) String() string { 98 return re.expr 99 } 100 101 // Compile parses a regular expression and returns, if successful,
漂亮列印
GDB 的漂亮列印機制是由類型名稱上的 regexp 比對觸發的。以下是切片的範例
(gdb) p utf $22 = []uint8 = {0 '\000', 0 '\000', 0 '\000', 0 '\000'}
由於切片、陣列和字串不是 C 指標,GDB 無法為您詮釋下標運算,但您可以查看執行時期表示來執行此動作(按 Tab 鍵完成有助於此處)
(gdb) p slc $11 = []int = {0, 0} (gdb) p slc-><TAB> array slc len (gdb) p slc->array $12 = (int *) 0xf84057af00 (gdb) p slc->array[1] $13 = 0
擴充函式 $len 和 $cap 可用於字串、陣列和切片
(gdb) p $len(utf) $23 = 4 (gdb) p $cap(utf) $24 = 4
通道和映射是「參考」類型,gdb 會將其顯示為指向類似 C++ 類型的指標 hash<int,string>*
。取消參考會觸發漂亮列印
介面在執行時期表示為指向類型描述的指標和指向值的指標。Go GDB 執行時期擴充會解碼此資料並自動觸發執行時期類型的漂亮列印。擴充函式 $dtype
會為您解碼動態類型(範例取自 regexp.go
第 293 行的中斷點)
(gdb) p i $4 = {str = "cbb"} (gdb) whatis i type = regexp.input (gdb) p $dtype(i) $26 = (struct regexp.inputBytes *) 0xf8400b4930 (gdb) iface i regexp.input: struct regexp.inputBytes *