[调试][实现][原理]用Golang实现建议断点调试器
1 目的
深入理解调试的原理
2 实现
golang 版本:1.24.2
系统:Rocky Linux 9.5
2.1 被调试程序
package mainimport ("fmt""time"
)func main() {fmt.Println("start demo")// NOTE:手速快点 30秒内启动断点调试程序time.Sleep(30 * time.Second)i := 0fmt.Println(i)time.Sleep(3 * time.Second)i++fmt.Println(i)time.Sleep(3 * time.Second)i++fmt.Println(i)fmt.Println("stop demo")
}
编译时 一定要带上调试信息,默认时带调试信息的
go build -o demo demo.go
2.2 简易断点调试器
2.2.1 原理
需求:给程序指定行打断点
实现:程序运行之前代码已经被编译成一条一条的指令,运行时这些指令会被加载到内存,然后再到寄存器,CPU读取寄存器的指令来执行。打断点的原理是给运行的程序增加中断指令(INT),从而实现打断点的目的。
(1)调试程序如何与被调试程序建立联系
调试程序使用ptrace系统调用来操作被调试程序
链接:ptrace(2) - Linux manual page
ptrace系统调用:Linux/Unix 下的系统调用,全称是 Process Trace,它是实现调试器(如 gdb, strace)的核心机制。ptrace 允许一个进程(通常是调试器)去 观察和控制另一个进程(被调试进程),包括:读取 / 修改寄存器、内存;捕获系统调用(syscall);设置断点 / 单步执行;捕获信号。换句话说,调试器就是靠 ptrace 来 “附加 attach” 到目标程序,然后拦截它的执行。
使用的相关操作:
- PTRACE_ATTACH:调试器 attach 到某个正在运行的进程。
- PTRACE_DETACH:调试器脱离。脱离之前要先attach 否则会报错!
- PTRACE_PEEKDATA / PEEKUSER:读目标进程的内存/寄存器。
- PTRACE_POKEDATA / POKEUSER:写目标进程的内存/寄存器(比如往代码里写入 INT 3 (0xCC) 来设置断点)。
- PTRACE_CONT:让目标进程继续运行。
(2)如何获取需要打断点的地址
即:如何获取指定行的地址?每一行代码被编译的时候会编译成一条或多条指令,这些汇编指令的地址在编译的时候已经就确定了。可以通过读取可执行文件的dwarf信息来获取指定行编译出来的第一个指令的地址。
ELF(Executable and Linkable Format,可执行和可链接格式)是一种在 Linux 和类Unix系统中使用的标准二进制文件格式,用于表示可执行文件、目标代码(编译生成的目标文件)、共享库和核心转储文件。它包含了除机器码本身之外的额外元数据,如程序的入口点、符号表、段信息等,这些元数据使得操作系统能够正确地加载和运行程序。
DWARF(Debugging With Attributed Record Formats)是一种通用的标准调试信息格式(https://dwarfstd.org/doc/dwarf-2.0.0.pdf),用于在编译后的可执行文件和原始源代码之间建立映射关系,从而实现源代码级别的调试。它以树状结构存储信息,通过调试信息条目(DIE)描述源代码中的变量、函数、类型、以及它们与机器码的对应关系。通过Dwarf,调试器可以显示当前代码的行号、局部变量、调用堆栈等信息,便于开发者进行程序调试和崩溃信息解析。
golang也有工具来获取汇编指令的地址
go tool objdump demo | grep demo.go | more
(3)断点怎么打
INT 是x86 架构中CPU的中断指令,INT 3 就是调用 中断向量 3,它被保留专门作为 断点异常(Breakpoint Exception)。当 CPU 执行到 INT 3:一是:产生 #BP 异常(Breakpoint Exception)。二是:控制权交给中断向量表中的处理程序(如调试程序),如果没有处理程序,程序将崩溃退出。
2.2.2 代码实现
package mainimport ("debug/dwarf""debug/elf""fmt""syscall"
)// getLineAddrByNumber 获取指定代码行的地址
func getLineAddrByNumber(execPath, fileName string, lineNum int) (uintptr, error) {execFile, err := elf.Open(execPath)if err != nil {return 0, err}defer execFile.Close()dwarfData, err := execFile.DWARF()if err != nil {return 0, err}reader := dwarfData.Reader()// 从头开始读reader.Seek(0)for {entry, err := reader.Next()if err != nil {return 0, err}if entry == nil {break}if entry.Tag == dwarf.TagCompileUnit {lineReader, err := dwarfData.LineReader(entry)if err != nil {return 0, fmt.Errorf("error get line reader: %v", err)}if lineReader == nil {continue}for {entry := dwarf.LineEntry{}err = lineReader.Next(&entry)if err != nil {break}if entry.File.Name == fileName && entry.Line == lineNum {return uintptr(entry.Address), nil}}}}return 0, fmt.Errorf("not find line address")
}// insertBreakpoint 插入断点指令并返回原始指令
func insertBreakpoint(pid int, addr uintptr) (byte, error) {// 读取原始指令var origIns [1]byte_, err := syscall.PtracePeekText(pid, addr, origIns[:])if err != nil {return 0, fmt.Errorf("error read original instruction: %v", err)}// 插入断点指令 (0xCC 是 INT 3 指令)_, err = syscall.PtracePokeText(pid, addr, []byte{0xCC})if err != nil {return 0, err}fmt.Printf("Breakpoint set at address %x\n", addr)// 返回原始指令,以便之后恢复return origIns[0], nil
}func breakpointDemo(pid int, execPath string) error {// 调用ptrace系统调用的PTRACE_ATTACH操作,附加到指定的进程// attach到程序后 pid对应的程序会Stopped,会停止 不是退出if err := syscall.PtraceAttach(pid); err != nil {return fmt.Errorf("error attacting to process: %v", err)}fmt.Println("Attached to process", pid)// 调用wait4系统调用等待进程停止fmt.Println("wait process stop")var pStatus syscall.WaitStatusif _, err := syscall.Wait4(pid, &pStatus, 0, nil); err != nil {return fmt.Errorf("error wait process stop: %v", err)}// Stopped: true, Signaled: false, ExitStatus: -1, StopSignal: 19fmt.Printf("Status: %v Stopped: %v Signaled: %v ExitStatus: %d StopSignal: %d\n",pStatus, pStatus.Stopped(), pStatus.Signaled(), pStatus.ExitStatus(),pStatus.StopSignal())// 获取断点地址addr1, err := getLineAddrByNumber(execPath, "/code/local/goscripts/demo.go", 22)if err != nil {return fmt.Errorf("error get 1 line addr: %v", err)}addr2, err := getLineAddrByNumber(execPath, "/code/local/goscripts/demo.go", 25)if err != nil {return fmt.Errorf("error get 2 line addr: %v", err)}// 插入断点指令orgIns1, err := insertBreakpoint(pid, addr1)if err != nil {return fmt.Errorf("error inserting breakpoint 1: %v", err)}orgIns2, err := insertBreakpoint(pid, addr2)if err != nil {return fmt.Errorf("error inserting breakpoint 2: %v", err)}// 继续执行子进程if err := syscall.PtraceCont(pid, 0); err != nil {return fmt.Errorf("error continuing process: %v", err)}// 等待第一个断点触发_, err = syscall.Wait4(pid, &pStatus, 0, nil)if err != nil {return fmt.Errorf("error waiting for process: %v", err)}fmt.Printf("Status: %v Stopped: %v Signaled: %v ExitStatus: %d StopSignal: %d\n",pStatus, pStatus.Stopped(), pStatus.Signaled(), pStatus.ExitStatus(), pStatus.StopSignal())// 检查是否由于断点停止if pStatus.Stopped() && pStatus.StopSignal() == syscall.SIGTRAP {fmt.Println("Process hit a breakpoint 1")// 恢复原始指令_, err = syscall.PtracePokeText(pid, addr1, []byte{orgIns1})if err != nil {return fmt.Errorf("Error restoring original instruction: %v", err)}fmt.Printf("Restored original instruction at address %x\n", addr1)}// 继续执行子进程if err := syscall.PtraceCont(pid, 0); err != nil {return fmt.Errorf("error continuing process: %v", err)}// 等待第二个断点触发_, err = syscall.Wait4(pid, &pStatus, 0, nil)if err != nil {return fmt.Errorf("Error waiting for process: %v", err)}fmt.Printf("Status: %v Stopped: %v Signaled: %v ExitStatus: %d StopSignal: %d\n",pStatus, pStatus.Stopped(), pStatus.Signaled(), pStatus.ExitStatus(), pStatus.StopSignal())// 检查是否由于断点停止if pStatus.Stopped() && pStatus.StopSignal() == syscall.SIGTRAP {fmt.Println("Process hit a breakpoint 2")// 恢复原始指令_, err = syscall.PtracePokeText(pid, addr2, []byte{orgIns2})if err != nil {return fmt.Errorf("Error restoring original instruction: %v", err)}fmt.Printf("Restored original instruction at address %x\n", addr2)}// 调用ptrace系统调用的PTRACE_DETACH操作// PTRACE_DETACH 之后pid程序会自动开始运行,如果前面调用的PtraceCont这里会detach失败if err := syscall.PtraceDetach(pid); err != nil {return fmt.Errorf("Error detaching from process: %v", err)}fmt.Println("Detached from process", pid)return nil
}func main() {breakpointDemo(1628245, "/code/local/goscripts/demo")
}
2.2.3 运行结果
被调试程序输出:
调试程序输出: