1 简介
在Go程序中可能需要使用调试,golang的汇编基于Plan 9风格,包含PC、FP、SP、SB等伪寄存器。
调试工具dlv可用于Go程序的调试,安装可通过go install github.com/go-delve/delve/cmd/dlv@latest
。
启动调试会话如dlv debug main.go
,设置断点break main.main
,调试测试用dlv test
。
2 使用汇编和调试工具 dlv
在中大型程序中,部分汇编和调试工作可以给我们提供。
汇编简介
go语言自带了汇编模式cgo,用于调用其他c库和在某些场景调试程序。
官方文档:
golang.org/doc/asm
go汇编需要在go包中使用,必须指明当前包名等信息。
汇编代码中定义的变量和函数要被其他Go语言引用,还需要通过Go语言代码将汇编定义的符号声明出来。
用于变量和函数的定义的Go汇编文件,类似与C的 .c文件,用于导出汇编定义符号的Go源文件类似C语言的 .h 文件。go 汇编不类似于 8位寄存器,而是引入了 PC,FP,SP,SB 4类伪寄存器。
4个伪寄存器加其他通用寄存器就是Go汇编语言对CPU的重新抽象。
因此go的汇编代码也经常被称之为伪汇编代码。
汇编程序基于 Plan 9 汇编程序的输入样式,在 其他地方有详细记录。
如果您打算编写汇编语言,您应该阅读该文档,尽管其中大部分内容是特定于 Plan 9 的。当前文档提供了语法的摘要以及与该文档中解释内容的差异,并描述了编写汇编代码以与 Go 交互时适用的特殊性。
关于 Go 的汇编器最重要的一点是它不是底层机器的直接表示。一些细节精确映射到机器,但有些则不然。
这是因为编译器套件(请参阅 此说明)不需要在通常的管道中传递汇编程序。
相反,编译器在一种半抽象的指令集上运行,指令选择部分发生在代码生成之后。
汇编器以半抽象的形式工作,所以当你看到这样的MOV 指令时。
工具链实际为该操作生成的可能根本不是移动指令,可能是清除指令或加载指令。或者它可能与具有该名称的机器指令完全对应。
一般来说,特定于机器的操作往往表现为它们自己,而更一般的概念,如内存移动和子程序调用和返回则更抽象。
细节因架构而异,go语言处于发展阶段,因此也经常处于变动中。
汇编程序是一种解析该半抽象指令集的描述并将其转换为要输入到链接器的指令的方法。
如果您想查看给定体系结构(例如 amd64)的汇编指令,标准库的源代码中有很多示例,
例如 runtime和 math/big.您还可以检查编译器作为汇编代码发出的内容(实际输出可能与您在此处看到的不同):
GOOS=linux GOARCH=amd64 go tool compile -S x.go
可以使用tool 工具检查go 程序的伪汇编代码
go tool compile -S go_pkg.go
3 调试工具
安装
https://github.com/go-delve/delve/tree/master/Documentation/installation
git clone https://github.com/go-delve/delve
cd .\delve\
go install github.com/go-delve/delve/cmd/dlv...
>> go install github.com/go-delve/delve/cmd/dlv@latest
go: downloading github.com/go-delve/delve v1.20.1
go: downloading golang.org/x/sys v0.0.0-20220908164124-27713097b956
\delve> go help install
4 使用调试
在任意一个 包括main.go 的 go 模块包中, 使用 dlv debug 开始调试
dlv debug .\main.go
Type 'help' for list of commands.
(dlv)
要将标志传递给您的程序,请用--:将它们分开dlv debug ./main.go -- -arg1 value。
调用该命令将使 Delve 以最适合调试的方式编译程序,然后执行并附加到程序并开始调试会话。
现在,当调试会话首次启动时,您就处于程序初始化的最开始。
为了到达更有用的地方,您需要设置一个或两个断点并继续执行到该点。
dlv 默认为 panic设置了一个断点。
设置自定义断点和继续执行 main 功能:
(dlv) break main.main
Breakpoint 1 set at 0xd1b76a for main.main() /main.go:116
(dlv) continue
> main.main() /main.go:116 (hits goroutine(1):1 total:1) (PC: 0xd1b76a)
111: }(i)
112: }
113: wg.Wait()
114: }
115:
=> 116: func main() {
117: fmt.Printf("gls:%#v \n", gls)
118: UsageGls()
119: }
调试测试, 如果有测试包和文件可以使用 dlv test执行,类似go test的规则。
给定与上面相同的目录结构,您可以通过执行测试套件来调试代码。
为此,您可以使用dlv test子命令,它采用与 相同的可选包路径dlv debug,并且如果没有给出任何参数,也将构建当前包。
$ dlv test ./main.go
Type 'help' for list of commands.
(dlv) funcs test.Test*
/home/me/go/src/github.com/me/foo/pkg/baz/test.TestHi
(dlv) break TestHi
Breakpoint 1 set at 0x536513 for /home/me/go/src/github.com/me/foo/pkg/baz/test.TestHi() ./test_test.go:5
(dlv) continue
> /home/me/go/src/github.com/me/foo/pkg/baz/test.TestHi() ./bar_test.go:5 (hits goroutine(5):1 total:1) (PC: 0x536513)
1: package baz
2:
3: import "testing"
4:
=> 5: func TestHi(t *testing.T) {
6: t.Fatal("implement me!")
7: }
(dlv)
我们开始调试测试二进制文件,通过使用正funcs则表达式过滤函数列表的命令找到我们的测试函数,设置断点,然后继续执行直到遇到该断点。
有关您可以使用的子命令的更多信息,请键入dlv help,一旦在调试会话中,您可以随时通过键入来查看所有可用的命令help 。
使用dlv 调试 go 运行时
可以使用 Delve 来调试 Go 运行时,但是需要牢记一些注意事项
1 该runtime包始终使用优化和内联进行编译,所有适用于调试优化二进制文件的警告都适用于运行时包。
2 特别是一些变量可能不可用或具有陈旧的值,并且它可能会暴露一些编译器为指令分配行号的错误。
3 接下来,step 和 stepout 尝试跟随当前的 goroutine,如果你在运行时调试其中一个修改 curg 指针的函数,它们会感到困惑。应该改用“步骤指令”命令。
4 从 g0 执行堆栈跟踪时,Delve 将返回顶部帧,然后立即切换到 goroutine 堆栈。
5 如果您想查看 g0 堆栈跟踪,请使用
stack -mode simple.
6 如果 step 命令已经在运行时函数中,则它只会进入私有运行时函数。
要进入由编译器插入到用户代码中的私有运行时函数,请设置断点,然后runtime.curg.goid == 用作条件。