使用Delve调试Go程序
2021-10-20
Delve是一个使用Go语言开发的专门用于Go语言的调试工具。Delve项目的地址是https://github.com/go-delve/delve,
这个项目的目标是为Go提供一个简单的、功能齐全、易于调用和使用的调试器。当前Go语言支持GDB、LLDB、Delve三种调试工具,LLDB是MacOS系统推荐的,已经成为了XCode默认调试器,但GDB和LLDB对Go的支持比不上Delve这个专门为Go设计和开发的调试工具。
Goland
和Go for Visual Studio Code
这种集成开发环境的调试功能也都是集成了delve的。
本文将学习Delve命令行的基本使用。
开发环境准备 #
Delve使用Go开发,支持Linux、MacOS、Windows平台,本文的学习环境基于Linux,首先构建一个golang-debug
的Docker镜像,关于对Delve的学习过程都在这个镜像的容器中进行。
下面给出这个镜像构建的Dockerfile:
1FROM alpine:3.14.2
2
3ENV LANG=C.UTF-8
4
5# Here we install GNU libc (aka glibc) and set C.UTF-8 locale as default.
6
7RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories &&\
8 ALPINE_GLIBC_BASE_URL="https://github.com/sgerrand/alpine-pkg-glibc/releases/download" && \
9 ALPINE_GLIBC_PACKAGE_VERSION="2.33-r0" && \
10 ALPINE_GLIBC_BASE_PACKAGE_FILENAME="glibc-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && \
11 ALPINE_GLIBC_BIN_PACKAGE_FILENAME="glibc-bin-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && \
12 ALPINE_GLIBC_I18N_PACKAGE_FILENAME="glibc-i18n-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && \
13 apk add --no-cache --virtual=.build-dependencies wget ca-certificates && \
14 echo \
15 "-----BEGIN PUBLIC KEY-----\
16 MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApZ2u1KJKUu/fW4A25y9m\
17 y70AGEa/J3Wi5ibNVGNn1gT1r0VfgeWd0pUybS4UmcHdiNzxJPgoWQhV2SSW1JYu\
18 tOqKZF5QSN6X937PTUpNBjUvLtTQ1ve1fp39uf/lEXPpFpOPL88LKnDBgbh7wkCp\
19 m2KzLVGChf83MS0ShL6G9EQIAUxLm99VpgRjwqTQ/KfzGtpke1wqws4au0Ab4qPY\
20 KXvMLSPLUp7cfulWvhmZSegr5AdhNw5KNizPqCJT8ZrGvgHypXyiFvvAH5YRtSsc\
21 Zvo9GI2e2MaZyo9/lvb+LbLEJZKEQckqRj4P26gmASrZEPStwc+yqy1ShHLA0j6m\
22 1QIDAQAB\
23 -----END PUBLIC KEY-----" | sed 's/ */\n/g' > "/etc/apk/keys/sgerrand.rsa.pub" && \
24 wget \
25 "$ALPINE_GLIBC_BASE_URL/$ALPINE_GLIBC_PACKAGE_VERSION/$ALPINE_GLIBC_BASE_PACKAGE_FILENAME" \
26 "$ALPINE_GLIBC_BASE_URL/$ALPINE_GLIBC_PACKAGE_VERSION/$ALPINE_GLIBC_BIN_PACKAGE_FILENAME" \
27 "$ALPINE_GLIBC_BASE_URL/$ALPINE_GLIBC_PACKAGE_VERSION/$ALPINE_GLIBC_I18N_PACKAGE_FILENAME" && \
28 apk add --no-cache \
29 "$ALPINE_GLIBC_BASE_PACKAGE_FILENAME" \
30 "$ALPINE_GLIBC_BIN_PACKAGE_FILENAME" \
31 "$ALPINE_GLIBC_I18N_PACKAGE_FILENAME" && \
32 \
33 rm "/etc/apk/keys/sgerrand.rsa.pub" && \
34 /usr/glibc-compat/bin/localedef --force --inputfile POSIX --charmap UTF-8 "$LANG" || true && \
35 echo "export LANG=$LANG" > /etc/profile.d/locale.sh && \
36 \
37 apk del glibc-i18n && \
38 \
39 rm "/root/.wget-hsts" && \
40 apk del .build-dependencies && \
41 rm \
42 "$ALPINE_GLIBC_BASE_PACKAGE_FILENAME" \
43 "$ALPINE_GLIBC_BIN_PACKAGE_FILENAME" \
44 "$ALPINE_GLIBC_I18N_PACKAGE_FILENAME"
45
46RUN apk update --no-cache && apk add ca-certificates --no-cache && \
47 apk add tzdata --no-cache && \
48 ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
49 echo "Asia/Shanghai" > /etc/timezone
50
51RUN apk add --no-cache -X http://mirrors.aliyun.com/alpine/edge/community go
52
53
54RUN apk add --no-cache binutils gdb vim &&\
55 apk add --no-cache -X http://mirrors.aliyun.com/alpine/edge/testing vim-go delve
上面的Dockerfile主要包含以下几个步骤:
- 以docker官方
alpine:3.14.2
作为基础镜像构建 - 第5~44行是在alpine镜像的基础上按照glibc
- 第51行为使用alpine community repository中当前最新的go package安装go语言
- 第54行安装了一些开发和调试工具,包括:
binutils
是一个二进制工具集,包含了ELF文件格式解析器readelf
,可以解析Linux上可执行文件的文件格式gdb
调试器vim
文本编辑器vim-go
vim的go插件delve
go语言调试器
ELF (Executable and Linkable Format)是一种为可执行文件,目标文件,共享链接库和内核转储(core dumps)准备的标准文件格式。 Linux和很多类Unix操作系统都使用这个格式。
使用上面的Dockerfile构建出golang-debug:latest
这个容器镜像后,就可以启动容器作为我们的开发环境,这里没有使用docker,我的容器开发环境为nerdctl+containerd
,在使用nerdctl启动containerd容器之前,创建一个volume
用来存放我们开发的go代码。
1nerdctl volume create go-projects
2
3nerdctl volume ls
4VOLUME NAME DIRECTORY
5go-projects /var/lib/nerdctl/1935db59/volumes/default/go-projects/_data
下面使用nerdctl启动golang-debug:latest
的容器,进入开发环境:
1nerdctl run -it --rm --privileged -v go-projects:/root golang-debug:latest
上面的命令启动了Go开发环境的容器,将go-projects
这个volume挂载到了容器中的/root
目录,另外还需要注意为了在容器中使用delve
调试器,需要以--privileged
启动容器。
Delve简单使用 #
下面对Delve做一个简单使用,进入golang-debug
容器的开发环境:
1nerdctl run -it --rm --privileged -v go-projects:/root golang-debug:latest
进入到工作目录/root
中,创建一个用于存放项目的目录:
1cd /root
2mkdir projects
3cd projects
使用go mod
创建一个名称为hello的go工程:
1mkdir hello && cd hello
2go mod init hello
使用vim编写一个如下main.go的文件:
1package main
2
3import "fmt"
4
5var pi = 3.14
6
7func main() {
8 s := []int{}
9 for i := 0; i < 5; i++ {
10 s = append(s, i)
11 }
12 fmt.Println(pi, s)
13}
项目的目录结构如下:
1hello
2├── go.mod
3└── main.go
在项目目录中(go.mod文件所在目录中)执行dlv debug
命令进入调试:
1dlv debug
2Type 'help' for list of commands.
3(dlv)
输入help命令就可以查看Delve进行调试可以使用的命令帮助:
1The following commands are available:
2
3Running the program:
4 call ------------------------ Resumes process, injecting a function call (EXPERIMENTAL!!!)
5 continue (alias: c) --------- Run until breakpoint or program termination.
6 next (alias: n) ------------- Step over to next source line.
7 rebuild --------------------- Rebuild the target executable and restarts it. It does not work if the executable was not built by delve.
8 restart (alias: r) ---------- Restart process.
9 step (alias: s) ------------- Single step through program.
10 step-instruction (alias: si) Single step a single cpu instruction.
11 stepout (alias: so) --------- Step out of the current function.
12
13Manipulating breakpoints:
14 break (alias: b) ------- Sets a breakpoint.
15 breakpoints (alias: bp) Print out info for active breakpoints.
16 clear ------------------ Deletes breakpoint.
17 clearall --------------- Deletes multiple breakpoints.
18 condition (alias: cond) Set breakpoint condition.
19 on --------------------- Executes a command when a breakpoint is hit.
20 toggle ----------------- Toggles on or off a breakpoint.
21 trace (alias: t) ------- Set tracepoint.
22 watch ------------------ Set watchpoint.
23
24Viewing program variables and memory:
25 args ----------------- Print function arguments.
26 display -------------- Print value of an expression every time the program stops.
27 examinemem (alias: x) Examine raw memory at the given address.
28 locals --------------- Print local variables.
29 print (alias: p) ----- Evaluate an expression.
30 regs ----------------- Print contents of CPU registers.
31 set ------------------ Changes the value of a variable.
32 vars ----------------- Print package variables.
33 whatis --------------- Prints type of an expression.
34
35Listing and switching between threads and goroutines:
36 goroutine (alias: gr) -- Shows or changes current goroutine
37 goroutines (alias: grs) List program goroutines.
38 thread (alias: tr) ----- Switch to the specified thread.
39 threads ---------------- Print out info for every traced thread.
40
41Viewing the call stack and selecting frames:
42 deferred --------- Executes command in the context of a deferred call.
43 down ------------- Move the current frame down.
44 frame ------------ Set the current frame, or execute command on a different frame.
45 stack (alias: bt) Print stack trace.
46 up --------------- Move the current frame up.
47
48Other commands:
49 config --------------------- Changes configuration parameters.
50 disassemble (alias: disass) Disassembler.
51 dump ----------------------- Creates a core dump from the current process state
52 edit (alias: ed) ----------- Open where you are in $DELVE_EDITOR or $EDITOR
53 exit (alias: quit | q) ----- Exit the debugger.
54 funcs ---------------------- Print list of functions.
55 help (alias: h) ------------ Prints the help message.
56 libraries ------------------ List loaded dynamic libraries
57 list (alias: ls | l) ------- Show source code.
58 source --------------------- Executes a file containing a list of delve commands
59 sources -------------------- Print list of source files.
60 types ---------------------- Print list of types
61
62Type help followed by a command for full documentation.
实际常用上面命令的别名,几个比较常用的如下:
b
可以打断点。dlv中打断点有多种方式,b
后边可以跟一个*地址
,包名.函数名
,或者文件名:行号
的形式bp
打印当前启用的所有断点c
表示continue,从当前断点跳转到下一个断点n
表示next,从当前行代码行执行到下一步vars
命令可以查看全部包级别的变量,一般通过正则表达式过滤查看,例如vars main
查看main包中的变量。args
命令可以查看函数的参数locals
可以查看函数的局部变量p
即print查看某个变量的具体值,或者评估一个表达式的值stack
可以查看当前函数执行的栈帧信息goroutine
查看当前的goroutine信息goroutines
查看程序的goroutine列表
下面实操一下,在main.main
函数上打一个断点:
1(dlv) b main.main
2Breakpoint 1 set at 0x49474f for main.main() ./main.go:7
3(dlv)
下面通过bp命令查看当前已经启用的所有断点:
1(dlv) bp
2Breakpoint runtime-fatal-throw (enabled) at 0x432ca0 for runtime.throw() /usr/lib/go/src/runtime/panic.go:1188 (0)
3Breakpoint unrecovered-panic (enabled) at 0x433000 for runtime.fatalpanic() /usr/lib/go/src/runtime/panic.go:1271 (0)
4 print runtime.curg._panic.arg
5Breakpoint 1 (enabled) at 0x49474f for main.main() ./main.go:7 (0)
可以看到除了我们在main函数打的断点外,delve调试器还为我们在runtime包中的panic相关的两个函数上打上了断点。
下面查看一下main包中包级别的变量:
1(dlv) vars main
2runtime.main_init_done = chan bool nil
3runtime.mainStarted = false
4main.pi = 3.14
下面再在main.go的第10行打一个断点:
1(dlv) b main.go:10
2Breakpoint 2 set at 0x49479f for main.main() ./main.go:10
3(dlv) bp
4Breakpoint runtime-fatal-throw (enabled) at 0x432ca0 for runtime.throw() /usr/lib/go/src/runtime/panic.go:1188 (0)
5Breakpoint unrecovered-panic (enabled) at 0x433000 for runtime.fatalpanic() /usr/lib/go/src/runtime/panic.go:1271 (0)
6 print runtime.curg._panic.arg
7Breakpoint 1 (enabled) at 0x49474f for main.main() ./main.go:7 (1)
8Breakpoint 2 (enabled) at 0x49479f for main.main() ./main.go:10 (0)
9(dlv)
使用c
命令从断点1直接跳转到断点2:
1(dlv) c
2> main.main() ./main.go:10 (hits goroutine(1):1 total:1) (PC: 0x49479f)
3 5: var pi = 3.14
4 6:
5 7: func main() {
6 8: s := []int{}
7 9: for i := 0; i < 5; i++ {
8=> 10: s = append(s, i)
9 11: }
10 12: fmt.Println(pi, s)
11 13: }
12(dlv)
查看一下函数的参数和本地局部变量:
1(dlv) args
2(no args)
3(dlv) locals
4s = []int len: 0, cap: 0, []
5i = 0
6(dlv)
下面就可以使用n
命令一步步执行了:
1(dlv) n
2> main.main() ./main.go:9 (PC: 0x494811)
3 4:
4 5: var pi = 3.14
5 6:
6 7: func main() {
7 8: s := []int{}
8=> 9: for i := 0; i < 5; i++ {
9 10: s = append(s, i)
10 11: }
11 12: fmt.Println(pi, s)
12 13: }
13(dlv) n
14> main.main() ./main.go:10 (hits goroutine(1):2 total:2) (PC: 0x49479f)
15 5: var pi = 3.14
16 6:
17 7: func main() {
18 8: s := []int{}
19 9: for i := 0; i < 5; i++ {
20=> 10: s = append(s, i)
21 11: }
22 12: fmt.Println(pi, s)
23 13: }
24(dlv) locals
25s = []int len: 1, cap: 1, [...]
26i = 1
27(dlv)
查看一下当前s
变量的值:
1p s
2[]int len: 1, cap: 1, [0]
查看一下当前执行函数的栈帧信息:
1(dlv) stack
20 0x000000000049479f in main.main
3 at ./main.go:10
41 0x0000000000435273 in runtime.main
5 at /usr/lib/go/src/runtime/proc.go:255
62 0x000000000045f6c1 in runtime.goexit
7 at /usr/lib/go/src/runtime/asm_amd64.s:1581
8(dlv)
查看一下当前的goroutine信息:
1(dlv) gr
2Thread 1540 at ./main.go:10
3Goroutine 1:
4 Runtime: ./main.go:10 main.main (0x49479f)
5 User: ./main.go:10 main.main (0x49479f)
6 Go: <autogenerated>:1 runtime.newproc (0x461b89)
7 Start: /usr/lib/go/src/runtime/proc.go:145 runtime.main (0x435080)
8(dlv)
查看一下所有的groutines:
1(dlv) grs
2* Goroutine 1 - User: ./main.go:10 main.main (0x49479f) (thread 1540)
3 Goroutine 2 - User: /usr/lib/go/src/runtime/proc.go:367 runtime.gopark (0x435692) [force gc (idle)]
4 Goroutine 3 - User: /usr/lib/go/src/runtime/proc.go:367 runtime.gopark (0x435692) [GC sweep wait]
5 Goroutine 4 - User: /usr/lib/go/src/runtime/proc.go:367 runtime.gopark (0x435692) [GC scavenge wait]
6 Goroutine 5 - User: /usr/lib/go/src/runtime/proc.go:367 runtime.gopark (0x435692) [finalizer wait]
7[5 goroutines]
8(dlv)
Delve子命令 #
前面我们通过一个例子简单演示了使用delve调试go程序的main包。
dlv提供了很多子命令应对不同的使用场景:
1dlv attach - Attach to running process and begin debugging.
2dlv connect - Connect to a headless debug server.
3dlv core - Examine a core dump.
4dlv dap - [EXPERIMENTAL] Starts a headless TCP server communicating via Debug Adaptor Protocol (DAP).
5dlv debug - Compile and begin debugging main package in current directory, or the package specified.
6dlv exec - Execute a precompiled binary, and begin a debug session.
7dlv replay - Replays a rr trace.
8dlv test - Compile test binary and begin debugging program.
9dlv trace - Compile and begin tracing program.
10dlv version - Prints version.
11dlv log - Help about logging flags
12dlv backend - Help about the --backend flag
例如我们还对go的测试调试需求,需要用到dlv test
子命令,调试go的单元测试。
另外,还可以通过dlv实现远程调试。