Delve是Go官方推荐的调试器,我们熟知的Goland、LiteIDE都集成了Delve,同时各大通用IDE如VSCode、Atom、Sublime等的Go插件也都集成了Delve调试Go代码。
除了IDE以外,还有一个使用Delve的GUI调试器——Gdlv。除了提供图形界面外,Gdlv也可以执行Delve命令。
开始
我们需要安装Delve和Gdlv。
Go1.16及以上版本:
$ go install github.com/go-delve/delve/cmd/dlv@latest
$ go install github.com/aarzilli/gdlv@latest
Go1.16以下版本
$ go get -u github.com/derekparker/delve/cmd/dlv
$ go get -u github.com/aarzilli/gdlv
确保的你Go bin目录已添加到path
环境变量。
查看是否安装成功:
$ dlv version
$ gdlv version
配置存储路径
Delve会在磁盘存储一个配置文件和一个历史命令文件。默认情况下,Linux系统是放在$HOME/.config/dlv
下,其他系统包括Windows是在$HOME/.dlv
下,$HOME
就是你的用户目录。
可以通过配置XDG_CONFIG_HOME
环境变量来更改存放目录。
启动调试
源代码调式
打开命令行切换到main包目录,然后执行以下命令。
$ gdlv debug
也可以手动指定源码目录。
$ gdlv -d 源码目录 debug
可执行文件调试
Delve也可以调试编译好的可执行文件,当然也是由Delve编译的调试版可执行文件。
$ gdlv exec xxx.debug.exe
exec
不支持-d
选项。
调试Go测试代码
同调试源代码基本一致,只是将debug
换成了test
。
$ gdlv test
也可通过-d
选项指定代码目录。
打开调试窗口以后默认断点在main函数处,在Command窗口底部可以输入Delve命令。
Gdlv启动调试默认会停在main
函数处,但是Delve并不会停在main
处,所以在直接用dlv debug
命令调试时,需要先在main
函数处设置断点,然后运行至断点处,具体命令见后面章节。
Gdlv界面操作
设置断点
在Gdlv的Listing窗口,右键会弹出一个菜单,一个是设置断点,一个是运行到指定行。
右键断点,可以禁用/启动断点,编辑断点,以及运行到断点处。
编辑断点可以设置断点条件。
单步
在Command窗口的顶部有运行、单步、跳入跳出按钮。
右上角的NEW WINDOW
下拉框可以打开更多窗口,查看其他信息。比如选则Breakpoints
可以查看所有断点。
查看变量
Variables窗口可以查看本地变量,通过Filter可以过滤变量。Full Types单选框可以显示完整类型信息,Address单选框可以显示变量地址。
Delve命令
接下来要介绍的是Devle的调试命令,我们可以在Gdlv的Command窗口输入这些命令。
断点
设置断点
设置断点的命令是break <loction>
,break
的别名是b
,因此也可以使用b <location>
。
此外断点也可以命名,格式为break 断点名 <location>
,或者b 断点名 <location>
。
location有五种格式。
内存地址
支持十进制,十六进制和八进制。
文件名:行号
文件名可以是相对路径,如果没有歧义,也可以只写文件名。如果省略文件名,则表示当前文件。
b main.go:8
b 8
偏移量
基于当前行上下偏移。
b +1
b -2
函数:行号
在不引起歧义的情况下,可以直接写函数名,否则应该写包名.函数名
。这里的行号表示的是基于函数的偏移行数,如果省略,表示第0行,也就是func xxx
那行。
b main.max
b main.max:2
正则
在所有满足正则匹配的函数处设置断点。正则表达式需用/
包裹:/正则表达式/
。
查看断点
breakpoints [-a]
或者简写bp [-a]
。通过-a
选项可以查看所有物理断点,包括内部断点。
这个命令在Gdlv中不可用,可以通过点击NEW WINDOW
->Breakpoints
查看断点。
清除断点
单个清除
命令格式为clear <id or name>
。
当我们通过break
命令设置断点时,会打印出断点ID。
如果设置断点时指定了名称,那么也可以通过断点名删除。
通过这个命令删除断点不会在Gdlv中实时显示,需要做一个别的操作刷新页面,删除的断点才会消失。
批量清除
命令为clearall [<locspec>]
,如果省略<location>
则清除所有断点,否则,删除与<location>
匹配处的断点。
这个命令在Gdlv中也不可用。
断点条件
设置断点条件的命令是condition
,别名是cond
。这个命令在Gdlv中也不可用,所以在Gdlv中只能通过UI来设置断点条件。条件是一个布尔表达式,关于Delve表达式见表达式。
满足条件进入断点
命令格式如下:
cond <id or name> expr
进入断点次数做为条件
除了普通的布尔表达式,也可以根据进入断点的次数决定此次进入断点是否要停留。支持以下几种表达式。
cond -hitcount bp > n
cond -hitcount bp >= n
cond -hitcount bp < n
cond -hitcount bp <= n
cond -hitcount bp == n
cond -hitcount bp != n
cond -hitcount bp % n
bp
是内置变量,表示进入断点次数,n
是条件。
清除条件
通过-clear
选项可以清除某个断点上的条件。
cond -clear <id or name>
断点回调
进入断点时执行命令。格式为:
on <id or name> <command>
支持的<command>
有以下5个:
-
print
打印表达式 -
stack
打印调用栈 -
goroutine
显示goroutine -
trace
将断点变成跟踪点(trace point) -
cond
等同于cond
修改命令使用-edit
选项。
on <id or name> -edit
禁用/启动断点
禁用/启用断点可以通过Gdlv界面进行操作。Delve官方文档给了一个toggle
命令来禁用/启用断点,但是实测会提示命令不可用,不知道是不是版本的原因。
运行调试
单步
next [n]
Delve中,这个next
命令可以指定执行几行代码。但在Gdlv中,n
会被忽略,始终执行1行。
单步进入
命令为step
,别名为s
。
单步跳出
命令为stepout
,别名为so
。
在Gdlv中不支持so
。
单步cpu指令
命令为step-instruction
,别名为si
。CPU指令级别的单步。
重新开始
命令restart
,别名r
。
退出调试
命令:exit
。
继续执行
命令为continue
,别名为c
。
默认执行到下一个断点或至程序结束。也可以指定location,运行到指定位置。
c <loccation>
在Gdlv中,<location>
会被忽略。
查看状态
查看函数参数
命令格式:
[goroutine <n>] [frame <m>] args [-v] [<regex>]
打印函数参数,-v
选项用于显示参数详细信息,<regexp>
可以过滤变量。
args
args a
goroutine 1 args
args
在Gdlv中不可用,当进入函数后,参数和本地变量会自动显示在Valriables窗口。
查看本地变量
locals
的使用与args
一样。
[goroutine <n>] [frame <m>] locals [-v] [<regex>]
locals
在Gdlv中也不可用,原因同args
。
查看包变量
vars [-v] [<regex>]
只打印包级变量。
监控表达式
添加监控表达式
dispaly -a <expr>
将<expr>
添加到监控列表,每当程序停下时都会打印出监控列表中表达式的值。
(dlv) display -a a+1
0: a+1 = 2
:
前面的0
是表达式的下标,删除时会用到。
查看监控表达式列表display
不带任何参数会打印出所有被监控的表达式以及表达式的值。
(dlv) display
0: a+1 = 3
1: b = 4
这里也会打印出表达式的编号。
删除表达式监控
display -d <index>
这里的<index>
是表达式的编号。
求值表达式
命令为print
,别名p
。命令格式:
[goroutine <n>] [frame <m>] print <expression>
与display
不同的是,print
只会打印表达式的值1次。
print a
goroutine 1 print b
查看寄存器
regs [-a]
打印CPU寄存器的值,-a
选项打印更多寄存器的值。
打印内存
命令examinemem
,别名x
。
x [-fmt <format>] [-count|-len <count>] [-size <size>] <address>
从<address>
开始按<format>
格式以<size>
字节为单位打印<count>
次。
<format>
有5种:
- bin 二进制格式
- oct 八进制格式
- dec 十进制格式
- hex 十六进制格式(默认)
- addr 地址
<count>
表示次数,默认1,最大值1000。
<size>
表示字节数,默认1,最大值8。
<address
表示内存地址。
示例:
(dlv) x 0xc00010fec8
0xc00010fec8: 0x02
(dlv) x -fmt dec 0xc00010fec8
0xc00010fec8: 002
(dlv) x -fmt dec -size 8 0xc00010fec8
0xc00010fec8: 000000000000000000000002
(dlv) x -fmt dec -count 2 -size 8 0xc00010fec8
0xc00010fec8: 000000000000000000000002 000000000000000000000004
查看表达式类型
whatis <expr>
打印表达式类型。
修改变量值
[goroutine <n>] [frame <m>] set <variable> = <value>
只能修改数字和指针类型的值。
查看文档
查看所有命令文档。
help
查看某个命令文档。
help xxx
表达式
Delve支持的表达式语法是Go语法的子集。
语法
- 除
<-
、++
、--
外的单目和双目运算符 - 比较运算符
- 数字类型的类型转换
- 数字和指针类型相互转换
-
string
、[]byte
和[]rune
相互转换 - 访问结构体成员(如
a.A
) - 切片和下标运算,支持数组、切片和字符串
- 访问
map
- 指针解引用
- 调用内置函数:
cap
、len
、complex
、imag
、real
。 - 类型断言(如
somevar.(concretetyp)
)
特殊变量
-
runtime.curg
:当前goroutine的G结构体。如runtime.curg.goid
就是当前goroutine的goroutine id。 -
runtime.frameoff
:当前栈帧基址距栈底的偏移量。
(dlv) print runtime.curg.goid
1
(dlv) print runtime.frameoff
-312
嵌套深度限制
Delve打印变量时默认会解引用,但是解引用最多解两层,嵌套超过两层的变量会打印出地址。
例如:
(dlv) print c1
main.cstruct {
pb: *struct main.bstruct {
a: (*main.astruct)(0xc82000a430),
},
sa: []*main.astruct len: 3, cap: 3, [
*(*main.astruct)(0xc82000a440),
*(*main.astruct)(0xc82000a450),
*(*main.astruct)(0xc82000a460),
],
}
想打印sa
的第一个元素,有两种方式:
- 下标运算:
print c1.sa[0]
- 直接用地址:
print *(*main.astruct)(0xc82000a440)
元素数量限制
对于数组、切片、字符串和map,Delve一次最多打印64个元素。想打印更多元素需要切片,如:
print arr
print arr[64:]
注意,在Delve中,map也是可以切片的,Delve会按照一个固定的顺序遍历map。
这个限制也可以通过config
配置。
config max-string-len 100
config max-array-values 100
接口
接口打印格式如下:
接口名(具体类型) 值
例如:
(dlv) p iface1
interface {}(*struct main.astruct) *{A: 1, B: 2}
(dlv) p iface2
interface {}(*struct string) *"test"
(dlv) p err1
error(*struct main.astruct) *{A: 1, B: 2}
访问接口类型变量的字段有3种方式:
- 类型断言
(dlv) p iface1.(*main.astruct).B
2
- 使用
.(data)
(dlv) p iface1.(data).B
2
- 如果是结构体类型或指向结构体的指针,也可省略类型断言。
(dlv) p iface1.B
2
包路径
当变量名相同时,需要加以包名区分。
p "some/package".A
p "some/other/package".A
Cgo的指针
Char指针被当作字符串,可以索引和切片。其他C指针做为Go切片,可以索引和切片。
CPU寄存器
CPU寄存器名称必须全大写,如RAX
表示RAX
寄存器。
如果本地变量名和寄存器重名,表达式的结果是本地变量,寄存器会被隐藏。
寄存器名前可以加任意多下划线,比如_RAX
、__RAX
都表示RAX
寄存器。
小于等于64比特的寄存器用uint64表示,大于64比特的寄存器用字符串以十六进制表示。
也可以将寄存器值表示为数组:
RegName.intN //表示为intN的数组
RegName.uintN //表示为uintN的数组
RegName.floatN //表示为floatN的数组
其中N必须是2的幂。
附录
- Delve:https://github.com/go-delve/delve
- Delve命令:https://github.com/go-delve/delve/blob/master/Documentation/cli/README.md
- Gdlv:https://github.com/aarzilli/gdlv