nx_loader.nx source
↩ module page · 209 lines · 7327 B
1// nx_loader.nx -- ELF segment loader for nx_rv64_sim.
2//
3// Closes the loop: nx_elf_read parses, nx_loader copies PT_LOAD
4// segments into a memory buffer, nx_rv64_sim executes from there.
5//
6// With these three modules we can run an RV64 ELF entirely in
7// NishiLang -- no qemu, no docker, no Linux process boundary.
8// This is THE off-C bootstrap-verification capability.
9//
10// What we do:
11//
12// 1. Parse the ELF header.
13// 2. For each program header:
14// * If type = PT_LOAD, copy `p_filesz` bytes from
15// ELF[p_offset..p_offset+p_filesz) to mem[p_vaddr...]
16// * Then zero `p_memsz - p_filesz` bytes (BSS region).
17// 3. Return the entry point so the simulator can set PC.
18//
19// Memory model: we treat `mem_base[0..mem_size)` as a flat region
20// where addresses are file-vaddrs. If p_vaddr is 0x10000, then
21// mem_base[0x10000] is where the segment lands. The caller
22// allocates a mem buffer big enough to hold all loaded segments;
23// today that means `>= max_vaddr + max_segment_size`.
24//
25// PT_LOAD is the only program-header type we handle; PT_PHDR /
26// PT_INTERP / etc. are silently skipped (irrelevant for static
27// RV64 ELFs from elf_writer).
28
29// nx_safety_envelope:
30// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
31// sil_target: SIL1
32// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
33// verdict: NOT_YET_EVALUATED
34
35import "syscalls.nx"
36import "nx_elf_read.nx"
37
38// ---- program header (decoded) -----------------------------------
39
40struct NxElfPhdr {
41 p_type: i64,
42 p_flags: i64,
43 p_offset: i64,
44 p_vaddr: i64,
45 p_paddr: i64,
46 p_filesz: i64,
47 p_memsz: i64,
48 p_align: i64,
49}
50
51const NX_ELF_PHDR_BYTES: i64 = 64
52
53const NX_PT_NULL: i64 = 0
54const NX_PT_LOAD: i64 = 1
55const NX_PT_DYNAMIC: i64 = 2
56const NX_PT_INTERP: i64 = 3
57const NX_PT_PHDR: i64 = 6
58
59// ---- decode a single Elf64_Phdr at file offset `off` --------------
60
61func nx_loader_read_phdr(buf: *u8, off: i64) -> *NxElfPhdr {
62 let raw: *u8 = sys_mmap(NX_ELF_PHDR_BYTES)
63 let p: *NxElfPhdr = raw as *NxElfPhdr
64 p.p_type = nx_elf_get_u32_le(buf, off + 0)
65 p.p_flags = nx_elf_get_u32_le(buf, off + 4)
66 p.p_offset = nx_elf_get_u64_le(buf, off + 8)
67 p.p_vaddr = nx_elf_get_u64_le(buf, off + 16)
68 p.p_paddr = nx_elf_get_u64_le(buf, off + 24)
69 p.p_filesz = nx_elf_get_u64_le(buf, off + 32)
70 p.p_memsz = nx_elf_get_u64_le(buf, off + 40)
71 p.p_align = nx_elf_get_u64_le(buf, off + 48)
72 return p
73}
74
75// ---- byte-copy helper --------------------------------------------
76
77func nx_loader_memcpy(dst: *u8, src: *u8, n: i64) -> i64 {
78 var i: i64 = 0
79 while i < n {
80 dst[i] = src[i]
81 i = i + 1
82 }
83 return 0
84}
85
86func nx_loader_memzero(dst: *u8, n: i64) -> i64 {
87 var i: i64 = 0
88 while i < n {
89 dst[i] = 0
90 i = i + 1
91 }
92 return 0
93}
94
95// ---- top-level loader --------------------------------------------
96//
97// Loads `elf_buf` (length `elf_len`) into `mem_base` (size
98// `mem_size`). Returns the entry-point vaddr, or -1 on error.
99// Bounds-checks every segment copy to avoid overrunning mem.
100
101func nx_loader_load(elf_buf: *u8, elf_len: i64,
102 mem_base: *u8, mem_size: i64) -> i64 {
103 let h: *NxElfHeader = nx_elf_parse_header(elf_buf, elf_len)
104 if h.valid != 1 { return -1 }
105 if h.is_64bit != 1 { return -1 }
106 if h.is_little != 1 { return -1 }
107 if h.e_machine != 0xF3 { return -1 } // EM_RISCV
108
109 var i: i64 = 0
110 while i < h.e_phnum {
111 let phoff: i64 = h.e_phoff + i * h.e_phentsize
112 let p: *NxElfPhdr = nx_loader_read_phdr(elf_buf, phoff)
113 if p.p_type == NX_PT_LOAD {
114 // Bounds: p_vaddr + p_memsz must fit inside mem.
115 if p.p_vaddr + p.p_memsz > mem_size { return -1 }
116 // Bounds: p_offset + p_filesz must fit inside elf_buf.
117 if p.p_offset + p.p_filesz > elf_len { return -1 }
118 // Copy file bytes.
119 let dst_addr: i64 = (mem_base as i64) + p.p_vaddr
120 let src_addr: i64 = (elf_buf as i64) + p.p_offset
121 let dst: *u8 = dst_addr as *u8
122 let src: *u8 = src_addr as *u8
123 nx_loader_memcpy(dst, src, p.p_filesz)
124 // Zero the BSS tail (memsz > filesz).
125 if p.p_memsz > p.p_filesz {
126 let bss_addr: i64 = dst_addr + p.p_filesz
127 nx_loader_memzero(bss_addr as *u8, p.p_memsz - p.p_filesz)
128 }
129 }
130 i = i + 1
131 }
132
133 return h.e_entry
134}
135
136// ---- self-test: build a tiny ELF in memory + load it -------------
137
138func main() -> i64 {
139 // Construct a minimal ELF in memory:
140 // header (64) + 1 program header (56) + 12 bytes of code
141 // total = 132 bytes; vaddr = 0x10000; entry = 0x10000 + 120
142 let elf: *u8 = sys_mmap(256)
143 var i: i64 = 0
144 while i < 256 { elf[i] = 0; i = i + 1 }
145
146 // ELF header
147 elf[0] = 0x7F; elf[1] = 0x45; elf[2] = 0x4C; elf[3] = 0x46
148 elf[4] = 2 // ELF64
149 elf[5] = 1 // little-endian
150 elf[6] = 1 // version
151 elf[16] = 2 // ET_EXEC
152 elf[18] = 0xF3 // EM_RISCV
153 // e_entry = 0x10078 (vaddr 0x10000 + 120 byte offset)
154 elf[24] = 0x78; elf[25] = 0x00; elf[26] = 0x01; elf[27] = 0x00
155 // e_phoff = 64
156 elf[32] = 64
157 // e_ehsize = 64, phentsize = 56, phnum = 1
158 elf[52] = 64
159 elf[54] = 56
160 elf[56] = 1
161
162 // Program header at offset 64.
163 // p_type = PT_LOAD = 1
164 elf[64] = 1
165 // p_flags = R|X = 5
166 elf[68] = 5
167 // p_offset = 120 (where code begins)
168 elf[72] = 120
169 // p_vaddr = 0x10000
170 elf[80] = 0x00; elf[81] = 0x00; elf[82] = 0x01; elf[83] = 0x00
171 // p_filesz = p_memsz = 12
172 elf[96] = 12
173 elf[104] = 12
174
175 // Code bytes at offset 120.
176 elf[120] = 0x13; elf[121] = 0x05; elf[122] = 0xA0; elf[123] = 0x02
177 elf[124] = 0x93; elf[125] = 0x08; elf[126] = 0xD0; elf[127] = 0x05
178 elf[128] = 0x73; elf[129] = 0x00; elf[130] = 0x00; elf[131] = 0x00
179
180 // Now load into a 1 MB memory buffer.
181 let mem: *u8 = sys_mmap(0x20000)
182 let entry: i64 = nx_loader_load(elf, 132, mem, 0x20000)
183
184 // Entry must be 0x10078.
185 if entry != 0x10078 { return __syscall(93, 10, 0, 0, 0, 0, 0) }
186
187 // The 12 code bytes must have landed at mem[0x10000..0x1000C).
188 if mem[0x10000] != 0x13 { return __syscall(93, 20, 0, 0, 0, 0, 0) }
189 if mem[0x10001] != 0x05 { return __syscall(93, 21, 0, 0, 0, 0, 0) }
190 if mem[0x10003] != 0x02 { return __syscall(93, 22, 0, 0, 0, 0, 0) }
191 if mem[0x10008] != 0x73 { return __syscall(93, 23, 0, 0, 0, 0, 0) }
192
193 // Bytes outside the loaded segment should be zero (mmap default).
194 if mem[0x10010] != 0 { return __syscall(93, 30, 0, 0, 0, 0, 0) }
195
196 // Bad ELF: not a RISC-V machine -> reject.
197 elf[18] = 0x3E // EM_X86_64
198 let bad: i64 = nx_loader_load(elf, 132, mem, 0x20000)
199 if bad != -1 { return __syscall(93, 40, 0, 0, 0, 0, 0) }
200 elf[18] = 0xF3 // restore
201
202 // Out-of-bounds segment -> reject (vaddr way past mem_size).
203 elf[80] = 0x00; elf[81] = 0x00; elf[82] = 0x00; elf[83] = 0x00
204 elf[84] = 0x00; elf[85] = 0x00; elf[86] = 0x00; elf[87] = 0x80
205 let oob: i64 = nx_loader_load(elf, 132, mem, 0x20000)
206 if oob != -1 { return __syscall(93, 50, 0, 0, 0, 0, 0) }
207
208 return 0
209}