Kind Pure package
Name v0
Namespace moul / x / vm / riscv
Exported functions n/a — not supported for pure packages by the node (vm/qfuncs)
Module gno.land/p/moul/x/vm/riscv/v0
gno 0.9 exec.gno gno
⧉
1 // Package riscv is an RV32IM emulator for gno.land: 48 instructions that every 2 // real compiler already targets. 3 // 4 // A guest here is not written in a new language. `rustc --target 5 // riscv32im-unknown-none-elf`, `clang -target riscv32`, TinyGo and Zig all emit 6 // it, so a program arrives compiled by the real compiler and optimized by the 7 not supported for pure packages by the node (vm/qfuncs)
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
// real optimizer. The interpreter is a decode loop rather than a semantic
8 // minefield: fetch four bytes, switch on seven bits, do arithmetic.
9 //
10 // No F or D. No floats means no rounding-mode ambiguity and nothing that can
11 // differ between nodes. The M extension's divide-by-zero and overflow cases are
12 // defined by the spec to RETURN a value rather than trap, which is exactly what
13 // a deterministic chain wants.
14 //
15 // It implements [vmkit.Machine], so it shares the fuel meter, the snapshot
16 // codec and the instance store with [p/moul/x/vm/bf](/p/moul/x/vm/bf/v0). That
17 // sharing is the point: the second guest is what tells you whether the ABI was
18 // shaped around the first one.
19 //
20 // Live demo: [r/moul/x/vm/riscvdemo](/r/moul/x/vm/riscvdemo/v0).
21 package riscv
22
23 import (
24 "errors"
25
26 "gno.land/p/moul/x/vm/vmkit/v0"
27 )
28
29 // VMName is the identifier this machine registers under in a vmkit.Instance.
30 const VMName = "riscv32im"
31
32 // DefaultEntry is where a flat text image is conventionally loaded and where
33 // execution starts. Low enough to leave a scratch page below it, aligned, and
34 // nothing about the machine requires it: NewMachine takes the entry point.
35 const DefaultEntry uint32 = 0x1000
36
37 const (
38 snapMagic uint32 = 0x52563332 // "RV32"
39
40 // snapVersion 2 carries the text segment's extent, which version 1 did
41 // not need because the interpreter decoded from memory on every fetch.
42 // Predecoding moved that work to load time, so a restored hart has to be
43 // told which part of its memory is code.
44 snapVersion byte = 2
45 )
46
47 // ErrNoProgram is returned when a Machine is stepped with no image loaded.
48 var ErrNoProgram = errors.New( "riscv: machine has no program" )
49
50 // Machine is an RV32IM hart, mid-execution.
51 type Machine struct {
52 reg [ 32 ] uint32
53 pc uint32
54 mem *Memory
55
56 // code is the text segment with its fields already pulled apart. See
57 // predecode.gno for why the dispatch loop reads arrays and not memory.
58 code *code
59
60 status vmkit.Status
61 trap string
62
63 // out is what the guest wrote through the write syscall, mirrored to the
64 // host as it goes. Kept here too so a snapshot round-trips it.
65 outLen int
66 }
67
68 // NewMachine returns a hart with the image loaded at entry, pc at entry, and
69 // the stack pointer at the top of memory.
70 func NewMachine(image [] byte , entry uint32 ) (*Machine, error ) {
71 if entry% 4 != 0 {
72 return nil , errors.New( "riscv: entry point is not 4-byte aligned" )
73 }
74 if len(image)% 4 != 0 {
75 return nil , errors.New( "riscv: image length is not a whole number of instructions" )
76 }
77 m := &Machine{mem: NewMemory(), pc: entry, status: vmkit.Running}
78 if !m.mem.WriteImage(entry, image) {
79 return nil , errors.New( "riscv: image does not fit at the entry point" )
80 }
81 m.code = predecode(entry, image)
82 // x2 is sp by convention. A guest that never touches the stack does not
83 // care; one compiled by rustc does, immediately.
84 m.reg[ 2 ] = MemSize
85 return m, nil
86 }
87
88 // LoadData places initialized data in memory, outside the text segment.
89 //
90 // The other half of a flat-image loader. A program with a .data section needs
91 // its initial bytes in memory and needs to write them back afterwards, which
92 // the text segment forbids: W xor X is what makes the predecode sound. So the
93 // linker puts data at its own address and it arrives here separately, rather
94 // than being concatenated onto the code and silently becoming read-only.
95 //
96 // The pages are marked dirty, so a snapshot carries the data the same way it
97 // carries the program.
98 func (m *Machine) LoadData(addr uint32 , b [] byte ) error {
99 if m.mem == nil {
100 return ErrNoProgram
101 }
102 if len(b) > 0 && m.code != nil && m.code.words > 0 {
103 lo, hi := uint64 (addr), uint64 (addr)+ uint64 (len(b))
104 if lo < uint64 (m.code.end) && hi > uint64 (m.code.base) {
105 return errors.New( "riscv: data segment overlaps the text segment" )
106 }
107 }
108 if !m.mem.WriteImage(addr, b) {
109 return errors.New( "riscv: data does not fit at that address" )
110 }
111 return nil
112 }
113
114 // Registers returns a copy of the register file, for tests and rendering.
115 func (m *Machine) Registers() [ 32 ] uint32 { return m.reg }
116
117 // PC returns the program counter.
118 func (m *Machine) PC() uint32 { return m.pc }
119
120 // Memory returns the address space.
121 func (m *Machine) Memory() *Memory { return m.mem }
122
123 // Status returns the machine's status.
124 func (m *Machine) Status() vmkit.Status { return m.status }
125
126 // Trap returns why the machine trapped, making Machine a [vmkit.Trapper].
127 func (m *Machine) Trap() string { return m.trap }
128
129 // Step runs until the program halts, traps, or spends `fuel` instructions.
130 //
131 // Written the way the bf ladder concluded, and for the same measured reasons:
132 // the hot state is in locals and written back once, and the fuel counter is two
133 // locals rather than a vmkit.Meter, because calling a method per instruction
134 // cost 86% there and would cost more here, where an instruction is cheaper.
135 //
136 // Two things in this loop look like premature optimization and are not, both
137 // measured in switchcost_test.gno on this VM:
138 //
139 // - The case order is the internal opcode order from predecode.gno, which is
140 // by dynamic frequency, because the GnoVM scans switch cases in source
141 // order at 278 gas each. Reordering the arms below changes what the
142 // interpreter costs.
143 // - Everything the loop touches is a local. Reading cop[i] through m.code.op
144 // instead would pay for two field lookups on every instruction.
145 func (m *Machine) Step(h vmkit.Host, fuel int64 ) ( int64 , vmkit.Status) {
146 if m.mem == nil || m.code == nil {
147 m.status, m.trap = vmkit.Trapped, "no program"
148 return 0 , m.status
149 }
150 if m.status != vmkit.Running {
151 return 0 , m.status
152 }
153
154 pc := m.pc
155 // A misaligned pc would index the word containing it rather than trap,
156 // because the index is a shift. Only a restored snapshot can get here with
157 // one, since JALR is the only instruction that can produce it and it
158 // checks its own target.
159 if pc% 4 != 0 {
160 m.status, m.trap = vmkit.Trapped, "misaligned program counter"
161 return 0 , m.status
162 }
163
164 reg := &m.reg
165 mem := m.mem
166
167 c := m.code
168 cop, crd, crs1, crs2, cimm := c.op, c.rd, c.rs1, c.rs2, c.imm
169 cbase, cwords, cend := c.base, c.words, c.end
170 defer func () { m.pc = pc }()
171
172 budget := fuel
173 if budget < 0 {
174 budget = vmkit.Unmetered
175 }
176 metered := budget != vmkit.Unmetered
177 var used int64
178
179 for {
180 if metered && used >= budget {
181 return used, vmkit.Running
182 }
183 used++
184
185 // One subtraction and one shift replace the fetch. A pc below the text
186 // segment underflows to a huge index and is caught by the same
187 // comparison, which is why there is no second bound here.
188 i := (pc - cbase) >> 2
189 if i >= cwords {
190 m.status, m.trap = vmkit.Trapped, "instruction fetch out of range"
191 return used, m.status
192 }
193 next := pc + 4
194
195 switch cop[i] {
196 case iADDI:
197 reg[crd[i]] = reg[crs1[i]] + cimm[i]
198 case iLW:
199 v, ok := mem.Load32(reg[crs1[i]] + cimm[i])
200 if !ok {
201 m.status, m.trap = vmkit.Trapped, "load out of range"
202 return used, m.status
203 }
204 reg[crd[i]] = v
205 case iSW:
206 addr := reg[crs1[i]] + cimm[i]
207 if addr < cend && addr+ 4 > cbase {
208 m.status, m.trap = vmkit.Trapped, "store into text segment"
209 return used, m.status
210 }
211 if !mem.Store32(addr, reg[crs2[i]]) {
212 m.status, m.trap = vmkit.Trapped, "store out of range"
213 return used, m.status
214 }
215 case iADD:
216 reg[crd[i]] = reg[crs1[i]] + reg[crs2[i]]
217 case iBNE:
218 if reg[crs1[i]] != reg[crs2[i]] {
219 next = pc + cimm[i]
220 }
221 case iBEQ:
222 if reg[crs1[i]] == reg[crs2[i]] {
223 next = pc + cimm[i]
224 }
225 case iJAL:
226 reg[crd[i]] = next
227 next = pc + cimm[i]
228 case iJALR:
229 // The target is computed before rd is written, because rd and rs1
230 // are allowed to be the same register and usually are: `ret` is
231 // jalr x0, 0(x1).
232 t := (reg[crs1[i]] + cimm[i]) &^ 1
233 if t% 4 != 0 {
234 m.status, m.trap = vmkit.Trapped, "misaligned jump target"
235 return used, m.status
236 }
237 reg[crd[i]] = next
238 next = t
239 case iLUI:
240 reg[crd[i]] = cimm[i]
241 case iSLLI:
242 reg[crd[i]] = reg[crs1[i]] << (cimm[i] & 0x1F )
243 case iSRLI:
244 reg[crd[i]] = reg[crs1[i]] >> (cimm[i] & 0x1F )
245 case iANDI:
246 reg[crd[i]] = reg[crs1[i]] & cimm[i]
247 case iSUB:
248 reg[crd[i]] = reg[crs1[i]] - reg[crs2[i]]
249 case iORI:
250 reg[crd[i]] = reg[crs1[i]] | cimm[i]
251 case iXORI:
252 reg[crd[i]] = reg[crs1[i]] ^ cimm[i]
253 case iBLT:
254 if int32 (reg[crs1[i]]) < int32 (reg[crs2[i]]) {
255 next = pc + cimm[i]
256 }
257 case iBGE:
258 if int32 (reg[crs1[i]]) >= int32 (reg[crs2[i]]) {
259 next = pc + cimm[i]
260 }
261 case iBLTU:
262 if reg[crs1[i]] < reg[crs2[i]] {
263 next = pc + cimm[i]
264 }
265 case iBGEU:
266 if reg[crs1[i]] >= reg[crs2[i]] {
267 next = pc + cimm[i]
268 }
269 case iSRAI:
270 reg[crd[i]] = uint32 ( int32 (reg[crs1[i]]) >> (cimm[i] & 0x1F ))
271 case iSLL:
272 reg[crd[i]] = reg[crs1[i]] << (reg[crs2[i]] & 0x1F )
273 case iSRL:
274 reg[crd[i]] = reg[crs1[i]] >> (reg[crs2[i]] & 0x1F )
275 case iSRA:
276 reg[crd[i]] = uint32 ( int32 (reg[crs1[i]]) >> (reg[crs2[i]] & 0x1F ))
277 case iAND:
278 reg[crd[i]] = reg[crs1[i]] & reg[crs2[i]]
279 case iOR:
280 reg[crd[i]] = reg[crs1[i]] | reg[crs2[i]]
281 case iXOR:
282 reg[crd[i]] = reg[crs1[i]] ^ reg[crs2[i]]
283 case iLBU:
284 v, ok := mem.Load8(reg[crs1[i]] + cimm[i])
285 if !ok {
286 m.status, m.trap = vmkit.Trapped, "load out of range"
287 return used, m.status
288 }
289 reg[crd[i]] = uint32 (v)
290 case iLB:
291 v, ok := mem.Load8(reg[crs1[i]] + cimm[i])
292 if !ok {
293 m.status, m.trap = vmkit.Trapped, "load out of range"
294 return used, m.status
295 }
296 reg[crd[i]] = uint32 ( int32 ( int8 (v)))
297 case iLH:
298 v, ok := mem.Load16(reg[crs1[i]] + cimm[i])
299 if !ok {
300 m.status, m.trap = vmkit.Trapped, "load out of range"
301 return used, m.status
302 }
303 reg[crd[i]] = uint32 ( int32 ( int16 (v)))
304 case iLHU:
305 v, ok := mem.Load16(reg[crs1[i]] + cimm[i])
306 if !ok {
307 m.status, m.trap = vmkit.Trapped, "load out of range"
308 return used, m.status
309 }
310 reg[crd[i]] = uint32 (v)
311 case iSB:
312 addr := reg[crs1[i]] + cimm[i]
313 if addr < cend && addr+ 4 > cbase {
314 m.status, m.trap = vmkit.Trapped, "store into text segment"
315 return used, m.status
316 }
317 if !mem.Store8(addr, uint8 (reg[crs2[i]])) {
318 m.status, m.trap = vmkit.Trapped, "store out of range"
319 return used, m.status
320 }
321 case iSH:
322 addr := reg[crs1[i]] + cimm[i]
323 if addr < cend && addr+ 4 > cbase {
324 m.status, m.trap = vmkit.Trapped, "store into text segment"
325 return used, m.status
326 }
327 if !mem.Store16(addr, uint16 (reg[crs2[i]])) {
328 m.status, m.trap = vmkit.Trapped, "store out of range"
329 return used, m.status
330 }
331 case iAUIPC:
332 reg[crd[i]] = pc + cimm[i]
333 case iSLT:
334 reg[crd[i]] = b2u( int32 (reg[crs1[i]]) < int32 (reg[crs2[i]]))
335 case iSLTU:
336 reg[crd[i]] = b2u(reg[crs1[i]] < reg[crs2[i]])
337 case iSLTI:
338 reg[crd[i]] = b2u( int32 (reg[crs1[i]]) < int32 (cimm[i]))
339 case iSLTIU:
340 reg[crd[i]] = b2u(reg[crs1[i]] < cimm[i])
341 case iMUL:
342 reg[crd[i]] = reg[crs1[i]] * reg[crs2[i]]
343 case iMULH:
344 reg[crd[i]] = uint32 (( int64 ( int32 (reg[crs1[i]])) * int64 ( int32 (reg[crs2[i]]))) >> 32 )
345 case iMULHSU:
346 reg[crd[i]] = uint32 (( int64 ( int32 (reg[crs1[i]])) * int64 ( uint64 (reg[crs2[i]]))) >> 32 )
347 case iMULHU:
348 reg[crd[i]] = uint32 (( uint64 (reg[crs1[i]]) * uint64 (reg[crs2[i]])) >> 32 )
349 case iDIV:
350 // The M extension defines divide by zero and the one signed
351 // overflow case to RETURN a value rather than trap. That is what
352 // makes it safe on a chain: there is no host arithmetic exception
353 // to differ between nodes.
354 reg[crd[i]] = divS( int32 (reg[crs1[i]]), int32 (reg[crs2[i]]))
355 case iDIVU:
356 if reg[crs2[i]] == 0 {
357 reg[crd[i]] = ^ uint32 ( 0 )
358 } else {
359 reg[crd[i]] = reg[crs1[i]] / reg[crs2[i]]
360 }
361 case iREM:
362 reg[crd[i]] = remS( int32 (reg[crs1[i]]), int32 (reg[crs2[i]]))
363 case iREMU:
364 if reg[crs2[i]] == 0 {
365 reg[crd[i]] = reg[crs1[i]]
366 } else {
367 reg[crd[i]] = reg[crs1[i]] % reg[crs2[i]]
368 }
369 case iECALL:
370 st, halt := m.ecall(h)
371 if halt {
372 // pc advances past the ecall before returning, so a halted
373 // hart points at what it would run next rather than at the
374 // syscall it already made. The deferred write-back is the only
375 // thing that sets m.pc, so assigning the local is how this is
376 // said.
377 m.status = st
378 pc = next
379 return used, m.status
380 }
381 case iEBREAK:
382 m.status, m.trap = vmkit.Trapped, "ebreak"
383 pc = next
384 return used, m.status
385 case iFENCE:
386 // FENCE orders memory for a hart that has neighbours. There is
387 // exactly one here, so it is architecturally a no-op rather than
388 // an unimplemented instruction.
389 default :
390 m.status, m.trap = vmkit.Trapped, "illegal instruction"
391 return used, m.status
392 }
393
394 // x0 is hardwired to zero. Writing it is legal and discarded, and
395 // clearing after the fact is cheaper than branching on rd in every
396 // arm above.
397 reg[ 0 ] = 0
398 pc = next
399 }
400 }
401
402 // b2u is the spec's "set if" result: 1 or 0, never a Go bool.
403 func b2u(b bool ) uint32 {
404 if b {
405 return 1
406 }
407 return 0
408 }
409
410 // divS is DIV with the two cases the spec pins down: division by zero yields
411 // all ones, and the most negative value divided by -1 overflows to itself.
412 func divS(a, b int32 ) uint32 {
413 if b == 0 {
414 return ^ uint32 ( 0 )
415 }
416 if a == - 2147483648 && b == - 1 {
417 return uint32 (a)
418 }
419 return uint32 (a / b)
420 }
421
422 // remS is REM, with the matching pair: remainder by zero is the dividend, and
423 // the overflow case is zero.
424 func remS(a, b int32 ) uint32 {
425 if b == 0 {
426 return uint32 (a)
427 }
428 if a == - 2147483648 && b == - 1 {
429 return 0
430 }
431 return uint32 (a % b)
432 }
433