kit

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

rv32_jit_test.c (10404B)


      1 /* RV32 JIT smoke test.
      2  *
      3  * Builds a tiny ELF relocatable object in memory for rv32 containing
      4  * one function:
      5  *
      6  *   .text
      7  *   .globl rv32_jit_answer
      8  *   rv32_jit_answer:
      9  *     addi a0, zero, 42      # 0x02a00513
     10  *     jalr zero, ra, 0       # 0x00008067  (ret)
     11  *
     12  * Feeds it through kit_link_session in KIT_LINK_OUTPUT_JIT mode,
     13  * which exercises the rv32 path of:
     14  *   - executable-memory reservation + W^X protect cycle
     15  *   - relocation application (none needed here, but the path runs)
     16  *   - symbol resolution / lookup by C-mangled name
     17  *   - icache flush (fence.i / __builtin___clear_cache on riscv hosts)
     18  *
     19  * If we are running on a rv32 host, the test then *calls* the JITed
     20  * function and asserts the return is 42 — that's the native-host
     21  * execution leg the parity checklist asked for.  On non-rv32 hosts
     22  * we still build the image (verifying the in-memory machinery is wired
     23  * end-to-end) but SKIP the actual call: the bytes are valid rv32 but
     24  * the host CPU can't decode them.  The test prints "SKIP <reason>" and
     25  * exits 77 (the GNU autotools "skipped" convention) when this happens.
     26  *
     27  * Wired into mk/test.mk via test-rv32-jit.  Always builds; calls only on
     28  * rv32 hosts.  This mirrors the rv64 JIT smoke test (test/link/rv64_jit_test.c):
     29  * have the code path in place for the day someone runs kit on a rv32 dev box.
     30  *
     31  * The two base-ISA encodings are identical on rv32 and rv64 (they are RV32I
     32  * instructions reused unchanged by RV64I), so the encoding constants are the
     33  * same as the rv64 sibling test. */
     34 
     35 #include <kit/core.h>
     36 #include <kit/jit.h>
     37 #include <kit/link.h>
     38 #include <kit/object.h>
     39 #include <stdarg.h>
     40 #include <stdint.h>
     41 #include <stdio.h>
     42 #include <stdlib.h>
     43 #include <string.h>
     44 #include <sys/mman.h>
     45 #include <unistd.h>
     46 
     47 #include "lib/kit_unit.h"
     48 
     49 /* Native execution requires the host CPU to be rv32 (any OS that gives
     50  * us POSIX mmap + mprotect, which on rv32 means Linux today).  Anywhere
     51  * else we still build the JIT image but skip the call. */
     52 #if defined(__riscv) && (__riscv_xlen == 32)
     53 #define RV32_HOST_NATIVE 1
     54 #else
     55 #define RV32_HOST_NATIVE 0
     56 #endif
     57 
     58 /* ---- host glue: heap + diag come from the shared KitUnit ---- */
     59 static KitUnit g_u;
     60 
     61 /* ---- execmem with W^X dual-mapping (mirrors test/link/harness) ---- */
     62 static int xm_to_posix(int p) {
     63   int q = 0;
     64   if (p & KIT_PROT_READ) q |= PROT_READ;
     65   if (p & KIT_PROT_WRITE) q |= PROT_WRITE;
     66   if (p & KIT_PROT_EXEC) q |= PROT_EXEC;
     67   return q;
     68 }
     69 
     70 #if defined(__linux__)
     71 #include <sys/syscall.h>
     72 #define XM_DUAL_LINUX 1
     73 #else
     74 #define XM_DUAL_LINUX 0
     75 #endif
     76 
     77 typedef struct XmTok {
     78   void* w;
     79   void* r;
     80   size_t n;
     81 } XmTok;
     82 
     83 static KitStatus xm_reserve_single(size_t n, KitExecMemRegion* out) {
     84   void* p =
     85       mmap(NULL, n, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
     86   if (p == MAP_FAILED) return KIT_NOMEM;
     87   out->write = out->runtime = p;
     88   out->size = n;
     89   out->token = NULL;
     90   return KIT_OK;
     91 }
     92 
     93 static KitStatus xm_reserve(void* u, size_t n, int p, KitExecMemRegion* out) {
     94   (void)u;
     95   if (!out || !n) return KIT_INVALID;
     96   if (!(p & KIT_PROT_EXEC)) return xm_reserve_single(n, out);
     97 #if XM_DUAL_LINUX
     98   {
     99     int fd = (int)syscall(SYS_memfd_create, "kit-rv32-jit-test", 0u);
    100     void *w, *r;
    101     XmTok* tok;
    102     if (fd < 0) return KIT_NOMEM;
    103     if (ftruncate(fd, (off_t)n) != 0) {
    104       close(fd);
    105       return KIT_NOMEM;
    106     }
    107     w = mmap(NULL, n, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    108     if (w == MAP_FAILED) {
    109       close(fd);
    110       return KIT_NOMEM;
    111     }
    112     r = mmap(NULL, n, PROT_READ, MAP_SHARED, fd, 0);
    113     close(fd);
    114     if (r == MAP_FAILED) {
    115       munmap(w, n);
    116       return KIT_NOMEM;
    117     }
    118     tok = (XmTok*)malloc(sizeof(*tok));
    119     if (!tok) {
    120       munmap(r, n);
    121       munmap(w, n);
    122       return KIT_NOMEM;
    123     }
    124     tok->w = w;
    125     tok->r = r;
    126     tok->n = n;
    127     out->write = w;
    128     out->runtime = r;
    129     out->size = n;
    130     out->token = tok;
    131     return KIT_OK;
    132   }
    133 #else
    134   return xm_reserve_single(n, out);
    135 #endif
    136 }
    137 
    138 static KitStatus xm_protect(void* u, void* a, size_t n, int p) {
    139   (void)u;
    140   return mprotect(a, n, xm_to_posix(p)) == 0 ? KIT_OK : KIT_IO;
    141 }
    142 
    143 static void xm_release(void* u, KitExecMemRegion* region) {
    144   (void)u;
    145   if (!region || !region->size) return;
    146   if (region->token) {
    147     XmTok* tok = (XmTok*)region->token;
    148     if (tok->r && tok->r != tok->w) munmap(tok->r, tok->n);
    149     if (tok->w) munmap(tok->w, tok->n);
    150     free(tok);
    151   } else if (region->write) {
    152     munmap(region->write, region->size);
    153   }
    154   region->write = region->runtime = NULL;
    155   region->size = 0;
    156   region->token = NULL;
    157 }
    158 
    159 static void xm_flush(void* u, void* a, size_t n) {
    160   (void)u;
    161 #if defined(__aarch64__) || defined(__arm__) || defined(__riscv)
    162 #if defined(__riscv)
    163   /* Local-hart self-modify ordering; __builtin___clear_cache below also
    164    * issues the cross-hart syscall on Linux. */
    165   __asm__ __volatile__("fence.i" ::: "memory");
    166 #endif
    167   __builtin___clear_cache((char*)a, (char*)a + n);
    168 #else
    169   (void)a;
    170   (void)n;
    171 #endif
    172 }
    173 
    174 static KitExecMem g_execmem = {
    175     16 * 1024, xm_reserve, xm_protect, xm_release, xm_flush, NULL,
    176 };
    177 
    178 /* ---- rv32 instruction encodings used by the test ---- */
    179 /* These are RV32I base-ISA instructions, byte-identical to the rv64 forms. */
    180 /* `addi a0, zero, 42` — I-type: imm[11:0]=42, rs1=0, funct3=000 (ADDI),
    181  *  rd=10 (a0), opcode=0010011. */
    182 #define ENC_ADDI_A0_ZERO_42 0x02a00513u
    183 /* `jalr zero, 0(ra)` (= ret) — I-type: imm=0, rs1=1 (ra), funct3=000,
    184  *  rd=0 (zero), opcode=1100111. */
    185 #define ENC_RET 0x00008067u
    186 
    187 /* ---- the test ---- */
    188 typedef int (*answer_fn)(void);
    189 
    190 int main(void) {
    191   /* Page size for the execmem.  Same dance as the other runners. */
    192   {
    193     long ps = sysconf(_SC_PAGESIZE);
    194     if (ps > 0) g_execmem.page_size = (size_t)ps;
    195   }
    196 
    197   KitTargetSpec target =
    198       kit_unit_target(KIT_ARCH_RV32, KIT_OS_LINUX, KIT_OBJ_ELF);
    199   /* kit_unit_target hardcodes ptr_size/ptr_align=8 for the rv64-era suite;
    200    * rv32 is ILP32 and the ELF class (ELFCLASS32 vs 64) is selected from
    201    * ptr_size, so narrow them here. */
    202   target.ptr_size = 4;
    203   target.ptr_align = 4;
    204 
    205   kit_unit_init(&g_u);
    206   g_u.ctx.now = -1;
    207 
    208   KitCompiler* c = NULL;
    209   if (kit_unit_compiler_new(&g_u, target, &c) != KIT_OK || !c) {
    210     fprintf(stderr, "rv32_jit_test: compiler_new failed\n");
    211     return 2;
    212   }
    213 
    214   /* Build the object. */
    215   KitObjBuilder* ob = NULL;
    216   if (kit_obj_builder_new(c, &ob) != KIT_OK || !ob) {
    217     fprintf(stderr, "rv32_jit_test: obj_builder_new failed\n");
    218     kit_compiler_free(c);
    219     return 2;
    220   }
    221 
    222   KitObjSectionDesc sec_desc;
    223   memset(&sec_desc, 0, sizeof(sec_desc));
    224   sec_desc.name = kit_sym_intern(c, KIT_SLICE_LIT(".text"));
    225   sec_desc.kind = KIT_SEC_TEXT;
    226   sec_desc.flags = KIT_SF_EXEC | KIT_SF_ALLOC;
    227   sec_desc.align = 4;
    228   KitObjSection text = KIT_SECTION_NONE;
    229   if (kit_obj_builder_section(ob, &sec_desc, &text) != KIT_OK) {
    230     fprintf(stderr, "rv32_jit_test: section failed\n");
    231     return 2;
    232   }
    233 
    234   uint32_t code[2] = {ENC_ADDI_A0_ZERO_42, ENC_RET};
    235   if (kit_obj_builder_write(ob, text, code, sizeof(code)) != KIT_OK) {
    236     fprintf(stderr, "rv32_jit_test: write failed\n");
    237     return 2;
    238   }
    239 
    240   KitObjSymbolDesc sym_desc;
    241   memset(&sym_desc, 0, sizeof(sym_desc));
    242   sym_desc.name = kit_sym_intern(c, KIT_SLICE_LIT("rv32_jit_answer"));
    243   sym_desc.bind = KIT_SB_GLOBAL;
    244   sym_desc.kind = KIT_SK_FUNC;
    245   sym_desc.section = text;
    246   sym_desc.value = 0;
    247   sym_desc.size = sizeof(code);
    248   KitObjSymbol sym = KIT_OBJ_SYMBOL_NONE;
    249   if (kit_obj_builder_symbol(ob, &sym_desc, &sym) != KIT_OK) {
    250     fprintf(stderr, "rv32_jit_test: symbol failed\n");
    251     return 2;
    252   }
    253 
    254   if (kit_obj_builder_finalize(ob) != KIT_OK) {
    255     fprintf(stderr, "rv32_jit_test: finalize failed\n");
    256     return 2;
    257   }
    258 
    259   /* JIT the object.  The host's execmem is the W^X dual-map above. */
    260   KitJitHost jhost;
    261   memset(&jhost, 0, sizeof(jhost));
    262   jhost.execmem = &g_execmem;
    263 
    264   KitLinkSessionOptions opts;
    265   memset(&opts, 0, sizeof(opts));
    266   opts.output_kind = KIT_LINK_OUTPUT_JIT;
    267   opts.entry = KIT_SLICE_LIT("rv32_jit_answer");
    268   opts.jit_host = &jhost;
    269 
    270   KitLinkSession* sess = NULL;
    271   if (kit_link_session_new(c, &opts, &sess) != KIT_OK || !sess) {
    272     fprintf(stderr, "rv32_jit_test: link_session_new failed\n");
    273     return 1;
    274   }
    275   if (kit_link_session_add_obj(sess, ob) != KIT_OK) {
    276     fprintf(stderr, "rv32_jit_test: add_obj failed\n");
    277     kit_link_session_free(sess);
    278     return 1;
    279   }
    280 
    281   KitJit* jit = NULL;
    282   if (kit_link_session_jit(sess, &jit) != KIT_OK || !jit) {
    283     fprintf(stderr, "rv32_jit_test: link_session_jit failed\n");
    284     kit_link_session_free(sess);
    285     return 1;
    286   }
    287   kit_link_session_free(sess);
    288 
    289   void* fn = kit_jit_lookup(jit, KIT_SLICE_LIT("rv32_jit_answer"));
    290   if (!fn) {
    291     fprintf(stderr, "rv32_jit_test: lookup failed\n");
    292     kit_jit_free(jit);
    293     kit_compiler_free(c);
    294     return 1;
    295   }
    296 
    297   /* Reading back the first instruction bytes through the runtime alias
    298    * is always safe and verifies the bytes survived the W^X dance plus
    299    * the icache-flush hook fired without crashing.  This is the portable
    300    * check on non-rv32 hosts. */
    301   uint32_t got = 0;
    302   memcpy(&got, fn, sizeof(got));
    303   if (got != ENC_ADDI_A0_ZERO_42) {
    304     fprintf(stderr,
    305             "rv32_jit_test: bytes corrupted at runtime alias: got 0x%08x "
    306             "expected 0x%08x\n",
    307             (unsigned)got, (unsigned)ENC_ADDI_A0_ZERO_42);
    308     kit_jit_free(jit);
    309     kit_compiler_free(c);
    310     return 1;
    311   }
    312 
    313 #if RV32_HOST_NATIVE
    314   /* Real execution on a rv32 host. */
    315   {
    316     answer_fn f = (answer_fn)(uintptr_t)fn;
    317     int r = f();
    318     if (r != 42) {
    319       fprintf(stderr, "rv32_jit_test: jit fn returned %d, expected 42\n", r);
    320       kit_jit_free(jit);
    321       kit_compiler_free(c);
    322       return 1;
    323     }
    324     printf("rv32_jit_test: PASS (native rv32 execution returned 42)\n");
    325   }
    326 #else
    327   /* Non-rv32 host: JIT plumbing worked end-to-end (image built,
    328    * permissions flipped, lookup resolved, bytes intact at the runtime
    329    * alias).  Skip the actual call — calling rv32 bytes on a non-rv32
    330    * CPU would SIGILL.  Exit-code 77 is the GNU autotools convention
    331    * for "skipped" so test wrappers can distinguish from pass/fail. */
    332   printf(
    333       "rv32_jit_test: SKIP — non-rv32 host (image built, "
    334       "lookup OK, bytes intact)\n");
    335   kit_jit_free(jit);
    336   kit_compiler_free(c);
    337   return 77;
    338 #endif
    339 
    340   kit_jit_free(jit);
    341   kit_compiler_free(c);
    342   return 0;
    343 }