kit

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

pe-import-mingw.c (12031B)


      1 /* PE import-directory smoke test using a real mingw archive.
      2  *
      3  * Counterpart to pe-import-smoke.c, which exercises the Microsoft
      4  * short-import format (Sig1=0/Sig2=0xFFFF). Mingw archives use the
      5  * long-form layout instead — every member is a regular long-form COFF
      6  * `.o` file with `.idata$N` sections — so this test drives the
      7  * long-form absorption path in link_add_archive_bytes (per-function
      8  * stubs reclassified as DSO shims at archive-ingest time, head/trailer
      9  * members dropped).
     10  *
     11  * Skips cleanly when the mingw toolchain isn't installed.
     12  *
     13  * Verification: assemble the target program against the archive, write
     14  * a PE32+ to /tmp, then probe with x86_64-w64-mingw32-objdump -p and
     15  * assert (a) the import directory has KERNEL32.dll, (b) ExitProcess is
     16  * the only resolved import. */
     17 
     18 #include <kit/core.h>
     19 #include <kit/link.h>
     20 #include <kit/object.h>
     21 #include <setjmp.h>
     22 #include <stdarg.h>
     23 #include <stdio.h>
     24 #include <stdlib.h>
     25 #include <string.h>
     26 #include <unistd.h>
     27 
     28 #include "core/core.h"
     29 #include "core/pool.h"
     30 #include "link/link.h"
     31 #include "obj/obj.h"
     32 
     33 #define MINGW_ARCHIVE_PATH                                               \
     34   "/opt/homebrew/opt/mingw-w64/toolchain-x86_64/x86_64-w64-mingw32/lib/" \
     35   "libkernel32.a"
     36 
     37 /* mingw's archive declares the DLL name in uppercase. */
     38 #define MINGW_DLL_NAME "KERNEL32.dll"
     39 /* Whichever case derive_dll_name_from_archive_path picks; matched
     40  * case-insensitively in objdump output below. */
     41 #define MINGW_IMPORT_FN "ExitProcess"
     42 
     43 /* The exit-process program: e8 disp32 c3 (call ExitProcess; ret). */
     44 static const uint8_t PROG_TEXT_X64[6] = {0xe8, 0, 0, 0, 0, 0xc3};
     45 
     46 /* ---- env vtables --------------------------------------------------- */
     47 
     48 static void* heap_alloc(KitHeap* h, size_t n, size_t a) {
     49   (void)h;
     50   (void)a;
     51   return n ? malloc(n) : NULL;
     52 }
     53 static void* heap_realloc(KitHeap* h, void* p, size_t o, size_t n, size_t a) {
     54   (void)h;
     55   (void)o;
     56   (void)a;
     57   return realloc(p, n);
     58 }
     59 static void heap_free(KitHeap* h, void* p, size_t n) {
     60   (void)h;
     61   (void)n;
     62   free(p);
     63 }
     64 static KitHeap g_heap = {heap_alloc, heap_realloc, heap_free, NULL};
     65 
     66 static void diag_emit(KitDiagSink* s, KitDiagKind k, KitSrcLoc loc,
     67                       const char* fmt, va_list ap) {
     68   static const char* names[] = {"note", "warning", "error", "fatal"};
     69   (void)s;
     70   (void)loc;
     71   fprintf(stderr, "%s: ", names[k]);
     72   vfprintf(stderr, fmt, ap);
     73   fputc('\n', stderr);
     74 }
     75 static KitDiagSink g_diag = {diag_emit, NULL, 0, 0};
     76 
     77 /* ---- failure tracking --------------------------------------------- */
     78 
     79 static int g_failures;
     80 #define EXPECT(cond, ...)                                  \
     81   do {                                                     \
     82     if (!(cond)) {                                         \
     83       fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \
     84       fprintf(stderr, __VA_ARGS__);                        \
     85       fputc('\n', stderr);                                 \
     86       g_failures++;                                        \
     87     }                                                      \
     88   } while (0)
     89 
     90 /* ---- target / compiler ------------------------------------------- */
     91 
     92 static KitContext g_ctx;
     93 
     94 static void target_x64_windows(KitTargetSpec* t) {
     95   memset(t, 0, sizeof *t);
     96   t->arch = KIT_ARCH_X86_64;
     97   t->os = KIT_OS_WINDOWS;
     98   t->obj = KIT_OBJ_COFF;
     99   t->ptr_size = 8;
    100   t->ptr_align = 8;
    101   t->big_endian = false;
    102   t->pic = KIT_PIC_PIE;
    103   t->code_model = KIT_CM_SMALL;
    104 }
    105 
    106 static Compiler* make_compiler(const KitTargetSpec* t) {
    107   memset(&g_ctx, 0, sizeof g_ctx);
    108   g_ctx.heap = &g_heap;
    109   g_ctx.diag = &g_diag;
    110   g_ctx.now = -1;
    111   KitTargetOptions opts;
    112   memset(&opts, 0, sizeof opts);
    113   opts.spec = *t;
    114   KitTarget* target = NULL;
    115   if (kit_target_new(&g_ctx, &opts, &target) != KIT_OK || !target) return NULL;
    116   KitCompiler* cc = NULL;
    117   if (kit_compiler_new(target, &g_ctx, &cc) != KIT_OK || !cc) {
    118     kit_target_free(target);
    119     return NULL;
    120   }
    121   return (Compiler*)cc;
    122 }
    123 
    124 static void free_compiler(Compiler* c) {
    125   if (!c) return;
    126   const KitTarget* target = kit_compiler_target((KitCompiler*)c);
    127   kit_compiler_free((KitCompiler*)c);
    128   kit_target_free((KitTarget*)target);
    129 }
    130 
    131 /* ---- program ObjBuilder builder ----------------------------------- */
    132 
    133 static ObjBuilder* build_program(Compiler* c) {
    134   ObjBuilder* ob = obj_new(c);
    135   Pool* p = c->global;
    136   Sym text_name = pool_intern_slice(p, SLICE_LIT(".text"));
    137   Sym main_name = pool_intern_slice(p, SLICE_LIT("mainCRTStartup"));
    138   Sym exit_name = pool_intern_slice(p, SLICE_LIT(MINGW_IMPORT_FN));
    139   ObjSecId text = obj_section(ob, text_name, SEC_TEXT, SF_ALLOC | SF_EXEC, 16);
    140   obj_write(ob, text, PROG_TEXT_X64, sizeof PROG_TEXT_X64);
    141   obj_symbol(ob, main_name, SB_GLOBAL, SK_FUNC, text, 0, sizeof PROG_TEXT_X64);
    142   ObjSymId exit_sym =
    143       obj_symbol(ob, exit_name, SB_GLOBAL, SK_UNDEF, OBJ_SEC_NONE, 0, 0);
    144   obj_reloc(ob, text, 1, R_PC32, exit_sym, -4);
    145   obj_finalize(ob);
    146   return ob;
    147 }
    148 
    149 /* ---- archive / objdump helpers ------------------------------------ */
    150 
    151 static int have_mingw_objdump(void) {
    152   FILE* fp = popen("command -v x86_64-w64-mingw32-objdump 2>/dev/null", "r");
    153   if (!fp) return 0;
    154   char buf[256];
    155   size_t n = fread(buf, 1, sizeof buf - 1, fp);
    156   pclose(fp);
    157   return n > 0;
    158 }
    159 
    160 static uint8_t* slurp_file(const char* path, size_t* len_out) {
    161   FILE* fp = fopen(path, "rb");
    162   if (!fp) return NULL;
    163   if (fseek(fp, 0, SEEK_END) != 0) {
    164     fclose(fp);
    165     return NULL;
    166   }
    167   long sz = ftell(fp);
    168   if (sz < 0) {
    169     fclose(fp);
    170     return NULL;
    171   }
    172   rewind(fp);
    173   uint8_t* buf = (uint8_t*)malloc((size_t)sz);
    174   if (!buf) {
    175     fclose(fp);
    176     return NULL;
    177   }
    178   size_t got = fread(buf, 1, (size_t)sz, fp);
    179   fclose(fp);
    180   if (got != (size_t)sz) {
    181     free(buf);
    182     return NULL;
    183   }
    184   *len_out = (size_t)sz;
    185   return buf;
    186 }
    187 
    188 static char* slurp_cmd(const char* cmd) {
    189   FILE* fp = popen(cmd, "r");
    190   if (!fp) return NULL;
    191   size_t cap = 4096, len = 0;
    192   char* buf = (char*)malloc(cap);
    193   if (!buf) {
    194     pclose(fp);
    195     return NULL;
    196   }
    197   for (;;) {
    198     if (len + 1024 + 1 > cap) {
    199       cap *= 2;
    200       char* nb = (char*)realloc(buf, cap);
    201       if (!nb) {
    202         free(buf);
    203         pclose(fp);
    204         return NULL;
    205       }
    206       buf = nb;
    207     }
    208     size_t got = fread(buf + len, 1, 1024, fp);
    209     len += got;
    210     if (got < 1024) break;
    211   }
    212   int rc = pclose(fp);
    213   (void)rc;
    214   buf[len] = '\0';
    215   return buf;
    216 }
    217 
    218 static int contains_ci(const char* haystack, const char* needle) {
    219   size_t nn = strlen(needle);
    220   for (const char* p = haystack; *p; ++p) {
    221     size_t i = 0;
    222     while (i < nn) {
    223       int hc = (unsigned char)p[i];
    224       int nc = (unsigned char)needle[i];
    225       if (hc >= 'A' && hc <= 'Z') hc += 32;
    226       if (nc >= 'A' && nc <= 'Z') nc += 32;
    227       if (hc != nc) break;
    228       ++i;
    229     }
    230     if (i == nn) return 1;
    231   }
    232   return 0;
    233 }
    234 
    235 /* ---- main ---------------------------------------------------------- */
    236 
    237 int main(void) {
    238   if (!have_mingw_objdump()) {
    239     fprintf(stderr, "SKIP: x86_64-w64-mingw32-objdump not on PATH\n");
    240     return 0;
    241   }
    242 
    243   size_t ar_len = 0;
    244   uint8_t* ar_bytes = slurp_file(MINGW_ARCHIVE_PATH, &ar_len);
    245   if (!ar_bytes || !ar_len) {
    246     fprintf(stderr, "SKIP: cannot read %s\n", MINGW_ARCHIVE_PATH);
    247     free(ar_bytes);
    248     return 0;
    249   }
    250 
    251   KitTargetSpec t;
    252   target_x64_windows(&t);
    253   Compiler* c = make_compiler(&t);
    254   if (!c) {
    255     fprintf(stderr, "FAIL: compiler_new\n");
    256     free(ar_bytes);
    257     return 1;
    258   }
    259   if (setjmp(c->panic)) {
    260     fprintf(stderr, "FAIL: panic during pe-import-mingw\n");
    261     compiler_run_cleanups(c);
    262     free_compiler(c);
    263     free(ar_bytes);
    264     return 1;
    265   }
    266 
    267   /* 1. Program ObjBuilder calling ExitProcess. */
    268   ObjBuilder* prog = build_program(c);
    269 
    270   /* 2. Drive the linker. The archive feeds in raw — link_add_archive_bytes
    271    *    classifies its ~3000 members and rewrites the per-function stubs
    272    *    into DSO shims; demand resolution then pulls only ExitProcess. */
    273   Linker* l = link_new(c);
    274   EXPECT(l != NULL, "link_new returned NULL");
    275   link_add_obj(l, prog);
    276   LinkInputId ar_id = link_add_archive_bytes(l, "libkernel32.a", ar_bytes,
    277                                              ar_len, /*whole_archive=*/0,
    278                                              /*link_mode=*/0,
    279                                              /*group_id=*/0);
    280   EXPECT(ar_id != LINK_INPUT_NONE,
    281          "link_add_archive_bytes returned LINK_INPUT_NONE");
    282   link_set_entry(l, KIT_SLICE_LIT("mainCRTStartup"));
    283   link_set_pie(l, 1);
    284   link_set_emit_static_exe(l, 1);
    285 
    286   LinkImage* img = link_resolve(l);
    287   EXPECT(img != NULL, "link_resolve returned NULL");
    288   if (!img) {
    289     link_free(l);
    290     free_compiler(c);
    291     free(ar_bytes);
    292     return 1;
    293   }
    294 
    295   /* Sanity: ExitProcess should be present and marked imported. */
    296   {
    297     Sym exit_name = pool_intern_slice(c->global, SLICE_LIT(MINGW_IMPORT_FN));
    298     const LinkSymbol* found = NULL;
    299     for (LinkSymId i = 1;; ++i) {
    300       const LinkSymbol* s = link_symbol(img, i);
    301       if (!s) break;
    302       if (s->name == exit_name) {
    303         found = s;
    304         break;
    305       }
    306     }
    307     EXPECT(found != NULL, "%s LinkSymbol not present after link_resolve",
    308            MINGW_IMPORT_FN);
    309     if (found) {
    310       EXPECT(found->imported, "%s.imported=0 (expected 1 after archive match)",
    311              MINGW_IMPORT_FN);
    312     }
    313   }
    314 
    315   /* 3. Emit the PE. */
    316   KitWriter* w = NULL;
    317   if (kit_writer_mem(&g_heap, &w) != KIT_OK || !w) {
    318     fprintf(stderr, "FAIL: kit_writer_mem\n");
    319     link_image_free(img);
    320     link_free(l);
    321     free_compiler(c);
    322     free(ar_bytes);
    323     return 1;
    324   }
    325   link_emit_image_writer(img, w);
    326 
    327   size_t out_len = 0;
    328   const uint8_t* out_bytes = kit_writer_mem_bytes(w, &out_len);
    329   EXPECT(out_len > 0, "link_emit_image_writer produced %zu bytes", out_len);
    330 
    331   const char* exe_path = "/tmp/pe-import-mingw.exe";
    332   (void)unlink(exe_path);
    333   FILE* fp = fopen(exe_path, "wb");
    334   EXPECT(fp != NULL, "fopen(%s) for write", exe_path);
    335   if (fp) {
    336     size_t wr = fwrite(out_bytes, 1, out_len, fp);
    337     EXPECT(wr == out_len, "fwrite wrote %zu / %zu", wr, out_len);
    338     fclose(fp);
    339   }
    340 
    341   kit_writer_close(w);
    342   link_image_free(img);
    343   link_free(l);
    344 
    345   /* 4. Probe with objdump. */
    346   char* dump_p =
    347       slurp_cmd("x86_64-w64-mingw32-objdump -p /tmp/pe-import-mingw.exe 2>&1");
    348   EXPECT(dump_p != NULL, "slurp objdump -p");
    349   if (dump_p) {
    350     /* KERNEL32.dll listed (case-insensitive — derive_dll_name picks
    351      * lowercase, but mingw objdump renders names verbatim from the
    352      * import directory's name string). */
    353     EXPECT(contains_ci(dump_p, "DLL Name: " MINGW_DLL_NAME) ||
    354                contains_ci(dump_p, "DLL Name: kernel32.dll"),
    355            "objdump -p: KERNEL32.dll not in import directory\n---\n%s\n---",
    356            dump_p);
    357     EXPECT(strstr(dump_p, MINGW_IMPORT_FN) != NULL,
    358            "objdump -p: '%s' not in import directory\n---\n%s\n---",
    359            MINGW_IMPORT_FN, dump_p);
    360     /* Verify it's the only KERNEL32 import — no other functions
    361      * pulled in (dead-strip working). Count NUL-separated entries
    362      * under DLL Name: KERNEL32 by counting hint/name lines that
    363      * start with whitespace followed by a hex hint. mingw objdump
    364      * prints them like:
    365      *   vma:  Hint/Ord Member-Name Bound-To
    366      *   3008	     6  ExitProcess
    367      * We just confirm the expected one shows up; an over-pull
    368      * would show extra names like CreateFileA, CloseHandle, etc.
    369      * The dead-strip pass should suppress everything except the
    370      * single referenced symbol. */
    371     EXPECT(strstr(dump_p, "CreateFile") == NULL,
    372            "objdump -p: unexpected CreateFile import (dead-strip "
    373            "failure?)\n---\n%s\n---",
    374            dump_p);
    375     EXPECT(strstr(dump_p, "CloseHandle") == NULL,
    376            "objdump -p: unexpected CloseHandle import\n---\n%s\n---", dump_p);
    377     free(dump_p);
    378   }
    379 
    380   free_compiler(c);
    381   free(ar_bytes);
    382 
    383   if (g_failures) {
    384     fprintf(stderr, "FAILED %d assertion(s)\n", g_failures);
    385     return 1;
    386   }
    387   fprintf(stderr, "OK pe-import-mingw\n");
    388   return 0;
    389 }