使用 GDB 除錯 Go 程式碼
以下說明適用於標準工具鏈(gc Go 編譯器和工具)。Gccgo 原生支援 gdb。
請注意,當除錯使用標準工具鏈構建的 Go 程式時,Delve 是 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 除錯資訊,最新版本(≥7.5)的 GDB 偵錯程式可以使用該資訊檢查即時程序或核心轉儲。
將 '-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,以檢查執行時程式碼的內部(例如 goroutines)並漂亮地列印內建的 map、slice 和 channel 型別。
- 漂亮地列印字串、切片、map、channel 或介面
(gdb) p var
- 字串、切片和 map 的 $len() 和 $cap() 函式
(gdb) p $len(var)
- 將介面強制轉換為其動態型別的函式
(gdb) p $dtype(var) (gdb) iface var
已知問題:如果介面值的長名稱與其短名稱不同,GDB 無法自動找到其動態型別(在列印堆疊跟蹤時很煩人,漂亮列印器會回退到列印短型別名稱和指標)。
- 檢查 goroutines
(gdb) info goroutines (gdb) goroutine n cmd (gdb) help goroutine
例如(gdb) goroutine 12 bt
您可以透過傳入all而不是特定的 goroutine ID 來檢查所有 goroutines。例如(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_* 部分。
已知問題
- 字串漂亮列印僅對型別 string 觸發,不對從其派生的型別觸發。
- 執行時庫的 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)
訊息 "Loading Go Runtime support" 意味著 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) {
執行已在斷點處暫停。檢視正在執行的 goroutines,以及它們正在做什麼
(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 中,阻塞在 channel 接收上
(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 錯誤地將 * 放在型別名稱的右側,並編造了一個“struct”關鍵字,採用傳統的 C 風格。
(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> 是 channel 的執行時內部表示。它當前為空,否則 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 的漂亮列印機制由型別名稱上的正則表示式匹配觸發。切片的一個例子
(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
Channel 和 map 是“引用”型別,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 *