我一般调试Go程序都是通过log日志,性能调试的话通过 pprof 、trace、flamegraph等,主要是Go没有一个很好的集成的debugger,前两年虽然关注了delve,但是在IDE中集成比较粗糙,调试也很慢,所以基本不使用debugger进行调试, 最近看到滴滴的工程师分享的使用debugger在调试Go程序,我觉得有必要在尝试一下这方面的技术了。
本文翻译自 Debugging Go Code with LLDB , 更好的调试Go程序的工具是delve , 因为它是专门为Go开发, 使用起来也很简单,并且还可以远程调试。delve的命令还可参考: dlv cli ,但是流行的通用的基础的debugger也是常用的手段之一。我在译文后面也列出了几篇其它关于go debug的相关文章,有兴趣的话也可以扩展阅读一下。
本文主要介绍应用于glang compiler 工具链的技术, 除了本文的介绍外,你还可以参考 LLDB 手册
介绍
在 Linux、Mac OS X, FreeBSD 或者 NetBSD环境中,当你使用 gc工具链编译和链接Go程序的时候, 编译出的二进制文件会携带DWARFv3 调试信息。 LLDB调试器( > 3.7)可以使用这个信息调试进程或者core dump文件。
使用-w
可以告诉链接器忽略这个调试信息, 比如go build -ldflags "-w" prog.go
。
gc编译器产生的代码可能会包含内联的优化,这不方便调试器调试,为了禁止内联, 你可以使用-gcflags "-N -l"
参数。
安装lldb
MacOS下如果你安装了XCode,应该已经安装了LLDB, LLDB是XCode默认的调试器。
Linux/MacOS/Windows下的安装方法可以参考: Installing-LLDB 。
通用操作
1
2
3
4
5
6
(lldb) l
(lldb) l line
(lldb) l file .go:line
(lldb) b line
(lldb) b file .go:line
(lldb) disas
显示 backtrace 和 unwind stack frame:
1
2
(lldb) bt
(lldb) frame n
Show the name, type and location on the stack frame of local variables, arguments and return values:
1
2
3
(lldb) frame variable
(lldb) p varname
(lldb) expr -T
Go扩展
表达式解析
LLDB支持Go表达式:
1
2
3
(lldb) p x
(lldb) expr *(*int32 )(t)
(lldb) help expr
Interface
默认LLDB显示接口的动态类型。通常它是一个指针, 比如func foo(a interface{}) { ... }
, 如果你调用callfoo(1.0)
, lldb会把a
看作*float64inside
,你也可以禁止为一个表达式禁止这种处理,或者在全局禁用:
1
2
(lldb) expr -d no -dynamic-values -- a
(lldb) settings set target.prefer-dynamic-values no -dynamic-values
LLDB包含 go string 和 slice的格式化输出器,查看LLDB docs 文档学习定制格式化输出。如果你想扩展内建的格式化方式,可以参考GoLanguageRuntime.cpp 。
Channel和map被看作引用类型,lldb把它们作为指针类型, 就像C++的类型hash<int,string>*
。Dereferencing会显示类型内部的表示。
Goroutine
LLDB 把 Goroutine 看作 thread。
1
2
3
(lldb) thread list
(lldb) bt all
(lldb) thread select 2
已知问题
如果编译时开启优化,调试信息可能是错误的。请确保开启参数 -gcflags "-N -l"
不能改变变量的值,或者调用goh函数
需要更好的支持 chan 和 map 类型
调试信息不包含输入的package, 所以你在表达式中需要package的全路径。当package中包含 non-identifier 字符的时候你需要用引号包含它: x.(*foo/bar.BarType)
或者 (*“v.io/x/foo”.FooType)(x)
调试信息不包含作用域,所以变量在它们初始化之前是可见的。 如果有同名的本地变量,比如shadowed 变量, 你不知道哪个是哪个
调试信息仅仅描述了变量在内存中的位置,所以你可能看到寄存器中的变量的stale数据
不能打印函数类型
教程
在这个例子中我们可以检查标准库正则表达式。为了构建二进制文件, 进入$GOROOT/src/regexp
然后运行run go test -gcflags "-N -l" -c
,这会产生可执行文件 regexp.test
。
启动
启动 lldb, 调试 regexp.test:
1
2
3
4
$ lldb regexp.test
(lldb) target create "regexp.test"
Current executable set to 'regexp.test' (x86_64).
(lldb)
设置断点
在TestFind 函数上设置断点:
1
(lldb) b regexp .TestFind
有时候 go编译器会使用全路径为函数名添加前缀,如果你不能使用上面简单的名称,你可以使用正则表达式设置断点:
1
2
(lldb) break set -r regexp .TestFind$
Breakpoint 5 : where = regexp .test`_/code/go/src/regexp.TestFind + 37 at find_test.go:149, address = 0x00000000000863a5
运行程序:
1
2
3
4
5
6
7
8
9
10
11
12
(lldb) run -- test. run= TestFind
Process 8496 launched: '/code/go/src/regexp/regexp.test' (x86_64)
Process 8496 stopped
* thread #9 : tid = 0x0017 , 0x00000000000863a5 regexp. test`_/code/go/src/regexp.TestFind(t=0x000000020834a000) + 37 at find_test.go:149, stop reason = breakpoint 2.1 3.1 5.1
frame #0: 0x00000000000863a5 regexp.test`_/code/go/src/regexp. TestFind(t= 0x000000020834a000 ) + 37 at find_test. go:149
146
147
148 func TestFind(t * testing. T) {
-> 149 for _, test := range findTests {
150 re := MustCompile(test. pat)
151 if re. String () != test. pat {
152 t. Errorf("String() = `%s`; should be `%s`" , re. String (), test. pat)
程序会运行到设置的断点上,查看运行的goroutine以及它们在做什么:
1
2
3
4
5
6
7
8
9
(lldb) thread list
Process 8496 stopped
thread #1: tid = 0x12201, 0x000000000003c0ab regexp.test`runtime.mach_semaphore_wait + 11 at sys_darwin_amd64.s:412
thread #2: tid = 0x122fa, 0x000000000003bf7c regexp.test`runtime.usleep + 44 at sys_darwin_amd64.s:290
thread #4: tid = 0x0001, 0x0000000000015865 regexp.test`runtime.gopark(unlockf=0x00000000000315a0, lock =0x00000002083220b8 , reason="chan receive" ) + 261 at proc.go :131
thread #5 : tid = 0x0002 , 0x0000000000015865 regexp .test`runtime.gopark(unlockf=0x00000000000315a0, lock=0x00000000002990d0, reason="force gc (idle)") + 261 at proc.go:131
thread #6: tid = 0x0003, 0x0000000000015754 regexp.test`runtime.Gosched + 20 at proc.go :114
thread #7 : tid = 0x0004 , 0x0000000000015865 regexp .test`runtime.gopark(unlockf=0x00000000000315a0, lock=0x00000000002a07d8, reason="finalizer wait") + 261 at proc.go:131
* thread #9: tid = 0x0017, 0x00000000000863a5 regexp.test`_/code/go /src/regexp .TestFind(t=0x000000020834a000 ) + 37 at find_test.go :149 , stop reason = breakpoint 2.1 3.1 5.1
用*
标出的那个goroutine是当前的goroutine。
查看代码
使用l
或者list
查看代码, #
重复最后的命令:
1
2
(lldb) l
(lldb) # Hit enter to repeat last command . Here, list the next few lines
命名
变量和函数名必须使用它们所隶属的package的全名, 比如Compile
函数的名称是regexp.Compile
。
方法必须使用receiver类型的全程, 比如*Regexp
类型的String
方法是regexp.(*Regexp).String
。
被closure引用的变量会有&
前缀。
查看堆栈
查看程序暂停的位置处的堆栈:
1
2
3
4
5
6
7
(lldb) bt
* thread #9: tid = 0x0017, 0x00000000000863a5 regexp.test`_/code/go/src/regexp.TestFind(t=0x000000020834a000) + 37 at find_test.go:149, stop reason = breakpoint 2.1 3.1 5.1
* frame #0 : 0x00000000000863a5 regexp .test`_/code/go/src/regexp.TestFind(t=0x000000020834a000) + 37 at find_test.go:149
frame #1: 0x0000000000056e3f regexp.test`testing.tRunner(t=0x000000000003b671 , test=0x000000020834a000 ) + 191 at testing.go :447
frame #2 : 0x00000000002995a0 regexp .test`/code/go/src/regexp.statictmp_3759 + 96
frame #3: 0x000000000003b671 regexp.test`runtime.goexit + 1 at asm_amd64.s:2232
The stack frame shows we’re currently executing the regexp .TestFind function , as expected.
命令frame variable
会列出这个函数所有的本地变量以及它们的值。但是使用它有点危险,因为它会尝试打印出未初始化的变量。未初始化的slice可能会导致lldb打印出巨大的数组。
函数参数:
1
2
(lldb) frame var -l
(*testing.T) t = 0x000000020834a000
打印这个参数的时候,你会注意到它是一个指向Regexp
的指针。
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
(lldb) p re
(*_/code/go /src/regexp.Regexp) $3 = 0 x000000020834a090
(lldb) p t
(*testing.T) $4 = 0 x000000020834a000
(lldb) p *t
(testing.T) $5 = {
testing.common = {
mu = {
w = (state = 0 , sema = 0 )
writerSem = 0
readerSem = 0
readerCount = 0
readerWait = 0
}
output = (len 0 , cap 0 ) {}
failed = false
skipped = false
finished = false
start = {
sec = 63579066045
nsec = 777400918
loc = 0 x00000000002995a0
}
duration = 0
self = 0 x000000020834a000
signal = 0 x0000000208322060
}
name = "TestFind"
startParallel = 0 x0000000208322240
}
(lldb) p *t.startParallel
(hchan<bool >) $3 = {
qcount = 0
dataqsiz = 0
buf = 0 x0000000208322240
elemsize = 1
closed = 0
elemtype = 0 x000000000014eda0
sendx = 0
recvx = 0
recvq = {
first = 0 x0000000000000000
last = 0 x0000000000000000
}
sendq = {
first = 0 x0000000000000000
last = 0 x0000000000000000
}
lock = (key = 0 x0000000000000000)
}
hchan<bool>
是这个channel的在运行时的内部数据结构。
步进:
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
(lldb) n
(lldb)
(lldb)
Process 17917 stopped
* thread
frame
148 func TestFind(t *testing.T) {
149 for _, test := range findTests {
150 re := MustCompile(test.pat)
-> 151 if re.String() != test.pat {
152 t.Errorf("String() = `%s`; should be `%s`" , re.String(), test.pat)
153 }
154 result := re.Find([]byte(test.text))
(lldb) p test.pat
(string) $4 = ""
(lldb) p re
(*_/code/go/src/regexp.Regexp) $5 = 0 x0000000208354320
(lldb) p *re
(_/code/go/src/regexp.Regexp) $6 = {
expr = ""
prog = 0 x0000000208ac6090
onepass = 0 x0000000000000000
prefix = ""
prefixBytes = (len 0 , cap 0 ) {}
prefixComplete = true
prefixRune = 0
prefixEnd = 0
cond = 0
numSubexp = 0
subexpNames = (len 1 , cap 1 ) {
[0 ] = ""
}
longest = false
mu = (state = 0 , sema = 0 )
machine = (len 0 , cap 0 ) {}
}
(lldb) p *re.prog
(regexp/syntax.Prog) $7 = {
Inst = (len 3 , cap 4 ) {
[0 ] = {
Op = 5
Out = 0
Arg = 0
Rune = (len 0 , cap 0 ) {}
}
[1 ] = {
Op = 6
Out = 2
Arg = 0
Rune = (len 0 , cap 0 ) {}
}
[2 ] = {
Op = 4
Out = 0
Arg = 0
Rune = (len 0 , cap 0 ) {}
}
}
Start = 1
NumCap = 2
}
我们还可以通过s
命令 Step Into
:
1
2
3
4
5
6
7
8
9
10
11
(lldb) s
Process 17917 stopped
* thread #8 : tid = 0x0017 , 0x0000000000067332 regexp. test`_/code/go/src/regexp.(re=0x0000000208354320, ~r0="").String + 18 at regexp.go:104, stop reason = step in
frame #0: 0x0000000000067332 regexp.test`_/code/go/src/regexp. (re= 0x0000000208354320 , ~r0= "" ). String + 18 at regexp. go:104
101
102
103 func (re * Regexp) String () string {
-> 104 return re. expr
105 }
106
107
查看堆栈信息,看看目前我们停在哪儿:
1
2
3
4
5
6
7
(lldb) bt
* thread #8 : tid = 0x0017 , 0x0000000000067332 regexp .test`_/code/go/src/regexp .(re=0x0000000208354320 , ~r0="" ).String + 18 at regexp .go:104 , stop reason = step in
* frame #0 : 0x0000000000067332 regexp .test`_/code/go/src/regexp .(re=0x0000000208354320 , ~r0="" ).String + 18 at regexp .go:104
frame #1 : 0x00000000000864a0 regexp .test`_/code/go/src/regexp .TestFind(t=0x000000020834a000 ) + 288 at find_test.go:151
frame #2 : 0x0000000000056e3f regexp .test`testing.tRunner(t=0x000000000003b671 , test=0x000000020834a000 ) + 191 at testing.go:447
frame #3 : 0x00000000002995a0 regexp .test`/code/go/src/regexp .statictmp_3759 + 96
frame #4 : 0x000000000003b671 regexp .test`runtime.goexit + 1 at asm_amd64.s:2232
其它调试参考文章
Debugging Go code using VS Code
Debugging Go Code with GDB
Debugging Go Code
Debugging Go programs with Delve
debug by Goland
Using the gdb debugger with Go
用 debugger 学习 golang