使用Delve调试Golang 代码

追踪代码中的错误可能是一件非常头疼的事情。这在高度依赖goroutine的Golang代码调试中更加的突出。有一个趁手的 debug 工具就显得非常的重要。我们先来看看 Go 官方的debug tool文档写的啥。

GDB does not understand Go programs well. The stack management, threading, and runtime contain aspects that differ enough from the execution model GDB expects that they can confuse the debugger, even when the program is compiled with gccgo.

In short, the instructions below should be taken only as a guide to how to use GDB when it works, not as a guarantee of success.

In time, a more Go-centric debugging architecture may be required.

总结一下上面说的话。

  1. Go 的debug工具有GDB这个玩意,但是目前貌似工作的不咋滴
  2. 目前官方只是给你介绍介绍这个玩意怎么用,但是不保证能成功
  3. 实话说,我们需要一个更懂 Go 的调试器

最后一句话透露了本质,目前还没有一个非常好的调试工具。难道我们只能在不断打日志然后 build 然后再打日志中调试程序吗? 当然不是,下面我来介绍一个专门为 Go而生的 debug 工具 Delve

Delve目的就是为了解决开发者在使用 GDB 调试中遇到的各种各样的问题。我们开始详细的介绍一些使用Delve 调试代码的例子。

安装

首先默认你已经安装了 Go 环境,安装命令很简单,一句话。

go get github.com/derekparker/delve/cmd/dlv

注意:如果你使用Go1.5,你必须在运行这个命令前设置GO15VENDOREXPERIMENT=1

调试代码

首先得说明一下,实诚点说,当你想用debug 工具的时候,你的代码估计已经不按照你想象的方式运行了。只是你不知道为什么会这样,因此你可能需要换一种方式启动你的程序,下面我们来演示一下如果使用dlv来启动你的程序。我们的示例代码如下:

package main

import (
    "fmt"
    "sync"
    "time"
)

func dostuff(wg *sync.WaitGroup, i int) {
    fmt.Printf("goroutine id %d\n", i)
    time.Sleep(300 * time.Second)
    fmt.Printf("goroutine id %d\n", i)
    wg.Done()
}

func main() {
    var wg sync.WaitGroup
    workers := 10

    wg.Add(workers)
    for i := 0; i < workers; i++ {
        go dostuff(&wg, i)
    }
    wg.Wait()
}

在这个示例代码中,我们创建了10个goroutine,这种代码对于 GDB 来说是几乎不可读的,因为它对于goroutine的支持很差。但是Delve作为一个专业的 Go 调试器,对于goroutine这种杀手级功能还是非常了解的。下面我们来启动程序。

dlv debug test-debug.go

运行这个命令,dlv会去编译你的代码,然后传一些参数给编译器,好让编译器编译出来更加方便调试的可执行文件,然后启动了你的程序,并且attach上去,这样你的console就会停留在了debug session,下面就可以调试程序了。

首先我们在main函数上设置一个断点。

(dlv) break main.main
Breakpoint 1 set at 0x22d3 for main.main() ./test-debug.go:16

输出信息里面告诉了我们断点的 ID和断点的位置,函数名和文件名以及所在行数。我们使用continue命令让程序运行到我们打断点的地方。

(dlv) continue
> main.main() ./test-debug.go:16 (hits goroutine(1):1 total:1) (PC: 0x22d3)
    11:        time.Sleep(300 * time.Second)
    12:        fmt.Printf("goroutine id %d\n", i)
    13:        wg.Done()
    14:    }
    15:
=>  16:    func main() {
    17:        var wg sync.WaitGroup
    18:        workers := 10
    19:
    20:        wg.Add(workers)
    21:        for i := 0; i < workers; i++ {
(dlv)

现在程序就停在了第一个断点,现在我们可以使用next命令让程序运行到下一句话,如果你想继续向下,可以直接按回车(Delve会重复上一条命令如果你按下回车键)。

(dlv) next
> main.main() ./test-debug.go:17 (PC: 0x22d7)
    12:        fmt.Printf("goroutine id %d\n", i)
    13:        wg.Done()
    14:    }
    15:
    16:    func main() {
=>  17:        var wg sync.WaitGroup
    18:        workers := 10
    19:
    20:        wg.Add(workers)
    21:        for i := 0; i < workers; i++ {
    22:            go dostuff(&wg, i)
(dlv)
> main.main() ./test-debug.go:18 (PC: 0x22f1)
    13:        wg.Done()
    14:    }
    15:
    16:    func main() {
    17:        var wg sync.WaitGroup
=>  18:        workers := 10
    19:
    20:        wg.Add(workers)
    21:        for i := 0; i < workers; i++ {
    22:            go dostuff(&wg, i)
    23:        }
(dlv)
> main.main() ./test-debug.go:20 (PC: 0x22fa)
    15:
    16:    func main() {
    17:        var wg sync.WaitGroup
    18:        workers := 10
    19:
=>  20:        wg.Add(workers)
    21:        for i := 0; i < workers; i++ {
    22:            go dostuff(&wg, i)
    23:        }
    24:        wg.Wait()
    25:    }
(dlv)    

现在我们可以尝试使用print命令去看一下变量的值。

(dlv) print wg
sync.WaitGroup {
    state1: [12]uint8 [0,0,0,0,0,0,0,0,0,0,0,0],
    sema: 0,}
(dlv) print workers
10
(dlv)

同时你也可以输出一个表达式

(dlv) print workers < 100
true

下面我们在另外一个函数dostuff 上设置一个断点

(dlv) break dostuff
Breakpoint 2 set at 0x2058 for main.dostuff() ./test-debug.go:9

我们使用continue到我们设置断点的地方,然后next

(dlv) next
goroutine id 3
> main.dostuff() ./test-debug.go:10 (PC: 0x205f)
     5:        "sync"
     6:        "time"
     7:    )
     8:
     9:    func dostuff(wg *sync.WaitGroup, i int) {
=>  10:        fmt.Printf("goroutine id %d\n", i)
    11:        time.Sleep(300 * time.Second)
    12:        fmt.Printf("goroutine id %d\n", i)
    13:        wg.Done()
    14:    }
    15:
(dlv)

可以看到Delve会告诉你目前的goroutine id,我们试试输出一下i和wg.

(dlv) print i
4
(dlv) print wg
*sync.WaitGroup {
    state1: [12]uint8 [1,0,0,0,10,0,0,0,0,0,0,0],
    sema: 0,}
    

我们创建了10个goroutine,如果你继续使用next,你会发现你还是在同一个goroutine下。这样就避免了被调试器跳转到了另外的goroutine下导致不必要的调试错误。可见还是为 Go 而生的调试器才是真爱啊。

进阶调试

其实很多时候,我们调试的代码可能是daemon程序或者需要实现编译好在不同机器运行的程序。这就需要我们attach到一个已经在运行中的程序上,下面我们就使用上面的代码来演示一下如何attach到一个程序上进行调试。首先将刚才的程序运行起来,我这里直接使用了

go build test-debug.go
./test-debug

然后使用ps查看正在运行的程序pid

  501 40994   549   0 12:08AM ttys003    0:00.00 ./test-debug
 

然后我们attach上去


dlv attach 40994

可以看到,熟悉的debug seesion又回来了。下面我们可以继续使用上面的命令去设置断点了

(dlv) break dostuff
Breakpoint 1 set at 0x2058 for main.dostuff() /Users/xianlu/WorkSpace/golang/src/test-debug.go:9
(dlv) break dostuff:3
Breakpoint 2 set at 0x2144 for main.dostuff() /Users/xianlu/WorkSpace/golang/src/test-debug.go:12

我使用continue使程序运行到我设置断点的地方

(dlv) continue
> main.dostuff() /Users/xianlu/WorkSpace/golang/src/test-debug.go:12 (hits goroutine(18):1 total:8) (PC: 0x2144)
> main.dostuff() /Users/xianlu/WorkSpace/golang/src/test-debug.go:12 (hits goroutine(19):1 total:8) (PC: 0x2144)
> main.dostuff() /Users/xianlu/WorkSpace/golang/src/test-debug.go:12 (hits goroutine(26):1 total:8) (PC: 0x2144)
> main.dostuff() /Users/xianlu/WorkSpace/golang/src/test-debug.go:12 (hits goroutine(23):1 total:8) (PC: 0x2144)
> main.dostuff() /Users/xianlu/WorkSpace/golang/src/test-debug.go:12 (hits goroutine(24):1 total:8) (PC: 0x2144)
> main.dostuff() /Users/xianlu/WorkSpace/golang/src/test-debug.go:12 (hits goroutine(20):1 total:8) (PC: 0x2144)
> main.dostuff() /Users/xianlu/WorkSpace/golang/src/test-debug.go:12 (hits goroutine(21):1 total:8) (PC: 0x2144)
> main.dostuff() /Users/xianlu/WorkSpace/golang/src/test-debug.go:12 (hits goroutine(25):1 total:8) (PC: 0x2144)
     7:    )
     8:
     9:    func dostuff(wg *sync.WaitGroup, i int) {
    10:        fmt.Printf("goroutine id %d\n", i)
    11:        time.Sleep(300 * time.Second)
=>  12:        fmt.Printf("goroutine id %d\n", i)
    13:        wg.Done()
    14:    }
    15:
    16:    func main() {
    17:        var wg sync.WaitGroup
(dlv)

可以看到,Delve已经打印出来了当前正在运行的goroutine,下面我们print一下我们当前的i

(dlv) print i
7
(dlv) print wg
*sync.WaitGroup {
    state1: [12]uint8 [1,0,0,0,10,0,0,0,0,0,0,0],
    sema: 0,}

和上面一样,而且attach到这个进程后,也可以把对应的源码显示出来,是不是很强大呢。更多的功能就自己参考文档摸索吧。

上一篇:利用PowerShell代码注入漏洞绕过受限语言模式


下一篇:Kubernetes集群中基于 CRD 实现分批发布