1 目的
深入理解调试的原理
2 实现
golang 版本:1.24.2
系统:Rocky Linux 9.5
2.1 被调试程序
Go
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println("start demo")
// NOTE:手速快点 30秒内启动断点调试程序
time.Sleep(30 * time.Second)
i := 0
fmt.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也有工具来获取汇编指令的地址
bash
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 代码实现
Go
package main
import (
"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.WaitStatus
if _, 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: 19
fmt.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 运行结果
被调试程序输出:

调试程序输出:
