Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

exec.gno

13.54 Kb · 432 lines
  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// 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).
 21package riscv
 22
 23import (
 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.
 30const 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.
 35const DefaultEntry uint32 = 0x1000
 36
 37const (
 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.
 48var ErrNoProgram = errors.New("riscv: machine has no program")
 49
 50// Machine is an RV32IM hart, mid-execution.
 51type 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.
 70func 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.
 98func (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.
115func (m *Machine) Registers() [32]uint32 { return m.reg }
116
117// PC returns the program counter.
118func (m *Machine) PC() uint32 { return m.pc }
119
120// Memory returns the address space.
121func (m *Machine) Memory() *Memory { return m.mem }
122
123// Status returns the machine's status.
124func (m *Machine) Status() vmkit.Status { return m.status }
125
126// Trap returns why the machine trapped, making Machine a [vmkit.Trapper].
127func (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.
145func (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.
403func 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.
412func 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.
424func 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}