排除應用程序故障是比較復雜的,特別是處理像 Go 這樣的高並發語言。它更容易在具體位置使用 print 打印語句來確定程序狀態,但是這個方法很難根據條件發展去動態響應你的代碼。
調試器提供了一個強大得令人難以置信的故障排除機制。添加排除故障的代碼可以巧妙地影響到應用程序該如何運行。調試器可以給正在迷茫的你更精確的看法。
已經有許多 Go 的調試器存在了,其中一些調試器的不好之處是通過在編譯時注入代碼來提供一個交互終端。gdb 調試器則允許你調試已經編譯好的二進制文件,只要他們已經與 debug 信息連接,並不用修改源代碼。這是個相當不錯的特性,因此你可以從你的部署環境中取一個產品然後靈活地調試它。你可以從Golang 官方文檔中閱讀更多關於 gdb 的信息,那麼這篇指南將簡單講解使用 gdb 調試器來調試 Go 應用程序的基本用法。
這兒會宣布一些 gdb 的最新更新,最特別的是替換 -> 操作為 . 符號來訪問對象屬性。記住這兒可能在gdb 和 Go 版本中有細微改變。本篇指南基於 gdb 7.7.1和go 1.5beta2。
為了實驗 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 不用的成員方法監聽增加的 x 和 y 的值,要注意的是,這裡的 struct 屬性可以通過指針訪問,所以 p pairs.array[2].y 一樣可行。
現在我們已經可以訪問 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,有不少地方有突破:
根據 go 博客上的文章, go 的 interfaces 應該已經支持了,這允許在 gdb 中動態的投影其基類型。這應該算一個突破。
目前沒有辦法轉換 interface{} 為它的類型。
在其他 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
GDB調試程序用法 http://www.linuxidc.com/Linux/2013-06/86044.htm
GDB+GDBserver無源碼調試Android 動態鏈接庫的技巧 http://www.linuxidc.com/Linux/2013-06/85936.htm
使用hello-gl2建立ndk-GDB環境(有源碼和無源碼調試環境) http://www.linuxidc.com/Linux/2013-06/85935.htm
在Ubuntu上用GDB調試printf源碼 http://www.linuxidc.com/Linux/2013-03/80346.htm
Linux下用GDB調試可加載模塊 http://www.linuxidc.com/Linux/2013-01/77969.htm
Ubuntu下使用GDB斷點Go程序 http://www.linuxidc.com/Linux/2012-06/62941.htm
使用GDB命令行調試器調試C/C++程序 http://www.linuxidc.com/Linux/2014-11/109845.htm
GDB 的詳細介紹:請點這裡
GDB 的下載地址:請點這裡
英文原文:Using the gdb debugger with Go