Go 使用GDB調(diào)試

2022-05-13 17:04 更新

開發(fā)程序過程中調(diào)試代碼是開發(fā)者經(jīng)常要做的一件事情,Go語言不像PHP、Python等動態(tài)語言,只要修改不需要編譯就可以直接輸出,而且可以動態(tài)的在運(yùn)行環(huán)境下打印數(shù)據(jù)。當(dāng)然Go語言也可以通過Println之類的打印數(shù)據(jù)來調(diào)試,但是每次都需要重新編譯,這是一件相當(dāng)麻煩的事情。我們知道在Python中有pdb/ipdb之類的工具調(diào)試,Javascript也有類似工具,這些工具都能夠動態(tài)的顯示變量信息,單步調(diào)試等。不過慶幸的是Go也有類似的工具支持:GDB。Go內(nèi)部已經(jīng)內(nèi)置支持了GDB,所以,我們可以通過GDB來進(jìn)行調(diào)試,那么本小節(jié)就來介紹一下如何通過GDB來調(diào)試Go程序。

GDB調(diào)試簡介

GDB是FSF(自由軟件基金會)發(fā)布的一個強(qiáng)大的類UNIX系統(tǒng)下的程序調(diào)試工具。使用GDB可以做如下事情:

  1. 啟動程序,可以按照開發(fā)者的自定義要求運(yùn)行程序。
  2. 可讓被調(diào)試的程序在開發(fā)者設(shè)定的調(diào)置的斷點(diǎn)處停住。(斷點(diǎn)可以是條件表達(dá)式)
  3. 當(dāng)程序被停住時(shí),可以檢查此時(shí)程序中所發(fā)生的事。
  4. 動態(tài)的改變當(dāng)前程序的執(zhí)行環(huán)境。

目前支持調(diào)試Go程序的GDB版本必須大于7.1。

編譯Go程序的時(shí)候需要注意以下幾點(diǎn)

  1. 傳遞參數(shù)-ldflags "-s",忽略debug的打印信息
  2. 傳遞-gcflags "-N -l" 參數(shù),這樣可以忽略Go內(nèi)部做的一些優(yōu)化,聚合變量和函數(shù)等優(yōu)化,這樣對于GDB調(diào)試來說非常困難,所以在編譯的時(shí)候加入這兩個參數(shù)避免這些優(yōu)化。

常用命令

GDB的一些常用命令如下所示

  • list

    簡寫命令l,用來顯示源代碼,默認(rèn)顯示十行代碼,后面可以帶上參數(shù)顯示的具體行,例如: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,用來設(shè)置斷點(diǎn),后面跟上參數(shù)設(shè)置斷點(diǎn)的行數(shù),例如b 10在第十行設(shè)置斷點(diǎn)。

  • delete 簡寫命令 d,用來刪除斷點(diǎn),后面跟上斷點(diǎn)設(shè)置的序號,這個序號可以通過info breakpoints獲取相應(yīng)的設(shè)置的斷點(diǎn)序號,如下是顯示的設(shè)置斷點(diǎn)序號。

    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,用來打印執(zhí)行的代碼過程,如下所示:

    #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命令用來顯示信息,后面有幾種參數(shù),我們常用的有如下幾種:

    • info locals

      顯示當(dāng)前執(zhí)行的程序中的變量值

    • info breakpoints

      顯示當(dāng)前設(shè)置的斷點(diǎn)列表

    • info goroutines

      顯示當(dāng)前執(zhí)行的goroutine列表,如下代碼所示,帶*的表示當(dāng)前執(zhí)行的

      * 1  running runtime.gosched
      * 2  syscall runtime.entersyscall
        3  waiting runtime.gosched
        4 runnable runtime.gosched
  • print

    簡寫命令p,用來打印變量或者其他信息,后面跟上需要打印的變量名,當(dāng)然還有一些很有用的函數(shù)$len()和$cap(),用來返回當(dāng)前string、slices或者maps的長度和容量。

  • whatis

    用來顯示當(dāng)前變量的類型,后面跟上變量名,例如whatis msg,顯示如下:

    type = struct string
  • next

    簡寫命令 n,用來單步調(diào)試,跳到下一步,當(dāng)有斷點(diǎn)之后,可以輸入n跳轉(zhuǎn)到下一步繼續(xù)執(zhí)行

  • coutinue

    簡稱命令 c,用來跳出當(dāng)前斷點(diǎn)處,后面可以跟參數(shù)N,跳過多少次斷點(diǎn)

  • set variable

    該命令用來改變運(yùn)行過程中的變量值,格式如:set variable =

調(diào)試過程

我們通過下面這個代碼來演示如何通過GDB來調(diào)試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)
    }
}

編譯文件,生成可執(zhí)行文件gdbfile:

go build -gcflags "-N -l" gdbfile.go

通過gdb命令啟動調(diào)試:

gdb gdbfile

啟動之后首先看看這個程序是不是可以運(yùn)行起來,只要輸入run命令回車后程序就開始運(yùn)行,程序正常的話可以看到程序輸出如下,和我們在命令行直接執(zhí)行程序輸出是一樣的:

(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] 

好了,現(xiàn)在我們已經(jīng)知道怎么讓程序跑起來了,接下來開始給代碼設(shè)置斷點(diǎn):

(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行設(shè)置了斷點(diǎn),之后輸入run開始運(yùn)行程序?,F(xiàn)在程序在前面設(shè)置斷點(diǎn)的地方停住了,我們需要查看斷點(diǎn)相應(yīng)上下文的源碼,輸入list就可以看到源碼顯示從當(dāng)前停止行的前五行開始:

(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  }

現(xiàn)在GDB在運(yùn)行當(dāng)前的程序的環(huán)境中已經(jīng)保留了一些有用的調(diào)試信息,我們只需打印出相應(yīng)的變量,查看相應(yīng)變量的類型及值:

(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

接下來該讓程序繼續(xù)往下執(zhí)行,請繼續(xù)看下面的命令

(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之后都會執(zhí)行一次代碼,又跳到下一次for循環(huán),繼續(xù)打印出來相應(yīng)的信息。

設(shè)想目前需要改變上下文相關(guān)變量的信息,跳過一些過程,并繼續(xù)執(zhí)行下一步,得出修改后想要的結(jié)果:

(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)     

最后稍微思考一下,前面整個程序運(yùn)行的過程中到底創(chuàng)建了多少個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內(nèi)部是怎么執(zhí)行的,每個函數(shù)的調(diào)用順序已經(jīng)明明白白地顯示出來了。

小結(jié)

本小節(jié)我們介紹了GDB調(diào)試Go程序的一些基本命令,包括run、print、info、set variablecoutinue、list、break 等經(jīng)常用到的調(diào)試命令,通過上面的例子演示,我相信讀者已經(jīng)對于通過GDB調(diào)試Go程序有了基本的理解,如果你想獲取更多的調(diào)試技巧請參考官方網(wǎng)站的GDB調(diào)試手冊,還有GDB官方網(wǎng)站的手冊。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號