[TOC]
背景
目前 Go
语言支持 GDB
、LLDB
、Delve
调试器,其中 GDB 是最早支持的调试工具,LLDB 是 macOS 系统推荐的标准调试工具。只有 Delve
是专门为 Go
语言设计开发的调试工具(比如优化了 error, channel
结构体的打印、针对协程栈扩容做了优化等),所以使用 Delve
可以轻松调试 Go
汇编程序。
delve 的安装:go install github.com/go-delve/delve/cmd/dlv@latest
验证安装:dlv version
dlv 使用
dlv 有如下常见使用方法:
debug: dlv debug ${源代码.go}
,调试 go 源码文件
attach: dlv attach ${进程pid}
,调试正在运行的进程,断点会hang住进程,导致上游健康检查失败,因此不建议线上使用
core: dlv core ${bin} ${core_file}
,分析转储文件,可用于分析程序 panic 时的现场、原因
api: ./dlv --listen=127.0.0.1:26953 --headless=true --api-version=2 --check-go-version=false --only-same-user=false attach ${pid}
,可用于远程调试
下面以 debug
方式为例,看一看如何调试 Go
源码
大家都知道向一个 nil
的切片追加元素,不会有任何问题,在源码中是怎么实现的呢?接下来我们使用 dlv
调试跟踪一下,先写一个 demo1.go
:
// dlv/demo1.go
package main
import (
"fmt"
)
func demo1() {
var s []int
s = append(s, 1)
fmt.Println(s)
}
再写个 main.go
文件调用 demo1
:
package main
func main() {
demo1()
}
进入命令行包目录,然后输入 dlv debug
进入调试
因为这里我们想看到 append
的内部实现,所以在 append
那行加上断点,执行 break
命令:
(dlv) break ./demo1.go:9
Breakpoint 1 set at 0x104ae421c for main.demo1() ./demo1.go:9
执行 continue
命令,运行到断点处:
(dlv) continue
> main.demo1() ./demo1.go:9 (hits goroutine(1):1 total:1) (PC: 0x1006a41e8)
4: "fmt"
5: )
6:
7: func demo1() {
8: var s []int
=> 9: s = append(s, 1)
接下来我们执行 si
命令进入 append
方法:
(dlv) si
> main.demo1() ./demo1.go:9 (PC: 0x1006a41ec)
demo1.go:7 0x1006a41d8 fd2300d1 SUB $8, RSP, R29
demo1.go:8 0x1006a41dc ff3f00f9 MOVD ZR, 120(RSP)
demo1.go:8 0x1006a41e0 ff7f08a9 STP (ZR, ZR), 128(RSP)
demo1.go:9 0x1006a41e4 01000014 JMP 1(PC)
demo1.go:9 0x1006a41e8* e0031faa MOVD ZR, R0
=> demo1.go:9 0x1006a41ec e10340b2 ORR $1, ZR, R1
demo1.go:9 0x1006a41f0 e2031faa MOVD ZR, R2
demo1.go:9 0x1006a41f4 e30301aa MOVD R1, R3
demo1.go:9 0x1006a41f8 240300d0 ADRP 417792(PC), R4
demo1.go:9 0x1006a41fc 84002c91 ADD $2816, R4, R4
demo1.go:9 0x1006a4200 2c20f697 CALL runtime.growslice(SB)
从以上内容我们看到调用了 runtime.growslice
方法,我们在这里加一个断点:
(dlv) break runtime.growslice
Breakpoint 2 set at 0x10042c2bc for runtime.growslice() /usr/local/go/src/runtime/slice.go:157
之后我们再次执行 continue
执行到该断点处:
(dlv) continue
> runtime.growslice() /usr/local/go/src/runtime/slice.go:157 (hits goroutine(1):1 total:1) (PC: 0x10225fd3c)
...
=> 166: func growslice(et *_type, old slice, cap int) slice {
167: if raceenabled {
168: callerpc := getcallerpc()
169: racereadrangepc(old.array, uintptr(old.len*int(et.size)), callerpc, abi.FuncPCABIInternal(growslice))
170: }
点进 /usr/local/go/src/runtime/slice.go:157
就可以看到完整的切片扩容策略;到这里大家也就明白了为啥向 nil
的切片追加数据不会有问题了,因为在容量不够时会调用 growslice
函数进行扩容
常用命令
dlv --help
:可看到 dlv 命令行支持的命令
dlv debug ${xxx.go}
:调试源代码
dlv exec ${bin}
:调试可执行文件
dlv attach ${pid}
:调试指定 pid 进程
通过 dlv debug ${xxx.go}
进入交互模式后,可通过 help/h
命令查看支持的交互命令
控制流
continue/c
:执行函数到下一断点
next/n
:跳到下一个断点
step/s
:单步调试
step-instruction/si
:执行单步 CPU 指令
stepout/so
:跳出当前单步调试到下一个断点
断点管理
break/b ${文件名:行号} 或 ${包名.函数名}
:打断点
breakpoints/bp
:列出所有断点
clear ${断点号}
:清除断点
condition/cond ${断点号} ${条件表达式}
:为断点添加条件
watch -[r/w/rw] ${变量名}
:在变量读/写时打断点
查看状态
print/p ${表达式}
:打印
stack/bt
:查看当前函数调用栈信息
locals
:查看当前函数所有变量值
funcs ${关键词}
:列出包含关键词的函数
协程相关命令
goroutines/grs
命令查看所有协程
goroutine/gr
命令查看当前协程
gr ${goid}
进入指定协程
下面放另一个例子看下上述命令的用法,先放源代码:
// dlv/demo.go
package main
import (
"fmt"
"net/http"
"time"
)
// 这段代码起了2个协程,爬取指定网站并计算各网站的延时,最后打印延时
func demo2() {
urls := []string{"https://www.google.com", "https://www.baidu.com", "https://www.facebook.com"}
urlChan := make(chan string, len(urls))
for _, url := range urls {
urlChan <- url
}
// 管道 latencies 存放各协程计算的结果
latencies := make(chan string, 2)
// totalTimes 存放各协程累积耗时
totalTimes := [2]int{}
// for 循环起 2 个协程
for i := 0; i < 2; i++ {
go func(i int) {
// 协程内通过 for 循环不断取目标网址并调用 calcLatency 函数计算延时
for url := range urlChan {
latency := calcLatency(url)
latencies <- fmt.Sprintf("%s: %dms", url, latency)
totalTimes[i] += latency
}
}(i)
}
//for i := 0; i < len(urls); i++ {
for {
println(<-latencies)
}
}
// 调用指定 url 并计算请求延时
func calcLatency(url string) int {
t1 := time.Now()
resp, _ := http.Get(url)
defer resp.Body.Close()
return int(time.Since(t1).Milliseconds())
}
然后修改下 main.go
:
package main
func main() {
demo2()
}
然后进入 dlv
:
➜ dlv git:(master) dlv debug
# 步进到 totalTimes 变量定义后
(dlv) b demo2.go:22
(dlv) c
(dlv) s
=> 25: for i := 0; i < 2; i++ {
# 监控 totalTimes[0] 的写
(dlv) watch -w totalTimes[0]
Watchpoint totalTimes[0] set at 0x1400001c180
# 在 main.calcLatency 函数打断点
(dlv) b main.calcLatency
Breakpoint 3 set at 0x103084480 for main.calcLatency() ./demo2.go:42
# 设置断点条件
(dlv) cond 3 url=="https://www.baidu.com"
# 运行到断点3
(dlv) c
> main.calcLatency() ./demo2.go:43 (hits goroutine(18):1 total:1) (PC: 0x105218860)
38: println(<-latencies)
39: }
40: }
41:
42: // 调用指定 url 并计算请求延时
=> 43: func calcLatency(url string) int {
# 查看函数参数
(dlv) args
url = "https://www.baidu.com"
~r0 = 1374389806568
# 查看协程列表
(dlv) grs
Goroutine 1 - User: ./demo2.go:38 main.demo2 (0x1052185b0) [chan receive]
Goroutine 2 - User: /usr/local/go/src/runtime/proc.go:382 runtime.gopark (0x104f8c108) [force gc (idle)]
Goroutine 3 - User: /usr/local/go/src/runtime/proc.go:382 runtime.gopark (0x104f8c108) [GC sweep wait]
Goroutine 4 - User: /usr/local/go/src/runtime/proc.go:382 runtime.gopark (0x104f8c108) [GC scavenge wait]
Goroutine 17 - User: /usr/local/go/src/runtime/proc.go:382 runtime.gopark (0x104f8c108) [finalizer wait]
* Goroutine 18 - User: ./demo2.go:43 main.calcLatency (0x105218860) (thread 299030)
Goroutine 19 - User: ./demo2.go:43 main.calcLatency (0x105218860) (thread 297552)
[7 goroutines]
# 切换协程,并查看参数
(dlv) gr 19
Switched from 18 to 19 (thread 297552)
# 打印 url 变量
(dlv) p url
"https://www.google.com"
# 查看所有断点
(dlv) bp
Breakpoint runtime-fatal-throw (enabled) at 0x104f898b0,0x104f89970 for (multiple functions)() <multiple locations>:0 (0)
Breakpoint unrecovered-panic (enabled) at 0x104f89c10 for runtime.fatalpanic() /usr/local/go/src/runtime/panic.go:1141 (0)
print runtime.curg._panic.arg
Breakpoint 1 (enabled) at 0x105218438 for main.demo2() ./demo2.go:22 (1)
Watchpoint totalTimes[0] (enabled) at 0x1400010e0e0 (0)
Breakpoint 3 (enabled) at 0x105218860 for main.calcLatency() ./demo2.go:43 (1)
cond url == "https://www.baidu.com"
# 继续执行到 watch 断点
(dlv) c
https://www.baidu.com: 102ms
> watchpoint on [totalTimes[0]] main.demo2.func1() ./demo2.go:31 (hits goroutine(18):1 total:1) (PC: 0x105218804)
26: go func(i int) {
27: // 协程内通过 for 循环不断取目标网址并调用 calcLatency 函数计算延时
28: for url := range urlChan {
29: latency := calcLatency(url)
30: latencies <- fmt.Sprintf("%s: %dms", url, latency)
=> 31: totalTimes[i] += latency
(dlv) s
(dlv) p totalTimes
[2]int [102,0]
(dlv) q
远程调试
其实很多 Golang IDE 的 debugger 使用的就是 dlv,以 IntelliJ IDEA 的 为例,点击 debug 图标后,可以在控制台输出看到它使用的 dlv 路径:
"/Users/xxx/Library/Application Support/JetBrains/IntelliJIdea2023.1/plugins/go-plugin/lib/dlv/macarm/dlv" --listen=127.0.0.1:51683 --headless=true --api-version=2 --check-go-version=false --only-same-user=false exec /Users/xxx/Library/Caches/JetBrains/IntelliJIdea2023.1/tmp/GoLand/___go_build_github_com_WTIFS_project_diva_src_myGo --
其中的 --listen
参数表示将 dlv 作为一个服务启动,可通过其后面指定的地址交互。这样可以在生产环境机器上启动 dlv,然后在 IDE 的 debugger 里配置这个地址,就可以远程 debug、短点调试了
在 IDE 里新建远程配置,然后按提示在远端机器上安装好 dlv 环境后 执行如下命令:
dlv debug --headless --listen=:2345 --api-version=2 --accept-multiclient
之后在 IDE 里打断点并启动 debug,便可以像在本地一样调试了
基本原理
delve 的源码可以看这里:https://github.com/go-delve/delve
其基本原理是:
将断点这一地址的指令覆盖掉,替换成一条新的指令:当执行该指令时,线程暂停运行并发送信号
将 dlv 进程作为被调试程序的父进程,子进程执行到断点时会发信号给 dlv 进程,dlv 进程等待用户的进一步指令
架构
Delve的整体架构如下:
UI Layer,直接与用户交互,接受用户输入的指令;各 IDE 实现的就是这层
Service Layer抽象出的一个服务层,作为 UI 和 Symbolic 层中间的层,通过 RPC 交互
Symbolic Layer,转换源码和内存地址,拥有代码行号、数据类型、变量名等信息
Target Layer,控制被调试进程,读写目标进程的寄存器、内存
Service Layer
提供 FindLocation
(根据函数名找代码地址)、CreateBreakpoint
(创建断点)这样的 API
通过这样的架构可以分离客户端和被调试程序,从而支持远端调试、支持多种客户端
API 支持 JSON/DAP
两种协议,后者主要被 VS Code
使用。基于这些 API 你也可以自己写一个客户端,API文档在这里
Symbol Layer
编译器会向可执行文件中写入一些用于调试的信息,我们称为 debug symbol,Symbolic Layer 所做的事情就是从可执行文件中读取这些符号
Go语言采用的 debug symbol 规范是 DWARFv4 (2018年),DWARF 中比较重要的有三种:
debug_line:这是一个表,它将指令地址映射到文件:行号
debug_info:描述程序中的所有函数、类型和变量
dlv 通过 debug_info 找到函数名,通过 debug_line 找到某条指令对应的代码在哪个文件的哪一行,再通过 debug_frame 获取指令地址
Target Layer
作用是控制目标进程。在delve 中提供了对 target layer 的三种实现方式:
pkg/proc/native: 使用 OS API call 控制目标进程, 如 Linux 下调用了 ptrace, waitpid, tgkill
API
pkg/proc/core: 读取 Linux 的 core dump 文件
pkg/proc/gdbserial: 通过 TCP/IP 连接服务器。采用的协议叫做 GDB Remote Serial Protocol,GDB的标准远程通信协议
这里展开看下 pkg/proc/native/proc_linux.go
里对 Attach
的实现:
// pkg/proc/native/proc_linux.go
func Attach(pid int, debugInfoDirs []string) (*proc.TargetGroup, error) {
dbp := newProcess(pid) // 新建 dlv 进程
dbp.execPtraceFunc(func() { err = ptraceAttach(dbp.pid) })
}
// pkg/proc/native/ptrace_linux.go
func ptraceAttach(pid int) error {
return sys.PtraceAttach(pid)
}
// 调用 linux 的 ptrace API
// golang.org/x/sys/unix/syscall_linux.go
func PtraceAttach(pid int) (err error) { return ptrace(PTRACE_ATTACH, pid, 0, 0) }
// golang.org/x/sys/unix/zsyscall_linux.go
func ptrace(request int, pid int, addr uintptr, data uintptr) (err error) {
_, _, e1 := Syscall6(SYS_PTRACE, uintptr(request), uintptr(pid), uintptr(addr), uintptr(data), 0, 0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
可以看到 Linux 下的 dlv attach
命令实际调用了 Linux 提供的 ptrace
API。
ptrace 系统调用于进程跟踪,它提供了父进程可以观察和控制其子进程执行的能力,并允许父进程检查和替换子进程的内核镜像(包括寄存器)的值。其基本原理是:当使用了 ptrace
跟踪后,所有发送给被跟踪的子进程的信号(除了SIGKILL),都会被转发给父进程,而子进程则会被阻塞,这时子进程的状态就会被系统标注为 TASK_TRACED
。父进程收到信号后,就可以对停止下来的子进程进行检查和修改,然后让子进程继续运行。
当我们用 dlv 设置断点时, dlv 会把断点处的指令修改成中断指令(amd/386
架构下为 int 3 (0xCC)
,3 号中断,可以触发 debugger,这是一种约定),同时把断点信息及修改前的指令保存起来。当被调试子进程运行到断点处时,便会执行中断命令产生 SIGTRAP
信号。由于 dlv 已经用 ptrace
和调试进程建立了跟踪关系,此时的 SIGTRAP
信号会被发送给 dlv,dlv 通过和已有的断点信息做对比 (通过指令位置)来判断这次 SIGTRAP
是不是一个断点。如果是的话,就等待用户的输入以做进一步的处理
源码解读
// 入口:Makefile
build: $(GO_SRC)
@go run _scripts/make.go build
命令行工具是用 cobra
构建的
// _scripts/make.go
import "github.com/spf13/cobra"
const DelveMainPackagePath = "github.com/go-delve/delve/cmd/dlv"
buildCmd := &cobra.Command{
Use: "build",
Short: "Build delve",
Run: func(cmd *cobra.Command, args []string) {
execute("go", "build", "-ldflags", "-extldflags -static", tagFlags(), buildFlags(), DelveMainPackagePath)
},
}
// cmd/dlv/main.go
func main() {
// 启动RPC服务、创建终端
cmds.New(false).Execute()
}
这里以 attach
命令为例向下追踪,可以看到起了一个 RPC 服务:
// cmd/dlv/cmds/commands.go
func New(docCall bool) *cobra.Command {
attachCommand := &cobra.Command{
Run: attachCmd,
}
}
func attachCmd(cmd *cobra.Command, args []string) {
pid, err := strconv.Atoi(args[0])
os.Exit(execute(pid, args[1:], conf, "", debugger.ExecutingOther, args, buildFlags))
}
// 启动服务、创建终端
func execute(attachPid int, processArgs []string, conf *config.Config, coreFile string, kind debugger.ExecuteKind, dlvArgs []string, buildFlags string) int {
server = rpccommon.NewServer(...)
server.Run()
return connect(listener.Addr().String(), clientConn, conf, kind)
}
Run
方法里注册了 RPC 方法,并路由了请求:
// service/rpccommon/server.go
func (s *ServerImpl) Run() error {
// v1/v2 RPC 服务
s.s1 = rpc1.NewServer(s.config, s.debugger)
s.s2 = rpc2.NewServer(s.config, s.debugger)
rpcServer := &RPCServer{s}
// 注册 RPC 方法路由
s.methodMaps = make([]map[string]*methodType, 2)
s.methodMaps[0] = map[string]*methodType{}
suitableMethods(s.s1, s.methodMaps[0], s.log)
// 监听并处理连接
go func() {
for {
c, err := s.listener.Accept()
// 路由连接并处理
go s.serveConnectionDemux(c)
}
}
}
// 路由并分发请求
func (s *ServerImpl) serveConnectionDemux(c io.ReadWriteCloser) {
if b[0] == 'C' { // C is for DAP's Content-Length
ds := dap.NewSession(conn, &dap.Config{Config: s.config, StopTriggered: s.stopChan}, s.debugger)
go ds.ServeDAPCodec()
} else {
go s.serveJSONCodec(conn)
}
}
终端部分的代码在 connect
方法里:
// 创建终端
func connect(addr string, clientConn net.Conn, conf *config.Config, kind debugger.ExecuteKind) int {
term := terminal.New(client, conf)
}
// pkg/terminal/terminal.go
// 创建终端
func New(client service.Client, conf *config.Config) *Term {
cmds := DebugCommands(client)
}
// pkg/terminal/command.go
// 终端命令实现
func DebugCommands(client service.Client) *Commands {
c := &Commands{client: client}
c.cmds = []command{
{aliases: []string{"help", "h"}, cmdFn: c.help},
{aliases: []string{"break", "b"}, group: breakCmds, cmdFn: breakpoint}
}
}
接下来以 break
命令为例,看下命令是如何从客户端传递到服务端并执行的。
客户端这边,在上面的代码里将 break
命令和 breakpoint
函数做了注册。breakpoint
方法的实现如下:
// pkg/terminal/command.go
func breakpoint(t *Term, ctx callContext, args string) error {
_, err := setBreakpoint(t, ctx, false, args)
return err
}
func setBreakpoint(t *Term, ctx callContext, tracepoint bool, argstr string) ([]*api.Breakpoint, error) {
// spec 为要打的断点,格式可为包名.函数名、文件名:行号等
spec = argstr
// RPC调用,调用 FindLocation 查找断点对应的代码的地址、程序计数器、进程id
locs, findLocErr := t.client.FindLocation(ctx.Scope, spec, true, t.substitutePathRules())
for _, loc := range locs {
requestedBp.Addr = loc.PC
requestedBp.Addrs = loc.PCs
requestedBp.AddrPid = loc.PCPids
// RPC调用,实际调用 CreateBreakpoint 方法
bp, err := t.client.CreateBreakpointWithExpr(requestedBp, spec, t.substitutePathRules(), false)
}
}
// RPC调用,实际调用 CreateBreakpoint 方法
func (c *RPCClient) CreateBreakpointWithExpr(breakPoint *api.Breakpoint, locExpr string, substitutePathRules [][2]string, suspended bool) (*api.Breakpoint, error) {
err := c.call("CreateBreakpoint", CreateBreakpointIn{*breakPoint, locExpr, substitutePathRules, suspended}, &out)
}
看一下 RPC 侧对 CreateBreakpoint
方法的实现,这里以 v2 版的实现为例:
// service/rpc2/server.go
func (s *RPCServer) CreateBreakpoint(arg CreateBreakpointIn, out *CreateBreakpointOut) error {
createdbp, err := s.debugger.CreateBreakpoint(&arg.Breakpoint, arg.LocExpr, arg.SubstitutePathRules, arg.Suspended)
}
// service/debugger/debugger.go
func (d *Debugger) CreateBreakpoint(requestedBp *api.Breakpoint, locExpr string, substitutePathRules [][2]string, suspended bool) (*api.Breakpoint, error) {
createdBp, err := createLogicalBreakpoint(d, requestedBp, &setbp, suspended)
}
func createLogicalBreakpoint(d *Debugger, requestedBp *api.Breakpoint, setbp *proc.SetBreakpoint, suspended bool) (*api.Breakpoint, error) {
err = d.target.EnableBreakpoint(lbp)
}
// pkg/proc/target_group.go
func (grp *TargetGroup) EnableBreakpoint(lbp *LogicalBreakpoint) error {
for _, p := range grp.targets {
err := enableBreakpointOnTarget(p, lbp)
}
}
func enableBreakpointOnTarget(p *Target, lbp *LogicalBreakpoint) error {
for _, addr := range addrs {
_, err = p.SetBreakpoint(lbp.LogicalID, addr, UserBreakpoint, nil)
}
}
// pkg/proc/breakpoints.go
func (t *Target) SetBreakpoint(logicalID int, addr uint64, kind BreakpointKind, cond ast.Expr) (*Breakpoint, error) {
return t.setBreakpointInternal(logicalID, addr, kind, 0, cond)
}
func (t *Target) setBreakpointInternal(logicalID int, addr uint64, kind BreakpointKind, wtype WatchType, cond ast.Expr) (*Breakpoint, error) {
err := t.proc.WriteBreakpoint(newBreakpoint)
}
WriteBreakpoint
是一个 interface
,在 target 层有不同的实现:
// pkg/proc/interface.go
WriteBreakpoint(*Breakpoint) error
// pkg/proc/native/proc.go
func (dbp *nativeProcess) WriteBreakpoint(bp *proc.Breakpoint) error {
}
// pkg/proc/core/core.go
func (dbp *nativeProcess) WriteBreakpoint(bp *proc.Breakpoint) error {
}
// pkg/proc/gdbserial/gdbserver.go
func (dbp *nativeProcess) WriteBreakpoint(bp *proc.Breakpoint) error {
}
这里追一下 native 里的实现:
// pkg/proc/native/proc.go
func (dbp *nativeProcess) WriteBreakpoint(bp *proc.Breakpoint) error {
// 如果是 watch 类型断点,使用硬中断
if bp.WatchType != 0 {
for _, thread := range dbp.threads {
err := thread.writeHardwareBreakpoint(bp.Addr, bp.WatchType, bp.HWBreakIndex)
}
return nil
}
// 读取断点地址的原数据
bp.OriginalData = make([]byte, dbp.bi.Arch.BreakpointSize())
_, err := dbp.memthread.ReadMemory(bp.OriginalData, bp.Addr)
// 其他类型断点使用软中断
return dbp.writeSoftwareBreakpoint(dbp.memthread, bp.Addr)
}
硬中断分支逻辑如下,最后实际调用 Linux ptrace 系统调用:
// pkg/proc/native/threads_linux_arm64.go
// 硬中断断点
func (t *nativeThread) writeHardwareBreakpoint(addr uint64, wtype proc.WatchType, idx uint8) error {
return t.setWatchpoints(wpstate)
}
// 实际调用 Linux ptrace 系统调用
func (t *nativeThread) setWatchpoints(wpstate *watchpointState) error {
_, _, err = syscall.Syscall6(syscall.SYS_PTRACE, sys.PTRACE_SETREGSET, uintptr(t.ID), _NT_ARM_HW_WATCH, uintptr(unsafe.Pointer(&iov)), 0, 0)
}
软中断分支逻辑如下:
// pkg/proc/native/proc.go
func (dbp *nativeProcess) writeSoftwareBreakpoint(thread *nativeThread, addr uint64) error {
_, err := thread.WriteMemory(addr, dbp.bi.Arch.BreakpointInstruction())
return err
}
dbp.bi.Arch.BreakpointInstruction()
根据不同的 CPU架构有不同的取值:
// pkg/proc/amd64_arch.go
var amd64BreakInstruction = []byte{0xCC}
// pkg/proc/arm64_arch.go
var arm64BreakInstruction = []byte{0x0, 0x0, 0x20, 0xd4}
// pkg/proc/i386_arch.go
var arm64WindowsBreakInstruction = []byte{0x0, 0x0, 0x3e, 0xd4}
i386BreakInstruction = []byte{0xCC}
WriteMemory
方法底层调用的也是 Linux ptrace 系统调用:
// pkg/proc/native/threads_linux.go
func (t *nativeThread) WriteMemory(addr uint64, data []byte) (written int, err error) {
t.dbp.execPtraceFunc(func() { written, err = sys.PtracePokeData(t.ID, uintptr(addr), data) })
}
// golang.org/x/sys/unix/syscall_linux.go
func PtracePokeData(pid int, addr uintptr, data []byte) (count int, err error) {
return ptracePoke(PTRACE_POKEDATA, PTRACE_PEEKDATA, pid, addr, data)
}
func ptracePoke(pokeReq int, peekReq int, pid int, addr uintptr, data []byte) (count int, err error) {
err = ptrace(...)
}
func ptrace(request int, pid int, addr uintptr, data uintptr) (err error) {
_, _, e1 := Syscall6(SYS_PTRACE, uintptr(request), uintptr(pid), uintptr(addr), uintptr(data), 0, 0)
}
参考
AsongGo - 分享如何阅读Go源码
曹春晖 - Delve 调试器
mercury - Dlv 深入探究
日天不吃糖 - Delve的内部架构与实现
Alessandro Arzilli - Internal Architecture of Delve