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
compares two registers (`break loop if RAX > RBX`), not only a register
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
architectures via hand-written assembly trampolines
(`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")
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")
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")
fs.Parse(args)
@@ -195,32 +195,49 @@ REPL commands:
srcLines = append(srcLines, debug.SourceLine{Offset: le.Offset, Line: le.Line})
}
// Coverage mode: pre-register a breakpoint on every label (the basic
// blocks of a hand-written kernel) and let the kernel run to completion.
// Each hit removes its breakpoint, so the final report lists exactly the
// labels execution reached — the fast answer to "did my test ever enter
// the tail loop / the negative branch?".
// Coverage mode: pre-register a breakpoint on every instruction (walked
// by length through the function body while the debuggee is stopped) and
// let the kernel run to completion. Each trap counts a hit for that
// instruction, so the final report shows exactly which instructions
// 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 {
covered := map[string]bool{}
for _, l := range labels {
addr := sess.CodeBase() + uint64(fl.Offset) + uint64(l.Offset)
name := l.Name
if _, err := bm.SetWithCond(addr, name, nil); err != nil {
base := sess.CodeBase() + uint64(fl.Offset)
type coverInstr struct {
off uint64
text string
}
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)
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 _, bp := range bm.All() {
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()
if rerr == nil {
if bp := bm.At(regs.GetPC()); bp != nil {
bm.Clear(bp.Addr)
covered[bp.Label] = true
traps++
hits[regs.GetPC()-base]++
}
}
if err := sess.Continue(); err != nil {
@@ -233,7 +250,7 @@ REPL commands:
var hit []string
var missed []string
for _, l := range labels {
if covered[l.Name] {
if hits[uint64(l.Offset)] > 0 {
hit = append(hit, l.Name)
} else {
missed = append(missed, l.Name)
@@ -241,6 +258,7 @@ REPL commands:
}
sort.Strings(hit)
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))
for _, l := range hit {
fmt.Printf(" covered %s\n", l)
@@ -248,6 +266,12 @@ REPL commands:
for _, l := range missed {
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
}
+5 -1
View File
@@ -28,7 +28,11 @@ func printRegs(regs *Regs, codeBase, funcOff uint64) {
}
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).
+5 -1
View File
@@ -28,7 +28,11 @@ func printRegs(regs *Regs, codeBase, funcOff uint64) {
}
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).
+14 -4
View File
@@ -78,10 +78,20 @@ func (s *Session) GetFPRegs() (FPRegs, error) {
return fp, nil
}
// VectorRegs holds the LSX/LASX vector register state (placeholder).
type VectorRegs struct{}
// VectorRegs holds the FP register state shown by the regs command
// (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) {
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
}
// VectorRegs holds the vector register state (placeholder).
type VectorRegs struct{}
// VectorRegs holds the FP register state shown by the regs command
// (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) {
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 |
| `--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 |
| `--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:
@@ -126,7 +126,7 @@ REPL commands:
| `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 + vector/FP 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) |