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

riscv_test.gno

10.47 Kb · 311 lines
  1package riscv
  2
  3import (
  4	"testing"
  5
  6	"gno.land/p/moul/x/vm/vmkit/v0"
  7	"gno.land/p/nt/uassert/v0"
  8)
  9
 10// A tiny assembler, so the programs below read as instructions rather than as
 11// hex. Encoding here and decoding in decode.gno are written from the spec
 12// independently: if one has a field in the wrong place the tests fail, which is
 13// the point of not sharing a helper between them.
 14
 15func rType(f7, rs2, rs1, f3, rd, op uint32) uint32 {
 16	return f7<<25 | rs2<<20 | rs1<<15 | f3<<12 | rd<<7 | op
 17}
 18func iType(imm, rs1, f3, rd, op uint32) uint32 {
 19	return (imm&0xFFF)<<20 | rs1<<15 | f3<<12 | rd<<7 | op
 20}
 21func sType(imm, rs2, rs1, f3, op uint32) uint32 {
 22	return (imm>>5&0x7F)<<25 | rs2<<20 | rs1<<15 | f3<<12 | (imm&0x1F)<<7 | op
 23}
 24func bType(imm, rs2, rs1, f3, op uint32) uint32 {
 25	return (imm>>12&0x1)<<31 | (imm>>5&0x3F)<<25 | rs2<<20 | rs1<<15 |
 26		f3<<12 | (imm>>1&0xF)<<8 | (imm>>11&0x1)<<7 | op
 27}
 28func uType(imm, rd, op uint32) uint32 { return imm&0xFFFFF000 | rd<<7 | op }
 29func jType(imm, rd, op uint32) uint32 {
 30	return (imm>>20&0x1)<<31 | (imm>>1&0x3FF)<<21 | (imm>>11&0x1)<<20 |
 31		(imm>>12&0xFF)<<12 | rd<<7 | op
 32}
 33
 34func addi(rd, rs1, imm uint32) uint32 { return iType(imm, rs1, 0x0, rd, opImm) }
 35func add(rd, rs1, rs2 uint32) uint32  { return rType(0x00, rs2, rs1, 0x0, rd, opReg) }
 36func sub(rd, rs1, rs2 uint32) uint32  { return rType(0x20, rs2, rs1, 0x0, rd, opReg) }
 37func mul(rd, rs1, rs2 uint32) uint32  { return rType(0x01, rs2, rs1, 0x0, rd, opReg) }
 38func divi(rd, rs1, rs2 uint32) uint32 { return rType(0x01, rs2, rs1, 0x4, rd, opReg) }
 39func remi(rd, rs1, rs2 uint32) uint32 { return rType(0x01, rs2, rs1, 0x6, rd, opReg) }
 40func sw(rs2, rs1, imm uint32) uint32  { return sType(imm, rs2, rs1, 0x2, opStore) }
 41func lw(rd, rs1, imm uint32) uint32   { return iType(imm, rs1, 0x2, rd, opLoad) }
 42func lbu(rd, rs1, imm uint32) uint32  { return iType(imm, rs1, 0x4, rd, opLoad) }
 43func bne(rs1, rs2, imm uint32) uint32 { return bType(imm, rs2, rs1, 0x1, opBranch) }
 44func ecall() uint32                   { return iType(0, 0, 0, 0, opSystem) }
 45
 46// exitWith is the two instructions every test program ends with: set a7 to the
 47// exit syscall and trap into the host.
 48func exitWith() []uint32 { return []uint32{addi(17, 0, sysExit), ecall()} }
 49
 50func asm(words []uint32) []byte {
 51	out := make([]byte, 0, len(words)*4)
 52	for _, w := range words {
 53		out = append(out, byte(w), byte(w>>8), byte(w>>16), byte(w>>24))
 54	}
 55	return out
 56}
 57
 58const entry = 0x1000
 59
 60func run(t *testing.T, words []uint32, h vmkit.Host) *Machine {
 61	t.Helper()
 62	m, err := NewMachine(asm(words), entry)
 63	uassert.NoError(t, err)
 64	if m == nil {
 65		return nil
 66	}
 67	_, status := m.Step(h, vmkit.Unmetered)
 68	uassert.Equal(t, "halted", status.String())
 69	return m
 70}
 71
 72func TestArithmetic(t *testing.T) {
 73	// x5 = 7 + 35; x6 = x5 - 2; x7 = x6 * 3
 74	prog := append([]uint32{
 75		addi(5, 0, 7),
 76		addi(5, 5, 35),
 77		addi(6, 0, 2),
 78		sub(6, 5, 6),
 79		addi(7, 0, 3),
 80		mul(7, 6, 7),
 81	}, exitWith()...)
 82	m := run(t, prog, vmkit.NewTestHost())
 83	r := m.Registers()
 84	uassert.Equal(t, uint64(42), uint64(r[5]))
 85	uassert.Equal(t, uint64(40), uint64(r[6]))
 86	uassert.Equal(t, uint64(120), uint64(r[7]))
 87}
 88
 89// Every immediate in the base set is sign extended from its top bit. Small
 90// positive programs work either way, which is what makes this the classic
 91// decoder bug: it only shows up on a negative constant.
 92func TestImmediatesAreSignExtended(t *testing.T) {
 93	prog := append([]uint32{
 94		addi(5, 0, 10),
 95		addi(5, 5, 0xFFF), // -1
 96		addi(6, 0, 0x800), // -2048, the most negative 12-bit immediate
 97	}, exitWith()...)
 98	m := run(t, prog, vmkit.NewTestHost())
 99	r := m.Registers()
100	uassert.Equal(t, uint64(9), uint64(r[5]))
101	uassert.Equal(t, int64(-2048), int64(int32(r[6])))
102}
103
104// x0 reads as zero and discards every write, and the spec has no exception.
105func TestRegisterZeroIsHardwired(t *testing.T) {
106	prog := append([]uint32{
107		addi(0, 0, 99),
108		add(5, 0, 0),
109	}, exitWith()...)
110	m := run(t, prog, vmkit.NewTestHost())
111	uassert.Equal(t, uint64(0), uint64(m.Registers()[0]))
112	uassert.Equal(t, uint64(0), uint64(m.Registers()[5]))
113}
114
115func TestLoadsAndStores(t *testing.T) {
116	prog := append([]uint32{
117		addi(5, 0, 0x7F0), // an address well clear of the program
118		addi(6, 0, 0x123),
119		sw(6, 5, 0),
120		lw(7, 5, 0),
121		lbu(8, 5, 0), // little-endian: the low byte is 0x23
122	}, exitWith()...)
123	m := run(t, prog, vmkit.NewTestHost())
124	r := m.Registers()
125	uassert.Equal(t, uint64(0x123), uint64(r[7]))
126	uassert.Equal(t, uint64(0x23), uint64(r[8]))
127}
128
129// A loop, which is the only way to know branches and the pc agree.
130func TestBranchLoopSumsToTen(t *testing.T) {
131	// x5 = 0; x6 = 5; do { x5 += x6; x6 -= 1 } while (x6 != 0)  => 15
132	prog := append([]uint32{
133		addi(5, 0, 0),
134		addi(6, 0, 5),
135		add(5, 5, 6),        // loop:
136		addi(6, 6, 0xFFF),   // x6 -= 1
137		bne(6, 0, 0x1FF8),   // -8, back to loop
138	}, exitWith()...)
139	m := run(t, prog, vmkit.NewTestHost())
140	uassert.Equal(t, uint64(15), uint64(m.Registers()[5]))
141}
142
143// The M extension's defined answers. These are the cases a chain cares about,
144// because the spec makes them RETURN rather than trap and every implementation
145// therefore has to agree.
146func TestDivisionEdgeCases(t *testing.T) {
147	const minInt32 = 0x80000000
148	prog := append([]uint32{
149		addi(5, 0, 7),
150		addi(6, 0, 0),
151		divi(7, 5, 6), // 7 / 0 = all ones
152		remi(8, 5, 6), // 7 % 0 = 7
153		uType(minInt32, 9, opLUI),
154		addi(10, 0, 0xFFF), // -1
155		divi(11, 9, 10),    // overflow: stays minInt32
156		remi(12, 9, 10),    // overflow: 0
157	}, exitWith()...)
158	m := run(t, prog, vmkit.NewTestHost())
159	r := m.Registers()
160	uassert.Equal(t, uint64(0xFFFFFFFF), uint64(r[7]))
161	uassert.Equal(t, uint64(7), uint64(r[8]))
162	uassert.Equal(t, uint64(minInt32), uint64(r[11]))
163	uassert.Equal(t, uint64(0), uint64(r[12]))
164}
165
166// The syscall table is the whole interface between guest and chain.
167func TestWriteSyscallReachesTheHost(t *testing.T) {
168	// Store "hi" at 0x800, then write(1, 0x800, 2).
169	prog := append([]uint32{
170		addi(5, 0, 0x7F0),
171		addi(6, 0, 'h'),
172		sType(0, 6, 5, 0x0, opStore), // sb
173		addi(6, 0, 'i'),
174		sType(1, 6, 5, 0x0, opStore),
175		addi(17, 0, sysWrite),
176		addi(10, 0, 1),
177		addi(11, 0, 0x7F0),
178		addi(12, 0, 2),
179		ecall(),
180	}, exitWith()...)
181	h := vmkit.NewTestHost()
182	run(t, prog, h)
183	uassert.Equal(t, "hi", h.OutString())
184}
185
186// A guest that exits non-zero has HALTED, not trapped: "your code said no" is
187// not "the VM broke", and a chain should not conflate them.
188func TestNonZeroExitIsStillHalted(t *testing.T) {
189	prog := []uint32{addi(10, 0, 1), addi(17, 0, sysExit), ecall()}
190	m, err := NewMachine(asm(prog), entry)
191	uassert.NoError(t, err)
192	_, status := m.Step(vmkit.NewTestHost(), vmkit.Unmetered)
193	uassert.Equal(t, "halted", status.String())
194	uassert.Equal(t, "", m.Trap())
195	uassert.Equal(t, uint64(1), uint64(m.Registers()[10]))
196}
197
198func TestUnknownSyscallTraps(t *testing.T) {
199	prog := []uint32{addi(17, 0, 777), ecall()}
200	m, err := NewMachine(asm(prog), entry)
201	uassert.NoError(t, err)
202	_, status := m.Step(vmkit.NewTestHost(), vmkit.Unmetered)
203	uassert.Equal(t, "trapped", status.String())
204	uassert.Equal(t, "unknown syscall", m.Trap())
205}
206
207func TestIllegalInstructionTraps(t *testing.T) {
208	m, err := NewMachine(asm([]uint32{0xFFFFFFFF}), entry)
209	uassert.NoError(t, err)
210	_, status := m.Step(vmkit.NewTestHost(), vmkit.Unmetered)
211	uassert.Equal(t, "trapped", status.String())
212}
213
214func TestFuelStopsTheHart(t *testing.T) {
215	prog := append([]uint32{
216		addi(5, 0, 1), addi(5, 5, 1), addi(5, 5, 1), addi(5, 5, 1),
217	}, exitWith()...)
218	m, err := NewMachine(asm(prog), entry)
219	uassert.NoError(t, err)
220	used, status := m.Step(vmkit.NewTestHost(), 2)
221	uassert.Equal(t, int64(2), used)
222	uassert.Equal(t, "running", status.String())
223	uassert.Equal(t, uint64(2), uint64(m.Registers()[5]))
224}
225
226// The property that makes a guest program a contract: slices must equal one go.
227func TestSlicedRunEqualsOneShot(t *testing.T) {
228	prog := append([]uint32{
229		addi(5, 0, 0),
230		addi(6, 0, 12),
231		add(5, 5, 6),
232		addi(6, 6, 0xFFF),
233		bne(6, 0, 0x1FF8),
234		addi(7, 0, 0x7F0),
235		sw(5, 7, 0),
236	}, exitWith()...)
237	image := asm(prog)
238
239	one, err := NewMachine(image, entry)
240	uassert.NoError(t, err)
241	one.Step(vmkit.NewTestHost(), vmkit.Unmetered)
242
243	sliced, err := NewMachine(image, entry)
244	uassert.NoError(t, err)
245	h := vmkit.NewTestHost()
246	status := vmkit.Running
247	for i := 0; i < 5000 && status == vmkit.Running; i++ {
248		fresh, err := NewMachine(image, entry)
249		uassert.NoError(t, err)
250		uassert.NoError(t, fresh.Restore(sliced.Snapshot()))
251		_, status = fresh.Step(h, 3)
252		sliced = fresh
253	}
254	uassert.Equal(t, "halted", status.String())
255	uassert.Equal(t, uint64(one.Registers()[5]), uint64(sliced.Registers()[5]))
256	uassert.Equal(t, uint64(78), uint64(sliced.Registers()[5]))
257}
258
259// A 1 MiB address space must not cost 1 MiB to pause. This is the kill
260// criterion vmkit set for continuations, measured rather than asserted.
261func TestSnapshotCarriesOnlyTouchedPages(t *testing.T) {
262	prog := append([]uint32{
263		addi(5, 0, 0x7F0),
264		addi(6, 0, 1),
265		sw(6, 5, 0),
266	}, exitWith()...)
267	m := run(t, prog, vmkit.NewTestHost())
268
269	// Two: the page holding the program at 0x1000, and the one holding the
270	// store at 0x7F0. Two out of 256, which is the whole point.
271	uassert.Equal(t, 2, m.Memory().DirtyPages())
272	// The property is proportionality: a snapshot costs what the guest
273	// touched plus a fixed header, not what the address space is. Two pages
274	// out of 256 means about 8 KiB out of a megabyte.
275	snap := m.Snapshot()
276	pages := m.Memory().DirtyPages()
277	uassert.True(t, len(snap) <= pages*PageSize+512,
278		"a snapshot must not exceed its dirty pages plus a header")
279	uassert.True(t, len(snap) < MemSize/100,
280		"a snapshot of a lightly-used hart must be a rounding error against the address space")
281}
282
283func TestRestoreRejectsJunk(t *testing.T) {
284	m, err := NewMachine(asm(exitWith()), entry)
285	uassert.NoError(t, err)
286	uassert.Error(t, m.Restore([]byte{}))
287	uassert.Error(t, m.Restore([]byte{1, 2, 3, 4, 5}))
288
289	good := m.Snapshot()
290	bad := make([]byte, len(good))
291	copy(bad, good)
292	bad[4] = 99 // version
293	uassert.ErrorIs(t, m.Restore(bad), vmkit.ErrBadSnapshot)
294}
295
296// vmkit's plumbing has to work for a second guest, which is the only way to
297// know the ABI was not shaped around the first one.
298func TestInstanceIntegration(t *testing.T) {
299	prog := append([]uint32{addi(5, 0, 21), add(5, 5, 5)}, exitWith()...)
300	image := asm(prog)
301
302	inst := vmkit.NewInstance("001", address("g1x"), VMName, image, vmkit.Unmetered)
303	h := vmkit.NewTestHost()
304	for i := 0; i < 100 && inst.Status == vmkit.Running; i++ {
305		m, err := NewMachine(image, entry)
306		uassert.NoError(t, err)
307		uassert.NoError(t, inst.Run(m, h, 2))
308	}
309	uassert.Equal(t, "halted", inst.Status.String())
310	uassert.True(t, inst.Slices > 1)
311}