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)推薦的標(biāo)準(zhǔn)調(diào)試工具。但是 GDB 和 LLDB 對(duì) Go 語言的專有特性都缺乏很大支持,而只有 Delve 是專門為 Go 語言設(shè)計(jì)開發(fā)的調(diào)試工具。而且 Delve 本身也是采用 Go 語言開發(fā),對(duì) Windows 平臺(tái)也提供了一樣的支持。本節(jié)我們基于 Delve 簡(jiǎn)單解釋如何調(diào)試 Go 匯編程序。

3.9.1 Delve 入門

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

創(chuàng)建 main.go 文件,main 函數(shù)先通過循初始化一個(gè)切片,然后輸出切片的內(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)
}

命令行進(jìn)入包所在目錄,然后輸入 dlv debug 命令進(jìn)入調(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)

每個(gè) Go 程序的入口是 main.main 函數(shù),我們可以用 break 在此設(shè)置一個(gè)斷點(diǎn):

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

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

(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)除了我們自己設(shè)置的 main.main 函數(shù)斷點(diǎn)外,Delve 內(nèi)部已經(jīng)為 panic 異常函數(shù)設(shè)置了一個(gè)斷點(diǎn)。

通過 vars 命令可以查看全部包級(jí)的變量。因?yàn)樽罱K的目標(biāo)程序可能含有大量的全局變量,我們可以通過一個(gè)正則參數(shù)選擇想查看的全局變量:

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

然后就可以通過 continue 命令讓程序運(yùn)行到下一個(gè)斷點(diǎn)處:

(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í)行進(jìn)入 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)

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

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

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

再次輸入 next 命令單步執(zhí)行后就可以查看到 nums 切片初始化之后的結(jié)果了:

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

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

下面我們通過組合使用 break 和 condition 命令,在循環(huán)內(nèi)部設(shè)置一個(gè)條件斷點(diǎn),當(dāng)循環(huán)變量 i 等于 3 時(shí)斷點(diǎn)生效:

(dlv) break main.go:10
Breakpoint 2 set at 0x10aea33 for main.main() ./main.go:10
(dlv) condition 2 i==3
(dlv)

然后通過 continue 執(zhí)行到剛設(shè)置的條件斷點(diǎn),并且輸出局部變量:

(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)當(dāng)循環(huán)變量 i 等于 3 時(shí),nums 切片的前 3 個(gè)元素已經(jīng)正確初始化。

我們還可以通過 stack 查看當(dāng)前執(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 命令查看當(dāng)前 Goroutine 相關(guān)的信息:

(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)試器器的簡(jiǎn)單用法。

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

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

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

package main

func main() { asmSayHello() }

func asmSayHello()

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

asmSayHello 函數(shù)在 main_amd64.s 文件中實(shí)現(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ù)斷點(diǎn)時(shí),可以 disassemble 反匯編命令查看 main 函數(shù)對(duì)應(yīng)的匯編代碼:

(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是否不足,如果不足則跳轉(zhuǎn)到 0x1050139 地址調(diào)用 runtime.morestack 函數(shù)進(jìn)行棧擴(kuò)容,然后跳回到 main 函數(shù)開始位置重新進(jìn)行棧空間測(cè)試。而在 asmSayHello 函數(shù)調(diào)用之前,先擴(kuò)展 rsp 空間用于臨時(shí)存儲(chǔ) rbp 寄存器的狀態(tài),在函數(shù)返回后通過?;謴?fù) rbp 的值并回收臨時(shí)??臻g。通過對(duì)比 Go 語言代碼和對(duì)應(yīng)的匯編代碼,我們可以加深對(duì) Go 匯編語言的理解。

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

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

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

此時(shí)我們可以通過 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)

因?yàn)?AMD64 的各種寄存器非常多,項(xiàng)目的信息中刻意省略了非通用的寄存器。如果再單步執(zhí)行到 13 行時(shí),可以發(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] 剛好是對(duì)應(yīng) “Hello” 字符串。通過類似的方法,我們可以通過查看 SP 對(duì)應(yīng)的棧指針位置,然后查看棧中局部變量的值。

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



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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)