1
0
mirror of https://github.com/chai2010/advanced-go-programming-book.git synced 2025-05-24 04:22:22 +00:00
2018-07-11 08:25:08 +08:00

332 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 3.8. Delve调试器
目前Go语言支持GDB、LLDB和Delve几种调试器。其中GDB是最早支持的调试工具LLDB是macOS系统推荐的标准调试工具。但是GDB和LLDB对Go语言的专有特性都缺乏很大支持而只有Delve是专门为Go语言设计开发的调试工具。而且Delve本身也是采用Go语言开发对Windows平台也提供了一样的支持。本节我们基于Delve简单解释如何调试Go汇编程序。
## Delve入门
首先根据官方的文档正确安装Delve调试器。我们会先构造一个简单的Go语言代码用于熟悉下Delve的简单用法。
创建main.go文件main函数先通过循初始化一个切片然后输出切片的内容
```go
package main
import (
"fmt"
)
func main() {
nums := make([]int, 5)
for i := 0; i < len(nums); i++ {
nums[i] = i * i
}
fmt.Println(nums)
}
```
命令行进入包所在目录,然后输入`dlv debug`命令进入调试:
```
$ dlv debug
Type 'help' for list of commands.
(dlv)
```
输入help命令可以查看到Delve提供的调试命令列表
```
(dlv) help
The following commands are available:
args ------------------------ Print function arguments.
break (alias: b) ------------ Sets a breakpoint.
breakpoints (alias: bp) ----- Print out info for active breakpoints.
clear ----------------------- Deletes breakpoint.
clearall -------------------- Deletes multiple breakpoints.
condition (alias: cond) ----- Set breakpoint condition.
config ---------------------- Changes configuration parameters.
continue (alias: c) --------- Run until breakpoint or program termination.
disassemble (alias: disass) - Disassembler.
down ------------------------ Move the current frame down.
exit (alias: quit | q) ------ Exit the debugger.
frame ----------------------- Set the current frame, or execute command on a different frame.
funcs ----------------------- Print list of functions.
goroutine ------------------- Shows or changes current goroutine
goroutines ------------------ List program goroutines.
help (alias: h) ------------- Prints the help message.
list (alias: ls | l) -------- Show source code.
locals ---------------------- Print local variables.
next (alias: n) ------------- Step over to next source line.
on -------------------------- Executes a command when a breakpoint is hit.
print (alias: p) ------------ Evaluate an expression.
regs ------------------------ Print contents of CPU registers.
restart (alias: r) ---------- Restart process.
set ------------------------- Changes the value of a variable.
source ---------------------- Executes a file containing a list of delve commands
sources --------------------- Print list of source files.
stack (alias: bt) ----------- Print stack trace.
step (alias: s) ------------- Single step through program.
step-instruction (alias: si) Single step a single cpu instruction.
stepout --------------------- Step out of the current function.
thread (alias: tr) ---------- Switch to the specified thread.
threads --------------------- Print out info for every traced thread.
trace (alias: t) ------------ Set tracepoint.
types ----------------------- Print list of types
up -------------------------- Move the current frame up.
vars ------------------------ Print package variables.
whatis ---------------------- Prints type of an expression.
Type help followed by a command for full documentation.
(dlv)
```
每个Go程序的入口是main.main函数我们可以用break在此设置一个断点
```
(dlv) break main.main
Breakpoint 1 set at 0x10ae9b8 for main.main() ./main.go:7
```
然后通过breakpoints查看已经设置的所有断点
```
(dlv) breakpoints
Breakpoint unrecovered-panic at 0x102a380 for runtime.startpanic() /usr/local/go/src/runtime/panic.go:588 (0)
print runtime.curg._panic.arg
Breakpoint 1 at 0x10ae9b8 for main.main() ./main.go:7 (0)
```
我们发现除了我们自己设置的main.main邯郸断点外Delve内部已经为panic异常函数设置了一个断点。
然后就可以通过continue命令让程序运行到下一个断点处
```
(dlv) continue
> 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 < len(nums); i++ {
10: nums[i] = i * i
11: }
12: fmt.Println(nums)
(dlv)
```
输入next命令单步执行进入main函数内部
```
(dlv) next
> 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 < len(nums); i++ {
10: nums[i] = i * i
11: }
12: fmt.Println(nums)
13: }
(dlv)
```
进入函数之后可以通过args和locals命令查看函数的参数和局部变量
```
(dlv) args
(no args)
(dlv) locals
nums = []int len: 842350763880, cap: 17491881, nil
```
因为main函数没有参数因此args命令没有任何输出。而locals命令则输出了局部变量nums切片的值此时切片还未完成初始化切片的底层指针为nil长度和容量都是一个随机数值。
再次输入next命令单步执行后就可以查看到nums切片初始化之后的结果了
```
(dlv) next
> main.main() ./main.go:9 (PC: 0x10aea12)
4: "fmt"
5: )
6:
7: func main() {
8: nums := make([]int, 5)
=> 9: for i := 0; i < len(nums); i++ {
10: nums[i] = i * i
11: }
12: fmt.Println(nums)
13: }
(dlv) locals
nums = []int len: 5, cap: 5, [...]
i = 17601536
(dlv)
```
此时因为调试器已经到了for语句行因此局部变量出现了还未初始化的循环迭代变量i。
下面我们通过组合使用break和condition命令在循环内部设置一个条件断点当循环变量i等于3时断点生效
```
(dlv) break main.go:10
Breakpoint 2 set at 0x10aea33 for main.main() ./main.go:10
(dlv) condition 2 i==3
(dlv)
```
然后通过continue执行到刚设置的条件断点并且输出局部变量
```
(dlv) continue
> 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 < len(nums); 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调试器器的简单用法。
## 调试汇编程序
用Delve调试Go汇编程序的过程比调试Go语言程序更加简单。调试汇编程序时我们需要时刻关注寄存器的状态如果涉及函数调用或局部变量或参数还需要重点关注栈寄存器SP的状态。
为了编译演示我们用汇编重新实现main函数简单打印一个字符串
```
#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 main()
TEXT ·main(SB), $16-0
NO_LOCAL_POINTERS
MOVQ $text<>+0(SB), AX
MOVQ AX, (SP)
MOVQ $16, 8(SP)
CALL runtime·printstring(SB)
RET
```
然后依然用break命令在main函数设置断点并且输入continue命令让调试器执行到断点位置停下
```
(dlv) break main.main
Breakpoint 1 set at 0x105018f for main.main() ./main_amd64.s:10
(dlv) continue
> main.main() ./main_amd64.s:10 (hits goroutine(1):1 total:1) (PC: 0x105018f)
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 main()
=> 10: TEXT ·main(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 = 0x0000000001050180
rbx = 0x0000000000000000
rcx = 0x000000c420000300
rdx = 0x0000000001070bc0
rdi = 0x000000c42007c020
rsi = 0x0000000000000001
rbp = 0x00007fffffe00000
rsp = 0x000000c420049f80
r8 = 0x7fffffffffffffff
r9 = 0xffffffffffffffff
r10 = 0x0000000000000100
r11 = 0x0000000000000286
r12 = 0x000000c41fffff7c
r13 = 0x0000000000000000
r14 = 0x0000000000000178
r15 = 0x0000000000000004
rip = 0x000000000105018f
rflags = 0x0000000000000202
...
(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汇编程序的简单调试技术。