kit

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

rv32_jit_test.c (10407B)


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