Go 語言 Delve 調(diào)試器

2023-03-22 15:02 更新

原文鏈接:https://chai2010.cn/advanced-go-programming-book/ch3-asm/ch3-09-debug.html


3.9 Delve 調(diào)試器

目前 Go 語言支持 GDB、LLDB 和 Delve 幾種調(diào)試器。其中 GDB 是最早支持的調(diào)試工具,LLDB 是 macOS 系統(tǒng)推薦的標準調(diào)試工具。但是 GDB 和 LLDB 對 Go 語言的專有特性都缺乏很大支持,而只有 Delve 是專門為 Go 語言設計開發(fā)的調(diào)試工具。而且 Delve 本身也是采用 Go 語言開發(fā),對 Windows 平臺也提供了一樣的支持。本節(jié)我們基于 Delve 簡單解釋如何調(diào)試 Go 匯編程序。

3.9.1 Delve 入門

首先根據(jù)官方的文檔正確安裝 Delve 調(diào)試器。我們會先構造一個簡單的 Go 語言代碼,用于熟悉下 Delve 的簡單用法。

創(chuàng)建 main.go 文件,main 函數(shù)先通過循初始化一個切片,然后輸出切片的內(nèi)容:

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 命令進入調(diào)試:

$ dlv debug
Type 'help' for list of commands.
(dlv)

輸入 help 命令可以查看到 Delve 提供的調(diào)試命令列表:

(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...
    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...
    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 函數(shù),我們可以用 break 在此設置一個斷點:

(dlv) break main.main
Breakpoint 1 set at 0x10ae9b8 for main.main() ./main.go:7

然后通過 breakpoints 查看已經(jīng)設置的所有斷點:

(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)

我們發(fā)現(xiàn)除了我們自己設置的 main.main 函數(shù)斷點外,Delve 內(nèi)部已經(jīng)為 panic 異常函數(shù)設置了一個斷點。

通過 vars 命令可以查看全部包級的變量。因為最終的目標程序可能含有大量的全局變量,我們可以通過一個正則參數(shù)選擇想查看的全局變量:

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

然后就可以通過 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 命令單步執(zhí)行進入 main 函數(shù)內(nèi)部:

(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)

進入函數(shù)之后可以通過 args 和 locals 命令查看函數(shù)的參數(shù)和局部變量:

(dlv) args
(no args)
(dlv) locals
nums = []int len: 842350763880, cap: 17491881, nil

因為 main 函數(shù)沒有參數(shù),因此 args 命令沒有任何輸出。而 locals 命令則輸出了局部變量 nums 切片的值:此時切片還未完成初始化,切片的底層指針為 nil,長度和容量都是一個隨機數(shù)值。

再次輸入 next 命令單步執(zhí)行后就可以查看到 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)

此時因為調(diào)試器已經(jīng)到了 for 語句行,因此局部變量出現(xiàn)了還未初始化的循環(huán)迭代變量 i。

下面我們通過組合使用 break 和 condition 命令,在循環(huán)內(nèi)部設置一個條件斷點,當循環(huán)變量 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 執(zhí)行到剛設置的條件斷點,并且輸出局部變量:

(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)

我們發(fā)現(xiàn)當循環(huán)變量 i 等于 3 時,nums 切片的前 3 個元素已經(jīng)正確初始化。

我們還可以通過 stack 查看當前執(zhí)行函數(shù)的棧幀信息:

(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)

最后完成調(diào)試工作后輸入 quit 命令退出調(diào)試器。至此我們已經(jīng)掌握了 Delve 調(diào)試器器的簡單用法。

3.9.2 調(diào)試匯編程序

用 Delve 調(diào)試 Go 匯編程序的過程比調(diào)試 Go 語言程序更加簡單。調(diào)試匯編程序時,我們需要時刻關注寄存器的狀態(tài),如果涉及函數(shù)調(diào)用或局部變量或參數(shù)還需要重點關注棧寄存器 SP 的狀態(tài)。

為了編譯演示,我們重新實現(xiàn)一個更簡單的 main 函數(shù):

package main

func main() { asmSayHello() }

func asmSayHello()

在 main 函數(shù)中調(diào)用匯編語言實現(xiàn)的 asmSayHello 函數(shù)輸出一個字符串。

asmSayHello 函數(shù)在 main_amd64.s 文件中實現(xiàn):

#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

參考前面的調(diào)試流程,在執(zhí)行到 main 函數(shù)斷點時,可以 disassemble 反匯編命令查看 main 函數(shù)對應的匯編代碼:

(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 函數(shù)內(nèi)部只有一行函數(shù)調(diào)用語句,但是卻生成了很多匯編指令。在函數(shù)的開頭通過比較 rsp 寄存器判斷??臻g是否不足,如果不足則跳轉到 0x1050139 地址調(diào)用 runtime.morestack 函數(shù)進行棧擴容,然后跳回到 main 函數(shù)開始位置重新進行棧空間測試。而在 asmSayHello 函數(shù)調(diào)用之前,先擴展 rsp 空間用于臨時存儲 rbp 寄存器的狀態(tài),在函數(shù)返回后通過?;謴?rbp 的值并回收臨時??臻g。通過對比 Go 語言代碼和對應的匯編代碼,我們可以加深對 Go 匯編語言的理解。

從匯編語言角度深刻 Go 語言各種特性的工作機制對調(diào)試工作也是一個很大的幫助。如果希望在匯編指令層面調(diào)試 Go 代碼,Delve 還提供了一個 step-instruction 單步執(zhí)行匯編指令的命令。

現(xiàn)在我們依然用 break 命令在 asmSayHello 函數(shù)設置斷點,并且輸入 continue 命令讓調(diào)試器執(zhí)行到斷點位置停下:

(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 查看全部的寄存器狀態(tài):

(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 的各種寄存器非常多,項目的信息中刻意省略了非通用的寄存器。如果再單步執(zhí)行到 13 行時,可以發(fā)現(xiàn) AX 寄存器值的變化。

(dlv) regs
       rax = 0x00000000010a4060
       rbx = 0x0000000000000000
       rcx = 0x000000c420000300
...
(dlv)

因此我們可以推斷匯編程序內(nèi)部定義的 text<> 數(shù)據(jù)的地址為 0x00000000010a4060。我們可以用過 print 命令來查看該內(nèi)存內(nèi)的數(shù)據(jù):

(dlv) print *(*[5]byte)(uintptr(0x00000000010a4060))
[5]uint8 [72,101,108,108,111]
(dlv)

我們可以發(fā)現(xiàn)輸出的 [5]uint8 [72,101,108,108,111] 剛好是對應 “Hello” 字符串。通過類似的方法,我們可以通過查看 SP 對應的棧指針位置,然后查看棧中局部變量的值。

至此我們就掌握了 Go 匯編程序的簡單調(diào)試技術。



以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號