使用 gdb 工具调试 Go

jopen 4年前

使用 gdb 工具调试 Go

排除应用程序故障是比较复杂的,特别是处理像 Go 这样的高并发语言。它更容易在具体位置使用 print 打印语句来确定程序状态,但是这个方法很难根据条件发展去动态响应你的代码

调试器提供了一个强大得令人难以置信的故障排除机制。添加排除故障的代码可以巧妙地影响到应用程序该如何运行。调试器可以给正在迷茫的你更精确的看法。

已经有许多 Go 的调试器存在了,其中一些调试器的不好之处是通过在编译时注入代码来提供一个交互终端。gdb 调试器则允许你调试已经编译好的二进制文件,只要他们已经与 debug 信息连接,并不用修改源代码。这是个相当不错的特性,因此你可以从你的部署环境中取一个产品然后灵活地调试它。你可以从Golang 官方文档中阅读更多关于 gdb 的信息,那么这篇指南将简单讲解使用 gdb 调试器来调试 Go 应用程序的基本用法。

这儿会宣布一些 gdb 的最新更新,最特别的是替换 -> 操作为 . 符号来访问对象属性。记住这儿可能在gdb 和 Go 版本中有细微改变。本篇指南基于 gdb 7.7.1和go 1.5beta2。

开始 gdb 调试

为了实验 gdb 我使用了一个测试程序,完整的源代码可以在gdb_sandbox_on_Github上查看。让我们从一个非常简单的程序开始吧:

package main    import (       "fmt"   )    func main() {       for i := 0; i < 5; i++ {          fmt.Println("looping")       }       fmt.Println("Done")   }

我们可以运行这段代码并看到它输出内容的和我们想象的一样:

$ go run main.go  looping  looping  looping  looping  looping  Done

 我们来调试这个程序吧。首先,使用 go build 编译成二进制文件,接着使用这个二进制文件的路径做为参数运行 gdb。根据你的设定,你也可以使用 source 命令来获取 Go 运行时(Go runtime)的支持。现在我们已经在 gdb 的命令行中了,我们可以在运行我们的二进制文件前为它设置断点。

$ go build -gcflags "-N -l" -o gdb_sandbox main.go   $ ls  gdb_sandbox  main.go  README.md  $ gdb gdb_sandbox  ....  (gdb) source /usr/local/src/go/src/runtime/runtime-gdb.py  Loading Go Runtime support.

第一关,我们在 for 循环里面设置一个断点(b)来查看执行每次循环时我们的代码会各有什么状态。我们可以使用print(p)命令来检查当前内容的一个变量,还有 list(l)和 backtrace(bt)命令查看当前步骤周围的代码。程序运行时可以使用 next(n)执行下一步或者使用 breakpoint(c)执行到下一个断点。

(gdb) b main.go:9   Breakpoint 1 at 0x400d35: file /home/bfosberry/workspace/gdb_sandbox/main.go, line 9.   (gdb) run   Starting program: /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/gdb_sandbox Breakpoint 1, main.main () at   /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:9   9         fmt.Println("looping")   (gdb) l   4         "fmt"   5         )   6    7 func main() {  8         for i := 0; i < 5; i++ {   9         fmt.Println("looping")   10        }`   11        fmt.Println("Done")   12 }   (gdb) p i   $1 = 0   (gdb) n   looping   Breakpoint 1, main.main () at   /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:9   9        fmt.Println("looping")   (gdb) p i   $2 = 1   (gdb) bt  # 0 main.main () at /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:9

我们的断点可以设置在关联文件的行号中、GOPATH里的文件的行号或一个包里的函数。如下也是一个有效的断点:

(gdb) b github.com/bfosberry/gdb_sandbox/main.go:9  (gdb) b 'main.main'

Structs

我们可以用稍微复杂一点的代码来实例演示如何调试。我们将使用f函数生成一个简单的pair,x和y,当x相等时y=f(x),否则=x。

type pair struct {       x int       y int   }    func handleNumber(i int) *pair {       val := i       if i%2 == 0 {           val = f(i)       }       return &pair{          x: i,          y: val,       }   }    func f(int x) int {       return x*x + x   }

也可以在循环中改变代码来访问这些新函数。

    p := handleNumber(i)      fmt.Printf("%+v\n", p)      fmt.Println("looping")

因为我们需要调试的是变量 y。我们可以在y被设置的地方放置断点然后单步执行。可以使用 info args 查看函数的参数,在 bt 之前可以返回当前回溯。

(gdb) b 'main.f'   (gdb) run   Starting program: /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/gdb_sandbox    Breakpoint 1, main.f (x=0, ~anon1=833492132160)       at /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:33   33       return x*x + x   (gdb) info args   x = 0   (gdb) continue   Breakpoint 1, main.f (x=0, ~anon1=833492132160)       at /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:33   33       return x*x + x   (gdb) info args   x = 2   (gdb) bt  #0 main.f (x=2, ~anon1=1)       at /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:33  #1 0x0000000000400f0e in main.handleNumber (i=2, ~anon1=0x1)      at /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:24  #2 0x0000000000400c47 in main.main ()      at /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:14

因为我们在变量 y 是在函数 f 中被设定的这样一个条件下,我们可以跳到这个函数的上下文并检查堆区的代码。应用运行时我们可以在一个更高的层次上设置断点并检查其状态。

    (gdb) b main.go:26   Breakpoint 2 at 0x400f22: file   /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go, line 26.   (gdb) continue   Continuing.  Breakpoint 2, main.handleNumber (i=2, ~anon1=0x1)       at /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:28   28             y: val,   (gdb) l   23         if i%2 == 0 {   24             val = f(i)   25         }   26         return &pair{   27             x: i,   28             y: val,   29         }   30     }   31    32 func f(x int) int {   (gdb) p val   $1 = 6   (gdb) p i   $2 = 2

如果我们在这个断点处继续住下走我们将越过在这个函数中的断点1,而且将立即触发在 HandleNumer 函数中的断点,因为函数 f 只是对变量 i 每隔一次才执行。我们可以通过暂时使断点 2不工作来避免这种情况的发生。

(gdb) disable breakpoint 2   (gdb) continue   Continuing.   &{x:2 y:6}   looping   &{x:3 y:3}   looping   [New LWP 15200]   [Switching to LWP 15200]  Breakpoint 1, main.f (x=4, ~anon1=1)       at /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:33   33         return x*x + x   (gdb)

我们也可以分别使用 clear 和 delete breakpoint NUMBER 来清除和删除断点。动态产生和系住断点,我们可以有效地在应用流中来回移动。

Slices and Pointers

上例程序太简单了,只用到了整数型和字符串,所以我们将写一个稍微复杂一点的。首先添加一个slice(切片类型)的指针到 main 函数,并保存生成的 pair,我们后面将用到它。

    var pairs []*pair      for i := 0; i < 10; i++ {          p := handleNumber(i)          fmt.Printf("%+v\n", p)          pairs = append(pairs, p)          fmt.Println("looping")          }

现在我们来检查生成出来的 slice 或 pairs,首先我们用转换成数组来看一下这个 slice。因为 handleNumber 返回的是一个 *pair 类型,我们需要引用这个指针来访问 struct(结构)的属性。

(gdb) b main.go:18   Breakpoint 1 at 0x400e14: file /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go, line 18.   (gdb) run   Starting program: /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/gdb_sandbox &{x:0 y:0}    Breakpoint 1, main.main () at /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:18   18         fmt.Println("looping")   (gdb) p pairs   $1 = []*main.pair = {0xc82000a3a0}   (gdb) p pairs[0]   Structure has no component named operator[].   (gdb) p pairs.array   $2 = (struct main.pair **) 0xc820030028   (gdb) p pairs.array[0]   $3 = (struct main.pair *) 0xc82000a3a0   (gdb) p *pairs.array[0]   $4 = {x = 0, y = 0}   (gdb) p (*pairs.array[0]).x   $5 = 0   (gdb) p (*pairs.array[0]).y   $6 = 0   (gdb) continue   Continuing.   looping   &{x:1 y:1}    Breakpoint 1, main.main () at /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:18   18         fmt.Println("looping")   (gdb) p (pairs.array[1][5]).y   $7 = 1   (gdb) continue   Continuing.   looping   &{x:2 y:6}    Breakpoint 1, main.main () at /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:18   18         fmt.Println("looping")   (gdb) p (pairs.array[2][6]).y   $8 = 6   (gdb)

你会发现这里 gdb 并不确定 pairs 是一个 slice 类型,我们不能直接访问它的属性,为了访问它的成员我们需要使用 pairs.array 来转换成数组,然后我们就可以检查 slice 的 length(长度)和 capacity(容量):

(gdb) p $len(pairs)  $12 = 3  (gdb) p $cap(pairs)  $13 = 4

这时我们可以让它循环几次,并透过这个 slice 不用的成员方法监听增加的 xy 的值,要注意的是,这里的 struct 属性可以通过指针访问,所以 p pairs.array[2].y 一样可行。

Goroutines

现在我们已经可以访问 struct 和 slice 了,下面再来更加复杂一点的程序吧。让我们添加一些goroutines 到 mian 函数,并行处理每一个数字,返回的结果存入信道(chan)中:

    pairs := []*pair{}      pairChan := make(chan *pair)      wg := sync.WaitGroup{}          for i := 0; i < 10; i++ {            wg.Add(1)            go func(val int) {              p := handleNumber(val)              fmt.Printf("%+v\n", p)              pairChan <- p              wg.Done()              }(i)      }      go func() {              for p := range pairChan {                pairs = append(pairs, p)              }      }()      wg.Wait()      close(pairChan)

如果我等待 WaitGroup 执行完毕再检查 pairs slice 的结果,我们可以预期到内容是完全相同的,虽然它的排序可能有些出入。gdb 真正的威力来自于它可以在 goroutines 正在运行时进行检查:

(gdb) b main.go:43   Breakpoint 1 at 0x400f7f: file /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go, line 43.   (gdb) run   Starting program: /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/gdb_sandbox    Breakpoint 1, main.handleNumber (i=0, ~r1=0x0)       at /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:43   43         y: val,   (gdb) l   38     if i%2 == 0 {   39         val = f(i)   40     }   41     return &pair{   42         x: i,   43         y: val,   44     }   45 }   46    47 func f(x int) int {   (gdb) info args   i = 0   ~r1 = 0x0   (gdb) p val   $1 = 0

你会发现我们在 goroutine 要执行的代码段中放置了一个断点,从这里我们可以检查到局部变量,和进程中的其它 goroutines:

(gdb) info goroutines     1 waiting runtime.gopark     2 waiting runtime.gopark     3 waiting runtime.gopark     4 waiting runtime.gopark   * 5 running main.main.func1     6 runnable main.main.func1     7 runnable main.main.func1     8 runnable main.main.func1     9 runnable main.main.func1   * 10 running main.main.func1     11 runnable main.main.func1     12 runnable main.main.func1     13 runnable main.main.func1     14 runnable main.main.func1     15 waiting runtime.gopark   (gdb) goroutine 11 bt  #0 main.main.func1 (val=6, pairChan=0xc82001a180, &wg=0xc82000a3a0)      at /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:19  #1 0x0000000000454991 in runtime.goexit () at /usr/local/go/src/runtime/asm_amd64.s:1696  #2 0x0000000000000006 in ?? ()  #3 0x000000c82001a180 in ?? ()  #4 0x000000c82000a3a0 in ?? ()  #5 0x0000000000000000 in ?? ()  (gdb) goroutine 11 l   48         return x*x + x   49     }   (gdb) goroutine 11 info args   val = 6   pairChan = 0xc82001a180   &wg = 0xc82000a3a0   (gdb) goroutine 11 p val   $2 = 6

在这里我们做的第一件事就是列出所有正在运行的 goroutine,并确定我们正在处理的那一个。然后我们可以看到一些回溯,并发送任何调试命令到 goroutine。这个回溯和列表清单并不太准确,如何让回溯更准确,goroutine 上的 info args 显示了我们的局部变量,以及主函数中的可用变量,goroutine 函数之外的使用前缀&

结论

当调试应用时,gdb 的强大令人难以置信。但它仍然是一个相当新的事物,并不是所有的地方工作地都很完美。使用最新的稳定版 gdb,go 1.5 beta2,有不少地方有突破:

Interfaces

根据 go 博客上的文章, go 的 interfaces 应该已经支持了,这允许在 gdb 中动态的投影其基类型。这应该算一个突破。

Interface{} 类型

目前没有办法转换 interface{} 为它的类型。

列出 goroutine 的不同点

在其他 goroutine 中列出周边代码会导致一些行数的漂移,最终导致 gdb 认为当前的行数超出文件范围并抛出一个错误:

(gdb) info goroutines     1 waiting runtime.gopark     2 waiting runtime.gopark     3 waiting runtime.gopark     4 waiting runtime.gopark   * 5 running main.main.func1     6 runnable main.main.func1     7 runnable main.main.func1     8 runnable main.main.func1     9 runnable main.main.func1   * 10 running main.main.func1     11 runnable main.main.func1     12 runnable main.main.func1     13 runnable main.main.func1     14 runnable main.main.func1     15 waiting runtime.gopark   (gdb) goroutine 11 bt  #0 main.main.func1 (val=6, pairChan=0xc82001a180, &wg=0xc82000a3a0)      at /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go:19  #1 0x0000000000454991 in runtime.goexit () at /usr/local/go/src/runtime/asm_amd64.s:1696  #2 0x0000000000000006 in ?? ()  #3 0x000000c82001a180 in ?? ()  #4 0x000000c82000a3a0 in ?? ()  #5 0x0000000000000000 in ?? ()  (gdb) goroutine 11 l   48         return x*x + x   49     }   (gdb) goroutine 11 l   Python Exception <class 'gdb.error'> Line number 50 out of range; /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go has 49 lines.:   Error occurred in Python command: Line number 50 out of range; /home/bfosberry/.go/src/github.com/bfosberry/gdb_sandbox/main.go has 49 lines.

Goroutine 调试还不稳定

处理 goroutines 往往不稳定;我遇到过执行简单命令产生错误的情况。现阶段你应该做好处理类似问题的准备。

gdb 支持 Go 的配置非常麻烦

运行 gdb 支持 Go 调试的配置非常麻烦,获取正确的路径结合与构建 flags,还有 gdb 自动加载功能好像都不能正常的工作。首先,通过一个 gdb 初始化文件加载 Go 运行时支持就会产生初始化错误。这就需要手动通过一个源命令去加载,调试 shell 需要像指南里面描述的那样去进行初始化。

我什么时候该使用一个调试器?

所以什么情况下使用 gdb 更有用?使用 print 语言和调试代码是更有针对性的方法。

  • 当不适合修改代码的时候

  • 当调试一个问题,但是不知道源头,动态断点或许更有效

  • 当包含许多 goroutines 时,暂停然后审查程序状态会更好

“Debugging #golang with gdb” – via @codeship —— from Tweet