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 }