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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
FROM alpine:3.14.2

ENV LANG=C.UTF-8

# Here we install GNU libc (aka glibc) and set C.UTF-8 locale as default.

RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories &&\
    ALPINE_GLIBC_BASE_URL="https://github.com/sgerrand/alpine-pkg-glibc/releases/download" && \
    ALPINE_GLIBC_PACKAGE_VERSION="2.33-r0" && \
    ALPINE_GLIBC_BASE_PACKAGE_FILENAME="glibc-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && \
    ALPINE_GLIBC_BIN_PACKAGE_FILENAME="glibc-bin-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && \
    ALPINE_GLIBC_I18N_PACKAGE_FILENAME="glibc-i18n-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && \
    apk add --no-cache --virtual=.build-dependencies wget ca-certificates && \
    echo \
        "-----BEGIN PUBLIC KEY-----\
        MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApZ2u1KJKUu/fW4A25y9m\
        y70AGEa/J3Wi5ibNVGNn1gT1r0VfgeWd0pUybS4UmcHdiNzxJPgoWQhV2SSW1JYu\
        tOqKZF5QSN6X937PTUpNBjUvLtTQ1ve1fp39uf/lEXPpFpOPL88LKnDBgbh7wkCp\
        m2KzLVGChf83MS0ShL6G9EQIAUxLm99VpgRjwqTQ/KfzGtpke1wqws4au0Ab4qPY\
        KXvMLSPLUp7cfulWvhmZSegr5AdhNw5KNizPqCJT8ZrGvgHypXyiFvvAH5YRtSsc\
        Zvo9GI2e2MaZyo9/lvb+LbLEJZKEQckqRj4P26gmASrZEPStwc+yqy1ShHLA0j6m\
        1QIDAQAB\
        -----END PUBLIC KEY-----" | sed 's/   */\n/g' > "/etc/apk/keys/sgerrand.rsa.pub" && \
    wget \
        "$ALPINE_GLIBC_BASE_URL/$ALPINE_GLIBC_PACKAGE_VERSION/$ALPINE_GLIBC_BASE_PACKAGE_FILENAME" \
        "$ALPINE_GLIBC_BASE_URL/$ALPINE_GLIBC_PACKAGE_VERSION/$ALPINE_GLIBC_BIN_PACKAGE_FILENAME" \
        "$ALPINE_GLIBC_BASE_URL/$ALPINE_GLIBC_PACKAGE_VERSION/$ALPINE_GLIBC_I18N_PACKAGE_FILENAME" && \
    apk add --no-cache \
        "$ALPINE_GLIBC_BASE_PACKAGE_FILENAME" \
        "$ALPINE_GLIBC_BIN_PACKAGE_FILENAME" \
        "$ALPINE_GLIBC_I18N_PACKAGE_FILENAME" && \
    \
    rm "/etc/apk/keys/sgerrand.rsa.pub" && \
    /usr/glibc-compat/bin/localedef --force --inputfile POSIX --charmap UTF-8 "$LANG" || true && \
    echo "export LANG=$LANG" > /etc/profile.d/locale.sh && \
    \
    apk del glibc-i18n && \
    \
    rm "/root/.wget-hsts" && \
    apk del .build-dependencies && \
    rm \
        "$ALPINE_GLIBC_BASE_PACKAGE_FILENAME" \
        "$ALPINE_GLIBC_BIN_PACKAGE_FILENAME" \
        "$ALPINE_GLIBC_I18N_PACKAGE_FILENAME"

RUN apk update --no-cache && apk add ca-certificates --no-cache && \
    apk add tzdata --no-cache && \
    ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
    echo "Asia/Shanghai" > /etc/timezone

RUN apk add --no-cache  -X http://mirrors.aliyun.com/alpine/edge/community go


RUN apk add --no-cache binutils gdb vim &&\
    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代码。

1
2
3
4
5
nerdctl volume create go-projects

nerdctl volume ls
VOLUME NAME    DIRECTORY
go-projects    /var/lib/nerdctl/1935db59/volumes/default/go-projects/_data

下面使用nerdctl启动golang-debug:latest的容器,进入开发环境:

1
nerdctl run -it --rm --privileged -v go-projects:/root golang-debug:latest

上面的命令启动了Go开发环境的容器,将go-projects这个volume挂载到了容器中的/root目录,另外还需要注意为了在容器中使用delve调试器,需要以--privileged启动容器。

Delve简单使用

下面对Delve做一个简单使用,进入golang-debug容器的开发环境:

1
nerdctl run -it --rm --privileged -v go-projects:/root golang-debug:latest

进入到工作目录/root中,创建一个用于存放项目的目录:

1
2
3
cd /root
mkdir projects
cd projects

使用go mod创建一个名称为hello的go工程:

1
2
mkdir hello && cd hello
go mod init hello

使用vim编写一个如下main.go的文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main

import "fmt"

var pi = 3.14

func main() {
	s := []int{}
	for i := 0; i < 5; i++ {
		s = append(s, i)
	}
	fmt.Println(pi, s)
}

项目的目录结构如下:

1
2
3
hello
├── go.mod
└── main.go

在项目目录中(go.mod文件所在目录中)执行dlv debug命令进入调试:

1
2
3
4
dlv debug
Type 'help' for list of commands.
(dlv)

输入help命令就可以查看Delve进行调试可以使用的命令帮助:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
The following commands are available:

Running the program:
    call ------------------------ Resumes process, injecting a function call (EXPERIMENTAL!!!)
    continue (alias: c) --------- Run until breakpoint or program termination.
    next (alias: n) ------------- Step over to next source line.
    rebuild --------------------- Rebuild the target executable and restarts it. It does not work if the executable was not built by delve.
    restart (alias: r) ---------- Restart process.
    step (alias: s) ------------- Single step through program.
    step-instruction (alias: si)  Single step a single cpu instruction.
    stepout (alias: so) --------- Step out of the current function.

Manipulating breakpoints:
    break (alias: b) ------- Sets a breakpoint.
    breakpoints (alias: bp)  Print out info for active breakpoints.
    clear ------------------ Deletes breakpoint.
    clearall --------------- Deletes multiple breakpoints.
    condition (alias: cond)  Set breakpoint condition.
    on --------------------- Executes a command when a breakpoint is hit.
    toggle ----------------- Toggles on or off a breakpoint.
    trace (alias: t) ------- Set tracepoint.
    watch ------------------ Set watchpoint.

Viewing program variables and memory:
    args ----------------- Print function arguments.
    display -------------- Print value of an expression every time the program stops.
    examinemem (alias: x)  Examine raw memory at the given address.
    locals --------------- Print local variables.
    print (alias: p) ----- Evaluate an expression.
    regs ----------------- Print contents of CPU registers.
    set ------------------ Changes the value of a variable.
    vars ----------------- Print package variables.
    whatis --------------- Prints type of an expression.

Listing and switching between threads and goroutines:
    goroutine (alias: gr) -- Shows or changes current goroutine
    goroutines (alias: grs)  List program goroutines.
    thread (alias: tr) ----- Switch to the specified thread.
    threads ---------------- Print out info for every traced thread.

Viewing the call stack and selecting frames:
    deferred --------- Executes command in the context of a deferred call.
    down ------------- Move the current frame down.
    frame ------------ Set the current frame, or execute command on a different frame.
    stack (alias: bt)  Print stack trace.
    up --------------- Move the current frame up.

Other commands:
    config --------------------- Changes configuration parameters.
    disassemble (alias: disass)  Disassembler.
    dump ----------------------- Creates a core dump from the current process state
    edit (alias: ed) ----------- Open where you are in $DELVE_EDITOR or $EDITOR
    exit (alias: quit | q) ----- Exit the debugger.
    funcs ---------------------- Print list of functions.
    help (alias: h) ------------ Prints the help message.
    libraries ------------------ List loaded dynamic libraries
    list (alias: ls | l) ------- Show source code.
    source --------------------- Executes a file containing a list of delve commands
    sources -------------------- Print list of source files.
    types ---------------------- Print list of types

Type 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
2
3
(dlv) b main.main
Breakpoint 1 set at 0x49474f for main.main() ./main.go:7
(dlv)

下面通过bp命令查看当前已经启用的所有断点:

1
2
3
4
5
(dlv) bp
Breakpoint runtime-fatal-throw (enabled) at 0x432ca0 for runtime.throw() /usr/lib/go/src/runtime/panic.go:1188 (0)
Breakpoint unrecovered-panic (enabled) at 0x433000 for runtime.fatalpanic() /usr/lib/go/src/runtime/panic.go:1271 (0)
	print runtime.curg._panic.arg
Breakpoint 1 (enabled) at 0x49474f for main.main() ./main.go:7 (0)

可以看到除了我们在main函数打的断点外,delve调试器还为我们在runtime包中的panic相关的两个函数上打上了断点。

下面查看一下main包中包级别的变量:

1
2
3
4
(dlv) vars main
runtime.main_init_done = chan bool nil
runtime.mainStarted = false
main.pi = 3.14

下面再在main.go的第10行打一个断点:

1
2
3
4
5
6
7
8
9
(dlv) b main.go:10
Breakpoint 2 set at 0x49479f for main.main() ./main.go:10
(dlv) bp
Breakpoint runtime-fatal-throw (enabled) at 0x432ca0 for runtime.throw() /usr/lib/go/src/runtime/panic.go:1188 (0)
Breakpoint unrecovered-panic (enabled) at 0x433000 for runtime.fatalpanic() /usr/lib/go/src/runtime/panic.go:1271 (0)
	print runtime.curg._panic.arg
Breakpoint 1 (enabled) at 0x49474f for main.main() ./main.go:7 (1)
Breakpoint 2 (enabled) at 0x49479f for main.main() ./main.go:10 (0)
(dlv)

使用c命令从断点1直接跳转到断点2:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
(dlv) c
> main.main() ./main.go:10 (hits goroutine(1):1 total:1) (PC: 0x49479f)
     5:	var pi = 3.14
     6:
     7:	func main() {
     8:		s := []int{}
     9:		for i := 0; i < 5; i++ {
=>  10:			s = append(s, i)
    11:		}
    12:		fmt.Println(pi, s)
    13:	}
(dlv)

查看一下函数的参数和本地局部变量:

1
2
3
4
5
6
(dlv) args
(no args)
(dlv) locals
s = []int len: 0, cap: 0, []
i = 0
(dlv)

下面就可以使用n命令一步步执行了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
(dlv) n
> main.main() ./main.go:9 (PC: 0x494811)
     4:
     5:	var pi = 3.14
     6:
     7:	func main() {
     8:		s := []int{}
=>   9:		for i := 0; i < 5; i++ {
    10:			s = append(s, i)
    11:		}
    12:		fmt.Println(pi, s)
    13:	}
(dlv) n
> main.main() ./main.go:10 (hits goroutine(1):2 total:2) (PC: 0x49479f)
     5:	var pi = 3.14
     6:
     7:	func main() {
     8:		s := []int{}
     9:		for i := 0; i < 5; i++ {
=>  10:			s = append(s, i)
    11:		}
    12:		fmt.Println(pi, s)
    13:	}
(dlv) locals
s = []int len: 1, cap: 1, [...]
i = 1
(dlv)

查看一下当前s变量的值:

1
2
p s
[]int len: 1, cap: 1, [0]

查看一下当前执行函数的栈帧信息:

1
2
3
4
5
6
7
8
(dlv) stack
0  0x000000000049479f in main.main
   at ./main.go:10
1  0x0000000000435273 in runtime.main
   at /usr/lib/go/src/runtime/proc.go:255
2  0x000000000045f6c1 in runtime.goexit
   at /usr/lib/go/src/runtime/asm_amd64.s:1581
(dlv)

查看一下当前的goroutine信息:

1
2
3
4
5
6
7
8
(dlv) gr
Thread 1540 at ./main.go:10
Goroutine 1:
	Runtime: ./main.go:10 main.main (0x49479f)
	User: ./main.go:10 main.main (0x49479f)
	Go: <autogenerated>:1 runtime.newproc (0x461b89)
	Start: /usr/lib/go/src/runtime/proc.go:145 runtime.main (0x435080)
(dlv)

查看一下所有的groutines:

1
2
3
4
5
6
7
8
(dlv) grs
* Goroutine 1 - User: ./main.go:10 main.main (0x49479f) (thread 1540)
  Goroutine 2 - User: /usr/lib/go/src/runtime/proc.go:367 runtime.gopark (0x435692) [force gc (idle)]
  Goroutine 3 - User: /usr/lib/go/src/runtime/proc.go:367 runtime.gopark (0x435692) [GC sweep wait]
  Goroutine 4 - User: /usr/lib/go/src/runtime/proc.go:367 runtime.gopark (0x435692) [GC scavenge wait]
  Goroutine 5 - User: /usr/lib/go/src/runtime/proc.go:367 runtime.gopark (0x435692) [finalizer wait]
[5 goroutines]
(dlv)

Delve子命令

前面我们通过一个例子简单演示了使用delve调试go程序的main包。

dlv提供了很多子命令应对不同的使用场景:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
dlv attach - Attach to running process and begin debugging.
dlv connect - Connect to a headless debug server.
dlv core - Examine a core dump.
dlv dap - [EXPERIMENTAL] Starts a headless TCP server communicating via Debug Adaptor Protocol (DAP).
dlv debug - Compile and begin debugging main package in current directory, or the package specified.
dlv exec - Execute a precompiled binary, and begin a debug session.
dlv replay - Replays a rr trace.
dlv test - Compile test binary and begin debugging program.
dlv trace - Compile and begin tracing program.
dlv version - Prints version.
dlv log - Help about logging flags
dlv backend - Help about the --backend flag

例如我们还对go的测试调试需求,需要用到dlv test子命令,调试go的单元测试。 另外,还可以通过dlv实现远程调试。

参考