张三admin

V1

2022/07/26阅读:15主题:默认主题

go开发调试之Delve的使用

简介

目前Go语言支持GDB、LLDB和Delve几种调试器。其中GDB是最早支持的调试工具,LLDB是macOS系统推荐的标准调试工具。但是GDB和LLDB对Go语言的专有特性都缺乏很大支持,而只有Delve是专门为Go语言设计开发的调试工具。而且Delve本身也是采用Go语言开发,对Windows平台也提供了一样的支持。本节我们基于Delve简单解释如何调试Go汇编程序。

安装

Delve 安装非常简单,如果读者朋友使用的是 Go 1.16 或更高版本,可以直接使用 go install 安装:

go install github.com/go-delve/delve/cmd/dlv@latest

如果读者朋友们使用的是低于 Go 1.16 的版本,可是先下载 Delve 源码,然后使用 go install 安装:

git clone https://github.com/go-delve/delve
cd delve
go install github.com/go-delve/delve/cmd/dlv

安装完成之后,可以使用 go help install 查看 dlv 可执行文件的详细位置。我建议读者朋友们将 dlv可执行文件,配置到 PATH 环境变量。

需要注意的是,如果读者朋友们使用的是 macOS,还需要安装命令行开发工具:

xcode-select --install

为了避免每次使用 dlv 都需要授权允许使用 debugger,建议读者朋友们开启开发者模式:

sudo /usr/sbin/DevToolsSecurity -enable

实战

创建main.go文件,main函数先通过循初始化一个切片,然后输出切片的内容:

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:

Running the program:
    call ------------------------ Resumes process, injecting a function call (EXPERIMENTAL!!!)
    continue (alias: c) --------- Run until breakpoint or program termination.
    next (alias: n) ------------- Step over to next source line.
    rebuild --------------------- Rebuild the target executable and restarts it. It does not work if the executable was not built by delve.
    restart (alias: r) ---------- Restart process.
    step (alias: s) ------------- Single step through program.
    step-instruction (alias: si)  Single step a single cpu instruction.
    stepout (alias: so) --------- Step out of the current function.

Manipulating breakpoints:
    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.
    on --------------------- Executes a command when a breakpoint is hit.
    toggle ----------------- Toggles on or off a breakpoint.
    trace (alias: t) ------- Set tracepoint.
    watch ------------------ Set watchpoint.

Viewing program variables and memory:
    args ----------------- Print function arguments.
    display -------------- Print value of an expression every time the program stops.
    examinemem (alias: x)  Examine raw memory at the given address.
    locals --------------- Print local variables.
    print (alias: p) ----- Evaluate an expression.
    regs ----------------- Print contents of CPU registers.
    set ------------------ Changes the value of a variable.
    vars ----------------- Print package variables.
    whatis --------------- Prints type of an expression.

Listing and switching between threads and goroutines:
    goroutine (alias: gr) -- Shows or changes current goroutine
    goroutines (alias: grs)  List program goroutines.
    thread (alias: tr) ----- Switch to the specified thread.
    threads ---------------- Print out info for every traced thread.

Viewing the call stack and selecting frames:
    deferred --------- Executes command in the context of a deferred call.
    down ------------- Move the current frame down.
    frame ------------ Set the current frame, or execute command on a different frame.
    stack (alias: bt)  Print stack trace.
    up --------------- Move the current frame up.

Other commands:
    config --------------------- Changes configuration parameters.
    disassemble (alias: disass)  Disassembler.
    dump ----------------------- Creates a core dump from the current process state
    edit (alias: ed) ----------- Open where you are in $DELVE_EDITOR or $EDITOR
    exit (alias: quit | q) ----- Exit the debugger.
    funcs ---------------------- Print list of functions.
    help (alias: h) ------------ Prints the help message.
    libraries ------------------ List loaded dynamic libraries
    list (alias: ls | l) ------- Show source code.
    source --------------------- Executes a file containing a list of delve commands
    sources -------------------- Print list of source files.
    transcript ----------------- Appends command output to a file.
    types ---------------------- Print list of types

Type help followed by a command for full documentation.
(dlv) 

启动一个调试会话:

[root@VM-8-14-centos work]# dlv debug
Type 'help' for list of commands.
(dlv)

阅读上面这段代码,我们使用 dlv debug 启动一个调试会话,在没有任何参数的情况下,Delve 编译并开始调试当前目录中的 main 包。

我们也可以指定一个文件名,Delve 将会编译该指定文件的 main 包,并启动一个调试会话。

[root@VM-8-14-centos work]# dlv debug main.go
Type 'help' for list of commands.
(dlv)

调试会话启动后,我们可以使用调试命令进行调试程序。

查看程序

(dlv) list
> main.main() ./main.go:10 (hits goroutine(1):5 total:5) (PC: 0x102dfc09c)
     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) list main.main
Showing /Users/xxxx/Desktop/test/testCode/main.go:7 (PC: 0x102dfc040)
     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) list ./main.go:10
Showing /Users/xxx/Desktop/test/testCode/main.go:10 (PC: 0x102dfc09c)
     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) 

调试会话启动后,我们可以使用 list 命令列出指定位置的源码,包含两种方式,第一种方式是 . ,第二种方式是 :

设置断点

每个Go程序的入口是main.main函数,我们可以用break在此设置一个断点:

(dlv) break main.main
Breakpoint 2 set at 0x102dfc040 for main.main() ./main.go:7
(dlv) breakpoints
Breakpoint runtime-fatal-throw (enabled) at 0x102d7b2a0 for runtime.throw() /usr/local/go/src/runtime/panic.go:1107 (0)
Breakpoint unrecovered-panic (enabled) at 0x102d7b4b0 for runtime.fatalpanic() /usr/local/go/src/runtime/panic.go:1190 (0)
        print runtime.curg._panic.arg
Breakpoint 1 (enabled) at 0x102dfc09c for main.main() ./main.go:10 (5)
Breakpoint 2 (enabled) at 0x102dfc040 for main.main() ./main.go:7 (0)
(dlv) 

break设置断点 breakpoints 查看所有断点

我们可以使用 breakpoints 命令,列出所有断点,可以使用** clear** 命令删除指定断点,可以使用 clearall 删除所有断定。

我们发现除了我们自己设置的main.main函数断点外,Delve内部已经为panic异常函数设置了一个断点。

通过vars命令可以查看全部包级的变量。因为最终的目标程序可能含有大量的全局变量,我们可以通过一个正则参数选择想查看的全局变量:

vars查看所有包级别的变量

(dlv) vars main
runtime.main_init_done = chan bool 0/0
runtime.mainStarted = true

跳至设置的断点

然后通过continue执行到刚设置的条件断点

(dlv) break main.go:7
Breakpoint 1 set at 0x104b14040 for main.main() ./main.go:7
(dlv) continue
> main.main() ./main.go:7 (hits goroutine(1):1 total:1) (PC: 0x104b14040)
     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:         Print(nums)
(dlv) 

stack查看当前执行函数的栈帧信息:

(dlv) stack
0  0x0000000104b14040 in main.main
   at ./main.go:7
1  0x0000000104a957f4 in runtime.main
   at /usr/local/go/src/runtime/proc.go:225
2  0x0000000104ac50a4 in runtime.goexit
   at /usr/local/go/src/runtime/asm_arm64.s:1130
(dlv) 
                                       

goroutine和goroutines命令查看当前Goroutine相关的信息:

(dlv) goroutine
Thread 9341620 at ./main.go:7
Goroutine 1:
        Runtime: ./main.go:7 main.main (0x104b14040)
        User: ./main.go:7 main.main (0x104b14040)
        Go: /usr/local/go/src/runtime/asm_arm64.s:92 runtime.rt0_go (0x104ac2bb4)
        Start: /usr/local/go/src/runtime/proc.go:115 runtime.main (0x104a955b0)
(dlv) goroutines
* Goroutine 1 - User: ./main.go:7 main.main (0x104b14040) (thread 9341620)
  Goroutine 2 - User: /usr/local/go/src/runtime/proc.go:337 runtime.gopark (0x104a95bd8) [force gc (idle)]
  Goroutine 3 - User: /usr/local/go/src/runtime/proc.go:337 runtime.gopark (0x104a95bd8) [GC sweep wait]
  Goroutine 4 - User: /usr/local/go/src/runtime/proc.go:337 runtime.gopark (0x104a95bd8) [GC scavenge wait]
  Goroutine 17 - User: /usr/local/go/src/runtime/proc.go:337 runtime.gopark (0x104a95bd8) [finalizer wait]
[5 goroutines]

实战

实际使用

$ dlv debug
Type 'help' for list of commands.
(dlv) break main.go:7
Breakpoint 1 set at 0x100de8040 for main.main() ./main.go:7
(dlv) continue
> main.main() ./main.go:7 (hits goroutine(1):1 total:1) (PC: 0x100de8040)
     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:         Print(nums)
(dlv) next
> main.main() ./main.go:8 (PC: 0x100de804c)
     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:         Print(nums)
    13: }
(dlv) next
> main.main() ./main.go:9 (PC: 0x100de807c)
     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:         Print(nums)
    13: }
    14: 
(dlv) next
> main.main() ./main.go:10 (PC: 0x100de809c)
     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:         Print(nums)
    13: }
    14: 
    15: func Print(num []int){
(dlv) next
> main.main() ./main.go:9 (PC: 0x100de80c8)
     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:         Print(nums)
    13: }
    14: 
(dlv) next
> main.main() ./main.go:10 (PC: 0x100de809c)
     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:         Print(nums)
    13: }
    14: 
    15: func Print(num []int){
(dlv) next
> main.main() ./main.go:9 (PC: 0x100de80c8)
     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:         Print(nums)
    13: }
    14: 
(dlv) next
> main.main() ./main.go:10 (PC: 0x100de809c)
     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:         Print(nums)
    13: }
    14: 
    15: func Print(num []int){
(dlv) next
> main.main() ./main.go:9 (PC: 0x100de80c8)
     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:         Print(nums)
    13: }
    14: 
(dlv) next
> main.main() ./main.go:10 (PC: 0x100de809c)
     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:         Print(nums)
    13: }
    14: 
    15: func Print(num []int){
(dlv) next
> main.main() ./main.go:9 (PC: 0x100de80c8)
     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:         Print(nums)
    13: }
    14: 
(dlv) next
> main.main() ./main.go:10 (PC: 0x100de809c)
     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:         Print(nums)
    13: }
    14: 
    15: func Print(num []int){
(dlv) next
> main.main() ./main.go:9 (PC: 0x100de80c8)
     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:         Print(nums)
    13: }
    14: 
(dlv) next
> main.main() ./main.go:12 (PC: 0x100de80d8)
     7: func main() {
     8:         nums := make([]int, 5)
     9:         for i := 0; i < len(nums); i++ {
    10:                 nums[i] = i * i
    11:         }
=>  12:         Print(nums)
    13: }
    14: 
    15: func Print(num []int){
    16:         fmt.Println(num)
    17: }
(dlv) step
> main.Print() ./main.go:15 (PC: 0x100de8130)
    10:                 nums[i] = i * i
    11:         }
    12:         Print(nums)
    13: }
    14: 
=>  15: func Print(num []int){
    16:         fmt.Println(num)
    17: }
(dlv) stepout
[0 1 4 9 16]
> main.main() ./main.go:13 (PC: 0x100de80f4)
Values returned:

     8:         nums := make([]int, 5)
     9:         for i := 0; i < len(nums); i++ {
    10:                 nums[i] = i * i
    11:         }
    12:         Print(nums)
=>  13: }
    14: 
    15: func Print(num []int){
    16:         fmt.Println(num)
    17: }
(dlv) print nums
[]int len: 5, cap: 5, [0,1,4,9,16]
(dlv) 

我们使用 Delve 添加断点后,执行 continue 命令,程序将执行到断点位置;执行 next 命令,程序继续执行下一行代码;执行 step 命令,程序进入到调用函数内部;执行 stepout 命令,程序步出到调用函数的调用位置;执行 print 命令,打印指定参数的值。

读者朋友们使用以上命令,可以满足大部分调试场景。为了方便理解,以上示例中使用的命令都没有使用简写形式,在实际使用时,使用简写形式会更加便捷。

简写形式:

  • break(b) 设置断点
  • continue(c) 跳至下一个断点
  • next(n) 下一步
  • step(s) 进入函数内部
  • stepout(so) 推到到函数调用处
  • print(p) 打印变量
  • vars 包的变量
  • locals 函数内的变量
  • list 函数代码

最后完成调试工作后输入quit命令退出调试器。至此我们已经掌握了Delve调试器器的简单用法。

调试汇编程序

用Delve调试Go汇编程序的过程比调试Go语言程序更加简单。调试汇编程序时,我们需要时刻关注寄存器的状态,如果涉及函数调用或局部变量或参数还需要重点关注栈寄存器SP的状态。

为了编译演示,我们重新实现一个更简单的main函数:

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汇编程序的简单调试技术。

分类:

后端

标签:

后端

作者介绍

张三admin
V1