使用 gdb 工具調(diào)試 Go
排除應(yīng)用程序故障是比較復(fù)雜的,特別是處理像 Go 這樣的高并發(fā)語言。它更容易在具體位置使用 print 打印語句來確定程序狀態(tài),但是這個(gè)方法很難根據(jù)條件發(fā)展去動(dòng)態(tài)響應(yīng)你的代碼。
調(diào)試器提供了一個(gè)強(qiáng)大得令人難以置信的故障排除機(jī)制。添加排除故障的代碼可以巧妙地影響到應(yīng)用程序該如何運(yùn)行。調(diào)試器可以給正在迷茫的你更精確的看法。
已經(jīng)有許多 Go 的調(diào)試器存在了,其中一些調(diào)試器的不好之處是通過在編譯時(shí)注入代碼來提供一個(gè)交互終端。gdb 調(diào)試器則允許你調(diào)試已經(jīng)編譯好的二進(jìn)制文件,只要他們已經(jīng)與 debug 信息連接,并不用修改源代碼。這是個(gè)相當(dāng)不錯(cuò)的特性,因此你可以從你的部署環(huán)境中取一個(gè)產(chǎn)品然后靈活地調(diào)試它。你可以從Golang 官方文檔中閱讀更多關(guān)于 gdb 的信息,那么這篇指南將簡單講解使用 gdb 調(diào)試器來調(diào)試 Go 應(yīng)用程序的基本用法。
這兒會(huì)宣布一些 gdb 的***更新,最特別的是替換 -> 操作為 . 符號來訪問對象屬性。記住這兒可能在gdb 和 Go 版本中有細(xì)微改變。本篇指南基于 gdb 7.7.1和go 1.5beta2。
開始 gdb 調(diào)試
為了實(shí)驗(yàn) gdb 我使用了一個(gè)測試程序,完整的源代碼可以在gdb_sandbox_on_Github上查看。讓我們從一個(gè)非常簡單的程序開始吧:
- package main
- import (
- "fmt"
- )
- func main() {
- for i := 0; i < 5; i++ {
- fmt.Println("looping")
- }
- fmt.Println("Done")
- }
我們可以運(yùn)行這段代碼并看到它輸出內(nèi)容的和我們想象的一樣:
- $ go run main.go
- looping
- looping
- looping
- looping
- looping
- Done
我們來調(diào)試這個(gè)程序吧。首先,使用 go build 編譯成二進(jìn)制文件,接著使用這個(gè)二進(jìn)制文件的路徑做為參數(shù)運(yùn)行 gdb。根據(jù)你的設(shè)定,你也可以使用 source 命令來獲取 Go 運(yùn)行時(shí)(Go runtime)的支持?,F(xiàn)在我們已經(jīng)在 gdb 的命令行中了,我們可以在運(yùn)行我們的二進(jìn)制文件前為它設(shè)置斷點(diǎn)。
- $ go build -gcflags "-N -l" -o gdb_sandbox main.go
- $ ls
- gdb_sandbox main.go README.md
- $ gdb gdb_sandbox
- ....
- (gdb) source /usr/local/src/go/src/runtime/runtime-gdb.py
- Loading Go Runtime support.
***關(guān),我們在 for 循環(huán)里面設(shè)置一個(gè)斷點(diǎn)(b)來查看執(zhí)行每次循環(huán)時(shí)我們的代碼會(huì)各有什么狀態(tài)。我們可以使用print(p)命令來檢查當(dāng)前內(nèi)容的一個(gè)變量,還有 list(l)和 backtrace(bt)命令查看當(dāng)前步驟周圍的代碼。程序運(yùn)行時(shí)可以使用 next(n)執(zhí)行下一步或者使用 breakpoint(c)執(zhí)行到下一個(gè)斷點(diǎn)。
- (gdb) b main.go:9
- Breakpoint 1 at 0x400d35: file /home/bfosberry/workspace/gdb_sandbox/main.go, line 9.
- (gdb) run
- Starting program: /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/gdb_sandbox Breakpoint 1, main.main () at
- /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:9
- 9 fmt.Println("looping")
- (gdb) l
- 4 "fmt"
- 5 )
- 6
- 7 func main() {
- 8 for i := 0; i < 5; i++ {
- 9 fmt.Println("looping")
- 10 }`
- 11 fmt.Println("Done")
- 12 }
- (gdb) p i
- $1 = 0
- (gdb) n
- looping
- Breakpoint 1, main.main () at
- /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:9
- 9 fmt.Println("looping")
- (gdb) p i
- $2 = 1
- (gdb) bt
- # 0 main.main () at /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:9
我們的斷點(diǎn)可以設(shè)置在關(guān)聯(lián)文件的行號中、GOPATH里的文件的行號或一個(gè)包里的函數(shù)。如下也是一個(gè)有效的斷點(diǎn):
- (gdb) b github.com/bfosberry/gdb_sandbox/main.go:9
- (gdb) b 'main.main'
Structs
我們可以用稍微復(fù)雜一點(diǎn)的代碼來實(shí)例演示如何調(diào)試。我們將使用f函數(shù)生成一個(gè)簡單的pair,x和y,當(dāng)x相等時(shí)y=f(x),否則=x。
- type pair struct {
- x int
- y int
- }
- func handleNumber(i int) *pair {
- val := i
- if i%2 == 0 {
- val = f(i)
- }
- return &pair{
- x: i,
- y: val,
- }
- }
- func f(int x) int {
- return x*x + x
- }
也可以在循環(huán)中改變代碼來訪問這些新函數(shù)。
- p := handleNumber(i)
- fmt.Printf("%+v/n", p)
- fmt.Println("looping")
因?yàn)槲覀冃枰{(diào)試的是變量 y。我們可以在y被設(shè)置的地方放置斷點(diǎn)然后單步執(zhí)行。可以使用 info args 查看函數(shù)的參數(shù),在 bt 之前可以返回當(dāng)前回溯。
- (gdb) b 'main.f'
- (gdb) run
- Starting program: /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/gdb_sandbox
- Breakpoint 1, main.f (x=0, ~anon1=833492132160)
- at /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:33
- 33 return x*x + x
- (gdb) info args
- x = 0
- (gdb) continue
- Breakpoint 1, main.f (x=0, ~anon1=833492132160)
- at /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:33
- 33 return x*x + x
- (gdb) info args
- x = 2
- (gdb) bt
- #0 main.f (x=2, ~anon1=1)
- at /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:33
- #1 0x0000000000400f0e in main.handleNumber (i=2, ~anon1=0x1)
- at /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:24
- #2 0x0000000000400c47 in main.main ()
- at /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:14
因?yàn)槲覀冊谧兞?y 是在函數(shù) f 中被設(shè)定的這樣一個(gè)條件下,我們可以跳到這個(gè)函數(shù)的上下文并檢查堆區(qū)的代碼。應(yīng)用運(yùn)行時(shí)我們可以在一個(gè)更高的層次上設(shè)置斷點(diǎn)并檢查其狀態(tài)。
- (gdb) b main.go:26
- Breakpoint 2 at 0x400f22: file
- /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go, line 26.
- (gdb) continue
- Continuing.
- Breakpoint 2, main.handleNumber (i=2, ~anon1=0x1)
- at /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:28
- 28 y: val,
- (gdb) l
- 23 if i%2 == 0 {
- 24 val = f(i)
- 25 }
- 26 return &pair{
- 27 x: i,
- 28 y: val,
- 29 }
- 30 }
- 31
- 32 func f(x int) int {
- (gdb) p val
- $1 = 6
- (gdb) p i
- $2 = 2
如果我們在這個(gè)斷點(diǎn)處繼續(xù)住下走我們將越過在這個(gè)函數(shù)中的斷點(diǎn)1,而且將立即觸發(fā)在 HandleNumer 函數(shù)中的斷點(diǎn),因?yàn)楹瘮?shù) f 只是對變量 i 每隔一次才執(zhí)行。我們可以通過暫時(shí)使斷點(diǎn) 2不工作來避免這種情況的發(fā)生。
- (gdb) disable breakpoint 2
- (gdb) continue
- Continuing.
- &{x:2 y:6}
- looping
- &{x:3 y:3}
- looping
- [New LWP 15200]
- [Switching to LWP 15200]
- Breakpoint 1, main.f (x=4, ~anon1=1)
- at /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:33
- 33 return x*x + x
- (gdb)
我們也可以分別使用 clear 和 delete breakpoint NUMBER 來清除和刪除斷點(diǎn)。動(dòng)態(tài)產(chǎn)生和系住斷點(diǎn),我們可以有效地在應(yīng)用流中來回移動(dòng)。
Slices and Pointers
上例程序太簡單了,只用到了整數(shù)型和字符串,所以我們將寫一個(gè)稍微復(fù)雜一點(diǎn)的。首先添加一個(gè)slice(切片類型)的指針到 main 函數(shù),并保存生成的 pair,我們后面將用到它。
- var pairs []*pair
- for i := 0; i < 10; i++ {
- p := handleNumber(i)
- fmt.Printf("%+v/n", p)
- pairs = append(pairs, p)
- fmt.Println("looping")
- }
現(xiàn)在我們來檢查生成出來的 slice 或 pairs,首先我們用轉(zhuǎn)換成數(shù)組來看一下這個(gè) slice。因?yàn)?handleNumber 返回的是一個(gè) *pair 類型,我們需要引用這個(gè)指針來訪問 struct(結(jié)構(gòu))的屬性。
- (gdb) b main.go:18
- Breakpoint 1 at 0x400e14: file /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go, line 18.
- (gdb) run
- Starting program: /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/gdb_sandbox &{x:0 y:0}
- Breakpoint 1, main.main () at /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:18
- 18 fmt.Println("looping")
- (gdb) p pairs
- $1 = []*main.pair = {0xc82000a3a0}
- (gdb) p pairs[0]
- Structure has no component named operator[].
- (gdb) p pairs.array
- $2 = (struct main.pair **) 0xc820030028
- (gdb) p pairs.array[0]
- $3 = (struct main.pair *) 0xc82000a3a0
- (gdb) p *pairs.array[0]
- $4 = {x = 0, y = 0}
- (gdb) p (*pairs.array[0]).x
- $5 = 0
- (gdb) p (*pairs.array[0]).y
- $6 = 0
- (gdb) continue
- Continuing.
- looping
- &{x:1 y:1}
- Breakpoint 1, main.main () at /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:18
- 18 fmt.Println("looping")
- (gdb) p (pairs.array[1][5]).y
- $7 = 1
- (gdb) continue
- Continuing.
- looping
- &{x:2 y:6}
- Breakpoint 1, main.main () at /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:18
- 18 fmt.Println("looping")
- (gdb) p (pairs.array[2][6]).y
- $8 = 6
- (gdb)
你會(huì)發(fā)現(xiàn)這里 gdb 并不確定 pairs 是一個(gè) slice 類型,我們不能直接訪問它的屬性,為了訪問它的成員我們需要使用 pairs.array 來轉(zhuǎn)換成數(shù)組,然后我們就可以檢查 slice 的 length(長度)和 capacity(容量):
- (gdb) p $len(pairs)
- $12 = 3
- (gdb) p $cap(pairs)
- $13 = 4
這時(shí)我們可以讓它循環(huán)幾次,并透過這個(gè) slice 不用的成員方法監(jiān)聽增加的 x 和 y 的值,要注意的是,這里的 struct 屬性可以通過指針訪問,所以 p pairs.array[2].y 一樣可行。
Goroutines
現(xiàn)在我們已經(jīng)可以訪問 struct 和 slice 了,下面再來更加復(fù)雜一點(diǎn)的程序吧。讓我們添加一些goroutines 到 mian 函數(shù),并行處理每一個(gè)數(shù)字,返回的結(jié)果存入信道(chan)中:
- pairs := []*pair{}
- pairChan := make(chan *pair)
- wg := sync.WaitGroup{}
- for i := 0; i < 10; i++ {
- wg.Add(1)
- go func(val int) {
- p := handleNumber(val)
- fmt.Printf("%+v/n", p)
- pairChan <- p
- wg.Done()
- }(i)
- }
- go func() {
- for p := range pairChan {
- pairs = append(pairs, p)
- }
- }()
- wg.Wait()
- close(pairChan)
- 如果我等待 WaitGroup 執(zhí)行完畢再檢查 pairs slice 的結(jié)果,我們可以預(yù)期到內(nèi)容是完全相同的,雖然它的排序可能有些出入。gdb 真正的威力來自于它可以在 goroutines 正在運(yùn)行時(shí)進(jìn)行檢查:
- (gdb) b main.go:43
- Breakpoint 1 at 0x400f7f: file /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go, line 43.
- (gdb) run
- Starting program: /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/gdb_sandbox
- Breakpoint 1, main.handleNumber (i=0, ~r1=0x0)
- at /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:43
- 43 y: val,
- (gdb) l
- 38 if i%2 == 0 {
- 39 val = f(i)
- 40 }
- 41 return &pair{
- 42 x: i,
- 43 y: val,
- 44 }
- 45 }
- 46
- 47 func f(x int) int {
- (gdb) info args
- i = 0
- ~r1 = 0x0
- (gdb) p val
- $1 = 0
你會(huì)發(fā)現(xiàn)我們在 goroutine 要執(zhí)行的代碼段中放置了一個(gè)斷點(diǎn),從這里我們可以檢查到局部變量,和進(jìn)程中的其它 goroutines:
- (gdb) info goroutines
- 1 waiting runtime.gopark
- 2 waiting runtime.gopark
- 3 waiting runtime.gopark
- 4 waiting runtime.gopark
- * 5 running main.main.func1
- 6 runnable main.main.func1
- 7 runnable main.main.func1
- 8 runnable main.main.func1
- 9 runnable main.main.func1
- * 10 running main.main.func1
- 11 runnable main.main.func1
- 12 runnable main.main.func1
- 13 runnable main.main.func1
- 14 runnable main.main.func1
- 15 waiting runtime.gopark
- (gdb) goroutine 11 bt
- #0 main.main.func1 (val=6, pairChan=0xc82001a180, &wg=0xc82000a3a0)
- at /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:19
- #1 0x0000000000454991 in runtime.goexit () at /usr/local/go/src/runtime/asm_amd64.s:1696
- #2 0x0000000000000006 in ?? ()
- #3 0x000000c82001a180 in ?? ()
- #4 0x000000c82000a3a0 in ?? ()
- #5 0x0000000000000000 in ?? ()
- (gdb) goroutine 11 l
- 48 return x*x + x
- 49 }
- (gdb) goroutine 11 info args
- val = 6
- pairChan = 0xc82001a180
- &wg = 0xc82000a3a0
- (gdb) goroutine 11 p val
- $2 = 6
在這里我們做的***件事就是列出所有正在運(yùn)行的 goroutine,并確定我們正在處理的那一個(gè)。然后我們可以看到一些回溯,并發(fā)送任何調(diào)試命令到 goroutine。這個(gè)回溯和列表清單并不太準(zhǔn)確,如何讓回溯更準(zhǔn)確,goroutine 上的 info args 顯示了我們的局部變量,以及主函數(shù)中的可用變量,goroutine 函數(shù)之外的使用前綴&。
結(jié)論
當(dāng)調(diào)試應(yīng)用時(shí),gdb 的強(qiáng)大令人難以置信。但它仍然是一個(gè)相當(dāng)新的事物,并不是所有的地方工作地都很***。使用***的穩(wěn)定版 gdb,go 1.5 beta2,有不少地方有突破:
Interfaces
根據(jù) go 博客上的文章, go 的 interfaces 應(yīng)該已經(jīng)支持了,這允許在 gdb 中動(dòng)態(tài)的投影其基類型。這應(yīng)該算一個(gè)突破。
Interface{} 類型
目前沒有辦法轉(zhuǎn)換 interface{} 為它的類型。
列出 goroutine 的不同點(diǎn)
在其他 goroutine 中列出周邊代碼會(huì)導(dǎo)致一些行數(shù)的漂移,最終導(dǎo)致 gdb 認(rèn)為當(dāng)前的行數(shù)超出文件范圍并拋出一個(gè)錯(cuò)誤:
- (gdb) info goroutines
- 1 waiting runtime.gopark
- 2 waiting runtime.gopark
- 3 waiting runtime.gopark
- 4 waiting runtime.gopark
- * 5 running main.main.func1
- 6 runnable main.main.func1
- 7 runnable main.main.func1
- 8 runnable main.main.func1
- 9 runnable main.main.func1
- * 10 running main.main.func1
- 11 runnable main.main.func1
- 12 runnable main.main.func1
- 13 runnable main.main.func1
- 14 runnable main.main.func1
- 15 waiting runtime.gopark
- (gdb) goroutine 11 bt
- #0 main.main.func1 (val=6, pairChan=0xc82001a180, &wg=0xc82000a3a0)
- at /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:19
- #1 0x0000000000454991 in runtime.goexit () at /usr/local/go/src/runtime/asm_amd64.s:1696
- #2 0x0000000000000006 in ?? ()
- #3 0x000000c82001a180 in ?? ()
- #4 0x000000c82000a3a0 in ?? ()
- #5 0x0000000000000000 in ?? ()
- (gdb) goroutine 11 l
- 48 return x*x + x
- 49 }
- (gdb) goroutine 11 l
- Python Exception <class 'gdb.error'> Line number 50 out of range; /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go has 49 lines.:
- Error occurred in Python command: Line number 50 out of range; /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go has 49 lines.
Goroutine 調(diào)試還不穩(wěn)定
處理 goroutines 往往不穩(wěn)定;我遇到過執(zhí)行簡單命令產(chǎn)生錯(cuò)誤的情況?,F(xiàn)階段你應(yīng)該做好處理類似問題的準(zhǔn)備。
gdb 支持 Go 的配置非常麻煩
運(yùn)行 gdb 支持 Go 調(diào)試的配置非常麻煩,獲取正確的路徑結(jié)合與構(gòu)建 flags,還有 gdb 自動(dòng)加載功能好像都不能正常的工作。首先,通過一個(gè) gdb 初始化文件加載 Go 運(yùn)行時(shí)支持就會(huì)產(chǎn)生初始化錯(cuò)誤。這就需要手動(dòng)通過一個(gè)源命令去加載,調(diào)試 shell 需要像指南里面描述的那樣去進(jìn)行初始化。
我什么時(shí)候該使用一個(gè)調(diào)試器?
所以什么情況下使用 gdb 更有用?使用 print 語言和調(diào)試代碼是更有針對性的方法。
-
當(dāng)不適合修改代碼的時(shí)候
-
當(dāng)調(diào)試一個(gè)問題,但是不知道源頭,動(dòng)態(tài)斷點(diǎn)或許更有效
-
當(dāng)包含許多 goroutines 時(shí),暫停然后審查程序狀態(tài)會(huì)更好
“Debugging #golang with gdb” – via @codeship —— from Tweet