Go 使用GDB調(diào)試

2022-05-13 17:04 更新

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

GDB調(diào)試簡(jiǎn)介

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

  1. 啟動(dòng)程序,可以按照開(kāi)發(fā)者的自定義要求運(yùn)行程序。
  2. 可讓被調(diào)試的程序在開(kāi)發(fā)者設(shè)定的調(diào)置的斷點(diǎn)處停住。(斷點(diǎn)可以是條件表達(dá)式)
  3. 當(dāng)程序被停住時(shí),可以檢查此時(shí)程序中所發(fā)生的事。
  4. 動(dòng)態(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)化,這樣對(duì)于GDB調(diào)試來(lái)說(shuō)非常困難,所以在編譯的時(shí)候加入這兩個(gè)參數(shù)避免這些優(yōu)化。

常用命令

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

  • list

    簡(jiǎn)寫(xiě)命令l,用來(lái)顯示源代碼,默認(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

    簡(jiǎn)寫(xiě)命令 b,用來(lái)設(shè)置斷點(diǎn),后面跟上參數(shù)設(shè)置斷點(diǎn)的行數(shù),例如b 10在第十行設(shè)置斷點(diǎn)。

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

    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

    簡(jiǎn)寫(xiě)命令 bt,用來(lái)打印執(zhí)行的代碼過(guò)程,如下所示:

    #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命令用來(lái)顯示信息,后面有幾種參數(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

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

  • whatis

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

    type = struct string
  • next

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

  • coutinue

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

  • set variable

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

調(diào)試過(guò)程

我們通過(guò)下面這個(gè)代碼來(lái)演示如何通過(guò)GDB來(lái)調(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

通過(guò)gdb命令啟動(dòng)調(diào)試:

gdb gdbfile

啟動(dòng)之后首先看看這個(gè)程序是不是可以運(yùn)行起來(lái),只要輸入run命令回車后程序就開(kāi)始運(yùn)行,程序正常的話可以看到程序輸出如下,和我們?cè)诿钚兄苯訄?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)知道怎么讓程序跑起來(lái)了,接下來(lái)開(kāi)始給代碼設(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開(kāi)始運(yùn)行程序?,F(xiàn)在程序在前面設(shè)置斷點(diǎn)的地方停住了,我們需要查看斷點(diǎn)相應(yīng)上下文的源碼,輸入list就可以看到源碼顯示從當(dāng)前停止行的前五行開(kāi)始:

(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

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

設(shè)想目前需要改變上下文相關(guān)變量的信息,跳過(guò)一些過(guò)程,并繼續(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)     

最后稍微思考一下,前面整個(gè)程序運(yùn)行的過(guò)程中到底創(chuàng)建了多少個(gè)goroutine,每個(gè)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 ?? ()

通過(guò)查看goroutines的命令我們可以清楚地了解goruntine內(nèi)部是怎么執(zhí)行的,每個(gè)函數(shù)的調(diào)用順序已經(jīng)明明白白地顯示出來(lái)了。

小結(jié)

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

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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)