我一般调试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 -- varname
|
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 // First the simple cases. 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 = 0x000000020834a090 (lldb) p t (*testing.T) $4 = 0x000000020834a000 (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 = 0x00000000002995a0 } duration = 0 self = 0x000000020834a000 signal = 0x0000000208322060 } name = "TestFind" startParallel = 0x0000000208322240 } (lldb) p *t.startParallel (hchan<bool>) $3 = { qcount = 0 dataqsiz = 0 buf = 0x0000000208322240 elemsize = 1 closed = 0 elemtype = 0x000000000014eda0 sendx = 0 recvx = 0 recvq = { first = 0x0000000000000000 last = 0x0000000000000000 } sendq = { first = 0x0000000000000000 last = 0x0000000000000000 } lock = (key = 0x0000000000000000) }
|
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 # execute next line (lldb) # enter is repeat (lldb) # enter is repeat Process 17917 stopped * thread #8: tid = 0x0017, 0x000000000008648f regexp.test`_/code/go/src/regexp.TestFind(t=0x000000020834a000) + 271 at find_test.go:151, stop reason = step over frame #0: 0x000000000008648f regexp.test`_/code/go/src/regexp.TestFind(t=0x000000020834a000) + 271 at find_test.go:151 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 = 0x0000000208354320 (lldb) p *re (_/code/go/src/regexp.Regexp) $6 = { expr = "" prog = 0x0000000208ac6090 onepass = 0x0000000000000000 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 // String returns the source text used to compile the regular expression. 103 func (re *Regexp) String() string { -> 104 return re.expr 105 } 106 107 // Compile parses a regular expression and returns, if successful,
|
查看堆栈信息,看看目前我们停在哪儿:
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