summaryrefslogtreecommitdiffstats
path: root/debug/risc-v-gsp-rm-vm/docs/03-SCRIPTING.md
blob: 93dfccf05738b1bd77134ae95d0871890e5701fb (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
# 03 — risc-v-gsp-rm-debugger scripting

> **# TOOL IN DEVELOPMENT - NOT READY YET!** See 00 for status.

risc-v-gsp-rm-debugger scripts are ordinary Python programs run with
`./risc-v-gsp-rm-debugger.sh run <script.py>` (or `python3 -m greeneye run …`). The
names `ge`, `session`/`s`, `memory`, `symbols`, `recorder` are preloaded, and
`__file__` points at the script path (use it to locate bundled assets).

## 1. Attach or launch

```python
import greeneye as ge

# Launch the bundled demo (QEMU -kernel fast path), recording by default.
s = ge.launch(kernel="firmware/uproc_demo/build/uproc_demo.elf", record=True, name="my-run")

# Or attach to an already-running gdbstub (e.g. started by `greeneye launch`).
# s = ge.attach(port=4444, record="dumps/attach-run")

try:
    ...
finally:
    s.close()          # clears breakpoints/watchpoints, kills owned QEMU
```

`launch(..., keep_alive=True)` leaves QEMU running after `close()`.

## 2. Symbols

```python
sym = s.symbols.get("rpc_sum")        # Symbol | None
addr = s.symbols.lookup("rpc_sum")    # int, raises if missing
s.symbols.annotate(0x80000048)        # "0x80000048 rpc_ping"
s.symbols.resolve(0x80000050)         # Symbol (containing or nearest)
s.resolve("rpc_sum")                  # int | int passthrough | "0x..." prefix
```

Symbols come from ELF (demo), Ghidra `names.tsv`/`decompiled_index.tsv`
(`symbols export`), or native JSON.

## 3. Hooks and breakpoints

```python
events = []

def on_sum_enter(hit):
    events.append(("enter", hit.arg(0), hit.arg(1)))   # a0, a1

def on_sum_leave(hit):
    events.append(("leave", hit.retval))               # a0 at return

handle = s.hook("rpc_sum", on_enter=on_sum_enter, on_leave=on_sum_leave)
handle.hits, handle.label, handle.addr
handle.detach()

bp = s.breakpoint(0x800000fe, callback=lambda hit: print(hit), temporary=True, kind="sw")
s.remove_breakpoint(bp)
```

`Hit` carries `kind`, `addr`, `regs` (full register dict), `symbol`,
`retval` (leave hits), `watch`, `stop`, `session`, plus helpers
`hit.arg(n)` and `hit.label()`.

A hook's `on_leave` is realised as a return breakpoint; the callback's
`hit.addr`/`hit.symbol` are the hooked function (not the return site), and
`hit.retval` is `a0`. Tail calls and interrupt returns are not tracked.

## 4. Watchpoints

```python
def on_write(hit):
    print(f"{hit.label()} written; pc={hit.pc:#x}")

wp = s.watch("gsp_ctrl+0x10", size=8, kind="w", callback=on_write)  # w|r|a
wp.hits
s.unwatch(wp)
```

The session transparently steps over the triggering store (required with
QEMU; see 01 §4).

## 5. Memory

```python
s.memory.read(0x80000840, 64)
s.memory.write_u64(0x80000860, 1)              # mailbox = PING
s.memory.hexdump(0x80000840, 128)              # annotated
s.memory.search_string(0x80000000, 0x10000, "GREEN")
s.memory.search_u32(0x80000000, 0x10000, 0x47535031)
s.memory.read_cstring(0x800007d0)
s.memory.snapshot(0x80000840, 392, name="ctrl")
```

`Memory.read`/`write` record `mem_read`/`mem_write` events; snapshots are
saved and hashed by the recorder.

## 6. Structures

```python
import json
from greeneye.structs import schema_from_json, render, decode

schema = schema_from_json(json.load(open("firmware/uproc_demo/gsp_ctrl.schema.json")))
values = decode(schema, s.memory.read(ctrl, schema.size), base=ctrl)
print(render(schema, values))
```

A schema is JSON: `{"name":…, "fields":[{"name","offset","fmt","size?","enum?","note?"}]}`
with `fmt` in `u8..u64`, `s8..s64`, `f32/f64`, `bool`, `bytes`, `cstr`,
`ptr`.

## 7. Snapshots and diffs

```python
from greeneye import Memory

before = s.snapshot(ctrl, schema.size, "before")
s.memory.write_u64(ctrl + 32, 1)
s.run(duration=0.4)
after = s.snapshot(ctrl, schema.size, "after")
print(Memory.render_diff(Memory.diff(before, after, base=ctrl), s.symbols))
```

## 8. Running

```python
s.run(duration=1.0)                 # service events for one second
s.run(max_stops=20)                 # or until 20 stops
hits = s.step(10)                   # single-step and dispatch hooks
s.stop()                            # interrupt and dispatch
```

`run` returns the number of stops serviced. Every stop is recorded.

## 9. Declarative probes

For simple instrumentation, ship a JSON spec instead of Python:

```json
{
  "probes": [
    {"name": "sum", "symbol": "rpc_sum", "when": "a1 > 0",
     "on_enter": {"log": ["a0", "a1"],
                  "dump": [{"reg": "a0", "len": 32, "label": "sum-input"}],
                  "note": "sum called"},
     "on_leave": {"log": ["retval"]}}
  ]
}
```

```python
from greeneye.probes import load_spec, install
install(s, load_spec("examples/probes/demo_probes.json"))
s.run(duration=1.0)
```

Conditions accept register names (`a0..a7`, `pc`, `ra`, `sp`, `x0..x31`,
`retval`), integer/hex literals and `and/or/not` with comparisons; arbitrary
code is rejected. Actions: `log`, `dump` (save a blob), `snapshot`, `note`.

## 10. Event stream and plugins

```python
s.on_event("watch_hit", lambda hit: print("wrote", hit.label()))
s.note("starting phase 2")

# In a plugin module:
def register(ge):
    ge.add_command("mycmd", lambda session, args: 0)
```

See [04-PLUGINS](04-PLUGINS.md).

## 11. Recording and post-analysis from Python

```python
s.recorder.dir                    # session directory (or None)
s.recorder.event("phase", name="p2")
s.recorder.close()                # writes session.json + summary.md

from greeneye.analyze import render_summary, compare_sessions
print(render_summary("dumps/run-a", symbols=s.symbols))
print(compare_sessions("dumps/run-a", "dumps/run-b", symbols=s.symbols))
```

## 12. A complete worked script

```python
"""Drive the demo, capture a PING and a FAULT, and summarise."""
import greeneye as ge

s = ge.launch(kernel="firmware/uproc_demo/build/uproc_demo.elf", record=True, name="worked")
ctrl = s.symbols.lookup("gsp_ctrl")
hits = []
try:
    for name in ("rpc_ping", "rpc_sum", "rpc_fault"):
        if s.symbols.get(name):
            s.hook(name, on_enter=lambda h: hits.append(h.label()))
    s.watch(ctrl + 0x10, size=8, kind="w")
    s.run(duration=0.5)                      # self-test
    s.memory.write_u64(ctrl + 0x20, 1)       # PING
    s.memory.write_u64(ctrl + 0x28, 0x1234)
    s.run(duration=0.5)
    s.memory.write_u64(ctrl + 0x20, 6)       # FAULT -> ebreak
    s.run(duration=1.0, max_stops=1)
    print("pc:", hex(s.pc), s.symbols.annotate(s.pc))
    s.step()                                 # past the trap
    print("hits:", hits)
finally:
    print("recording:", getattr(s.recorder, "dir", None))
    s.close()
```

## 13. Errors and timeouts

All risc-v-gsp-rm-debugger errors derive from `greeneye.GreenEyeError`:
`RspTimeout` (transport), `RspProtocolError`, `TargetError`,
`SymbolNotFound`, `StructError`. `RspClient.timeout` (default 10 s) bounds a
single packet read; `run`'s budget bounds the whole pass.