# 3.9 Delve 调试器 目前 Go 语言支持 GDB、LLDB 和 Delve 几种调试器。其中 GDB 是最早支持的调试工具,LLDB 是 macOS 系统推荐的标准调试工具。但是 GDB 和 LLDB 对 Go 语言的专有特性都缺乏很大支持,而只有 Delve 是专门为 Go 语言设计开发的调试工具。而且 Delve 本身也是采用 Go 语言开发,对 Windows 平台也提供了一样的支持。本节我们基于 Delve 简单解释如何调试 Go 汇编程序。 ## 3.9.1 Delve 入门 首先根据官方的文档正确安装 Delve 调试器。我们会先构造一个简单的 Go 语言代码,用于熟悉下 Delve 的简单用法。 创建 main.go 文件,main 函数先通过循初始化一个切片,然后输出切片的内容: ```go package main import ( "fmt" ) func main() { nums := make([]int, 5) for i := 0; i main.main() ./main.go:7 (hits goroutine(1):1 total:1) (PC: 0x10ae9b8) 2: 3: import ( 4: "fmt" 5: ) 6: => 7: func main() { 8: nums := make([]int, 5) 9: for i := 0; i main.main() ./main.go:8 (PC: 0x10ae9cf) 3: import ( 4: "fmt" 5: ) 6: 7: func main() { => 8: nums := make([]int, 5) 9: for i := 0; i main.main() ./main.go:9 (PC: 0x10aea12) 4: "fmt" 5: ) 6: 7: func main() { 8: nums := make([]int, 5) => 9: for i := 0; i main.main() ./main.go:10 (hits goroutine(1):1 total:1) (PC: 0x10aea33) 5: ) 6: 7: func main() { 8: nums := make([]int, 5) 9: for i := 0; i 10: nums[i] = i * i 11: } 12: fmt.Println(nums) 13: } (dlv) locals nums = []int len: 5, cap: 5, [...] i = 3 (dlv) print nums []int len: 5, cap: 5, [0,1,4,0,0] (dlv) ``` 我们发现当循环变量 i 等于 3 时,nums 切片的前 3 个元素已经正确初始化。 我们还可以通过 stack 查看当前执行函数的栈帧信息: ``` (dlv) stack 0 0x00000000010aea33 in main.main at ./main.go:10 1 0x000000000102bd60 in runtime.main at /usr/local/go/src/runtime/proc.go:198 2 0x0000000001053bd1 in runtime.goexit at /usr/local/go/src/runtime/asm_amd64.s:2361 (dlv) ``` 或者通过 goroutine 和 goroutines 命令查看当前 Goroutine 相关的信息: ``` (dlv) goroutine Thread 101686 at ./main.go:10 Goroutine 1: Runtime: ./main.go:10 main.main (0x10aea33) User: ./main.go:10 main.main (0x10aea33) Go: /usr/local/go/src/runtime/asm_amd64.s:258 runtime.rt0_go (0x1051643) Start: /usr/local/go/src/runtime/proc.go:109 runtime.main (0x102bb90) (dlv) goroutines [4 goroutines] * Goroutine 1 - User: ./main.go:10 main.main (0x10aea33) (thread 101686) Goroutine 2 - User: /usr/local/go/src/runtime/proc.go:292 \ runtime.gopark (0x102c189) Goroutine 3 - User: /usr/local/go/src/runtime/proc.go:292 \ runtime.gopark (0x102c189) Goroutine 4 - User: /usr/local/go/src/runtime/proc.go:292 \ runtime.gopark (0x102c189) (dlv) ``` 最后完成调试工作后输入 quit 命令退出调试器。至此我们已经掌握了 Delve 调试器器的简单用法。 ## 3.9.2 调试汇编程序 用 Delve 调试 Go 汇编程序的过程比调试 Go 语言程序更加简单。调试汇编程序时,我们需要时刻关注寄存器的状态,如果涉及函数调用或局部变量或参数还需要重点关注栈寄存器 SP 的状态。 为了编译演示,我们重新实现一个更简单的 main 函数: ```go package main func main() { asmSayHello() } func asmSayHello() ``` 在 main 函数中调用汇编语言实现的 asmSayHello 函数输出一个字符串。 asmSayHello 函数在 main_amd64.s 文件中实现: ``` #include "textflag.h" #include "funcdata.h" // "Hello World!\n" DATA text<>+0(SB)/8,$"Hello Wo" DATA text<>+8(SB)/8,$"rld!\n" GLOBL text<>(SB),NOPTR,$16 // func asmSayHello() TEXT ·asmSayHello(SB), $16-0 NO_LOCAL_POINTERS MOVQ $text<>+0(SB), AX MOVQ AX, (SP) MOVQ $16, 8(SP) CALL runtime·printstring(SB) RET ``` 参考前面的调试流程,在执行到 main 函数断点时,可以 disassemble 反汇编命令查看 main 函数对应的汇编代码: ``` (dlv) break main.main Breakpoint 1 set at 0x105011f for main.main() ./main.go:3 (dlv) continue > main.main() ./main.go:3 (hits goroutine(1):1 total:1) (PC: 0x105011f) 1: package main 2: =>3: func main() { asmSayHello() } 4: 5: func asmSayHello() (dlv) disassemble TEXT main.main(SB) /path/to/pkg/main.go main.go:3 0x1050110 65488b0c25a0080000 mov rcx, qword ptr g [0x8a0] main.go:3 0x1050119 483b6110 cmp rsp, qword ptr [r +0x10] main.go:3 0x105011d 761a jbe 0x1050139 =>main.go:3 0x105011f* 4883ec08 sub rsp, 0x8 main.go:3 0x1050123 48892c24 mov qword ptr [rsp], rbp main.go:3 0x1050127 488d2c24 lea rbp, ptr [rsp] main.go:3 0x105012b e880000000 call $main.asmSayHello main.go:3 0x1050130 488b2c24 mov rbp, qword ptr [rsp] main.go:3 0x1050134 4883c408 add rsp, 0x8 main.go:3 0x1050138 c3 ret main.go:3 0x1050139 e87288ffff call $runtime.morestack_noctxt main.go:3 0x105013e ebd0 jmp $main.main (dlv) ``` 虽然 main 函数内部只有一行函数调用语句,但是却生成了很多汇编指令。在函数的开头通过比较 rsp 寄存器判断栈空间是否不足,如果不足则跳转到 0x1050139 地址调用 runtime.morestack 函数进行栈扩容,然后跳回到 main 函数开始位置重新进行栈空间测试。而在 asmSayHello 函数调用之前,先扩展 rsp 空间用于临时存储 rbp 寄存器的状态,在函数返回后通过栈恢复 rbp 的值并回收临时栈空间。通过对比 Go 语言代码和对应的汇编代码,我们可以加深对 Go 汇编语言的理解。 从汇编语言角度深刻 Go 语言各种特性的工作机制对调试工作也是一个很大的帮助。如果希望在汇编指令层面调试 Go 代码,Delve 还提供了一个 step-instruction 单步执行汇编指令的命令。 现在我们依然用 break 命令在 asmSayHello 函数设置断点,并且输入 continue 命令让调试器执行到断点位置停下: ``` (dlv) break main.asmSayHello Breakpoint 2 set at 0x10501bf for main.asmSayHello() ./main_amd64.s:10 (dlv) continue > main.asmSayHello() ./main_amd64.s:10 (hits goroutine(1):1 total:1) (PC: 0x10501bf) 5: DATA text<>+0(SB)/8,$"Hello Wo" 6: DATA text<>+8(SB)/8,$"rld!\n" 7: GLOBL text<>(SB),NOPTR,$16 8: 9: // func asmSayHello() => 10: TEXT ·asmSayHello(SB), $16-0 11: NO_LOCAL_POINTERS 12: MOVQ $text<>+0(SB), AX 13: MOVQ AX, (SP) 14: MOVQ $16, 8(SP) 15: CALL runtime·printstring(SB) (dlv) ``` 此时我们可以通过 regs 查看全部的寄存器状态: ``` (dlv) regs rax = 0x0000000001050110 rbx = 0x0000000000000000 rcx = 0x000000c420000300 rdx = 0x0000000001070be0 rdi = 0x000000c42007c020 rsi = 0x0000000000000001 rbp = 0x000000c420049f78 rsp = 0x000000c420049f70 r8 = 0x7fffffffffffffff r9 = 0xffffffffffffffff r10 = 0x0000000000000100 r11 = 0x0000000000000286 r12 = 0x000000c41fffff7c r13 = 0x0000000000000000 r14 = 0x0000000000000178 r15 = 0x0000000000000004 rip = 0x00000000010501bf rflags = 0x0000000000000206 ... (dlv) ``` 因为 AMD64 的各种寄存器非常多,项目的信息中刻意省略了非通用的寄存器。如果再单步执行到 13 行时,可以发现 AX 寄存器值的变化。 ``` (dlv) regs rax = 0x00000000010a4060 rbx = 0x0000000000000000 rcx = 0x000000c420000300 ... (dlv) ``` 因此我们可以推断汇编程序内部定义的 `text<>` 数据的地址为 0x00000000010a4060。我们可以用过 print 命令来查看该内存内的数据: ``` (dlv) print *(*[5]byte)(uintptr(0x00000000010a4060)) [5]uint8 [72,101,108,108,111] (dlv) ``` 我们可以发现输出的 `[5]uint8 [72,101,108,108,111]` 刚好是对应 “Hello” 字符串。通过类似的方法,我们可以通过查看 SP 对应的栈指针位置,然后查看栈中局部变量的值。 至此我们就掌握了 Go 汇编程序的简单调试技术。