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
", i)
time.Sleep(300 * time.Second)
fmt.Printf("goroutine id %d
", 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
", 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
", i)
11: time.Sleep(300 * time.Second)
12: fmt.Printf("goroutine id %d
", 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
", i)
11: time.Sleep(300 * time.Second)
12: fmt.Printf("goroutine id %d
", i)
13: wg.Done()
14: }
15:
(dlv) print i
0
(dlv) n
goroutine id 5
可以看到Delve会告诉你目前的goroutine id.