pe-import-smoke.c (15793B)
1 /* PE import-directory smoke test — Phase 4.5 from doc/WINDOWS.md. 2 * 3 * Exercises the full chain: 4 * short-import shim bytes 5 * -> link_add_obj_bytes (reclassifies as DSO via OBJ_EXT_COFF 6 * annotation set by read_coff's short-import path) 7 * -> link_resolve (marks ExitProcess as imported, dso_input_id 8 * = the shim) 9 * -> link_emit_image_writer -> link_emit_coff (synthesizes 10 * .idata, IAT, per-arch IAT stub in .text) 11 * 12 * No execution — verification is byte-shape only via mingw's 13 * x86_64-w64-mingw32-objdump -p (import directory / headers) 14 * x86_64-w64-mingw32-objdump -d (disassembly of .text shows the 15 * call through the IAT stub) 16 * 17 * Skips cleanly with non-zero diagnostic-style message but exit 0 18 * when the mingw objdump is not on PATH. */ 19 20 #include <kit/core.h> 21 #include <kit/link.h> 22 #include <kit/object.h> 23 #include <setjmp.h> 24 #include <stdarg.h> 25 #include <stdio.h> 26 #include <stdlib.h> 27 #include <string.h> 28 #include <unistd.h> 29 30 #include "core/core.h" 31 #include "core/pool.h" 32 #include "link/link.h" 33 #include "obj/obj.h" 34 35 /* ---- short-import wire constants (mirror the spec in 36 * test/coff/kit-roundtrip-coff.c::test_short_import_amd64). ---- */ 37 #define SHIM_HEADER_SIZE 20u 38 #define SHIM_SYM_CSTR "ExitProcess" 39 #define SHIM_DLL_CSTR "KERNEL32.dll" 40 #define SHIM_SYM_NUL_LEN 12u /* "ExitProcess\0" */ 41 #define SHIM_DLL_NUL_LEN 13u /* "KERNEL32.dll\0" */ 42 #define SHIM_DATA_LEN (SHIM_SYM_NUL_LEN + SHIM_DLL_NUL_LEN) /* 25 */ 43 #define SHIM_TOTAL_LEN (SHIM_HEADER_SIZE + SHIM_DATA_LEN) /* 45 */ 44 45 /* IMAGE_FILE_MACHINE_AMD64. */ 46 #define COFF_MACHINE_AMD64 0x8664u 47 /* Sig1=0, Sig2=0xFFFF marks a short-import record. */ 48 #define COFF_SHIMP_SIG2 0xFFFFu 49 /* TypeFlags = Type=CODE(0) | (NameType=NAME(1) << 2) = 0x0004. */ 50 #define COFF_SHIMP_TYPEFLAGS 0x0004u 51 52 /* PE optional-header / data-directory constants we assert. */ 53 #define PE_DD_IDX_IMPORT 1 54 #define PE_DD_IDX_IAT 12 55 56 /* The exit-process program: e8 disp32 c3 (call ExitProcess; ret). 57 * disp32 is patched by R_PC32 against an undef ExitProcess. */ 58 static const uint8_t PROG_TEXT_X64[6] = {0xe8, 0, 0, 0, 0, 0xc3}; 59 60 /* ---- env vtables --------------------------------------------------- */ 61 62 static void* heap_alloc(KitHeap* h, size_t n, size_t a) { 63 (void)h; 64 (void)a; 65 return n ? malloc(n) : NULL; 66 } 67 static void* heap_realloc(KitHeap* h, void* p, size_t o, size_t n, size_t a) { 68 (void)h; 69 (void)o; 70 (void)a; 71 return realloc(p, n); 72 } 73 static void heap_free(KitHeap* h, void* p, size_t n) { 74 (void)h; 75 (void)n; 76 free(p); 77 } 78 static KitHeap g_heap = {heap_alloc, heap_realloc, heap_free, NULL}; 79 80 static void diag_emit(KitDiagSink* s, KitDiagKind k, KitSrcLoc loc, 81 const char* fmt, va_list ap) { 82 static const char* names[] = {"note", "warning", "error", "fatal"}; 83 (void)s; 84 (void)loc; 85 fprintf(stderr, "%s: ", names[k]); 86 vfprintf(stderr, fmt, ap); 87 fputc('\n', stderr); 88 } 89 static KitDiagSink g_diag = {diag_emit, NULL, 0, 0}; 90 91 /* ---- failure tracking --------------------------------------------- */ 92 93 static int g_failures; 94 #define EXPECT(cond, ...) \ 95 do { \ 96 if (!(cond)) { \ 97 fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \ 98 fprintf(stderr, __VA_ARGS__); \ 99 fputc('\n', stderr); \ 100 g_failures++; \ 101 } \ 102 } while (0) 103 104 /* ---- target / compiler ------------------------------------------- */ 105 106 static KitContext g_ctx; 107 108 static void target_x64_windows(KitTargetSpec* t) { 109 memset(t, 0, sizeof *t); 110 t->arch = KIT_ARCH_X86_64; 111 t->os = KIT_OS_WINDOWS; 112 t->obj = KIT_OBJ_COFF; 113 t->ptr_size = 8; 114 t->ptr_align = 8; 115 t->big_endian = false; 116 t->pic = KIT_PIC_PIE; 117 t->code_model = KIT_CM_SMALL; 118 } 119 120 static Compiler* make_compiler(const KitTargetSpec* t) { 121 memset(&g_ctx, 0, sizeof g_ctx); 122 g_ctx.heap = &g_heap; 123 g_ctx.diag = &g_diag; 124 g_ctx.now = -1; 125 KitTargetOptions opts; 126 memset(&opts, 0, sizeof opts); 127 opts.spec = *t; 128 KitTarget* target = NULL; 129 if (kit_target_new(&g_ctx, &opts, &target) != KIT_OK || !target) return NULL; 130 KitCompiler* cc = NULL; 131 if (kit_compiler_new(target, &g_ctx, &cc) != KIT_OK || !cc) { 132 kit_target_free(target); 133 return NULL; 134 } 135 return (Compiler*)cc; 136 } 137 138 static void free_compiler(Compiler* c) { 139 if (!c) return; 140 const KitTarget* target = kit_compiler_target((KitCompiler*)c); 141 kit_compiler_free((KitCompiler*)c); 142 kit_target_free((KitTarget*)target); 143 } 144 145 /* ---- short-import shim builder ------------------------------------ */ 146 147 static void build_short_import_amd64(uint8_t buf[SHIM_TOTAL_LEN]) { 148 memset(buf, 0, SHIM_TOTAL_LEN); 149 /* Sig1 = 0 (bytes 0..1 already 0). */ 150 /* Sig2 = 0xFFFF. */ 151 buf[2] = (uint8_t)(COFF_SHIMP_SIG2 & 0xFF); 152 buf[3] = (uint8_t)((COFF_SHIMP_SIG2 >> 8) & 0xFF); 153 /* Version = 0. */ 154 /* Machine. */ 155 buf[6] = (uint8_t)(COFF_MACHINE_AMD64 & 0xFF); 156 buf[7] = (uint8_t)((COFF_MACHINE_AMD64 >> 8) & 0xFF); 157 /* TimeDateStamp = 0 (bytes 8..11). */ 158 /* SizeOfData. */ 159 buf[12] = (uint8_t)(SHIM_DATA_LEN & 0xFFu); 160 buf[13] = (uint8_t)((SHIM_DATA_LEN >> 8) & 0xFFu); 161 buf[14] = (uint8_t)((SHIM_DATA_LEN >> 16) & 0xFFu); 162 buf[15] = (uint8_t)((SHIM_DATA_LEN >> 24) & 0xFFu); 163 /* OrdinalOrHint = 0 (16..17). */ 164 /* TypeFlags. */ 165 buf[18] = (uint8_t)(COFF_SHIMP_TYPEFLAGS & 0xFF); 166 buf[19] = (uint8_t)((COFF_SHIMP_TYPEFLAGS >> 8) & 0xFF); 167 /* Body: "ExitProcess\0" + "KERNEL32.dll\0". */ 168 memcpy(buf + SHIM_HEADER_SIZE, SHIM_SYM_CSTR, SHIM_SYM_NUL_LEN); 169 memcpy(buf + SHIM_HEADER_SIZE + SHIM_SYM_NUL_LEN, SHIM_DLL_CSTR, 170 SHIM_DLL_NUL_LEN); 171 } 172 173 /* ---- program ObjBuilder builder ----------------------------------- */ 174 175 static ObjBuilder* build_program(Compiler* c) { 176 ObjBuilder* ob = obj_new(c); 177 Pool* p = c->global; 178 Sym text_name = pool_intern_slice(p, SLICE_LIT(".text")); 179 Sym main_name = pool_intern_slice(p, SLICE_LIT("mainCRTStartup")); 180 Sym exit_name = pool_intern_slice(p, SLICE_LIT(SHIM_SYM_CSTR)); 181 ObjSecId text = obj_section(ob, text_name, SEC_TEXT, SF_ALLOC | SF_EXEC, 16); 182 obj_write(ob, text, PROG_TEXT_X64, sizeof PROG_TEXT_X64); 183 /* mainCRTStartup at .text offset 0. */ 184 obj_symbol(ob, main_name, SB_GLOBAL, SK_FUNC, text, 0, sizeof PROG_TEXT_X64); 185 /* ExitProcess as undef; reloc against the `e8` displacement (offset 1). */ 186 ObjSymId exit_sym = 187 obj_symbol(ob, exit_name, SB_GLOBAL, SK_UNDEF, OBJ_SEC_NONE, 0, 0); 188 obj_reloc(ob, text, 1, R_PC32, exit_sym, -4); 189 obj_finalize(ob); 190 return ob; 191 } 192 193 /* ---- objdump probe ------------------------------------------------ */ 194 195 static int have_mingw_objdump(void) { 196 FILE* fp = popen("command -v x86_64-w64-mingw32-objdump 2>/dev/null", "r"); 197 if (!fp) return 0; 198 char buf[256]; 199 size_t n = fread(buf, 1, sizeof buf - 1, fp); 200 pclose(fp); 201 return n > 0; 202 } 203 204 /* Run a shell command and slurp its stdout into a fresh malloc'd 205 * NUL-terminated string. Returns NULL on failure. */ 206 static char* slurp_cmd(const char* cmd) { 207 FILE* fp = popen(cmd, "r"); 208 if (!fp) return NULL; 209 size_t cap = 4096, len = 0; 210 char* buf = (char*)malloc(cap); 211 if (!buf) { 212 pclose(fp); 213 return NULL; 214 } 215 for (;;) { 216 if (len + 1024 + 1 > cap) { 217 cap *= 2; 218 char* nb = (char*)realloc(buf, cap); 219 if (!nb) { 220 free(buf); 221 pclose(fp); 222 return NULL; 223 } 224 buf = nb; 225 } 226 size_t got = fread(buf + len, 1, 1024, fp); 227 len += got; 228 if (got < 1024) break; 229 } 230 int rc = pclose(fp); 231 (void)rc; 232 buf[len] = '\0'; 233 return buf; 234 } 235 236 /* ---- main ---------------------------------------------------------- */ 237 238 int main(void) { 239 if (!have_mingw_objdump()) { 240 fprintf(stderr, "SKIP: x86_64-w64-mingw32-objdump not on PATH\n"); 241 return 0; 242 } 243 244 KitTargetSpec t; 245 target_x64_windows(&t); 246 Compiler* c = make_compiler(&t); 247 if (!c) { 248 fprintf(stderr, "FAIL: compiler_new\n"); 249 return 1; 250 } 251 if (setjmp(c->panic)) { 252 fprintf(stderr, "FAIL: panic during pe-import-smoke\n"); 253 compiler_run_cleanups(c); 254 free_compiler(c); 255 return 1; 256 } 257 258 /* 1. Program ObjBuilder. */ 259 ObjBuilder* prog = build_program(c); 260 261 /* 2. Short-import shim bytes. */ 262 uint8_t shim[SHIM_TOTAL_LEN]; 263 build_short_import_amd64(shim); 264 265 /* 3. Drive the linker. */ 266 Linker* l = link_new(c); 267 EXPECT(l != NULL, "link_new returned NULL"); 268 link_add_obj(l, prog); 269 LinkInputId dso_id = 270 link_add_obj_bytes(l, "ExitProcess.lib-member", shim, SHIM_TOTAL_LEN); 271 EXPECT(dso_id != LINK_INPUT_NONE, 272 "link_add_obj_bytes returned LINK_INPUT_NONE for short-import shim"); 273 link_set_entry(l, KIT_SLICE_LIT("mainCRTStartup")); 274 link_set_pie(l, 1); 275 link_set_emit_static_exe(l, 1); 276 277 LinkImage* img = link_resolve(l); 278 EXPECT(img != NULL, "link_resolve returned NULL"); 279 if (!img) { 280 link_free(l); 281 free_compiler(c); 282 return 1; 283 } 284 285 /* Sanity: walk LinkSyms and find ExitProcess. The globals hashmap only 286 * holds defined symbols, so link_symbol_lookup can't find an imported 287 * undef by name — iterate the dense LinkSyms array instead. */ 288 { 289 Sym exit_name = pool_intern_slice(c->global, SLICE_LIT(SHIM_SYM_CSTR)); 290 const LinkSymbol* found = NULL; 291 /* link_symbol returns NULL once we walk off the end. */ 292 for (LinkSymId i = 1;; ++i) { 293 const LinkSymbol* s = link_symbol(img, i); 294 if (!s) break; 295 if (s->name == exit_name) { 296 found = s; 297 break; 298 } 299 } 300 EXPECT(found != NULL, 301 "ExitProcess LinkSymbol not present after link_resolve"); 302 if (found) { 303 EXPECT(found->imported, 304 "ExitProcess.imported=0 (expected 1 after DSO match)"); 305 EXPECT(found->dso_input_id == dso_id, 306 "ExitProcess.dso_input_id=%u (expected %u)", 307 (unsigned)found->dso_input_id, (unsigned)dso_id); 308 } 309 } 310 311 /* 4. Emit the PE. */ 312 KitWriter* w = NULL; 313 if (kit_writer_mem(&g_heap, &w) != KIT_OK || !w) { 314 fprintf(stderr, "FAIL: kit_writer_mem\n"); 315 link_image_free(img); 316 link_free(l); 317 free_compiler(c); 318 return 1; 319 } 320 link_emit_image_writer(img, w); 321 322 size_t out_len = 0; 323 const uint8_t* out_bytes = kit_writer_mem_bytes(w, &out_len); 324 EXPECT(out_len > 0, "link_emit_image_writer produced %zu bytes", out_len); 325 326 /* 5. Write to /tmp and shell out to objdump. */ 327 const char* exe_path = "/tmp/pe-import-smoke.exe"; 328 (void)unlink(exe_path); 329 FILE* fp = fopen(exe_path, "wb"); 330 EXPECT(fp != NULL, "fopen(%s) for write", exe_path); 331 if (fp) { 332 size_t wr = fwrite(out_bytes, 1, out_len, fp); 333 EXPECT(wr == out_len, "fwrite wrote %zu / %zu", wr, out_len); 334 fclose(fp); 335 } 336 337 kit_writer_close(w); 338 link_image_free(img); 339 link_free(l); 340 341 /* objdump -p shows headers + import directory. */ 342 char* dump_p = 343 slurp_cmd("x86_64-w64-mingw32-objdump -p /tmp/pe-import-smoke.exe 2>&1"); 344 EXPECT(dump_p != NULL, "slurp objdump -p"); 345 if (dump_p) { 346 EXPECT(strstr(dump_p, "Magic\t\t\t020b") != NULL || 347 strstr(dump_p, "Magic\t020b") != NULL || 348 strstr(dump_p, "020b\t(PE32+)") != NULL || 349 strstr(dump_p, "PE32+") != NULL, 350 "objdump -p: missing PE32+ magic 020b\n---\n%s\n---", dump_p); 351 EXPECT(strstr(dump_p, "SectionAlignment") != NULL && 352 strstr(dump_p, "00001000") != NULL, 353 "objdump -p: SectionAlignment 0x1000 missing"); 354 EXPECT(strstr(dump_p, "FileAlignment") != NULL && 355 strstr(dump_p, "00000200") != NULL, 356 "objdump -p: FileAlignment 0x200 missing"); 357 EXPECT(strstr(dump_p, "Subsystem") != NULL, 358 "objdump -p: Subsystem line missing"); 359 /* mingw objdump prints "Subsystem\t\t00000003\t(Windows CUI)" */ 360 EXPECT(strstr(dump_p, "Windows CUI") != NULL || 361 strstr(dump_p, "(Windows CUI)") != NULL, 362 "objdump -p: Subsystem != Windows CUI\n---\n%s\n---", dump_p); 363 /* Import directory: DLL Name: KERNEL32.dll. */ 364 EXPECT(strstr(dump_p, "DLL Name: " SHIM_DLL_CSTR) != NULL, 365 "objdump -p: 'DLL Name: %s' not found\n---\n%s\n---", SHIM_DLL_CSTR, 366 dump_p); 367 /* The hint/name array prints "<hint> <name>". Check ExitProcess 368 * appears in the import list. */ 369 EXPECT(strstr(dump_p, SHIM_SYM_CSTR) != NULL, 370 "objdump -p: '%s' not in import directory\n---\n%s\n---", 371 SHIM_SYM_CSTR, dump_p); 372 /* Data directories: IMPORT (idx 1) and IAT (idx 12) must be set. 373 * mingw objdump prints them as 374 * "Entry 1 NNNNNNNN NNNNNNNN Import Directory" 375 * "Entry c NNNNNNNN NNNNNNNN Import Address Table Directory" 376 * Reject "00000000 00000000" on those lines. */ 377 { 378 const char* imp_line = strstr(dump_p, "Import Directory"); 379 EXPECT(imp_line != NULL, "objdump -p: 'Import Directory' line missing"); 380 if (imp_line) { 381 /* Walk back to start of line. */ 382 const char* ls = imp_line; 383 while (ls > dump_p && ls[-1] != '\n') --ls; 384 EXPECT(strstr(ls, "00000000 00000000 [size]") == NULL && 385 strstr(ls, "\t00000000\t00000000\t") == NULL, 386 "Import Directory data-dir entry is zero\nline: %.*s", 387 (int)(imp_line - ls + (int)strlen("Import Directory")), ls); 388 } 389 const char* iat_line = strstr(dump_p, "Import Address Table Directory"); 390 EXPECT(iat_line != NULL, 391 "objdump -p: 'Import Address Table Directory' line missing"); 392 if (iat_line) { 393 const char* ls = iat_line; 394 while (ls > dump_p && ls[-1] != '\n') --ls; 395 EXPECT(strstr(ls, "00000000 00000000 [size]") == NULL && 396 strstr(ls, "\t00000000\t00000000\t") == NULL, 397 "IAT data-dir entry is zero\nline: %.*s", 398 (int)(iat_line - ls + 399 (int)strlen("Import Address Table Directory")), 400 ls); 401 } 402 } 403 free(dump_p); 404 } 405 406 /* objdump -d: confirm the .text disassembly has the call (from 407 * mainCRTStartup) plus the per-arch IAT stub `jmp *off(%rip)` that 408 * link_emit_coff appends. The PE has no symbol table — there's no 409 * <mainCRTStartup> label in the disassembly, just .text. */ 410 char* dump_d = 411 slurp_cmd("x86_64-w64-mingw32-objdump -d /tmp/pe-import-smoke.exe 2>&1"); 412 EXPECT(dump_d != NULL, "slurp objdump -d"); 413 if (dump_d) { 414 EXPECT(strstr(dump_d, "<.text>") != NULL, 415 "objdump -d: <.text> section header missing\n---\n%s\n---", dump_d); 416 /* The mainCRTStartup body is a `call <disp32>` at the entry. The 417 * disp32 must have been patched away from zero by the linker — 418 * objdump renders it as `call 0xNNNNNNNN`, never `call 0x0`. */ 419 EXPECT(strstr(dump_d, "call ") != NULL || strstr(dump_d, "callq ") != NULL, 420 "objdump -d: no call instruction in disassembly\n---\n%s\n---", 421 dump_d); 422 EXPECT(strstr(dump_d, "call 0x0\n") == NULL && 423 strstr(dump_d, "callq 0x0\n") == NULL, 424 "objdump -d: call target left at 0x0 (unrelocated)\n---\n%s\n---", 425 dump_d); 426 /* The IAT stub is the `ff 25 disp32` indirect jmp the per-arch 427 * stub emitter appends to .text for the imported symbol. */ 428 EXPECT(strstr(dump_d, "jmp *") != NULL || 429 strstr(dump_d, "jmpq *") != NULL || 430 strstr(dump_d, "ff 25") != NULL, 431 "objdump -d: no IAT stub `jmp *off(%%rip)` in .text\n---\n%s\n---", 432 dump_d); 433 free(dump_d); 434 } 435 436 free_compiler(c); 437 438 if (g_failures) { 439 fprintf(stderr, "FAILED %d assertion(s)\n", g_failures); 440 return 1; 441 } 442 fprintf(stderr, "OK pe-import-smoke\n"); 443 return 0; 444 }