docs: document watchpoint slot tracking and update debugger commands
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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*
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
@@ -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>`
|
||||
|
||||
|
||||
Reference in New Issue
Block a user