以下说明适用于标准工具链(即 gc Go 编译器及工具集)。
Gccgo 原生支持 GDB 调试。
请注意,当调试使用标准工具链构建的 Go 程序时,
Delve 是比 GDB 更好的替代方案。
它比 GDB 更能理解 Go 运行时、数据结构和表达式。
Delve 目前支持在 amd64 架构的 Linux、OSX 和 Windows 上使用。
有关支持平台的最新列表,请参阅
Delve 文档。
GDB 对 Go 程序的理解不够好。 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 来在程序崩溃时触发转储(更多信息请参阅
runtime 包文档)。
常见操作
-
显示代码的文件和行号,设置断点以及反汇编:
(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,增加了几个命令来检查运行时代码的内部(如 goroutine),并美观地打印内置的 map、slice 和 channel 类型。
-
美观地打印 string、slice、map、channel 或 interface:
(gdb) p var
-
针对 string、slice 和 map 的 $len() 和 $cap() 函数:
(gdb) p $len(var)
-
将接口转换为其动态类型的函数:
(gdb) p $dtype(var) (gdb) iface var
已知问题: 如果接口值的长名称与短名称不同,GDB 无法自动找到其动态类型(这在打印堆栈跟踪时很烦人,美观打印器会回退到打印短类型名称和一个指针)。
-
检查 goroutine:
(gdb) info goroutines (gdb) goroutine n cmd (gdb) help goroutine
例如:(gdb) goroutine 12 bt
您可以通过传递all来检查所有 goroutine,而不是指定某个 goroutine 的 ID。 例如:(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 仍然找不到该目录或该脚本,你可以手动加载它(假设 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) # 按回车键重复上一个命令。这里,列出接下来的 10 行。
命名
变量和函数名必须用它们所属的包名来限定。
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) {
执行已在断点处暂停。 查看哪些协程正在运行,以及它们正在做什么:
(gdb) info goroutines 1 waiting runtime.gosched * 13 running runtime.goexit
标有 * 的是当前协程。
检查调用栈
查看程序暂停位置的栈追踪:
(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 ?? ()
另一个协程,编号 1,卡在 runtime.gosched 中,在一个通道接收操作上阻塞:
(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错误地将 * 放在了类型名称的右侧,并模仿传统C风格添加了'struct'关键字。
(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 # 执行下一行
149 for _, test := range findTests {
(gdb) # 回车表示重复
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>*。解引用这些指针将触发美化打印。
接口在运行时表示为一个指向类型描述符(type descriptor)的指针和一个指向值的指针。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 *