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}