"""Post-run analysis: turn a session directory into a symbol-annotated report. The report is the "complete map" half of the deliverable: hits by function, watch hits, memory writes, coverage and snapshot inventory, ready for offline comparison between runs (bare-metal vs VFIO, old firmware vs new). """ from __future__ import annotations import os import re from collections import Counter, defaultdict from .record import load_events, load_meta _ACCESS_RE = re.compile( r"Invalid (read|write) at addr (0x[0-9a-fA-F]+), size (\d+), region '([^']*)', reason: (\w+)" ) _INTERRUPT_RE = re.compile( r"riscv_cpu_do_interrupt: hart:(\d+), async:(\d+), cause:([0-9a-fA-F]+), " r"epc:(0x[0-9a-fA-F]+), tval:(0x[0-9a-fA-F]+), desc=([\w_]+)" ) def parse_qemu_faults(log_text: str) -> list[dict]: """Parse QEMU ``-d guest_errors,unimp,int`` logs into fault records. Each record pairs the most recent invalid access (if any) with the exception it raised, which is exactly the "first divergence" report a firmware probe needs. """ faults: list[dict] = [] pending_access: dict | None = None for line in log_text.splitlines(): access = _ACCESS_RE.search(line) if access: pending_access = { "rw": access.group(1), "access_addr": int(access.group(2), 16), "size": int(access.group(3)), "region": access.group(4), "reason": access.group(5), } continue interrupt = _INTERRUPT_RE.search(line) if interrupt: faults.append( { "hart": int(interrupt.group(1)), "async": int(interrupt.group(2)), "cause": int(interrupt.group(3), 16), "epc": int(interrupt.group(4), 16), "tval": int(interrupt.group(5), 16), "desc": interrupt.group(6), "access": pending_access, } ) pending_access = None return faults _RISCV_CAUSES = { 0: "instruction address misaligned", 1: "instruction access fault", 2: "illegal instruction", 3: "breakpoint", 4: "load address misaligned", 5: "load access fault", 6: "store address misaligned", 7: "store access fault", } def describe_fault(fault: dict, bias: int = 0) -> str: epc = fault["epc"] fw_pc = epc - bias cause = fault["cause"] desc = _RISCV_CAUSES.get(cause & 0xFF, fault.get("desc", "exception")) lines = [ f"first exception: {fault.get('desc', '?')} (cause {cause:#x}: {desc})", f" epc: {epc:#x}" + (f" (firmware vaddr {fw_pc:#x})" if bias else ""), f" tval: {fault.get('tval', 0):#x}", ] access = fault.get("access") if access: lines.insert( 1, f" access: {access['rw']} at {access['access_addr']:#x} " f"({access['size']} bytes, reason={access['reason']})", ) return "\n".join(lines) def render_summary(session_dir: str, symbols=None, max_rows: int = 40) -> str: meta = load_meta(session_dir) events = load_events(session_dir) return render_summary_from(meta, events, symbols=symbols, max_rows=max_rows) def render_summary_from(meta: dict, events: list[dict], symbols=None, max_rows: int = 40) -> str: lines: list[str] = [] lines.append(f"# green-eye session summary: {meta.get('name', '?')}") lines.append("") lines.append(f"* format: `{meta.get('format', '?')}` green-eye {meta.get('greeneye', '?')}") lines.append(f"* directory: `{meta.get('dir', '.')}`") lines.append(f"* events: {len(events)}") counts = Counter(event.get("type", "?") for event in events) lines.append("* event counts: " + ", ".join(f"{k}={v}" for k, v in sorted(counts.items()))) counts_meta = meta.get("counts") if counts_meta: lines.append("* recorder counts: " + ", ".join(f"{k}={v}" for k, v in sorted(counts_meta.items()))) lines.append("") # QEMU exception report (probe sessions) fault_count = meta.get("qemu_fault_count") first = meta.get("qemu_first_fault") if fault_count or first: lines.append("## QEMU exceptions") lines.append("") lines.append(f"* total exceptions: {fault_count if fault_count is not None else '?'}") if meta.get("qemu_log_blob"): lines.append(f"* qemu log blob: `{meta['qemu_log_blob']}`") if first: access = first.get("access") or {} lines.append("") lines.append("| field | value |") lines.append("|---|---|") lines.append(f"| descriptor | `{first.get('desc', '?')}` |") lines.append(f"| cause | `{first.get('cause', 0):#x}` ({_RISCV_CAUSES.get(first.get('cause', 0) & 0xFF, '?')}) |") epc = first.get("epc", 0) bias = meta.get("bias") or 0 epc_cell = f"`{epc:#x}`" + (f" (firmware vaddr `{epc - bias:#x}`)" if bias else "") lines.append(f"| epc | {epc_cell} |") lines.append(f"| tval | `{first.get('tval', 0):#x}` |") if access: lines.append( f"| first access | `{access.get('rw')}` at `{access.get('access_addr', 0):#x}` " f"({access.get('size')} bytes, reason `{access.get('reason')}`) |" ) lines.append("") def annotate(addr): if not isinstance(addr, int): return "" if symbols is not None: return symbols.annotate(addr) return f"{addr:#x}" # Breakpoint hits bp_hits = [e for e in events if e.get("type") == "bp_hit"] if bp_hits: by_site: Counter = Counter() by_symbol: Counter = Counter() for event in bp_hits: key = event.get("symbol") or f"{event.get('pc', 0):#x}" by_site[(event.get("pc"), key)] += 1 by_symbol[key] += 1 lines.append("## Breakpoint hits") lines.append("") lines.append("| hits | pc | symbol |") lines.append("|---:|---|---|") for (pc, key), count in by_site.most_common(max_rows): lines.append(f"| {count} | `{pc:#x}` | `{key}` |") lines.append("") # Watchpoint hits watch_hits = [e for e in events if e.get("type") == "watch_hit"] if watch_hits: by_addr: Counter = Counter() for event in watch_hits: by_addr[(event.get("addr"), event.get("kind"))] += 1 lines.append("## Watchpoint hits") lines.append("") lines.append("| hits | address | kind | symbol |") lines.append("|---:|---|---|---|") for (addr, kind), count in by_addr.most_common(max_rows): sym = symbols.resolve(addr) if symbols else None lines.append(f"| {count} | `{addr:#x}` | {kind} | `{sym.name if sym else ''}` |") lines.append("") # Memory writes writes = [e for e in events if e.get("type") == "mem_write"] if writes: regions = Counter() for event in writes: regions[(event.get("addr", 0) >> 12) << 12] += 1 lines.append("## Memory writes") lines.append("") lines.append(f"* writes: {len(writes)}, distinct 4 KiB pages: {len(regions)}") lines.append("") lines.append("| page | writes | symbol |") lines.append("|---|---:|---|") for page, count in regions.most_common(max_rows): sym = symbols.resolve(page) if symbols else None lines.append(f"| `{page:#x}` | {count} | `{sym.name if sym else ''}` |") lines.append("") # Coverage steps = [e for e in events if e.get("type") == "step"] if steps: pcs = {(e.get("pc")) for e in steps} lines.append("## Stepped coverage") lines.append("") lines.append(f"* steps: {len(steps)}, unique PCs: {len(pcs)}") by_sym = Counter() for pc in pcs: sym = symbols.resolve(pc) if symbols else None by_sym[sym.name if sym else f"{pc:#x}"] += 1 lines.append("") lines.append("| PCs | symbol |") lines.append("|---:|---|") for name, count in by_sym.most_common(max_rows): lines.append(f"| {count} | `{name}` |") lines.append("") # Snapshots snapshots = meta.get("snapshots") or [e for e in events if e.get("type") == "snapshot_saved"] if snapshots: lines.append("## Snapshots") lines.append("") lines.append("| label | address | size | path | sha256 |") lines.append("|---|---|---:|---|---|") for snap in snapshots[:max_rows]: lines.append( f"| {snap.get('label')} | `{snap.get('addr', 0):#x}` | {snap.get('size', 0)} | " f"`{snap.get('path')}` | `{(snap.get('sha256') or '')[:16]}` |" ) lines.append("") # Timeline excerpt notable = [e for e in events if e.get("type") in ("bp_hit", "leave", "watch_hit", "mem_write", "note", "exit")] if notable: lines.append("## Timeline (first hits)") lines.append("") for event in notable[:max_rows]: t = event.get("t", 0) kind = event.get("type") if kind in ("bp_hit", "leave"): site = event.get("symbol") or f"{event.get('pc', 0):#x}" extra = "" regs = event.get("regs") or {} if "a0" in regs: extra = f" a0={regs['a0']:#x}" if kind == "leave" and "retval" in event: extra += f" -> {event['retval']:#x}" lines.append(f"* `{t:8.3f}s` {kind}: {site}{extra}") elif kind == "watch_hit": lines.append(f"* `{t:8.3f}s` watch: {annotate(event.get('addr'))} ({event.get('kind')})") elif kind == "mem_write": lines.append( f"* `{t:8.3f}s` write: {annotate(event.get('addr'))} " f"{event.get('size')}B {event.get('data', '')[:32]}" ) elif kind == "note": lines.append(f"* `{t:8.3f}s` note: {event.get('text')}") elif kind == "exit": lines.append(f"* `{t:8.3f}s` exit status={event.get('status')}") lines.append("") # Symbol-annotation table for automation lines.append("## Annotated sites") lines.append("") sites = Counter() for event in events: for key in ("pc", "addr"): value = event.get(key) if isinstance(value, int) and value and event.get("type") in ("bp_hit", "watch_hit", "mem_write"): sites[value] += 1 lines.append("| address | count | annotation |") lines.append("|---|---:|---|") for addr, count in sites.most_common(max_rows): lines.append(f"| `{addr:#x}` | {count} | `{annotate(addr)}` |") lines.append("") return "\n".join(lines) + "\n" def compare_sessions(a_dir: str, b_dir: str, symbols=None, max_rows: int = 40) -> str: """Diff two recorded sessions at the symbolic event level (A/B tooling).""" a_meta, b_meta = load_meta(a_dir), load_meta(b_dir) a_events, b_events = load_events(a_dir), load_events(b_dir) lines = [f"# green-eye session comparison", "", f"* A: {a_meta.get('name')} ({a_dir})", f"* B: {b_meta.get('name')} ({b_dir})", ""] def profile(events): bp = Counter() watch = Counter() writes = Counter() for event in events: if event.get("type") == "bp_hit": bp[event.get("symbol") or f"{event.get('pc', 0):#x}"] += 1 elif event.get("type") == "watch_hit": watch[(event.get("addr"), event.get("kind"))] += 1 elif event.get("type") == "mem_write": writes[(event.get("addr", 0) >> 12) << 12] += 1 return bp, watch, writes a_bp, a_watch, a_writes = profile(a_events) b_bp, b_watch, b_writes = profile(b_events) lines.append("## Breakpoint profile delta (A vs B)") lines.append("") lines.append("| symbol | A hits | B hits | delta |") lines.append("|---|---:|---:|---:|") for name in sorted(set(a_bp) | set(b_bp), key=lambda k: -(a_bp.get(k, 0) + b_bp.get(k, 0)))[:max_rows]: lines.append(f"| `{name}` | {a_bp.get(name, 0)} | {b_bp.get(name, 0)} | {b_bp.get(name, 0) - a_bp.get(name, 0):+d} |") lines.append("") lines.append("## Watchpoint delta") lines.append("") lines.append("| address | kind | A | B |") lines.append("|---|---|---:|---:|") for key in sorted(set(a_watch) | set(b_watch), key=lambda k: -(a_watch.get(k, 0) + b_watch.get(k, 0)))[:max_rows]: lines.append(f"| `{key[0]:#x}` | {key[1]} | {a_watch.get(key, 0)} | {b_watch.get(key, 0)} |") lines.append("") lines.append("## Written pages delta") lines.append("") lines.append("| page | A writes | B writes |") lines.append("|---|---:|---:|") for page in sorted(set(a_writes) | set(b_writes), key=lambda k: -(a_writes.get(k, 0) + b_writes.get(k, 0)))[:max_rows]: lines.append(f"| `{page:#x}` | {a_writes.get(page, 0)} | {b_writes.get(page, 0)} |") lines.append("") return "\n".join(lines) + "\n"