天天看點

使用GDB調試使用GDB調試

我在找c++的GDB的調試,結果發現了這篇關于GO語言的GDB調試文章,發現GDB的操作是一樣的

https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/11.2.md

使用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

    跳轉到下一步繼續執行
  • coutinue

    簡稱指令

    c

    ,用來跳出目前斷點處,後面可以跟參數N,跳過多少次斷點
  • set variable

    該指令用來改變運作過程中的變量值,格式如:

    set variable <var>=<value>

調試過程

我們通過下面這個代碼來示範如何通過GDB來調試Go程式,下面是将要示範的代碼:

package main

    import (
        "fmt"
        "time"
    )

    func counting(c chan<- int) {
        for i :=; i <; i++ {
            time.Sleep * 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

指令回車後程式就開始運作,程式正常的話可以看到程式輸出如下,和我們在指令行直接執行程式輸出是一樣的:

(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

  • 目錄
  • 上一節: 錯誤處理
  • 下一節: Go怎麼寫測試用例