使用 LLDB 调试 Go 程序

发布于 2024-01-10 09:01:55 字数 12619 浏览 59 评论 0

我一般调试 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

通用操作

(lldb) l
(lldb) l line
(lldb) l file.go:line
(lldb) b line
(lldb) b file.go:line
(lldb) disas

显示 backtrace 和 unwind stack frame:

(lldb) bt
(lldb) frame n

Show the name, type and location on the stack frame of local variables, arguments and return values:

(lldb) frame variable
(lldb) p varname
(lldb) expr -T -- varname

Go 扩展

表达式解析

LLDB 支持 Go 表达式:

(lldb) p x
(lldb) expr *(*int32)(t)
(lldb) help expr

Interface

默认 LLDB 显示接口的动态类型。通常它是一个指针, 比如 func foo(a interface{}) { ... } , 如果你调用 callfoo(1.0) , lldb 会把 a 看作 *float64inside ,你也可以禁止为一个表达式禁止这种处理,或者在全局禁用:

(lldb) expr -d no-dynamic-values -- a
(lldb) settings set target.prefer-dynamic-values no-dynamic-values

Data Formatter

LLDB 包含 go string 和 slice 的格式化输出器,查看 LLDB docs 文档学习定制格式化输出。如果你想扩展内建的格式化方式,可以参考 GoLanguageRuntime.cpp

Channel 和 map 被看作引用类型,lldb 把它们作为指针类型, 就像 C++的类型 hash<int,string>* 。Dereferencing 会显示类型内部的表示。

Goroutine

LLDB 把 Goroutine 看作 thread。

(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:

$ lldb regexp.test
(lldb) target create "regexp.test"
Current executable set to 'regexp.test' (x86_64).
(lldb)

设置断点

在 TestFind 函数上设置断点:

(lldb) b regexp.TestFind

有时候 go 编译器会使用全路径为函数名添加前缀,如果你不能使用上面简单的名称,你可以使用正则表达式设置断点:

(lldb) break set -r regexp.TestFind$
Breakpoint 5: where = regexp.test`_/code/go/src/regexp.TestFind + 37 at find_test.go:149, address = 0x00000000000863a5

运行程序:

(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 以及它们在做什么:

(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 查看代码, # 重复最后的命令:

(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 引用的变量会有 & 前缀。

查看堆栈

查看程序暂停的位置处的堆栈:

(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 打印出巨大的数组。

函数参数:

(lldb) frame var -l
(*testing.T) t = 0x000000020834a000

打印这个参数的时候,你会注意到它是一个指向 Regexp 的指针。

(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 的在运行时的内部数据结构。

步进:

(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 :

(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,

查看堆栈信息,看看目前我们停在哪儿:

(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

其它调试参考文章

  1. Debugging Go code using VS Code
  2. Debugging Go Code with GDB
  3. Debugging Go Code
  4. Debugging Go programs with Delve
  5. debug by Goland
  6. Using the gdb debugger with Go
  7. 用 debugger 学习 golang

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

夜血缘

暂无简介

0 文章
0 评论
1014 人气
更多

推荐作者

qq_E2Iff7

文章 0 评论 0

Archangel

文章 0 评论 0

freedog

文章 0 评论 0

Hunk

文章 0 评论 0

18819270189

文章 0 评论 0

wenkai

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文