summaryrefslogtreecommitdiffstats
path: root/debug/risc-v-gsp-rm-vm/greeneye/analyze.py
blob: 1c7e930fe56abf830288f0fd91d928cdd99a9c70 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
"""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 json
import os
import re
from collections import Counter, defaultdict

from .record import load_events, load_meta

_TRACE_KINDS = ("exec", "access", "read", "write")
_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 _trace_kind(record: dict) -> str | None:
    """Native/TCG trace records use ``t``; folded session events use ``type``."""
    return record.get("type") or record.get("t")


def _number(value, default: int = 0) -> int:
    if isinstance(value, int):
        return value
    if isinstance(value, str):
        try:
            return int(value, 0)
        except ValueError:
            return default
    return default


def load_trace(session_dir: str, meta: dict | None = None) -> list[dict]:
    """Load the trace blob referenced by ``meta['trace_blob']`` (R4)."""
    meta = meta if meta is not None else load_meta(session_dir)
    rel = meta.get("trace_blob")
    if not rel:
        return []
    path = rel if os.path.isabs(rel) else os.path.join(session_dir, rel)
    records: list[dict] = []
    try:
        with open(path, "r", encoding="utf-8") as fh:
            for line in fh:
                line = line.strip()
                if not line:
                    continue
                record = json.loads(line)
                if _trace_kind(record) != "summary":
                    records.append(record)
    except (OSError, ValueError):
        return []
    return records


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, trace=load_trace(session_dir, meta)
    )


def render_summary_from(meta: dict, events: list[dict], symbols=None, max_rows: int = 40,
                        trace: list[dict] | None = None) -> str:
    if trace is None:
        trace = [e for e in events if _trace_kind(e) in _TRACE_KINDS]
    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("")

    # Execution/access trace coverage (native R4 or folded TCG trace)
    if trace:
        def access_kind(record):
            kind = _trace_kind(record)
            if kind == "access":
                return "write" if str(record.get("rw", "")).startswith("w") else "read"
            return kind

        execs = [r for r in trace if access_kind(r) == "exec"]
        reads = [r for r in trace if access_kind(r) == "read"]
        writes = [r for r in trace if access_kind(r) == "write"]
        lines.append("## Execution trace")
        lines.append("")
        blob = meta.get("trace_blob")
        if blob:
            lines.append(f"* trace blob: `{blob}`")
        pcs = Counter()
        for record in execs:
            pcs[_number(record.get("pc"))] += 1
        suffix = f", unique PCs: {len(pcs)}" if execs else " (exec=off)"
        lines.append(f"* exec events: {len(execs)}{suffix}")
        read_pages = Counter()
        for record in reads:
            read_pages[(_number(record.get("addr")) >> 12) << 12] += 1
        write_pages = Counter()
        for record in writes:
            write_pages[(_number(record.get("addr")) >> 12) << 12] += 1
        lines.append(
            f"* accesses: {len(reads)} reads ({len(read_pages)} pages), "
            f"{len(writes)} writes ({len(write_pages)} pages)"
        )
        config = meta.get("trace") or {}
        if config:
            bits = [f"exec={'on' if config.get('exec') else 'off'}"]
            for key in ("start", "end"):
                if config.get(key) is not None:
                    bits.append(f"{key}={config[key]:#x}" if isinstance(config[key], int) else f"{key}={config[key]}")
            if config.get("max_events"):
                bits.append(f"max_events={config['max_events']}")
            if config.get("dropped"):
                bits.append(f"dropped={config['dropped']}")
            lines.append("* trace config: " + ", ".join(bits))
        lines.append("")

        if execs:
            by_sym = defaultdict(lambda: [0, 0])  # unique PCs, exec count
            for pc, count in pcs.items():
                sym = symbols.resolve(pc) if symbols else None
                entry = by_sym[sym.name if sym else f"{pc:#x}"]
                entry[0] += 1
                entry[1] += count
            lines.append("### Unique PCs by symbol")
            lines.append("")
            lines.append("| unique PCs | execs | symbol |")
            lines.append("|---:|---:|---|")
            for name, (unique, count) in sorted(by_sym.items(), key=lambda kv: -kv[1][1])[:max_rows]:
                lines.append(f"| {unique} | {count} | `{name}` |")
            lines.append("")

        if read_pages or write_pages:
            lines.append("### Accessed pages")
            lines.append("")
            lines.append("| page | reads | writes | symbol |")
            lines.append("|---|---:|---:|---|")
            for page in sorted(set(read_pages) | set(write_pages),
                               key=lambda p: -(read_pages.get(p, 0) + write_pages.get(p, 0)))[:max_rows]:
                sym = symbols.resolve(page) if symbols else None
                lines.append(
                    f"| `{page:#x}` | {read_pages.get(page, 0)} | {write_pages.get(page, 0)} "
                    f"| `{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)
    a_trace, b_trace = load_trace(a_dir, a_meta), load_trace(b_dir, b_meta)
    lines = [f"# green-eye session comparison", "", f"* A: {a_meta.get('name')} ({a_dir})", f"* B: {b_meta.get('name')} ({b_dir})", ""]

    def trace_kind(record: dict) -> str:
        kind = _trace_kind(record)
        if kind == "access":
            return "write" if str(record.get("rw", "")).startswith("w") else "read"
        return kind

    def profile(events, trace):
        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
        for record in trace:
            if trace_kind(record) == "write":
                writes[(_number(record.get("addr")) >> 12) << 12] += 1
        return bp, watch, writes

    a_bp, a_watch, a_writes = profile(a_events, a_trace)
    b_bp, b_watch, b_writes = profile(b_events, b_trace)

    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("")

    def trace_totals(trace):
        totals = Counter()
        for record in trace:
            kind = trace_kind(record)
            if kind in ("exec", "read", "write"):
                totals[kind] += 1
        return totals

    a_totals, b_totals = trace_totals(a_trace), trace_totals(b_trace)
    if a_trace or b_trace:
        lines.append("## Trace stream totals (A vs B)")
        lines.append("")
        lines.append("| stream | A | B | delta |")
        lines.append("|---|---:|---:|---:|")
        for kind in ("exec", "read", "write"):
            a_count, b_count = a_totals.get(kind, 0), b_totals.get(kind, 0)
            lines.append(f"| {kind} | {a_count} | {b_count} | {b_count - a_count:+d} |")
        lines.append("")
    return "\n".join(lines) + "\n"