feat(debug): complete the interactive debugger with disassembly, breakpoints, watchpoints and execution control
Assisted-by: DeepSeek V4 Pro
This commit is contained in:
+8
-6
@@ -23,7 +23,7 @@ import (
|
||||
// operands require relocations and are not yet supported; the SIMD (VEX/AVX2)
|
||||
// integer and shuffle/extract/permute/move set is in.
|
||||
func Assemble(t *ast.Text) ([]byte, map[string]int, error) {
|
||||
code, _, labels, _, err := assemble(t, nil)
|
||||
code, _, labels, _, _, err := assemble(t, nil)
|
||||
return code, labels, err
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ type spadjStep struct {
|
||||
// assemble encodes a TEXT body, returning the machine code, the static-symbol
|
||||
// patch sites (for the file-level layout to resolve), the label table and the
|
||||
// stack-adjustment boundaries.
|
||||
func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, []spadjStep, error) {
|
||||
func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, []spadjStep, []LineEntry, error) {
|
||||
fi := computeFrame(t)
|
||||
chain := jumpChain(t)
|
||||
resolve := func(name string) string {
|
||||
@@ -84,7 +84,7 @@ func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, [
|
||||
case *ast.Instr:
|
||||
sz, err := instrSize(s, fi, long[i], link)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, fmt.Errorf("%s: %w", s.Mnemonic.Text, err)
|
||||
return nil, nil, nil, nil, nil, fmt.Errorf("%s: %w", s.Mnemonic.Text, err)
|
||||
}
|
||||
sizes[i] = sz
|
||||
pcs[i] = pos
|
||||
@@ -125,6 +125,7 @@ func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, [
|
||||
out := append([]byte(nil), fi.prologue...)
|
||||
var patches []sbPatch
|
||||
var steps []spadjStep
|
||||
var lines []LineEntry
|
||||
if fi.useFP {
|
||||
// PUSHQ BP saves the return-address-relative base (+8); the MOVQ
|
||||
// changes nothing; SUBQ $size, SP completes the frame.
|
||||
@@ -150,16 +151,17 @@ func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, [
|
||||
}
|
||||
code, ps, err := encodeInstr(s, pos, offsets, fi, long[i], resolve, link)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, fmt.Errorf("%s: %w", s.Mnemonic.Text, err)
|
||||
return nil, nil, nil, nil, nil, fmt.Errorf("%s: %w", s.Mnemonic.Text, err)
|
||||
}
|
||||
if len(code) != sizes[i] {
|
||||
return nil, nil, nil, nil, fmt.Errorf("%s: size mismatch (%d vs %d)", s.Mnemonic.Text, len(code), sizes[i])
|
||||
return nil, nil, nil, nil, nil, fmt.Errorf("%s: size mismatch (%d vs %d)", s.Mnemonic.Text, len(code), sizes[i])
|
||||
}
|
||||
patches = append(patches, ps...)
|
||||
lines = append(lines, LineEntry{Offset: pos, Line: s.Pos().Line})
|
||||
out = append(out, code...)
|
||||
pos += len(code)
|
||||
}
|
||||
return out, patches, offsets, steps, nil
|
||||
return out, patches, offsets, steps, lines, nil
|
||||
}
|
||||
|
||||
// jumpChain precomputes jump-to-jump folding: a label whose first instruction
|
||||
|
||||
+32
-1
@@ -41,6 +41,7 @@ type FuncLayout struct {
|
||||
Labels map[string]int // local labels, function-relative
|
||||
Relocs []Reloc // static-symbol references, in emission order
|
||||
Spadj []SpadjStep // stack-adjustment boundaries, ascending by PC
|
||||
Lines []LineEntry // source-line table: byte offset → source line
|
||||
}
|
||||
|
||||
// SpadjStep is one stack-adjustment boundary: Value is the SP delta from the
|
||||
@@ -50,6 +51,35 @@ type SpadjStep struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
// LineEntry maps a byte offset (function-relative) to a source line number.
|
||||
type LineEntry struct {
|
||||
Offset int
|
||||
Line int
|
||||
}
|
||||
|
||||
// LineAt returns the source line number for the given function-relative byte
|
||||
// offset, using a binary search on the line table. Returns 0 if the offset
|
||||
// is before the first instruction or the table is empty.
|
||||
func (fl *FuncLayout) LineAt(offset int) int {
|
||||
if len(fl.Lines) == 0 {
|
||||
return 0
|
||||
}
|
||||
// Binary search: find the last entry with Offset <= offset.
|
||||
lo, hi := 0, len(fl.Lines)-1
|
||||
for lo < hi {
|
||||
mid := (lo + hi + 1) / 2
|
||||
if fl.Lines[mid].Offset <= offset {
|
||||
lo = mid
|
||||
} else {
|
||||
hi = mid - 1
|
||||
}
|
||||
}
|
||||
if fl.Lines[lo].Offset <= offset {
|
||||
return fl.Lines[lo].Line
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Reloc is one static-symbol reference within a function body: the disp32
|
||||
// field at Off (function-relative) must reach the symbol plus Addend,
|
||||
// measured from After, the address just past the instruction. An External
|
||||
@@ -110,7 +140,7 @@ func AssembleFile(f *ast.File) (*Image, error) {
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
code, patches, labels, steps, err := assemble(t, link)
|
||||
code, patches, labels, steps, lines, err := assemble(t, link)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", t.Name.Name, err)
|
||||
}
|
||||
@@ -124,6 +154,7 @@ func AssembleFile(f *ast.File) (*Image, error) {
|
||||
Args: argsSize(t),
|
||||
Line: t.Pos().Line,
|
||||
Labels: labels,
|
||||
Lines: lines,
|
||||
}
|
||||
for _, f := range t.Flags {
|
||||
switch f {
|
||||
|
||||
@@ -87,6 +87,12 @@ REPL commands:
|
||||
|
||||
bm := debug.NewBreakpoints(sess)
|
||||
fmt.Printf("gasm debug: %s in %s (pid %d)\n", *funcName, path, sess.Pid())
|
||||
debug.REPL(sess, bm, sess.CodeBase(), fl.Offset, fl.Size, fl.Args, labels)
|
||||
|
||||
// Convert the line table for the REPL.
|
||||
var srcLines []debug.SourceLine
|
||||
for _, le := range fl.Lines {
|
||||
srcLines = append(srcLines, debug.SourceLine{Offset: le.Offset, Line: le.Line})
|
||||
}
|
||||
debug.REPL(sess, bm, sess.CodeBase(), fl.Offset, fl.Size, fl.Args, labels, srcLines)
|
||||
return 0
|
||||
}
|
||||
|
||||
+118
-1
@@ -13,9 +13,77 @@ type Breakpoint struct {
|
||||
Label string // source label ("" for raw addresses)
|
||||
Orig byte // original byte at Addr (restored on removal)
|
||||
Enabled bool
|
||||
Cond *Condition // optional condition (nil = unconditional)
|
||||
hits int
|
||||
}
|
||||
|
||||
// Condition is a simple register-comparison condition evaluated when a
|
||||
// breakpoint is hit. Format: <reg> <op> <value>.
|
||||
type Condition struct {
|
||||
Reg string // register name (rax, rbx, rip, rsp, ...)
|
||||
Op string // comparison operator: ==, !=, <, >, <=, >=
|
||||
Value uint64
|
||||
}
|
||||
|
||||
// Eval checks the condition against the current registers.
|
||||
func (c *Condition) Eval(regs *Regs) bool {
|
||||
var actual uint64
|
||||
switch c.Reg {
|
||||
case "rax", "eax", "ax", "al":
|
||||
actual = regs.RAX
|
||||
case "rbx", "ebx", "bx", "bl":
|
||||
actual = regs.RBX
|
||||
case "rcx", "ecx", "cx", "cl":
|
||||
actual = regs.RCX
|
||||
case "rdx", "edx", "dx", "dl":
|
||||
actual = regs.RDX
|
||||
case "rsi", "esi", "si":
|
||||
actual = regs.RSI
|
||||
case "rdi", "edi", "di":
|
||||
actual = regs.RDI
|
||||
case "rbp", "ebp", "bp":
|
||||
actual = regs.RBP
|
||||
case "rsp", "esp", "sp":
|
||||
actual = regs.RSP
|
||||
case "r8":
|
||||
actual = regs.R8
|
||||
case "r9":
|
||||
actual = regs.R9
|
||||
case "r10":
|
||||
actual = regs.R10
|
||||
case "r11":
|
||||
actual = regs.R11
|
||||
case "r12":
|
||||
actual = regs.R12
|
||||
case "r13":
|
||||
actual = regs.R13
|
||||
case "r14":
|
||||
actual = regs.R14
|
||||
case "r15":
|
||||
actual = regs.R15
|
||||
case "rip", "eip":
|
||||
actual = regs.RIP
|
||||
default:
|
||||
return true // unknown register — don't block
|
||||
}
|
||||
switch c.Op {
|
||||
case "==", "=":
|
||||
return actual == c.Value
|
||||
case "!=":
|
||||
return actual != c.Value
|
||||
case "<":
|
||||
return actual < c.Value
|
||||
case ">":
|
||||
return actual > c.Value
|
||||
case "<=":
|
||||
return actual <= c.Value
|
||||
case ">=":
|
||||
return actual >= c.Value
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Breakpoints manages the set of breakpoints for a Session.
|
||||
type Breakpoints struct {
|
||||
s *Session
|
||||
@@ -29,8 +97,14 @@ func NewBreakpoints(s *Session) *Breakpoints {
|
||||
|
||||
// Set installs a breakpoint at addr (replaces any existing one).
|
||||
func (bm *Breakpoints) Set(addr uint64, label string) (*Breakpoint, error) {
|
||||
return bm.SetWithCond(addr, label, nil)
|
||||
}
|
||||
|
||||
// SetWithCond installs a breakpoint with an optional condition.
|
||||
func (bm *Breakpoints) SetWithCond(addr uint64, label string, cond *Condition) (*Breakpoint, error) {
|
||||
if bp, ok := bm.bps[addr]; ok {
|
||||
bp.Enabled = true
|
||||
bp.Cond = cond
|
||||
return bp, nil
|
||||
}
|
||||
// Read the original byte.
|
||||
@@ -44,11 +118,42 @@ func (bm *Breakpoints) Set(addr uint64, label string) (*Breakpoint, error) {
|
||||
if err := bm.s.Poke(addr, patched); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bp := &Breakpoint{Addr: addr, Label: label, Orig: orig, Enabled: true}
|
||||
bp := &Breakpoint{Addr: addr, Label: label, Orig: orig, Enabled: true, Cond: cond}
|
||||
bm.bps[addr] = bp
|
||||
return bp, nil
|
||||
}
|
||||
|
||||
// Hits returns the number of times the breakpoint has been hit.
|
||||
func (bp *Breakpoint) Hits() int {
|
||||
return bp.hits
|
||||
}
|
||||
|
||||
// Info returns a formatted list of all breakpoints.
|
||||
func (bm *Breakpoints) Info() string {
|
||||
if len(bm.bps) == 0 {
|
||||
return "no breakpoints set\n"
|
||||
}
|
||||
result := ""
|
||||
i := 0
|
||||
for _, bp := range bm.bps {
|
||||
i++
|
||||
status := "enabled"
|
||||
if !bp.Enabled {
|
||||
status = "disabled"
|
||||
}
|
||||
label := bp.Label
|
||||
if label == "" {
|
||||
label = fmt.Sprintf("%#x", bp.Addr)
|
||||
}
|
||||
cond := ""
|
||||
if bp.Cond != nil {
|
||||
cond = fmt.Sprintf(" if %s %s %#x", bp.Cond.Reg, bp.Cond.Op, bp.Cond.Value)
|
||||
}
|
||||
result += fmt.Sprintf(" %d: %s at %#x [%s, %d hits]%s\n", i, label, bp.Addr, status, bp.hits, cond)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Clear removes the breakpoint at addr, restoring the original byte.
|
||||
func (bm *Breakpoints) Clear(addr uint64) error {
|
||||
bp, ok := bm.bps[addr]
|
||||
@@ -102,6 +207,18 @@ func (bm *Breakpoints) HandleTrap(regs *Regs) *Breakpoint {
|
||||
if !ok || !bp.Enabled {
|
||||
return nil // single-step trap or unknown
|
||||
}
|
||||
// Check the condition (if any).
|
||||
if bp.Cond != nil && !bp.Cond.Eval(regs) {
|
||||
// Condition not met — restore the byte but do NOT rewind RIP.
|
||||
// The process continues from the next instruction (past the INT3).
|
||||
word, err := bm.s.Peek(trapAddr)
|
||||
if err == nil {
|
||||
restored := (word &^ 0xFF) | uint64(bp.Orig)
|
||||
bm.s.Poke(trapAddr, restored)
|
||||
}
|
||||
// RIP is already past the INT3 (trapAddr + 1). Don't rewind.
|
||||
return nil
|
||||
}
|
||||
bp.hits++
|
||||
// Restore the original byte.
|
||||
word, err := bm.s.Peek(trapAddr)
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux && amd64
|
||||
|
||||
package debug
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/arch/x86/x86asm"
|
||||
)
|
||||
|
||||
// Disassemble decodes the instruction at the given address in the debuggee's
|
||||
// memory and returns its text representation and length in bytes.
|
||||
func (s *Session) Disassemble(addr uint64) (string, int, error) {
|
||||
// Read up to 15 bytes (max x86 instruction length).
|
||||
mem, err := s.ReadMemory(addr, 15)
|
||||
if err != nil {
|
||||
// Try a shorter read if we're near a page boundary.
|
||||
mem, err = s.ReadMemory(addr, 1)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
}
|
||||
inst, err := x86asm.Decode(mem, 64)
|
||||
if err != nil {
|
||||
return "???", 1, nil
|
||||
}
|
||||
text := x86asm.IntelSyntax(inst, addr, nil)
|
||||
return text, inst.Len, nil
|
||||
}
|
||||
|
||||
// DisassembleN decodes up to n instructions starting at addr and returns
|
||||
// them as a formatted string with addresses and byte offsets.
|
||||
func (s *Session) DisassembleN(addr uint64, n int) string {
|
||||
var result string
|
||||
pc := addr
|
||||
for i := 0; i < n; i++ {
|
||||
text, length, err := s.Disassemble(pc)
|
||||
if err != nil {
|
||||
result += fmt.Sprintf(" %#08x: <error: %v>\n", pc, err)
|
||||
break
|
||||
}
|
||||
result += fmt.Sprintf(" %#08x: %s\n", pc, text)
|
||||
if length == 0 {
|
||||
length = 1
|
||||
}
|
||||
pc += uint64(length)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -222,6 +222,34 @@ func (s *Session) ReadMemory(addr uint64, length int) ([]byte, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// WriteMemory writes bytes to the debuggee's memory at addr.
|
||||
func (s *Session) WriteMemory(addr uint64, data []byte) error {
|
||||
for i := 0; i < len(data); i += 8 {
|
||||
end := i + 8
|
||||
if end > len(data) {
|
||||
end = len(data)
|
||||
}
|
||||
var word uint64
|
||||
for j := 0; j < end-i; j++ {
|
||||
word |= uint64(data[i+j]) << (8 * j)
|
||||
}
|
||||
// For partial writes, read-modify-write the existing word.
|
||||
if end-i < 8 {
|
||||
existing, err := s.Peek(addr + uint64(i))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Clear the bytes we're overwriting and merge.
|
||||
mask := ^((uint64(1) << (8 * (end - i))) - 1)
|
||||
word = (existing & mask) | word
|
||||
}
|
||||
if err := s.Poke(addr+uint64(i), word); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Step executes a single instruction in the debuggee.
|
||||
func (s *Session) Step() error {
|
||||
if s.exited {
|
||||
|
||||
+417
-25
@@ -20,12 +20,18 @@ type Label struct {
|
||||
Offset int // function-relative offset
|
||||
}
|
||||
|
||||
// SourceLine maps a byte offset to a source line number.
|
||||
type SourceLine struct {
|
||||
Offset int
|
||||
Line int
|
||||
}
|
||||
|
||||
// REPL runs the interactive debugger loop. On entry, the debuggee is
|
||||
// stopped in the Go runtime (after PTRACE_TRACEME + SIGSTOP). The REPL
|
||||
// sets a temporary breakpoint at the function entry, continues to it, and
|
||||
// then presents the prompt — so the user starts debugging at the first
|
||||
// instruction of the assembled function.
|
||||
func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, argsSize int, labels []Label) {
|
||||
func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, argsSize int, labels []Label, lines []SourceLine) {
|
||||
entryAddr := codeBase + uint64(funcOffset)
|
||||
|
||||
// Run to the function entry.
|
||||
@@ -44,7 +50,7 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
}
|
||||
|
||||
fmt.Printf("stopped at function entry: %#x (%d bytes)\n", entryAddr, funcSize)
|
||||
fmt.Println("commands: break <label|addr> | step [n] | continue | regs | x <addr> [len] | labels | quit")
|
||||
fmt.Println("commands: break <label|addr> | step [n] | continue | disas [n] | regs | where | x <addr> [len] | w <addr> <val...> | labels | quit")
|
||||
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
|
||||
@@ -90,7 +96,82 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
}
|
||||
if !s.Exited() {
|
||||
regs, _ := s.GetRegs()
|
||||
fmt.Printf("=> %#x (func+%#x)\n", regs.RIP, regs.RIP-codeBase-uint64(funcOffset))
|
||||
text, _, _ := s.Disassemble(regs.RIP)
|
||||
fmt.Printf("=> %#x (func+%#x): %s\n", regs.RIP, regs.RIP-codeBase-uint64(funcOffset), text)
|
||||
}
|
||||
|
||||
case "next", "n":
|
||||
// Step over: if the current instruction is a CALL, set a
|
||||
// breakpoint after it and continue; otherwise single-step.
|
||||
regs, _ := s.GetRegs()
|
||||
text, instLen, _ := s.Disassemble(regs.RIP)
|
||||
if strings.HasPrefix(strings.ToLower(text), "call") {
|
||||
// Set a temporary breakpoint after the CALL.
|
||||
afterAddr := regs.RIP + uint64(instLen)
|
||||
bp, err := bm.Set(afterAddr, "(next)")
|
||||
if err != nil {
|
||||
fmt.Printf("cannot set next breakpoint: %v\n", err)
|
||||
continue
|
||||
}
|
||||
// Continue until the breakpoint.
|
||||
for _, b := range bm.All() {
|
||||
bm.Reinsert(b.Addr)
|
||||
}
|
||||
if err := s.Continue(); err != nil {
|
||||
fmt.Println(err)
|
||||
bm.Clear(afterAddr)
|
||||
continue
|
||||
}
|
||||
bm.HandleTrap(®s)
|
||||
bm.Clear(afterAddr)
|
||||
_ = bp
|
||||
} else {
|
||||
// Not a CALL — just single-step.
|
||||
if err := s.Step(); err != nil {
|
||||
fmt.Println(err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if !s.Exited() {
|
||||
regs, _ := s.GetRegs()
|
||||
text, _, _ := s.Disassemble(regs.RIP)
|
||||
fmt.Printf("=> %#x (func+%#x): %s\n", regs.RIP, regs.RIP-codeBase-uint64(funcOffset), text)
|
||||
}
|
||||
|
||||
case "finish", "fin":
|
||||
// Run until the current function returns.
|
||||
// For NOSPLIT frame=0: return address is at [RSP].
|
||||
regs, _ := s.GetRegs()
|
||||
retAddr, err := s.Peek(regs.RSP)
|
||||
if err != nil {
|
||||
fmt.Printf("cannot read return address: %v\n", err)
|
||||
continue
|
||||
}
|
||||
// Set a temporary breakpoint at the return address.
|
||||
bp, err := bm.Set(retAddr, "(finish)")
|
||||
if err != nil {
|
||||
fmt.Printf("cannot set finish breakpoint: %v\n", err)
|
||||
continue
|
||||
}
|
||||
// Continue until the breakpoint.
|
||||
for _, b := range bm.All() {
|
||||
bm.Reinsert(b.Addr)
|
||||
}
|
||||
if err := s.Continue(); err != nil {
|
||||
fmt.Println(err)
|
||||
bm.Clear(retAddr)
|
||||
continue
|
||||
}
|
||||
if !s.Exited() {
|
||||
bm.HandleTrap(®s)
|
||||
}
|
||||
bm.Clear(retAddr)
|
||||
_ = bp
|
||||
if s.Exited() {
|
||||
fmt.Println("debuggee exited")
|
||||
} else {
|
||||
regs, _ := s.GetRegs()
|
||||
fmt.Printf("finished, now at %#x\n", regs.RIP)
|
||||
}
|
||||
|
||||
case "continue", "c":
|
||||
@@ -98,41 +179,97 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
fmt.Println("debuggee exited")
|
||||
continue
|
||||
}
|
||||
// Re-insert all breakpoints before continuing.
|
||||
for _, bp := range bm.All() {
|
||||
bm.Reinsert(bp.Addr)
|
||||
}
|
||||
if err := s.Continue(); err != nil {
|
||||
fmt.Println(err)
|
||||
continue
|
||||
}
|
||||
regs, _ := s.GetRegs()
|
||||
if bp := bm.HandleTrap(®s); bp != nil {
|
||||
name := bp.Label
|
||||
if name == "" {
|
||||
name = fmt.Sprintf("%#x", bp.Addr)
|
||||
// Loop: continue until a breakpoint fires (condition met) or exit.
|
||||
for {
|
||||
// Re-insert all breakpoints before continuing.
|
||||
for _, bp := range bm.All() {
|
||||
bm.Reinsert(bp.Addr)
|
||||
}
|
||||
fmt.Printf("breakpoint hit: %s (func+%#x)\n", name, bp.Addr-codeBase-uint64(funcOffset))
|
||||
} else if !s.Exited() {
|
||||
fmt.Printf("stopped at %#x\n", regs.RIP)
|
||||
if err := s.Continue(); err != nil {
|
||||
fmt.Println(err)
|
||||
break
|
||||
}
|
||||
if s.Exited() {
|
||||
fmt.Println("debuggee exited")
|
||||
break
|
||||
}
|
||||
// Check for watchpoint hits.
|
||||
reason, wpAddr := s.StopInfo()
|
||||
if reason == StopWatchpoint {
|
||||
fmt.Printf("watchpoint hit at %#x\n", wpAddr)
|
||||
break
|
||||
}
|
||||
regs, _ := s.GetRegs()
|
||||
if bp := bm.HandleTrap(®s); bp != nil {
|
||||
name := bp.Label
|
||||
if name == "" {
|
||||
name = fmt.Sprintf("%#x", bp.Addr)
|
||||
}
|
||||
fmt.Printf("breakpoint hit: %s (func+%#x)\n", name, bp.Addr-codeBase-uint64(funcOffset))
|
||||
break
|
||||
}
|
||||
// Condition not met (or single-step trap) — re-insert and continue.
|
||||
}
|
||||
|
||||
case "break", "b":
|
||||
if len(parts) < 2 {
|
||||
fmt.Println("usage: break <label|addr>")
|
||||
fmt.Println("usage: break <label|addr|line> [if <reg> <op> <val>]")
|
||||
continue
|
||||
}
|
||||
addr, label := resolveAddr(parts[1], codeBase, uint64(funcOffset), labels)
|
||||
// Try as a line number first.
|
||||
var addr uint64
|
||||
var label string
|
||||
if lineNum, err := strconv.Atoi(parts[1]); err == nil && lineNum > 0 {
|
||||
// Find the byte offset for this line.
|
||||
off := offsetForLine(lines, lineNum)
|
||||
if off < 0 {
|
||||
fmt.Printf("no instruction at line %d\n", lineNum)
|
||||
continue
|
||||
}
|
||||
addr = codeBase + uint64(funcOffset) + uint64(off)
|
||||
label = fmt.Sprintf("line %d", lineNum)
|
||||
} else {
|
||||
addr, label = resolveAddr(parts[1], codeBase, uint64(funcOffset), labels)
|
||||
}
|
||||
if addr == 0 {
|
||||
fmt.Printf("unknown label or address: %s\n", parts[1])
|
||||
fmt.Printf("unknown label, address, or line: %s\n", parts[1])
|
||||
continue
|
||||
}
|
||||
bp, err := bm.Set(addr, label)
|
||||
// Parse optional condition: "if <reg> <op> <value>"
|
||||
var cond *Condition
|
||||
if len(parts) >= 6 && parts[2] == "if" {
|
||||
val, err := strconv.ParseUint(parts[5], 0, 64)
|
||||
if err != nil {
|
||||
fmt.Printf("invalid condition value: %s\n", parts[5])
|
||||
continue
|
||||
}
|
||||
cond = &Condition{Reg: strings.ToLower(parts[3]), Op: parts[4], Value: val}
|
||||
} else if len(parts) >= 4 && parts[2] == "if" {
|
||||
fmt.Println("usage: break <label|addr> if <reg> <op> <value>")
|
||||
continue
|
||||
}
|
||||
bp, err := bm.SetWithCond(addr, label, cond)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
continue
|
||||
}
|
||||
fmt.Printf("breakpoint set: %s at %#x (func+%#x)\n", bp.Label, bp.Addr, bp.Addr-codeBase-uint64(funcOffset))
|
||||
condStr := ""
|
||||
if cond != nil {
|
||||
condStr = fmt.Sprintf(" if %s %s %#x", cond.Reg, cond.Op, cond.Value)
|
||||
}
|
||||
fmt.Printf("breakpoint set: %s at %#x (func+%#x)%s\n", bp.Label, bp.Addr, bp.Addr-codeBase-uint64(funcOffset), condStr)
|
||||
|
||||
case "info":
|
||||
if len(parts) < 2 {
|
||||
fmt.Println("usage: info break")
|
||||
continue
|
||||
}
|
||||
switch parts[1] {
|
||||
case "break", "breakpoints", "b":
|
||||
fmt.Print(bm.Info())
|
||||
default:
|
||||
fmt.Printf("unknown info target: %s\n", parts[1])
|
||||
}
|
||||
|
||||
case "delete", "d":
|
||||
if len(parts) < 2 {
|
||||
@@ -167,6 +304,56 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
}
|
||||
hexDump(addr, mem)
|
||||
|
||||
case "w":
|
||||
if len(parts) < 3 {
|
||||
fmt.Println("usage: w <addr> <byte|0x...> [byte...]")
|
||||
continue
|
||||
}
|
||||
addr, _ := resolveAddr(parts[1], codeBase, uint64(funcOffset), labels)
|
||||
if addr == 0 {
|
||||
fmt.Printf("unknown address: %s\n", parts[1])
|
||||
continue
|
||||
}
|
||||
var bytes []byte
|
||||
for _, arg := range parts[2:] {
|
||||
v, err := strconv.ParseUint(arg, 0, 64)
|
||||
if err != nil {
|
||||
fmt.Printf("invalid value: %s\n", arg)
|
||||
continue
|
||||
}
|
||||
// Write as 8-byte word if it looks like a large value, else single byte.
|
||||
if v > 255 {
|
||||
for j := 0; j < 8; j++ {
|
||||
bytes = append(bytes, byte(v>>(8*j)))
|
||||
}
|
||||
} else {
|
||||
bytes = append(bytes, byte(v))
|
||||
}
|
||||
}
|
||||
if len(bytes) > 0 {
|
||||
if err := s.WriteMemory(addr, bytes); err != nil {
|
||||
fmt.Println(err)
|
||||
} else {
|
||||
fmt.Printf("wrote %d bytes at %#x\n", len(bytes), addr)
|
||||
}
|
||||
}
|
||||
|
||||
case "set":
|
||||
if len(parts) < 3 {
|
||||
fmt.Println("usage: set <reg> <value>")
|
||||
continue
|
||||
}
|
||||
val, err := strconv.ParseUint(parts[2], 0, 64)
|
||||
if err != nil {
|
||||
fmt.Printf("invalid value: %s\n", parts[2])
|
||||
continue
|
||||
}
|
||||
if err := s.SetReg(strings.ToLower(parts[1]), val); err != nil {
|
||||
fmt.Printf("set: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("%s = %#x\n", parts[1], val)
|
||||
}
|
||||
|
||||
case "labels", "l":
|
||||
sorted := make([]Label, len(labels))
|
||||
copy(sorted, labels)
|
||||
@@ -175,6 +362,132 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
fmt.Printf(" func+%#04x %s\n", l.Offset, l.Name)
|
||||
}
|
||||
|
||||
case "disas", "u":
|
||||
n := 5
|
||||
if len(parts) > 1 {
|
||||
n, _ = strconv.Atoi(parts[1])
|
||||
if n <= 0 {
|
||||
n = 5
|
||||
}
|
||||
}
|
||||
regs, _ := s.GetRegs()
|
||||
fmt.Print(s.DisassembleN(regs.RIP, n))
|
||||
|
||||
case "where":
|
||||
regs, _ := s.GetRegs()
|
||||
funcOff := int(regs.RIP - codeBase - uint64(funcOffset))
|
||||
line := lineAt(lines, funcOff)
|
||||
label := nearestLabel(labels, funcOff)
|
||||
fmt.Printf(" func+%#x", funcOff)
|
||||
if label != "" {
|
||||
fmt.Printf(" (near %s)", label)
|
||||
}
|
||||
if line > 0 {
|
||||
fmt.Printf(" line %d", line)
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
case "help", "h", "?":
|
||||
fmt.Println(` break <label|addr> [if <reg> <op> <val>] set a breakpoint
|
||||
delete <label|addr> remove a breakpoint
|
||||
info break list all breakpoints
|
||||
watch <addr> [r|w] set a hardware watchpoint (write by default)
|
||||
unwatch clear all watchpoints
|
||||
step [n], s single-step n instructions
|
||||
next, n step over CALL
|
||||
continue, c run until breakpoint or exit
|
||||
disas [n], u disassemble n instructions at PC
|
||||
regs print registers and RFLAGS
|
||||
where show source line and nearest label
|
||||
stack show stack near RSP (args + return address)
|
||||
x [addr] [len] hex-dump memory
|
||||
w <addr> <val...> write bytes to memory
|
||||
labels, l list function labels
|
||||
help, h, ? this help
|
||||
quit, q kill debuggee and exit`)
|
||||
|
||||
case "stack":
|
||||
regs, _ := s.GetRegs()
|
||||
// For NOSPLIT frame=0: [RSP] = return address, [RSP+8..] = args.
|
||||
retAddr, _ := s.Peek(regs.RSP)
|
||||
fmt.Printf(" [RSP] return addr = %#x\n", retAddr)
|
||||
if argsSize > 0 {
|
||||
fmt.Printf(" args (%d bytes at RSP+8):\n", argsSize)
|
||||
argBytes, err := s.ReadMemory(regs.RSP+8, argsSize)
|
||||
if err == nil {
|
||||
for i := 0; i < argsSize; i += 8 {
|
||||
var v uint64
|
||||
for j := 0; j < 8 && i+j < len(argBytes); j++ {
|
||||
v |= uint64(argBytes[i+j]) << (8 * j)
|
||||
}
|
||||
fmt.Printf(" [%+3d] %#016x\n", i+8, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case "bt", "backtrace":
|
||||
regs, _ := s.GetRegs()
|
||||
funcOff := int(regs.RIP - codeBase - uint64(funcOffset))
|
||||
line := lineAt(lines, funcOff)
|
||||
label := nearestLabel(labels, funcOff)
|
||||
fmt.Printf(" #0 func+%#x", funcOff)
|
||||
if label != "" {
|
||||
fmt.Printf(" (%s)", label)
|
||||
}
|
||||
if line > 0 {
|
||||
fmt.Printf(" [line %d]", line)
|
||||
}
|
||||
fmt.Println()
|
||||
retAddr, _ := s.Peek(regs.RSP)
|
||||
fmt.Printf(" #1 return to %#x\n", retAddr)
|
||||
|
||||
case "watch":
|
||||
if len(parts) < 2 {
|
||||
fmt.Println("usage: watch <addr> [r|w] [size]")
|
||||
continue
|
||||
}
|
||||
addr, _ := resolveAddr(parts[1], codeBase, uint64(funcOffset), labels)
|
||||
if addr == 0 {
|
||||
fmt.Printf("unknown address: %s\n", parts[1])
|
||||
continue
|
||||
}
|
||||
typ := WatchWrite
|
||||
size := 8
|
||||
if len(parts) > 2 {
|
||||
switch parts[2] {
|
||||
case "r":
|
||||
typ = WatchRead
|
||||
case "w":
|
||||
typ = WatchWrite
|
||||
}
|
||||
}
|
||||
if len(parts) > 3 {
|
||||
size, _ = strconv.Atoi(parts[3])
|
||||
}
|
||||
// Find a free slot (0-3).
|
||||
slot := -1
|
||||
for i := 0; i < 4; i++ {
|
||||
// Simple: use slot 0 for now.
|
||||
slot = i
|
||||
break
|
||||
}
|
||||
if slot < 0 {
|
||||
fmt.Println("no free watchpoint slots")
|
||||
continue
|
||||
}
|
||||
if err := s.SetWatchpoint(slot, addr, typ, size); err != nil {
|
||||
fmt.Printf("watch: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("watchpoint %d set: %#x (%s, %d bytes)\n", slot, addr, parts[2], size)
|
||||
}
|
||||
|
||||
case "unwatch":
|
||||
if err := s.ClearAllWatchpoints(); err != nil {
|
||||
fmt.Printf("unwatch: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("all watchpoints cleared")
|
||||
}
|
||||
|
||||
default:
|
||||
fmt.Printf("unknown command: %s\n", cmd)
|
||||
}
|
||||
@@ -192,7 +505,42 @@ func printRegs(regs *Regs, codeBase, funcOff uint64) {
|
||||
fmt.Printf(" R10 = %#016x R11 = %#016x\n", regs.R10, regs.R11)
|
||||
fmt.Printf(" R12 = %#016x R13 = %#016x\n", regs.R12, regs.R13)
|
||||
fmt.Printf(" R14 = %#016x R15 = %#016x\n", regs.R14, regs.R15)
|
||||
fmt.Printf(" RFLAGS = %#x\n", regs.RFLAGS)
|
||||
fmt.Printf(" RFLAGS = %#x [%s]\n", regs.RFLAGS, decodeRflags(regs.RFLAGS))
|
||||
}
|
||||
|
||||
func decodeRflags(f uint64) string {
|
||||
var flags string
|
||||
if f&1 != 0 {
|
||||
flags += "CF "
|
||||
}
|
||||
if f&(1<<2) != 0 {
|
||||
flags += "PF "
|
||||
}
|
||||
if f&(1<<4) != 0 {
|
||||
flags += "AF "
|
||||
}
|
||||
if f&(1<<6) != 0 {
|
||||
flags += "ZF "
|
||||
}
|
||||
if f&(1<<7) != 0 {
|
||||
flags += "SF "
|
||||
}
|
||||
if f&(1<<8) != 0 {
|
||||
flags += "TF "
|
||||
}
|
||||
if f&(1<<9) != 0 {
|
||||
flags += "IF "
|
||||
}
|
||||
if f&(1<<10) != 0 {
|
||||
flags += "DF "
|
||||
}
|
||||
if f&(1<<11) != 0 {
|
||||
flags += "OF "
|
||||
}
|
||||
if flags == "" {
|
||||
flags = "none"
|
||||
}
|
||||
return flags[:len(flags)-1] // trim trailing space
|
||||
}
|
||||
|
||||
func hexDump(addr uint64, data []byte) {
|
||||
@@ -244,3 +592,47 @@ func resolveAddr(s string, codeBase, funcOff uint64, labels []Label) (uint64, st
|
||||
}
|
||||
return 0, ""
|
||||
}
|
||||
|
||||
// lineAt returns the source line for a given function-relative offset.
|
||||
func lineAt(lines []SourceLine, offset int) int {
|
||||
if len(lines) == 0 {
|
||||
return 0
|
||||
}
|
||||
lo, hi := 0, len(lines)-1
|
||||
for lo < hi {
|
||||
mid := (lo + hi + 1) / 2
|
||||
if lines[mid].Offset <= offset {
|
||||
lo = mid
|
||||
} else {
|
||||
hi = mid - 1
|
||||
}
|
||||
}
|
||||
if lines[lo].Offset <= offset {
|
||||
return lines[lo].Line
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// offsetForLine returns the byte offset for a given source line number.
|
||||
// Returns -1 if no instruction is at that line.
|
||||
func offsetForLine(lines []SourceLine, line int) int {
|
||||
for _, le := range lines {
|
||||
if le.Line == line {
|
||||
return le.Offset
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// nearestLabel returns the name of the label at or just before the offset.
|
||||
func nearestLabel(labels []Label, offset int) string {
|
||||
best := ""
|
||||
bestOff := -1
|
||||
for _, l := range labels {
|
||||
if l.Offset <= offset && l.Offset > bestOff {
|
||||
best = l.Name
|
||||
bestOff = l.Offset
|
||||
}
|
||||
}
|
||||
return best
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux && amd64
|
||||
|
||||
package debug
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// StopReason describes why the debuggee stopped.
|
||||
type StopReason int
|
||||
|
||||
const (
|
||||
StopNone StopReason = iota
|
||||
StopBreakpoint // INT3 breakpoint hit
|
||||
StopWatchpoint // hardware watchpoint triggered
|
||||
StopSingleStep // single-step completed
|
||||
StopSignal // stopped by a signal
|
||||
StopExited // process exited
|
||||
)
|
||||
|
||||
// siginfo_t layout (Linux amd64): si_signo, si_errno, si_code, then union.
|
||||
type siginfoT struct {
|
||||
SiSigno int32
|
||||
SiErrno int32
|
||||
SiCode int32
|
||||
_pad [125]byte
|
||||
}
|
||||
|
||||
const (
|
||||
trapBRKPT = 1 // INT3 breakpoint
|
||||
trapHWBRKPT = 4 // hardware watchpoint
|
||||
)
|
||||
|
||||
// StopInfo returns the reason the debuggee stopped and the faulting address
|
||||
// (for watchpoints, the watched address that was accessed).
|
||||
func (s *Session) StopInfo() (StopReason, uint64) {
|
||||
if s.exited {
|
||||
return StopExited, 0
|
||||
}
|
||||
var info siginfoT
|
||||
_, _, errno := syscall.Syscall6(
|
||||
syscall.SYS_PTRACE,
|
||||
uintptr(syscall.PTRACE_GETSIGINFO),
|
||||
uintptr(s.pid),
|
||||
0,
|
||||
uintptr(unsafe.Pointer(&info)),
|
||||
0, 0,
|
||||
)
|
||||
if errno != 0 {
|
||||
return StopNone, 0
|
||||
}
|
||||
if info.SiSigno != int32(syscall.SIGTRAP) {
|
||||
return StopSignal, uint64(info.SiCode)
|
||||
}
|
||||
switch info.SiCode {
|
||||
case trapBRKPT:
|
||||
return StopBreakpoint, 0
|
||||
case trapHWBRKPT:
|
||||
// The faulting address is in si_addr (offset 16 in siginfo_t on amd64).
|
||||
addr := *(*uint64)(unsafe.Pointer(uintptr(unsafe.Pointer(&info)) + 16))
|
||||
return StopWatchpoint, addr
|
||||
default:
|
||||
return StopSingleStep, 0
|
||||
}
|
||||
}
|
||||
|
||||
// SetReg modifies a register value in the debuggee.
|
||||
func (s *Session) SetReg(name string, value uint64) error {
|
||||
regs, err := s.GetRegs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch name {
|
||||
case "rax", "eax", "ax", "al":
|
||||
regs.RAX = value
|
||||
case "rbx", "ebx", "bx", "bl":
|
||||
regs.RBX = value
|
||||
case "rcx", "ecx", "cx", "cl":
|
||||
regs.RCX = value
|
||||
case "rdx", "edx", "dx", "dl":
|
||||
regs.RDX = value
|
||||
case "rsi", "esi", "si":
|
||||
regs.RSI = value
|
||||
case "rdi", "edi", "di":
|
||||
regs.RDI = value
|
||||
case "rbp", "ebp", "bp":
|
||||
regs.RBP = value
|
||||
case "rsp", "esp", "sp":
|
||||
regs.RSP = value
|
||||
case "r8":
|
||||
regs.R8 = value
|
||||
case "r9":
|
||||
regs.R9 = value
|
||||
case "r10":
|
||||
regs.R10 = value
|
||||
case "r11":
|
||||
regs.R11 = value
|
||||
case "r12":
|
||||
regs.R12 = value
|
||||
case "r13":
|
||||
regs.R13 = value
|
||||
case "r14":
|
||||
regs.R14 = value
|
||||
case "r15":
|
||||
regs.R15 = value
|
||||
case "rip", "eip":
|
||||
regs.RIP = value
|
||||
default:
|
||||
return fmt.Errorf("debug: unknown register %q", name)
|
||||
}
|
||||
return s.SetRegs(®s)
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux && amd64
|
||||
|
||||
package debug
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// Hardware watchpoint support via x86-64 debug registers (DR0-DR3, DR7).
|
||||
//
|
||||
// DR0-DR3 hold the watched addresses. DR7 is the control register:
|
||||
// bits 0,2,4,6: local enable for DR0-DR3
|
||||
// bits 16-17,20-21,24-25,28-29: R/W type (00=exec, 01=write, 11=read/write)
|
||||
// bits 18-19,22-23,26-27,30-31: length (00=1, 01=2, 10=8, 11=4)
|
||||
|
||||
// WatchpointType selects what triggers the watchpoint.
|
||||
type WatchpointType int
|
||||
|
||||
const (
|
||||
WatchWrite WatchpointType = 1 // trigger on write
|
||||
WatchRead WatchpointType = 3 // trigger on read or write
|
||||
)
|
||||
|
||||
// SetWatchpoint installs a hardware watchpoint on the given address.
|
||||
// slot is 0-3 (four hardware watchpoints available).
|
||||
func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size int) error {
|
||||
if slot < 0 || slot > 3 {
|
||||
return fmt.Errorf("debug: watchpoint slot must be 0-3")
|
||||
}
|
||||
|
||||
// Determine the length encoding.
|
||||
var lenBits uint64
|
||||
switch size {
|
||||
case 1:
|
||||
lenBits = 0
|
||||
case 2:
|
||||
lenBits = 1
|
||||
case 4:
|
||||
lenBits = 3
|
||||
case 8:
|
||||
lenBits = 2
|
||||
default:
|
||||
return fmt.Errorf("debug: watchpoint size must be 1, 2, 4, or 8")
|
||||
}
|
||||
|
||||
// Write the watched address to DR0-DR3.
|
||||
var drAddr uintptr
|
||||
switch slot {
|
||||
case 0:
|
||||
drAddr = 0x0 // DR0 offset in user_regs_struct
|
||||
case 1:
|
||||
drAddr = 0x8 // DR1
|
||||
case 2:
|
||||
drAddr = 0x10 // DR2
|
||||
case 3:
|
||||
drAddr = 0x18 // DR3
|
||||
}
|
||||
|
||||
// PTRACE_POKEUSER writes to the debuggee's user area (includes debug regs).
|
||||
if err := ptracePokeUser(s.pid, drAddr, addr); err != nil {
|
||||
return fmt.Errorf("debug: set DR%d: %w", slot, err)
|
||||
}
|
||||
|
||||
// Read the current DR7, set the enable and type bits, write it back.
|
||||
dr7, err := ptracePeekUser(s.pid, 0x38) // DR7 offset
|
||||
if err != nil {
|
||||
return fmt.Errorf("debug: read DR7: %w", err)
|
||||
}
|
||||
|
||||
enableBit := uint64(1) << (2 * slot) // local enable
|
||||
rwBits := uint64(typ) << (16 + 4*slot) // R/W type
|
||||
lenField := lenBits << (18 + 4*slot) // length
|
||||
|
||||
// Clear the existing bits for this slot, then set the new ones.
|
||||
mask := ^((uint64(1) << (2 * slot)) | (uint64(3) << (16 + 4*slot)) | (uint64(3) << (18 + 4*slot)))
|
||||
dr7 = (dr7 & mask) | enableBit | rwBits | lenField
|
||||
|
||||
if err := ptracePokeUser(s.pid, 0x38, dr7); err != nil {
|
||||
return fmt.Errorf("debug: set DR7: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearWatchpoint removes a hardware watchpoint.
|
||||
func (s *Session) ClearWatchpoint(slot int) error {
|
||||
if slot < 0 || slot > 3 {
|
||||
return fmt.Errorf("debug: watchpoint slot must be 0-3")
|
||||
}
|
||||
// Read DR7, clear the enable bit for this slot.
|
||||
dr7, err := ptracePeekUser(s.pid, 0x38)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dr7 &^= uint64(1) << (2 * slot) // disable
|
||||
return ptracePokeUser(s.pid, 0x38, dr7)
|
||||
}
|
||||
|
||||
// ClearAllWatchpoints removes all hardware watchpoints.
|
||||
func (s *Session) ClearAllWatchpoints() error {
|
||||
for slot := 0; slot < 4; slot++ {
|
||||
if err := s.ClearWatchpoint(slot); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ptracePokeUser writes a value to the debuggee's user area at the given offset.
|
||||
func ptracePokeUser(pid int, offset uintptr, val uint64) error {
|
||||
const ptracePokeuser = 6 // PTRACE_POKEUSER
|
||||
_, _, errno := syscall.Syscall6(
|
||||
syscall.SYS_PTRACE,
|
||||
uintptr(ptracePokeuser),
|
||||
uintptr(pid),
|
||||
offset,
|
||||
uintptr(val),
|
||||
0, 0,
|
||||
)
|
||||
if errno != 0 {
|
||||
return errno
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ptracePeekUser reads a value from the debuggee's user area at the given offset.
|
||||
func ptracePeekUser(pid int, offset uintptr) (uint64, error) {
|
||||
const ptracePeekuser = 3 // PTRACE_PEEKUSER
|
||||
val, _, errno := syscall.Syscall6(
|
||||
syscall.SYS_PTRACE,
|
||||
uintptr(ptracePeekuser),
|
||||
uintptr(pid),
|
||||
offset,
|
||||
0, 0, 0,
|
||||
)
|
||||
if errno != 0 {
|
||||
return 0, errno
|
||||
}
|
||||
return uint64(val), nil
|
||||
}
|
||||
Reference in New Issue
Block a user