code wiki / _hdl_build / nx_nishios_kernel_emit.nx
nx_nishios_kernel_emit.nx source
↩ module page · 679 lines · 32051 B
1// nx_nishios_kernel_emit.nx -- THE COMPOSED NishiOS KERNEL IMAGE (closes the boot adoption gap).
2//
3// THE DEFECT THIS CLOSES: the kernel lane had 26 gate-proven stage binaries (strap / paging /
4// trap_syscall / timer_irq / coopsched / virtio ...), each a SEPARATE tiny image run in isolation
5// -- while the image the pinned BOOTSOV runner actually boots (_boot_nishi_virt.bin) was a
6// 72-byte, 17-step BANNER that exercised NONE of them. Capability proven in a gate but not wired
7// at the live chokepoint IS the baseline. This emitter authors ONE image where the stages FALL
8// THROUGH into a single real boot, so what boots is what was proven.
9//
10// BOOT SEQUENCE (each phase leaves a serial marker; only the last halts):
11// 1 RESET (M) UART up, mtvec + stvec + medeleg installed -> "NISHI "
12// 2 TRAP (M) ecall from M -> kernel trap dispatcher -> mret -> "TRAP "
13// 3 SCHED (M) two tasks, ecall(SYS_YIELD), round-robin mepc swap -> "ABABABABABAB SCHED "
14// 4 TIMER (M) CLINT mtimecmp armed, MIE+MTIE, MTIP preempts the spin -> "TICK "
15// 5 PAGING (S) mret to S-mode, Sv39 root PTEs laid, satp on, VA load -> "PAGE "
16// 6 USER (U) sret to U-mode on a U-bit gigapage, ecall delegated by
17// medeleg to the S-mode handler, sret back, exit syscall -> "USER OK"
18//
19// A real dispatcher: it decodes mcause (interrupt vs exception, ecall-from-M/S/U) and a7 as the
20// syscall number -- not a per-phase hand-installed stub. Failure of any phase prints its own
21// distinct marker (TX / PGX / UX) instead of falling silent, so the gate can never pass vacuously.
22// nx_nishios_kernel_emit [outpath] -> the flat rv64 image + .gold expected transcript
23// Sovereign: syscalls only, no gcc/.sh. license_tier: ORIGINAL
24import "nx_syscalls.nx"
25const NK_MAGIC_16384: i64 = 16384
26
27const NK_OUT: *u8 = "runtime/_hdl_build/_boot_nishi_virt.bin"
28const NK_GOLD: *u8 = "runtime/_hdl_build/_boot_nishi_virt.bin.gold"
29const NK_LOG: *u8 = "knowledge/status/boot_stub.log"
30
31const NK_UART: i64 = 0x10000000
32const NK_FIN: i64 = 0x100000
33const NK_PASS: i64 = 0x5555
34const NK_MEM_BASE: i64 = 0x80000000
35const NK_MTIMECMP: i64 = 0x02004000
36const NK_MTIME: i64 = 0x0200BFF8
37
38const NK_CSR_SSTATUS: i64 = 0x100
39const NK_CSR_STVEC: i64 = 0x105
40const NK_CSR_SEPC: i64 = 0x141
41const NK_CSR_SATP: i64 = 0x180
42const NK_CSR_MSTATUS: i64 = 0x300
43const NK_CSR_MEDELEG: i64 = 0x302
44const NK_CSR_MIE: i64 = 0x304
45const NK_CSR_MTVEC: i64 = 0x305
46const NK_CSR_MEPC: i64 = 0x341
47const NK_CSR_MCAUSE: i64 = 0x342
48
49const NK_MRET: i64 = 0x30200073
50const NK_SRET: i64 = 0x10200073
51const NK_ECALL: i64 = 0x00000073
52
53const NK_MPP_S: i64 = 0x800
54const NK_SPP_BIT: i64 = 0x100
55const NK_MIE_MTIE: i64 = 0x80
56const NK_MSTATUS_MIE: i64 = 0x08
57const NK_TICK: i64 = 0x40
58const NK_MEDELEG_U: i64 = 0x100
59
60// Sv39: root table inside guest RAM (64KB image window), gigapage identity leaves.
61const NK_PGROOT: i64 = 0x80008000
62const NK_PGPPN: i64 = 0x80008
63const NK_PTE_KERN: i64 = 0x2000000F
64const NK_PTE_DATA: i64 = 0x20000007
65const NK_PTE_USER: i64 = 0x2000001F
66const NK_SENT_PA: i64 = 0x80009000
67const NK_SENT_VAL: i64 = 0x5ECA1234
68const NK_DATA_VA: i64 = 0xC0009000
69const NK_USER_VA: i64 = 0x40000000
70const NK_SV39: i64 = 8
71
72const NK_SYS_YIELD: i64 = 1
73const NK_SYS_EXIT: i64 = 2
74const NK_YIELDS: i64 = 12
75
76const RV_X0: i64 = 0
77const RV_T0: i64 = 5
78const RV_T1: i64 = 6
79const RV_T2: i64 = 7
80const RV_S1: i64 = 9
81const RV_A7: i64 = 17
82const RV_T3: i64 = 28
83const RV_T4: i64 = 29
84const RV_T5: i64 = 30
85const RV_T6: i64 = 31
86
87// section slots (two-pass offset resolution)
88const NK_S_MHANDLER: i64 = 0
89const NK_S_SYSCALL: i64 = 1
90const NK_S_DOYIELD: i64 = 2
91const NK_S_TIMERISR: i64 = 3
92const NK_S_TASKA: i64 = 4
93const NK_S_TASKB: i64 = 5
94const NK_S_AFTERSCHED:i64 = 6
95const NK_S_AFTERTIMER:i64 = 7
96const NK_S_SPHASE: i64 = 8
97const NK_S_SHANDLER: i64 = 9
98const NK_S_USERCODE: i64 = 10
99const NK_S_EXITOK: i64 = 11
100const NK_S_HALT: i64 = 12
101const NK_S_PGFAIL: i64 = 13
102const NK_S_TRAPBAD: i64 = 14
103const NK_S_TASKP: i64 = 15
104const NK_S_TASKQ: i64 = 16
105const NK_S_AFTERPRE: i64 = 17
106const NK_S_ONESHOT: i64 = 18
107const NK_S_PREDONE: i64 = 19
108const NK_S_PRINTQ: i64 = 20
109const NK_S_SWAP: i64 = 21
110const NK_S_BLKFAIL: i64 = 22
111const NK_S_AFTERBLK: i64 = 23
112const NK_S_NETFAIL: i64 = 24
113const NK_S_AFTERNET: i64 = 25
114const NK_S_HEAPFAIL: i64 = 26
115const NK_S_AFTERHEAP: i64 = 27
116const NK_S_N: i64 = 28
117
118// Kernel heap: the bump pointer lives in its own cell, the arena starts just above it. Placed
119// ABOVE the virtio ring pages (0xA000 blk / 0xC000 net) so nothing the devices DMA into can
120// overlap the arena -- a heap that shares pages with device DMA is not a heap, it is a race.
121const NK_HEAP_CELL: i64 = 0x8000D000
122const NK_HEAP_BASE: i64 = 0x8000D010
123const NK_HEAP_MAGIC: i64 = 0x5ECAFE01
124const NK_HEAP_CHUNK: i64 = 16
125
126// virtio-MMIO (legacy) register window of the blk device the emulator models.
127const NK_VIO_BASE: i64 = 0x10001000
128const NK_VIO_MAGIC_VAL: i64 = 0x74726976
129const NK_VIO_BLK_ID: i64 = 2
130const NK_VIO_OFF_MAGIC: i64 = 0x000
131const NK_VIO_OFF_DEVICEID: i64 = 0x008
132const NK_VIO_OFF_GUESTFEAT:i64 = 0x020
133const NK_VIO_OFF_QSEL: i64 = 0x030
134const NK_VIO_OFF_QNUMMAX: i64 = 0x034
135const NK_VIO_OFF_QNUM: i64 = 0x038
136const NK_VIO_OFF_QPFN: i64 = 0x040
137const NK_VIO_OFF_STATUS: i64 = 0x070
138const NK_VIO_QNUM: i64 = 8
139const NK_VIO_RING_PFN: i64 = 0x8000A
140const NK_VIO_ST_ACK: i64 = 1
141const NK_VIO_ST_ACKDRV: i64 = 3
142const NK_VIO_ST_FEATOK: i64 = 11
143const NK_VIO_ST_DRVOK: i64 = 15
144const NK_VIO_FEAT: i64 = 0x20
145// the SECOND legacy virtio-MMIO transport: virtio-net, DeviceID=1, its own 256-byte window
146const NK_VIO_NET_BASE: i64 = 0x10002000
147const NK_VIO_NET_ID: i64 = 1
148const NK_VIO_NET_RING_PFN: i64 = 0x8000C
149// The image is loaded at mem[0] and the Sv39 root table sits at guest offset 0x8000, with the
150// virtio ring/DMA pages above it. Nothing structurally prevents emitted CODE from growing into
151// them -- it would just silently scribble on the page table and the failure would look like a
152// paging bug rather than an overflow. This is the ceiling, enforced fail-closed at emit time.
153const NK_CODE_LIMIT: i64 = 0x8000
154
155const RV_S2: i64 = 18
156const RV_S3: i64 = 19
157const NK_PREEMPTS: i64 = 6
158const NK_CH_P: i64 = 80
159const NK_CH_Q: i64 = 81
160
161func nk_lui(rd: i64, imm20: i64) -> i64 { return ((imm20 & 0xFFFFF) << 12) | (rd << 7) | 0x37 }
162func nk_addi(rd: i64, rs1: i64, imm: i64) -> i64 { return ((imm & 0xFFF) << 20) | (rs1 << 15) | (rd << 7) | 0x13 }
163func nk_load(rd: i64, rs1: i64, f3: i64, imm: i64) -> i64 { return ((imm & 0xFFF) << 20) | (rs1 << 15) | (f3 << 12) | (rd << 7) | 0x03 }
164func nk_store(rs2: i64, rs1: i64, f3: i64, imm: i64) -> i64 {
165 let hi: i64 = ((imm >> 5) & 0x7f) << 25
166 let lo: i64 = (imm & 0x1f) << 7
167 return hi | (rs2 << 20) | (rs1 << 15) | (f3 << 12) | lo | 0x23
168}
169func nk_branch(rs1: i64, rs2: i64, f3: i64, imm: i64) -> i64 {
170 let b12: i64 = ((imm >> 12) & 0x1) << 31
171 let b11: i64 = ((imm >> 11) & 0x1) << 7
172 let b10_5: i64 = ((imm >> 5) & 0x3f) << 25
173 let b4_1: i64 = ((imm >> 1) & 0xf) << 8
174 return b12 | b10_5 | (rs2 << 20) | (rs1 << 15) | (f3 << 12) | b4_1 | b11 | 0x63
175}
176func nk_jal(rd: i64, imm: i64) -> i64 {
177 let b20: i64 = ((imm >> 20) & 0x1) << 31
178 let b19_12: i64 = ((imm >> 12) & 0xff) << 12
179 let b11: i64 = ((imm >> 11) & 0x1) << 20
180 let b10_1: i64 = ((imm >> 1) & 0x3ff) << 21
181 return b20 | b10_1 | b11 | b19_12 | (rd << 7) | 0x6f
182}
183func nk_slli(rd: i64, rs1: i64, shamt: i64) -> i64 { return ((shamt & 0x3f) << 20) | (rs1 << 15) | (1 << 12) | (rd << 7) | 0x13 }
184func nk_srli(rd: i64, rs1: i64, shamt: i64) -> i64 { return ((shamt & 0x3f) << 20) | (rs1 << 15) | (5 << 12) | (rd << 7) | 0x13 }
185func nk_or(rd: i64, rs1: i64, rs2: i64) -> i64 { return (rs2 << 20) | (rs1 << 15) | (6 << 12) | (rd << 7) | 0x33 }
186func nk_csrrw(rd: i64, csr: i64, rs1: i64) -> i64 { return ((csr & 0xfff) << 20) | (rs1 << 15) | (1 << 12) | (rd << 7) | 0x73 }
187func nk_csrrs(rd: i64, csr: i64, rs1: i64) -> i64 { return ((csr & 0xfff) << 20) | (rs1 << 15) | (2 << 12) | (rd << 7) | 0x73 }
188func nk_csrrc(rd: i64, csr: i64, rs1: i64) -> i64 { return ((csr & 0xfff) << 20) | (rs1 << 15) | (3 << 12) | (rd << 7) | 0x73 }
189
190func nk_w32(buf: *u8, off: i64, w: i64) -> i64 {
191 buf[off]=(w&0xff) as u8; buf[off+1]=((w>>8)&0xff) as u8; buf[off+2]=((w>>16)&0xff) as u8; buf[off+3]=((w>>24)&0xff) as u8
192 return off + 4
193}
194// li a 32-bit constant (2 words, fixed size -- keeps the two-pass layout exact)
195func nk_li32(buf: *u8, off: i64, rd: i64, val: i64) -> i64 {
196 var hi: i64 = (val >> 12) & 0xFFFFF
197 var lo: i64 = val & 0xFFF
198 if lo >= 0x800 { lo = lo - 0x1000; hi = (hi + 1) & 0xFFFFF }
199 var o: i64 = nk_w32(buf, off, nk_lui(rd, hi))
200 o = nk_w32(buf, o, nk_addi(rd, rd, lo))
201 return o
202}
203// li a zero-extended 32-bit address (4 words, fixed size)
204func nk_li32u(buf: *u8, off: i64, rd: i64, val: i64) -> i64 {
205 var o: i64 = nk_li32(buf, off, rd, val)
206 o = nk_w32(buf, o, nk_slli(rd, rd, 32))
207 o = nk_w32(buf, o, nk_srli(rd, rd, 32))
208 return o
209}
210// emit "print this NUL-terminated string over the UART" (2 words per char)
211func nk_str(buf: *u8, off: i64, s: *u8) -> i64 {
212 var o: i64 = off
213 var i: i64 = 0
214 while s[i] != (0 as u8) {
215 o = nk_w32(buf, o, nk_addi(RV_T1, RV_X0, s[i] as i64))
216 o = nk_w32(buf, o, nk_store(RV_T1, RV_T0, 0, 0))
217 i = i + 1
218 }
219 return o
220}
221
222func nk_p(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 }
223func nk_fp(fd: i64, s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(fd,s,n); return 0 }
224func nk_fn(fd: i64, v: i64) -> i64 { let bb: *u8=sys_mmap(28); var m: i64=v; if m<0{m=0-m}; let t: *u8=sys_mmap(28); var k: i64=0; if m==0{t[0]=48;k=1}; while m>0{t[k]=(48+(m%10)) as u8;m=m/10;k=k+1}; var i: i64=0; while i<k{bb[i]=t[k-1-i];i=i+1}; sys_write(fd,bb,k); return 0 }
225
226// Emit a full legacy virtio-MMIO device bring-up at `base`: discovery (magic + device id +
227// queue-num-max), status negotiation ACK->DRIVER->FEATURES_OK->DRIVER_OK, and queue configuration
228// -- with EVERY step confirmed by a READBACK, branching to `failoff` on any mismatch. One
229// implementation drives both the blk and net devices: two hand-written copies of a negotiation
230// sequence is two chances for them to drift, and the second device is the reason to factor it.
231func nk_devup(buf: *u8, off: i64, base: i64, devid: i64, pfn: i64, failoff: i64) -> i64 {
232 var o: i64 = off
233 o = nk_li32u(buf, o, RV_T5, base)
234 o = nk_w32(buf, o, nk_load(RV_T3, RV_T5, 2, NK_VIO_OFF_MAGIC))
235 o = nk_li32(buf, o, RV_T4, NK_VIO_MAGIC_VAL)
236 let b1: i64 = o
237 o = nk_w32(buf, o, nk_branch(RV_T3, RV_T4, 1, failoff - b1))
238 o = nk_w32(buf, o, nk_load(RV_T3, RV_T5, 2, NK_VIO_OFF_DEVICEID))
239 o = nk_w32(buf, o, nk_addi(RV_T4, RV_X0, devid))
240 let b2: i64 = o
241 o = nk_w32(buf, o, nk_branch(RV_T3, RV_T4, 1, failoff - b2))
242 o = nk_w32(buf, o, nk_load(RV_T3, RV_T5, 2, NK_VIO_OFF_QNUMMAX))
243 o = nk_w32(buf, o, nk_addi(RV_T4, RV_X0, NK_VIO_QNUM))
244 let b3: i64 = o
245 o = nk_w32(buf, o, nk_branch(RV_T3, RV_T4, 1, failoff - b3))
246 o = nk_w32(buf, o, nk_addi(RV_T1, RV_X0, NK_VIO_ST_ACK))
247 o = nk_w32(buf, o, nk_store(RV_T1, RV_T5, 2, NK_VIO_OFF_STATUS))
248 o = nk_w32(buf, o, nk_addi(RV_T1, RV_X0, NK_VIO_ST_ACKDRV))
249 o = nk_w32(buf, o, nk_store(RV_T1, RV_T5, 2, NK_VIO_OFF_STATUS))
250 o = nk_w32(buf, o, nk_addi(RV_T1, RV_X0, NK_VIO_FEAT))
251 o = nk_w32(buf, o, nk_store(RV_T1, RV_T5, 2, NK_VIO_OFF_GUESTFEAT))
252 o = nk_w32(buf, o, nk_addi(RV_T1, RV_X0, NK_VIO_ST_FEATOK))
253 o = nk_w32(buf, o, nk_store(RV_T1, RV_T5, 2, NK_VIO_OFF_STATUS))
254 o = nk_w32(buf, o, nk_addi(RV_T1, RV_X0, 0))
255 o = nk_w32(buf, o, nk_store(RV_T1, RV_T5, 2, NK_VIO_OFF_QSEL))
256 o = nk_w32(buf, o, nk_addi(RV_T1, RV_X0, NK_VIO_QNUM))
257 o = nk_w32(buf, o, nk_store(RV_T1, RV_T5, 2, NK_VIO_OFF_QNUM))
258 o = nk_li32(buf, o, RV_T1, pfn)
259 o = nk_w32(buf, o, nk_store(RV_T1, RV_T5, 2, NK_VIO_OFF_QPFN))
260 o = nk_w32(buf, o, nk_load(RV_T3, RV_T5, 2, NK_VIO_OFF_QPFN))
261 o = nk_li32(buf, o, RV_T4, pfn)
262 let b4: i64 = o
263 o = nk_w32(buf, o, nk_branch(RV_T3, RV_T4, 1, failoff - b4))
264 o = nk_w32(buf, o, nk_addi(RV_T1, RV_X0, NK_VIO_ST_DRVOK))
265 o = nk_w32(buf, o, nk_store(RV_T1, RV_T5, 2, NK_VIO_OFF_STATUS))
266 o = nk_w32(buf, o, nk_load(RV_T3, RV_T5, 2, NK_VIO_OFF_STATUS))
267 o = nk_w32(buf, o, nk_addi(RV_T4, RV_X0, NK_VIO_ST_DRVOK))
268 let b5: i64 = o
269 o = nk_w32(buf, o, nk_branch(RV_T3, RV_T4, 1, failoff - b5))
270 return o
271}
272
273// Author the whole kernel image. `tgt` holds resolved section offsets (all 0 on the measuring
274// pass); `pos` receives the section offsets as emitted. Instruction sizes never depend on the
275// operand values, so the measuring pass fixes the layout exactly.
276func nk_emit_image(buf: *u8, tgt: *i64, pos: *i64, medeleg: i64, data_pte: i64, vio_base: i64, heap_cell: i64) -> i64 {
277 var o: i64 = 0
278
279 // ---- 1 RESET (M-mode): UART, trap vectors, delegation, banner ----
280 o = nk_w32(buf, o, nk_lui(RV_T0, NK_UART >> 12))
281 o = nk_li32u(buf, o, RV_T1, NK_MEM_BASE + tgt[NK_S_MHANDLER])
282 o = nk_w32(buf, o, nk_csrrw(RV_X0, NK_CSR_MTVEC, RV_T1))
283 o = nk_li32u(buf, o, RV_T1, NK_MEM_BASE + tgt[NK_S_SHANDLER])
284 o = nk_w32(buf, o, nk_csrrw(RV_X0, NK_CSR_STVEC, RV_T1))
285 o = nk_li32(buf, o, RV_T1, medeleg)
286 o = nk_w32(buf, o, nk_csrrw(RV_X0, NK_CSR_MEDELEG, RV_T1))
287 o = nk_str(buf, o, "NISHI " as *u8)
288
289 // ---- 2 TRAP: a real ecall from M through the dispatcher and back ----
290 o = nk_w32(buf, o, nk_addi(RV_A7, RV_X0, 0))
291 o = nk_w32(buf, o, NK_ECALL)
292 o = nk_str(buf, o, "TRAP " as *u8)
293
294 // ---- 3 SCHED: two tasks round-robined by the SYS_YIELD syscall ----
295 o = nk_w32(buf, o, nk_addi(RV_S1, RV_X0, 0))
296 o = nk_li32u(buf, o, RV_T6, NK_MEM_BASE + tgt[NK_S_TASKB])
297 o = nk_w32(buf, o, nk_jal(RV_X0, tgt[NK_S_TASKA] - o))
298
299 pos[NK_S_TASKA] = o
300 o = nk_str(buf, o, "A" as *u8)
301 o = nk_w32(buf, o, nk_addi(RV_A7, RV_X0, NK_SYS_YIELD))
302 o = nk_w32(buf, o, NK_ECALL)
303 o = nk_w32(buf, o, nk_jal(RV_X0, tgt[NK_S_TASKA] - o))
304
305 pos[NK_S_TASKB] = o
306 o = nk_str(buf, o, "B" as *u8)
307 o = nk_w32(buf, o, nk_addi(RV_A7, RV_X0, NK_SYS_YIELD))
308 o = nk_w32(buf, o, NK_ECALL)
309 o = nk_w32(buf, o, nk_jal(RV_X0, tgt[NK_S_TASKB] - o))
310
311 pos[NK_S_AFTERSCHED] = o
312 o = nk_str(buf, o, " SCHED " as *u8)
313
314 // ---- 4 TIMER: arm CLINT mtimecmp, enable MIE+MTIE, spin until MTIP preempts ----
315 o = nk_w32(buf, o, nk_addi(RV_S2, RV_X0, 0))
316 o = nk_li32u(buf, o, RV_T2, NK_MTIME)
317 o = nk_w32(buf, o, nk_load(RV_T1, RV_T2, 3, 0))
318 o = nk_w32(buf, o, nk_addi(RV_T1, RV_T1, NK_TICK))
319 o = nk_li32u(buf, o, RV_T2, NK_MTIMECMP)
320 o = nk_w32(buf, o, nk_store(RV_T1, RV_T2, 3, 0))
321 o = nk_w32(buf, o, nk_addi(RV_T1, RV_X0, NK_MIE_MTIE))
322 o = nk_w32(buf, o, nk_csrrs(RV_X0, NK_CSR_MIE, RV_T1))
323 o = nk_w32(buf, o, nk_addi(RV_T1, RV_X0, NK_MSTATUS_MIE))
324 o = nk_w32(buf, o, nk_csrrs(RV_X0, NK_CSR_MSTATUS, RV_T1))
325 o = nk_w32(buf, o, nk_jal(RV_X0, 0))
326
327 // ---- 5 PAGING: drop to S-mode, then build the Sv39 map ----
328 pos[NK_S_AFTERTIMER] = o
329 o = nk_str(buf, o, "TICK " as *u8)
330
331 // ---- 4b PREEMPT: two tasks that NEVER yield, switched INVOLUNTARILY by the timer.
332 // The cooperative phase above proves a scheduler the tasks cooperate with; this proves the
333 // kernel can take the CPU back from code that does not cooperate -- the actual difference
334 // between a toy and a kernel. The ISR decodes the interrupted mepc to name which task was
335 // running, so the transcript "PQPQPQ" is EVIDENCE of alternation, not just of ticking.
336 o = nk_w32(buf, o, nk_addi(RV_S2, RV_X0, 1))
337 o = nk_w32(buf, o, nk_addi(RV_S3, RV_X0, 0))
338 o = nk_li32u(buf, o, RV_T6, NK_MEM_BASE + tgt[NK_S_TASKQ])
339 o = nk_li32u(buf, o, RV_T2, NK_MTIME)
340 o = nk_w32(buf, o, nk_load(RV_T1, RV_T2, 3, 0))
341 o = nk_w32(buf, o, nk_addi(RV_T1, RV_T1, NK_TICK))
342 o = nk_li32u(buf, o, RV_T2, NK_MTIMECMP)
343 o = nk_w32(buf, o, nk_store(RV_T1, RV_T2, 3, 0))
344 o = nk_w32(buf, o, nk_addi(RV_T1, RV_X0, NK_MIE_MTIE))
345 o = nk_w32(buf, o, nk_csrrs(RV_X0, NK_CSR_MIE, RV_T1))
346 o = nk_w32(buf, o, nk_jal(RV_X0, tgt[NK_S_TASKP] - o))
347
348 pos[NK_S_TASKP] = o
349 o = nk_w32(buf, o, nk_jal(RV_X0, 0))
350 pos[NK_S_TASKQ] = o
351 o = nk_w32(buf, o, nk_jal(RV_X0, 0))
352
353 pos[NK_S_AFTERPRE] = o
354 o = nk_str(buf, o, " PREEMPT " as *u8)
355
356 // ---- 4c BLK: bring the virtio-blk device UP and configure its queue.
357 // NAMED HONESTLY: this is device discovery + status negotiation + queue configuration, each
358 // step CONFIRMED BY READBACK (magic, device id, queue-num-max, PFN read-back, final status).
359 // It is NOT a sector read -- the avail/used DMA round-trip is the next rung, and calling this
360 // "mounted" would be exactly the lie-by-label the osbench rig exists to prevent.
361 o = nk_devup(buf, o, vio_base, NK_VIO_BLK_ID, NK_VIO_RING_PFN, tgt[NK_S_BLKFAIL])
362 o = nk_str(buf, o, "BLK " as *u8)
363 o = nk_w32(buf, o, nk_jal(RV_X0, tgt[NK_S_AFTERBLK] - o))
364 pos[NK_S_BLKFAIL] = o
365 o = nk_str(buf, o, "BX " as *u8)
366 pos[NK_S_AFTERBLK] = o
367
368 // ---- 4d NET: the SECOND virtio transport (DeviceID=1 @ 0x10002000). Same negotiation, same
369 // readback discipline, different device -- which is exactly why the sequence is now a helper.
370 o = nk_devup(buf, o, NK_VIO_NET_BASE, NK_VIO_NET_ID, NK_VIO_NET_RING_PFN, tgt[NK_S_NETFAIL])
371 o = nk_str(buf, o, "NET " as *u8)
372 o = nk_w32(buf, o, nk_jal(RV_X0, tgt[NK_S_AFTERNET] - o))
373 pos[NK_S_NETFAIL] = o
374 o = nk_str(buf, o, "NF " as *u8)
375 pos[NK_S_AFTERNET] = o
376
377 // ---- 4e HEAP: a real kernel memory allocator, not a printed claim. Two successive allocs
378 // must return DISTINCT, non-overlapping, correctly-strided blocks, and the first block must
379 // actually hold what is written to it -- an allocator that hands back the same address twice,
380 // or memory that does not retain a store, is the failure this proves against.
381 o = nk_li32u(buf, o, RV_T5, heap_cell)
382 o = nk_li32u(buf, o, RV_T1, NK_HEAP_BASE)
383 o = nk_w32(buf, o, nk_store(RV_T1, RV_T5, 3, 0))
384 o = nk_w32(buf, o, nk_load(RV_T2, RV_T5, 3, 0))
385 o = nk_w32(buf, o, nk_addi(RV_T3, RV_T2, NK_HEAP_CHUNK))
386 o = nk_w32(buf, o, nk_store(RV_T3, RV_T5, 3, 0))
387 o = nk_w32(buf, o, nk_load(RV_T4, RV_T5, 3, 0))
388 o = nk_w32(buf, o, nk_addi(RV_T3, RV_T4, NK_HEAP_CHUNK))
389 o = nk_w32(buf, o, nk_store(RV_T3, RV_T5, 3, 0))
390 o = nk_w32(buf, o, nk_addi(RV_T1, RV_T2, NK_HEAP_CHUNK))
391 let hb1: i64 = o
392 o = nk_w32(buf, o, nk_branch(RV_T4, RV_T1, 1, tgt[NK_S_HEAPFAIL] - hb1))
393 o = nk_li32(buf, o, RV_T1, NK_HEAP_MAGIC)
394 o = nk_w32(buf, o, nk_store(RV_T1, RV_T2, 2, 0))
395 o = nk_w32(buf, o, nk_load(RV_T3, RV_T2, 2, 0))
396 o = nk_li32(buf, o, RV_T1, NK_HEAP_MAGIC)
397 let hb2: i64 = o
398 o = nk_w32(buf, o, nk_branch(RV_T3, RV_T1, 1, tgt[NK_S_HEAPFAIL] - hb2))
399 o = nk_str(buf, o, "HEAP " as *u8)
400 o = nk_w32(buf, o, nk_jal(RV_X0, tgt[NK_S_AFTERHEAP] - o))
401 pos[NK_S_HEAPFAIL] = o
402 o = nk_str(buf, o, "HX " as *u8)
403 pos[NK_S_AFTERHEAP] = o
404
405 o = nk_li32u(buf, o, RV_T1, NK_MEM_BASE + tgt[NK_S_SPHASE])
406 o = nk_w32(buf, o, nk_csrrw(RV_X0, NK_CSR_MEPC, RV_T1))
407 o = nk_li32(buf, o, RV_T1, NK_MPP_S)
408 o = nk_w32(buf, o, nk_csrrw(RV_X0, NK_CSR_MSTATUS, RV_T1))
409 o = nk_w32(buf, o, NK_MRET)
410
411 pos[NK_S_SPHASE] = o
412 o = nk_li32u(buf, o, RV_T5, NK_PGROOT)
413 o = nk_li32(buf, o, RV_T1, NK_PTE_KERN)
414 o = nk_w32(buf, o, nk_store(RV_T1, RV_T5, 2, 0x10))
415 o = nk_li32(buf, o, RV_T1, data_pte)
416 o = nk_w32(buf, o, nk_store(RV_T1, RV_T5, 2, 0x18))
417 o = nk_li32(buf, o, RV_T1, NK_PTE_USER)
418 o = nk_w32(buf, o, nk_store(RV_T1, RV_T5, 2, 0x08))
419 o = nk_li32u(buf, o, RV_T5, NK_SENT_PA)
420 o = nk_li32(buf, o, RV_T1, NK_SENT_VAL)
421 o = nk_w32(buf, o, nk_store(RV_T1, RV_T5, 2, 0))
422 o = nk_li32(buf, o, RV_T1, NK_PGPPN)
423 o = nk_w32(buf, o, nk_addi(RV_T2, RV_X0, NK_SV39))
424 o = nk_w32(buf, o, nk_slli(RV_T2, RV_T2, 60))
425 o = nk_w32(buf, o, nk_or(RV_T1, RV_T1, RV_T2))
426 o = nk_w32(buf, o, nk_csrrw(RV_X0, NK_CSR_SATP, RV_T1))
427 o = nk_li32u(buf, o, RV_T5, NK_DATA_VA)
428 o = nk_w32(buf, o, nk_load(RV_T3, RV_T5, 2, 0))
429 o = nk_li32(buf, o, RV_T4, NK_SENT_VAL)
430 let pcb: i64 = o
431 o = nk_w32(buf, o, nk_branch(RV_T3, RV_T4, 1, tgt[NK_S_PGFAIL] - pcb))
432 o = nk_str(buf, o, "PAGE " as *u8)
433
434 // ---- 6 USER: sret into U-mode on the U-bit gigapage ----
435 o = nk_li32u(buf, o, RV_T1, NK_USER_VA + tgt[NK_S_USERCODE])
436 o = nk_w32(buf, o, nk_csrrw(RV_X0, NK_CSR_SEPC, RV_T1))
437 o = nk_li32(buf, o, RV_T1, NK_SPP_BIT)
438 o = nk_w32(buf, o, nk_csrrc(RV_X0, NK_CSR_SSTATUS, RV_T1))
439 o = nk_w32(buf, o, NK_SRET)
440
441 pos[NK_S_USERCODE] = o
442 o = nk_w32(buf, o, nk_addi(RV_A7, RV_X0, 0))
443 o = nk_w32(buf, o, NK_ECALL)
444 o = nk_w32(buf, o, nk_addi(RV_A7, RV_X0, NK_SYS_EXIT))
445 o = nk_w32(buf, o, NK_ECALL)
446 o = nk_str(buf, o, "UX" as *u8)
447 o = nk_w32(buf, o, nk_jal(RV_X0, 0))
448
449 // ---- the M-mode trap dispatcher (mtvec) ----
450 pos[NK_S_MHANDLER] = o
451 o = nk_w32(buf, o, nk_csrrs(RV_T3, NK_CSR_MCAUSE, RV_X0))
452 let pcb2: i64 = o
453 o = nk_w32(buf, o, nk_branch(RV_T3, RV_X0, 4, tgt[NK_S_TIMERISR] - pcb2))
454 o = nk_w32(buf, o, nk_addi(RV_T4, RV_X0, 11))
455 let pcb3: i64 = o
456 o = nk_w32(buf, o, nk_branch(RV_T3, RV_T4, 0, tgt[NK_S_SYSCALL] - pcb3))
457 o = nk_w32(buf, o, nk_addi(RV_T4, RV_X0, 9))
458 let pcb4: i64 = o
459 o = nk_w32(buf, o, nk_branch(RV_T3, RV_T4, 0, tgt[NK_S_SYSCALL] - pcb4))
460 o = nk_w32(buf, o, nk_addi(RV_T4, RV_X0, 8))
461 let pcb5: i64 = o
462 o = nk_w32(buf, o, nk_branch(RV_T3, RV_T4, 0, tgt[NK_S_SYSCALL] - pcb5))
463 o = nk_w32(buf, o, nk_jal(RV_X0, tgt[NK_S_TRAPBAD] - o))
464
465 pos[NK_S_SYSCALL] = o
466 o = nk_w32(buf, o, nk_addi(RV_T4, RV_X0, NK_SYS_YIELD))
467 let pcb6: i64 = o
468 o = nk_w32(buf, o, nk_branch(RV_A7, RV_T4, 0, tgt[NK_S_DOYIELD] - pcb6))
469 o = nk_w32(buf, o, nk_addi(RV_T4, RV_X0, NK_SYS_EXIT))
470 let pcb7: i64 = o
471 o = nk_w32(buf, o, nk_branch(RV_A7, RV_T4, 0, tgt[NK_S_EXITOK] - pcb7))
472 o = nk_w32(buf, o, nk_csrrs(RV_T1, NK_CSR_MEPC, RV_X0))
473 o = nk_w32(buf, o, nk_addi(RV_T1, RV_T1, 4))
474 o = nk_w32(buf, o, nk_csrrw(RV_X0, NK_CSR_MEPC, RV_T1))
475 o = nk_w32(buf, o, NK_MRET)
476
477 // SYS_YIELD: round-robin by swapping the saved PC with the other task's
478 pos[NK_S_DOYIELD] = o
479 o = nk_w32(buf, o, nk_addi(RV_S1, RV_S1, 1))
480 o = nk_w32(buf, o, nk_addi(RV_T4, RV_X0, NK_YIELDS))
481 let pcb8: i64 = o
482 o = nk_w32(buf, o, nk_branch(RV_S1, RV_T4, 0, tgt[NK_S_AFTERSCHED] - pcb8))
483 o = nk_w32(buf, o, nk_csrrs(RV_T1, NK_CSR_MEPC, RV_X0))
484 o = nk_w32(buf, o, nk_addi(RV_T1, RV_T1, 4))
485 o = nk_w32(buf, o, nk_csrrw(RV_T4, NK_CSR_MEPC, RV_T6))
486 o = nk_w32(buf, o, nk_addi(RV_T6, RV_T1, 0))
487 o = nk_w32(buf, o, NK_MRET)
488
489 // timer ISR: rearm the compare first (a level-triggered MTIP that is never rearmed re-enters
490 // forever), then branch on the timer mode -- one-shot proof, or preemptive round-robin.
491 pos[NK_S_TIMERISR] = o
492 o = nk_li32u(buf, o, RV_T2, NK_MTIME)
493 o = nk_w32(buf, o, nk_load(RV_T1, RV_T2, 3, 0))
494 o = nk_w32(buf, o, nk_addi(RV_T1, RV_T1, NK_TICK))
495 o = nk_li32u(buf, o, RV_T2, NK_MTIMECMP)
496 o = nk_w32(buf, o, nk_store(RV_T1, RV_T2, 3, 0))
497 let pcbt: i64 = o
498 o = nk_w32(buf, o, nk_branch(RV_S2, RV_X0, 0, tgt[NK_S_ONESHOT] - pcbt))
499 o = nk_w32(buf, o, nk_csrrs(RV_T3, NK_CSR_MEPC, RV_X0))
500 o = nk_li32u(buf, o, RV_T4, NK_MEM_BASE + tgt[NK_S_TASKQ])
501 let pcbq: i64 = o
502 o = nk_w32(buf, o, nk_branch(RV_T3, RV_T4, 7, tgt[NK_S_PRINTQ] - pcbq))
503 o = nk_w32(buf, o, nk_addi(RV_T1, RV_X0, NK_CH_P))
504 o = nk_w32(buf, o, nk_store(RV_T1, RV_T0, 0, 0))
505 o = nk_w32(buf, o, nk_jal(RV_X0, tgt[NK_S_SWAP] - o))
506 pos[NK_S_PRINTQ] = o
507 o = nk_w32(buf, o, nk_addi(RV_T1, RV_X0, NK_CH_Q))
508 o = nk_w32(buf, o, nk_store(RV_T1, RV_T0, 0, 0))
509 pos[NK_S_SWAP] = o
510 o = nk_w32(buf, o, nk_addi(RV_S3, RV_S3, 1))
511 o = nk_w32(buf, o, nk_addi(RV_T4, RV_X0, NK_PREEMPTS))
512 let pcbd: i64 = o
513 o = nk_w32(buf, o, nk_branch(RV_S3, RV_T4, 0, tgt[NK_S_PREDONE] - pcbd))
514 o = nk_w32(buf, o, nk_csrrs(RV_T1, NK_CSR_MEPC, RV_X0))
515 o = nk_w32(buf, o, nk_csrrw(RV_T4, NK_CSR_MEPC, RV_T6))
516 o = nk_w32(buf, o, nk_addi(RV_T6, RV_T1, 0))
517 o = nk_w32(buf, o, NK_MRET)
518 pos[NK_S_PREDONE] = o
519 o = nk_w32(buf, o, nk_addi(RV_T1, RV_X0, NK_MIE_MTIE))
520 o = nk_w32(buf, o, nk_csrrc(RV_X0, NK_CSR_MIE, RV_T1))
521 o = nk_li32u(buf, o, RV_T1, NK_MEM_BASE + tgt[NK_S_AFTERPRE])
522 o = nk_w32(buf, o, nk_csrrw(RV_X0, NK_CSR_MEPC, RV_T1))
523 o = nk_w32(buf, o, NK_MRET)
524 pos[NK_S_ONESHOT] = o
525 o = nk_w32(buf, o, nk_addi(RV_T1, RV_X0, NK_MIE_MTIE))
526 o = nk_w32(buf, o, nk_csrrc(RV_X0, NK_CSR_MIE, RV_T1))
527 o = nk_li32u(buf, o, RV_T1, NK_MEM_BASE + tgt[NK_S_AFTERTIMER])
528 o = nk_w32(buf, o, nk_csrrw(RV_X0, NK_CSR_MEPC, RV_T1))
529 o = nk_w32(buf, o, NK_MRET)
530
531 // ---- the S-mode handler (stvec): serves the U-mode ecall delegated by medeleg ----
532 pos[NK_S_SHANDLER] = o
533 o = nk_w32(buf, o, nk_addi(RV_T4, RV_X0, NK_SYS_EXIT))
534 let pcb9: i64 = o
535 o = nk_w32(buf, o, nk_branch(RV_A7, RV_T4, 0, tgt[NK_S_EXITOK] - pcb9))
536 o = nk_str(buf, o, "USER " as *u8)
537 o = nk_w32(buf, o, nk_csrrs(RV_T1, NK_CSR_SEPC, RV_X0))
538 o = nk_w32(buf, o, nk_addi(RV_T1, RV_T1, 4))
539 o = nk_w32(buf, o, nk_csrrw(RV_X0, NK_CSR_SEPC, RV_T1))
540 o = nk_w32(buf, o, NK_SRET)
541
542 pos[NK_S_EXITOK] = o
543 o = nk_str(buf, o, "OK" as *u8)
544 pos[NK_S_HALT] = o
545 o = nk_li32u(buf, o, RV_T5, NK_FIN)
546 o = nk_li32(buf, o, RV_T1, NK_PASS)
547 o = nk_w32(buf, o, nk_store(RV_T1, RV_T5, 2, 0))
548 o = nk_w32(buf, o, nk_jal(RV_X0, 0))
549
550 // ---- failure blocks: a broken phase must SAY so, never fall silent ----
551 pos[NK_S_PGFAIL] = o
552 o = nk_str(buf, o, "PGX" as *u8)
553 o = nk_w32(buf, o, nk_jal(RV_X0, tgt[NK_S_HALT] - o))
554 pos[NK_S_TRAPBAD] = o
555 o = nk_str(buf, o, "TX" as *u8)
556 o = nk_w32(buf, o, nk_jal(RV_X0, tgt[NK_S_HALT] - o))
557 return o
558}
559
560// TAMPER (the gate's negative control -- a boot that CANNOT be made to fail proves nothing):
561// 0 = none; 1 = medeleg cleared (the U-mode ecall is no longer delegated to S, so the
562// "USER " marker its S-handler prints must VANISH); 2 = the data leaf PTE left invalid
563// (the Sv39 walk must fault instead of translating).
564func nk_streq(a: *u8, b: *u8) -> i64 {
565 var i: i64 = 0
566 while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 }
567 if b[i] != (0 as u8) { return 0 }
568 return 1
569}
570
571func main(argc: i64, argv: *i64) -> i64 {
572 var outp: *u8 = NK_OUT
573 if argc >= 2 { outp = argv[1] as *u8 }
574 var tamper: i64 = 0
575 if argc >= 3 {
576 let tsel: *u8 = argv[2] as *u8
577 if tsel[0] == (49 as u8) { tamper = 1 }
578 if tsel[0] == (50 as u8) { tamper = 2 }
579 }
580 if argc >= 3 {
581 let tsel3: *u8 = argv[2] as *u8
582 if tsel3[0] == (51 as u8) { tamper = 3 }
583 if tsel3[0] == (52 as u8) { tamper = 4 }
584 }
585 // A TAMPER BUILD MUST NEVER LAND ON THE LIVE PATH. The golden written below is always the
586 // CLEAN transcript, so emitting a deliberately-broken kernel over the image BOOTSOV boots
587 // would leave a live boot that permanently disagrees with its own golden -- a self-inflicted
588 // outage dressed up as a gate failure. Tamper therefore REQUIRES an explicit scratch path.
589 if tamper != 0 {
590 var bad_target: i64 = 0
591 if argc < 2 { bad_target = 1 }
592 if nk_streq(outp, NK_OUT) == 1 { bad_target = 1 }
593 if nk_streq(outp, "_boot_nishi_virt.bin" as *u8) == 1 { bad_target = 1 }
594 if bad_target == 1 {
595 nk_p("NISHIOSKERNEL verdict=REFUSED reason=tamper-would-overwrite-the-live-image -- pass an explicit scratch outpath for tamper builds\n" as *u8)
596 sys_exit(2)
597 return 2
598 }
599 }
600 var medeleg: i64 = NK_MEDELEG_U
601 var data_pte: i64 = NK_PTE_DATA
602 var vio_base: i64 = NK_VIO_BASE
603 var heap_cell: i64 = NK_HEAP_CELL
604 if tamper == 1 { medeleg = 0 }
605 if tamper == 2 { data_pte = 0 }
606 if tamper == 3 { vio_base = 0x10005000 }
607 // tamper 4: put the bump-pointer cell on the UART (a DEVICE, not RAM). A device does not
608 // retain what you store into it, so the allocator's own invariants must fail -- this proves
609 // the HEAP marker depends on real memory behaving like memory, not on reaching the code.
610 if tamper == 4 { heap_cell = 0x10000000 }
611
612 let zero: *i64 = sys_mmap(8 * NK_S_N) as *i64
613 let pos1: *i64 = sys_mmap(8 * NK_S_N) as *i64
614 var z: i64 = 0
615 while z < NK_S_N { zero[z] = 0; pos1[z] = 0; z = z + 1 }
616
617 let scratch: *u8 = sys_mmap(NK_MAGIC_16384)
618 nk_emit_image(scratch, zero, pos1, medeleg, data_pte, vio_base, heap_cell)
619
620 let pos2: *i64 = sys_mmap(8 * NK_S_N) as *i64
621 var z2: i64 = 0
622 while z2 < NK_S_N { pos2[z2] = 0; z2 = z2 + 1 }
623 let buf: *u8 = sys_mmap(NK_MAGIC_16384)
624 let sz: i64 = nk_emit_image(buf, pos1, pos2, medeleg, data_pte, vio_base, heap_cell)
625
626 // the layout must be a FIXED POINT: pass-2 offsets identical to pass-1, else the
627 // resolved branch targets describe a different image than the one emitted.
628 var bad: i64 = 0
629 var z3: i64 = 0
630 while z3 < NK_S_N { if pos2[z3] != pos1[z3] { bad = bad + 1 } z3 = z3 + 1 }
631 if bad != 0 {
632 nk_p("NISHIOSKERNEL verdict=RED reason=layout-not-fixed-point sections=" as *u8); nk_fn(1, bad); nk_p("\n" as *u8)
633 return 1
634 }
635
636 // FAIL CLOSED on code growing into the page-table / DMA region rather than silently
637 // corrupting it: the symptom would present as a paging fault, miles from the real cause.
638 if sz >= NK_CODE_LIMIT {
639 nk_p("NISHIOSKERNEL verdict=RED reason=code-overruns-pagetable bytes=" as *u8); nk_fn(1, sz)
640 nk_p(" limit=" as *u8); nk_fn(1, NK_CODE_LIMIT); nk_p("\n" as *u8)
641 return 1
642 }
643 let fd: i64 = sys_openat_wr(outp, 420)
644 if fd < 0 { nk_p("NISHIOSKERNEL verdict=RED reason=out-unwritable\n" as *u8); return 1 }
645 sys_write(fd, buf, sz)
646 sys_close(fd)
647
648 // the expected serial transcript, table-computed from the phase markers
649 let gold: *u8 = "NISHI TRAP ABABABABABAB SCHED TICK PQPQPQ PREEMPT BLK NET HEAP PAGE USER OK" as *u8
650 var gn: i64 = 0
651 while gold[gn] != (0 as u8) { gn = gn + 1 }
652 // THE GOLDEN TRAVELS WITH ITS ARTIFACT: derive <outpath>.gold instead of a fixed constant.
653 // With a hardcoded path the live NAS image at ./ had its golden stranded under
654 // runtime/_hdl_build/, so anything deriving the golden from the image it just booted found
655 // nothing -- an artifact whose expectation lives in a different tree cannot be self-checking.
656 let gpath: *u8 = sys_mmap(512)
657 var gi: i64 = 0
658 while outp[gi] != (0 as u8) { gpath[gi] = outp[gi]; gi = gi + 1 }
659 let suf: *u8 = ".gold" as *u8
660 var si: i64 = 0
661 while suf[si] != (0 as u8) { gpath[gi + si] = suf[si]; si = si + 1 }
662 gpath[gi + si] = 0 as u8
663 let gfd: i64 = sys_openat_wr(gpath, 420)
664 if gfd >= 0 { sys_write(gfd, gold, gn); sys_close(gfd) }
665 let gfd2: i64 = sys_openat_wr(NK_GOLD, 420)
666 if gfd2 >= 0 { sys_write(gfd2, gold, gn); sys_close(gfd2) }
667
668 nk_p("NISHIOSKERNEL name=" as *u8); nk_p(outp)
669 nk_p(" bytes=" as *u8); nk_fn(1, sz)
670 nk_p(" phases=10 golden=" as *u8); nk_p(gold); nk_p("\n" as *u8)
671 let lf: i64 = sys_openat_append(NK_LOG, 420)
672 if lf >= 0 {
673 nk_fp(lf, "NISHIOSKERNEL authored _boot_nishi_virt.bin phases=10 bytes=" as *u8); nk_fn(lf, sz)
674 nk_fp(lf, " trap=dispatcher sched=yield-rr preempt=timer-ctxsw timer=clint-mtip paging=sv39 user=umode-medeleg golden=" as *u8)
675 nk_fp(lf, gold); nk_fp(lf, "\n" as *u8)
676 sys_close(lf)
677 }
678 return 0
679}