Emulate the kernel privilege boundary
aa9a023771f764a2324d431eea82b96b2c722a506ead5d2640b9cfc4658d82db
1 parent
92cdb0c5
Makefile
+7 -2
| 26 | 26 | install: bin/emulator |
|
| 27 | 27 | @echo "copy bin/emulator => $(PREFIX)/emulator" |
|
| 28 | 28 | @mkdir -p $(PREFIX) |
|
| 29 | 29 | @cp bin/emulator $(PREFIX)/emulator |
|
| 30 | 30 | ||
| 31 | + | test: bin/emulator |
|
| 32 | + | @mkdir -p build |
|
| 33 | + | @printf '\163\045\100\361\163\000\120\020' > build/machine.rv64 |
|
| 34 | + | @bin/emulator -machine -run build/machine.rv64 |
|
| 35 | + | ||
| 31 | 36 | fmt: |
|
| 32 | 37 | git ls-files "*.c" "*.h" | xargs clang-format -i |
|
| 33 | 38 | ||
| 34 | 39 | clean: |
|
| 35 | - | @rm -f bin/emulator |
|
| 40 | + | @rm -rf bin/emulator build |
|
| 36 | 41 | ||
| 37 | - | .PHONY: default clean install |
|
| 42 | + | .PHONY: default clean install test |
|
| 38 | 43 | .SUFFIXES: |
|
| 39 | 44 | .DELETE_ON_ERROR: |
|
| 40 | 45 | .SILENT: |
README
+5 -0
| 43 | 43 | ||
| 44 | 44 | OPTIONS |
|
| 45 | 45 | ||
| 46 | 46 | -run Run headless (no TUI). |
|
| 47 | 47 | -debug Load debug info for source-level diagnostics. |
|
| 48 | + | -machine Execute privileged RV64 machine images. Disables |
|
| 49 | + | Linux-compatible host ecalls and the JIT. |
|
| 48 | 50 | -no-jit Disable the JIT compiler; interpret only. |
|
| 49 | 51 | -memory-size=KB Physical memory size in KB (default 128 MB). |
|
| 50 | 52 | -data-size=KB Data memory size in KB. |
|
| 51 | 53 | -stack-size=KB Stack size in KB (default 256 KB). |
|
| 52 | 54 | -no-guard-stack Disable stack guard zones (enabled by default, 16 bytes). |
| 84 | 86 | x86-64 machine code. Blocks are compiled on first |
|
| 85 | 87 | encounter and cached (16 MB code cache, up to 256K |
|
| 86 | 88 | blocks). Falls back to the interpreter for system |
|
| 87 | 89 | calls and faults. |
|
| 88 | 90 | ||
| 91 | + | Machine mode implements the M-mode CSRs used by the Radiant kernel, |
|
| 92 | + | U-mode `ecall` traps, `mret`, and `wfi`. The emulated boot hart is hart 0. |
|
| 93 | + | ||
| 89 | 94 | The TUI debugger supports single-stepping, reverse execution (via |
|
| 90 | 95 | snapshots), register and stack inspection, and memory watchpoints. |
|
| 91 | 96 | ||
| 92 | 97 | LICENSE |
|
| 93 | 98 |
emulator.c
+124 -1
| 112 | 112 | instr_t *program; /* Program instructions. */ |
|
| 113 | 113 | bool running; /* Execution status. */ |
|
| 114 | 114 | bool faulted; /* There was a fault in execution. */ |
|
| 115 | 115 | bool ebreak; /* Program terminated via EBREAK. */ |
|
| 116 | 116 | reg_t modified; /* Index of the last modified register. */ |
|
| 117 | + | u64 mstatus; |
|
| 118 | + | u64 mie; |
|
| 119 | + | u64 mtvec; |
|
| 120 | + | u64 mscratch; |
|
| 121 | + | u64 mepc; |
|
| 122 | + | u64 mcause; |
|
| 123 | + | u64 mtval; |
|
| 124 | + | u64 mip; |
|
| 125 | + | u64 mhartid; |
|
| 126 | + | u8 privilege; /* RISC-V privilege level: U=0, M=3. */ |
|
| 117 | 127 | }; |
|
| 118 | 128 | ||
| 119 | 129 | /* Snapshot of CPU and memory state for reversing execution. */ |
|
| 120 | 130 | struct snapshot { |
|
| 121 | 131 | struct cpu cpu; /* Copy of CPU state. */ |
| 238 | 248 | bool watch_backtrace; |
|
| 239 | 249 | u32 watch_backtrace_depth; |
|
| 240 | 250 | bool validate_memory; |
|
| 241 | 251 | bool count_instructions; |
|
| 242 | 252 | bool jit_disabled; |
|
| 253 | + | bool machine_mode; |
|
| 243 | 254 | }; |
|
| 244 | 255 | ||
| 245 | 256 | /* Global emulator options. */ |
|
| 246 | 257 | static struct emulator_options g_opts = { |
|
| 247 | 258 | .stack_guard = true, |
| 263 | 274 | .watch_backtrace = false, |
|
| 264 | 275 | .watch_backtrace_depth = 8, |
|
| 265 | 276 | .validate_memory = true, |
|
| 266 | 277 | .count_instructions = false, |
|
| 267 | 278 | .jit_disabled = false, |
|
| 279 | + | .machine_mode = false, |
|
| 268 | 280 | }; |
|
| 269 | 281 | ||
| 270 | 282 | static void dump_watch_context(struct cpu *, u32 addr, u32 size, u32 value); |
|
| 271 | 283 | ||
| 272 | 284 | /* Return true if the given address range overlaps the watched region. */ |
| 499 | 511 | ||
| 500 | 512 | /* Print usage information and return 1. */ |
|
| 501 | 513 | static int usage(const char *prog) { |
|
| 502 | 514 | fprintf( |
|
| 503 | 515 | stderr, |
|
| 504 | - | "usage: %s [-run] [-no-guard-stack]" |
|
| 516 | + | "usage: %s [-run] [-machine] [-no-guard-stack]" |
|
| 505 | 517 | " [-stack-size=KB] [-no-validate] [-debug]" |
|
| 506 | 518 | " [-trace|-trace-headless] [-trace-depth=n] [-trace-instructions]" |
|
| 507 | 519 | " [-max-steps=n] [-memory-size=KB] [-data-size=KB]" |
|
| 508 | 520 | " [-watch=addr] [-watch-size=bytes] [-watch-arm-pc=addr]" |
|
| 509 | 521 | " [-watch-zero-only] [-watch-skip=n]" |
| 681 | 693 | if (strcmp(arg, "-run") == 0) { |
|
| 682 | 694 | headless = true; |
|
| 683 | 695 | argi++; |
|
| 684 | 696 | continue; |
|
| 685 | 697 | } |
|
| 698 | + | if (strcmp(arg, "-machine") == 0) { |
|
| 699 | + | g_opts.machine_mode = true; |
|
| 700 | + | g_opts.jit_disabled = true; |
|
| 701 | + | argi++; |
|
| 702 | + | continue; |
|
| 703 | + | } |
|
| 686 | 704 | ||
| 687 | 705 | if (strncmp(arg, "-stack-size=", 12) == 0) { |
|
| 688 | 706 | if (!parse_stack_size_value(arg + 12)) |
|
| 689 | 707 | return false; |
|
| 690 | 708 | argi++; |
| 1190 | 1208 | ||
| 1191 | 1209 | /* Reset CPU state (keeping program loaded). */ |
|
| 1192 | 1210 | static void cpu_reset(struct cpu *cpu) { |
|
| 1193 | 1211 | trace_reset(); |
|
| 1194 | 1212 | memset(cpu->regs, 0, sizeof(cpu->regs)); |
|
| 1213 | + | cpu->mstatus = 0; |
|
| 1214 | + | cpu->mie = 0; |
|
| 1215 | + | cpu->mtvec = 0; |
|
| 1216 | + | cpu->mscratch = 0; |
|
| 1217 | + | cpu->mepc = 0; |
|
| 1218 | + | cpu->mcause = 0; |
|
| 1219 | + | cpu->mtval = 0; |
|
| 1220 | + | cpu->mip = 0; |
|
| 1221 | + | cpu->mhartid = 0; |
|
| 1222 | + | cpu->privilege = 3; |
|
| 1195 | 1223 | ||
| 1196 | 1224 | /* Set SP to the top of the usable stack, aligned to 16 bytes |
|
| 1197 | 1225 | * as required by the RISC-V ABI. */ |
|
| 1198 | 1226 | cpu->regs[SP] = stack_usable_top() & ~0xF; |
|
| 1199 | 1227 | cpu->pc = program_base; |
| 1204 | 1232 | last_executed_pc = 0; |
|
| 1205 | 1233 | } |
|
| 1206 | 1234 | ||
| 1207 | 1235 | /* Initialize CPU and memory to a clean state. */ |
|
| 1208 | 1236 | static void cpu_init(struct cpu *cpu) { |
|
| 1237 | + | memset(cpu, 0, sizeof(*cpu)); |
|
| 1209 | 1238 | memset(memory, 0, g_opts.memory_size); |
|
| 1210 | 1239 | cpu->program = (instr_t *)memory; |
|
| 1211 | 1240 | cpu->programsize = 0; |
|
| 1212 | 1241 | trace_reset(); |
|
| 1242 | + | cpu->privilege = 3; |
|
| 1243 | + | cpu->mhartid = 0; |
|
| 1213 | 1244 | guest_fd_table_init(); |
|
| 1214 | 1245 | cpu_reset(cpu); |
|
| 1215 | 1246 | } |
|
| 1216 | 1247 | ||
| 1217 | 1248 | /* Open a file via the openat syscall (56). */ |
| 1623 | 1654 | ||
| 1624 | 1655 | cpu->programsize = program_bytes / sizeof(instr_t); |
|
| 1625 | 1656 | cpu->pc = program_base; |
|
| 1626 | 1657 | } |
|
| 1627 | 1658 | ||
| 1659 | + | static u64 *cpu_csr(struct cpu *cpu, u32 csr) { |
|
| 1660 | + | switch (csr) { |
|
| 1661 | + | case 0x300: return &cpu->mstatus; |
|
| 1662 | + | case 0x304: return &cpu->mie; |
|
| 1663 | + | case 0x305: return &cpu->mtvec; |
|
| 1664 | + | case 0x340: return &cpu->mscratch; |
|
| 1665 | + | case 0x341: return &cpu->mepc; |
|
| 1666 | + | case 0x342: return &cpu->mcause; |
|
| 1667 | + | case 0x343: return &cpu->mtval; |
|
| 1668 | + | case 0x344: return &cpu->mip; |
|
| 1669 | + | case 0xF14: return &cpu->mhartid; |
|
| 1670 | + | default: return NULL; |
|
| 1671 | + | } |
|
| 1672 | + | } |
|
| 1673 | + | ||
| 1674 | + | static void cpu_machine_trap( |
|
| 1675 | + | struct cpu *cpu, u32 executed_pc, u64 cause, u64 value, u32 *pc_next |
|
| 1676 | + | ) { |
|
| 1677 | + | u64 mie = (cpu->mstatus >> 3) & 1; |
|
| 1678 | + | cpu->mstatus &= ~((u64)3 << 11); |
|
| 1679 | + | cpu->mstatus |= (u64)cpu->privilege << 11; |
|
| 1680 | + | cpu->mstatus = (cpu->mstatus & ~((u64)1 << 7)) | (mie << 7); |
|
| 1681 | + | cpu->mstatus &= ~((u64)1 << 3); |
|
| 1682 | + | cpu->mepc = executed_pc; |
|
| 1683 | + | cpu->mcause = cause; |
|
| 1684 | + | cpu->mtval = value; |
|
| 1685 | + | cpu->privilege = 3; |
|
| 1686 | + | *pc_next = (u32)(cpu->mtvec & ~(u64)3); |
|
| 1687 | + | } |
|
| 1688 | + | ||
| 1628 | 1689 | /* Execute a single instruction. */ |
|
| 1629 | 1690 | static void cpu_execute(struct cpu *cpu, enum display display, bool headless) { |
|
| 1630 | 1691 | if (cpu_out_of_bounds(cpu)) { |
|
| 1631 | 1692 | cpu->running = false; |
|
| 1632 | 1693 | emit_fault_diagnostics(cpu, cpu->pc); |
| 2165 | 2226 | } |
|
| 2166 | 2227 | ||
| 2167 | 2228 | case OP_SYSTEM: { |
|
| 2168 | 2229 | u32 funct12 = ins.i.imm_11_0; |
|
| 2169 | 2230 | ||
| 2231 | + | if (g_opts.machine_mode) { |
|
| 2232 | + | u32 funct3 = (ins.raw >> 12) & 7; |
|
| 2233 | + | if (funct3 != 0) { |
|
| 2234 | + | u32 csr_num = (ins.raw >> 20) & 0xFFF; |
|
| 2235 | + | u32 rd = (ins.raw >> 7) & 0x1F; |
|
| 2236 | + | u32 rs1 = (ins.raw >> 15) & 0x1F; |
|
| 2237 | + | u64 src = funct3 >= 5 ? rs1 : cpu->regs[rs1]; |
|
| 2238 | + | bool writes = funct3 == 1 || funct3 == 5 |
|
| 2239 | + | || ((funct3 == 2 || funct3 == 3 |
|
| 2240 | + | || funct3 == 6 || funct3 == 7) && src != 0); |
|
| 2241 | + | u64 *csr = cpu_csr(cpu, csr_num); |
|
| 2242 | + | if (funct3 == 4 || !csr || cpu->privilege < ((csr_num >> 8) & 3) |
|
| 2243 | + | || (writes && ((csr_num >> 10) & 3) == 3)) { |
|
| 2244 | + | cpu_machine_trap(cpu, executed_pc, 2, ins.raw, &pc_next); |
|
| 2245 | + | break; |
|
| 2246 | + | } |
|
| 2247 | + | u64 old = *csr; |
|
| 2248 | + | switch (funct3) { |
|
| 2249 | + | case 1: case 5: *csr = src; break; |
|
| 2250 | + | case 2: case 6: if (src != 0) *csr = old | src; break; |
|
| 2251 | + | case 3: case 7: if (src != 0) *csr = old & ~src; break; |
|
| 2252 | + | default: break; |
|
| 2253 | + | } |
|
| 2254 | + | if (writes && csr_num == 0x300) { |
|
| 2255 | + | u64 mpp = (*csr >> 11) & 3; |
|
| 2256 | + | if (mpp != 0 && mpp != 3) |
|
| 2257 | + | *csr &= ~((u64)3 << 11); |
|
| 2258 | + | } else if (writes && (csr_num == 0x305 || csr_num == 0x341)) { |
|
| 2259 | + | *csr &= ~(u64)3; |
|
| 2260 | + | } |
|
| 2261 | + | if (rd != 0) |
|
| 2262 | + | cpu->regs[rd] = old; |
|
| 2263 | + | break; |
|
| 2264 | + | } |
|
| 2265 | + | if (funct12 == 0) { |
|
| 2266 | + | cpu_machine_trap( |
|
| 2267 | + | cpu, executed_pc, cpu->privilege == 0 ? 8 : 11, 0, &pc_next |
|
| 2268 | + | ); |
|
| 2269 | + | } else if (funct12 == 1) { |
|
| 2270 | + | cpu_machine_trap(cpu, executed_pc, 3, executed_pc, &pc_next); |
|
| 2271 | + | } else if (funct12 == 0x105) { |
|
| 2272 | + | cpu->running = false; |
|
| 2273 | + | } else if (funct12 == 0x302) { |
|
| 2274 | + | if (cpu->privilege != 3) { |
|
| 2275 | + | cpu_machine_trap(cpu, executed_pc, 2, ins.raw, &pc_next); |
|
| 2276 | + | break; |
|
| 2277 | + | } |
|
| 2278 | + | u8 next_priv = (cpu->mstatus >> 11) & 3; |
|
| 2279 | + | u64 mpie = (cpu->mstatus >> 7) & 1; |
|
| 2280 | + | if (next_priv != 0 && next_priv != 3) |
|
| 2281 | + | next_priv = 0; |
|
| 2282 | + | cpu->mstatus = (cpu->mstatus & ~((u64)1 << 3)) | (mpie << 3); |
|
| 2283 | + | cpu->mstatus |= (u64)1 << 7; |
|
| 2284 | + | cpu->mstatus &= ~((u64)3 << 11); |
|
| 2285 | + | cpu->privilege = next_priv; |
|
| 2286 | + | pc_next = (u32)(cpu->mepc & ~(u64)3); |
|
| 2287 | + | } else { |
|
| 2288 | + | cpu_machine_trap(cpu, executed_pc, 2, ins.raw, &pc_next); |
|
| 2289 | + | } |
|
| 2290 | + | break; |
|
| 2291 | + | } |
|
| 2292 | + | ||
| 2170 | 2293 | if (funct12 == 0) { |
|
| 2171 | 2294 | u32 syscall_num = (u32)cpu->regs[A7]; |
|
| 2172 | 2295 | ||
| 2173 | 2296 | switch (syscall_num) { |
|
| 2174 | 2297 | case 64: { /* write. */ |