使用 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 來觸發程式崩潰時的傾印(請參閱 執行時期套件文件 以取得更多資訊)。

常見操作

Go 擴充功能

最近的 GDB 擴充機制允許它為指定的二進位檔載入擴充指令碼。工具鏈使用此機制來擴充 GDB,以使用一些指令檢查執行時期程式碼的內部結構(例如 goroutine),並以美觀的方式印出內建的 map、slice 和 channel 類型。

如果您想了解這項功能如何運作,或想進一步擴充,請參閱 Go 原始碼發行版中的 src/runtime/runtime-gdb.py。它仰賴一些特殊的魔法類型(hash<T,U>)和變數(runtime.mruntime.g),連結器 (src/cmd/link/internal/ld/dwarf.go) 會確保這些變數在 DWARF 程式碼中都有說明。

如果您有興趣了解除錯資訊的樣貌,請執行 objdump -W a.out 並瀏覽 .debug_* 區段。

已知問題

  1. 字串美觀印表僅會觸發字串類型,不會觸發衍生自字串類型的類型。
  2. 執行時期函式庫的 C 部分缺少型別資訊。
  3. GDB 不了解 Go 的名稱限定,並將 "fmt.Print" 視為非結構化文字,其中包含一個需要加上引號的 "."。它更強烈地反對 pkg.(*MyType).Meth 形式的方法名稱。
  4. 自 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 *