[使用 LLDB 调试 Go 程序
原文作者:大道至简
我一般调试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"
3 Current executable set to 'regexp.test' (x86_64).
4 (lldb)
2、设置断点
在TestFind 函数上设置断点:
1 (lldb) b regexp.TestFind
有时候 go编译器会使用全路径为函数名添加前缀,如果你不能使用上面简单的名称,你可以使用正则表达式设置断点:
1 (lldb) break set -r regexp.TestFind$
2 Breakpoint 5: where = regexp.test`_/code/go/src/regexp.TestFind + 37 at find_test.go:149, address = 0x00000000000863a5
运行程序:
1 (lldb) run --test.run=TestFind
2 Process 8496 launched: '/code/go/src/regexp/regexp.test' (x86_64)
3 Process 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
2 Process 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
7 The 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
4 Process 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
2 Process 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
版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢。
推荐阅读
-
使用 GDB 调试 ROS2 程序
-
使用 WebStorm 调试现代前端应用程序
-
使用 go 编写简单的看门狗程序 (WatchDog)
-
[Golang 星图]实施弹性微服务架构:使用 go-micro 和 go-kit 构建可扩展的网络应用程序
-
.NET高级面试指南 Topic XVIII [ 介绍外观模式(Appearance Pattern),该模式提供了一个隐藏系统复杂性的简化界面 ]。- 简化复杂系统:当系统具有复杂的子系统结构时,可以使用外观模式来简化界面。提供统一界面:当客户端需要访问多个子系统时,可以使用外观模式提供统一界面。 外观模式在现代软件开发中得到广泛应用,尤其是在复杂系统中。例如 图形用户界面库:许多图形用户界面库(如 Qt、GTK+ 等)都使用外观模式来隐藏底层的复杂性,并为开发人员提供简单的界面来创建用户界面。 操作系统接口:操作系统中的系统调用和应用程序接口通常也使用外观模式来隐藏底层硬件和系统的复杂性,为应用程序提供访问系统资源的简单接口。企业应用程序:在可能涉及多个子系统的大型企业应用程序中,外观模式可用于封装这些子系统,并为客户端提供统一的使用界面。 网络框架:许多网络框架(如 ASP.NET MVC、Spring MVC 等)也使用外观模式来隐藏底层的复杂性,并为开发人员提供简单的接口来处理 HTTP 请求和响应。 集成开发环境(IDE):集成开发环境通常包含代码编辑器、编译器、调试器等多种功能。外观模式可用于封装这些功能,并为开发人员提供开发软件的简单界面。 代码示例:
-
使用 kubectl port-forward 端口转发功能快速调试应用程序
-
Java 类加载器的作用 - 简介:类加载器是 Java™ 中一个非常重要的概念。类加载器负责将 Java 类的字节码加载到 Java 虚拟机中。本文首先详细介绍了 Java 类加载器的基本概念,包括代理模型、加载类的具体过程和线程上下文类加载器等。然后介绍了如何开发自己的类加载器,最后介绍了类加载器在 Web 容器和 OSGi™ 中的应用。 类加载器是 Java 语言的一项创新,也是 Java 语言广受欢迎的重要原因之一。它允许将 Java 类动态加载到 Java 虚拟机中并执行。类加载器从 JDK 1.0 开始出现,最初是为了满足 Java Applets 的需求而开发的,Java Applets 需要从远程位置下载 Java 类文件并在浏览器中执行。现在,类加载器已广泛应用于网络容器和 OSGi。一般来说,Java 应用程序的开发人员不需要直接与类加载器交互;Java 虚拟机的默认行为足以应对大多数情况。但是,如果遇到需要与类加载器交互的情况,而您又不太了解类加载器的机制,就很容易花费大量时间调试异常,如 ClassNotFoundException 和 NoClassDefFoundError。本文将详细介绍 Java 的类加载器,帮助读者深入理解 Java 语言中的这一重要概念。下面先介绍一些基本概念。 类加载器的基本概念 顾名思义,类加载器用于将 Java 类加载到 Java 虚拟机中。一般来说,Java 虚拟机以如下方式使用 Java 类:Java 源程序(.java 文件)经 Java 编译器编译后转换为 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码并将其转换为 java.lang 实例。每个实例都用来表示一个 Java 类。通过该实例的 newInstance 方法创建该类的对象。实际情况可能更加复杂,例如,Java 字节代码可能是由工具动态生成或通过网络下载的。 基本上,所有类加载器都是 java.lang.ClassLoader 类的实例。下面将详细介绍这个 Java 类。 java.lang.ClassLoader 类简介 java.lang.ClassLoader 类的基本职责是根据给定类的名称为其查找或生成相应的字节码,然后根据这些字节码定义一个 Java 类,即 java.lang.Class 类的实例。除此之外,ClassLoader 还负责加载 Java 应用程序所需的资源,如图像文件和配置文件。不过,本文只讨论它加载类的功能。为了履行加载类的职责,ClassLoader 提供了许多方法,其中比较重要的方法如表 1 所示。下文将详细介绍这些方法。 表 1.与加载类相关的 ClassLoader 方法
-
ActiViz(VTK C#库)学习使用心得体会三:C# 形式下的应用程序 VTK 调试与开发环境构建 - 一、实现可视化控件 RenderWindowControl
-
使用 Python 脚本增强 LLDB 调试器
-
在 Ubuntu 18.04 上使用 LLDB 调试 Chromium Android C++ 代码