调试器
调试器可以提供源代码级别的调试,例如设置断点、单步调试、监视变量等。目前Go语言支持GDB、LLDB和Delve几种调试器,其中GDB是最早支持的调试工具,LLDB是macOS系统推荐的标准调试工具。但是GDB和LLDB对Go语言的专有特性都缺乏很好的支持,只有Delve是专门为Go语言设计开发的调试工具。而且Delve本身也是采用Go语言开发,对Windows平台也提供了同样的支持。
- GDB(GNU Debugger)是GNU软件系统中的标准调试器。Go语言通过 $GOROOT/src/runtime/runtime-gdb.py 扩展脚本理解Go符号。
- LLDB(Low Level Debugger)是LLVM(Low Level Virtual Machine)项目的调试器。
- Delve(探索;/delv/)是专门为Go语言提供的开源调试器,2014年由Derek Parker创建。
调试循环:启动调试器→设置断点→在调试器内运行程序→断点处使用命令打印需要的信息→单步调试等→退出调试器
Delve调试器
Delve是Go官方推荐的调试器,它是Go语言程序的调试利器。相比于其他调试器,Delve更懂 Go语言的运行时、数据结构和表达式,它可以与各种IDE和文本编辑器集成,提供强大的调试功能,支持本地调试和远程调试。此外,Delve的单步调试使其非常适合做源码分析辅助工具。
Note that Delve is a better alternative to GDB when debugging Go programs built with the standard toolchain. It understands the Go runtime, data structures, and expressions better than GDB. Delve currently supports Linux, OSX, and Windows on amd64. For the most up-to-date list of supported platforms, please see the Delve documentation.
下载安装
方式1:
$ go get github.com/go-delve/delve/cmd/dlv
方式2:
$ git clone https://github.com/go-delve/delve
$ cd delve
$ go install github.com/go-delve/delve/cmd/dlv
方式3:根据go版本不同,需要安装不同delve版本,如果go版本和delve版本不兼容,delve无法启动(--check-go-version=false)
# Install the latest release:
$ go install github.com/go-delve/delve/cmd/dlv@latest
# Install at tree head:
$ go install github.com/go-delve/delve/cmd/dlv@master
# Install at a specific version or pseudo-version: 推荐!!!
$ go install github.com/go-delve/delve/cmd/dlv@v1.7.3
$ go install github.com/go-delve/delve/cmd/dlv@v1.7.4-0.20211208103735-2f13672765fe
测试是否安装成功
$ dlv --help
$ dlv help
$ dlv version
启动命令
dlv debug [package] [flags] 调试源码文件(默认编译当前目录的main包)
dlv test [package] [flags] 调试测试文件
dlv exec <path/to/binary> [flags] 调试可执行文件(precompiled binary)
dlv attach pid [executable] [flags] 调试运行进程(在线挂接进程调试)
dlv core <executable> <core> [flags] 调试核心转储文件(core dump)
dlv trace [package] regexp [flags] 追踪目标程序(ptrace和eBPF)
dlv connect addr [flags] 连接无头调试服务器(远程调试)
方式1
$ dlv debug main.go
方式2
# 禁止内联,禁止优化
$ go build -gcflags="all=-N -l" -o main main.go
$ dlv exec ./main
说明1:Windows下启动delve可能需要添加 --allow-non-terminal-interactive,而且启动后不会出现(dlv)命令提示符。
说明2:调试可执行文件时,需要确保可执行文件包含DWARF调试信息和符号表信息。调试信息在可执行文件中占用很大的空间,可以通过-s和-w选项将符号表和调试信息从最终的二进制文件中去除,也可以使用strip命令去除ELF文件中调试信息。
调试命令
(1)运行程序
restart 重新启动
rebuild 重新编译
exit [-c] 停止执行(退出调试)
continue [<locspec>] 继续执行,直到指定位置、遇到断点或程序结束
next [count] 单步执行(不进入函数,将函数调用看作整体)---逐过程
step 单步执行(进入函数)---逐语句
stepout 单步执行(退出函数)
step-instruction 单步执行汇编指令
(2)管理断点
breakpoints [-a] 打印所有断点(所有类型),delve内部自动为panic异常函数设置断点:runtime.throw()和runtime.fatalpanic()
break [name] [locspec] 设置断点(匿名断点、命名断点)
clear <breakpoint name or id> 删除断点
clearall [<locspec>] 删除多个断点
condition <breakpoint name or id> <boolean expression> 设置条件断点
on <breakpoint name or id> <command> 在命中断点时执行命令(断点回调)
toggle <breakpoint name or id> 启用(enabled)或禁用(disabled)断点
trace [name] [locspec] 设置追踪点
watch [-r|-w|-rw] <expr> 设置监视点
- 断点(breakpoint):暂停程序的运行
- 追踪点(tracepoint):只打印相关信息,不会暂停程序的运行。通常用于调试和性能优化
- 监视点(watchpoint):读取或者修改某个内存地址时暂停程序运行并打印相关信息
(3)查看变量和内存
args [-v] [<regex>] 打印函数的参数和返回值
locals [-v] [<regex>] 打印函数的局部变量
print [%format] <expression> 打印表达式的值
whatis <expression> 打印表达式的类型
set <variable> = <value> 修改变量的值
call [-unsafe] <function call expression> 调用函数
examinemem [-fmt <format>] [-count|-len <count>] [-size <size>] <address> 打印内存地址的信息
display -a [%format] <expression> 当程序暂停时打印表达式的值。通常用于监视变量的变化情况,类似on print
vars [-v] [<regex>] 打印包级变量(包含整个程序的全局变量,最好通过regex过滤)---注意:如果全局变量没有被使用,vars命令则不会打印
regs [-a] 打印CPU寄存器信息(硬件寄存器,不支持伪寄存器)
(4)栈帧
stack [<depth>] [-full] [-offsets] [-defer] [-a <n>] [-adepth <depth>] [-mode <mode>] 打印函数调用栈信息
frame <m> <command> 移动到指定栈帧
up [<m>] <command> 向上移动栈帧
down [<m>] <command> 向下移动栈帧
(5)并发
goroutine [<id>] [<command>] 打印当前协程信息、或者切换到指定协程
goroutines 打印所有协程信息(前面有星号的goroutine表示当前goroutine)
thread <id> 切换到指定线程
threads 打印所有线程信息
(6)其他
list [<locspec>] 打印源代码
sources [<regex>] 打印所有源文件
funcs [<regex>] 打印所有函数
types [<regex>] 打印所有类型
source <path> 执行包含delve命令列表的文件,支持starlark脚本
dump <output file> 创建核心转储
disassemble [-a <start> <end>] [-l <locspec>] 反汇编程序
Starlark是一门配置语言,设计之初是为了作为 Bazel 的配置语言,Starlark语法类似 Python,但不是Python,保持语言类似于 Python 可以减少学习曲线,使语义对用户更加明显。
说明1:使用Delve调试Go汇编程序的过程比调试Go语言程序更加简单。调试汇编程序时,需要时刻关注寄存器的状态,如果涉及函数调用或局部变量和参数,还需要重点关注栈寄存器SP的状态。
说明2:使用Delve调试并发程序与普通程序并没有两样
说明3:使用Delve调试core dump文件
$ ulimit -c unlimited // 不限制core文件大小
$ go build main.go
$ GOTRACEBACK=crash ./main
$ dlv core ./main ./core
位置指示符
- <function>[:<line>] 函数:行号,函数格式为<package>.<receiver type>.<name> | <package>.(<receiver type>).<name> | <receiver type>.<name> | <package>.<name> | (<receiver type>).<name> | <name>
- <filename>:<line> 文件名:行号
- <line> 行号,当前文件
- *<address> 内存地址,支持十进制、十六进制和八进制
- (+|-)<offset> 偏移量
- /<regex>/ 正则表达式
表达式
- All (binary and unary) on basic types except <-, ++ and -- // 基本类型的一元、二元操作
- Comparison operators on any type // 任意类型的比较运算符
- Type casts between numeric types // 数值类型之间转换
- Type casts of integer constants into any pointer type and vice versa // 整型常量的类型强制转换为任何指针类型,反之亦然
- Type casts between string, []byte and []rune // string, []byte 和 []rune 之间转换
- Struct member access (i.e. somevar.memberfield) // 结构体成员访问
- Slicing and indexing operators on arrays, slices and strings// 数组、切片和字符串的切片和索引操作符
- Map access // 映射访问
- Pointer dereference // 指针解引用
- Calls to builtin functions: cap, len, complex, imag and real // 内建函数调用
- Type assertion on interface variables (i.e. somevar.(concretetype)) // Interface 类型断言
- 特殊变量:runtime.curg 和 runtime.frameoff
- 寄存器:CPU寄存器的名称全部用大写字母表示
配置参数
config -list // 列出所有配置项
config -save // 保存配置信息至文件
config max-string-len 9999 // 设置打印最大string长度为9999
config max-array-values 9999 // 设置打印最大数组长度为9999
实现原理
为了方便各种调试器前端(命令行、IDE、编辑器插件、图形化前端)与Delve集成,Delve采用了一个前后分离的架构。
- UI Layer:直接与用户交互
- Service Layer:用于前后端通信
- Symbolic Layer:通过读取Go编译器(包括链接器)以DWARF格式(一种标准的调试信息格式)写入目标二进制文件中的调试符号信息来了解被调试目标源码,并实现了被调试目标进程中的地址、二进制文件中的调试符号及源码相关信息三者之间的关系映射
- Target Layer:通过各个操作系统提供的系统API来控制被调试目标进程,它对被调试目标的源码没有任何了解
DWARF(Debugging With Attributed Record Format,使用属性记录信息调试)是一种标准的调试信息格式。在GCC中,DWARF调试信息是与编译好的可执行代码放在同一个ELF文件中的。调试信息一般保存在".debug_abbrev"、".debug_frame"等以".debug"开头的ELF段中。
代码示例
package main
import "fmt"
var (
g1 = 100
g2 = 200
)
func main() {
a := 1
b := 2
c := calc(a, b)
fmt.Println(c)
fmt.Println(g1, g2)
}
func calc(v1, v2 int) int {
v3 := v1 + v2
r := incr(v3)
return r
}
func incr(v int) int {
r := v + 1
return r
}
(1)命令行调试
$ dlv debug main.go
Type 'help' for list of commands.
(dlv) break main.main
Breakpoint 1 set at 0x4b67d8 for main.main() ./main.go:10
(dlv) break calc:3
Breakpoint 2 set at 0x4b6a28 for main.calc() ./main.go:21
(dlv) breakpoints
Breakpoint runtime-fatal-throw (enabled) at 0x4390a0 for runtime.throw() /usr/local/go/src/runtime/panic.go:1107 (0)
Breakpoint unrecovered-panic (enabled) at 0x439320 for runtime.fatalpanic() /usr/local/go/src/runtime/panic.go:1190 (0)
print runtime.curg._panic.arg
Breakpoint 1 (enabled) at 0x4b67d8 for main.main() ./main.go:10 (0)
Breakpoint 2 (enabled) at 0x4b6a28 for main.calc() ./main.go:21 (0)
(dlv) continue
> main.main() ./main.go:10 (hits goroutine(1):1 total:1) (PC: 0x4b67d8)
5: var (
6: g1 = 100
7: g2 = 200
8: )
9:
=> 10: func main() {
11: a := 1
12: b := 2
13: c := calc(a, b)
14: fmt.Println(c)
15: fmt.Println(g1, g2)
(dlv) continue
> main.calc() ./main.go:21 (hits goroutine(1):1 total:1) (PC: 0x4b6a28)
16: }
17:
18: func calc(v1, v2 int) int {
19: v3 := v1 + v2
20: r := incr(v3)
=> 21: return r
22: }
23:
24: func incr(v int) int {
25: r := v + 1
26: return r
(dlv) args
v1 = 1
v2 = 2
~r2 = 0
(dlv) locals
v3 = 3
r = 4
(dlv) vars main
runtime.main_init_done = chan bool 0/0
runtime.mainStarted = true
main.g1 = 100
main.g2 = 200
(dlv) continue
4
100 200
Process 2659532 has exited with status 0
(dlv) exit
$
(2)LiteIDE调试
(3)远程调试
服务器
# 线上环境使用 dlv exec 和 dlv attach。启动delve后,无法通过ctrl+c退出,需要直接kill掉进程,所以以后台方式启动
$ dlv --listen=:2345 --headless --api-version=2 --accept-multiclient --check-go-version=false exec ./main >./logs/stdout 2>./logs/stderr &
本地机器
$ dlv connect 192.168.23.209:2345
Type 'help' for list of commands.
config substitute-path /home/golang/src/delve C:\home\workspace\playground\delve
list main.main
Showing /home/golang/src/delve/main.go:10 (PC: 0x4b67d8)
5: var (
6: g1 = 100
7: g2 = 200
8: )
9:
10: func main() {
11: a := 1
12: b := 2
13: c := calc(a, b)
14: fmt.Println(c)
15: fmt.Println(g1, g2)
break main.main
Breakpoint 1 set at 0x4b67d8 for main.main() ./main.go:10
break calc:3
Breakpoint 2 set at 0x4b6a28 for main.calc() ./main.go:21
breakpoints
Breakpoint runtime-fatal-throw (enabled) at 0x4390a0 for runtime.throw() /usr/local/go/src/runtime/panic.go:1107 (0)
Breakpoint unrecovered-panic (enabled) at 0x439320 for runtime.fatalpanic() /usr/local/go/src/runtime/panic.go:1190 (0)
print runtime.curg._panic.arg
Breakpoint 1 (enabled) at 0x4b67d8 for main.main() ./main.go:10 (0)
Breakpoint 2 (enabled) at 0x4b6a28 for main.calc() ./main.go:21 (0)
continue
> main.main() ./main.go:10 (hits goroutine(1):1 total:1) (PC: 0x4b67d8)
5: var (
6: g1 = 100
7: g2 = 200
8: )
9:
=> 10: func main() {
11: a := 1
12: b := 2
13: c := calc(a, b)
14: fmt.Println(c)
15: fmt.Println(g1, g2)
continue
> main.calc() ./main.go:21 (hits goroutine(1):1 total:1) (PC: 0x4b6a28)
16: }
17:
18: func calc(v1, v2 int) int {
19: v3 := v1 + v2
20: r := incr(v3)
=> 21: return r
22: }
23:
24: func incr(v int) int {
25: r := v + 1
26: return r
args
v1 = 1
v2 = 2
~r2 = 0
locals
v3 = 3
r = 4
vars main
runtime.main_init_done = chan bool 0/0
runtime.mainStarted = true
main.g1 = 100
main.g2 = 200
continue
Process 2682253 has exited with status 0
常见问题:无法显示源文件或设置断点
$ dlv connect 192.168.23.209:2345
Type 'help' for list of commands.
list main.main
Showing /home/golang/src/delve/main.go:10 (PC: 0x4b67d8)
Command failed: open /home/golang/src/delve/main.go: The system cannot find the path specified.
解决方法:使用路径替换配置,详见substpath
config substitute-path /compiler/machine/directory /debugger/machine/directory
说明:源码路径是编译机器上项目路径,通过以下命令移除源码的前缀路径(使用相对路径)
$ go build -gcflags="all=-N -l" -o main main.go
$ dlv exec ./main
Type 'help' for list of commands.
(dlv) sources main.go
/home/golang/src/delve/main.go
(dlv) q
$ go build -gcflags="all=-trimpath=$PWD -N -l" -asmflags "all=-trimpath=$PWD" -o main main.go
$ dlv exec ./main
Type 'help' for list of commands.
(dlv) sources main.go
main.go
(dlv)
参考资料
-
《Go语言高级编程》3.9 Delve调试
-
《Go语言精进之路:从新手到高手的编程思想、方法和技巧》第49条 使用Delve调试Go代码