assembler: bound image linking storage and encode machine fences
fdf8f5d1395694cd90e79bb283efee2b1d20b6c4f9615dbb293f3061530b89f8
Verified: make -C kernel check; make std-test bin-test with the machine-capable emulator; all pass.
1 parent
566537b1
kernel/NOTES.md
+11 -2
| 1 | 1 | # Kernel implementation decisions |
|
| 2 | 2 | ||
| 3 | 3 | The specification at https://radiant.computer/system/kernel takes precedence |
|
| 4 | 4 | for fixed call numbers, handle layout, rights, and object behavior. These notes |
|
| 5 | - | record the contracts established through step 14 of the 22-step plan. |
|
| 5 | + | record the contracts established through step 15 of the 22-step plan. |
|
| 6 | 6 | ||
| 7 | 7 | ## Source and trust boundary |
|
| 8 | 8 | ||
| 9 | 9 | - Kernel mechanisms use freestanding Radiance; RAS owns machine entry, register |
|
| 10 | 10 | state, atomics, and MMIO. Hosted checks exercise the same mechanism modules. |
| 189 | 189 | exclusive receiver transfer. |
|
| 190 | 190 | - Missing, dead, or stale receiver authority leaves the source masked. Invalid |
|
| 191 | 191 | consumer progress cannot release a claim or rearm a source. The two-hart |
|
| 192 | 192 | IRQ/UART check exercises the primary PLIC route, not multicore dispatch. |
|
| 193 | 193 | ||
| 194 | + | ## Assembler storage and machine fences |
|
| 195 | + | ||
| 196 | + | - Size assembler symbol, fixup, and hash-table storage from possible name tokens, |
|
| 197 | + | not source byte length. Whitespace, comments, and literal bytes consume no |
|
| 198 | + | symbol slots. |
|
| 199 | + | - Bare fence orders memory and device I/O (iorw,iorw). fence.i synchronizes |
|
| 200 | + | local instruction fetch. These are distinct ordering obligations. |
|
| 201 | + | ||
| 194 | 202 | ## Validation |
|
| 195 | 203 | ||
| 196 | 204 | Use the current machine-capable sibling emulator. Set `RAD_EMULATOR`, pass |
|
| 197 | 205 | `EMU` to the kernel Make invocation, or put `emulator` on PATH. The kernel build |
|
| 198 | 206 | checks compiler dependencies. From the repository root, run: |
|
| 199 | 207 | ||
| 200 | 208 | ```sh |
|
| 201 | 209 | make -C kernel check |
|
| 210 | + | make std-test bin-test |
|
| 202 | 211 | ``` |
|
| 203 | 212 | ||
| 204 | - | Run the two-hart IRQ/UART scenario with IRQ 3 at tick 4,000,000 and UART byte R at tick 8,000,000. Check retained-claim transfer through a full queue, one delivery, draining, dropped authority, and the next timer wake. |
|
| 213 | + | Exercise name-token-bounded assembler allocation and both fence encodings along with the existing kernel machine paths. |
|
| 205 | 214 | ||
| 206 | 215 | Run the context reservation probe with an emulator that retains LR/SC |
|
| 207 | 216 | reservations across traps. This checks the kernel's reservation invalidation. |
lib/std/arch/rv64/asm.rad
+20 -2
| 124 | 124 | ||
| 125 | 125 | /// Parser and encoder behavior for one instruction mnemonic. |
|
| 126 | 126 | export union InstructionEncoder: Copy { |
|
| 127 | 127 | /// No-operand instruction encoded by a fixed encoder. |
|
| 128 | 128 | NoOperand { enc: fn() -> u32 }, |
|
| 129 | + | /// Memory/I/O fence or its `.i` instruction-cache form. |
|
| 130 | + | Fence, |
|
| 129 | 131 | /// Load-immediate pseudo-instruction. |
|
| 130 | 132 | Li, |
|
| 131 | 133 | /// Load-address pseudo-instruction. |
|
| 132 | 134 | La, |
|
| 133 | 135 | /// Two-register instruction or pseudo-instruction. |
| 301 | 303 | { name: "divu", encoder: InstructionEncoder::RRR { enc: encode::divu } }, |
|
| 302 | 304 | { name: "divuw", encoder: InstructionEncoder::RRR { enc: encode::divuw } }, |
|
| 303 | 305 | { name: "divw", encoder: InstructionEncoder::RRR { enc: encode::divw } }, |
|
| 304 | 306 | { name: "ebreak", encoder: InstructionEncoder::NoOperand { enc: encode::ebreak } }, |
|
| 305 | 307 | { name: "ecall", encoder: InstructionEncoder::NoOperand { enc: encode::ecall } }, |
|
| 306 | - | { name: "fence", encoder: InstructionEncoder::NoOperand { enc: encode::fence } }, |
|
| 308 | + | { name: "fence", encoder: InstructionEncoder::Fence }, |
|
| 307 | 309 | { name: "j", encoder: InstructionEncoder::Jump { rd: rv64::ZERO } }, |
|
| 308 | 310 | { name: "jal", encoder: InstructionEncoder::Jal }, |
|
| 309 | 311 | { name: "jalr", encoder: InstructionEncoder::RRI { enc: encode::jalr } }, |
|
| 310 | 312 | { name: "la", encoder: InstructionEncoder::La }, |
|
| 311 | 313 | { name: "lb", encoder: InstructionEncoder::Load { enc: encode::lb } }, |
| 490 | 492 | externalFixups: *mut [Fixup], |
|
| 491 | 493 | /// Absolute runtime address of data-section offset zero. |
|
| 492 | 494 | dataBase: u32, |
|
| 493 | 495 | } |
|
| 494 | 496 | ||
| 497 | + | /// Bound symbol, fixup, and dictionary entries by their possible name tokens. |
|
| 498 | + | /// Comments, whitespace, and literal byte contents need no symbol storage. |
|
| 499 | + | fn symbolCapacity(sourceKind: scanner::SourceKind, source: *[u8], pool: *mut strings::Pool) -> u32 throws (Error) { |
|
| 500 | + | let mut scan = scanner::scanner(sourceKind, source, pool); |
|
| 501 | + | let mut count = SOURCE_CAP_PADDING; |
|
| 502 | + | loop { |
|
| 503 | + | let token = scanner::next(&mut scan); |
|
| 504 | + | match token.kind { |
|
| 505 | + | case scanner::TokenKind::Eof => return count, |
|
| 506 | + | case scanner::TokenKind::Invalid => throw Error::Invalid { offset: token.offset, message: token.source }, |
|
| 507 | + | case scanner::TokenKind::Ident, scanner::TokenKind::Label, scanner::TokenKind::QuotedLabel => set count += 1, |
|
| 508 | + | else => {}, |
|
| 509 | + | } |
|
| 510 | + | } |
|
| 511 | + | } |
|
| 512 | + | ||
| 495 | 513 | /// Assemble source using `dataBase` as the runtime address of the data-section. |
|
| 496 | 514 | export fn assemble( |
|
| 497 | 515 | sourceKind: scanner::SourceKind, |
|
| 498 | 516 | source: *[u8], |
|
| 499 | 517 | textBuf: *mut [u32], |
|
| 500 | 518 | dataBuf: *mut [u8], |
|
| 501 | 519 | arena: *mut alloc::Arena, |
|
| 502 | 520 | pool: *mut strings::Pool, |
|
| 503 | 521 | dataBase: u32 |
|
| 504 | 522 | ) -> Program throws (Error) { |
|
| 505 | - | let slotCap = source.len + SOURCE_CAP_PADDING; |
|
| 523 | + | let slotCap = try symbolCapacity(sourceKind, source, pool); |
|
| 506 | 524 | let tableCap = nextPowerOfTwo(slotCap * TABLE_CAPACITY_SCALE); |
|
| 507 | 525 | ||
| 508 | 526 | let symbols = try! alloc::allocSlice(arena, @sizeOf(Symbol), @alignOf(Symbol), slotCap); |
|
| 509 | 527 | let fixups = try! alloc::allocSlice(arena, @sizeOf(Fixup), @alignOf(Fixup), slotCap); |
|
| 510 | 528 | let externalFixups = try! alloc::allocSlice(arena, @sizeOf(Fixup), @alignOf(Fixup), slotCap); |
lib/std/arch/rv64/asm/parser.rad
+16 -0
| 292 | 292 | throw fail(a, "unexpected operand"); |
|
| 293 | 293 | } |
|
| 294 | 294 | try emit::emitText(a, enc()); |
|
| 295 | 295 | return; |
|
| 296 | 296 | } |
|
| 297 | + | case super::InstructionEncoder::Fence => return try parseFence(a, tok), |
|
| 297 | 298 | case super::InstructionEncoder::Li => return try parseLi(a), |
|
| 298 | 299 | case super::InstructionEncoder::La => return try parseLa(a), |
|
| 299 | 300 | case super::InstructionEncoder::RR { enc } => return try parseRR(a, enc), |
|
| 300 | 301 | case super::InstructionEncoder::RRR { enc } => return try parseRRR(a, enc), |
|
| 301 | 302 | case super::InstructionEncoder::RRI { enc } => return try parseRRI(a, enc), |
| 316 | 317 | case super::InstructionEncoder::Csrsi => return try parseCsrsi(a), |
|
| 317 | 318 | case super::InstructionEncoder::Upper { enc } => return try parseUpper(a, enc), |
|
| 318 | 319 | } |
|
| 319 | 320 | } |
|
| 320 | 321 | ||
| 322 | + | /// Parse a full memory/I/O fence or the adjacent `.i` instruction-cache suffix. |
|
| 323 | + | fn parseFence(a: *mut super::Assembler, mnemonic: scanner::Token) throws (super::Error) { |
|
| 324 | + | let mut instruction = encode::fence(); |
|
| 325 | + | if a.scan.current.kind == scanner::TokenKind::Directive { |
|
| 326 | + | if a.scan.current.offset <> mnemonic.offset + mnemonic.source.len |
|
| 327 | + | or not mem::eq(a.scan.current.source, ".i") { |
|
| 328 | + | throw fail(a, "expected adjacent .i fence suffix"); |
|
| 329 | + | } |
|
| 330 | + | advance(a); |
|
| 331 | + | set instruction = encode::fenceI(); |
|
| 332 | + | } |
|
| 333 | + | try expectTerminator(a, "unexpected fence operand"); |
|
| 334 | + | try emit::emitText(a, instruction); |
|
| 335 | + | } |
|
| 336 | + | ||
| 321 | 337 | /// Parse the `li` pseudo-instruction. |
|
| 322 | 338 | fn parseLi(a: *mut super::Assembler) throws (super::Error) { |
|
| 323 | 339 | let rd = try parseRegister(a); |
|
| 324 | 340 | let value = try parseValue(a); |
|
| 325 | 341 | if encode::isSmallImm64(value) { |
lib/std/arch/rv64/asm/tests.rad
+23 -0
| 91 | 91 | try testing::expect(program.text.len == 2); |
|
| 92 | 92 | try testing::expect(program.text[0] == encode::jal(rv64::ZERO, 4)); |
|
| 93 | 93 | try testing::expect(program.text[1] == encode::jalr(rv64::ZERO, rv64::RA, 0)); |
|
| 94 | 94 | } |
|
| 95 | 95 | ||
| 96 | + | /// Verbose source must fit an arena sized for its small assembled program. |
|
| 97 | + | @test fn testAssembleLongCommentWithBoundedSymbolStorage() throws (testing::TestError) { |
|
| 98 | + | let mut source: [u8; 8192] = ['x'; 8192]; |
|
| 99 | + | set source[0] = '/'; |
|
| 100 | + | set source[1] = '/'; |
|
| 101 | + | let body = "\n.text; j @\"local::destination\"; @\"local::destination\" ret;"; |
|
| 102 | + | let _copied = try! mem::copy(&mut source[8192 - body.len..], body); |
|
| 103 | + | let program = try assembleSource(&source[..]); |
|
| 104 | + | try testing::expect(program.text.len == 2); |
|
| 105 | + | try testing::expect(program.text[0] == encode::jal(rv64::ZERO, 4)); |
|
| 106 | + | try testing::expect(program.text[1] == encode::jalr(rv64::ZERO, rv64::RA, 0)); |
|
| 107 | + | } |
|
| 108 | + | ||
| 109 | + | /// Fence encodings must order device I/O and synchronize instruction fetches. |
|
| 110 | + | @test fn testAssembleMachineFences() throws (testing::TestError) { |
|
| 111 | + | let program = try assembleSource(".text; fence; fence.i;"); |
|
| 112 | + | try testing::expect(program.text.len == 2); |
|
| 113 | + | try testing::expect(program.text[0] == 0x0ff0000f); |
|
| 114 | + | try testing::expect(program.text[1] == 0x0000100f); |
|
| 115 | + | try expectAssembleFail(".text; fence.i %a0;"); |
|
| 116 | + | try expectAssembleFail(".text; fence.x;"); |
|
| 117 | + | } |
|
| 118 | + | ||
| 96 | 119 | @test fn testAssembleGlobalMarksOnlyDeclaredSymbols() throws (testing::TestError) { |
|
| 97 | 120 | let program = try assembleSource( |
|
| 98 | 121 | ".text;\n.export @exported;\n@local\nret;\n@exported\nret;\n@late\n.export @late;\nret;\n" |
|
| 99 | 122 | ); |
|
| 100 | 123 | try testing::expect(program.symbols.len == 3); |
lib/std/arch/rv64/encode.rad
+7 -2
| 585 | 585 | /// Environment break (debugger breakpoint). |
|
| 586 | 586 | export fn ebreak() -> u32 { |
|
| 587 | 587 | return encodeI(OP_SYSTEM, super::ZERO, super::ZERO, 0, 1); |
|
| 588 | 588 | } |
|
| 589 | 589 | ||
| 590 | - | /// Full predecessor/successor memory fence (`fence rw, rw`). |
|
| 590 | + | /// Full predecessor/successor memory and device-I/O fence (`fence iorw, iorw`). |
|
| 591 | 591 | export fn fence() -> u32 { |
|
| 592 | - | return 0x0330000F; |
|
| 592 | + | return 0x0FF0000F; |
|
| 593 | + | } |
|
| 594 | + | ||
| 595 | + | /// Synchronize this hart's instruction fetches with prior instruction stores. |
|
| 596 | + | export fn fenceI() -> u32 { |
|
| 597 | + | return 0x0000100F; |
|
| 593 | 598 | } |
|
| 594 | 599 | ||
| 595 | 600 | /// Encode a CSR instruction with a register source. |
|
| 596 | 601 | fn encodeCsr(op: u32, rd: gen::Reg, csr: u32, funct3: u32, rs1: gen::Reg) -> u32 { |
|
| 597 | 602 | return (op & 0x7F) |