From cde7d0f96ab4c7282a5b27fce5d21e1967886ef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Fri, 7 Aug 2026 22:27:49 +0200 Subject: [PATCH] docs: document watchpoint slot tracking and update debugger commands --- CHANGELOG.md | 11 ++++++++ README.md | 22 ++++++++------- debug/debug_test.go | 50 +++++++++++++++++++++++++++++++++ debug/ptrace_linux_amd64.go | 3 +- debug/repl.go | 39 ++++++++++++++++--------- debug/watchpoint_linux_amd64.go | 40 +++++++++++++++++++++++--- docs/cli.md | 23 +++++++++++---- 7 files changed, 154 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23d46a3..5f0ef85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,21 @@ and this project adheres to [Conventional Commits](https://www.conventionalcommi Unreleased changes on the `development` branch. +### Fixed + +- **Debugger watchpoint slots.** `gasm debug`'s `watch` command always used + hardware watchpoint slot 0, so a second `watch` call silently overwrote + the first. Watchpoint slots are now tracked in the `Session` (DR0–DR3); + `watch` picks the first free slot and reports an error if all four are in + use, and `unwatch ` clears one (no argument clears all). + ### Changed - **Linux only.** The toolkit, its CI and the released binaries are now Linux-only; cross-compiled to linux/{amd64,arm64,riscv64,loong64}. +- **Phase 4 closed.** README's "Remaining" list for the debugger is gone; + disassembly at PC, memory-write, watchpoints, and source-line mapping are + all shipped. ## [0.29.0] — 2026-08-07 diff --git a/README.md b/README.md index 72ce3bb..5f862ad 100644 --- a/README.md +++ b/README.md @@ -251,16 +251,18 @@ portable Go implementation every kernel is derived from. ### Phase 4 — debugger · *done* - **`gasm debug`:** single-step a GAsm function, inspect registers (including - YMM vector registers), set breakpoints on labels, allocate and fill named - buffers, and hex-dump memory — the interactive counterpart to Phase 3's - execution substrate. - - **MVP** — *done.* ptrace-based debuggee subprocess (PTRACE_TRACEME + - LockOSThread), entry breakpoint (auto-run to function start), - single-step, register inspection (GPR + YMM/XMM via PTRACE_GETFPREGS), - label resolution, breakpoint management via `/proc/pid/mem`, named - buffer allocation with pattern filling (`--buf`), and an interactive REPL. - - **Remaining:** disassembly at PC (x86asm decode), memory-write support, - watchpoints, and source-line mapping. + YMM vector registers), set breakpoints and watchpoints on addresses, write + memory, allocate and fill named buffers, disassemble at PC, and trace the + source-line mapping — the interactive counterpart to Phase 3's execution + substrate. + - ptrace-based debuggee subprocess (PTRACE_TRACEME + LockOSThread), entry + breakpoint (auto-run to function start), single-step, register inspection + (GPR + YMM/XMM via PTRACE_GETFPREGS), label resolution, breakpoint + management via `/proc/pid/mem`, named buffer allocation with pattern + filling (`--buf`), interactive REPL with conditional breakpoints, four + hardware watchpoints (DR0–DR3), step-over-CALL, run-to-return, backtrace, + memory read/write, disassembly at PC (x86asm), and source-line ↔ offset + mapping. ### Phase 5 — the other architectures · *in progress* diff --git a/debug/debug_test.go b/debug/debug_test.go index 539b61c..eee5d02 100644 --- a/debug/debug_test.go +++ b/debug/debug_test.go @@ -271,3 +271,53 @@ func TestBreakpointInfo(t *testing.T) { t.Errorf("Info %q does not contain label", info) } } + +func TestWatchpointSlotTracking(t *testing.T) { + s := &Session{} + + // All four slots are free initially. + for i := 0; i < 4; i++ { + if s.IsWatchpointSlotUsed(i) { + t.Errorf("slot %d should be free initially", i) + } + } + if got := s.FindFreeWatchpointSlot(); got != 0 { + t.Errorf("FindFreeWatchpointSlot() = %d, want 0", got) + } + + // Manually mark slots 0 and 2 as used (simulating successful SetWatchpoint). + s.wpSlots[0] = true + s.wpSlots[2] = true + + if !s.IsWatchpointSlotUsed(0) { + t.Error("slot 0 should be in use") + } + if s.IsWatchpointSlotUsed(1) { + t.Error("slot 1 should be free") + } + if !s.IsWatchpointSlotUsed(2) { + t.Error("slot 2 should be in use") + } + if s.IsWatchpointSlotUsed(3) { + t.Error("slot 3 should be free") + } + if got := s.FindFreeWatchpointSlot(); got != 1 { + t.Errorf("FindFreeWatchpointSlot() = %d, want 1", got) + } + + // Out-of-range slot queries return false. + if s.IsWatchpointSlotUsed(-1) { + t.Error("slot -1 should be reported as free (out of range)") + } + if s.IsWatchpointSlotUsed(4) { + t.Error("slot 4 should be reported as free (out of range)") + } + + // Mark all slots used: FindFreeWatchpointSlot returns -1. + for i := 0; i < 4; i++ { + s.wpSlots[i] = true + } + if got := s.FindFreeWatchpointSlot(); got != -1 { + t.Errorf("FindFreeWatchpointSlot() with all slots used = %d, want -1", got) + } +} diff --git a/debug/ptrace_linux_amd64.go b/debug/ptrace_linux_amd64.go index 022c7d7..f314488 100644 --- a/debug/ptrace_linux_amd64.go +++ b/debug/ptrace_linux_amd64.go @@ -25,7 +25,8 @@ type Session struct { cmd *exec.Cmd stopped bool exited bool - codeBase uint64 // base address of the JIT code in the debuggee + codeBase uint64 // base address of the JIT code in the debuggee + wpSlots [4]bool // watchpoint slot occupancy (DR0-DR3) } // Launch starts the debuggee subprocess (gasm debug --target ...) and diff --git a/debug/repl.go b/debug/repl.go index a2e81cf..86f3d83 100644 --- a/debug/repl.go +++ b/debug/repl.go @@ -384,8 +384,8 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar fmt.Println(` break [if ] set a breakpoint delete remove a breakpoint info break list all breakpoints - watch [r|w] set a hardware watchpoint (write by default) - unwatch clear all watchpoints + watch [r|w] [size] set a hardware watchpoint (write by default) + unwatch [] clear one or all watchpoints step [n], s single-step n instructions next, n step over CALL continue, c run until breakpoint or exit @@ -457,28 +457,39 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar 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 - } + slot := s.FindFreeWatchpointSlot() if slot < 0 { - fmt.Println("no free watchpoint slots") + fmt.Println("no free watchpoint slots (use 'unwatch ' to clear one)") 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) + typStr := "w" + if typ == WatchRead { + typStr = "r" + } + fmt.Printf("watchpoint %d set: %#x (%s, %d bytes)\n", slot, addr, typStr, size) } case "unwatch": - if err := s.ClearAllWatchpoints(); err != nil { - fmt.Printf("unwatch: %v\n", err) + if len(parts) >= 2 { + slot, err := strconv.Atoi(parts[1]) + if err != nil || slot < 0 || slot > 3 { + fmt.Println("usage: unwatch []") + continue + } + if err := s.ClearWatchpoint(slot); err != nil { + fmt.Printf("unwatch: %v\n", err) + } else { + fmt.Printf("watchpoint %d cleared\n", slot) + } } else { - fmt.Println("all watchpoints cleared") + if err := s.ClearAllWatchpoints(); err != nil { + fmt.Printf("unwatch: %v\n", err) + } else { + fmt.Println("all watchpoints cleared") + } } default: diff --git a/debug/watchpoint_linux_amd64.go b/debug/watchpoint_linux_amd64.go index 609db70..a00fe37 100644 --- a/debug/watchpoint_linux_amd64.go +++ b/debug/watchpoint_linux_amd64.go @@ -25,12 +25,34 @@ const ( WatchRead WatchpointType = 3 // trigger on read or write ) +// FindFreeWatchpointSlot returns the index of the first free watchpoint slot +// (0-3), or -1 if all four hardware watchpoints are in use. +func (s *Session) FindFreeWatchpointSlot() int { + for i := 0; i < 4; i++ { + if !s.wpSlots[i] { + return i + } + } + return -1 +} + +// IsWatchpointSlotUsed reports whether slot (0-3) currently holds a watchpoint. +func (s *Session) IsWatchpointSlotUsed(slot int) bool { + if slot < 0 || slot > 3 { + return false + } + return s.wpSlots[slot] +} + // SetWatchpoint installs a hardware watchpoint on the given address. -// slot is 0-3 (four hardware watchpoints available). +// slot is 0-3 (four hardware watchpoints available); the slot must be free. 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") } + if s.wpSlots[slot] { + return fmt.Errorf("debug: watchpoint slot %d already in use", slot) + } // Determine the length encoding. var lenBits uint64 @@ -82,6 +104,7 @@ func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size if err := ptracePokeUser(s.pid, 0x38, dr7); err != nil { return fmt.Errorf("debug: set DR7: %w", err) } + s.wpSlots[slot] = true return nil } @@ -90,20 +113,29 @@ func (s *Session) ClearWatchpoint(slot int) error { if slot < 0 || slot > 3 { return fmt.Errorf("debug: watchpoint slot must be 0-3") } + if !s.wpSlots[slot] { + return fmt.Errorf("debug: watchpoint slot %d is not in use", slot) + } // 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) + if err := ptracePokeUser(s.pid, 0x38, dr7); err != nil { + return err + } + s.wpSlots[slot] = false + return nil } // 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 + if s.wpSlots[slot] { + if err := s.ClearWatchpoint(slot); err != nil { + return err + } } } return nil diff --git a/docs/cli.md b/docs/cli.md index dc7418c..9da764b 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -102,13 +102,26 @@ REPL commands: | Command | Description | |---------|-------------| -| `break ` | Set a breakpoint | -| `step [n]` | Single-step n instructions | -| `continue` | Run until next breakpoint or exit | +| `break [if ]` | Set a breakpoint, optionally conditional | +| `delete ` | Remove a breakpoint | +| `info break` | List all breakpoints | +| `step [n]`, `s` | Single-step n instructions | +| `next`, `n` | Step over CALL | +| `finish`, `fin` | Run until the function returns | +| `continue`, `c` | Run until breakpoint, watchpoint or exit | +| `disas [n]`, `u` | Disassemble n instructions at PC | | `regs` | Print general-purpose + YMM/XMM vector registers | +| `where` | Show source line and nearest label at PC | +| `stack` | Show stack near RSP (return address + ABI0 args) | +| `bt`, `backtrace` | Backtrace (current frame + return address) | | `x [addr] [len]` | Hex-dump memory | -| `labels` | List function labels and offsets | -| `quit` | Kill the debuggee and exit | +| `w ` | Write bytes to memory | +| `set ` | Set a register | +| `watch [r\|w] [size]` | Set a hardware watchpoint (write by default) | +| `unwatch []` | Clear one or all watchpoints | +| `labels`, `l` | List function labels and offsets | +| `help`, `h`, `?` | Show command help | +| `quit`, `q` | Kill the debuggee and exit | ## `gasm diff [--map old=new,...] `