原文作者:大道至简
我一般调试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"
参数。
1、安装lldb
MacOS下如果你安装了XCode,应该已经安装了LLDB, LLDB是XCode默认的调试器。
Linux/MacOS/Windows下的安装方法可以参考: Installing-LLDB。
2、通用操作
显示文件和行号,设置断点以及反编译:
1(lldb) l
2(lldb) l line
3(lldb) l file.go:line
4(lldb) b line
5(lldb) b file.go:line
6(lldb) disas
显示 backtrace 和 unwind stack frame:
1(lldb) bt
2(lldb) frame n
Show the name, type and location on the stack frame of local variables, arguments and return values:
1(lldb) frame variable
2(lldb) p varname
3(lldb) expr -T -- varname
Go扩展
1、表达式解析
LLDB支持Go表达式:
1(lldb) p x
2(lldb) expr *(*int32)(t)
3(lldb) help expr
2、Interface
默认LLDB显示接口的动态类型。通常它是一个指针, 比如func foo(a interface{}) { ... }
, 如果你调用callfoo(1.0)
, lldb会把a
看作*float64inside
,你也可以禁止为一个表达式禁止这种处理,或者在全局禁用:
1(lldb) expr -d no-dynamic-values -- a
2(lldb) settings set target.prefer-dynamic-values no-dynamic-values
3、Data Formatter
LLDB包含 go string 和 slice的格式化输出器,查看LLDB docs文档学习定制格式化输出。如果你想扩展内建的格式化方式,可以参考GoLanguageRuntime.cpp。
Channel和map被看作引用类型,lldb把它们作为指针类型, 就像C++的类型hash<int,string>*
。Dereferencing会显示类型内部的表示。
4、Goroutine
LLDB 把 Goroutine 看作 thread。
1(lldb) thread list
2(lldb) bt all
3(lldb) thread select 2
5、已知问题
-如果编译时开启优化,调试信息可能是错误的。请确保开启参数 -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
。
1、启动
启动 lldb, 调试 regexp.test:
1$ lldb regexp.test
2(lldb) target create "regexp.test"
3Current executable set to 'regexp.test' (x86_64).
4(lldb)
2、设置断点
在TestFind 函数上设置断点:
1(lldb) b regexp.TestFind
有时候 go编译器会使用全路径为函数名添加前缀,如果你不能使用上面简单的名称,你可以使用正则表达式设置断点:
1(lldb) break set -r regexp.TestFind$
2Breakpoint 5: where = regexp.test`_/code/go/src/regexp.TestFind + 37 at find_test.go:149, address = 0x00000000000863a5
运行程序:
1(lldb) run --test.run=TestFind
2Process 8496 launched: '/code/go/src/regexp/regexp.test' (x86_64)
3Process 8496 stopped
4* 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
5 frame #0: 0x00000000000863a5 regexp.test`_/code/go/src/regexp.TestFind(t=0x000000020834a000) + 37 at find_test.go:149
6 146 // First the simple cases.
7 147
8 148 func TestFind(t *testing.T) {
9-> 149 for _, test := range findTests {
10 150 re := MustCompile(test.pat)
11 151 if re.String() != test.pat {
12 152 t.Errorf("String() = `%s`; should be `%s`", re.String(), test.pat)
程序会运行到设置的断点上,查看运行的goroutine以及它们在做什么:
1(lldb) thread list
2Process 8496 stopped
3 thread #1: tid = 0x12201, 0x000000000003c0ab regexp.test`runtime.mach_semaphore_wait + 11 at sys_darwin_amd64.s:412
4 thread #2: tid = 0x122fa, 0x000000000003bf7c regexp.test`runtime.usleep + 44 at sys_darwin_amd64.s:290
5 thread #4: tid = 0x0001, 0x0000000000015865 regexp.test`runtime.gopark(unlockf=0x00000000000315a0, lock=0x00000002083220b8, reason="chan receive") + 261 at proc.go:131
6 thread #5: tid = 0x0002, 0x0000000000015865 regexp.test`runtime.gopark(unlockf=0x00000000000315a0, lock=0x00000000002990d0, reason="force gc (idle)") + 261 at proc.go:131
7 thread #6: tid = 0x0003, 0x0000000000015754 regexp.test`runtime.Gosched + 20 at proc.go:114
8 thread #7: tid = 0x0004, 0x0000000000015865 regexp.test`runtime.gopark(unlockf=0x00000000000315a0, lock=0x00000000002a07d8, reason="finalizer wait") + 261 at proc.go:131
9* 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。
3、查看代码
使用l
或者list
查看代码, #
重复最后的命令:
1(lldb) l
2(lldb) # Hit enter to repeat last command. Here, list the next few lines
4、命名
变量和函数名必须使用它们所隶属的package的全名, 比如Compile
函数的名称是regexp.Compile
。
方法必须使用receiver类型的全程, 比如*Regexp
类型的String
方法是regexp.(*Regexp).String
。
被closure引用的变量会有&
前缀。
5、查看堆栈
查看程序暂停的位置处的堆栈:
1(lldb) bt
2* 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
3 * frame #0: 0x00000000000863a5 regexp.test`_/code/go/src/regexp.TestFind(t=0x000000020834a000) + 37 at find_test.go:149
4 frame #1: 0x0000000000056e3f regexp.test`testing.tRunner(t=0x000000000003b671, test=0x000000020834a000) + 191 at testing.go:447
5 frame #2: 0x00000000002995a0 regexp.test`/code/go/src/regexp.statictmp_3759 + 96
6 frame #3: 0x000000000003b671 regexp.test`runtime.goexit + 1 at asm_amd64.s:2232
7The stack frame shows we’re currently executing the regexp.TestFind function, as expected.
命令frame variable
会列出这个函数所有的本地变量以及它们的值。但是使用它有点危险,因为它会尝试打印出未初始化的变量。未初始化的slice可能会导致lldb打印出巨大的数组。
函数参数:
1(lldb) frame var -l
2(*testing.T) t = 0x000000020834a000
打印这个参数的时候,你会注意到它是一个指向Regexp
的指针。
1(lldb) p re
2(*_/code/go/src/regexp.Regexp) $3 = 0x000000020834a090
3(lldb) p t
4(*testing.T) $4 = 0x000000020834a000
5(lldb) p *t
6(testing.T) $5 = {
7 testing.common = {
8 mu = {
9 w = (state = 0, sema = 0)
10 writerSem = 0
11 readerSem = 0
12 readerCount = 0
13 readerWait = 0
14 }
15 output = (len 0, cap 0) {}
16 failed = false
17 skipped = false
18 finished = false
19 start = {
20 sec = 63579066045
21 nsec = 777400918
22 loc = 0x00000000002995a0
23 }
24 duration = 0
25 self = 0x000000020834a000
26 signal = 0x0000000208322060
27 }
28 name = "TestFind"
29 startParallel = 0x0000000208322240
30}
31(lldb) p *t.startParallel
32(hchan<bool>) $3 = {
33 qcount = 0
34 dataqsiz = 0
35 buf = 0x0000000208322240
36 elemsize = 1
37 closed = 0
38 elemtype = 0x000000000014eda0
39 sendx = 0
40 recvx = 0
41 recvq = {
42 first = 0x0000000000000000
43 last = 0x0000000000000000
44 }
45 sendq = {
46 first = 0x0000000000000000
47 last = 0x0000000000000000
48 }
49 lock = (key = 0x0000000000000000)
50}
hchan<bool>
是这个channel的在运行时的内部数据结构。
步进:
1(lldb) n # execute next line
2(lldb) # enter is repeat
3(lldb) # enter is repeat
4Process 17917 stopped
5* thread #8: tid = 0x0017, 0x000000000008648f regexp.test`_/code/go/src/regexp.TestFind(t=0x000000020834a000) + 271 at find_test.go:151, stop reason = step over
6 frame #0: 0x000000000008648f regexp.test`_/code/go/src/regexp.TestFind(t=0x000000020834a000) + 271 at find_test.go:151
7 148 func TestFind(t *testing.T) {
8 149 for _, test := range findTests {
9 150 re := MustCompile(test.pat)
10-> 151 if re.String() != test.pat {
11 152 t.Errorf("String() = `%s`; should be `%s`", re.String(), test.pat)
12 153 }
13 154 result := re.Find([]byte(test.text))
14(lldb) p test.pat
15(string) $4 = ""
16(lldb) p re
17(*_/code/go/src/regexp.Regexp) $5 = 0x0000000208354320
18(lldb) p *re
19(_/code/go/src/regexp.Regexp) $6 = {
20 expr = ""
21 prog = 0x0000000208ac6090
22 onepass = 0x0000000000000000
23 prefix = ""
24 prefixBytes = (len 0, cap 0) {}
25 prefixComplete = true
26 prefixRune = 0
27 prefixEnd = 0
28 cond = 0
29 numSubexp = 0
30 subexpNames = (len 1, cap 1) {
31 [0] = ""
32 }
33 longest = false
34 mu = (state = 0, sema = 0)
35 machine = (len 0, cap 0) {}
36}
37(lldb) p *re.prog
38(regexp/syntax.Prog) $7 = {
39 Inst = (len 3, cap 4) {
40 [0] = {
41 Op = 5
42 Out = 0
43 Arg = 0
44 Rune = (len 0, cap 0) {}
45 }
46 [1] = {
47 Op = 6
48 Out = 2
49 Arg = 0
50 Rune = (len 0, cap 0) {}
51 }
52 [2] = {
53 Op = 4
54 Out = 0
55 Arg = 0
56 Rune = (len 0, cap 0) {}
57 }
58 }
59 Start = 1
60 NumCap = 2
61}
我们还可以通过s
命令 Step Into
:
1(lldb) s
2Process 17917 stopped
3* thread #8: tid = 0x0017, 0x0000000000067332 regexp.test`_/code/go/src/regexp.(re=0x0000000208354320, ~r0="").String + 18 at regexp.go:104, stop reason = step in
4 frame #0: 0x0000000000067332 regexp.test`_/code/go/src/regexp.(re=0x0000000208354320, ~r0="").String + 18 at regexp.go:104
5 101
6 102 // String returns the source text used to compile the regular expression.
7 103 func (re *Regexp) String() string {
8-> 104 return re.expr
9 105 }
10 106
11 107 // Compile parses a regular expression and returns, if successful,
查看堆栈信息,看看目前我们停在哪儿:
1(lldb) bt
2* thread #8: tid = 0x0017, 0x0000000000067332 regexp.test`_/code/go/src/regexp.(re=0x0000000208354320, ~r0="").String + 18 at regexp.go:104, stop reason = step in
3 * frame #0: 0x0000000000067332 regexp.test`_/code/go/src/regexp.(re=0x0000000208354320, ~r0="").String + 18 at regexp.go:104
4 frame #1: 0x00000000000864a0 regexp.test`_/code/go/src/regexp.TestFind(t=0x000000020834a000) + 288 at find_test.go:151
5 frame #2: 0x0000000000056e3f regexp.test`testing.tRunner(t=0x000000000003b671, test=0x000000020834a000) + 191 at testing.go:447
6 frame #3: 0x00000000002995a0 regexp.test`/code/go/src/regexp.statictmp_3759 + 96
7 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
版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢。
Golang语言社区
ID:GolangWeb
www.ByteEdu.Com
游戏服务器架构丨分布式技术丨大数据丨游戏算法学习