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.
|
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
|
### Changed
|
||||||
|
|
||||||
- **Linux only.** The toolkit, its CI and the released binaries are now
|
- **Linux only.** The toolkit, its CI and the released binaries are now
|
||||||
Linux-only; cross-compiled to linux/{amd64,arm64,riscv64,loong64}.
|
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
|
## [0.29.0] — 2026-08-07
|
||||||
|
|
||||||
|
|||||||
@@ -251,16 +251,18 @@ portable Go implementation every kernel is derived from.
|
|||||||
### Phase 4 — debugger · *done*
|
### Phase 4 — debugger · *done*
|
||||||
|
|
||||||
- **`gasm debug`:** single-step a GAsm function, inspect registers (including
|
- **`gasm debug`:** single-step a GAsm function, inspect registers (including
|
||||||
YMM vector registers), set breakpoints on labels, allocate and fill named
|
YMM vector registers), set breakpoints and watchpoints on addresses, write
|
||||||
buffers, and hex-dump memory — the interactive counterpart to Phase 3's
|
memory, allocate and fill named buffers, disassemble at PC, and trace the
|
||||||
execution substrate.
|
source-line mapping — the interactive counterpart to Phase 3's execution
|
||||||
- **MVP** — *done.* ptrace-based debuggee subprocess (PTRACE_TRACEME +
|
substrate.
|
||||||
LockOSThread), entry breakpoint (auto-run to function start),
|
- ptrace-based debuggee subprocess (PTRACE_TRACEME + LockOSThread), entry
|
||||||
single-step, register inspection (GPR + YMM/XMM via PTRACE_GETFPREGS),
|
breakpoint (auto-run to function start), single-step, register inspection
|
||||||
label resolution, breakpoint management via `/proc/pid/mem`, named
|
(GPR + YMM/XMM via PTRACE_GETFPREGS), label resolution, breakpoint
|
||||||
buffer allocation with pattern filling (`--buf`), and an interactive REPL.
|
management via `/proc/pid/mem`, named buffer allocation with pattern
|
||||||
- **Remaining:** disassembly at PC (x86asm decode), memory-write support,
|
filling (`--buf`), interactive REPL with conditional breakpoints, four
|
||||||
watchpoints, and source-line mapping.
|
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*
|
### 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)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ type Session struct {
|
|||||||
stopped bool
|
stopped bool
|
||||||
exited 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
|
// Launch starts the debuggee subprocess (gasm debug --target ...) and
|
||||||
|
|||||||
+22
-11
@@ -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
|
fmt.Println(` break <label|addr> [if <reg> <op> <val>] set a breakpoint
|
||||||
delete <label|addr> remove a breakpoint
|
delete <label|addr> remove a breakpoint
|
||||||
info break list all breakpoints
|
info break list all breakpoints
|
||||||
watch <addr> [r|w] set a hardware watchpoint (write by default)
|
watch <addr> [r|w] [size] set a hardware watchpoint (write by default)
|
||||||
unwatch clear all watchpoints
|
unwatch [<slot>] clear one or all watchpoints
|
||||||
step [n], s single-step n instructions
|
step [n], s single-step n instructions
|
||||||
next, n step over CALL
|
next, n step over CALL
|
||||||
continue, c run until breakpoint or exit
|
continue, c run until breakpoint or exit
|
||||||
@@ -457,29 +457,40 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
|||||||
if len(parts) > 3 {
|
if len(parts) > 3 {
|
||||||
size, _ = strconv.Atoi(parts[3])
|
size, _ = strconv.Atoi(parts[3])
|
||||||
}
|
}
|
||||||
// Find a free slot (0-3).
|
slot := s.FindFreeWatchpointSlot()
|
||||||
slot := -1
|
|
||||||
for i := 0; i < 4; i++ {
|
|
||||||
// Simple: use slot 0 for now.
|
|
||||||
slot = i
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if slot < 0 {
|
if slot < 0 {
|
||||||
fmt.Println("no free watchpoint slots")
|
fmt.Println("no free watchpoint slots (use 'unwatch <slot>' to clear one)")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := s.SetWatchpoint(slot, addr, typ, size); err != nil {
|
if err := s.SetWatchpoint(slot, addr, typ, size); err != nil {
|
||||||
fmt.Printf("watch: %v\n", err)
|
fmt.Printf("watch: %v\n", err)
|
||||||
} else {
|
} 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":
|
case "unwatch":
|
||||||
|
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 {
|
||||||
if err := s.ClearAllWatchpoints(); err != nil {
|
if err := s.ClearAllWatchpoints(); err != nil {
|
||||||
fmt.Printf("unwatch: %v\n", err)
|
fmt.Printf("unwatch: %v\n", err)
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("all watchpoints cleared")
|
fmt.Println("all watchpoints cleared")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
fmt.Printf("unknown command: %s\n", cmd)
|
fmt.Printf("unknown command: %s\n", cmd)
|
||||||
|
|||||||
@@ -25,12 +25,34 @@ const (
|
|||||||
WatchRead WatchpointType = 3 // trigger on read or write
|
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.
|
// 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 {
|
func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size int) error {
|
||||||
if slot < 0 || slot > 3 {
|
if slot < 0 || slot > 3 {
|
||||||
return fmt.Errorf("debug: watchpoint slot must be 0-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.
|
// Determine the length encoding.
|
||||||
var lenBits uint64
|
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 {
|
if err := ptracePokeUser(s.pid, 0x38, dr7); err != nil {
|
||||||
return fmt.Errorf("debug: set DR7: %w", err)
|
return fmt.Errorf("debug: set DR7: %w", err)
|
||||||
}
|
}
|
||||||
|
s.wpSlots[slot] = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,22 +113,31 @@ func (s *Session) ClearWatchpoint(slot int) error {
|
|||||||
if slot < 0 || slot > 3 {
|
if slot < 0 || slot > 3 {
|
||||||
return fmt.Errorf("debug: watchpoint slot must be 0-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.
|
// Read DR7, clear the enable bit for this slot.
|
||||||
dr7, err := ptracePeekUser(s.pid, 0x38)
|
dr7, err := ptracePeekUser(s.pid, 0x38)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
dr7 &^= uint64(1) << (2 * slot) // disable
|
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.
|
// ClearAllWatchpoints removes all hardware watchpoints.
|
||||||
func (s *Session) ClearAllWatchpoints() error {
|
func (s *Session) ClearAllWatchpoints() error {
|
||||||
for slot := 0; slot < 4; slot++ {
|
for slot := 0; slot < 4; slot++ {
|
||||||
|
if s.wpSlots[slot] {
|
||||||
if err := s.ClearWatchpoint(slot); err != nil {
|
if err := s.ClearWatchpoint(slot); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+18
-5
@@ -102,13 +102,26 @@ REPL commands:
|
|||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| `break <label\|addr>` | Set a breakpoint |
|
| `break <label\|addr> [if <reg> <op> <val>]` | Set a breakpoint, optionally conditional |
|
||||||
| `step [n]` | Single-step n instructions |
|
| `delete <label\|addr>` | Remove a breakpoint |
|
||||||
| `continue` | Run until next breakpoint or exit |
|
| `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 |
|
| `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 |
|
| `x [addr] [len]` | Hex-dump memory |
|
||||||
| `labels` | List function labels and offsets |
|
| `w <addr> <val...>` | Write bytes to memory |
|
||||||
| `quit` | Kill the debuggee and exit |
|
| `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>`
|
## `gasm diff [--map old=new,...] <file1.s> <file2.s>`
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user