kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

commit 0980aa9bd5b779ecfa4c0e35d2f5c5b18a4306b6
parent 4e4c6a08c1906ea9af6dae7157b9d2b27b72e9c6
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Fri, 29 May 2026 19:17:49 -0700

emu: add interpreter execution mode (cfree emu -interp)

Run each lifted guest block through the threaded IR interpreter instead of
JITing it, behind CfreeEmuMode {JIT,INTERP} on CfreeEmuOptions (driver flag
`-interp`, forces -O1). This is doc/INTERPRETER.md Phase 4, rv64-only (the only
arch with an emu lifter).

The interp frame stays host-identity: the rv64 lifter lowers guest loads/stores
to FFI calls into the bounds-checked __emu_* helpers and its block locals are
pure host scratch, so no guest-VA translate hook or guest-stack carving is
needed (the design doc's original SP-carving plan would have corrupted host
memory and is dropped). The whole JIT/INTERP fork is one branch at
cfree_emu_step: emu_interp_run_block resets a long-lived InterpStack, seeds
param0 with the EmuThread*, resumes, and returns the scalar result as next_pc.
cfree_emu_new attaches an InterpProgram sink (capturing each block as an
InterpFunc) and binds only resolve_sym = emu_runtime_extern_resolver;
translate_block caches the InterpFunc* (payload disambiguated by e->mode). A
non-interpretable block hard-fails with its reason. Guest faults/exits are
delivered in-band by the helpers and observed by the existing post-dispatch
check. New additive engine API: cfree_interp_stack_reset / _call_args_on /
_stack_trap_reason.

cfree_interp_lookup now returns the LAST same-named capture: under self-modifying
guest code the code cache is flushed on a generation bump but interp_prog is
append-only, so a stale first-match would diverge from the JIT (last == first
for the --no-jit path). Found by an adversarial review of the diff; guarded by a
mutation-verified differential test.

test/emu/rv64_interp_smoke_test.c (make test-interp-emu) runs two guests under
both modes at -O1 and asserts identical exit codes: an SD/LD/ecall guest
(exercises the helper memory path) and a self-modifying guest (exercises
code-cache + interp-capture invalidation).

Also fix the pre-existing test-emu link break (the visibility-hidden ld -r
archive localizes internal symbols, which the test reached directly): rewrite
rv64_smoke_test.c to drive the emulator entirely through the public cfree_emu_run
API so it links the public archive as a real consumer, and split the white-box
unit tests (rv64 decoder, EmuAddrSpace, Linux syscall handler) into
rv64_vm_unit_test.c / make test-emu-unit (links the library objects).

Diffstat:
Mdoc/INTERPRETER.md | 50+++++++++++++++++++++++++++++++++++++++++++++-----
Mdriver/emu.c | 10++++++++++
Minclude/cfree/emu.h | 11+++++++++++
Minclude/cfree/interp.h | 21+++++++++++++++++++++
Msrc/emu/emu.c | 112+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/interp/engine.c | 33+++++++++++++++++++++++++++++++++
Msrc/interp/interp_program.c | 8+++++++-
Msrc/interp/interp_stubs.c | 19+++++++++++++++++++
Atest/emu/rv64_interp_smoke_test.c | 604++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/emu/rv64_smoke_test.c | 274+++++--------------------------------------------------------------------------
Atest/emu/rv64_vm_unit_test.c | 340+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/test.mk | 38+++++++++++++++++++++++++++++++++++---
12 files changed, 1252 insertions(+), 268 deletions(-)

diff --git a/doc/INTERPRETER.md b/doc/INTERPRETER.md @@ -359,16 +359,56 @@ Implemented and validated differentially against the JIT: design); wasm `N` `.wat`/`.wasm` match; C (incl. libc via FFI — f32 args, multi-register struct returns) matches; `141_threadlocal_mutate` exercises TLS define+mutate. +## Phase 4: emu interpret-mode (implemented, rv64) + +The emulator can now run each lifted guest block through the interpreter instead of JITing it, +behind `CfreeEmuMode {JIT,INTERP}` on `CfreeEmuOptions` (driver flag `cfree emu -interp`, which +forces `-O1`). The key simplification — verified against the rv64 lifter — is that **the interp +frame stays host-identity**: the rv64 lifter (`src/arch/rv64/emu.c`) lowers guest loads/stores to +FFI calls into `__emu_*_checked` host helpers that bounds/perm-check the guest VA internally, and +the lifted block's frame locals are pure host scratch passed as host out-pointers. So no guest-VA +`translate` hook, no guest-stack frame carving, and no frame-model changes are needed (the doc's +original "carve `frame_bytes` off the guest SP" plan would have corrupted host memory and was +dropped). Concretely: + +- `cfree_emu_new` attaches an `InterpProgram` as the compiler's interp sink (so each translated + block is also captured as an `InterpFunc`) and stands up one long-lived `InterpStack`; it binds + only `resolve_sym = emu_runtime_extern_resolver` (translate hook stays NULL). The per-block JIT + image is still built to resolve helper externs and validate the lifted IR. +- `translate_block` caches the captured `InterpFunc*` (payload disambiguated by `e->mode`). +- The single JIT/INTERP fork is at `cfree_emu_step`: INTERP runs `emu_interp_run_block`, which + resets the stack, seeds param0 with `(u64)EmuThread*`, resumes, and returns the scalar result as + `next_pc`. Guest faults/exits are delivered in-band by the helpers (CPU trap + `next_pc`) and + observed by the existing post-dispatch check, exactly as in JIT mode. +- Chosen UX: a block the interpreter can't run (lower-time reject or runtime trap) **hard-fails** + via `compiler_panic` with the reason — no silent JIT fallback. +- New additive engine API: `cfree_interp_stack_reset`, `cfree_interp_call_args_on`, + `cfree_interp_stack_trap_reason` (so one stack is reused across blocks without realloc). +- **Self-modifying code:** when a guest writes a translated code page the addr-space generation + bumps and the code cache is flushed, but `interp_prog` is append-only, so re-translation appends + a new same-named `InterpFunc`. `cfree_interp_lookup` therefore returns the **last** (freshest) + name match, giving INTERP the same fresh-code semantics the JIT gets from its per-block object + lookup. (`--no-jit` captures each name once, so last == first.) +- Tests: `test/emu/rv64_interp_smoke_test.c` (`make test-interp-emu`) runs two guests under both + modes at `-O1` and asserts identical exit codes — an `SD`/`LD`/`ecall` guest (exercises the + helper memory path) and a self-modifying guest (exercises code-cache + interp-capture + invalidation; verified by mutation to catch a stale-lookup regression). Scope is rv64: it is the + only arch with an emu lifter, and that lifter currently implements only + `ADDI/ADD/AUIPC/LD/SD/JALR/ECALL` (others hit a re-dispatch-this-pc default). + Not yet implemented (diagnosed → SKIP, not miscompiled): inline asm (by design; needs machinize's constraint resolution), FFI signatures beyond the register-thunk family (`vararg_on_stack` external variadics, 3+-register struct returns, 32-bit-fp struct-return fields, and an aggregate/>8-byte scalar in an external variadic-argument position — these have -no per-call ABI classification, so they are rejected rather than marshalled), thread-locals on -non-Mach-O images or via foreign/dyld descriptors (extern thread-locals), and **Phase 4 emu -integration** (interpret-mode in `emu_translate_block`). +no per-call ABI classification, so they are rejected rather than marshalled), and thread-locals on +non-Mach-O images or via foreign/dyld descriptors (extern thread-locals). Emu interpret-mode is +rv64-only (no other arch has an emu lifter) and does not reclaim stale `InterpFunc` captures under +churny self-modifying code (bounded leak until emu teardown, like the JIT's `e->jits[]`). Known limitations (correct results, not bugs): the threaded-dispatch per-site branch benefit only materializes at optimization — the `-O0`+sanitizer test build merges the dispatch sites (still computed-goto through the handler field, never a switch). The `g_mem_fault` latch is -re-checked on straight-line ops and on branch selectors; full coverage matters once emu mode -(where `translate` can fault) lands. +re-checked on straight-line ops and on branch selectors; in emu mode this latch is not exercised +by guest memory (the interp frame is host-identity and guest loads/stores go through the bounds- +checked `__emu_*` helpers, not the interp `translate` hook), so guest faults surface in-band as a +block return value rather than through `g_mem_fault`. diff --git a/driver/emu.c b/driver/emu.c @@ -25,6 +25,7 @@ typedef struct EmuOptions { size_t argv_bound; int opt_level; + int interp; /* -interp: run translated blocks through the IR interpreter */ CfreeEmuTraceFlags trace; CfreeArchKind guest_arch; int guest_arch_set; @@ -72,6 +73,9 @@ void driver_help_emu(void) { "\n" "OPTIONS\n" " -O0 -O1 -O2 Translator optimization level (default -O0)\n" + " -interp Run translated blocks through the IR " + "interpreter\n" + " instead of JITing them (forces -O1)\n" " -arch ARCH Force guest arch: aarch64 (alias arm64) or\n" " riscv64 (alias rv64). When omitted the arch is\n" " auto-detected from the executable.\n" @@ -162,6 +166,11 @@ static int emu_parse(int argc, char** argv, EmuOptions* o) { continue; } + if (driver_streq(a, "-interp")) { + o->interp = 1; + continue; + } + if (driver_streq(a, "-arch")) { if (++i >= argc) { driver_errf(EMU_TOOL, "-arch requires an argument"); @@ -309,6 +318,7 @@ int driver_emu(int argc, char** argv) { opts.has_guest_target = eo.guest_target_set != 0; opts.jit_host = &jhost; opts.optimize = eo.opt_level; + opts.mode = eo.interp ? CFREE_EMU_MODE_INTERP : CFREE_EMU_MODE_JIT; opts.trace = eo.trace; opts.argv = (const char* const*)guest_argv; opts.envp = 0; diff --git a/include/cfree/emu.h b/include/cfree/emu.h @@ -87,6 +87,16 @@ typedef struct CfreeEmuExternalBindings { void* user; } CfreeEmuExternalBindings; +/* Execution strategy for translated guest blocks. JIT lowers each block to host + * machine code and calls it; INTERP runs each block through the cfree IR + * interpreter (no executable memory needed for the guest code itself, though the + * current INTERP path still builds a per-block JIT image to resolve helper + * externs). INTERP requires CFREE_INTERP_ENABLED and forces opt_level >= 1. */ +typedef enum CfreeEmuMode { + CFREE_EMU_MODE_JIT = 0, + CFREE_EMU_MODE_INTERP = 1, +} CfreeEmuMode; + typedef struct CfreeEmuOptions { CfreeSlice guest_name; CfreeSlice guest_bytes; @@ -94,6 +104,7 @@ typedef struct CfreeEmuOptions { bool has_guest_target; const CfreeJitHost* jit_host; int optimize; + CfreeEmuMode mode; CfreeEmuTraceFlags trace; CfreeEmuExternalBindings bindings; const char* const* argv; diff --git a/include/cfree/interp.h b/include/cfree/interp.h @@ -105,4 +105,25 @@ CFREE_API CfreeInterpStatus cfree_interp_call_args(CfreeInterpProgram*, uint32_t nargs, int64_t* out_ret); +/* Reset a stack to empty so it can be reseeded and resumed again, keeping its + * allocated arenas (no realloc). Clears the frame stack, arena bump tops, the + * return shuttle, and any status/trap from a prior run. Use this to reuse one + * long-lived stack across many entries (e.g. one interp stack per emu thread, + * reset before each guest block). */ +CFREE_API CfreeStatus cfree_interp_stack_reset(CfreeInterpStack* stack); + +/* Seed an already-allocated `stack` with an entry frame for `fn`, binding + * `nargs` raw register-width values to its first parameters. Unlike + * cfree_interp_call_args this neither allocates nor frees the stack and does not + * run — follow with cfree_interp_resume. Reset the stack first if reusing it. */ +CFREE_API CfreeStatus cfree_interp_call_args_on(CfreeInterpStack* stack, + CfreeInterpFunc* fn, + const uint64_t* args, + uint32_t nargs); + +/* The reason string for the most recent non-DONE resume on `stack` — an + * unsupported operation (the function's reject reason) or a runtime trap — or + * NULL if none. Borrowed; valid until the next resume/reset. */ +CFREE_API const char* cfree_interp_stack_trap_reason(CfreeInterpStack* stack); + #endif diff --git a/src/emu/emu.c b/src/emu/emu.c @@ -10,6 +10,8 @@ #include "emu/emu.h" +#include <cfree/config.h> +#include <cfree/interp.h> #include <cfree/link.h> #include <setjmp.h> #include <string.h> @@ -42,6 +44,13 @@ struct CfreeEmu { u32 njits; u32 jits_cap; + /* Execution strategy. In INTERP mode each cache payload is an InterpFunc* + * (run via interp_prog/interp_stack) instead of a host code entry. The + * pointers stay NULL in JIT mode. */ + CfreeEmuMode mode; + CfreeInterpProgram* interp_prog; + CfreeInterpStack* interp_stack; + int done; int exit_code; }; @@ -248,6 +257,10 @@ CfreeStatus cfree_emu_new(CfreeCompiler* c, const CfreeEmuOptions* opts, memset(e, 0, sizeof(*e)); e->c = c; e->opt_level = opts->optimize; + e->mode = opts->mode; + /* The interpreter consumes the O1 PReg-path IR (opt_run_o1_interp); force at + * least -O1 so the optimizer runs and each block is captured. */ + if (e->mode == CFREE_EMU_MODE_INTERP && e->opt_level < 1) e->opt_level = 1; e->trace = opts->trace; e->bindings = opts->bindings; e->host = opts->jit_host; @@ -324,6 +337,34 @@ CfreeStatus cfree_emu_new(CfreeCompiler* c, const CfreeEmuOptions* opts, compiler_panic(c, no_loc(), "emu: failed to initialize guest CPU state"); } + /* 3. In INTERP mode, attach an interp sink so each translated block is also + * captured as an InterpFunc, and stand up the long-lived stack used to run + * blocks. The sink stays attached for the whole emu lifetime (every + * translate_block compiles a fresh block that must be captured). External + * helper calls in a lifted block resolve through the same runtime resolver + * the JIT path uses; guest memory is reached only via those helpers, so no + * address translate hook is bound (host-identity frame). */ +#if CFREE_INTERP_ENABLED + if (e->mode == CFREE_EMU_MODE_INTERP) { + CfreeInterpHost ihost; + e->interp_prog = cfree_interp_program_new((CfreeCompiler*)c); + if (!e->interp_prog) + compiler_panic(c, no_loc(), "emu: failed to create interpreter program"); + cfree_interp_program_attach(e->interp_prog, (CfreeCompiler*)c); + memset(&ihost, 0, sizeof(ihost)); + ihost.resolve_sym = emu_runtime_extern_resolver; + ihost.ctx = e; + cfree_interp_program_set_host(e->interp_prog, &ihost); + e->interp_stack = cfree_interp_stack_new(e->interp_prog); + if (!e->interp_stack) + compiler_panic(c, no_loc(), "emu: failed to create interpreter stack"); + } +#else + if (e->mode == CFREE_EMU_MODE_INTERP) + compiler_panic(c, no_loc(), + "emu: interpreter mode not enabled in this build"); +#endif + compiler_panic_restore(c, &saved); *out = e; return CFREE_OK; @@ -334,6 +375,13 @@ void cfree_emu_free(CfreeEmu* e) { if (!e) return; heap = e->c->ctx->heap; +#if CFREE_INTERP_ENABLED + /* Detach the sink before freeing the program so a later compile on the + * (borrowed) compiler can't write into freed interp state. */ + if (e->interp_prog) cfree_interp_program_attach(NULL, (CfreeCompiler*)e->c); + if (e->interp_stack) cfree_interp_stack_free(e->interp_stack); + if (e->interp_prog) cfree_interp_program_free(e->interp_prog); +#endif while (e->njits) cfree_jit_free(e->jits[--e->njits]); if (e->jits) heap->free(heap, e->jits, sizeof(*e->jits) * e->jits_cap); if (e->cache) emu_cache_free(e->cache); @@ -497,6 +545,21 @@ static void* translate_block(CfreeEmu* e, u64 guest_pc) { } emu_keep_jit(e, jit); +#if CFREE_INTERP_ENABLED + /* INTERP mode: the JIT image above still resolved the block's helper externs + * and validated the lifted IR, but dispatch runs the captured InterpFunc + * (lowered during cfree_cg_end_obj, above) instead of the host code. Cache the + * InterpFunc*; cfree_emu_step disambiguates the payload by e->mode. A rejected + * block is still captured (ifn->ok == 0) and is reported with its reason when + * dispatched, so only a genuine capture miss yields NULL here. */ + if (e->mode == CFREE_EMU_MODE_INTERP) { + CfreeInterpFunc* ifn = + cfree_interp_lookup(e->interp_prog, *(CfreeSlice*)&block_slice); + if (!ifn) return NULL; + entry = (void*)ifn; + } +#endif + emu_cache_insert(e->cache, guest_pc, entry); emu_addr_space_mark_translated(&e->process.image.addr_space, guest_pc, decode_len); @@ -540,6 +603,44 @@ void* cfree_emu_lookup(CfreeEmu* e, uint64_t guest_pc) { /* ---- Dispatcher ---- */ +#if CFREE_INTERP_ENABLED +/* Run one lifted guest block through the IR interpreter on the emu's long-lived + * stack, returning the next guest pc. The block's host function takes the + * EmuThread* and returns next_pc, so we seed param0 with the thread pointer + * (host-identity: the interpreter never translates it) and shuttle the scalar + * return back. Guest registers and memory are reached only through the __emu_* + * helpers the block calls — the interpreter holds no guest state itself. + * + * A non-DONE result means the block could not be interpreted (an op the lowerer + * rejected) or trapped inside the interpreter; per the chosen UX we hard-fail + * with the reason rather than falling back to the JIT. A guest fault/exit is NOT + * such a case: the helpers deliver those in-band by setting the CPU trap reason + * and returning a next_pc, which the post-dispatch check below observes exactly + * as in JIT mode. */ +static u64 emu_interp_run_block(CfreeEmu* e, CfreeInterpFunc* ifn, + EmuThread* thread) { + u64 arg = (u64)(uintptr_t)thread; + int64_t ret = 0; + CfreeInterpStatus s; + + cfree_interp_stack_reset(e->interp_stack); + if (cfree_interp_call_args_on(e->interp_stack, ifn, &arg, 1u) != CFREE_OK) + compiler_panic(e->c, no_loc(), "emu: failed to seed interpreter frame"); + + s = cfree_interp_resume(e->interp_stack, &ret); + if (s == CFREE_INTERP_DONE) return (u64)ret; + + { + const char* why = cfree_interp_stack_trap_reason(e->interp_stack); + compiler_panic(e->c, no_loc(), + "emu: cannot interpret block at guest_pc=0x%llx: %s", + (unsigned long long)emu_cpu_pc(thread->cpu), + why ? why : "unsupported operation"); + } + return 0; /* unreachable: compiler_panic longjmps */ +} +#endif + CfreeStatus cfree_emu_step(CfreeEmu* e, uint32_t nblocks) { PanicSave saved; uint32_t i; @@ -575,8 +676,15 @@ CfreeStatus cfree_emu_step(CfreeEmu* e, uint32_t nblocks) { (unsigned long long)pc); } - fn = (EmuBlockFn)entry; - next_pc = fn(thread); +#if CFREE_INTERP_ENABLED + if (e->mode == CFREE_EMU_MODE_INTERP) { + next_pc = emu_interp_run_block(e, (CfreeInterpFunc*)entry, thread); + } else +#endif + { + fn = (EmuBlockFn)entry; + next_pc = fn(thread); + } emu_cpu_set_pc(cpu, next_pc); trap = emu_cpu_trap_reason(cpu); diff --git a/src/interp/engine.c b/src/interp/engine.c @@ -1611,3 +1611,36 @@ CfreeInterpStatus cfree_interp_call_args(CfreeInterpProgram* pp, cfree_interp_stack_free((CfreeInterpStack*)st); return rc; } + +CfreeStatus cfree_interp_stack_reset(CfreeInterpStack* s) { + InterpStack* st = (InterpStack*)s; + if (!st) return CFREE_INVALID; + /* Keep the (fixed, non-relocating) arenas; rewind their bump tops and drop + * all frames + the return shuttle + any prior status/trap. */ + st->nframes = 0; + st->regs_top = 0; + st->mem_top = 0; + st->scalar_ret = 0; + st->ret_is_fp = 0; + st->status = CFREE_INTERP_DONE; + st->trap_reason = NULL; + g_mem_fault = 0; + return CFREE_OK; +} + +CfreeStatus cfree_interp_call_args_on(CfreeInterpStack* s, CfreeInterpFunc* ff, + const uint64_t* args, uint32_t nargs) { + InterpStack* st = (InterpStack*)s; + InterpFunc* fn = (InterpFunc*)ff; + u32 idx, i; + if (!st || !fn) return CFREE_INVALID; + idx = frame_push(st, fn); + if (idx == 0xffffffffu) return CFREE_NOMEM; + for (i = 0; i < nargs; ++i) bind_entry_param(st, fn, idx, i, args[i]); + return CFREE_OK; +} + +const char* cfree_interp_stack_trap_reason(CfreeInterpStack* s) { + InterpStack* st = (InterpStack*)s; + return st ? st->trap_reason : NULL; +} diff --git a/src/interp/interp_program.c b/src/interp/interp_program.c @@ -157,7 +157,13 @@ CfreeInterpFunc* cfree_interp_lookup(CfreeInterpProgram* pp, CfreeSlice name) { InterpProgram* p = (InterpProgram*)pp; u32 i; if (!p || !name.s) return NULL; - for (i = 0; i < p->nfuncs; ++i) + /* Newest-first: the emu re-lifts a block (same guest_pc-derived name) after + * self-modifying code bumps the addr-space generation and flushes the code + * cache. Capture is append-only, so the LAST same-named InterpFunc is the + * freshest re-translation — returning it keeps INTERP in step with the fresh + * code the JIT runs via its per-block object lookup. For the host-identity + * --no-jit path each name is captured exactly once, so last == first. */ + for (i = p->nfuncs; i-- > 0;) if (name_matches(p->funcs[i]->name, name)) return (CfreeInterpFunc*)p->funcs[i]; return NULL; diff --git a/src/interp/interp_stubs.c b/src/interp/interp_stubs.c @@ -73,3 +73,22 @@ CfreeInterpStatus cfree_interp_call_args(CfreeInterpProgram* p, (void)out_ret; return CFREE_INTERP_ERROR; } + +CfreeStatus cfree_interp_stack_reset(CfreeInterpStack* s) { + (void)s; + return CFREE_UNSUPPORTED; +} + +CfreeStatus cfree_interp_call_args_on(CfreeInterpStack* s, CfreeInterpFunc* fn, + const uint64_t* args, uint32_t nargs) { + (void)s; + (void)fn; + (void)args; + (void)nargs; + return CFREE_UNSUPPORTED; +} + +const char* cfree_interp_stack_trap_reason(CfreeInterpStack* s) { + (void)s; + return NULL; +} diff --git a/test/emu/rv64_interp_smoke_test.c b/test/emu/rv64_interp_smoke_test.c @@ -0,0 +1,604 @@ +/* Differential smoke test for the emu's INTERP execution mode (doc/INTERPRETER.md + * Phase 4). Builds a tiny static rv64 ELF whose _start computes a data-page + * address with auipc, stores a value there (SD), loads it back (LD), and exits + * via ecall with the loaded value. The SD/LD exercise the guest-memory helper + * path (__emu_*_checked) — the part a wrong "guest-VA frame" model would corrupt + * — which the addi/ecall-only smoke fixture cannot reach. + * + * The guest is run twice through the PUBLIC cfree_emu_run API, once in JIT mode + * and once in INTERP mode, BOTH at -O1, and the two exit codes are asserted + * equal (and equal to the expected memory-derived value). This isolates + * interpreter-vs-JIT semantics rather than O0-vs-O1. + * + * Self-contained: uses only the public API plus its own ELF builder and rv64 + * encoders, so it links cleanly against the (visibility-hidden) library objects + * without reaching internal emu symbols. */ + +#include <cfree/compile.h> +#include <cfree/core.h> +#include <cfree/emu.h> +#include <cfree/jit.h> +#include "emu/emu.h" +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/mman.h> +#include <unistd.h> +#if defined(__APPLE__) +#include <mach/mach.h> +#include <mach/mach_vm.h> +#define XM_DUAL_APPLE 1 +#else +#define XM_DUAL_APPLE 0 +#endif +#if defined(__linux__) +#include <sys/syscall.h> +#define XM_DUAL_LINUX 1 +#else +#define XM_DUAL_LINUX 0 +#endif + +/* Not in the public header; provided by the linked library objects. */ +EmuCPUState* emu_internal_cpu(CfreeEmu*); + +/* ---- Host heap / diag glue ---- */ +static void* h_alloc(CfreeHeap* h, size_t n, size_t a) { + (void)h; + (void)a; + return n ? malloc(n) : NULL; +} +static void* h_realloc(CfreeHeap* h, void* p, size_t o, size_t n, size_t a) { + (void)h; + (void)o; + (void)a; + return realloc(p, n); +} +static void h_free(CfreeHeap* h, void* p, size_t n) { + (void)h; + (void)n; + free(p); +} +static CfreeHeap g_heap = {h_alloc, h_realloc, h_free, NULL}; + +static void diag_emit(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc, + const char* fmt, va_list ap) { + (void)s; + (void)loc; + fprintf(stderr, "diag %d: ", (int)k); + vfprintf(stderr, fmt, ap); + fputc('\n', stderr); +} +static CfreeDiagSink g_diag = {diag_emit, NULL, 0, 0}; +static CfreeContext g_ctx; + +/* ---- Dual-mapped (W^X) exec memory for the per-block JIT image (still built + * in INTERP mode to resolve helper externs). Same shape as rv64_smoke_test. ---- */ +static int xm_to_posix(int p) { + int q = 0; + if (p & CFREE_PROT_READ) q |= PROT_READ; + if (p & CFREE_PROT_WRITE) q |= PROT_WRITE; + if (p & CFREE_PROT_EXEC) q |= PROT_EXEC; + return q; +} + +typedef struct XmTok { + void* w; + void* r; + size_t n; +} XmTok; + +static CfreeStatus xm_reserve_single(size_t n, CfreeExecMemRegion* out) { + void* m = + mmap(NULL, n, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0); + if (m == MAP_FAILED) return CFREE_NOMEM; + out->write = m; + out->runtime = m; + out->size = n; + out->token = NULL; + return CFREE_OK; +} + +static CfreeStatus xm_reserve(void* user, size_t n, int prot, + CfreeExecMemRegion* out) { + (void)user; + if (!out || !n) return CFREE_INVALID; + if (!(prot & CFREE_PROT_EXEC)) return xm_reserve_single(n, out); +#if XM_DUAL_APPLE + { + void* w = + mmap(NULL, n, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0); + mach_vm_address_t r = 0; + vm_prot_t cur = 0, max = 0; + XmTok* tok; + if (w == MAP_FAILED) return CFREE_NOMEM; + if (mach_vm_remap(mach_task_self(), &r, (mach_vm_size_t)n, 0, + VM_FLAGS_ANYWHERE, mach_task_self(), + (mach_vm_address_t)(uintptr_t)w, FALSE, &cur, &max, + VM_INHERIT_NONE) != KERN_SUCCESS) { + munmap(w, n); + return CFREE_NOMEM; + } + if (mprotect((void*)(uintptr_t)r, n, PROT_READ) != 0) { + munmap((void*)(uintptr_t)r, n); + munmap(w, n); + return CFREE_NOMEM; + } + tok = (XmTok*)malloc(sizeof(*tok)); + if (!tok) { + munmap((void*)(uintptr_t)r, n); + munmap(w, n); + return CFREE_NOMEM; + } + tok->w = w; + tok->r = (void*)(uintptr_t)r; + tok->n = n; + out->write = w; + out->runtime = (void*)(uintptr_t)r; + out->size = n; + out->token = tok; + return CFREE_OK; + } +#elif XM_DUAL_LINUX + { + int fd = (int)syscall(SYS_memfd_create, "cfree-emu-interp-test", 0u); + void *w, *r; + XmTok* tok; + if (fd < 0) return CFREE_NOMEM; + if (ftruncate(fd, (off_t)n) != 0) { + close(fd); + return CFREE_NOMEM; + } + w = mmap(NULL, n, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (w == MAP_FAILED) { + close(fd); + return CFREE_NOMEM; + } + r = mmap(NULL, n, PROT_READ, MAP_SHARED, fd, 0); + close(fd); + if (r == MAP_FAILED) { + munmap(w, n); + return CFREE_NOMEM; + } + tok = (XmTok*)malloc(sizeof(*tok)); + if (!tok) { + munmap(r, n); + munmap(w, n); + return CFREE_NOMEM; + } + tok->w = w; + tok->r = r; + tok->n = n; + out->write = w; + out->runtime = r; + out->size = n; + out->token = tok; + return CFREE_OK; + } +#else + return xm_reserve_single(n, out); +#endif +} + +static CfreeStatus xm_protect(void* user, void* addr, size_t n, int prot) { + (void)user; + return mprotect(addr, n, xm_to_posix(prot)) == 0 ? CFREE_OK : CFREE_IO; +} + +static void xm_release(void* user, CfreeExecMemRegion* r) { + (void)user; + if (!r || !r->size) return; + if (r->token) { + XmTok* tok = (XmTok*)r->token; + if (tok->r && tok->r != tok->w) munmap(tok->r, tok->n); + if (tok->w) munmap(tok->w, tok->n); + free(tok); + } else if (r->write) { + munmap(r->write, r->size); + } + memset(r, 0, sizeof(*r)); +} + +static void xm_flush(void* user, void* addr, size_t n) { + (void)user; +#if defined(__aarch64__) || defined(__arm__) || defined(__riscv) + __builtin___clear_cache((char*)addr, (char*)addr + n); +#else + (void)addr; + (void)n; +#endif +} + +static CfreeExecMem g_execmem = { + 16 * 1024, xm_reserve, xm_protect, xm_release, xm_flush, NULL, +}; + +static int g_fail; +#define EXPECT(cond, ...) \ + do { \ + if (!(cond)) { \ + ++g_fail; \ + fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \ + fprintf(stderr, __VA_ARGS__); \ + fputc('\n', stderr); \ + } \ + } while (0) + +static CfreeCompiler* new_host_compiler(void) { + CfreeTarget t; + CfreeCompiler* c = NULL; + memset(&t, 0, sizeof t); +#if defined(__x86_64__) || defined(_M_X64) + t.arch = CFREE_ARCH_X86_64; +#elif defined(__aarch64__) || defined(_M_ARM64) + t.arch = CFREE_ARCH_ARM_64; +#elif defined(__riscv) && __riscv_xlen == 64 + t.arch = CFREE_ARCH_RV64; +#else + return NULL; +#endif +#if defined(__APPLE__) + t.os = CFREE_OS_MACOS; + t.obj = CFREE_OBJ_MACHO; +#elif defined(__linux__) + t.os = CFREE_OS_LINUX; + t.obj = CFREE_OBJ_ELF; +#else + return NULL; +#endif + t.ptr_size = 8; + t.ptr_align = 8; + memset(&g_ctx, 0, sizeof g_ctx); + g_ctx.heap = &g_heap; + g_ctx.diag = &g_diag; + if (cfree_compiler_new(t, &g_ctx, &c) != CFREE_OK || !c) { + fprintf(stderr, "host compiler_new failed\n"); + exit(2); + } + return c; +} + +/* ---- Minimal rv64 ELF + instruction encoders (self-contained) ---- */ +static void put16(unsigned char* b, size_t off, unsigned v) { + b[off + 0] = (unsigned char)v; + b[off + 1] = (unsigned char)(v >> 8); +} +static void put32(unsigned char* b, size_t off, uint32_t v) { + b[off + 0] = (unsigned char)v; + b[off + 1] = (unsigned char)(v >> 8); + b[off + 2] = (unsigned char)(v >> 16); + b[off + 3] = (unsigned char)(v >> 24); +} +static void put64(unsigned char* b, size_t off, uint64_t v) { + size_t i; + for (i = 0; i < 8; ++i) b[off + i] = (unsigned char)(v >> (8 * i)); +} + +/* The rv64 emu lifter implements a minimal op set (src/arch/rv64/emu.c): + * ADDI, ADD, AUIPC, LD, SD, JALR, ECALL. The guests below use only those — + * calls go through JALR, and constants come from LD'd data, not LUI. */ +enum { + RV_ZERO = 0, + RV_RA = 1, + RV_T0 = 5, + RV_T1 = 6, + RV_T2 = 7, + RV_A0 = 10, + RV_A7 = 17 +}; + +static uint32_t enc_addi(uint32_t rd, uint32_t rs1, int32_t imm) { + return (((uint32_t)imm & 0xfffu) << 20) | (rs1 << 15) | (0x0u << 12) | + (rd << 7) | 0x13u; +} +static uint32_t enc_auipc(uint32_t rd, uint32_t imm20) { + return ((imm20 & 0xfffffu) << 12) | (rd << 7) | 0x17u; +} +static uint32_t enc_ld(uint32_t rd, uint32_t rs1, int32_t imm) { + return (((uint32_t)imm & 0xfffu) << 20) | (rs1 << 15) | (0x3u << 12) | + (rd << 7) | 0x03u; +} +static uint32_t enc_sd(uint32_t rs2, uint32_t rs1, int32_t imm) { + uint32_t u = (uint32_t)imm; + return (((u >> 5) & 0x7fu) << 25) | (rs2 << 20) | (rs1 << 15) | (0x3u << 12) | + ((u & 0x1fu) << 7) | 0x23u; +} +static uint32_t enc_jalr(uint32_t rd, uint32_t rs1, int32_t imm) { + return (((uint32_t)imm & 0xfffu) << 20) | (rs1 << 15) | (0x0u << 12) | + (rd << 7) | 0x67u; +} +static uint32_t enc_ecall(void) { return 0x00000073u; } + +/* Build a 2-segment rv64 ELF (text R+X at 0x10000, data R+W at 0x20000) whose + * _start does: SD value -> data; LD data -> a0; exit(a0). Returns a heap buffer + * (free with free()); *out_len gets the size. */ +static unsigned char* build_sd_ld_elf(size_t* out_len, unsigned value) { + enum { + PAGE = 0x1000u, + TEXT_VA = 0x10000ull, + TEXT_OFF = 0x1000u, + DATA_VA = 0x20000ull, + DATA_OFF = 0x2000u, + DATA_LEN = 0x1000u, + }; + uint64_t entry = TEXT_VA + TEXT_OFF; + int64_t delta = (int64_t)DATA_VA - (int64_t)entry; /* auipc is at entry */ + uint32_t hi20 = (uint32_t)(((uint64_t)(delta + 0x800)) >> 12) & 0xfffffu; + int32_t lo12 = (int32_t)(delta - ((int64_t)hi20 << 12)); + size_t text_len; + size_t total = DATA_OFF + DATA_LEN; + unsigned char* b = (unsigned char*)calloc(1, total); + uint32_t code[8]; + size_t n = 0; + if (!b) return NULL; + + code[n++] = enc_auipc(RV_T0, hi20); /* t0 = hi(data) */ + code[n++] = enc_addi(RV_T0, RV_T0, lo12); /* t0 = &data */ + code[n++] = enc_addi(RV_T1, RV_ZERO, (int)value); /* t1 = value */ + code[n++] = enc_sd(RV_T1, RV_T0, 0); /* [data] = t1 */ + code[n++] = enc_ld(RV_A0, RV_T0, 0); /* a0 = [data] */ + code[n++] = enc_addi(RV_A7, RV_ZERO, 94); /* a7 = exit_group */ + code[n++] = enc_ecall(); /* exit(a0) */ + text_len = n * 4u; + + /* ELF64 header. */ + b[0] = 0x7f; + b[1] = 'E'; + b[2] = 'L'; + b[3] = 'F'; + b[4] = 2; /* ELFCLASS64 */ + b[5] = 1; /* ELFDATA2LSB */ + b[6] = 1; /* EV_CURRENT */ + b[7] = 0; /* ELFOSABI_NONE */ + put16(b, 16, 2); /* e_type = ET_EXEC */ + put16(b, 18, 243); /* e_machine = EM_RISCV */ + put32(b, 20, 1); /* e_version */ + put64(b, 24, entry); /* e_entry */ + put64(b, 32, 64); /* e_phoff */ + put64(b, 40, 0); /* e_shoff */ + put32(b, 48, 0); /* e_flags */ + put16(b, 52, 64); /* e_ehsize */ + put16(b, 54, 56); /* e_phentsize */ + put16(b, 56, 2); /* e_phnum */ + put16(b, 58, 0); /* e_shentsize */ + put16(b, 60, 0); /* e_shnum */ + put16(b, 62, 0); /* e_shstrndx */ + + /* PT_LOAD #0: text, R+X, [0, TEXT_OFF+text_len) at VA TEXT_VA. */ + put32(b, 64 + 0, 1); /* p_type = PT_LOAD */ + put32(b, 64 + 4, 0x4u | 0x1u); /* p_flags = PF_R|PF_X */ + put64(b, 64 + 8, 0); /* p_offset */ + put64(b, 64 + 16, TEXT_VA); /* p_vaddr */ + put64(b, 64 + 24, TEXT_VA); /* p_paddr */ + put64(b, 64 + 32, TEXT_OFF + text_len); /* p_filesz */ + put64(b, 64 + 40, TEXT_OFF + text_len); /* p_memsz */ + put64(b, 64 + 48, PAGE); /* p_align */ + + /* PT_LOAD #1: data, R+W, [DATA_OFF, DATA_OFF+DATA_LEN) at VA DATA_VA. */ + put32(b, 120 + 0, 1); /* p_type = PT_LOAD */ + put32(b, 120 + 4, 0x4u | 0x2u); /* p_flags = PF_R|PF_W */ + put64(b, 120 + 8, DATA_OFF); /* p_offset */ + put64(b, 120 + 16, DATA_VA); /* p_vaddr */ + put64(b, 120 + 24, DATA_VA); /* p_paddr */ + put64(b, 120 + 32, DATA_LEN); /* p_filesz */ + put64(b, 120 + 40, DATA_LEN); /* p_memsz */ + put64(b, 120 + 48, PAGE); /* p_align */ + + /* .text. */ + { + size_t i; + for (i = 0; i < n; ++i) put32(b, TEXT_OFF + i * 4u, code[i]); + } + + *out_len = total; + return b; +} + +/* Build a self-modifying rv64 ELF (single R+W+X segment at 0x10000, entry + * 0x11000) to exercise the code-cache + interp-capture invalidation path. Uses + * only lifter-supported ops (calls via JALR; the replacement instruction word is + * LD'd from a data slot rather than built with LUI): + * + * entry 0x11000: auipc t0,0; addi t0,t0,0x100 # t0 = &target (0x11100) + * jalr ra, t0, 0 # 1st call -> translates target, a0=1 + * 0x1100c: auipc t1,0; addi t1,t1,0x1f4 # t1 = &patchword (0x11200) + * ld t2, 0(t1) # t2 = [addi a0,2 | jalr ra] + * sd t2, 0(t0) # overwrite target IN A TRANSLATED PAGE + * 0x1101c: jalr ra, t0, 0 # 2nd call -> must re-decode, a0=2 + * 0x11020: addi a7,zero,94; ecall # exit(a0) + * target 0x11100: addi a0,zero,1; jalr zero,ra,0 # patched in place to addi a0,zero,2 + * patch 0x11200: u64 = (jalr<<32)|addi-a0-2 # 8 bytes covering both target words + * + * The store to the already-translated code page bumps the addr-space generation + * and flushes the code cache, so the 2nd call must run the FRESH block. Both JIT + * and INTERP must exit 2; a stale interp-capture lookup would re-run the old + * block and exit 1 — the divergence this guards. */ +static unsigned char* build_smc_elf(size_t* out_len) { + enum { + PAGE = 0x1000u, + TEXT_VA = 0x10000ull, + TEXT_OFF = 0x1000u, + TARGET_OFF = 0x1100u, /* VA 0x11100 */ + PATCH_OFF = 0x1200u, /* VA 0x11200 */ + TEXT_END = 0x1208u, /* end of the 8-byte patch word */ + }; + uint64_t entry = TEXT_VA + TEXT_OFF; /* 0x11000 */ + uint64_t target_va = TEXT_VA + TARGET_OFF; /* 0x11100 */ + uint64_t patch_va = TEXT_VA + PATCH_OFF; /* 0x11200 */ + /* 8 bytes = [addi a0,zero,2 ; jalr zero,ra,0], so the SD rewrites target's + * first word (1 -> 2) and preserves its ret. */ + uint64_t patch_word = ((uint64_t)enc_jalr(RV_ZERO, RV_RA, 0) << 32) | + (uint64_t)enc_addi(RV_A0, RV_ZERO, 2); + unsigned char* b = (unsigned char*)calloc(1, TEXT_END); + if (!b) return NULL; + + /* ELF64 header. */ + b[0] = 0x7f; + b[1] = 'E'; + b[2] = 'L'; + b[3] = 'F'; + b[4] = 2; + b[5] = 1; + b[6] = 1; + b[7] = 0; + put16(b, 16, 2); /* ET_EXEC */ + put16(b, 18, 243); /* EM_RISCV */ + put32(b, 20, 1); + put64(b, 24, entry); + put64(b, 32, 64); /* e_phoff */ + put64(b, 40, 0); + put32(b, 48, 0); + put16(b, 52, 64); + put16(b, 54, 56); + put16(b, 56, 1); /* e_phnum = 1 */ + put16(b, 58, 0); + put16(b, 60, 0); + put16(b, 62, 0); + + /* One PT_LOAD: R+W+X, [0, TEXT_END) at VA TEXT_VA (writable so the guest can + * patch its own code; executable so it runs). */ + put32(b, 64 + 0, 1); /* PT_LOAD */ + put32(b, 64 + 4, 0x4u | 0x2u | 0x1u); /* PF_R|PF_W|PF_X */ + put64(b, 64 + 8, 0); /* p_offset */ + put64(b, 64 + 16, TEXT_VA); + put64(b, 64 + 24, TEXT_VA); + put64(b, 64 + 32, TEXT_END); /* p_filesz */ + put64(b, 64 + 40, TEXT_END); /* p_memsz */ + put64(b, 64 + 48, PAGE); + + /* entry: compute &target, call it, load patch word, patch, call again, exit. */ + put32(b, TEXT_OFF + 0x00, enc_auipc(RV_T0, 0)); /* t0 = 0x11000 */ + put32(b, TEXT_OFF + 0x04, + enc_addi(RV_T0, RV_T0, (int32_t)(target_va - entry))); /* t0 = &target */ + put32(b, TEXT_OFF + 0x08, enc_jalr(RV_RA, RV_T0, 0)); /* call target */ + put32(b, TEXT_OFF + 0x0c, enc_auipc(RV_T1, 0)); /* t1 = 0x1100c */ + put32(b, TEXT_OFF + 0x10, + enc_addi(RV_T1, RV_T1, (int32_t)(patch_va - (entry + 0x0c)))); /* &patch */ + put32(b, TEXT_OFF + 0x14, enc_ld(RV_T2, RV_T1, 0)); /* t2 = patch word */ + put32(b, TEXT_OFF + 0x18, enc_sd(RV_T2, RV_T0, 0)); /* patch target page */ + put32(b, TEXT_OFF + 0x1c, enc_jalr(RV_RA, RV_T0, 0)); /* call target again */ + put32(b, TEXT_OFF + 0x20, enc_addi(RV_A7, RV_ZERO, 94)); + put32(b, TEXT_OFF + 0x24, enc_ecall()); + + /* target. */ + put32(b, TARGET_OFF + 0x00, enc_addi(RV_A0, RV_ZERO, 1)); + put32(b, TARGET_OFF + 0x04, enc_jalr(RV_ZERO, RV_RA, 0)); + + /* patch word data. */ + put64(b, PATCH_OFF, patch_word); + + *out_len = TEXT_END; + return b; +} + +/* Run the guest to completion in `mode` at -O1; returns the exit code, or -1 on + * a non-OK run (and sets *ok = 0). */ +static int run_guest(const unsigned char* elf, size_t elf_len, CfreeEmuMode mode, + int* ok) { + CfreeCompiler* c = new_host_compiler(); + CfreeJitHost host; + CfreeEmuOptions opts; + CfreeTarget gt; + int exit_code = -1; + CfreeStatus st; + long ps; + + *ok = 0; + if (!c) return -1; + ps = sysconf(_SC_PAGESIZE); + if (ps > 0) g_execmem.page_size = (size_t)ps; + + memset(&host, 0, sizeof(host)); + host.execmem = &g_execmem; + memset(&gt, 0, sizeof(gt)); + gt.arch = CFREE_ARCH_RV64; + gt.os = CFREE_OS_LINUX; + gt.obj = CFREE_OBJ_ELF; + gt.ptr_size = 8u; + gt.ptr_align = 8u; + memset(&opts, 0, sizeof(opts)); + opts.guest_bytes.data = elf; + opts.guest_bytes.len = elf_len; + opts.guest_target = gt; + opts.has_guest_target = true; + opts.jit_host = &host; + opts.optimize = 1; /* both arms at -O1: isolate interp-vs-jit, not O0-vs-O1 */ + opts.mode = mode; + + /* Bounded stepping (instead of the unbounded cfree_emu_run) so a guest that + * fails to terminate surfaces as a finite failure, not a hang. */ + { + CfreeEmu* emu = NULL; + EmuCPUState* cpu; + EmuTrapReason trap = EMU_TRAP_NONE; + uint32_t i; + enum { MAX_BLOCKS = 256u }; + st = cfree_emu_new(c, &opts, &emu); + if (st != CFREE_OK || !emu) { + cfree_compiler_free(c); + return -1; + } + for (i = 0; i < MAX_BLOCKS; ++i) { + st = cfree_emu_step(emu, 1); + if (st != CFREE_OK) break; + cpu = emu_internal_cpu(emu); + trap = emu_cpu_trap_reason(cpu); + if (trap != EMU_TRAP_NONE) break; + } + cpu = emu_internal_cpu(emu); + trap = emu_cpu_trap_reason(cpu); + if (st == CFREE_OK && trap == EMU_TRAP_EXIT) { + exit_code = emu_cpu_exit_code(cpu); + *ok = 1; + } else { + fprintf(stderr, " run: st=%d trap=%d after %u blocks, pc=0x%llx\n", + (int)st, (int)trap, i, (unsigned long long)emu_cpu_pc(cpu)); + } + cfree_emu_free(emu); + } + cfree_compiler_free(c); + return exit_code; +} + +/* Run `elf` under both modes (both -O1), asserting both complete, both equal + * `expect`, and the two agree. Takes ownership of `elf` (frees it). */ +static void check_differential(const char* name, unsigned char* elf, + size_t elf_len, int expect) { + int jit_ok = 0, interp_ok = 0; + int jit_exit, interp_exit; + + if (!elf) { + EXPECT(0, "%s: ELF buffer allocation failed", name); + return; + } + jit_exit = run_guest(elf, elf_len, CFREE_EMU_MODE_JIT, &jit_ok); + interp_exit = run_guest(elf, elf_len, CFREE_EMU_MODE_INTERP, &interp_ok); + free(elf); + + EXPECT(jit_ok, "%s: JIT run did not complete cleanly", name); + EXPECT(interp_ok, "%s: INTERP run did not complete cleanly", name); + EXPECT(jit_exit == expect, "%s: JIT exit should be %d, got %d", name, expect, + jit_exit); + EXPECT(interp_exit == expect, "%s: INTERP exit should be %d, got %d", name, + expect, interp_exit); + EXPECT(jit_exit == interp_exit, + "%s: JIT vs INTERP exit codes differ: jit=%d interp=%d", name, jit_exit, + interp_exit); + if (jit_ok && interp_ok && jit_exit == expect && interp_exit == expect) + fprintf(stderr, "PASS %s (jit=%d interp=%d)\n", name, jit_exit, interp_exit); +} + +int main(void) { + size_t len = 0; + + /* (1) SD/LD/ecall: exercises the guest-memory helper path. */ + check_differential("sd-ld-ecall", build_sd_ld_elf(&len, 99u), len, 99); + + /* (2) Self-modifying code: exercises code-cache + interp-capture + * invalidation (a stale interp lookup would make INTERP diverge from JIT). */ + check_differential("self-modifying-code", build_smc_elf(&len), len, 2); + + fprintf(stderr, "interp-emu-smoke: %d failure%s\n", g_fail, + g_fail == 1 ? "" : "s"); + return g_fail ? 1 : 0; +} diff --git a/test/emu/rv64_smoke_test.c b/test/emu/rv64_smoke_test.c @@ -19,6 +19,7 @@ #include <cfree/compile.h> #include <cfree/core.h> +#include <cfree/emu.h> #include <cfree/jit.h> #include <stdio.h> #include <stdlib.h> @@ -39,14 +40,16 @@ #define XM_DUAL_LINUX 0 #endif -#include "arch/arch.h" +/* Internal headers used only for header-only helpers (no internal link symbols + * are referenced — this test drives the emulator entirely through the public + * cfree_emu_* API, so it links the public archive): rv64 instruction encoders + * (static inline) and ELF64 layout constants. The accompanying white-box unit + * tests for the decoder / address space / syscall units live in + * rv64_vm_unit_test.c, which links the library objects directly. */ #include "arch/rv64/isa.h" #include "core/core.h" -#include "emu/emu.h" #include "obj/elf/elf.h" -EmuCPUState* emu_internal_cpu(CfreeEmu*); - /* Host heap glue (same shape as test/api). */ static void* h_alloc(CfreeHeap* h, size_t n, size_t a) { (void)h; @@ -227,25 +230,6 @@ static int g_fail; } \ } while (0) -static CfreeCompiler* new_compiler(void) { - CfreeTarget t; - CfreeCompiler* c = NULL; - memset(&t, 0, sizeof t); - t.arch = CFREE_ARCH_RV64; - t.os = CFREE_OS_LINUX; - t.obj = CFREE_OBJ_ELF; - t.ptr_size = 8; - t.ptr_align = 8; - memset(&g_ctx, 0, sizeof g_ctx); - g_ctx.heap = &g_heap; - g_ctx.diag = &g_diag; - if (cfree_compiler_new(t, &g_ctx, &c) != CFREE_OK || !c) { - fprintf(stderr, "compiler_new failed\n"); - exit(2); - } - return c; -} - static CfreeCompiler* new_host_compiler(void) { CfreeTarget t; CfreeCompiler* c = NULL; @@ -1045,209 +1029,6 @@ static unsigned char* build_signal_sigreturn_elf(size_t* out_len) { * Decoder smoke (sanity-check a handful of encodings before the * end-to-end JIT run). * ============================================================ */ -static void decoder_smoke(void) { - CfreeCompiler* c = new_compiler(); - const ArchImpl* arch = arch_lookup(CFREE_ARCH_RV64); - CfreeDecodedInsn insts[8]; - CfreeStatus st; - u32 n; - unsigned char buf[16]; - put32(buf, 0, rv_addi(RV_A0, RV_ZERO, 42)); - put32(buf, 4, rv_addi(RV_A7, RV_ZERO, 94)); - put32(buf, 8, rv_ecall()); - put32(buf, 12, rv_add(RV_T0, RV_A0, RV_A1)); - EXPECT(arch && arch->decode && arch->decode->decode_block, - "rv64 ArchDecodeOps unavailable"); - if (!arch || !arch->decode || !arch->decode->decode_block) { - cfree_compiler_free(c); - return; - } - st = arch->decode->decode_block((Compiler*)c, buf, sizeof(buf), 0x10000, - insts, 8, &n); - EXPECT(st == CFREE_OK, "decode_block returned %d", (int)st); - EXPECT(n >= 3u, "decode block returned %u insts", n); - EXPECT(insts[0].opcode == RV64_DEC_ADDI, "first insn must be ADDI, got %u", - insts[0].opcode); - EXPECT(insts[0].operands[0].reg == RV_A0, "rd should be a0"); - EXPECT(insts[0].operands[2].imm == 42, "imm should be 42"); - EXPECT(insts[1].opcode == RV64_DEC_ADDI, "second insn must be ADDI"); - EXPECT(insts[1].operands[2].imm == 94, "imm should be 94"); - EXPECT(insts[2].opcode == RV64_DEC_ECALL, "third insn must be ECALL, got %u", - insts[2].opcode); - EXPECT(insts[2].flags & CFREE_DECODE_TERMINATOR, - "ECALL must be marked terminator"); - /* The block stops at ECALL; the ADD at offset 12 should not have - * been decoded. */ - EXPECT(n == 3u, "decoder must stop at the terminator (got n=%u)", n); - cfree_compiler_free(c); -} - -static void vm_unit_smoke(void) { - CfreeCompiler* c = new_host_compiler(); - EmuAddrSpace as; - u8* p; - const EmuMemFault* fault; - CfreeStatus st; - if (!c) return; - memset(&as, 0, sizeof(as)); - st = emu_addr_space_init(&as, (Compiler*)c, 0x1000u); - EXPECT(st == CFREE_OK, "vm: init returned %d", (int)st); - if (st != CFREE_OK) { - cfree_compiler_free(c); - return; - } - - st = emu_addr_space_map(&as, 0x10000u, 0x3000u, EMU_MEM_READ | EMU_MEM_WRITE, - EMU_MAP_ANON); - EXPECT(st == CFREE_OK, "vm: initial anon map returned %d", (int)st); - EXPECT(as.nmaps == 1u, "vm: expected one map, got %u", as.nmaps); - - st = emu_addr_space_map(&as, 0x11000u, 0x1000u, EMU_MEM_READ, EMU_MAP_ANON); - EXPECT(st == CFREE_INVALID, "vm: overlapping map must be rejected"); - - p = emu_addr_space_ptr(&as, 0x10ff8u, 16u, EMU_MEM_WRITE); - EXPECT(p != NULL, "vm: cross-page access inside one map should succeed"); - EXPECT(as.maps[0].dirty_pages[0] && as.maps[0].dirty_pages[1], - "vm: cross-page write should dirty both pages"); - - st = emu_addr_space_map(&as, 0x20000u, 0x1000u, 0, EMU_MAP_GUARD); - EXPECT(st == CFREE_OK, "vm: guard map returned %d", (int)st); - p = emu_addr_space_ptr(&as, 0x20000u, 1u, EMU_MEM_READ); - fault = emu_addr_space_last_fault(&as); - EXPECT(p == NULL && fault && fault->kind == EMU_FAULT_PROT, - "vm: guard read should be a protection fault"); - - st = emu_addr_space_protect(&as, 0x11000u, 0x1000u, EMU_MEM_READ); - EXPECT(st == CFREE_OK, "vm: middle-page protect returned %d", (int)st); - EXPECT(as.nmaps == 4u, "vm: protect should split maps, got %u", as.nmaps); - p = emu_addr_space_ptr(&as, 0x11000u, 1u, EMU_MEM_WRITE); - fault = emu_addr_space_last_fault(&as); - EXPECT(p == NULL && fault && fault->kind == EMU_FAULT_PROT, - "vm: write to read-only split page should fault"); - p = emu_addr_space_ptr(&as, 0x11000u, 1u, EMU_MEM_READ); - EXPECT(p != NULL, "vm: read from read-only split page should succeed"); - - st = emu_addr_space_unmap(&as, 0x11000u, 0x1000u); - EXPECT(st == CFREE_OK, "vm: middle-page unmap returned %d", (int)st); - p = emu_addr_space_ptr(&as, 0x11000u, 1u, EMU_MEM_READ); - fault = emu_addr_space_last_fault(&as); - EXPECT(p == NULL && fault && fault->kind == EMU_FAULT_UNMAPPED, - "vm: read from unmapped hole should fault as unmapped"); - - { - u64 gap = 0; - st = emu_addr_space_find_gap(&as, 0x1000u, 0x1000u, 0x10000u, 0x30000u, - &gap); - EXPECT(st == CFREE_OK && gap == 0x11000u, - "vm: find_gap should return the unmapped hole, got st=%d gap=0x%llx", - (int)st, (unsigned long long)gap); - } - - emu_addr_space_destroy(&as); - cfree_compiler_free(c); -} - -static void linux_vm_syscall_smoke(void) { - CfreeCompiler* c = new_host_compiler(); - EmuProcess process; - EmuThread thread; - EmuCPUState* cpu; - const CfreeOsImpl* os; - const ArchImpl* arch; - u64 addr; - u8* p; - const EmuMemFault* fault; - if (!c) return; - memset(&process, 0, sizeof(process)); - memset(&thread, 0, sizeof(thread)); - os = os_lookup(CFREE_OS_LINUX); - arch = arch_lookup(CFREE_ARCH_RV64); - EXPECT(os && os->emu_default_syscall, "linux vm syscall: OS hook missing"); - EXPECT(arch && arch->emu, "linux vm syscall: arch hook missing"); - if (!os || !os->emu_default_syscall || !arch || !arch->emu) { - cfree_compiler_free(c); - return; - } - process.compiler = (Compiler*)c; - process.os = os; - process.arch = arch; - process.guest_target.arch = CFREE_ARCH_RV64; - process.guest_target.os = CFREE_OS_LINUX; - process.bindings.syscall = os->emu_default_syscall; - if (os->emu_init_process_private) { - EXPECT(os->emu_init_process_private((Compiler*)c, &process) == CFREE_OK, - "linux vm syscall: process private init failed"); - } - thread.process = &process; - if (os->emu_init_thread_private) { - EXPECT(os->emu_init_thread_private((Compiler*)c, &process, &thread) == - CFREE_OK, - "linux vm syscall: thread private init failed"); - } - EXPECT(emu_addr_space_init(&process.image.addr_space, (Compiler*)c, - 0x1000u) == CFREE_OK, - "linux vm syscall: address-space init failed"); - cpu = arch->emu->cpu_new((Compiler*)c, 0, 0); - EXPECT(cpu != NULL, "linux vm syscall: cpu alloc failed"); - if (!cpu) { - if (os->emu_destroy_thread_private) - os->emu_destroy_thread_private((Compiler*)c, &thread); - if (os->emu_destroy_process_private) - os->emu_destroy_process_private((Compiler*)c, &process); - emu_addr_space_destroy(&process.image.addr_space); - cfree_compiler_free(c); - return; - } - thread.cpu = cpu; - emu_cpu_set_thread(cpu, &thread); - emu_cpu_attach_addr_space(cpu, &process.image.addr_space); - - arch->emu->set_gpr(&thread, RV_A0, 0); - arch->emu->set_gpr(&thread, RV_A1, 0x1000u); - arch->emu->set_gpr(&thread, RV_A2, 3u); - arch->emu->set_gpr(&thread, RV_A3, 0x22u); - arch->emu->set_gpr(&thread, RV_A4, ~(u64)0); - arch->emu->set_gpr(&thread, RV_A5, 0); - arch->emu->set_gpr(&thread, RV_A7, 222u); - emu_syscall(&thread); - addr = arch->emu->get_gpr(&thread, RV_A0); - EXPECT((i64)addr > 0, "linux vm syscall: mmap returned 0x%llx", - (unsigned long long)addr); - p = emu_addr_space_ptr(&process.image.addr_space, addr, 8u, EMU_MEM_WRITE); - EXPECT(p != NULL, "linux vm syscall: mmap result should be writable"); - - arch->emu->set_gpr(&thread, RV_A0, addr); - arch->emu->set_gpr(&thread, RV_A1, 0x1000u); - arch->emu->set_gpr(&thread, RV_A2, 1u); - arch->emu->set_gpr(&thread, RV_A7, 226u); - emu_syscall(&thread); - EXPECT(arch->emu->get_gpr(&thread, RV_A0) == 0, - "linux vm syscall: mprotect failed"); - p = emu_addr_space_ptr(&process.image.addr_space, addr, 8u, EMU_MEM_WRITE); - fault = emu_addr_space_last_fault(&process.image.addr_space); - EXPECT(p == NULL && fault && fault->kind == EMU_FAULT_PROT, - "linux vm syscall: mprotect should deny writes"); - - arch->emu->set_gpr(&thread, RV_A0, addr); - arch->emu->set_gpr(&thread, RV_A1, 0x1000u); - arch->emu->set_gpr(&thread, RV_A7, 215u); - emu_syscall(&thread); - EXPECT(arch->emu->get_gpr(&thread, RV_A0) == 0, - "linux vm syscall: munmap failed"); - p = emu_addr_space_ptr(&process.image.addr_space, addr, 1u, EMU_MEM_READ); - fault = emu_addr_space_last_fault(&process.image.addr_space); - EXPECT(p == NULL && fault && fault->kind == EMU_FAULT_UNMAPPED, - "linux vm syscall: munmap should remove the mapping"); - - emu_cpu_free(cpu); - if (os->emu_destroy_thread_private) - os->emu_destroy_thread_private((Compiler*)c, &thread); - if (os->emu_destroy_process_private) - os->emu_destroy_process_private((Compiler*)c, &process); - emu_addr_space_destroy(&process.image.addr_space); - cfree_compiler_free(c); -} - static void emu_fixture_expect_exit_with_bindings( const char* name, unsigned char* elf, size_t elf_len, int want_exit, uint32_t max_blocks, const CfreeEmuExternalBindings* bindings); @@ -1268,12 +1049,10 @@ static void emu_fixture_expect_exit_with_bindings( CfreeJitHost host; CfreeEmuOptions opts; CfreeTarget guest_target; - CfreeEmu* emu = NULL; - EmuCPUState* cpu; - EmuTrapReason trap; CfreeStatus st; - uint32_t i; + int exit_code = -1; long ps; + (void)max_blocks; /* cfree_emu_run drives the guest to its exit syscall */ c = new_host_compiler(); if (!c) { @@ -1299,31 +1078,15 @@ static void emu_fixture_expect_exit_with_bindings( opts.jit_host = &host; if (bindings) opts.bindings = *bindings; - st = cfree_emu_new(c, &opts, &emu); - EXPECT(st == CFREE_OK, "%s: cfree_emu_new returned %d", name, (int)st); - if (st == CFREE_OK && emu) { - trap = EMU_TRAP_NONE; - for (i = 0; i < max_blocks && trap == EMU_TRAP_NONE; ++i) { - st = cfree_emu_step(emu, 1); - EXPECT(st == CFREE_OK, "%s: cfree_emu_step returned %d at block %u", name, - (int)st, i); - if (st != CFREE_OK) break; - cpu = emu_internal_cpu(emu); - trap = emu_cpu_trap_reason(cpu); - } - cpu = emu_internal_cpu(emu); - trap = emu_cpu_trap_reason(cpu); - EXPECT(trap == EMU_TRAP_EXIT, "%s: expected exit trap within %u blocks", - name, max_blocks); - EXPECT(emu_cpu_exit_code(cpu) == want_exit, - "%s: exit_code should be %d, got %d", name, want_exit, - emu_cpu_exit_code(cpu)); - if (trap == EMU_TRAP_EXIT && emu_cpu_exit_code(cpu) == want_exit) { - fprintf(stderr, "PASS %s\n", name); - } - } + /* Public end-to-end entry: loads the guest, runs it to its exit syscall, and + * returns the code. A guest fault makes cfree_emu_run return non-OK. */ + st = cfree_emu_run(c, &opts, &exit_code); + EXPECT(st == CFREE_OK, "%s: cfree_emu_run returned %d", name, (int)st); + EXPECT(exit_code == want_exit, "%s: exit_code should be %d, got %d", name, + want_exit, exit_code); + if (st == CFREE_OK && exit_code == want_exit) + fprintf(stderr, "PASS %s\n", name); - if (emu) cfree_emu_free(emu); free(elf); cfree_compiler_free(c); } @@ -1471,7 +1234,6 @@ static void signal_sigreturn_smoke(void) { } int main(void) { - decoder_smoke(); jit_vertical_smoke(); dynamic_import_tls_red(); host_import_bridge_smoke(); @@ -1480,8 +1242,6 @@ int main(void) { signal_perms_red(); signal_load_fault_smoke(); signal_sigreturn_smoke(); - vm_unit_smoke(); - linux_vm_syscall_smoke(); if (g_fail) { fprintf(stderr, "FAILED %d check(s)\n", g_fail); return 1; diff --git a/test/emu/rv64_vm_unit_test.c b/test/emu/rv64_vm_unit_test.c @@ -0,0 +1,340 @@ +/* RV64 emulator white-box unit tests. + * + * These exercise INTERNAL units that have no public API surface — the rv64 + * decoder (ArchDecodeOps), the guest address space (EmuAddrSpace), and the + * Linux syscall handler (mmap/mprotect/munmap) — so this binary links the + * library objects directly (test.mk), unlike rv64_smoke_test.c which drives the + * emulator end-to-end through the public cfree_emu_* API and links the archive. + */ + +#include <cfree/compile.h> +#include <cfree/core.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "arch/arch.h" +#include "arch/rv64/isa.h" +#include "core/core.h" +#include "emu/emu.h" + +/* ---- Host heap / diag glue ---- */ +static void* h_alloc(CfreeHeap* h, size_t n, size_t a) { + (void)h; + (void)a; + return n ? malloc(n) : NULL; +} +static void* h_realloc(CfreeHeap* h, void* p, size_t o, size_t n, size_t a) { + (void)h; + (void)o; + (void)a; + return realloc(p, n); +} +static void h_free(CfreeHeap* h, void* p, size_t n) { + (void)h; + (void)n; + free(p); +} +static CfreeHeap g_heap = {h_alloc, h_realloc, h_free, NULL}; + +static void diag_emit(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc, + const char* fmt, va_list ap) { + (void)s; + (void)loc; + fprintf(stderr, "diag %d: ", (int)k); + vfprintf(stderr, fmt, ap); + fputc('\n', stderr); +} +static CfreeDiagSink g_diag = {diag_emit, NULL, 0, 0}; +static CfreeContext g_ctx; + +static int g_fail; +#define EXPECT(cond, ...) \ + do { \ + if (!(cond)) { \ + ++g_fail; \ + fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \ + fprintf(stderr, __VA_ARGS__); \ + fputc('\n', stderr); \ + } \ + } while (0) + +static CfreeCompiler* new_compiler(void) { + CfreeTarget t; + CfreeCompiler* c = NULL; + memset(&t, 0, sizeof t); + t.arch = CFREE_ARCH_RV64; + t.os = CFREE_OS_LINUX; + t.obj = CFREE_OBJ_ELF; + t.ptr_size = 8; + t.ptr_align = 8; + memset(&g_ctx, 0, sizeof g_ctx); + g_ctx.heap = &g_heap; + g_ctx.diag = &g_diag; + if (cfree_compiler_new(t, &g_ctx, &c) != CFREE_OK || !c) { + fprintf(stderr, "compiler_new failed\n"); + exit(2); + } + return c; +} + +static CfreeCompiler* new_host_compiler(void) { + CfreeTarget t; + CfreeCompiler* c = NULL; + memset(&t, 0, sizeof t); +#if defined(__x86_64__) || defined(_M_X64) + t.arch = CFREE_ARCH_X86_64; +#elif defined(__aarch64__) || defined(_M_ARM64) + t.arch = CFREE_ARCH_ARM_64; +#elif defined(__riscv) && __riscv_xlen == 64 + t.arch = CFREE_ARCH_RV64; +#else + return NULL; +#endif +#if defined(__APPLE__) + t.os = CFREE_OS_MACOS; + t.obj = CFREE_OBJ_MACHO; +#elif defined(__linux__) + t.os = CFREE_OS_LINUX; + t.obj = CFREE_OBJ_ELF; +#else + return NULL; +#endif + t.ptr_size = 8; + t.ptr_align = 8; + memset(&g_ctx, 0, sizeof g_ctx); + g_ctx.heap = &g_heap; + g_ctx.diag = &g_diag; + if (cfree_compiler_new(t, &g_ctx, &c) != CFREE_OK || !c) { + fprintf(stderr, "host compiler_new failed\n"); + exit(2); + } + return c; +} + +static void put32(unsigned char* b, size_t off, u32 v) { + b[off + 0] = (unsigned char)v; + b[off + 1] = (unsigned char)(v >> 8); + b[off + 2] = (unsigned char)(v >> 16); + b[off + 3] = (unsigned char)(v >> 24); +} + +/* ---- The rv64 decoder: ADDI/ECALL decode + terminator stop. ---- */ +static void decoder_smoke(void) { + CfreeCompiler* c = new_compiler(); + const ArchImpl* arch = arch_lookup(CFREE_ARCH_RV64); + CfreeDecodedInsn insts[8]; + CfreeStatus st; + u32 n; + unsigned char buf[16]; + put32(buf, 0, rv_addi(RV_A0, RV_ZERO, 42)); + put32(buf, 4, rv_addi(RV_A7, RV_ZERO, 94)); + put32(buf, 8, rv_ecall()); + put32(buf, 12, rv_add(RV_T0, RV_A0, RV_A1)); + EXPECT(arch && arch->decode && arch->decode->decode_block, + "rv64 ArchDecodeOps unavailable"); + if (!arch || !arch->decode || !arch->decode->decode_block) { + cfree_compiler_free(c); + return; + } + st = arch->decode->decode_block((Compiler*)c, buf, sizeof(buf), 0x10000, + insts, 8, &n); + EXPECT(st == CFREE_OK, "decode_block returned %d", (int)st); + EXPECT(n >= 3u, "decode block returned %u insts", n); + EXPECT(insts[0].opcode == RV64_DEC_ADDI, "first insn must be ADDI, got %u", + insts[0].opcode); + EXPECT(insts[0].operands[0].reg == RV_A0, "rd should be a0"); + EXPECT(insts[0].operands[2].imm == 42, "imm should be 42"); + EXPECT(insts[1].opcode == RV64_DEC_ADDI, "second insn must be ADDI"); + EXPECT(insts[1].operands[2].imm == 94, "imm should be 94"); + EXPECT(insts[2].opcode == RV64_DEC_ECALL, "third insn must be ECALL, got %u", + insts[2].opcode); + EXPECT(insts[2].flags & CFREE_DECODE_TERMINATOR, + "ECALL must be marked terminator"); + /* The block stops at ECALL; the ADD at offset 12 should not have + * been decoded. */ + EXPECT(n == 3u, "decoder must stop at the terminator (got n=%u)", n); + cfree_compiler_free(c); +} + +/* ---- The guest address space: map/protect/unmap/find_gap + fault kinds. ---- */ +static void vm_unit_smoke(void) { + CfreeCompiler* c = new_host_compiler(); + EmuAddrSpace as; + u8* p; + const EmuMemFault* fault; + CfreeStatus st; + if (!c) return; + memset(&as, 0, sizeof(as)); + st = emu_addr_space_init(&as, (Compiler*)c, 0x1000u); + EXPECT(st == CFREE_OK, "vm: init returned %d", (int)st); + if (st != CFREE_OK) { + cfree_compiler_free(c); + return; + } + + st = emu_addr_space_map(&as, 0x10000u, 0x3000u, EMU_MEM_READ | EMU_MEM_WRITE, + EMU_MAP_ANON); + EXPECT(st == CFREE_OK, "vm: initial anon map returned %d", (int)st); + EXPECT(as.nmaps == 1u, "vm: expected one map, got %u", as.nmaps); + + st = emu_addr_space_map(&as, 0x11000u, 0x1000u, EMU_MEM_READ, EMU_MAP_ANON); + EXPECT(st == CFREE_INVALID, "vm: overlapping map must be rejected"); + + p = emu_addr_space_ptr(&as, 0x10ff8u, 16u, EMU_MEM_WRITE); + EXPECT(p != NULL, "vm: cross-page access inside one map should succeed"); + EXPECT(as.maps[0].dirty_pages[0] && as.maps[0].dirty_pages[1], + "vm: cross-page write should dirty both pages"); + + st = emu_addr_space_map(&as, 0x20000u, 0x1000u, 0, EMU_MAP_GUARD); + EXPECT(st == CFREE_OK, "vm: guard map returned %d", (int)st); + p = emu_addr_space_ptr(&as, 0x20000u, 1u, EMU_MEM_READ); + fault = emu_addr_space_last_fault(&as); + EXPECT(p == NULL && fault && fault->kind == EMU_FAULT_PROT, + "vm: guard read should be a protection fault"); + + st = emu_addr_space_protect(&as, 0x11000u, 0x1000u, EMU_MEM_READ); + EXPECT(st == CFREE_OK, "vm: middle-page protect returned %d", (int)st); + EXPECT(as.nmaps == 4u, "vm: protect should split maps, got %u", as.nmaps); + p = emu_addr_space_ptr(&as, 0x11000u, 1u, EMU_MEM_WRITE); + fault = emu_addr_space_last_fault(&as); + EXPECT(p == NULL && fault && fault->kind == EMU_FAULT_PROT, + "vm: write to read-only split page should fault"); + p = emu_addr_space_ptr(&as, 0x11000u, 1u, EMU_MEM_READ); + EXPECT(p != NULL, "vm: read from read-only split page should succeed"); + + st = emu_addr_space_unmap(&as, 0x11000u, 0x1000u); + EXPECT(st == CFREE_OK, "vm: middle-page unmap returned %d", (int)st); + p = emu_addr_space_ptr(&as, 0x11000u, 1u, EMU_MEM_READ); + fault = emu_addr_space_last_fault(&as); + EXPECT(p == NULL && fault && fault->kind == EMU_FAULT_UNMAPPED, + "vm: read from unmapped hole should fault as unmapped"); + + { + u64 gap = 0; + st = emu_addr_space_find_gap(&as, 0x1000u, 0x1000u, 0x10000u, 0x30000u, + &gap); + EXPECT(st == CFREE_OK && gap == 0x11000u, + "vm: find_gap should return the unmapped hole, got st=%d gap=0x%llx", + (int)st, (unsigned long long)gap); + } + + emu_addr_space_destroy(&as); + cfree_compiler_free(c); +} + +/* ---- The Linux syscall handler: mmap -> mprotect -> munmap. ---- */ +static void linux_vm_syscall_smoke(void) { + CfreeCompiler* c = new_host_compiler(); + EmuProcess process; + EmuThread thread; + EmuCPUState* cpu; + const CfreeOsImpl* os; + const ArchImpl* arch; + u64 addr; + u8* p; + const EmuMemFault* fault; + if (!c) return; + memset(&process, 0, sizeof(process)); + memset(&thread, 0, sizeof(thread)); + os = os_lookup(CFREE_OS_LINUX); + arch = arch_lookup(CFREE_ARCH_RV64); + EXPECT(os && os->emu_default_syscall, "linux vm syscall: OS hook missing"); + EXPECT(arch && arch->emu, "linux vm syscall: arch hook missing"); + if (!os || !os->emu_default_syscall || !arch || !arch->emu) { + cfree_compiler_free(c); + return; + } + process.compiler = (Compiler*)c; + process.os = os; + process.arch = arch; + process.guest_target.arch = CFREE_ARCH_RV64; + process.guest_target.os = CFREE_OS_LINUX; + process.bindings.syscall = os->emu_default_syscall; + if (os->emu_init_process_private) { + EXPECT(os->emu_init_process_private((Compiler*)c, &process) == CFREE_OK, + "linux vm syscall: process private init failed"); + } + thread.process = &process; + if (os->emu_init_thread_private) { + EXPECT(os->emu_init_thread_private((Compiler*)c, &process, &thread) == + CFREE_OK, + "linux vm syscall: thread private init failed"); + } + EXPECT(emu_addr_space_init(&process.image.addr_space, (Compiler*)c, + 0x1000u) == CFREE_OK, + "linux vm syscall: address-space init failed"); + cpu = arch->emu->cpu_new((Compiler*)c, 0, 0); + EXPECT(cpu != NULL, "linux vm syscall: cpu alloc failed"); + if (!cpu) { + if (os->emu_destroy_thread_private) + os->emu_destroy_thread_private((Compiler*)c, &thread); + if (os->emu_destroy_process_private) + os->emu_destroy_process_private((Compiler*)c, &process); + emu_addr_space_destroy(&process.image.addr_space); + cfree_compiler_free(c); + return; + } + thread.cpu = cpu; + emu_cpu_set_thread(cpu, &thread); + emu_cpu_attach_addr_space(cpu, &process.image.addr_space); + + arch->emu->set_gpr(&thread, RV_A0, 0); + arch->emu->set_gpr(&thread, RV_A1, 0x1000u); + arch->emu->set_gpr(&thread, RV_A2, 3u); + arch->emu->set_gpr(&thread, RV_A3, 0x22u); + arch->emu->set_gpr(&thread, RV_A4, ~(u64)0); + arch->emu->set_gpr(&thread, RV_A5, 0); + arch->emu->set_gpr(&thread, RV_A7, 222u); + emu_syscall(&thread); + addr = arch->emu->get_gpr(&thread, RV_A0); + EXPECT((i64)addr > 0, "linux vm syscall: mmap returned 0x%llx", + (unsigned long long)addr); + p = emu_addr_space_ptr(&process.image.addr_space, addr, 8u, EMU_MEM_WRITE); + EXPECT(p != NULL, "linux vm syscall: mmap result should be writable"); + + arch->emu->set_gpr(&thread, RV_A0, addr); + arch->emu->set_gpr(&thread, RV_A1, 0x1000u); + arch->emu->set_gpr(&thread, RV_A2, 1u); + arch->emu->set_gpr(&thread, RV_A7, 226u); + emu_syscall(&thread); + EXPECT(arch->emu->get_gpr(&thread, RV_A0) == 0, + "linux vm syscall: mprotect failed"); + p = emu_addr_space_ptr(&process.image.addr_space, addr, 8u, EMU_MEM_WRITE); + fault = emu_addr_space_last_fault(&process.image.addr_space); + EXPECT(p == NULL && fault && fault->kind == EMU_FAULT_PROT, + "linux vm syscall: mprotect should deny writes"); + + arch->emu->set_gpr(&thread, RV_A0, addr); + arch->emu->set_gpr(&thread, RV_A1, 0x1000u); + arch->emu->set_gpr(&thread, RV_A7, 215u); + emu_syscall(&thread); + EXPECT(arch->emu->get_gpr(&thread, RV_A0) == 0, + "linux vm syscall: munmap failed"); + p = emu_addr_space_ptr(&process.image.addr_space, addr, 1u, EMU_MEM_READ); + fault = emu_addr_space_last_fault(&process.image.addr_space); + EXPECT(p == NULL && fault && fault->kind == EMU_FAULT_UNMAPPED, + "linux vm syscall: munmap should remove the mapping"); + + emu_cpu_free(cpu); + if (os->emu_destroy_thread_private) + os->emu_destroy_thread_private((Compiler*)c, &thread); + if (os->emu_destroy_process_private) + os->emu_destroy_process_private((Compiler*)c, &process); + emu_addr_space_destroy(&process.image.addr_space); + cfree_compiler_free(c); +} + +int main(void) { + decoder_smoke(); + vm_unit_smoke(); + linux_vm_syscall_smoke(); + if (g_fail) { + fprintf(stderr, "FAILED %d check(s)\n", g_fail); + return 1; + } + fprintf(stderr, "OK\n"); + return 0; +} diff --git a/test/test.mk b/test/test.mk @@ -63,7 +63,9 @@ TEST_TARGETS = \ test-dwarf \ test-elf \ test-emu \ + test-emu-unit \ test-interp \ + test-interp-emu \ test-interp-toy \ test-ir-recorder \ test-isa \ @@ -122,7 +124,9 @@ DEFAULT_TEST_TARGETS = \ test-rv64-jit \ test-rv64-tls-link \ test-emu \ + test-emu-unit \ test-interp \ + test-interp-emu \ test-x64-inline \ test-x64-dbg \ test-rt-headers \ @@ -284,9 +288,11 @@ $(AA64_SWEEP_GEN): test/arch/aa64_sweep_gen.c $(LIB_OBJS) @mkdir -p $(dir $@) $(CC) $(TEST_HOST_CFLAGS) -Isrc test/arch/aa64_sweep_gen.c $(LIB_OBJS) -o $@ -# test-emu: emulator unit tests. The rv64 lane builds a tiny in-memory rv64 -# ELF and asserts the lifted/JIT path exits through the syscall handler with -# the expected code. Internal arch/emu surface — needs -Isrc. +# test-emu: emulator end-to-end integration test. Builds tiny in-memory rv64 +# ELFs and runs them to their exit syscall, asserting the exit code — entirely +# through the PUBLIC cfree_emu_* API, so it links the public archive ($(LIB_AR), +# whose `ld -r` step localizes internal symbols). -Isrc is only for the +# header-only rv64 encoders / ELF constants the in-memory ELF builders use. EMU_RV64_TEST_BIN = build/test/emu_rv64_test test-emu: $(EMU_RV64_TEST_BIN) @@ -307,6 +313,18 @@ test-link-reloc-uleb128: $(RELOC_ULEB128_TEST_BIN) $(RELOC_ULEB128_TEST_BIN): test/link/reloc_uleb128_unit.c $(LIB_OBJS) @mkdir -p $(dir $@) $(CC) $(TEST_HOST_CFLAGS) -Isrc test/link/reloc_uleb128_unit.c $(LIB_OBJS) -o $@ +# test-emu-unit: white-box unit tests for the emulator's INTERNAL units (rv64 +# decoder, EmuAddrSpace, Linux syscall handler) that have no public API. Reaches +# internal symbols -> links $(LIB_OBJS) (mirrors test-interp), not the archive. +EMU_RV64_UNIT_TEST_BIN = build/test/emu_rv64_unit_test + +test-emu-unit: $(EMU_RV64_UNIT_TEST_BIN) + $(EMU_RV64_UNIT_TEST_BIN) + +$(EMU_RV64_UNIT_TEST_BIN): test/emu/rv64_vm_unit_test.c $(LIB_OBJS) + @mkdir -p $(dir $@) + $(CC) $(TEST_HOST_CFLAGS) -Isrc test/emu/rv64_vm_unit_test.c $(LIB_OBJS) -o $@ + # test-interp: threaded-bytecode interpreter unit smoke test. Builds tiny CG IR # by hand, runs opt_run_o1_interp + interp_lower + the engine, asserts the # returned value. Reaches internal opt/interp symbols -> links $(LIB_OBJS) and @@ -320,6 +338,20 @@ $(INTERP_SMOKE_TEST_BIN): test/interp/interp_smoke_test.c $(LIB_OBJS) @mkdir -p $(dir $@) $(CC) $(TEST_HOST_CFLAGS) -Isrc test/interp/interp_smoke_test.c $(LIB_OBJS) -o $@ +# test-interp-emu: differential test of the emulator's INTERP execution mode +# (doc/INTERPRETER.md Phase 4). Builds a tiny rv64 ELF with SD/LD/ecall and runs +# it through cfree_emu_run in JIT and INTERP modes (both -O1), asserting the exit +# codes match. Public-API only, but links $(LIB_OBJS) like test-interp to dodge +# the visibility-hidden archive's localized internals. +INTERP_EMU_TEST_BIN = build/test/rv64_interp_smoke_test + +test-interp-emu: $(INTERP_EMU_TEST_BIN) + $(INTERP_EMU_TEST_BIN) + +$(INTERP_EMU_TEST_BIN): test/emu/rv64_interp_smoke_test.c $(LIB_OBJS) + @mkdir -p $(dir $@) + $(CC) $(TEST_HOST_CFLAGS) -Isrc test/emu/rv64_interp_smoke_test.c $(LIB_OBJS) -o $@ + # test-interp-toy: run the toy suite's interpreter (--no-jit) path only, # asserting it matches the golden exit codes (and SKIPping unimplemented ops). test-interp-toy: bin