使用 GDB 调试 Go 代码

以下说明适用于标准工具链(gc Go 编译器和工具)。Gccgo 具有本机 gdb 支持。

请注意,Delve 是使用标准工具链构建的 Go 程序调试的更好的替代方案。它比 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 调试信息,GDB 调试器的最新版本(≥7.5)可以使用这些信息来检查实时进程或核心转储。

'-w' 标志传递给链接器以省略调试信息(例如,go build -ldflags=-w prog.go)。

gc 编译器生成的代码包括函数调用的内联和变量的寄存器化。这些优化有时会让使用 gdb 调试变得更加困难。如果您发现需要禁用这些优化,请使用 go build -gcflags=all="-N -l" 构建您的程序。

如果您想使用 gdb 检查核心转储,可以在允许的系统上通过在环境中设置 GOTRACEBACK=crash 来触发程序崩溃时的转储(有关更多信息,请参阅 运行时包文档)。

常见操作

Go 扩展

GDB 的一个最新扩展机制允许其为给定的二进制文件加载扩展脚本。工具链使用此机制通过一些命令扩展 GDB,以检查运行时代码的内部(例如 goroutine),并美化打印内置的 map、slice 和 channel 类型。

如果您想了解其工作原理或想对其进行扩展,请查看 Go 源代码分发中的 src/runtime/runtime-gdb.py。它依赖于某些特殊的魔术类型(hash<T,U>)和变量(runtime.mruntime.g),链接器 (src/cmd/link/internal/ld/dwarf.go) 确保在 DWARF 代码中对它们进行了描述。

如果您有兴趣了解调试信息的样貌,请运行 objdump -W a.out 并浏览 .debug_* 部分。

已知问题

  1. 字符串美化打印仅适用于字符串类型,不适用于派生类型。
  2. 运行时库的 C 部分缺少类型信息。
  3. GDB 不理解 Go 的名称限定,并将 "fmt.Print" 视为带有需要加引号的 "." 的非结构化字面量。它更强烈地反对形式为 pkg.(*MyType).Meth 的方法名。
  4. 从 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)

消息“正在加载 Go 运行时支持”表示 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) {

执行已在断点处暂停。查看正在运行的协程以及它们在做什么

(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> 是通道的运行时内部表示。它当前为空,否则 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 无法为您解释下标操作,但您可以查看运行时表示来执行此操作(在此处,制表符补全很有帮助)

(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

通道和映射是“引用”类型,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 *