使用 GDB 调试 Go 代码
以下说明适用于标准工具链(gc
Go 编译器和工具)。Gccgo 原生支持 gdb。
请注意,当调试使用标准工具链构建的 Go 程序时,Delve 是 GDB 更好的替代品。它比 GDB 更了解 Go 运行时、数据结构和表达式。Delve 目前支持 amd64
架构上的 Linux、OSX 和 Windows。有关支持平台的最新列表,请参阅 Delve 文档。
GDB 对 Go 程序理解不佳。其堆栈管理、线程和运行时包含的方面与 GDB 预期的执行模型有足够的差异,这可能会使调试器混淆,即使程序是用 gccgo 编译的,也可能导致不正确的结果。因此,尽管 GDB 在某些情况下可能有用(例如,调试 Cgo 代码或调试运行时本身),但它不是 Go 程序的可靠调试器,特别是对于高度并发的程序。此外,解决这些困难的问题并不是 Go 项目的优先事项。
简而言之,以下说明仅应作为 GDB 在工作时如何使用的指南,而不是成功的保证。除了这份概述,您可能还需要查阅 GDB 手册。
引言
当您在 Linux、macOS、FreeBSD 或 NetBSD 上使用 gc
工具链编译和链接 Go 程序时,生成的二进制文件包含 DWARFv4 调试信息,最新版本(≥7.5)的 GDB 调试器可以使用该信息检查实时进程或核心转储。
将 '-w'
标志传递给链接器以省略调试信息(例如,go
build
-ldflags=-w
prog.go
)。
gc
编译器生成的代码包括函数调用的内联和变量的寄存器化。这些优化有时会使使用 gdb
进行调试变得更加困难。如果您发现需要禁用这些优化,请使用 go
build
-gcflags=all="-N -l"
构建程序。
如果您想使用 gdb 检查核心转储,您可以通过在环境中设置 GOTRACEBACK=crash
来在程序崩溃时触发转储,在允许的系统上(有关更多信息,请参阅运行时包文档)。
常用操作
- 显示代码的文件和行号,设置断点和反汇编
(gdb) list (gdb) list line (gdb) list file.go:line (gdb) break line (gdb) break file.go:line (gdb) disas
- 显示回溯和展开堆栈帧
(gdb) bt (gdb) frame n
- 显示局部变量、参数和返回值的名称、类型和在堆栈帧上的位置
(gdb) info locals (gdb) info args (gdb) p variable (gdb) whatis variable
- 显示全局变量的名称、类型和位置
(gdb) info variables regexp
Go 扩展
GDB 最近的扩展机制允许它为给定二进制文件加载扩展脚本。工具链使用此机制通过一些命令扩展 GDB,以检查运行时代码的内部(例如 goroutines)并漂亮地打印内置的 map、slice 和 channel 类型。
- 漂亮地打印字符串、切片、map、channel 或接口
(gdb) p var
- 字符串、切片和 map 的 $len() 和 $cap() 函数
(gdb) p $len(var)
- 将接口强制转换为其动态类型的函数
(gdb) p $dtype(var) (gdb) iface var
已知问题:如果接口值的长名称与其短名称不同,GDB 无法自动找到其动态类型(在打印堆栈跟踪时很烦人,漂亮打印器会回退到打印短类型名称和指针)。
- 检查 goroutines
(gdb) info goroutines (gdb) goroutine n cmd (gdb) help goroutine
例如(gdb) goroutine 12 bt
您可以通过传入all
而不是特定的 goroutine ID 来检查所有 goroutines。例如(gdb) goroutine all bt
如果您想了解其工作原理,或者想扩展它,请查看 Go 源代码分发中的 src/runtime/runtime-gdb.py。它依赖于一些特殊的魔法类型(hash<T,U>
)和变量(runtime.m
和 runtime.g
),链接器(src/cmd/link/internal/ld/dwarf.go)确保这些在 DWARF 代码中有所描述。
如果您对调试信息的外观感兴趣,请运行 objdump
-W
a.out
并浏览 .debug_*
部分。
已知问题
- 字符串漂亮打印仅对类型 string 触发,不对从其派生的类型触发。
- 运行时库的 C 部分缺少类型信息。
- GDB 不理解 Go 的名称限定,并将
"fmt.Print"
视为一个非结构化的字面量,其中包含一个需要用引号引起来的"."
。它对pkg.(*MyType).Meth
形式的方法名称更是强烈反对。 - 自 Go 1.11 起,调试信息默认是压缩的。较旧版本的 gdb(例如 MacOS 上默认可用的版本)无法理解这种压缩。您可以使用
go build -ldflags=-compressdwarf=false
生成未压缩的调试信息。(为方便起见,您可以将-ldflags
选项放在GOFLAGS
环境变量中,这样就不必每次都指定它。)
教程
在本教程中,我们将检查 regexp 包的单元测试的二进制文件。要构建二进制文件,请切换到 $GOROOT/src/regexp
并运行 go
test
-c
。这将生成一个名为 regexp.test
的可执行文件。
入门
启动 GDB,调试 regexp.test
$ gdb regexp.test GNU gdb (GDB) 7.2-gg8 Copyright (C) 2010 Free Software Foundation, Inc. License GPLv 3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> Type "show copying" and "show warranty" for licensing/warranty details. This GDB was configured as "x86_64-linux". Reading symbols from /home/user/go/src/regexp/regexp.test... done. Loading Go Runtime support. (gdb)
消息 "Loading Go Runtime support" 意味着 GDB 从 $GOROOT/src/runtime/runtime-gdb.py
加载了扩展。
为了帮助 GDB 找到 Go 运行时源和附带的支持脚本,请使用 '-d'
标志传递您的 $GOROOT
$ gdb regexp.test -d $GOROOT
如果由于某种原因 GDB 仍然找不到该目录或脚本,您可以手动加载它,方法是告诉 gdb(假设您的 go 源位于 ~/go/
)
(gdb) source ~/go/src/runtime/runtime-gdb.py Loading Go Runtime support.
检查源文件
使用 "l"
或 "list"
命令检查源代码。
(gdb) l
通过函数名(必须用其包名限定)参数化 "list"
来列出源代码的特定部分。
(gdb) l main.main
列出特定文件和行号
(gdb) l regexp.go:1 (gdb) # Hit enter to repeat last command. Here, this lists next 10 lines.
命名
变量和函数名必须用它们所属的包名限定。regexp
包中的 Compile
函数在 GDB 中被称为 'regexp.Compile'
。
方法必须用其接收器类型的名称限定。例如,*Regexp
类型的 String
方法被称为 'regexp.(*Regexp).String'
。
遮蔽其他变量的变量在调试信息中会神奇地加上一个数字后缀。闭包引用的变量将显示为神奇地以 '&' 为前缀的指针。
设置断点
在 TestFind
函数处设置断点
(gdb) b 'regexp.TestFind' Breakpoint 1 at 0x424908: file /home/user/go/src/regexp/find_test.go, line 148.
运行程序
(gdb) run Starting program: /home/user/go/src/regexp/regexp.test Breakpoint 1, regexp.TestFind (t=0xf8404a89c0) at /home/user/go/src/regexp/find_test.go:148 148 func TestFind(t *testing.T) {
执行已在断点处暂停。查看正在运行的 goroutines,以及它们正在做什么
(gdb) info goroutines 1 waiting runtime.gosched * 13 running runtime.goexit
标有 *
的是当前 goroutine。
检查堆栈
查看我们暂停程序时的堆栈跟踪
(gdb) bt # backtrace #0 regexp.TestFind (t=0xf8404a89c0) at /home/user/go/src/regexp/find_test.go:148 #1 0x000000000042f60b in testing.tRunner (t=0xf8404a89c0, test=0x573720) at /home/user/go/src/testing/testing.go:156 #2 0x000000000040df64 in runtime.initdone () at /home/user/go/src/runtime/proc.c:242 #3 0x000000f8404a89c0 in ?? () #4 0x0000000000573720 in ?? () #5 0x0000000000000000 in ?? ()
另一个 goroutine,编号 1,卡在 runtime.gosched
中,阻塞在 channel 接收上
(gdb) goroutine 1 bt #0 0x000000000040facb in runtime.gosched () at /home/user/go/src/runtime/proc.c:873 #1 0x00000000004031c9 in runtime.chanrecv (c=void, ep=void, selected=void, received=void) at /home/user/go/src/runtime/chan.c:342 #2 0x0000000000403299 in runtime.chanrecv1 (t=void, c=void) at/home/user/go/src/runtime/chan.c:423 #3 0x000000000043075b in testing.RunTests (matchString={void (struct string, struct string, bool *, error *)} 0x7ffff7f9ef60, tests= []testing.InternalTest = {...}) at /home/user/go/src/testing/testing.go:201 #4 0x00000000004302b1 in testing.Main (matchString={void (struct string, struct string, bool *, error *)} 0x7ffff7f9ef80, tests= []testing.InternalTest = {...}, benchmarks= []testing.InternalBenchmark = {...}) at /home/user/go/src/testing/testing.go:168 #5 0x0000000000400dc1 in main.main () at /home/user/go/src/regexp/_testmain.go:98 #6 0x00000000004022e7 in runtime.mainstart () at /home/user/go/src/runtime/amd64/asm.s:78 #7 0x000000000040ea6f in runtime.initdone () at /home/user/go/src/runtime/proc.c:243 #8 0x0000000000000000 in ?? ()
堆栈帧显示我们当前正在执行 regexp.TestFind
函数,正如预期。
(gdb) info frame Stack level 0, frame at 0x7ffff7f9ff88: rip = 0x425530 in regexp.TestFind (/home/user/go/src/regexp/find_test.go:148); saved rip 0x430233 called by frame at 0x7ffff7f9ffa8 source language minimal. Arglist at 0x7ffff7f9ff78, args: t=0xf840688b60 Locals at 0x7ffff7f9ff78, Previous frame's sp is 0x7ffff7f9ff88 Saved registers: rip at 0x7ffff7f9ff80
命令 info
locals
列出函数的所有局部变量及其值,但使用起来有点危险,因为它还会尝试打印未初始化的变量。未初始化的切片可能导致 gdb 尝试打印任意大的数组。
函数的参数
(gdb) info args t = 0xf840688b60
打印参数时,请注意它是一个指向 Regexp
值的指针。请注意,GDB 错误地将 *
放在类型名称的右侧,并编造了一个“struct”关键字,采用传统的 C 风格。
(gdb) p re (gdb) p t $1 = (struct testing.T *) 0xf840688b60 (gdb) p t $1 = (struct testing.T *) 0xf840688b60 (gdb) p *t $2 = {errors = "", failed = false, ch = 0xf8406f5690} (gdb) p *t->ch $3 = struct hchan<*testing.T>
那个 struct
hchan<*testing.T>
是 channel 的运行时内部表示。它当前为空,否则 gdb 会漂亮地打印其内容。
向前单步执行
(gdb) n # execute next line 149 for _, test := range findTests { (gdb) # enter is repeat 150 re := MustCompile(test.pat) (gdb) p test.pat $4 = "" (gdb) p re $5 = (struct regexp.Regexp *) 0xf84068d070 (gdb) p *re $6 = {expr = "", prog = 0xf840688b80, prefix = "", prefixBytes = []uint8, prefixComplete = true, prefixRune = 0, cond = 0 '\000', numSubexp = 0, longest = false, mu = {state = 0, sema = 0}, machine = []*regexp.machine} (gdb) p *re->prog $7 = {Inst = []regexp/syntax.Inst = {{Op = 5 '\005', Out = 0, Arg = 0, Rune = []int}, {Op = 6 '\006', Out = 2, Arg = 0, Rune = []int}, {Op = 4 '\004', Out = 0, Arg = 0, Rune = []int}}, Start = 1, NumCap = 2}
我们可以使用 "s"
进入 String
函数调用
(gdb) s regexp.(*Regexp).String (re=0xf84068d070, noname=void) at /home/user/go/src/regexp/regexp.go:97 97 func (re *Regexp) String() string {
获取堆栈跟踪以查看我们所在的位置
(gdb) bt #0 regexp.(*Regexp).String (re=0xf84068d070, noname=void) at /home/user/go/src/regexp/regexp.go:97 #1 0x0000000000425615 in regexp.TestFind (t=0xf840688b60) at /home/user/go/src/regexp/find_test.go:151 #2 0x0000000000430233 in testing.tRunner (t=0xf840688b60, test=0x5747b8) at /home/user/go/src/testing/testing.go:156 #3 0x000000000040ea6f in runtime.initdone () at /home/user/go/src/runtime/proc.c:243 ....
查看源代码
(gdb) l 92 mu sync.Mutex 93 machine []*machine 94 } 95 96 // String returns the source text used to compile the regular expression. 97 func (re *Regexp) String() string { 98 return re.expr 99 } 100 101 // Compile parses a regular expression and returns, if successful,
漂亮打印
GDB 的漂亮打印机制由类型名称上的正则表达式匹配触发。切片的一个例子
(gdb) p utf $22 = []uint8 = {0 '\000', 0 '\000', 0 '\000', 0 '\000'}
由于切片、数组和字符串不是 C 指针,GDB 无法为您解释下标操作,但您可以查看运行时表示以完成此操作(Tab 补全在此处有所帮助)
(gdb) p slc $11 = []int = {0, 0} (gdb) p slc-><TAB> array slc len (gdb) p slc->array $12 = (int *) 0xf84057af00 (gdb) p slc->array[1] $13 = 0
扩展函数 $len 和 $cap 适用于字符串、数组和切片
(gdb) p $len(utf) $23 = 4 (gdb) p $cap(utf) $24 = 4
Channel 和 map 是“引用”类型,gdb 将它们显示为指向 C++ 风格类型 hash<int,string>*
的指针。解引用将触发漂亮打印
接口在运行时表示为指向类型描述符的指针和指向值的指针。Go GDB 运行时扩展会解码此信息并自动触发运行时类型的漂亮打印。扩展函数 $dtype
为您解码动态类型(示例取自 regexp.go
第 293 行的断点)。
(gdb) p i $4 = {str = "cbb"} (gdb) whatis i type = regexp.input (gdb) p $dtype(i) $26 = (struct regexp.inputBytes *) 0xf8400b4930 (gdb) iface i regexp.input: struct regexp.inputBytes *