mem.gno
4.62 Kb · 169 lines
1package riscv
2
3import "gno.land/p/moul/x/vm/vmkit/v0"
4
5// Memory is the guest's flat address space, with a snapshot that carries only
6// the pages it actually touched.
7//
8// The shape is the one the bf ladder argued for. Running memory is a flat
9// []byte because a load or a store happens on a large fraction of instructions
10// and an avl lookup per access would dominate the interpreter the way
11// vmkit.Meter.Charge did there. Snapshot cost is solved separately, by a dirty
12// bitmap: a program that writes one page pauses in one page, not in a megabyte,
13// which is the same trick that lets a bf hello world pause in 43 bytes.
14//
15// That answers the open question on the design issue ("memory size versus
16// snapshot cost") without paying for it on every instruction.
17const (
18 // PageSize is the snapshot granularity, not an MMU page: there is no
19 // address translation here, only bookkeeping about what changed.
20 PageSize = 4096
21
22 // MemSize is the address space. 1 MiB is far more than a no_std guest
23 // needs and small enough that the flat allocation is not the cost.
24 MemSize = 1 << 20
25
26 pageCount = MemSize / PageSize
27)
28
29// Memory is a flat address space plus the record of which pages were written.
30type Memory struct {
31 b []byte
32 dirty []bool
33}
34
35// NewMemory returns a zeroed address space.
36func NewMemory() *Memory {
37 return &Memory{b: make([]byte, MemSize), dirty: make([]bool, pageCount)}
38}
39
40// Size returns the address space size.
41func (m *Memory) Size() int { return len(m.b) }
42
43// DirtyPages reports how many pages the guest has written, which is what a
44// snapshot costs.
45func (m *Memory) DirtyPages() int {
46 n := 0
47 for _, d := range m.dirty {
48 if d {
49 n++
50 }
51 }
52 return n
53}
54
55// inRange reports whether [addr, addr+size) is inside the address space.
56//
57// Out of range is a trap rather than a wrap. RISC-V leaves the behaviour of an
58// access outside physical memory to the platform, and a chain has to pick the
59// one that cannot differ between nodes: refusing is deterministic, wrapping
60// invites a guest to alias two addresses and get different answers from
61// different memory sizes.
62func (m *Memory) inRange(addr uint32, size int) bool {
63 end := uint64(addr) + uint64(size)
64 return end <= uint64(len(m.b))
65}
66
67// Load8, Load16 and Load32 read little-endian, which is what RV32 is.
68func (m *Memory) Load8(addr uint32) (uint8, bool) {
69 if !m.inRange(addr, 1) {
70 return 0, false
71 }
72 return m.b[addr], true
73}
74
75func (m *Memory) Load16(addr uint32) (uint16, bool) {
76 if !m.inRange(addr, 2) {
77 return 0, false
78 }
79 return uint16(m.b[addr]) | uint16(m.b[addr+1])<<8, true
80}
81
82func (m *Memory) Load32(addr uint32) (uint32, bool) {
83 if !m.inRange(addr, 4) {
84 return 0, false
85 }
86 return uint32(m.b[addr]) | uint32(m.b[addr+1])<<8 |
87 uint32(m.b[addr+2])<<16 | uint32(m.b[addr+3])<<24, true
88}
89
90func (m *Memory) Store8(addr uint32, v uint8) bool {
91 if !m.inRange(addr, 1) {
92 return false
93 }
94 m.b[addr] = v
95 m.dirty[addr/PageSize] = true
96 return true
97}
98
99func (m *Memory) Store16(addr uint32, v uint16) bool {
100 if !m.inRange(addr, 2) {
101 return false
102 }
103 m.b[addr] = uint8(v)
104 m.b[addr+1] = uint8(v >> 8)
105 m.dirty[addr/PageSize] = true
106 m.dirty[(addr+1)/PageSize] = true
107 return true
108}
109
110func (m *Memory) Store32(addr uint32, v uint32) bool {
111 if !m.inRange(addr, 4) {
112 return false
113 }
114 m.b[addr] = uint8(v)
115 m.b[addr+1] = uint8(v >> 8)
116 m.b[addr+2] = uint8(v >> 16)
117 m.b[addr+3] = uint8(v >> 24)
118 m.dirty[addr/PageSize] = true
119 m.dirty[(addr+3)/PageSize] = true
120 return true
121}
122
123// WriteImage places a program image at addr and marks the pages it covers, so
124// a snapshot taken before the first store still carries the program.
125func (m *Memory) WriteImage(addr uint32, img []byte) bool {
126 if !m.inRange(addr, len(img)) {
127 return false
128 }
129 copy(m.b[addr:], img)
130 for i := 0; i < len(img); i++ {
131 m.dirty[(uint64(addr)+uint64(i))/PageSize] = true
132 }
133 return true
134}
135
136// Snapshot writes only the dirty pages, each prefixed by its index.
137func (m *Memory) Snapshot(w *vmkit.Writer) {
138 w.Uint32(uint32(m.DirtyPages()))
139 for i, d := range m.dirty {
140 if !d {
141 continue
142 }
143 w.Uint32(uint32(i))
144 w.Bytes(m.b[i*PageSize : (i+1)*PageSize])
145 }
146}
147
148// Restore reads a snapshot written by Snapshot into a zeroed address space.
149func (m *Memory) Restore(r *vmkit.Reader) error {
150 m.b = make([]byte, MemSize)
151 m.dirty = make([]bool, pageCount)
152 n := int(r.Uint32())
153 if n < 0 || n > pageCount {
154 return vmkit.ErrBadSnapshot
155 }
156 for i := 0; i < n; i++ {
157 idx := int(r.Uint32())
158 page := r.Bytes()
159 if err := r.Err(); err != nil {
160 return err
161 }
162 if idx < 0 || idx >= pageCount || len(page) != PageSize {
163 return vmkit.ErrBadSnapshot
164 }
165 copy(m.b[idx*PageSize:], page)
166 m.dirty[idx] = true
167 }
168 return r.Err()
169}