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}