254 lines
9.3 KiB
Markdown
254 lines
9.3 KiB
Markdown
# 11.2 使用 GDB 除錯
|
||
開發程式過程中除錯程式碼是開發者經常要做的一件事情,Go 語言不像 PHP、Python 等動態語言,只要修改不需要編譯就可以直接輸出,而且可以動態的在執行環境下列印資料。當然 Go 語言也可以透過 Println 之類別的列印資料來除錯,但是每次都需要重新編譯,這是一件相當麻煩的事情。我們知道在 Python 中有 pdb/ipdb 之類別的工具除錯,Javascript 也有類似工具,這些工具都能夠動態的顯示變數資訊,單步除錯等。不過慶幸的是 Go 也有類似的工具支援:GDB。Go 內部已經內建支援了 GDB,所以,我們可以透過 GDB 來進行除錯,那麼本小節就來介紹一下如何透過 GDB 來除錯 Go 程式。
|
||
|
||
另外建議純 go 程式碼使用[delve](https://github.com/derekparker/delve)可以很好的進行 Go 程式碼除錯
|
||
|
||
## GDB 除錯簡介
|
||
GDB 是 FSF(自由軟體基金會)釋出的一個強大的類別 UNIX 系統下的程式除錯工具。使用 GDB 可以做如下事情:
|
||
|
||
1. 啟動程式,可以按照開發者的自訂要求執行程式。
|
||
2. 可讓被除錯的程式在開發者設定的調置的斷點處停住。(斷點可以是條件表示式)
|
||
3. 當程式被停住時,可以檢查此時程式中所發生的事。
|
||
4. 動態的改變當前程式的執行環境。
|
||
|
||
目前支援除錯 Go 程式的 GDB 版本必須大於 7.1。
|
||
|
||
編譯 Go 程式的時候需要注意以下幾點
|
||
|
||
1. 傳遞參數-ldflags "-s",忽略 debug 的列印資訊
|
||
2. 傳遞-gcflags "-N -l" 參數,這樣可以忽略 Go 內部做的一些優化,聚合變數和函式等優化,這樣對於 GDB 除錯來說非常困難,所以在編譯的時候加入這兩個參數避免這些優化。
|
||
|
||
## 常用命令
|
||
GDB 的一些常用命令如下所示
|
||
|
||
- list
|
||
|
||
簡寫命令`l`,用來顯示原始碼,預設顯示十行程式碼,後面可以帶上參數顯示的具體行,例如:`list 15`,顯示十行程式碼,其中第 15 行在顯示的十行裡面的中間,如下所示。
|
||
|
||
10 time.Sleep(2 * time.Second)
|
||
11 c <- i
|
||
12 }
|
||
13 close(c)
|
||
14 }
|
||
15
|
||
16 func main() {
|
||
17 msg := "Starting main"
|
||
18 fmt.Println(msg)
|
||
19 bus := make(chan int)
|
||
|
||
|
||
- break
|
||
|
||
簡寫命令 `b`,用來設定斷點,後面跟上參數設定斷點的行數,例如`b 10`在第十行設定斷點。
|
||
|
||
- delete
|
||
簡寫命令 `d`,用來刪除斷點,後面跟上斷點設定的序號,這個序號可以透過`info breakpoints`取得相應的設定的斷點序號,如下是顯示的設定斷點序號。
|
||
|
||
Num Type Disp Enb Address What
|
||
2 breakpoint keep y 0x0000000000400dc3 in main.main at /home/xiemengjun/gdb.go:23
|
||
breakpoint already hit 1 time
|
||
|
||
- backtrace
|
||
|
||
簡寫命令 `bt`,用來列印執行的程式碼過程,如下所示:
|
||
|
||
#0 main.main () at /home/xiemengjun/gdb.go:23
|
||
#1 0x000000000040d61e in runtime.main () at /home/xiemengjun/go/src/pkg/runtime/proc.c:244
|
||
#2 0x000000000040d6c1 in schedunlock () at /home/xiemengjun/go/src/pkg/runtime/proc.c:267
|
||
#3 0x0000000000000000 in ?? ()
|
||
- info
|
||
|
||
info 命令用來顯示資訊,後面有幾種參數,我們常用的有如下幾種:
|
||
|
||
- `info locals`
|
||
|
||
顯示當前執行的程式中的變數值
|
||
- `info breakpoints`
|
||
|
||
顯示當前設定的斷點列表
|
||
- `info goroutines`
|
||
|
||
顯示當前執行的 goroutine 列表,如下程式碼所示,帶*的表示當前執行的
|
||
|
||
* 1 running runtime.gosched
|
||
* 2 syscall runtime.entersyscall
|
||
3 waiting runtime.gosched
|
||
4 runnable runtime.gosched
|
||
- print
|
||
|
||
簡寫命令`p`,用來列印變數或者其他資訊,後面跟上需要列印的變數名,當然還有一些很有用的函式$len()和$cap(),用來回傳當前 string、slices 或者 maps 的長度和容量。
|
||
|
||
- whatis
|
||
|
||
用來顯示當前變數的型別,後面跟上變數名,例如`whatis msg`,顯示如下:
|
||
|
||
type = struct string
|
||
- next
|
||
|
||
簡寫命令 `n`,用來單步除錯,跳到下一步,當有斷點之後,可以輸入 `n` 跳轉到下一步繼續執行
|
||
- continue
|
||
|
||
簡稱命令 `c`,用來跳出當前斷點處,後面可以跟參數 N,跳過多少次斷點
|
||
|
||
- set variable
|
||
|
||
該命令用來改變執行過程中的變數值,格式如:`set variable <var>=<value>`
|
||
|
||
## 除錯過程
|
||
我們透過下面這個程式碼來示範如何透過 GDB 來除錯 Go 程式,下面是將要示範的程式碼:
|
||
|
||
```Go
|
||
|
||
package main
|
||
|
||
import (
|
||
"fmt"
|
||
"time"
|
||
)
|
||
|
||
func counting(c chan<- int) {
|
||
for i := 0; i < 10; i++ {
|
||
time.Sleep(2 * time.Second)
|
||
c <- i
|
||
}
|
||
close(c)
|
||
}
|
||
|
||
func main() {
|
||
msg := "Starting main"
|
||
fmt.Println(msg)
|
||
bus := make(chan int)
|
||
msg = "starting a gofunc"
|
||
go counting(bus)
|
||
for count := range bus {
|
||
fmt.Println("count:", count)
|
||
}
|
||
}
|
||
```
|
||
編譯檔案,產生可執行檔案 gdbfile:
|
||
|
||
go build -gcflags "-N -l" gdbfile.go
|
||
|
||
透過 gdb 命令啟動除錯:
|
||
|
||
gdb gdbfile
|
||
|
||
啟動之後首先看看這個程式是不是可以執行起來,只要輸入 `run` 命令 Enter 後程序就開始執行,程式正常的話可以看到程式輸出如下,和我們在命令列直接執行程式輸出是一樣的:
|
||
|
||
(gdb) run
|
||
Starting program: /home/xiemengjun/gdbfile
|
||
Starting main
|
||
count: 0
|
||
count: 1
|
||
count: 2
|
||
count: 3
|
||
count: 4
|
||
count: 5
|
||
count: 6
|
||
count: 7
|
||
count: 8
|
||
count: 9
|
||
[LWP 2771 exited]
|
||
[Inferior 1 (process 2771) exited normally]
|
||
好了,現在我們已經知道怎麼讓程式跑起來了,接下來開始給程式碼設定斷點:
|
||
|
||
(gdb) b 23
|
||
Breakpoint 1 at 0x400d8d: file /home/xiemengjun/gdbfile.go, line 23.
|
||
(gdb) run
|
||
Starting program: /home/xiemengjun/gdbfile
|
||
Starting main
|
||
[New LWP 3284]
|
||
[Switching to LWP 3284]
|
||
|
||
Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23
|
||
23 fmt.Println("count:", count)
|
||
|
||
上面例子`b 23`表示在第 23 行設定了斷點,之後輸入 `run` 開始執行程式。現在程式在前面設定斷點的地方停住了,我們需要檢視斷點相應上下文的原始碼,輸入 `list` 就可以看到原始碼顯示從當前停止行的前五行開始:
|
||
|
||
(gdb) list
|
||
18 fmt.Println(msg)
|
||
19 bus := make(chan int)
|
||
20 msg = "starting a gofunc"
|
||
21 go counting(bus)
|
||
22 for count := range bus {
|
||
23 fmt.Println("count:", count)
|
||
24 }
|
||
25 }
|
||
|
||
現在 GDB 在運行當前的程式的環境中已經保留了一些有用的除錯資訊,我們只需顯示出相應的變數,檢視相應變數的型別及值:
|
||
|
||
(gdb) info locals
|
||
count = 0
|
||
bus = 0xf840001a50
|
||
(gdb) p count
|
||
$1 = 0
|
||
(gdb) p bus
|
||
$2 = (chan int) 0xf840001a50
|
||
(gdb) whatis bus
|
||
type = chan int
|
||
|
||
接下來該讓程式繼續往下執行,請繼續看下面的命令
|
||
|
||
(gdb) c
|
||
Continuing.
|
||
count: 0
|
||
[New LWP 3303]
|
||
[Switching to LWP 3303]
|
||
|
||
Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23
|
||
23 fmt.Println("count:", count)
|
||
(gdb) c
|
||
Continuing.
|
||
count: 1
|
||
[Switching to LWP 3302]
|
||
|
||
Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23
|
||
23 fmt.Println("count:", count)
|
||
|
||
每次輸入 `c` 之後都會執行一次程式碼,又跳到下一次 for 迴圈,繼續顯示出來相應的資訊。
|
||
|
||
設想目前需要改變上下文相關變數的資訊,跳過一些過程,並繼續執行下一步,得出修改後想要的結果:
|
||
|
||
(gdb) info locals
|
||
count = 2
|
||
bus = 0xf840001a50
|
||
(gdb) set variable count=9
|
||
(gdb) info locals
|
||
count = 9
|
||
bus = 0xf840001a50
|
||
(gdb) c
|
||
Continuing.
|
||
count: 9
|
||
[Switching to LWP 3302]
|
||
|
||
Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23
|
||
23 fmt.Println("count:", count)
|
||
|
||
最後稍微思考一下,前面整個程式執行的過程中到底建立了多少個 goroutine,每個 goroutine 都在做什麼:
|
||
|
||
(gdb) info goroutines
|
||
* 1 running runtime.gosched
|
||
* 2 syscall runtime.entersyscall
|
||
3 waiting runtime.gosched
|
||
4 runnable runtime.gosched
|
||
(gdb) goroutine 1 bt
|
||
#0 0x000000000040e33b in runtime.gosched () at /home/xiemengjun/go/src/pkg/runtime/proc.c:927
|
||
#1 0x0000000000403091 in runtime.chanrecv (c=void, ep=void, selected=void, received=void)
|
||
at /home/xiemengjun/go/src/pkg/runtime/chan.c:327
|
||
#2 0x000000000040316f in runtime.chanrecv2 (t=void, c=void)
|
||
at /home/xiemengjun/go/src/pkg/runtime/chan.c:420
|
||
#3 0x0000000000400d6f in main.main () at /home/xiemengjun/gdbfile.go:22
|
||
#4 0x000000000040d0c7 in runtime.main () at /home/xiemengjun/go/src/pkg/runtime/proc.c:244
|
||
#5 0x000000000040d16a in schedunlock () at /home/xiemengjun/go/src/pkg/runtime/proc.c:267
|
||
#6 0x0000000000000000 in ?? ()
|
||
|
||
透過檢視 goroutines 的命令我們可以清楚地了解 goruntine 內部是怎麼執行的,每個函式的呼叫順序已經明明白白地顯示出來了。
|
||
|
||
## 小結
|
||
本小節我們介紹了 GDB 除錯 Go 程式的一些基本命令,包括`run`、`print`、`info`、`set variable`、`coutinue`、`list`、`break` 等經常用到的除錯命令,透過上面的例子示範,我相信讀者已經對於透過 GDB 除錯 Go 程式有了基本的理解,如果你想取得更多的除錯技巧請參考官方網站的 GDB 除錯手冊,還有 GDB 官方網站的手冊。
|
||
|
||
## links
|
||
* [目錄](<preface.md>)
|
||
* 上一節:[錯誤處理](<11.1.md>)
|
||
* 下一節:[Go 怎麼寫測試案例](<11.3.md>)
|