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}