使用 gdb 工具除錯 Go

oschina發表於2015-08-14

排除應用程式故障是比較複雜的,特別是處理像 Go 這樣的高併發語言。它更容易在具體位置使用 print 列印語句來確定程式狀態,但是這個方法很難根據條件發展去動態響應你的程式碼

偵錯程式提供了一個強大得令人難以置信的故障排除機制。新增排除故障的程式碼可以巧妙地影響到應用程式該如何執行。偵錯程式可以給正在迷茫的你更精確的看法。

已經有許多 Go 的偵錯程式存在了,其中一些偵錯程式的不好之處是通過在編譯時注入程式碼來提供一個互動終端。gdb 偵錯程式則允許你除錯已經編譯好的二進位制檔案,只要他們已經與 debug 資訊連線,並不用修改原始碼。這是個相當不錯的特性,因此你可以從你的部署環境中取一個產品然後靈活地除錯它。你可以從Golang 官方文件中閱讀更多關於 gdb 的資訊,那麼這篇指南將簡單講解使用 gdb 偵錯程式來除錯 Go 應用程式的基本用法。

這兒會宣佈一些 gdb 的最新更新,最特別的是替換 -> 操作為 . 符號來訪問物件屬性。記住這兒可能在gdb 和 Go 版本中有細微改變。本篇指南基於 gdb 7.7.1和go 1.5beta2。

開始 gdb 除錯

為了實驗 gdb 我使用了一個測試程式,完整的原始碼可以在gdb_sandbox_on_Github上檢視。讓我們從一個非常簡單的程式開始吧:

package main

import ( 
    "fmt" 
)

func main() { 
    for i := 0; i < 5; i++ {
        fmt.Println("looping") 
    } 
    fmt.Println("Done") 
}

我們可以執行這段程式碼並看到它輸出內容的和我們想象的一樣:

$ go run main.go
looping
looping
looping
looping
looping
Done

我們來除錯這個程式吧。首先,使用 go build 編譯成二進位制檔案,接著使用這個二進位制檔案的路徑做為引數執行 gdb。根據你的設定,你也可以使用 source 命令來獲取 Go 執行時(Go runtime)的支援。現在我們已經在 gdb 的命令列中了,我們可以在執行我們的二進位制檔案前為它設定斷點。

$ 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.

第一關,我們在 for 迴圈裡面設定一個斷點(b)來檢視執行每次迴圈時我們的程式碼會各有什麼狀態。我們可以使用print(p)命令來檢查當前內容的一個變數,還有 list(l)和 backtrace(bt)命令檢視當前步驟周圍的程式碼。程式執行時可以使用 next(n)執行下一步或者使用 breakpoint(c)執行到下一個斷點。

(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

我們的斷點可以設定在關聯檔案的行號中、GOPATH裡的檔案的行號或一個包裡的函式。如下也是一個有效的斷點:

(gdb) b github.com/bfosberry/gdb_sandbox/main.go:9
(gdb) b 'main.main'

Structs

我們可以用稍微複雜一點的程式碼來例項演示如何除錯。我們將使用f函式生成一個簡單的pair,x和y,當x相等時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 
}

也可以在迴圈中改變程式碼來訪問這些新函式。

    p := handleNumber(i)
    fmt.Printf("%+v/n", p)
    fmt.Println("looping")

因為我們需要除錯的是變數 y。我們可以在y被設定的地方放置斷點然後單步執行。可以使用 info args 檢視函式的引數,在 bt 之前可以返回當前回溯。

(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 是在函式 f 中被設定的這樣一個條件下,我們可以跳到這個函式的上下文並檢查堆區的程式碼。應用執行時我們可以在一個更高的層次上設定斷點並檢查其狀態。

  
(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

如果我們在這個斷點處繼續住下走我們將越過在這個函式中的斷點1,而且將立即觸發在 HandleNumer 函式中的斷點,因為函式 f 只是對變數 i 每隔一次才執行。我們可以通過暫時使斷點 2不工作來避免這種情況的發生。

(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 來清除和刪除斷點。動態產生和繫住斷點,我們可以有效地在應用流中來回移動。

Slices and Pointers

上例程式太簡單了,只用到了整數型和字串,所以我們將寫一個稍微複雜一點的。首先新增一個slice(切片型別)的指標到 main 函式,並儲存生成的 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")
        }

現在我們來檢查生成出來的 slice 或 pairs,首先我們用轉換成陣列來看一下這個 slice。因為 handleNumber 返回的是一個 *pair 型別,我們需要引用這個指標來訪問 struct(結構)的屬性。

(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)

你會發現這裡 gdb 並不確定 pairs 是一個 slice 型別,我們不能直接訪問它的屬性,為了訪問它的成員我們需要使用 pairs.array 來轉換成陣列,然後我們就可以檢查 slice 的 length(長度)和 capacity(容量):

(gdb) p $len(pairs)
$12 = 3
(gdb) p $cap(pairs)
$13 = 4

這時我們可以讓它迴圈幾次,並透過這個 slice 不用的成員方法監聽增加的 xy 的值,要注意的是,這裡的 struct 屬性可以通過指標訪問,所以 p pairs.array[2].y 一樣可行。

Goroutines

現在我們已經可以訪問 struct 和 slice 了,下面再來更加複雜一點的程式吧。讓我們新增一些goroutines 到 mian 函式,並行處理每一個數字,返回的結果存入通道(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 執行完畢再檢查 pairs slice 的結果,我們可以預期到內容是完全相同的,雖然它的排序可能有些出入。gdb 真正的威力來自於它可以在 goroutines 正在執行時進行檢查:

(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

你會發現我們在 goroutine 要執行的程式碼段中放置了一個斷點,從這裡我們可以檢查到區域性變數,和程式中的其它 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

在這裡我們做的第一件事就是列出所有正在執行的 goroutine,並確定我們正在處理的那一個。然後我們可以看到一些回溯,併傳送任何除錯命令到 goroutine。這個回溯和列表清單並不太準確,如何讓回溯更準確,goroutine 上的 info args 顯示了我們的區域性變數,以及主函式中的可用變數,goroutine 函式之外的使用字首&

結論

當除錯應用時,gdb 的強大令人難以置信。但它仍然是一個相當新的事物,並不是所有的地方工作地都很完美。使用最新的穩定版 gdb,go 1.5 beta2,有不少地方有突破:

Interfaces

根據 go 部落格上的文章, go 的 interfaces 應該已經支援了,這允許在 gdb 中動態的投影其基型別。這應該算一個突破。

Interface{} 型別

目前沒有辦法轉換 interface{} 為它的型別。

列出 goroutine 的不同點

在其他 goroutine 中列出周邊程式碼會導致一些行數的漂移,最終導致 gdb 認為當前的行數超出檔案範圍並丟擲一個錯誤:

(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 除錯還不穩定

處理 goroutines 往往不穩定;我遇到過執行簡單命令產生錯誤的情況。現階段你應該做好處理類似問題的準備。

gdb 支援 Go 的配置非常麻煩

執行 gdb 支援 Go 除錯的配置非常麻煩,獲取正確的路徑結合與構建 flags,還有 gdb 自動載入功能好像都不能正常的工作。首先,通過一個 gdb 初始化檔案載入 Go 執行時支援就會產生初始化錯誤。這就需要手動通過一個源命令去載入,除錯 shell 需要像指南里面描述的那樣去進行初始化。

我什麼時候該使用一個偵錯程式?

所以什麼情況下使用 gdb 更有用?使用 print 語言和除錯程式碼是更有針對性的方法。

  • 當不適合修改程式碼的時候
  • 當除錯一個問題,但是不知道源頭,動態斷點或許更有效
  • 當包含許多 goroutines 時,暫停然後審查程式狀態會更好

“Debugging #golang with gdb” – via @codeship —— from Tweet

相關文章