使用 gdb 工具調試 Go

jopen 9年前發布 | 25K 次閱讀 Google Go/Golang開發 GDB

使用 gdb 工具調試 Go

排除應用程序故障是比較復雜的,特別是處理像 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

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!