code wiki / _hdl_build / rv64im_min_elf_loader.nx
rv64im_min_elf_loader.nx source
↩ module page · 269 lines · 10391 B
1// rv64im_min_elf_loader.nx -- ELF64 loader for the RV64IM-min sim.
2//
3// Parses an ELF64 (little-endian, EM_RISCV) byte buffer and copies
4// every PT_LOAD segment into the simulator's mem_buf at the
5// segment's physical address. Sets sim.pc to e_entry.
6//
7// This is what lets the sim boot the actual nishi-os kernel ELF
8// (nishi-os/kernel/nishi-kernel.elf) without hand-coding instructions.
9// Follow-on commits add an end-to-end harness that:
10// 1. Reads kernel ELF bytes via sys_openat + sys_read
11// 2. Calls nx_elf_load to populate sim.mem_buf
12// 3. Runs the sim until halted (or 100k cycles, whichever first)
13// 4. Asserts uart.tx_buf contains the 10 expected [m0-*] markers
14//
15// Status: SEED. 2026-05-26. Loader-only. ELF parsing per the
16// System V ABI (ELF Specification 1.2) + RISC-V ELF psABI.
17
18import "nx_syscalls.nx"
19import "nishi_hdl_primitives.nx"
20import "rv64im_min_decoder.nx"
21import "rv64im_min_alu.nx"
22import "rv64im_min_regfile.nx"
23import "rv64im_min_csr.nx"
24import "rv64im_min_clint.nx"
25import "rv64im_min_uart.nx"
26import "rv64im_min_sim.nx"
27
28// ===== ELF magic + identification =================================================
29const NX_ELF_MAG0: i64 = 0x7f
30const NX_ELF_MAG1: i64 = 0x45 // 'E'
31const NX_ELF_MAG2: i64 = 0x4c // 'L'
32const NX_ELF_MAG3: i64 = 0x46 // 'F'
33
34const NX_ELF_CLASS_64: i64 = 2 // EI_CLASS = ELFCLASS64
35const NX_ELF_DATA_LSB: i64 = 1 // EI_DATA = ELFDATA2LSB (little-endian)
36const NX_ELF_VERSION_CURRENT: i64 = 1
37const NX_ELF_MACHINE_RISCV: i64 = 243 // EM_RISCV = 0xF3
38
39// e_type values (only ET_EXEC matters for kernel boot)
40const NX_ELF_ET_EXEC: i64 = 2
41const NX_ELF_ET_DYN: i64 = 3
42
43// Program header types
44const NX_ELF_PT_NULL: i64 = 0
45const NX_ELF_PT_LOAD: i64 = 1
46const NX_ELF_PT_DYNAMIC: i64 = 2
47const NX_ELF_PT_INTERP: i64 = 3
48const NX_ELF_PT_NOTE: i64 = 4
49const NX_ELF_PT_PHDR: i64 = 6
50const NX_ELF_PT_GNU_STACK: i64 = 0x6474e551
51
52// ELF64 header field offsets (per gabi41.pdf ยง4)
53const NX_ELF_OFF_E_IDENT: i64 = 0
54const NX_ELF_OFF_E_TYPE: i64 = 16
55const NX_ELF_OFF_E_MACHINE: i64 = 18
56const NX_ELF_OFF_E_VERSION: i64 = 20
57const NX_ELF_OFF_E_ENTRY: i64 = 24
58const NX_ELF_OFF_E_PHOFF: i64 = 32
59const NX_ELF_OFF_E_SHOFF: i64 = 40
60const NX_ELF_OFF_E_FLAGS: i64 = 48
61const NX_ELF_OFF_E_EHSIZE: i64 = 52
62const NX_ELF_OFF_E_PHENTSIZE: i64 = 54
63const NX_ELF_OFF_E_PHNUM: i64 = 56
64const NX_ELF_OFF_E_SHENTSIZE: i64 = 58
65const NX_ELF_OFF_E_SHNUM: i64 = 60
66const NX_ELF_OFF_E_SHSTRNDX: i64 = 62
67const NX_ELF_EHDR_SIZE: i64 = 64
68
69// Program header (Phdr64) field offsets + total size
70const NX_ELF_PH_OFF_P_TYPE: i64 = 0
71const NX_ELF_PH_OFF_P_FLAGS: i64 = 4
72const NX_ELF_PH_OFF_P_OFFSET: i64 = 8
73const NX_ELF_PH_OFF_P_VADDR: i64 = 16
74const NX_ELF_PH_OFF_P_PADDR: i64 = 24
75const NX_ELF_PH_OFF_P_FILESZ: i64 = 32
76const NX_ELF_PH_OFF_P_MEMSZ: i64 = 40
77const NX_ELF_PH_OFF_P_ALIGN: i64 = 48
78const NX_ELF_PHDR_SIZE: i64 = 56
79
80// ===== Verdicts =================================================
81const NX_ELF_OK: i64 = 0
82const NX_ELF_BAD_MAGIC: i64 = 1
83const NX_ELF_NOT_64BIT: i64 = 2
84const NX_ELF_NOT_LITTLE_ENDIAN: i64 = 3
85const NX_ELF_BAD_VERSION: i64 = 4
86const NX_ELF_NOT_RISCV: i64 = 5
87const NX_ELF_BAD_TYPE: i64 = 6
88const NX_ELF_TRUNCATED: i64 = 7
89const NX_ELF_BAD_PHENTSIZE: i64 = 8
90const NX_ELF_SEGMENT_OUT_OF_RANGE: i64 = 9 // segment doesn't fit in sim.mem
91const NX_ELF_FILESZ_GT_MEMSZ: i64 = 10 // malformed: filesz > memsz
92
93// ===== Little-endian byte unpacking =================================================
94//
95// Reads bytes from a buffer at offset. All ELF integers are little-
96// endian per the standard. Callers must validate offset+N <= bufsize
97// before calling (these helpers don't bounds-check; they're inner-loop).
98
99func nx_elf_read_u8(buf: *u8, off: i64) -> i64 {
100 return buf[off] as i64
101}
102
103func nx_elf_read_u16(buf: *u8, off: i64) -> i64 {
104 let b0: i64 = buf[off] as i64
105 let b1: i64 = buf[off + 1] as i64
106 return b0 | (b1 << 8)
107}
108
109func nx_elf_read_u32(buf: *u8, off: i64) -> i64 {
110 let b0: i64 = buf[off] as i64
111 let b1: i64 = buf[off + 1] as i64
112 let b2: i64 = buf[off + 2] as i64
113 let b3: i64 = buf[off + 3] as i64
114 return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)
115}
116
117func nx_elf_read_u64(buf: *u8, off: i64) -> i64 {
118 var v: i64 = 0
119 var i: i64 = 0
120 while i < 8 {
121 let b: i64 = buf[off + i] as i64
122 v = v | (b << (i * 8))
123 i = i + 1
124 }
125 return v
126}
127
128// ===== Load result =================================================
129//
130// Out-param filled by nx_elf_load on success. n_pt_load is the
131// count of PT_LOAD segments that were copied; useful for the test
132// harness's assertions.
133
134struct NxElfLoad {
135 entry: i64 // e_entry (target sim.pc)
136 n_pt_load: i64 // segments actually loaded
137 total_bytes: i64 // sum of memsz across PT_LOAD
138 valid: i64
139}
140
141// ===== Header validation =================================================
142//
143// Verifies the ELF identifier matches what the RV64IM-min sim can
144// load. Specifically: ELFCLASS64, ELFDATA2LSB, EM_RISCV, ET_EXEC.
145
146func nx_elf_validate_ident(buf: *u8, size: i64) -> i64 {
147 if size < NX_ELF_EHDR_SIZE { return 0 - NX_ELF_TRUNCATED }
148 if (buf[0] as i64) != NX_ELF_MAG0 { return 0 - NX_ELF_BAD_MAGIC }
149 if (buf[1] as i64) != NX_ELF_MAG1 { return 0 - NX_ELF_BAD_MAGIC }
150 if (buf[2] as i64) != NX_ELF_MAG2 { return 0 - NX_ELF_BAD_MAGIC }
151 if (buf[3] as i64) != NX_ELF_MAG3 { return 0 - NX_ELF_BAD_MAGIC }
152 if (buf[4] as i64) != NX_ELF_CLASS_64 { return 0 - NX_ELF_NOT_64BIT }
153 if (buf[5] as i64) != NX_ELF_DATA_LSB { return 0 - NX_ELF_NOT_LITTLE_ENDIAN }
154 if (buf[6] as i64) != NX_ELF_VERSION_CURRENT { return 0 - NX_ELF_BAD_VERSION }
155 if nx_elf_read_u16(buf, NX_ELF_OFF_E_MACHINE) != NX_ELF_MACHINE_RISCV {
156 return 0 - NX_ELF_NOT_RISCV
157 }
158 let etype: i64 = nx_elf_read_u16(buf, NX_ELF_OFF_E_TYPE)
159 if etype != NX_ELF_ET_EXEC { if etype != NX_ELF_ET_DYN { return 0 - NX_ELF_BAD_TYPE } }
160 return NX_ELF_OK
161}
162
163// ===== Segment copy =================================================
164//
165// Copies one PT_LOAD segment from the ELF byte buffer into the sim's
166// mem_buf. Validates the segment fits within sim.mem_base..sim.mem_base+mem_size.
167// Per ELF spec, filesz <= memsz; the gap (memsz - filesz) is zeroed
168// (this is the .bss path -- kernel relies on it for zero-init of
169// uninitialised statics).
170
171func nx_elf_copy_segment(buf: *u8, buf_size: i64, sim: *NxRv64imSim,
172 file_off: i64, paddr: i64, filesz: i64, memsz: i64) -> i64 {
173 if filesz > memsz { return 0 - NX_ELF_FILESZ_GT_MEMSZ }
174 if file_off + filesz > buf_size { return 0 - NX_ELF_TRUNCATED }
175
176 let mem_off: i64 = paddr - sim.mem_base
177 if mem_off < 0 { return 0 - NX_ELF_SEGMENT_OUT_OF_RANGE }
178 if mem_off + memsz > sim.mem_size { return 0 - NX_ELF_SEGMENT_OUT_OF_RANGE }
179
180 // Copy filesz bytes from ELF buffer to sim memory.
181 var i: i64 = 0
182 while i < filesz {
183 sim.mem_buf[mem_off + i] = buf[file_off + i]
184 i = i + 1
185 }
186 // Zero the BSS tail (memsz - filesz bytes).
187 while i < memsz {
188 sim.mem_buf[mem_off + i] = 0 as u8
189 i = i + 1
190 }
191 return NX_ELF_OK
192}
193
194// ===== Top-level load =================================================
195//
196// Validates header, walks program-header table, copies every PT_LOAD,
197// populates NxElfLoad, sets sim.pc to entry. Returns NX_ELF_OK or
198// negated verdict.
199
200func nx_elf_load(buf: *u8, buf_size: i64, sim: *NxRv64imSim, out: *NxElfLoad) -> i64 {
201 if (buf as i64) == 0 { return 0 - NX_HDL_BAD_KIND }
202 if (sim as i64) == 0 { return 0 - NX_HDL_BAD_KIND }
203 if (out as i64) == 0 { return 0 - NX_HDL_BAD_KIND }
204
205 let vv: i64 = nx_elf_validate_ident(buf, buf_size)
206 if vv != NX_ELF_OK { return vv }
207
208 let entry: i64 = nx_elf_read_u64(buf, NX_ELF_OFF_E_ENTRY)
209 let phoff: i64 = nx_elf_read_u64(buf, NX_ELF_OFF_E_PHOFF)
210 let phentsize: i64 = nx_elf_read_u16(buf, NX_ELF_OFF_E_PHENTSIZE)
211 let phnum: i64 = nx_elf_read_u16(buf, NX_ELF_OFF_E_PHNUM)
212
213 if phentsize != NX_ELF_PHDR_SIZE { return 0 - NX_ELF_BAD_PHENTSIZE }
214 if phoff + (phentsize * phnum) > buf_size { return 0 - NX_ELF_TRUNCATED }
215
216 var n_loaded: i64 = 0
217 var total_mem: i64 = 0
218 var i: i64 = 0
219 while i < phnum {
220 let ph_off: i64 = phoff + (i * phentsize)
221 let p_type: i64 = nx_elf_read_u32(buf, ph_off + NX_ELF_PH_OFF_P_TYPE)
222 if p_type == NX_ELF_PT_LOAD {
223 let p_offset: i64 = nx_elf_read_u64(buf, ph_off + NX_ELF_PH_OFF_P_OFFSET)
224 let p_paddr: i64 = nx_elf_read_u64(buf, ph_off + NX_ELF_PH_OFF_P_PADDR)
225 let p_filesz: i64 = nx_elf_read_u64(buf, ph_off + NX_ELF_PH_OFF_P_FILESZ)
226 let p_memsz: i64 = nx_elf_read_u64(buf, ph_off + NX_ELF_PH_OFF_P_MEMSZ)
227 let rc: i64 = nx_elf_copy_segment(buf, buf_size, sim,
228 p_offset, p_paddr, p_filesz, p_memsz)
229 if rc != NX_ELF_OK { return rc }
230 n_loaded = n_loaded + 1
231 total_mem = total_mem + p_memsz
232 }
233 // Other p_types (NOTE, GNU_STACK, DYNAMIC, etc.) are
234 // metadata; we don't load them. Per Linux kernel + nishi-
235 // os pattern: a bare-metal kernel has 1 PT_LOAD covering
236 // .text + .rodata + .data + .bss (boot.S layout); user
237 // programs may have 2-3 PT_LOADs.
238 i = i + 1
239 }
240
241 out.entry = entry
242 out.n_pt_load = n_loaded
243 out.total_bytes = total_mem
244 out.valid = 1
245
246 // Set sim PC to entry point. Kernel: 0x80000000 per
247 // rv64im_min_target_spec.md.
248 sim.pc = entry
249
250 return NX_ELF_OK
251}
252
253// ===== Diagnostic accessors =================================================
254//
255// Read-only inspection of an ELF header for the test harness +
256// diagnostic prints. Skips full validation; callers must have
257// called nx_elf_validate_ident first.
258
259func nx_elf_entry(buf: *u8) -> i64 {
260 return nx_elf_read_u64(buf, NX_ELF_OFF_E_ENTRY)
261}
262
263func nx_elf_phnum(buf: *u8) -> i64 {
264 return nx_elf_read_u16(buf, NX_ELF_OFF_E_PHNUM)
265}
266
267func nx_elf_e_type(buf: *u8) -> i64 {
268 return nx_elf_read_u16(buf, NX_ELF_OFF_E_TYPE)
269}