kit

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

emu.c (25575B)


      1 /* libkit's guest-ISA emulator: load a guest executable, translate one
      2  * basic block at a time into host code via the existing CG/MC/link
      3  * pipeline, dispatch through a code cache. See doc/EMU.md for design
      4  * and ยง6 for the incremental-link discipline.
      5  *
      6  * This file owns KitEmu lifecycle and the translate/dispatch loop.
      7  * Per-ISA decoders/lifters, CPUState synthesis, the code cache and
      8  * reserved-VA region, and the runtime helper trampolines each live
      9  * behind APIs declared in src/emu/emu.h. */
     10 
     11 #include "emu/emu.h"
     12 
     13 #include <kit/config.h>
     14 #include <kit/interp.h>
     15 #include <kit/link.h>
     16 #include <setjmp.h>
     17 #include <string.h>
     18 
     19 #include "arch/arch.h"
     20 #include "core/pool.h"
     21 #include "core/slice.h"
     22 #include "obj/format.h"
     23 #include "obj/obj.h"
     24 
     25 /* ---- Lifecycle ---- */
     26 
     27 struct KitEmu {
     28   Compiler* c;
     29   KitTargetSpec guest_target;
     30   int opt_level;
     31   KitEmuTraceFlags trace;
     32   KitEmuExternalBindings bindings;
     33 
     34   /* Borrowed JIT host (execmem + tls). When NULL, runs of this emu surface
     35    * KIT_UNSUPPORTED. */
     36   const KitJitHost* host;
     37 
     38   EmuProcess process;
     39   EmuThread main_thread;
     40 
     41   EmuCodeCache* cache;
     42   u64 cache_generation;
     43   KitJit** jits;
     44   u32 njits;
     45   u32 jits_cap;
     46 
     47   /* Execution strategy. In INTERP mode each cache payload is an InterpFunc*
     48    * (run via interp_prog/interp_stack) instead of a host code entry. The
     49    * pointers stay NULL in JIT mode. */
     50   KitEmuMode mode;
     51   KitInterpProgram* interp_prog;
     52   KitInterpStack* interp_stack;
     53 
     54   int done;
     55   int exit_code;
     56 };
     57 
     58 /* The block function call ABI: u64 entry(EmuThread*). Cast through
     59  * a typedef so the call site reads cleanly in the dispatcher. */
     60 typedef u64 (*EmuBlockFn)(EmuThread*);
     61 
     62 typedef struct EmuResolvedConfig {
     63   KitTargetSpec target;
     64   const ObjFormatImpl* obj_format;
     65   const ArchImpl* arch;
     66   const KitOsImpl* os;
     67 } EmuResolvedConfig;
     68 
     69 static EmuCPUState* emu_main_cpu(KitEmu* e) {
     70   return e ? e->main_thread.cpu : NULL;
     71 }
     72 
     73 static KitStatus emu_public_syscall_adapter(void* user, EmuProcess* process,
     74                                             EmuThread* thread,
     75                                             const EmuSyscallRequest* req,
     76                                             EmuSyscallResult* out) {
     77   KitEmu* e = (KitEmu*)user;
     78   KitEmuSyscallRequest public_req;
     79   KitEmuSyscallResult public_out;
     80   KitStatus st;
     81   u32 i;
     82   (void)process;
     83   (void)thread;
     84   if (!e || !e->bindings.syscall || !req || !out) return KIT_INVALID;
     85   memset(&public_req, 0, sizeof(public_req));
     86   public_req.number = req->number;
     87   for (i = 0; i < 6u; ++i) public_req.args[i] = req->args[i];
     88   memset(&public_out, 0, sizeof(public_out));
     89   st = e->bindings.syscall(e->bindings.user, e, &public_req, &public_out);
     90   if (st != KIT_OK) return st;
     91   memset(out, 0, sizeof(*out));
     92   out->result = public_out.result;
     93   out->guest_errno = public_out.guest_errno;
     94   out->flags = public_out.flags;
     95   return KIT_OK;
     96 }
     97 
     98 static KitStatus emu_public_import_adapter(void* user, EmuProcess* process,
     99                                            const EmuDynamicImport* req,
    100                                            KitEmuResolvedImport* out) {
    101   KitEmu* e = (KitEmu*)user;
    102   KitEmuImportRequest public_req;
    103   (void)process;
    104   if (!e || !e->bindings.resolve_import || !req || !out) return KIT_INVALID;
    105   memset(&public_req, 0, sizeof(public_req));
    106   public_req.object_name = req->object_name;
    107   public_req.symbol_name = req->symbol_name;
    108   public_req.signature = req->signature;
    109   return e->bindings.resolve_import(e->bindings.user, e, &public_req, out);
    110 }
    111 
    112 static KitStatus emu_public_object_adapter(void* user, EmuProcess* process,
    113                                            KitSlice object_name,
    114                                            KitSlice* out_bytes) {
    115   KitEmu* e = (KitEmu*)user;
    116   KitEmuObjectRequest public_req;
    117   KitEmuResolvedObject public_out;
    118   KitStatus st;
    119   (void)process;
    120   if (!e || !e->bindings.resolve_object || !out_bytes) return KIT_INVALID;
    121   memset(&public_req, 0, sizeof(public_req));
    122   public_req.object_name = object_name;
    123   memset(&public_out, 0, sizeof(public_out));
    124   st =
    125       e->bindings.resolve_object(e->bindings.user, e, &public_req, &public_out);
    126   if (st != KIT_OK) return st;
    127   *out_bytes = public_out.object_bytes;
    128   return public_out.object_bytes.data ? KIT_OK : KIT_NOT_FOUND;
    129 }
    130 
    131 void emu_set_jit_host(KitEmu* e, const KitJitHost* host) {
    132   if (!e) return;
    133   e->host = host;
    134 }
    135 
    136 const KitJitHost* emu_get_jit_host(const KitEmu* e) {
    137   return e ? e->host : NULL;
    138 }
    139 
    140 KitStatus emu_process_os_alloc(Compiler* c, EmuProcess* process, size_t size,
    141                                size_t align) {
    142   Heap* heap;
    143   if (!c || !process || !size || process->os_private) return KIT_INVALID;
    144   heap = c->ctx->heap;
    145   process->os_private = heap->alloc(heap, size, align ? align : 1u);
    146   if (!process->os_private) return KIT_NOMEM;
    147   memset(process->os_private, 0, size);
    148   return KIT_OK;
    149 }
    150 
    151 void emu_process_os_free(Compiler* c, EmuProcess* process, size_t size) {
    152   Heap* heap;
    153   if (!c || !process || !process->os_private) return;
    154   heap = c->ctx->heap;
    155   heap->free(heap, process->os_private, size);
    156   process->os_private = NULL;
    157 }
    158 
    159 KitStatus emu_thread_os_alloc(Compiler* c, EmuThread* thread, size_t size,
    160                               size_t align) {
    161   Heap* heap;
    162   if (!c || !thread || !size || thread->os_private) return KIT_INVALID;
    163   heap = c->ctx->heap;
    164   thread->os_private = heap->alloc(heap, size, align ? align : 1u);
    165   if (!thread->os_private) return KIT_NOMEM;
    166   memset(thread->os_private, 0, size);
    167   return KIT_OK;
    168 }
    169 
    170 void emu_thread_os_free(Compiler* c, EmuThread* thread, size_t size) {
    171   Heap* heap;
    172   if (!c || !thread || !thread->os_private) return;
    173   heap = c->ctx->heap;
    174   heap->free(heap, thread->os_private, size);
    175   thread->os_private = NULL;
    176 }
    177 
    178 static KitStatus emu_resolve_config(Compiler* c, const KitEmuOptions* opts,
    179                                     EmuResolvedConfig* out) {
    180   KitBinFmt bin_fmt;
    181   KitTargetSpec target;
    182   const ObjFormatImpl* obj_format;
    183   const ObjFormatImpl* target_format;
    184   const ArchImpl* arch;
    185   const KitOsImpl* os;
    186   KitStatus st;
    187 
    188   if (!c || !opts || !out || !opts->guest_bytes.data ||
    189       opts->guest_bytes.len == 0)
    190     return KIT_INVALID;
    191   memset(out, 0, sizeof(*out));
    192 
    193   bin_fmt = kit_detect_fmt(opts->guest_bytes.data, opts->guest_bytes.len);
    194   obj_format = obj_format_lookup_bin(bin_fmt);
    195   if (!obj_format || !obj_format->emu || !obj_format->emu->load_executable)
    196     return KIT_UNSUPPORTED;
    197 
    198   if (opts->has_guest_target) {
    199     target = opts->guest_target;
    200   } else {
    201     if (!obj_format->emu->detect_executable) return KIT_UNSUPPORTED;
    202     memset(&target, 0, sizeof(target));
    203     st = obj_format->emu->detect_executable(c, opts->guest_bytes, &target);
    204     if (st != KIT_OK) return st;
    205   }
    206 
    207   target_format = obj_format_lookup(target.obj);
    208   if (target_format != obj_format) return KIT_UNSUPPORTED;
    209 
    210   arch = arch_lookup(target.arch);
    211   os = os_lookup(target.os);
    212   if (!arch || !arch->decode || !arch->decode->decode_block || !arch->emu ||
    213       !arch->emu->cpu_new || !arch->emu->block_fn_type ||
    214       !arch->emu->lift_block || !os) {
    215     return KIT_UNSUPPORTED;
    216   }
    217 
    218   out->target = target;
    219   out->obj_format = obj_format;
    220   out->arch = arch;
    221   out->os = os;
    222   return KIT_OK;
    223 }
    224 
    225 KitStatus kit_emu_new(KitCompiler* c, const KitEmuOptions* opts, KitEmu** out) {
    226   PanicFrame panic;
    227   Heap* heap;
    228   KitEmu* e;
    229   EmuResolvedConfig resolved;
    230   EmuLoadOptions load_opts;
    231   KitStatus st;
    232 
    233   if (out) *out = NULL;
    234   if (!c || !opts || !out) return KIT_INVALID;
    235   if (!opts->guest_bytes.data || opts->guest_bytes.len == 0) return KIT_INVALID;
    236 
    237   compiler_panic_push(c, &panic);
    238   if (setjmp(panic.env)) {
    239     compiler_run_cleanups(c);
    240     compiler_panic_pop(c, &panic);
    241     return KIT_ERR;
    242   }
    243 
    244   heap = c->ctx->heap;
    245   e = (KitEmu*)heap->alloc(heap, sizeof(*e), _Alignof(KitEmu));
    246   if (!e) compiler_panic(c, SRCLOC_NONE, "emu: out of memory");
    247   memset(e, 0, sizeof(*e));
    248   e->c = c;
    249   e->opt_level = opts->optimize;
    250   e->mode = opts->mode;
    251   /* The interpreter consumes the O1 PReg-path IR (opt_run_o1_interp); force at
    252    * least -O1 so the optimizer runs and each block is captured. */
    253   if (e->mode == KIT_EMU_MODE_INTERP && e->opt_level < 1) e->opt_level = 1;
    254   e->trace = opts->trace;
    255   e->bindings = opts->bindings;
    256   e->host = opts->jit_host;
    257 
    258   st = emu_resolve_config(c, opts, &resolved);
    259   if (st != KIT_OK) {
    260     compiler_panic(c, SRCLOC_NONE, "emu: unsupported guest executable");
    261   }
    262   e->guest_target = resolved.target;
    263 
    264   memset(&load_opts, 0, sizeof(load_opts));
    265   load_opts.name = opts->guest_name;
    266   load_opts.bytes = opts->guest_bytes;
    267   load_opts.guest_target = resolved.target;
    268   load_opts.argv = opts->argv;
    269   load_opts.envp = opts->envp;
    270   load_opts.os = resolved.os;
    271   load_opts.process = &e->process;
    272 
    273   e->process.compiler = c;
    274   e->process.guest_target = resolved.target;
    275   e->process.obj_format = resolved.obj_format;
    276   e->process.arch = resolved.arch;
    277   e->process.os = resolved.os;
    278   if (e->bindings.syscall) {
    279     e->process.bindings.syscall = emu_public_syscall_adapter;
    280     e->process.bindings.user = e;
    281   } else {
    282     e->process.bindings.syscall = resolved.os->emu_default_syscall;
    283     e->process.bindings.user = NULL;
    284   }
    285   if (e->bindings.resolve_import) {
    286     e->process.bindings.resolve_import = emu_public_import_adapter;
    287     e->process.bindings.user = e;
    288   }
    289   if (e->bindings.resolve_object) {
    290     e->process.bindings.resolve_object = emu_public_object_adapter;
    291     e->process.bindings.user = e;
    292   }
    293   load_opts.bindings = &e->process.bindings;
    294   e->main_thread.process = &e->process;
    295 
    296   if (resolved.os->emu_init_process_private &&
    297       resolved.os->emu_init_process_private(c, &e->process) != KIT_OK) {
    298     compiler_panic(c, SRCLOC_NONE, "emu: failed to initialize OS process state");
    299   }
    300   if (resolved.os->emu_init_thread_private &&
    301       resolved.os->emu_init_thread_private(c, &e->process, &e->main_thread) !=
    302           KIT_OK) {
    303     compiler_panic(c, SRCLOC_NONE, "emu: failed to initialize OS thread state");
    304   }
    305 
    306   /* 1. Load the guest executable through the object-format emu hook. */
    307   st = resolved.obj_format->emu->load_executable(c, &load_opts,
    308                                                  &e->process.image);
    309   if (st != KIT_OK) {
    310     compiler_panic(c, SRCLOC_NONE, "emu: failed to load guest executable");
    311   }
    312   if (resolved.os->emu_init_process &&
    313       resolved.os->emu_init_process(c, &e->process, &load_opts,
    314                                     &e->process.image) != KIT_OK) {
    315     compiler_panic(c, SRCLOC_NONE, "emu: failed to initialize guest process");
    316   }
    317 
    318   /* 2. Allocate per-thread CPU state and seed PC/SP. */
    319   e->main_thread.cpu = resolved.arch->emu->cpu_new(c, e->process.image.entry_pc,
    320                                                    e->process.image.initial_sp);
    321   emu_cpu_set_thread(e->main_thread.cpu, &e->main_thread);
    322   if (!e->main_thread.cpu ||
    323       emu_loaded_image_attach_cpu(e->main_thread.cpu, &e->process.image) != 0 ||
    324       (resolved.os->emu_init_thread &&
    325        resolved.os->emu_init_thread(c, &e->process, &e->main_thread) !=
    326            KIT_OK)) {
    327     compiler_panic(c, SRCLOC_NONE, "emu: failed to initialize guest CPU state");
    328   }
    329 
    330   /* 3. In INTERP mode, attach an interp sink so each translated block is also
    331    * captured as an InterpFunc, and stand up the long-lived stack used to run
    332    * blocks. The sink stays attached for the whole emu lifetime (every
    333    * translate_block compiles a fresh block that must be captured). External
    334    * helper calls in a lifted block resolve through the same runtime resolver
    335    * the JIT path uses; guest memory is reached only via those helpers, so no
    336    * address translate hook is bound (host-identity frame). */
    337 #if KIT_INTERP_ENABLED
    338   if (e->mode == KIT_EMU_MODE_INTERP) {
    339     KitInterpHost ihost;
    340     e->interp_prog = kit_interp_program_new((KitCompiler*)c);
    341     if (!e->interp_prog)
    342       compiler_panic(c, SRCLOC_NONE, "emu: failed to create interpreter program");
    343     kit_interp_program_attach(e->interp_prog, (KitCompiler*)c);
    344     memset(&ihost, 0, sizeof(ihost));
    345     ihost.resolve_sym = emu_runtime_extern_resolver;
    346     ihost.ctx = e;
    347     kit_interp_program_set_host(e->interp_prog, &ihost);
    348     e->interp_stack = kit_interp_stack_new(e->interp_prog);
    349     if (!e->interp_stack)
    350       compiler_panic(c, SRCLOC_NONE, "emu: failed to create interpreter stack");
    351   }
    352 #else
    353   if (e->mode == KIT_EMU_MODE_INTERP)
    354     compiler_panic(c, SRCLOC_NONE,
    355                    "emu: interpreter mode not enabled in this build");
    356 #endif
    357 
    358   compiler_panic_pop(c, &panic);
    359   *out = e;
    360   return KIT_OK;
    361 }
    362 
    363 void kit_emu_free(KitEmu* e) {
    364   Heap* heap;
    365   if (!e) return;
    366   heap = e->c->ctx->heap;
    367 
    368 #if KIT_INTERP_ENABLED
    369   /* Detach the sink before freeing the program so a later compile on the
    370    * (borrowed) compiler can't write into freed interp state. */
    371   if (e->interp_prog) kit_interp_program_attach(NULL, (KitCompiler*)e->c);
    372   if (e->interp_stack) kit_interp_stack_free(e->interp_stack);
    373   if (e->interp_prog) kit_interp_program_free(e->interp_prog);
    374 #endif
    375   while (e->njits) kit_jit_free(e->jits[--e->njits]);
    376   if (e->jits) heap->free(heap, e->jits, sizeof(*e->jits) * e->jits_cap);
    377   if (e->cache) emu_cache_free(e->cache);
    378   if (e->process.os && e->process.os->emu_destroy_thread_private)
    379     e->process.os->emu_destroy_thread_private(e->c, &e->main_thread);
    380   if (e->main_thread.cpu) emu_cpu_free(e->main_thread.cpu);
    381   emu_tls_destroy_process(e->c, &e->process);
    382   if (e->process.os && e->process.os->emu_destroy_process_private)
    383     e->process.os->emu_destroy_process_private(e->c, &e->process);
    384   emu_unload_image(e->c, &e->process.image);
    385 
    386   heap->free(heap, e, sizeof(*e));
    387 }
    388 
    389 /* Lazily allocate the code cache the first time kit_emu_lookup runs.
    390  * Requires a wired JIT host because cold blocks are published as ordinary
    391  * one-block JIT images. */
    392 static KitStatus ensure_runtime(KitEmu* e) {
    393   if (e->cache) return KIT_OK;
    394   if (!e->host || !e->host->execmem) return KIT_UNSUPPORTED;
    395   e->cache = emu_cache_new(e->c);
    396   return KIT_OK;
    397 }
    398 
    399 static void emu_keep_jit(KitEmu* e, KitJit* jit) {
    400   Heap* heap = e->c->ctx->heap;
    401   if (e->njits == e->jits_cap) {
    402     u32 old_cap = e->jits_cap;
    403     u32 new_cap = old_cap ? old_cap * 2u : 8u;
    404     KitJit** grown =
    405         (KitJit**)heap->realloc(heap, e->jits, sizeof(*e->jits) * old_cap,
    406                                 sizeof(*e->jits) * new_cap, _Alignof(KitJit*));
    407     if (!grown) compiler_panic(e->c, SRCLOC_NONE, "emu: out of memory");
    408     e->jits = grown;
    409     e->jits_cap = new_cap;
    410   }
    411   e->jits[e->njits++] = jit;
    412 }
    413 
    414 /* ---- Translation (cold-miss path) ---- */
    415 
    416 static void* translate_block(KitEmu* e, u64 guest_pc) {
    417   KitDecodedInsn* insts;
    418   const ArchImpl* arch;
    419   Heap* heap;
    420   const u8* host_pc;
    421   u64 va_end;
    422   size_t decode_len;
    423   u32 ninsts;
    424   ObjBuilder* ob;
    425   KitCg* cg;
    426   KitCodeOptions copts;
    427   KitCgUnitOptions unit_opts;
    428   Sym block_name;
    429   KitCgDecl block_decl;
    430   KitCgSym block_sym;
    431   EmuLiftCtx ctx;
    432   void* entry;
    433   KitStatus st;
    434   KitLinkSessionOptions lopts;
    435   KitLinkSession* sess;
    436   KitJit* jit;
    437   KitSlice block_slice;
    438 
    439   if (e->trace & KIT_EMU_TRACE_BLOCK) emu_trace_block(e->c, guest_pc);
    440 
    441   arch = e->process.arch;
    442   if (!arch || !arch->decode || !arch->decode->decode_block || !arch->emu ||
    443       !arch->emu->block_fn_type || !arch->emu->lift_block)
    444     return NULL;
    445 
    446   host_pc = emu_cpu_va_to_host_perm(emu_main_cpu(e), guest_pc,
    447                                     arch->decode->min_insn_len, EMU_MEM_EXEC);
    448   if (!host_pc) return NULL;
    449   va_end = emu_addr_space_contig_len(&e->process.image.addr_space, guest_pc,
    450                                      EMU_MEM_EXEC);
    451   if (!va_end) return NULL;
    452   decode_len = (size_t)va_end;
    453   heap = e->c->ctx->heap;
    454   insts = (KitDecodedInsn*)heap->alloc(
    455       heap, sizeof(*insts) * EMU_MAX_INSTS_PER_BLOCK, _Alignof(KitDecodedInsn));
    456   if (!insts) compiler_panic(e->c, SRCLOC_NONE, "emu: out of memory");
    457   st = arch->decode->decode_block(e->c, host_pc, decode_len, guest_pc, insts,
    458                                   EMU_MAX_INSTS_PER_BLOCK, &ninsts);
    459   if (st != KIT_OK || ninsts == 0) {
    460     heap->free(heap, insts, sizeof(*insts) * EMU_MAX_INSTS_PER_BLOCK);
    461     return NULL;
    462   }
    463 
    464   if (e->trace & KIT_EMU_TRACE_INSN) {
    465     u32 j;
    466     for (j = 0; j < ninsts; ++j) emu_trace_insn(e->c, guest_pc, &insts[j]);
    467   }
    468 
    469   /* Per-block ObjBuilder + public CG pipeline. The block lands as a single
    470    * host function once per-ISA lifters start emitting real code. */
    471   ob = obj_new(e->c);
    472   memset(&copts, 0, sizeof(copts));
    473   copts.opt_level = e->opt_level;
    474   st = kit_cg_new(e->c, &cg);
    475   if (st == KIT_OK) st = kit_cg_begin(cg, (KitObjBuilder*)ob, &copts);
    476   memset(&unit_opts, 0, sizeof unit_opts);
    477   unit_opts.source_name = KIT_SLICE_LIT("<emu-block>");
    478   if (st == KIT_OK) st = kit_cg_begin_unit(cg, &unit_opts);
    479   if (st != KIT_OK || !cg)
    480     compiler_panic(e->c, SRCLOC_NONE, "emu: kit_cg_new failed");
    481 
    482   block_name = emu_block_sym_name(e->c, guest_pc);
    483   memset(&block_decl, 0, sizeof(block_decl));
    484   block_decl.kind = KIT_CG_DECL_FUNC;
    485   block_decl.linkage_name =
    486       kit_cg_c_linkage_name((KitCompiler*)e->c, (KitSym)block_name);
    487   block_decl.display_name = (KitSym)block_name;
    488   block_decl.type = arch->emu->block_fn_type(e->c);
    489   block_decl.sym.bind = KIT_SB_GLOBAL;
    490   block_decl.sym.visibility = KIT_CG_VIS_DEFAULT;
    491   block_sym = kit_cg_decl(cg, block_decl);
    492   if (block_sym == KIT_CG_SYM_NONE)
    493     compiler_panic(e->c, SRCLOC_NONE, "emu: failed to declare block symbol");
    494 
    495   memset(&ctx, 0, sizeof(ctx));
    496   ctx.compiler = e->c;
    497   ctx.arch = e->guest_target.arch;
    498   ctx.thread_type = emu_thread_type(e->c);
    499   ctx.block_fn_type = arch->emu->block_fn_type(e->c);
    500   ctx.block_sym = block_sym;
    501   ctx.guest_pc = guest_pc;
    502 
    503   st = arch->emu->lift_block(e->c, cg, insts, ninsts, &ctx);
    504   heap->free(heap, insts, sizeof(*insts) * EMU_MAX_INSTS_PER_BLOCK);
    505   insts = NULL;
    506   if (st != KIT_OK) compiler_panic(e->c, SRCLOC_NONE, "emu: failed to lift block");
    507 
    508   st = kit_cg_end_unit(cg);
    509   if (st == KIT_OK) st = kit_cg_finish(cg, NULL);
    510   if (st == KIT_OK) st = kit_cg_detach(cg);
    511   if (st != KIT_OK)
    512     compiler_panic(e->c, SRCLOC_NONE, "emu: kit_cg_finish failed");
    513   kit_cg_free(cg);
    514   obj_finalize(ob);
    515 
    516   block_slice = pool_slice(e->c->global, block_name);
    517   memset(&lopts, 0, sizeof(lopts));
    518   lopts.output_kind = KIT_LINK_OUTPUT_JIT;
    519   lopts.jit_host = e->host;
    520   lopts.extern_resolver = emu_runtime_extern_resolver;
    521   lopts.extern_resolver_user = e;
    522 
    523   sess = NULL;
    524   jit = NULL;
    525   st = kit_link_session_new((KitCompiler*)e->c, &lopts, &sess);
    526   if (st == KIT_OK) st = kit_link_session_add_obj(sess, (KitObjBuilder*)ob);
    527   if (st == KIT_OK) st = kit_link_session_jit(sess, &jit);
    528   if (sess) kit_link_session_free(sess);
    529   if (st != KIT_OK || !jit)
    530     compiler_panic(e->c, SRCLOC_NONE, "emu: failed to publish JIT block");
    531 
    532   entry = kit_jit_lookup(jit, *(KitSlice*)&block_slice);
    533   if (!entry) {
    534     kit_jit_free(jit);
    535     return NULL;
    536   }
    537   emu_keep_jit(e, jit);
    538 
    539 #if KIT_INTERP_ENABLED
    540   /* INTERP mode: the JIT image above still resolved the block's helper externs
    541    * and validated the lifted IR, but dispatch runs the captured InterpFunc
    542    * (lowered during kit_cg_finish, above) instead of the host code. Cache the
    543    * InterpFunc*; kit_emu_step disambiguates the payload by e->mode. A rejected
    544    * block is still captured (ifn->ok == 0) and is reported with its reason when
    545    * dispatched, so only a genuine capture miss yields NULL here. */
    546   if (e->mode == KIT_EMU_MODE_INTERP) {
    547     KitInterpFunc* ifn =
    548         kit_interp_lookup(e->interp_prog, *(KitSlice*)&block_slice);
    549     if (!ifn) return NULL;
    550     entry = (void*)ifn;
    551   }
    552 #endif
    553 
    554   emu_cache_insert(e->cache, guest_pc, entry);
    555   emu_addr_space_mark_translated(&e->process.image.addr_space, guest_pc,
    556                                  decode_len);
    557   e->cache_generation = e->process.image.addr_space.generation;
    558   return entry;
    559 }
    560 
    561 void* kit_emu_lookup(KitEmu* e, uint64_t guest_pc) {
    562   PanicFrame panic;
    563   void* entry;
    564 
    565   if (!e) return NULL;
    566 
    567   if (e->cache &&
    568       e->cache_generation != e->process.image.addr_space.generation) {
    569     emu_cache_free(e->cache);
    570     e->cache = NULL;
    571     e->cache_generation = 0;
    572   }
    573 
    574   /* Cache hit short-circuits the panic boundary. */
    575   if (e->cache) {
    576     entry = emu_cache_lookup(e->cache, guest_pc);
    577     if (entry) return entry;
    578   }
    579 
    580   if (ensure_runtime(e) != KIT_OK) return NULL;
    581 
    582   compiler_panic_push(e->c, &panic);
    583   if (setjmp(panic.env)) {
    584     compiler_run_cleanups(e->c);
    585     compiler_panic_pop(e->c, &panic);
    586     return NULL;
    587   }
    588 
    589   entry = translate_block(e, guest_pc);
    590 
    591   compiler_panic_pop(e->c, &panic);
    592   return entry;
    593 }
    594 
    595 /* ---- Dispatcher ---- */
    596 
    597 #if KIT_INTERP_ENABLED
    598 /* Run one lifted guest block through the IR interpreter on the emu's long-lived
    599  * stack, returning the next guest pc. The block's host function takes the
    600  * EmuThread* and returns next_pc, so we seed param0 with the thread pointer
    601  * (host-identity: the interpreter never translates it) and shuttle the scalar
    602  * return back. Guest registers and memory are reached only through the __emu_*
    603  * helpers the block calls โ€” the interpreter holds no guest state itself.
    604  *
    605  * A non-DONE result means the block could not be interpreted (an op the lowerer
    606  * rejected) or trapped inside the interpreter; per the chosen UX we hard-fail
    607  * with the reason rather than falling back to the JIT. A guest fault/exit is
    608  * NOT such a case: the helpers deliver those in-band by setting the CPU trap
    609  * reason and returning a next_pc, which the post-dispatch check below observes
    610  * exactly as in JIT mode. */
    611 static u64 emu_interp_run_block(KitEmu* e, KitInterpFunc* ifn,
    612                                 EmuThread* thread) {
    613   u64 arg = (u64)(uintptr_t)thread;
    614   int64_t ret = 0;
    615   KitInterpStatus s;
    616 
    617   kit_interp_stack_reset(e->interp_stack);
    618   if (kit_interp_call_args_on(e->interp_stack, ifn, &arg, 1u) != KIT_OK)
    619     compiler_panic(e->c, SRCLOC_NONE, "emu: failed to seed interpreter frame");
    620 
    621   s = kit_interp_resume(e->interp_stack, &ret);
    622   if (s == KIT_INTERP_DONE) return (u64)ret;
    623 
    624   {
    625     const char* why = kit_interp_stack_trap_reason(e->interp_stack);
    626     compiler_panic(e->c, SRCLOC_NONE,
    627                    "emu: cannot interpret block at guest_pc=0x%llx: %s",
    628                    (unsigned long long)emu_cpu_pc(thread->cpu),
    629                    why ? why : "unsupported operation");
    630   }
    631   return 0; /* unreachable: compiler_panic longjmps */
    632 }
    633 #endif
    634 
    635 KitStatus kit_emu_step(KitEmu* e, uint32_t nblocks) {
    636   PanicFrame panic;
    637   uint32_t i;
    638   KitStatus st;
    639 
    640   if (!e) return KIT_INVALID;
    641   if (e->done) return KIT_OK;
    642   st = ensure_runtime(e);
    643   if (st != KIT_OK) return st;
    644 
    645   compiler_panic_push(e->c, &panic);
    646   if (setjmp(panic.env)) {
    647     compiler_run_cleanups(e->c);
    648     compiler_panic_pop(e->c, &panic);
    649     return KIT_ERR;
    650   }
    651 
    652   for (i = 0; i < nblocks && !e->done; ++i) {
    653     EmuThread* thread = &e->main_thread;
    654     EmuCPUState* cpu = thread->cpu;
    655     u64 pc = emu_cpu_pc(cpu);
    656     void* entry;
    657     EmuBlockFn fn;
    658     u64 next_pc;
    659     EmuTrapReason trap;
    660 
    661     if (e->trace & KIT_EMU_TRACE_PC) emu_trace_pc(e->c, pc);
    662 
    663     entry = kit_emu_lookup(e, pc);
    664     if (!entry) {
    665       compiler_panic(e->c, SRCLOC_NONE,
    666                      "emu: failed to translate block at guest_pc=0x%llx",
    667                      (unsigned long long)pc);
    668     }
    669 
    670 #if KIT_INTERP_ENABLED
    671     if (e->mode == KIT_EMU_MODE_INTERP) {
    672       next_pc = emu_interp_run_block(e, (KitInterpFunc*)entry, thread);
    673     } else
    674 #endif
    675     {
    676       fn = (EmuBlockFn)entry;
    677       next_pc = fn(thread);
    678     }
    679     emu_cpu_set_pc(cpu, next_pc);
    680 
    681     trap = emu_cpu_trap_reason(cpu);
    682     if (trap == EMU_TRAP_EXIT) {
    683       e->done = 1;
    684       e->exit_code = emu_cpu_exit_code(cpu);
    685     } else if (trap == EMU_TRAP_FAULT) {
    686       compiler_panic(e->c, SRCLOC_NONE, "emu: guest faulted at pc=0x%llx",
    687                      (unsigned long long)next_pc);
    688     }
    689   }
    690 
    691   compiler_panic_pop(e->c, &panic);
    692   return KIT_OK;
    693 }
    694 
    695 KitStatus kit_emu_run(KitCompiler* c, const KitEmuOptions* opts,
    696                       int* out_exit_code) {
    697   KitEmu* e = NULL;
    698   KitStatus st;
    699 
    700   if (out_exit_code) *out_exit_code = 0;
    701   if (!c || !opts) return KIT_INVALID;
    702 
    703   st = kit_emu_new(c, opts, &e);
    704   if (st != KIT_OK) return st;
    705 
    706   while (!e->done) {
    707     st = kit_emu_step(e, 1024);
    708     if (st != KIT_OK) break;
    709   }
    710 
    711   if (st == KIT_OK && out_exit_code) *out_exit_code = e->exit_code;
    712   kit_emu_free(e);
    713   return st;
    714 }
    715 
    716 /* Runtime accessor for the resolver โ€” exposes the running emu's
    717  * CPUState pointer without baking the KitEmu layout into runtime.c.
    718  * Used by emu_runtime_extern_resolver for EMU_SYM_CPU_STATE. */
    719 EmuCPUState* emu_internal_cpu(KitEmu* e) { return emu_main_cpu(e); }
    720 
    721 EmuProcess* emu_internal_process(KitEmu* e) { return e ? &e->process : NULL; }
    722 
    723 /* ---- Block symbol naming ----
    724  * "emu_block_<16-hex-pc>" โ€” fixed-width hex so the linker's hash
    725  * lookup never collides between two blocks at distinct guest PCs.
    726  * Interned in the compiler's global pool; the Sym is stable for the
    727  * Compiler's lifetime, which is what the linker assumes. */
    728 Sym emu_block_sym_name(Compiler* c, u64 guest_pc) {
    729   char buf[32];
    730   static const char hex[] = "0123456789abcdef";
    731   int i;
    732   /* "emu_block_" + 16 hex digits + NUL = 27 chars, fits in 32. */
    733   memcpy(buf, "emu_block_", 10);
    734   for (i = 0; i < 16; ++i) {
    735     buf[10 + 15 - i] = hex[guest_pc & 0xfu];
    736     guest_pc >>= 4;
    737   }
    738   buf[26] = '\0';
    739   return pool_intern_slice(c->global, slice_from_cstr(buf));
    740 }