使用Delve调试Go程序

使用Delve调试Go程序

2021-10-20
Go

Delve是一个使用Go语言开发的专门用于Go语言的调试工具。Delve项目的地址是https://github.com/go-delve/delve, 这个项目的目标是为Go提供一个简单的、功能齐全、易于调用和使用的调试器。当前Go语言支持GDB、LLDB、Delve三种调试工具,LLDB是MacOS系统推荐的,已经成为了XCode默认调试器,但GDB和LLDB对Go的支持比不上Delve这个专门为Go设计和开发的调试工具。 GolandGo 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-govim的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实现远程调试。

参考 #

© 2024 青蛙小白