feat(debug): instruction-level coverage and FP register display

Assisted-by: GLM 5.3 Flash
This commit is contained in:
2026-08-30 21:42:12 +02:00
parent 4171e412b5
commit 41b387e54d
7 changed files with 85 additions and 27 deletions
+8
View File
@@ -23,6 +23,14 @@ Unreleased changes on the `development` branch.
- **Conditional breakpoints.** `break <label> if <reg> <op> <val>` now also - **Conditional breakpoints.** `break <label> if <reg> <op> <val>` now also
compares two registers (`break loop if RAX > RBX`), not only a register compares two registers (`break loop if RAX > RBX`), not only a register
against an immediate. against an immediate.
- **Instruction-level coverage.** `gasm debug --cover` now sets a
breakpoint on every instruction (walked by disassembly length), counts
the hits per instruction and reports the executed instructions with
their hit counts, with the label coverage derived from the same run.
Expect the run to slow to ptrace speed.
- **FP register display on riscv64 and loong64.** The debugger `regs`
command shows the 32 FP registers plus fcsr (and fcc on loong64) via
`PTRACE_GETREGSET`, where it previously printed nothing.
- **JIT execution trampolines.** `verify.Call` now works on all four - **JIT execution trampolines.** `verify.Call` now works on all four
architectures via hand-written assembly trampolines architectures via hand-written assembly trampolines
(`trampoline_{arm64,riscv64,loong64}.s`) that save the Go stack, switch (`trampoline_{arm64,riscv64,loong64}.s`) that save the Go stack, switch
+39 -15
View File
@@ -52,7 +52,7 @@ REPL commands:
argsFile := fs.String("args", "", "file containing the ABI0 argument block") argsFile := fs.String("args", "", "file containing the ABI0 argument block")
bufSpec := fs.String("buf", "", "buffer specification: name:size:pattern[,name:size:pattern...] where pattern is zero, ones, seq, or hex") bufSpec := fs.String("buf", "", "buffer specification: name:size:pattern[,name:size:pattern...] where pattern is zero, ones, seq, or hex")
script := fs.String("script", "", "run REPL commands from a file (one per line) and exit; '-' reads stdin") script := fs.String("script", "", "run REPL commands from a file (one per line) and exit; '-' reads stdin")
cover := fs.Bool("cover", false, "run to completion with breakpoints on every label and report which blocks executed") cover := fs.Bool("cover", false, "run to completion with a breakpoint on every instruction and report which executed and how often")
timeout := fs.Duration("timeout", 0, "kill the debuggee after this duration (e.g. 30s); for headless --script runs") timeout := fs.Duration("timeout", 0, "kill the debuggee after this duration (e.g. 30s); for headless --script runs")
fs.Parse(args) fs.Parse(args)
@@ -195,32 +195,49 @@ REPL commands:
srcLines = append(srcLines, debug.SourceLine{Offset: le.Offset, Line: le.Line}) srcLines = append(srcLines, debug.SourceLine{Offset: le.Offset, Line: le.Line})
} }
// Coverage mode: pre-register a breakpoint on every label (the basic // Coverage mode: pre-register a breakpoint on every instruction (walked
// blocks of a hand-written kernel) and let the kernel run to completion. // by length through the function body while the debuggee is stopped) and
// Each hit removes its breakpoint, so the final report lists exactly the // let the kernel run to completion. Each trap counts a hit for that
// labels execution reached — the fast answer to "did my test ever enter // instruction, so the final report shows exactly which instructions
// the tail loop / the negative branch?". // executed and how often, with the label-level view derived from it.
// Expect the run to slow to ptrace speed: one trap per executed
// instruction.
if *cover { if *cover {
covered := map[string]bool{} base := sess.CodeBase() + uint64(fl.Offset)
for _, l := range labels { type coverInstr struct {
addr := sess.CodeBase() + uint64(fl.Offset) + uint64(l.Offset) off uint64
name := l.Name text string
if _, err := bm.SetWithCond(addr, name, nil); err != nil { }
var instrs []coverInstr
for off := uint64(0); off < uint64(fl.Size); {
text, ln, err := sess.Disassemble(base + off)
if err != nil || ln == 0 {
break
}
instrs = append(instrs, coverInstr{off: off, text: text})
off += uint64(ln)
}
for _, in := range instrs {
if _, err := bm.SetWithCond(base+in.off, fmt.Sprintf("func+%#x", in.off), nil); err != nil {
fmt.Fprintf(os.Stderr, "gasm debug: cover: %v\n", err) fmt.Fprintf(os.Stderr, "gasm debug: cover: %v\n", err)
return 1 return 1
} }
} }
fmt.Printf("gasm debug: coverage run over %d labels\n", len(labels)) hits := map[uint64]int{}
traps := 0
fmt.Printf("gasm debug: coverage run over %d instructions\n", len(instrs))
for { for {
for _, bp := range bm.All() { for _, bp := range bm.All() {
bm.Reinsert(bp.Addr) bm.Reinsert(bp.Addr)
} }
// Remove the breakpoint the run is parked on, then continue. // Remove the breakpoint the run is parked on, count the hit,
// then continue.
regs, rerr := sess.GetRegs() regs, rerr := sess.GetRegs()
if rerr == nil { if rerr == nil {
if bp := bm.At(regs.GetPC()); bp != nil { if bp := bm.At(regs.GetPC()); bp != nil {
bm.Clear(bp.Addr) bm.Clear(bp.Addr)
covered[bp.Label] = true traps++
hits[regs.GetPC()-base]++
} }
} }
if err := sess.Continue(); err != nil { if err := sess.Continue(); err != nil {
@@ -233,7 +250,7 @@ REPL commands:
var hit []string var hit []string
var missed []string var missed []string
for _, l := range labels { for _, l := range labels {
if covered[l.Name] { if hits[uint64(l.Offset)] > 0 {
hit = append(hit, l.Name) hit = append(hit, l.Name)
} else { } else {
missed = append(missed, l.Name) missed = append(missed, l.Name)
@@ -241,6 +258,7 @@ REPL commands:
} }
sort.Strings(hit) sort.Strings(hit)
sort.Strings(missed) sort.Strings(missed)
fmt.Printf("coverage: %d/%d instructions executed (%d traps)\n", len(hits), len(instrs), traps)
fmt.Printf("coverage: %d/%d labels reached\n", len(hit), len(labels)) fmt.Printf("coverage: %d/%d labels reached\n", len(hit), len(labels))
for _, l := range hit { for _, l := range hit {
fmt.Printf(" covered %s\n", l) fmt.Printf(" covered %s\n", l)
@@ -248,6 +266,12 @@ REPL commands:
for _, l := range missed { for _, l := range missed {
fmt.Printf(" MISSED %s\n", l) fmt.Printf(" MISSED %s\n", l)
} }
fmt.Println("executed instructions:")
for _, in := range instrs {
if n := hits[in.off]; n > 0 {
fmt.Printf(" func+%#04x %4dx %s\n", in.off, n, in.text)
}
}
return 0 return 0
} }
+5 -1
View File
@@ -28,7 +28,11 @@ func printRegs(regs *Regs, codeBase, funcOff uint64) {
} }
func printVectorRegs(v *VectorRegs) { func printVectorRegs(v *VectorRegs) {
// No vector registers to display for loong64 yet. fmt.Println("\n FP registers (F0-F31):")
for i := 0; i < 32; i += 2 {
fmt.Printf(" F%-2d = %#018x F%-2d = %#018x\n", i, v.F[i], i+1, v.F[i+1])
}
fmt.Printf(" FCC = %#016x FCSR = %#x\n", v.FCC, v.FCSR)
} }
// archReturnAddr reads the return address from RA (loong64 convention). // archReturnAddr reads the return address from RA (loong64 convention).
+5 -1
View File
@@ -28,7 +28,11 @@ func printRegs(regs *Regs, codeBase, funcOff uint64) {
} }
func printVectorRegs(v *VectorRegs) { func printVectorRegs(v *VectorRegs) {
// No vector registers to display for riscv64 yet. fmt.Println("\n FP registers (F0-F31):")
for i := 0; i < 32; i += 2 {
fmt.Printf(" F%-2d = %#018x F%-2d = %#018x\n", i, v.F[i], i+1, v.F[i+1])
}
fmt.Printf(" FCSR = %#x\n", v.FCSR)
} }
// archReturnAddr reads the return address from RA (riscv64 convention). // archReturnAddr reads the return address from RA (riscv64 convention).
+14 -4
View File
@@ -78,10 +78,20 @@ func (s *Session) GetFPRegs() (FPRegs, error) {
return fp, nil return fp, nil
} }
// VectorRegs holds the LSX/LASX vector register state (placeholder). // VectorRegs holds the FP register state shown by the regs command
type VectorRegs struct{} // (the scalar FP subset: 32 64-bit registers, fcc and fcsr; the LSX/LASX
// vector files are not read yet).
type VectorRegs struct {
F [32]uint64
FCC uint64
FCSR uint32
}
// GetVectorRegs retrieves the vector registers (not yet implemented for loong64). // GetVectorRegs retrieves the FP registers.
func (s *Session) GetVectorRegs() (VectorRegs, error) { func (s *Session) GetVectorRegs() (VectorRegs, error) {
return VectorRegs{}, nil fp, err := s.GetFPRegs()
if err != nil {
return VectorRegs{}, err
}
return VectorRegs{F: fp.F, FCC: fp.FCC, FCSR: fp.FCSR}, nil
} }
+12 -4
View File
@@ -76,10 +76,18 @@ func (s *Session) GetFPRegs() (FPRegs, error) {
return fp, nil return fp, nil
} }
// VectorRegs holds the vector register state (placeholder). // VectorRegs holds the FP register state shown by the regs command
type VectorRegs struct{} // (riscv64 has 32 64-bit FP registers and fcsr).
type VectorRegs struct {
F [32]uint64
FCSR uint32
}
// GetVectorRegs retrieves the vector registers (not yet implemented for riscv64). // GetVectorRegs retrieves the FP registers.
func (s *Session) GetVectorRegs() (VectorRegs, error) { func (s *Session) GetVectorRegs() (VectorRegs, error) {
return VectorRegs{}, nil fp, err := s.GetFPRegs()
if err != nil {
return VectorRegs{}, err
}
return VectorRegs{F: fp.F, FCSR: fp.FCSR}, nil
} }
+2 -2
View File
@@ -112,7 +112,7 @@ loong64). Requires a compiled binary on `$PATH` (not `go run`).
| `--args <file>` | File containing the ABI0 argument block | | `--args <file>` | File containing the ABI0 argument block |
| `--script <file>` | Run REPL commands from a file (one per line) and exit; `-` reads stdin | | `--script <file>` | Run REPL commands from a file (one per line) and exit; `-` reads stdin |
| `--timeout <dur>` | Kill the debuggee after this duration (e.g. `30s`); for headless `--script` runs | | `--timeout <dur>` | Kill the debuggee after this duration (e.g. `30s`); for headless `--script` runs |
| `--cover` | Run to completion with breakpoints on every label and report which blocks executed | | `--cover` | Run to completion with a breakpoint on every instruction; report which executed, how often, and which labels were reached |
REPL commands: REPL commands:
@@ -126,7 +126,7 @@ REPL commands:
| `finish`, `fin` | Run until the function returns | | `finish`, `fin` | Run until the function returns |
| `continue`, `c` | Run until breakpoint, watchpoint or exit | | `continue`, `c` | Run until breakpoint, watchpoint or exit |
| `disas [n]`, `u` | Disassemble n instructions at PC | | `disas [n]`, `u` | Disassemble n instructions at PC |
| `regs` | Print general-purpose + YMM/XMM vector registers | | `regs` | Print general-purpose + vector/FP registers |
| `where` | Show source line and nearest label at PC | | `where` | Show source line and nearest label at PC |
| `stack` | Show stack near RSP (return address + ABI0 args) | | `stack` | Show stack near RSP (return address + ABI0 args) |
| `bt`, `backtrace` | Backtrace (current frame + return address) | | `bt`, `backtrace` | Backtrace (current frame + return address) |