Files
build-web-application-with-…/zh-tw/11.2.md
2019-02-26 01:40:54 +08:00

9.2 KiB
Raw Blame History

11.2 使用GDB除錯

開發程式過程中除錯程式碼是開發者經常要做的一件事情Go語言不像PHP、Python等動態語言只要修改不需要編譯就可以直接輸出而且可以動態的在執行環境下列印資料。當然Go語言也可以透過Println之類別的列印資料來除錯但是每次都需要重新編譯這是一件相當麻煩的事情。我們知道在Python中有pdb/ipdb之類別的工具除錯Javascript也有類似工具這些工具都能夠動態的顯示變數資訊單步除錯等。不過慶幸的是Go也有類似的工具支援GDB。Go內部已經內建支援了GDB所以我們可以透過GDB來進行除錯那麼本小節就來介紹一下如何透過GDB來除錯Go程式。

另外建議純go程式碼使用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程式下面是將要示範的程式碼


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程式的一些基本命令包括runprintinfoset variablecoutinuelistbreak 等經常用到的除錯命令透過上面的例子示範我相信讀者已經對於透過GDB除錯Go程式有了基本的理解如果你想取得更多的除錯技巧請參考官方網站的GDB除錯手冊還有GDB官方網站的手冊。