Delve调试器 汇编

摘要:
目前,Go语言支持GDB、LLDB和Delve调试器。然而,GDB和LLDB都缺乏对Go语言专有特性的极大支持,只有Delve是专门为Go语言设计和开发的调试工具。在本节中,我们将简要解释如何基于Delve调试Go汇编程序。我们将首先构造一个简单的Go语言代码,以熟悉下一个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函数先通过循初始化一个切片,然后输出切片的内容:

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...
    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函数,我们可以用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异常函数设置了一个断点。

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

(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命令单步执行进入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调试器器的简单用法。

3.9.2 调试汇编程序

用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!
"
DATA  text<>+0(SB)/8,$"Hello Wo"
DATA  text<>+8(SB)/8,$"rld!
"
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!
"
     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汇编程序的简单调试技术。

(dlv) disassemble
TEXT syscall.Syscall6(SB) src/syscall/asm_linux_arm64.s
        asm_linux_arm64.s:34    0x8dca0 fe0f1ff8        MOVD.W R30, -16(RSP)
        asm_linux_arm64.s:34    0x8dca4 fd831ff8        MOVD R29, -8(RSP)
        asm_linux_arm64.s:34    0x8dca8 fd2300d1        SUB $8, RSP, R29
        asm_linux_arm64.s:35    0x8dcac b592ff97        CALL runtime.entersyscall(SB)
        asm_linux_arm64.s:36    0x8dcb0 e01340f9        MOVD 32(RSP), R0
        asm_linux_arm64.s:37    0x8dcb4 e11740f9        MOVD 40(RSP), R1
        asm_linux_arm64.s:38    0x8dcb8 e21b40f9        MOVD 48(RSP), R2
        asm_linux_arm64.s:39    0x8dcbc e31f40f9        MOVD 56(RSP), R3
        asm_linux_arm64.s:40    0x8dcc0 e42340f9        MOVD 64(RSP), R4
        asm_linux_arm64.s:41    0x8dcc4 e52740f9        MOVD 72(RSP), R5
        asm_linux_arm64.s:42    0x8dcc8 e80f40f9        MOVD 24(RSP), R8
=>      asm_linux_arm64.s:43    0x8dccc 010000d4        SVC $0
        asm_linux_arm64.s:44    0x8dcd0 1ffc3fb1        CMN $4095, R0
        asm_linux_arm64.s:45    0x8dcd4 43010054        BCC 10(PC)
        asm_linux_arm64.s:46    0x8dcd8 04008092        MOVD $-1, R4
        asm_linux_arm64.s:47    0x8dcdc e42b00f9        MOVD R4, 80(RSP)
        asm_linux_arm64.s:48    0x8dce0 ff2f00f9        MOVD ZR, 88(RSP)
        asm_linux_arm64.s:49    0x8dce4 e00300cb        NEG R0, R0
        asm_linux_arm64.s:50    0x8dce8 e03300f9        MOVD R0, 96(RSP)
        asm_linux_arm64.s:51    0x8dcec b192ff97        CALL runtime.exitsyscall(SB)
        asm_linux_arm64.s:52    0x8dcf0 fd835ff8        LDUR -8(RSP), R29
        asm_linux_arm64.s:52    0x8dcf4 fe0741f8        MOVD.P 16(RSP), R30
        asm_linux_arm64.s:52    0x8dcf8 c0035fd6        RET
        asm_linux_arm64.s:54    0x8dcfc e02b00f9        MOVD R0, 80(RSP)
        asm_linux_arm64.s:55    0x8dd00 e12f00f9        MOVD R1, 88(RSP)
        asm_linux_arm64.s:56    0x8dd04 ff3300f9        MOVD ZR, 96(RSP)
        asm_linux_arm64.s:57    0x8dd08 aa92ff97        CALL runtime.exitsyscall(SB)
        asm_linux_arm64.s:58    0x8dd0c fd835ff8        LDUR -8(RSP), R29
        asm_linux_arm64.s:1     0x8dd10 fe0741f8        MOVD.P 16(RSP), R30
        asm_linux_arm64.s:1     0x8dd14 c0035fd6        RET
        asm_linux_arm64.s:1     0x8dd18 00000000        ?
        asm_linux_arm64.s:1     0x8dd1c 00000000        ?
(dlv) 
(dlv) regs
 PC = 0x000000000008dccc
 SP = 0x0000004000691560
 X0 = 0x0000004000266000
 X1 = 0x0000000000000004
 X2 = 0x0000000000000000
 X3 = 0x0000000000000000
 X4 = 0x0000000000000000
 X5 = 0x0000000000000000
 X6 = 0x0000000000000001
 X7 = 0x0000000000000004
 X8 = 0x0000000000000049
 X9 = 0x0000004000266010
X10 = 0x0000000000000002
X11 = 0x0000000000000200
X12 = 0x0000000000000003
X13 = 0x000000000000001b
X14 = 0x0000000000000001
X15 = 0x0000000000000000
X16 = 0x0000000000000000
X17 = 0x0000000000000008
X18 = 0x0000000000000000
X19 = 0x0000000000000150
X20 = 0x0000ffffdaabb040
X21 = 0x0000000001504260
X22 = 0x0000004000002000
X23 = 0x0000000000000000
X24 = 0x0000000000000000
X25 = 0x0000000000000000
X26 = 0x0000004000691768
X27 = 0x0000000001503680
X28 = 0x0000004000001980
X29 = 0x0000004000691558
X30 = 0x000000000008dcb0

免责声明:文章转载自《Delve调试器 汇编》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇Bypass_disable_func利用C#线程窗口调试多线程程序下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

Linux知识积累 (9) 创建用户、分配权限和更改所有者

一、useradd和adduser 1、useradd命令: 用于Linux中创建的新的系统用户。 useradd可用来建立用户帐号。帐号建好之后,再用passwd设定帐号的密码. 而可用userdel删除帐号。使用useradd指令所建立的帐号,实际上是保存在/etc/passwd文本文件中。 2、adduser命令:  在Slackware中,a...

linux 开机自启 假死自启 (史上最全)

狂创客圈 经典图书 : 《Netty Zookeeper Redis 高并发实战》 面试必备 + 面试必备 + 面试必备 【博客园总入口 】 疯狂创客圈 经典图书 : 《SpringCloud、Nginx高并发核心编程》 大厂必备 + 大厂必备 + 大厂必备 【博客园总入口 】 入大厂+涨工资必备: 高并发【 亿级流量IM实战】 实战系列 【 Sprin...

Linux命令行:对内容进行大小写字符转换 ​​​​

如果有一个字符串要确保全部是大写的,只需通过tr命令替换: [root@localhost ~]# echo "Hello World" | tr [:lower:] [:upper:] HELLO WORLD 下面是在脚本中使用此命令的示例,需要确保添加到文件中的所有文本都是大写,以保持一致性: #!/bin/bash read -p "Enter...

linux 搭建 nexus 私服及配置

安装篇 1、tar -zxvf nexus-latest-bundle.tar.gz 2、cdnexus-2.13.0-01/bin 3、./nexus start 这时可能提示 ****************************************WARNING – NOT RECOMMENDED TO RUN AS ROOT*********...

Linux内核调试方法总结之栈帧

栈帧 栈帧和指针可以说是C语言的精髓。栈帧是一种特殊的数据结构,在C语言函数调用时,栈帧用来保存当前函数的父一级函数的栈底指针,当前函数的局部变量以及被调用函数返回后下一条汇编指令的地址。如下图所示:                         栈帧位于栈内存中,接下里我们用一个实例展示一下栈帧的入栈和退栈过程。  stackframe.c #incl...

如何调试带参数的控制台程序(QT+OSG为例)

因为这两天,想熟悉下QT+OSG框架搭建,所以找到osg/examples下的osgviewerQT几个文件放到vs下调试。因为刚开始编译的时候,没有安装qt环境,所以只有osgviewerQT的代码,没有生成工程。 我装的是vs2008,不过这个过程,应该跟vs版本无关,下面具体来说: 1.先新建qt application工程,默认应该会生成main和...