当前位置: 首页 > news >正文

[调试][实现][原理]用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 运行结果

被调试程序输出:

调试程序输出:

http://www.dtcms.com/a/355310.html

相关文章:

  • 裸金属服务器与虚拟机、物理机的核心差异是什么?
  • 鸿蒙Harmony-从零开始构建类似于安卓GreenDao的ORM数据库(二)
  • Kea DHCP高危漏洞CVE-2025-40779:单个数据包即可导致服务器崩溃
  • 获取小红书某个用户列表
  • Swift 解法详解 LeetCode 365:水壶问题
  • day083-Filebeat数据采集案例与Kibana可视化应用
  • Leetcode 494. 目标和 动态规划-01背包
  • 【踩坑记录】SpringBoot 项目报 Communications link failure 的解决方法
  • 数据结构初阶:详解双链表
  • 在angular20中使用monaco-editor
  • LLM Agent工具设计规范总结
  • Python 实现归并排序:从原理到代码
  • Ansys Electronics Desktop 2025 R2 软件界面介绍
  • open webui源码分析10-四个特征之搜索
  • 系统架构师-软件工程
  • 前端响应式设计
  • 推荐系统王树森(五)重排多样性提升
  • 银河麒麟桌面操作系统:为什么不让root直接登录图形界面?以及如何安全地解决这个问题
  • 英伟达营收和利润均超出市场预期,但盘后一度跌超5%
  • Groovy语法入门
  • [光学原理与应用-321]:皮秒深紫外激光器产品不同阶段使用的工具软件、对应的输出文件
  • 客户案例 | 国际知名内衣品牌x甄知科技,领航IT服务新征程
  • linux 网络:并发服务器及IO多路复用
  • 开发避坑指南(42):Vue3 element ui el-table单选实现方案
  • HTML静态页面实现材积计算器和材积表生成器
  • 动手学深度学习(pytorch版):第七章节—现代卷积神经网络(5)批量规范化
  • 吴炳锡:AI 时代下的湖仓一体化平台建设的思考
  • 从枯燥C++到趣味音乐:我的Windows系统底层探索之旅
  • 公共字段自动填充
  • Foundry工具大全