Delve进行Golang代码的调试

Delve是托管在Github上的一个开源项目。

项目地址:https://github.com/go-delve/delve

概要

Delve是Go程序的源代码级调试器。

Delve使您可以通过控制进程的执行,评估变量并提供线程/ goroutine状态,CPU寄存器状态等信息来与程序进行交互。

该工具的目的是为调试Go程序提供一个简单而强大的界面。

使用
Usage:
  dlv [command]

Available Commands:
  attach      Attach to running process and begin debugging.
  connect     Connect to a headless debug server.
  core        Examine a core dump.
  dap         [EXPERIMENTAL] Starts a TCP server communicating via Debug Adaptor Protocol (DAP).
  debug       Compile and begin debugging main package in current directory, or the package specified.
  exec        Execute a precompiled binary, and begin a debug session.
  help        Help about any command
  run         Deprecated command. Use 'debug' instead.
  test        Compile test binary and begin debugging program.
  trace       Compile and begin tracing program.
  version     Prints version.
————————————————————————————————————————————————————————————————————————————————————
  attach      连接到正在运行的流程并开始调试.
  connect     连接到无头调试服务器.
  core        检查核心转储.
  dap		  [EXPERIMENTAL]启动通过调试适配器协议(DAP)进行通信的TCP服务器。
  debug       编译并开始调试当前目录下的主包或指定的包.
  exec        执行预编译的二进制文件,并开始调试会话.
  help        帮助信息
  run         弃用的命令。使用“debug”替代它.
  test        编译测试二进制文件并开始调试程序.
  trace       编译并开始跟踪程序.
  version     打印版本.

使用"dlv [command] --help"获取有关命令的详细信息.

上面列举了dlv的一些命令,其中常用的有如 help、attach、core、debug、trace、version 等。

dlv help:上面的信息只是列出了命令列表,具体使用方法没有给出,这里可以运行 dlv help 查看具体命令的使用方法。也可以运行 dlv help help 查看 help 命令的使用说明。这里以查看 core 命令为例。

Examine a core dump (only supports linux and windows core dumps).

The core command will open the specified core file and the associated
executable and let you examine the state of the process when the
core dump was taken.

Currently supports linux/amd64 and linux/arm64 core files and windows/amd64 minidumps.

Usage:
  dlv core <executable> <core> [flags]

core dump(核心转储)是包含程序内存意外终止快照的文件。它用于事后调试以了解崩溃原因和其中涉及的变量。Go提供了环境变量GOTRACEBACK 用于控制程序崩溃时生成的输出。 此变量还可以强制生成core dump,从而可以进行调试。

GOTRACEBACK
GOTRACEBACK 控制程序崩溃时输出的详细程度。 它可以采用不同的值:

none 不显示任何goroutine栈trace。

single, 默认选项,显示当前goroutine栈trace。

all 显示所有用户创建的goroutine栈trace。

system 显示所有goroutine栈trace,甚至运行时的trace。

crash 类似 system, 而且还会生成 core dump。

最后一个选项使我们能够在发生崩溃的情况下调试程序。

以以下程序为例:

package main

import "math/rand"

var sum int

func main() {
	for {
		n := rand.Intn(1e6)
		sum += n
		if sum % 42 == 0 {
			panic(":)")
		}
	}
}

运行它,程序很快就发生崩溃:

panic: :)

goroutine 1 [running]:
main.main()
	/home/liuxin/gopath/src/test/panic.go:12 +0x8a
exit status 2

只看崩溃结果,也没办法从栈跟踪中分析出崩溃所涉及的值,增加日志或许是一种解决方法,但是无从下手,所以要使用GOTRACEBACK=crash运行它,但是要运行它之前还要首先得到其核心转储文件,所以就要做以下步骤:

go build -gcflags=all='-N -l' panic.go # 编译,关闭优化方便调试。编译后生成了二进制文件 panic
ulimit -c unlimited # 解开转储文件大小限制,否则不会生成转储文件
GOTRACEBACK=crash ./panic # 运行,会提示核心已转储。生成了转储文件 core
dlv core panic core # 调试

命令运行,就可以与核心core dump进行交互了:

Type 'help' for list of commands.
(dlv) bt
 0  0x000000000045d371 in runtime.raise
    at /home/liuxin/go/src/runtime/sys_linux_amd64.s:165
 1  0x0000000000442a8b in runtime.dieFromSignal
    at /home/liuxin/go/src/runtime/signal_unix.go:729
 2  0x0000000000442f4e in runtime.sigfwdgo
    at /home/liuxin/go/src/runtime/signal_unix.go:943
 3  0x0000000000441bd4 in runtime.sigtrampgo
    at /home/liuxin/go/src/runtime/signal_unix.go:412
 4  0x000000000045d6d3 in runtime.sigtramp
    at /home/liuxin/go/src/runtime/sys_linux_amd64.s:389
 5  0x000000000045d7c0 in runtime.sigreturn
    at /home/liuxin/go/src/runtime/sys_linux_amd64.s:481
 6  0x0000000000442c3a in runtime.crash
    at /home/liuxin/go/src/runtime/signal_unix.go:821
 7  0x000000000042e6a4 in runtime.fatalpanic
    at /home/liuxin/go/src/runtime/panic.go:1216
 8  0x000000000042e01e in runtime.gopanic
    at /home/liuxin/go/src/runtime/panic.go:1064
 9  0x0000000000461ddd in main.main
    at ./panic.go:15
10  0x0000000000430ac8 in runtime.main
    at /home/liuxin/go/src/runtime/proc.go:203
11  0x000000000045b7f1 in runtime.goexit
    at /home/liuxin/go/src/runtime/asm_amd64.s:1373

dlv命令bt打印当前栈信息并显示程序生成的panic 打印,使用frame 9切换访问第9帧:

(dlv) frame 9
> runtime.raise() /home/liuxin/go/src/runtime/sys_linux_amd64.s:165 (PC: 0x45d371)
Warning: debugging optimized function
Frame 9: ./panic.go:15 (PC: 461ddd)
Warning: listing may not match stale executable
    10:			sum += n
    11:			if sum % 42 == 0 {
    12:				panic(":)")
    13:			}
    14:		}
=>  15:	}

使用locals打印局部变量,了解崩溃所涉及的值:

(dlv) locals
n = 203300

可以看到随机生成的数字为203300.关于函数中的sum变量的话,可以使用打印包变量命令vars去打印:

(dlv) vars main
runtime.mainStarted = true
runtime.main_init_done = chan bool 0/0
main.sum = 5705994
main..inittask = runtime.initTask {state: 2, ndeps: 1, nfns: 0}

我们知道一个可独立运行的golang程序,一定要有main.main()因为main()是程序的入口,而main()在执行前,根据package的初始化顺序,会先初始化依赖的package然后在初始化main,上述输出的一些值就是在main在初始化时的一些数值。至于其中含义,可以看源代码go/src/runtime/proc.go了解其所代表的含义。

调试

进入调试模式的几种方法:

dlv attach pid:类似于gdb attach pid,可以直接对正在运行的进程直接进行调试。

dlv debug:运行dlv debug test.go会先编译go源文件,同时执行attach命令进入调试模式,该命令会在当前目录下生成一个名为debug的可执行二进制文件,退出调试模式会自动被删除。

dlv exec executable_file:直接从二进制文件启动调试模式。

dlv core executable_file core_file:以core文件启动调试,通常进行dlv的目的就是为了找出可执行文件core的原因,通过core文件可直接找出具体进程异常的信息,就像上述演示一样。

dlv trace:该命令最直接的用途是可以追踪代码里函数的调用轨迹.

稍微演示下这个dlv trace命令,写了一个小例子:

package main

import (
	"fmt"
	"time"
)

func Afunc(){
	fmt.Println("Called")
}

func main(){
	ticker := time.NewTicker(time.Second)
	for{
		select{
		case <-ticker.C:
			Afunc()
	}
	}
} 													//每隔一秒执行调用一次Afunc这个函数

输入dlv trace trace.go Afunc追踪这个函数的调用轨迹:

$ dlv trace trace.go Afunc
> goroutine(1): main.Afunc()
Called
 => ()
> goroutine(1): main.Afunc()
Called
 => ()
> goroutine(1): main.Afunc()
Called
 => ()
> goroutine(1): main.Afunc()
Called
 => ()

和预期一样,一秒调用一次函数Afunc

下面完整的来一遍调试过程:

以写的代码为例:

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

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

dlv debug命令进行源码调试,进入调试界面:

$ dlv debug test.go 
Type 'help' for list of commands.
(dlv)

可输入dlv help输出帮助,查看所有命令.

大部分命令和gdb类似,break [name]:设置断点

(dlv) b test.go:16
Breakpoint 2 set at 0x4affc3 for main.main() ./test.go:16

执行c运行到断点处

(dlv) c
> main.main() ./test.go:16 (hits goroutine(1):1 total:1) (PC: 0x4affc3)
	workers: (unreadable eval error: could not find symbol value for workers)
    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++ {

执行bp查看所有的断点

(dlv) bp
Breakpoint runtime-fatal-throw at 0x434360 for runtime.fatalthrow() /home/liuxin/go/src/runtime/panic.go:1162 (0)
Breakpoint unrecovered-panic at 0x4343d0 for runtime.fatalpanic() /home/liuxin/go/src/runtime/panic.go:1189 (0)
	print runtime.curg._panic.arg
Breakpoint 1 at 0x4b0398 for main.main() ./test.go:16 (0)

上面也有提到

bt:打印当前栈信息。

frame:切换栈。

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

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

(dlv) print workers < 100
true

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

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

首先先c到断点处,然后n

(dlv) c
> main.dostuff() ./test.go:9 (hits goroutine(7):1 total:2) (PC: 0x4afe18)
> main.dostuff() ./test.go:9 (hits goroutine(6):1 total:2) (PC: 0x4afe18)
     4:	    "fmt"
     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:	}
(dlv) n
> main.dostuff() ./test.go:9 (hits goroutine(15):1 total:4) (PC: 0x4afe18)
> main.dostuff() ./test.go:9 (hits goroutine(11):1 total:4) (PC: 0x4afe18)
> main.dostuff() ./test.go:10 (PC: 0x4afe2f)
     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) print i
0
(dlv) n
goroutine id 5

可以看到Delve会告诉你目前的goroutine id.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值