docs: document watchpoint slot tracking and update debugger commands

This commit is contained in:
2026-08-07 22:27:49 +02:00
parent d7ee1b78d4
commit cde7d0f96a
7 changed files with 154 additions and 34 deletions
+11
View File
@@ -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 <slot>` 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
+12 -10
View File
@@ -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*
+50
View File
@@ -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)
}
}
+2 -1
View File
@@ -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
+25 -14
View File
@@ -384,8 +384,8 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
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
watch <addr> [r|w] [size] set a hardware watchpoint (write by default)
unwatch [<slot>] 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 <slot>' 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 [<slot>]")
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:
+36 -4
View File
@@ -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
+18 -5
View File
@@ -102,13 +102,26 @@ REPL commands:
| Command | Description |
|---------|-------------|
| `break <label\|addr>` | Set a breakpoint |
| `step [n]` | Single-step n instructions |
| `continue` | Run until next breakpoint or exit |
| `break <label\|addr> [if <reg> <op> <val>]` | Set a breakpoint, optionally conditional |
| `delete <label\|addr>` | 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 <addr> <val...>` | Write bytes to memory |
| `set <reg> <value>` | Set a register |
| `watch <addr> [r\|w] [size]` | Set a hardware watchpoint (write by default) |
| `unwatch [<slot>]` | 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,...] <file1.s> <file2.s>`