commit 277327a487828145938a8eeca9946f9f28d76256
parent 745214299916153bb7dcb7c0f446c55d330d7efc
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Tue, 9 Jun 2026 14:50:22 -0700
obj: COFF/PE image reader + format-specific raw-fields escape hatch
Open linked PE executables/DLLs through kit_obj_open like ELF/Mach-O.
read_coff dispatches the DOS 'MZ' magic to a new read_coff_image
(coff/read_image.c) that fills the neutral ObjImage -- one segment per PE
section, exports -> dynsyms + soname, imports -> deps + undefined dynsyms,
base relocs -> arch RELATIVE dynrelocs -- plus the full section/symbol
view. detect_pe routes PE images to KIT_OBJ_COFF/Windows (kit_detect_target
previously had no KIT_BIN_PE case). Shared RVA / bounded-string /
Characteristics->SecKind helpers are factored into coff/read_util.{c,h},
used by the .obj, DSO, and image readers.
Add a generic image-level escape hatch (KitObjImageRaw +
kit_obj_image_rawiter_*) for format-specific values outside the neutral
model, with producers for PE (data directories + subsystem + dllchars),
ELF (raw DT_*), and Mach-O (load commands). obj_image_add_dep now
deep-copies the imports[] array into image-heap memory.
objdump: delete the hand-rolled pe_parse_image walker and the KIT_BIN_PE
fallback branch; -p/-f render PE through the neutral image API + the
dependency iterator, and -h/-s/-t/-T/-d/-r/-R work for free.
Tests: non-gated pe-image-read round-trip links a PE in memory (x64 + aa64)
and reads it back, asserting the image view + the raw escape hatch;
committed pe-image.exe objdump golden; gated 04-pe-sections updated to the
neutral -h header.
Closes out doc/plan/IMAGE_INSPECT.md.
Diffstat:
22 files changed, 1400 insertions(+), 753 deletions(-)
diff --git a/doc/OBJ.md b/doc/OBJ.md
@@ -70,7 +70,7 @@ The five tables and what they model:
### Format pass-through without leaking format knowledge
Generic tables stay neutral, but `.o` round-tripping needs to preserve bits the
-canonical model doesn't name. Three escape hatches handle this without
+canonical model doesn't name. A few targeted escape hatches handle this without
polluting the core:
- Per-section `ext_type` / `ext_flags` (raw `sh_type`/`sh_flags`) re-emit
@@ -78,6 +78,10 @@ polluting the core:
(`SHT_LLVM_ADDRSIG`, `SHT_ARM_ATTRIBUTES`, `SHF_EXCLUDE`, ...).
- Per-symbol `flags` carry format attribute bits (today Mach-O `n_desc`).
- Builder-level fields: ELF `e_flags`, the COFF short-import DLL annotation.
+ - Per-image raw fields (`ObjImageRaw`, `kit_obj_image_rawiter_*`): a flat
+ `(tag, value, extra)` list for linked-image values outside the neutral model
+ — PE data directories / subsystem / dllchars, ELF raw `DT_*`, Mach-O load
+ commands (see "The linked-image dimension").
- `obj_ext_set`/`obj_ext_get` attach one opaque payload per `ObjExtKind`
(today the Wasm module model and Wasm import descriptors); the builder owns
the payload's lifetime via a registered free function.
@@ -258,13 +262,17 @@ dimension the section/symbol tables can't model — load segments, an entry
point, image base, interpreter, soname, dependencies, rpaths, dynamic symbols,
and dynamic relocations. The `ObjImage` (defined in `obj.c`, hung off the
builder, released by `obj_free`) holds this *common denominator* across formats.
-Readers call `obj_image_ensure(ob, OBJ_KIND_EXEC|DYN)` and the appenders; the
-section/symbol view stays populated where the format still carries it, so a
-non-stripped ELF exec presents both views and a table-stripped image presents
-only segments. The public API mirrors the relocatable iterators:
-`kit_obj_kind`, image-info scalars, and segment/dep/rpath/dynsym/dynreloc
-iterators. `OBJ_KIND_CORE` is reserved — detected and rejected cleanly, not
-parsed.
+All three native formats fill it: ELF (`ET_EXEC`/`ET_DYN`), Mach-O
+(`MH_EXECUTE`/`MH_DYLIB`), and COFF/PE (executables / DLLs). Readers call
+`obj_image_ensure(ob, OBJ_KIND_EXEC|DYN)` and the appenders; the section/symbol
+view stays populated where the format still carries it, so a non-stripped ELF
+exec presents both views and a table-stripped image presents only segments. The
+public API mirrors the relocatable iterators: `kit_obj_kind`, image-info
+scalars, and segment/dep/rpath/dynsym/dynreloc iterators, plus a raw-fields
+iterator (`kit_obj_image_rawiter_*`) — the image-level escape hatch for
+format-specific values the neutral model doesn't name (PE data directories /
+subsystem / dllchars, ELF raw `DT_*`, Mach-O load commands). `OBJ_KIND_CORE` is
+reserved — detected and rejected cleanly, not parsed.
## Per-format notes
@@ -311,8 +319,17 @@ mingw/llvm-mingw UCRT, not MSVC. `read.c`/`emit.c` round-trip relocatable
PE/COFF: sections with `Characteristics`, symbols with auxiliary records,
COMDAT groups and SELECTANY dedup, weak externals + mingw alias fallback,
commons, long section names via the string table, and per-arch relocations.
-`read_dso.c` walks raw PE DLL export directories (and forwarder ENT entries,
-surfaced as defined symbols so the OS loader chases the chain at runtime).
+`read_image.c` (`read_coff_image`, dispatched from `read.c` on the DOS `MZ`
+magic) is the peer of `read_elf_image`/`read_macho_image`: it parses a linked
+`.exe`/`.dll` into the `ObjImage` — one segment per PE section, exports →
+dynamic symbols + soname, imports → deps (with imported-name lists) + undefined
+dynamic symbols, base relocations → `RELATIVE` dynamic relocs — plus a full
+section/symbol view, and the raw data-directory / subsystem / dllchars fields
+through the image escape hatch. `read_util.c` holds the RVA→offset, bounded
+string, and `Characteristics`→`SecKind` helpers shared by the `.obj`, DSO, and
+image readers. `read_dso.c` walks raw PE DLL export directories (and forwarder
+ENT entries, surfaced as defined symbols so the OS loader chases the chain at
+runtime).
`archive.c` implements the registry's archive-ingestion hooks: it classifies
import-library members, routing Microsoft short-import records
(`Sig1=0, Sig2=0xFFFF`) through `read_coff` to synthesize the imported symbols
diff --git a/doc/plan/IMAGE_INSPECT.md b/doc/plan/IMAGE_INSPECT.md
@@ -1,174 +0,0 @@
-# Image Inspection (planned work)
-
-kit can read relocatable objects through `kit_obj_open`, and it has been
-extended to also inspect *linked images* -- executables and shared objects --
-across the same neutral API. ELF and Mach-O reading have landed; the
-remaining work is the COFF/PE image reader plus a handful of follow-ups. This
-doc captures the goal, what is already baseline, and what is left, so the PE
-work and any later refinements parallelize against a settled contract. The
-matching design lives in [../OBJ.md](../OBJ.md); see also
-[LINKER.md](LINKER.md) for the linker side and [../DBG.md](../DBG.md) /
-[../DWARF.md](../DWARF.md) for the debug-info flow that rides on it.
-
-## Goal
-
-One `kit_obj_open` call inspects any of: a relocatable object, an
-executable, or a shared object, for ELF, Mach-O, and COFF/PE. Sections and
-symbols keep working where the format still carries them; linked images
-additionally expose segments, an entry point and image base, an interpreter /
-SONAME, library dependencies and rpaths, and a dynamic symbol/relocation
-table. `objdump` and the inherited tools (nm, size, addr2line) operate on
-images the same way they operate on objects, with no per-format
-special-casing in the driver.
-
-## Why this is a real extension, not a flag
-
-The original reader was relocatable-object-shaped. `kit_obj_open` ->
-`kit_detect_target` -> `impl->read`, and the ELF backend rejected anything
-but `ET_REL`. There were DSO readers (`read_elf_dso`, `read_coff_dso`, a
-Mach-O dylib stub) but they were wired only into the *linker's* input path,
-not the public `impl->read` / `kit_obj_open` surface; `ET_EXEC` had no
-reader at all. The in-memory model (`ObjBuilder`) was section / symbol /
-reloc oriented with no notion of a **segment** (PT_LOAD), the **dynamic
-table** (DT_NEEDED / SONAME / RPATH), an **entry point**, **image base**,
-**imports**, or **data directories** -- which is exactly what image
-inspection is about. The fix is a new image dimension on the model plus
-neutral iterators, not a flag.
-
-## Baseline (done)
-
-The neutral API and internal model are in place, and ELF + Mach-O image
-reading work end to end:
-
-- **Neutral API** (`include/kit/object.h`): `KitObjKind` +
- `kit_obj_kind`; `KitObjImageInfo` + `kit_obj_image_info` (entry, image
- base, interp, soname); segment iterator (`kit_obj_segiter_*` over
- `KitObjSegInfo` with `KIT_SEG_R/W/X`); dependency iterator
- (`kit_obj_depiter_*`, carrying imported names for PE/Mach-O); rpath
- iterator (`kit_obj_rpathiter_*`); dynamic symbols and relocations reusing
- the `KitObjSymIter` / `KitObjRelocIter` shapes via
- `kit_obj_dynsymiter_new` / `kit_obj_dynreliter_new`.
-- **Internal model** (`src/obj/obj.h`, `src/obj/obj.c`): an `ObjImage` hung
- off `ObjBuilder`, NULL on pure relocatables. Readers call
- `obj_image_ensure(ob, OBJ_KIND_*)` then setters/appenders for entry, base,
- interp, soname, segments, deps, rpaths, dynsyms, and dynrelocs;
- `obj_free` releases it.
-- **Glue** (`src/api/object_file.c`): maps `ObjImage` to the public
- iterators; relocatable inputs report `KIT_OBJ_KIND_REL` with empty image
- iterators and the section/symbol path unchanged.
-- **ELF reader** (`src/obj/elf/read.c`): `read_elf` accepts `ET_EXEC` /
- `ET_DYN`, sharing one path -- the old `e_type != ET_REL` guard is now a kind
- switch. `read_elf_image` walks program headers for segments + PT_INTERP +
- image base, and parses `.dynamic` for DT_NEEDED / DT_SONAME / DT_RPATH /
- DT_RUNPATH plus the dynsym/dynstr/reloc pointers. A zeroed section-header
- table is accepted for images (empty section view, segment view carries the
- load picture).
-- **Mach-O reader** (`src/obj/macho/read.c`): accepts `MH_EXECUTE` /
- `MH_DYLIB`; `read_macho_image` re-walks load commands for segments
- (`LC_SEGMENT_64`, `__TEXT` base, VM_PROT->OBJ_SEG perms), interp
- (`LC_LOAD_DYLINKER`), soname (`LC_ID_DYLIB`), deps (`LC_LOAD_DYLIB` and
- weak/reexport variants), rpaths (`LC_RPATH`), entry (`LC_MAIN` /
- `LC_UNIXTHREAD`), dynamic symbols from the external `LC_SYMTAB` nlist
- entries, and `LC_DYLD_CHAINED_FIXUPS` binds/rebases
- (`DYLD_CHAINED_PTR_64`). Classic `LC_DYLD_INFO` and the exports trie are
- intentionally not read; non-64-bit chained pointer formats are skipped
- leniently.
-- **objdump** (`driver/cmd/objdump.c`): grew `-p` / `--private-headers`
- (program/dynamic headers, format-neutral via the image API), `-T` /
- `--dynamic-syms`, and `-R` / `--dynamic-reloc`; `-f` reports the image type
- flags and real entry point; `-h` / `-t` / `-d` work on executables. `-d`
- falls back to disassembling X-perm `PT_LOAD` segments by vaddr when the
- section walk is empty (stripped images), with no ELF special-casing.
-- **Inherited tools**: nm, size, addr2line open images via `kit_obj_open`.
- nm grew `-D` (`.dynsym`); `KitObjSecInfo.addr` carries the load vaddr (0
- for relocatables) so SysV `size -A` reports real layout. (strings is
- intentionally format-agnostic -- it scans raw bytes and does not call
- `kit_obj_open`.)
-- **Debug-info retention in the linker**: `.debug_*` sections are carried
- through to linked images as file-only sections with relocations resolved in
- place, so `addr2line` / `dbg` resolve `file:line` on kit-linked
- executables (single- and multi-input, ELF and Mach-O). See
- [../LINK.md](../LINK.md) and [../DWARF.md](../DWARF.md).
-
-## Remaining work
-
-### COFF/PE image reader (primary gap)
-
-PE is the one format whose linked images do not yet open through
-`kit_obj_open`. The COFF backend's `read` does not populate `ObjImage`, and
-`read_coff_dso` is still wired only into the linker. As a result `objdump`
-keeps a hand-rolled `pe_parse_image` raw-byte walker
-(`driver/cmd/objdump.c:392`) behind a `KIT_BIN_PE` special-case
-(`driver/cmd/objdump.c:1831`) that serves `-f` / `-h` / `-p` and soft-errors
-`-t` / `-d` / `-r` / `-s`. The plan:
-
-- Give the COFF backend a real image reader: DOS / NT headers, optional
- header (entry point + image base + subsystem), data directories, the
- section table, the import and export directories, and the base-relocation
- table. Reuse / fold in `read_coff_dso`'s machinery so EXEC and DLL share one
- path the way ELF EXEC/DYN do.
-- Populate `ObjImage`: segments from sections + image base, deps from the
- import directory (each DLL's imported names go into the per-dep imports
- list), exports from the export directory into the dynamic symbol table, and
- base relocations into the dynamic relocation view.
-- Extend objdump `-p` to render the PE optional header + data directories,
- `-T` for exports, `-R` for base relocations, all through the neutral image
- API.
-- **Delete** `pe_parse_image` and collapse the `KIT_BIN_PE` branch in
- `driver_objdump` into the normal `dump_obj` path once PE images open via
- `kit_obj_open`.
-
-### Escape hatch for format-specific raw fields
-
-Some inspection needs format-specific values that do not fit the neutral
-model: raw DT_* tag values, raw Mach-O load commands, PE data-directory
-entries. Surface these through a per-format escape hatch in the spirit of the
-existing `kit_obj_section_format_flags`, keeping the neutral API clean
-rather than widening it per format.
-
-### Mach-O classic-format breadth (deferred)
-
-The Mach-O reader deliberately supports only the modern fixup path
-(`LC_DYLD_CHAINED_FIXUPS`, with the symbol table as the authoritative
-dynamic-symbol source) and `LC_DYLD_EXPORTS_TRIE`. Classic `LC_DYLD_INFO`
-opcode/trie reading and the exports trie remain out of scope; reading older
-dylibs is a separate, lower-priority effort. Revisit only if a real input
-demands it.
-
-## Out of scope
-
-- **Core files** (`ET_CORE`, Mach-O `MH_CORE`): `KIT_OBJ_KIND_CORE` stays
- defined but unimplemented; detect and reject cleanly. Note / register-state
- parsing is a separate feature.
-- **Synthesizing pseudo-sections from segments** on stripped ELF: matches GNU
- `objdump` / `llvm-objdump`, which are section-header-driven and report "no
- sections" when the table is absent. The segment view (and `-d` over X-perm
- segments) covers the disassembly case.
-
-## Design notes carried forward
-
-- **One open call, two views.** `kit_obj_open` detects kind (reusing
- `kit_detect_target` + `e_type` / `filetype` / PE characteristics) and
- routes to the backend, which fills `ObjBuilder` (sections/symbols where
- present) and, for EXEC/DYN, `ObjImage`. Tools that already use
- `kit_obj_open` inherit image support for free.
-- **Segment is the load-layout unit.** `{ vaddr, vsize, file_off, file_size,
- perms, align, name }`, populated from PT_LOAD / LC_SEGMENT_64 / PE sections.
- Sections continue to map through the existing `ObjBuilder` view where the
- format retains them; the segment view carries the load picture when section
- headers are absent.
-- **Dynamic syms/relocs reuse object shapes.** The dynamic symbol and
- relocation iterators reuse `KitObjSymInfo` / `KitObjReloc` rather than
- introducing parallel types, so consumers written for objects work on
- images.
-
-## Test strategy
-
-The compiler links its own ELF / Mach-O / PE images, so tests round-trip:
-link a small program, open it via `kit_obj_open`, and assert
-kind/entry/segments/deps/dynsyms against what the linker emitted, cross-checked
-against host `readelf` / `objdump` in smoke tests where available. ELF and
-Mach-O goldens live under `test/objdump/`; PE corpora land under
-`test/{coff,pe}/` with the reader. Dynamic NEEDED/SONAME/dynsym paths fully
-exercise once `-shared` / dynamic linking emit populated tables; the
-empty-table rendering is already covered. See [../TESTING.md](../TESTING.md).
diff --git a/doc/plan/README.md b/doc/plan/README.md
@@ -17,7 +17,6 @@ shrinks to whatever remains open.
| [ARCH.md](ARCH.md) | Remaining native-backend completeness for x64/rv64 relative to the aa64 reference, and per-call cost follow-ups. | [../ARCH.md](../ARCH.md) |
| [BOOTSTRAP.md](BOOTSTRAP.md) | The 3-stage self-build reproducibility goal and the open `-O1` issues blocking it. | [../BUILD.md](../BUILD.md) |
| [windows.md](windows.md) | Self-hosting kit on Windows: cross-built `kit.exe` runs `cc` + JIT on aarch64-windows; the open self-host miscompile crash, JIT printf, x64 parity, compile-on-VM lane, and the Windows 3-stage bootstrap. | [../WINDOWS.md](../WINDOWS.md) |
-| [IMAGE_INSPECT.md](IMAGE_INSPECT.md) | Extending object inspection to executables and shared libraries. COFF/PE image reader is the main remaining gap. | [../OBJ.md](../OBJ.md) |
| [BUILD.md](BUILD.md) | A new content-addressed build coordinator (Bazel/Nix-style incremental builds layered on the CAS) — storage state machine, caching algorithm, recipe protocol. Distinct from `../BUILD.md` (kit's own Makefile build). | — (new subsystem) |
| [BUILD_COMMANDS.md](BUILD_COMMANDS.md) | The kit-native `build-exe`/`build-lib`/`build-obj` verbs that replace `compile`: polyglot, in-memory compile+link with `--group` flag scoping and full link-flag control. Distinct from `BUILD.md` (the CAS coordinator). | [../DRIVER.md](../DRIVER.md) |
| [LLGEN_IMPORT.md](LLGEN_IMPORT.md) | Importing the standalone LL(1)/Pratt parser and lexer generator into libkit, including public API renames, file moves, build gates, and a `kit llgen` command. | — |
diff --git a/driver/cmd/objdump.c b/driver/cmd/objdump.c
@@ -129,38 +129,17 @@ void driver_help_objdump(void) {
"usage\n")));
}
-/* ---- PE/COFF private-header walker (used by `-p`) ----
+/* ---- PE/COFF display helpers ----
*
- * The objdump driver currently relies on the high-level KitObjFile
- * interface for section/symbol/disasm output. For PE images that hides
- * a lot of useful structure: the optional header, data directories,
- * and per-DLL import lists. The walker below operates on the raw input
- * bytes so we can print this view without piping the data through
- * libkit. It does the strict minimum needed for a `-p` style dump
- * and bails out on malformed offsets — diagnostic, not security-grade.
- *
- * RVA-to-file resolution: each section header records VirtualAddress
- * (RVA) and PointerToRawData (file offset). A target RVA lands inside
- * a section iff RVA in [VA, VA + VirtualSize). The file offset of the
- * RVA inside the section's raw bytes is PointerToRawData + (RVA - VA).
- * `pe_rva_to_file` returns -1 when no section covers the RVA. */
-#define PE_DOS_E_LFANEW_OFFSET 60u
-#define PE_FILE_HEADER_SIZE 20u
-#define PE_OPT_HDR64_MAGIC 0x020Bu
-#define PE_NUM_DATA_DIRS 16u
-#define PE_DATA_DIRECTORY_SIZE 8u
-#define PE_SECTION_HEADER_SIZE 40u
-#define PE_DIR_EXPORT 0u
+ * PE executables / DLLs now open through kit_obj_open like every other
+ * format, so the image data (optional-header scalars, data directories,
+ * imports) arrive through the neutral image API — kit_obj_image_info, the
+ * raw-fields iterator (KitObjImageRaw), and the dependency iterator. The
+ * helpers below only map PE numeric constants to the symbolic names
+ * objdump prints (data-directory index, subsystem). */
+
+/* Data-directory index (IMAGE_DIRECTORY_ENTRY_IMPORT). */
#define PE_DIR_IMPORT 1u
-#define PE_DIR_RESOURCE 2u
-#define PE_DIR_EXCEPTION 3u
-#define PE_DIR_BASERELOC 5u
-#define PE_DIR_DEBUG 6u
-#define PE_DIR_TLS 9u
-#define PE_DIR_IAT 12u
-#define PE_IMPORT_DESCRIPTOR_SIZE 20u
-#define PE_THUNK_SIZE 8u
-#define PE_ORDINAL_FLAG64 0x8000000000000000ull
/* COFF-specific Characteristics bits we surface as tags. Kept in sync
* with src/obj/coff.h's IMAGE_SCN_* values; objdump only needs the
@@ -174,16 +153,6 @@ void driver_help_objdump(void) {
static int j_match(const ObjdumpOpts* o, KitSlice name);
-static uint16_t pe_rd_u16(const uint8_t* p) {
- return (uint16_t)(p[0] | ((uint32_t)p[1] << 8));
-}
-static uint32_t pe_rd_u32(const uint8_t* p) {
- return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) |
- ((uint32_t)p[3] << 24);
-}
-static uint64_t pe_rd_u64(const uint8_t* p) {
- return (uint64_t)pe_rd_u32(p) | ((uint64_t)pe_rd_u32(p + 4) << 32);
-}
/* Names match the IMAGE_DIRECTORY_ENTRY_* index. Keep aligned with the
* order in coff.h to avoid drift. */
@@ -257,351 +226,6 @@ static const char* pe_subsystem_name(uint16_t s) {
}
}
-/* Find file offset for an RVA by scanning the section headers. Returns
- * -1 if the RVA is outside every section's covered range. */
-static long pe_rva_to_file(const uint8_t* buf, size_t buf_len, size_t sec_off,
- uint16_t nsec, uint32_t rva) {
- uint16_t i;
- for (i = 0; i < nsec; ++i) {
- size_t sh = sec_off + (size_t)i * PE_SECTION_HEADER_SIZE;
- uint32_t va;
- uint32_t vsize;
- uint32_t raw_off;
- uint32_t raw_size;
- if (sh + PE_SECTION_HEADER_SIZE > buf_len) return -1;
- vsize = pe_rd_u32(buf + sh + 8);
- va = pe_rd_u32(buf + sh + 12);
- raw_size = pe_rd_u32(buf + sh + 16);
- raw_off = pe_rd_u32(buf + sh + 20);
- /* VirtualSize is sometimes 0 in object files; fall back to raw size. */
- if (vsize == 0) vsize = raw_size;
- if (rva >= va && rva < va + vsize) {
- uint32_t delta = rva - va;
- if (delta >= raw_size) return -1;
- return (long)(raw_off + delta);
- }
- }
- return -1;
-}
-
-/* Read a NUL-terminated ASCII string starting at `off`, capped to
- * 256 bytes. Writes a copy into `dst` (size `dstcap`) and returns
- * 0 on success, 1 if the offset is out of bounds. */
-static int pe_read_cstr(const uint8_t* buf, size_t buf_len, size_t off,
- char* dst, size_t dstcap) {
- size_t i;
- if (off >= buf_len) {
- if (dstcap) dst[0] = '\0';
- return 1;
- }
- for (i = 0; i + 1 < dstcap && off + i < buf_len && buf[off + i]; ++i) {
- dst[i] = (char)buf[off + i];
- }
- dst[i] = '\0';
- return 0;
-}
-
-static void pe_dump_imports(const uint8_t* buf, size_t buf_len, size_t sec_off,
- uint16_t nsec, uint32_t import_rva,
- uint32_t import_size) {
- long desc_off;
- uint32_t consumed;
- (void)import_size;
- desc_off = pe_rva_to_file(buf, buf_len, sec_off, nsec, import_rva);
- if (desc_off < 0) {
- driver_printf(" (import directory RVA not covered by any section)\n");
- return;
- }
- driver_printf("\nThe Import Tables:\n");
- for (consumed = 0;; consumed += PE_IMPORT_DESCRIPTOR_SIZE) {
- size_t off = (size_t)desc_off + consumed;
- uint32_t ilt_rva;
- uint32_t name_rva;
- uint32_t iat_rva;
- char dll[256];
- long thunk_off;
- uint32_t i;
- if (off + PE_IMPORT_DESCRIPTOR_SIZE > buf_len) break;
- ilt_rva = pe_rd_u32(buf + off + 0);
- name_rva = pe_rd_u32(buf + off + 12);
- iat_rva = pe_rd_u32(buf + off + 16);
- /* All-zero descriptor terminates the chain. */
- if (ilt_rva == 0 && name_rva == 0 && iat_rva == 0) break;
- {
- long name_off = pe_rva_to_file(buf, buf_len, sec_off, nsec, name_rva);
- if (name_off < 0 ||
- pe_read_cstr(buf, buf_len, (size_t)name_off, dll, sizeof dll) != 0) {
- dll[0] = '\0';
- }
- }
- driver_printf(" DLL Name: %.*s\n",
- KIT_SLICE_ARG(kit_slice_cstr(dll[0] ? dll : "(unreadable)")));
- driver_printf(" ILT RVA: 0x%x IAT RVA: 0x%x\n", ilt_rva, iat_rva);
- /* Prefer walking the original first thunk (ILT) for names. Some
- * mingw-emitted images zero the ILT and only ship the IAT; fall
- * back to the IAT in that case. */
- {
- uint32_t walk_rva = ilt_rva ? ilt_rva : iat_rva;
- thunk_off =
- walk_rva ? pe_rva_to_file(buf, buf_len, sec_off, nsec, walk_rva) : -1;
- }
- if (thunk_off < 0) continue;
- for (i = 0;; ++i) {
- size_t toff = (size_t)thunk_off + (size_t)i * PE_THUNK_SIZE;
- uint64_t t;
- if (toff + PE_THUNK_SIZE > buf_len) break;
- t = pe_rd_u64(buf + toff);
- if (t == 0) break;
- if (t & PE_ORDINAL_FLAG64) {
- driver_printf(" Ordinal: %u\n", (unsigned)(t & 0xFFFFu));
- } else {
- long hint_off =
- pe_rva_to_file(buf, buf_len, sec_off, nsec, (uint32_t)t);
- char name[256];
- if (hint_off < 0 || pe_read_cstr(buf, buf_len, (size_t)hint_off + 2u,
- name, sizeof name) != 0) {
- continue;
- }
- driver_printf(" Name: %.*s\n",
- KIT_SLICE_ARG(kit_slice_cstr(name)));
- }
- }
- }
- driver_printf("\n");
-}
-
-/* Parsed view of a PE32+ image's headers. Populated by pe_parse_image;
- * callers check `valid` before reading other fields. Avoids the prior
- * pattern of every PE-walker re-validating the DOS/PE/optional header
- * triplet from scratch. */
-typedef struct PeImage {
- int valid;
- uint16_t machine;
- uint16_t file_chars;
- uint16_t nsec;
- uint16_t opt_magic;
- uint16_t subsystem;
- uint16_t dllchars;
- uint64_t image_base;
- uint32_t entry_rva;
- size_t sec_off;
- size_t dir_off;
-} PeImage;
-
-static int pe_parse_image(const uint8_t* buf, size_t buf_len, PeImage* out) {
- uint32_t e_lfanew;
- size_t coff_off;
- size_t opt_off;
- uint16_t opt_size;
-
- out->valid = 0;
- if (buf_len < PE_DOS_E_LFANEW_OFFSET + 4u) return 0;
- if (pe_rd_u16(buf) != 0x5A4Du) return 0;
- e_lfanew = pe_rd_u32(buf + PE_DOS_E_LFANEW_OFFSET);
- if ((size_t)e_lfanew + 4u + PE_FILE_HEADER_SIZE > buf_len) return 0;
- if (pe_rd_u32(buf + e_lfanew) != 0x00004550u) return 0;
- coff_off = (size_t)e_lfanew + 4u;
- out->machine = pe_rd_u16(buf + coff_off + 0);
- out->nsec = pe_rd_u16(buf + coff_off + 2);
- out->file_chars = pe_rd_u16(buf + coff_off + 18);
- opt_size = pe_rd_u16(buf + coff_off + 16);
- opt_off = coff_off + PE_FILE_HEADER_SIZE;
- if (opt_size == 0 || opt_off + opt_size > buf_len) return 0;
- out->opt_magic = pe_rd_u16(buf + opt_off);
- if (out->opt_magic != PE_OPT_HDR64_MAGIC) {
- /* PE32 (0x10B) is out of scope but the caller may still want to
- * report what it found, so we return a "valid header, unsupported
- * subset" view rather than failing. */
- out->valid = 1;
- return 1;
- }
- out->entry_rva = pe_rd_u32(buf + opt_off + 16);
- out->image_base = pe_rd_u64(buf + opt_off + 24);
- out->subsystem = pe_rd_u16(buf + opt_off + 68);
- out->dllchars = pe_rd_u16(buf + opt_off + 70);
- out->sec_off = opt_off + opt_size;
- out->dir_off = opt_off + 112u;
- out->valid = 1;
- return 1;
-}
-
-static const char* pe_machine_name(uint16_t m) {
- switch (m) {
- case 0x8664u:
- return "x86_64 (AMD64)";
- case 0xAA64u:
- return "aarch64 (ARM64)";
- case 0xA641u:
- return "aarch64 (ARM64EC)";
- case 0x014Cu:
- return "i386";
- case 0x01C0u:
- return "arm";
- case 0x01C4u:
- return "armnt";
- case 0x0200u:
- return "ia64";
- case 0x5064u:
- return "riscv64";
- default:
- return "unknown";
- }
-}
-
-/* PE-image `-f`: architecture, image base, entry point, subsystem.
- * Counterpart to dump_file_header for inputs that kit_obj_open can't
- * parse yet (PE executables / DLLs vs .obj). */
-static void dump_pe_file_header(const char* label, const PeImage* pe) {
- driver_printf("%.*s:\tfile format pei-%.*s\n\n",
- KIT_SLICE_ARG(kit_slice_cstr(label)),
- KIT_SLICE_ARG(kit_slice_cstr(pe_machine_name(pe->machine))));
- driver_printf("architecture: %.*s, flags 0x%04x\n",
- KIT_SLICE_ARG(kit_slice_cstr(pe_machine_name(pe->machine))),
- (unsigned)pe->file_chars);
- if (pe->opt_magic == PE_OPT_HDR64_MAGIC) {
- driver_printf("start address 0x%016llx\n",
- (unsigned long long)(pe->image_base + pe->entry_rva));
- driver_printf(
- "image base: 0x%llx, entry rva: 0x%x, subsystem: %u (%.*s)\n\n",
- (unsigned long long)pe->image_base, pe->entry_rva,
- (unsigned)pe->subsystem,
- KIT_SLICE_ARG(kit_slice_cstr(pe_subsystem_name(pe->subsystem))));
- } else {
- driver_printf(
- "PE32 (magic 0x%x) — only PE32+ inspection is implemented\n\n",
- (unsigned)pe->opt_magic);
- }
-}
-
-/* Decode IMAGE_SECTION_HEADER.Characteristics into the GNU objdump tag
- * style used for COFF .obj inputs. Used by both dump_sections (via
- * render_sec_flags) and the PE section walker. */
-static void render_pe_sec_flags(uint32_t ch, char* buf, size_t cap) {
- size_t n = 0;
- const char* tags[16];
- int nt = 0;
- int i;
- /* Bit layout shared with render_sec_flags; PE images don't carry
- * BSS / TLS-by-name detection so we go straight from raw flags to
- * tags. */
- if (ch & 0x00000020u) tags[nt++] = "CODE";
- if (ch & 0x00000040u) tags[nt++] = "DATA";
- if (ch & 0x00000080u) tags[nt++] = "BSS";
- if (ch & OBJDUMP_IMAGE_SCN_LNK_INFO) tags[nt++] = "LINK_INFO";
- if (ch & OBJDUMP_IMAGE_SCN_LNK_REMOVE) tags[nt++] = "LINK_REMOVE";
- if (ch & OBJDUMP_IMAGE_SCN_LNK_COMDAT) tags[nt++] = "LINK_ONCE";
- if (ch & OBJDUMP_IMAGE_SCN_GPREL) tags[nt++] = "GPREL";
- if (ch & OBJDUMP_IMAGE_SCN_MEM_DISCARDABLE) tags[nt++] = "DISCARDABLE";
- if (ch & OBJDUMP_IMAGE_SCN_MEM_SHARED) tags[nt++] = "SHARED";
- if (ch & 0x20000000u) tags[nt++] = "EXEC";
- if (ch & 0x40000000u) tags[nt++] = "READ";
- if (ch & 0x80000000u) tags[nt++] = "WRITE";
- for (i = 0; i < nt && n + 1 < cap; ++i) {
- const char* t = tags[i];
- if (i > 0 && n + 1 < cap) buf[n++] = ',';
- while (*t && n + 1 < cap) buf[n++] = *t++;
- }
- buf[n] = '\0';
-}
-
-/* PE-image `-h`: walks IMAGE_SECTION_HEADER table directly from raw
- * bytes (kit_obj_open doesn't yet parse PE executables). Output
- * shape mirrors dump_sections for .obj inputs so postprocessing
- * grep-fu doesn't care which path produced the line. */
-static void dump_pe_sections(const char* label, const PeImage* pe,
- const uint8_t* buf, size_t buf_len,
- const ObjdumpOpts* opts) {
- uint16_t i;
- char flagbuf[160];
- char name[9];
- driver_printf("%.*s:\tSections (PE image):\n",
- KIT_SLICE_ARG(kit_slice_cstr(label)));
- driver_printf(
- "Idx Name VMA Size "
- "FileOff Align Flags\n");
- for (i = 0; i < pe->nsec; ++i) {
- size_t sh = pe->sec_off + (size_t)i * PE_SECTION_HEADER_SIZE;
- uint32_t vsize;
- uint32_t va;
- uint32_t raw_size;
- uint32_t raw_off;
- uint32_t ch;
- uint32_t align_field;
- unsigned align_log2;
- int j;
- if (sh + PE_SECTION_HEADER_SIZE > buf_len) break;
- for (j = 0; j < 8; ++j) name[j] = (char)buf[sh + (size_t)j];
- name[8] = '\0';
- vsize = pe_rd_u32(buf + sh + 8);
- va = pe_rd_u32(buf + sh + 12);
- raw_size = pe_rd_u32(buf + sh + 16);
- raw_off = pe_rd_u32(buf + sh + 20);
- ch = pe_rd_u32(buf + sh + 36);
- align_field = (ch >> 20) & 0xFu;
- align_log2 = align_field ? (align_field - 1u) : 0u;
- if (!j_match(opts, kit_slice_cstr(name))) continue;
- render_pe_sec_flags(ch, flagbuf, sizeof(flagbuf));
- driver_printf("%3u %-16s %016llx %08x %08x 2**%-2u %.*s\n", (unsigned)i,
- name, (unsigned long long)(pe->image_base + va),
- vsize ? vsize : raw_size, raw_off, align_log2,
- KIT_SLICE_ARG(kit_slice_cstr(flagbuf)));
- driver_printf(
- " "
- "Characteristics: 0x%08x\n",
- ch);
- }
- driver_printf("\n");
-}
-
-/* Walk a PE image (DOS → "PE\0\0" → COFF file header → optional header
- * → data directories) and print the highlights GNU objdump's `-p`
- * shows. Bails silently on any header that doesn't validate — leaves
- * the basic dump_obj() output untouched. */
-static void dump_pe_private(const char* label, const uint8_t* buf,
- size_t buf_len) {
- PeImage pe;
- uint32_t import_rva = 0;
- uint32_t import_size = 0;
- uint32_t i;
- if (!pe_parse_image(buf, buf_len, &pe) || !pe.valid) return;
- if (pe.opt_magic != PE_OPT_HDR64_MAGIC) {
- driver_printf("%.*s:\tPE optional header magic 0x%x (PE32) — skipping\n",
- KIT_SLICE_ARG(kit_slice_cstr(label)), (unsigned)pe.opt_magic);
- return;
- }
-
- driver_printf("\n%.*s:\tPE32+ private headers\n",
- KIT_SLICE_ARG(kit_slice_cstr(label)));
- driver_printf(" Magic: 0x%x (PE32+)\n", pe.opt_magic);
- driver_printf(" Machine: 0x%04x (%.*s)\n", (unsigned)pe.machine,
- KIT_SLICE_ARG(kit_slice_cstr(pe_machine_name(pe.machine))));
- driver_printf(" Characteristics: 0x%04x\n", (unsigned)pe.file_chars);
- driver_printf(" ImageBase: 0x%llx\n",
- (unsigned long long)pe.image_base);
- driver_printf(" AddressOfEntryPoint: 0x%x\n", pe.entry_rva);
- driver_printf(" Subsystem: %u (%.*s)\n", (unsigned)pe.subsystem,
- KIT_SLICE_ARG(kit_slice_cstr(pe_subsystem_name(pe.subsystem))));
- driver_printf(" DllCharacteristics: 0x%04x\n", (unsigned)pe.dllchars);
- driver_printf(" NumberOfSections: %u\n", (unsigned)pe.nsec);
-
- if (pe.dir_off + PE_NUM_DATA_DIRS * PE_DATA_DIRECTORY_SIZE > buf_len) return;
- driver_printf("\nData Directories:\n");
- driver_printf(" Idx Name RVA Size\n");
- for (i = 0; i < PE_NUM_DATA_DIRS; ++i) {
- uint32_t rva = pe_rd_u32(buf + pe.dir_off + i * PE_DATA_DIRECTORY_SIZE);
- uint32_t sz = pe_rd_u32(buf + pe.dir_off + i * PE_DATA_DIRECTORY_SIZE + 4);
- if (rva == 0 && sz == 0) continue;
- driver_printf(" %2u %-14s 0x%08x 0x%08x\n", i, pe_dir_name(i), rva, sz);
- if (i == PE_DIR_IMPORT) {
- import_rva = rva;
- import_size = sz;
- }
- }
-
- if (import_rva && import_size) {
- pe_dump_imports(buf, buf_len, pe.sec_off, pe.nsec, import_rva, import_size);
- }
-}
/* Render objdump's "file format" spelling: the obj layer's canonical bare
* name (elf/coff/macho/wasm) plus the bitwidth suffix this tool presents.
@@ -638,6 +262,91 @@ static const char* arch_str(KitArchKind arch) {
return "unknown";
}
+/* Collected PE optional-header escape-hatch view, gathered from the neutral
+ * raw-fields iterator (KitObjImageRaw): the 16 data directories plus the
+ * Subsystem / DllCharacteristics scalars. Returns 0 if the image carries no
+ * raw fields (i.e. not a PE image). */
+typedef struct PeRaw {
+ uint16_t subsystem;
+ uint16_t dllchars;
+ uint32_t dir_rva[16];
+ uint32_t dir_size[16];
+} PeRaw;
+
+static int pe_collect_raw(KitObjFile* f, PeRaw* out) {
+ KitObjImageRawIter* it = NULL;
+ KitObjImageRaw r;
+ memset(out, 0, sizeof *out);
+ if (kit_obj_image_rawiter_new(f, &it) != KIT_OK) return 0;
+ while (kit_obj_image_rawiter_next(it, &r) == KIT_ITER_ITEM) {
+ if (r.tag < 16) {
+ out->dir_rva[r.tag] = (uint32_t)r.value;
+ out->dir_size[r.tag] = (uint32_t)r.extra;
+ } else if (r.tag == KIT_OBJ_RAW_PE_SUBSYSTEM) {
+ out->subsystem = (uint16_t)r.value;
+ } else if (r.tag == KIT_OBJ_RAW_PE_DLLCHARS) {
+ out->dllchars = (uint16_t)r.value;
+ }
+ }
+ kit_obj_image_rawiter_free(it);
+ return 1;
+}
+
+/* PE-image `-p`: the GNU objdump "PE32+ private headers" view — optional
+ * header highlights, data directories, and the import tables — rendered
+ * entirely from the neutral image API (kit_obj_image_info + the raw-fields
+ * iterator + the dependency iterator). */
+static void dump_pe_private(KitObjFile* f, const char* label) {
+ PeRaw raw;
+ KitObjImageInfo info;
+ KitTargetSpec target = kit_obj_target(f);
+ KitObjDepIter* dit = NULL;
+ KitObjDepInfo dep;
+ uint32_t i;
+ int have_imports = 0;
+ if (!pe_collect_raw(f, &raw)) return;
+ if (kit_obj_image_info(f, &info) != KIT_OK) return;
+
+ driver_printf("\n%.*s:\tPE32+ private headers\n",
+ KIT_SLICE_ARG(kit_slice_cstr(label)));
+ driver_printf(" Magic: 0x20b (PE32+)\n");
+ driver_printf(" Machine: %.*s\n",
+ KIT_SLICE_ARG(kit_slice_cstr(arch_str(target.arch))));
+ driver_printf(" ImageBase: 0x%llx\n",
+ (unsigned long long)info.image_base);
+ driver_printf(
+ " AddressOfEntryPoint: 0x%llx\n",
+ (unsigned long long)(info.entry > info.image_base
+ ? info.entry - info.image_base
+ : 0));
+ driver_printf(" Subsystem: %u (%.*s)\n", (unsigned)raw.subsystem,
+ KIT_SLICE_ARG(kit_slice_cstr(pe_subsystem_name(raw.subsystem))));
+ driver_printf(" DllCharacteristics: 0x%04x\n", (unsigned)raw.dllchars);
+
+ driver_printf("\nData Directories:\n");
+ driver_printf(" Idx Name RVA Size\n");
+ for (i = 0; i < 16; ++i) {
+ if (raw.dir_rva[i] == 0 && raw.dir_size[i] == 0) continue;
+ driver_printf(" %2u %-14s 0x%08x 0x%08x\n", i, pe_dir_name(i),
+ raw.dir_rva[i], raw.dir_size[i]);
+ }
+
+ if (kit_obj_depiter_new(f, &dit) == KIT_OK) {
+ while (kit_obj_depiter_next(dit, &dep) == KIT_ITER_ITEM) {
+ uint32_t k;
+ if (!have_imports) {
+ driver_printf("\nThe Import Tables:\n");
+ have_imports = 1;
+ }
+ driver_printf(" DLL Name: %.*s\n", KIT_SLICE_ARG(dep.name));
+ for (k = 0; k < dep.nimports; ++k)
+ driver_printf(" Name: %.*s\n", KIT_SLICE_ARG(dep.imports[k]));
+ }
+ kit_obj_depiter_free(dit);
+ }
+ driver_printf("\n");
+}
+
static char sym_bind_char(KitSymBind b) {
switch (b) {
case KIT_SB_LOCAL:
@@ -1066,10 +775,11 @@ static void dump_disasm(const KitDisasmContext* dctx, KitObjFile* f,
}
/* `-f`: GNU objdump-style file header summary. Object files have no
- * meaningful entry point so start address is always 0; PE images are
- * handled separately by dump_pe_private. The flags line summarizes
- * whether the input has symbols and relocations so it's clear at a
- * glance whether further -t / -r work is going to be productive. */
+ * meaningful entry point so start address is always 0. For a PE image we
+ * also surface the Windows subsystem (via the raw-fields escape hatch). The
+ * flags line summarizes whether the input has symbols and relocations so
+ * it's clear at a glance whether further -t / -r work is going to be
+ * productive. */
static void dump_file_header(KitObjFile* f, const char* label) {
KitTargetSpec target = kit_obj_target(f);
KitObjFmt fmt = kit_obj_fmt(f);
@@ -1128,6 +838,13 @@ static void dump_file_header(KitObjFile* f, const char* label) {
fmt_str(fmt, target.ptr_size, fmt_buf, sizeof fmt_buf))),
nsec, nsym);
}
+ if (fmt == KIT_OBJ_COFF && kind != KIT_OBJ_KIND_REL) {
+ PeRaw raw;
+ if (pe_collect_raw(f, &raw))
+ driver_printf(
+ "subsystem: %u (%.*s)\n\n", (unsigned)raw.subsystem,
+ KIT_SLICE_ARG(kit_slice_cstr(pe_subsystem_name(raw.subsystem))));
+ }
(void)label;
}
@@ -1524,11 +1241,12 @@ static unsigned u32_log2(uint32_t v) {
return n;
}
-/* Private/program headers (-p): the linked-image view — entry point, load
- * segments, and dynamic dependencies. Format-neutral across ELF / Mach-O /
- * (eventually) PE via the kit_obj image API. Relocatable objects have no
- * image and report so. */
-static void dump_private(KitObjFile* f) {
+/* Private/program headers (-p): the linked-image view. PE images get the
+ * GNU objdump "PE32+ private headers" rendering (optional header + data
+ * directories + import tables); ELF / Mach-O get the entry/segments/dynamic
+ * view. Both are driven by the neutral kit_obj image API. Relocatable
+ * objects have no image and report so. */
+static void dump_private(KitObjFile* f, const char* label) {
KitObjImageInfo info;
KitObjSegIter* sit = NULL;
KitObjDepIter* dit = NULL;
@@ -1536,6 +1254,11 @@ static void dump_private(KitObjFile* f) {
KitObjDepInfo dep;
int have_info;
+ if (kit_obj_fmt(f) == KIT_OBJ_COFF && kit_obj_kind(f) != KIT_OBJ_KIND_REL) {
+ dump_pe_private(f, label);
+ return;
+ }
+
if (kit_obj_kind(f) == KIT_OBJ_KIND_REL) {
driver_printf(
"Private headers:\n"
@@ -1615,7 +1338,7 @@ static void dump_obj(const KitContext* ctx, const KitDisasmContext* dctx,
if (opts->h) dump_groups(f, opts);
if (opts->t) dump_symbols(f, opts, 0);
if (opts->T) dump_symbols(f, opts, 1);
- if (opts->p) dump_private(f);
+ if (opts->p) dump_private(f, label);
if (opts->s) dump_hex(f, opts);
if (opts->d || opts->D) dump_disasm(dctx, f, opts, image);
if (opts->r) dump_relocs(f, opts);
@@ -1907,45 +1630,15 @@ int driver_objdump(int argc, char** argv) {
case KIT_BIN_MACHO:
case KIT_BIN_WASM: {
KitObjFile* f = NULL;
- /* PE executables aren't yet readable via kit_obj_open (the
- * obj reader is .obj-shaped only). For PE inputs we serve -f /
- * -h / -p by walking the raw image bytes; -t / -d / -r / -s
- * still need an ObjFile and are skipped with a soft error so
- * the other ops don't get swallowed. */
+ /* PE executables / DLLs open through kit_obj_open like every other
+ * format (read_coff dispatches the DOS 'MZ' magic to the image
+ * reader); the whole dump flows through the neutral dump_obj path. */
if (kit_obj_open(&ctx, kit_slice_cstr(a), &input, &f) != KIT_OK) {
- if (bin == KIT_BIN_PE) {
- PeImage pe;
- int parsed = pe_parse_image(input.data, input.len, &pe) && pe.valid;
- int handled = 0;
- if (parsed && opts.f) {
- dump_pe_file_header(a, &pe);
- handled = 1;
- }
- if (parsed && opts.h && pe.opt_magic == PE_OPT_HDR64_MAGIC) {
- dump_pe_sections(a, &pe, input.data, input.len, &opts);
- handled = 1;
- }
- if (opts.p) {
- dump_pe_private(a, input.data, input.len);
- handled = 1;
- }
- if (!handled) {
- driver_errf(OBJDUMP_TOOL,
- "%.*s: PE images support only -f / -h / -p; "
- "use -p for image details",
- KIT_SLICE_ARG(kit_slice_cstr(a)));
- rc = 1;
- }
- } else {
- driver_errf(OBJDUMP_TOOL, "failed to parse: %.*s",
- KIT_SLICE_ARG(kit_slice_cstr(a)));
- rc = 1;
- }
+ driver_errf(OBJDUMP_TOOL, "failed to parse: %.*s",
+ KIT_SLICE_ARG(kit_slice_cstr(a)));
+ rc = 1;
} else {
dump_obj(&ctx, dctx_p, a, f, &opts, &input);
- if (opts.p && bin == KIT_BIN_PE) {
- dump_pe_private(a, input.data, input.len);
- }
kit_obj_free(f);
}
break;
diff --git a/include/kit/object.h b/include/kit/object.h
@@ -404,6 +404,33 @@ KIT_API KitStatus kit_obj_dynsymiter_new(KitObjFile*, KitObjSymIter** out);
* kit_obj_reliter_next / _free. Empty on relocatable objects. */
KIT_API KitStatus kit_obj_dynreliter_new(KitObjFile*, KitObjRelocIter** out);
+/* Raw, format-specific image fields that don't fit the neutral model. One
+ * neutral signature with per-format tag semantics, in the spirit of
+ * kit_obj_section_format_flags:
+ * PE : data directories tag = 0..15 (index), value = RVA, extra = size
+ * subsystem tag = KIT_OBJ_RAW_PE_SUBSYSTEM, value = u16
+ * dllcharacteristics tag = KIT_OBJ_RAW_PE_DLLCHARS, value = u16
+ * ELF : .dynamic entries tag = d_tag, value = d_val, extra = 0
+ * Mach-O: load commands tag = cmd, value = file offset, extra = cmdsize
+ * Reserved tags use the high range (>= 0x80000000) so they never collide
+ * with a PE directory index or an ELF DT_* tag. _new returns KIT_NOT_FOUND on
+ * a relocatable object (no image); an image with no raw fields yields
+ * KIT_ITER_END immediately. */
+typedef struct KitObjImageRaw {
+ uint32_t tag;
+ uint64_t value;
+ uint64_t extra;
+} KitObjImageRaw;
+
+#define KIT_OBJ_RAW_PE_SUBSYSTEM 0x80000000u
+#define KIT_OBJ_RAW_PE_DLLCHARS 0x80000001u
+
+typedef struct KitObjImageRawIter KitObjImageRawIter;
+KIT_API KitStatus kit_obj_image_rawiter_new(KitObjFile*, KitObjImageRawIter** out);
+KIT_API KitIterResult kit_obj_image_rawiter_next(KitObjImageRawIter*,
+ KitObjImageRaw* out);
+KIT_API void kit_obj_image_rawiter_free(KitObjImageRawIter*);
+
/* Roundtrip: open an object via kit_obj_open, then hand its underlying
* builder back. The builder is the same one the reader populated; it is
* already finalized, so callers may inspect it (e.g. iterate sections via
diff --git a/mk/test.mk b/mk/test.mk
@@ -659,6 +659,7 @@ COFF_IMPORT_SMOKE_BIN = build/test/pe-import-smoke
COFF_IMPORT_MINGW_BIN = build/test/pe-import-mingw
COFF_DSO_FORWARDER_BIN = build/test/pe-dso-forwarder
COFF_MIXED_ARCHIVE_BIN = build/test/pe-mixed-archive
+COFF_IMAGE_READ_BIN = build/test/pe-image-read
LLVM_MINGW_SYSROOT_X64_MARKER = build/llvm-mingw/20260602/ucrt/x86_64-w64-mingw32/PROVENANCE
LLVM_MINGW_SYSROOT_AARCH64_MARKER = build/llvm-mingw/20260602/ucrt/aarch64-w64-mingw32/PROVENANCE
JIT_RUNNER = build/test/jit-runner
@@ -715,6 +716,14 @@ $(COFF_MIXED_ARCHIVE_BIN): test/coff/pe-mixed-archive.c $(LIB_OBJS)
@mkdir -p $(dir $@)
$(CC) $(HARNESS_CFLAGS) -Isrc test/coff/pe-mixed-archive.c $(LIB_OBJS) -o $@
+# PE32+ linked-image reader round-trip (test/coff/pe-image-read.c). Links a
+# tiny PIE .exe in memory (import + base reloc), re-opens it via kit_obj_open,
+# and asserts the neutral image view + raw escape hatch. Needs no external
+# toolchain — runs on every host.
+$(COFF_IMAGE_READ_BIN): test/coff/pe-image-read.c $(LIB_OBJS)
+ @mkdir -p $(dir $@)
+ $(CC) $(HARNESS_CFLAGS) -Isrc test/coff/pe-image-read.c $(LIB_OBJS) -o $@
+
$(LLVM_MINGW_SYSROOT_X64_MARKER): scripts/llvm_mingw_sysroot.sh
@bash scripts/llvm_mingw_sysroot.sh prepare x64
@@ -750,11 +759,12 @@ test-elf: lib bin-soft $(ROUNDTRIP_BIN)
# PE/COFF round-trip harness plus optional hosted Windows smoke. The
# UCRT smoke self-skips when llvm-mingw is not installed.
-test-coff: lib bin rt-aarch64-windows $(ROUNDTRIP_BIN_COFF) $(COFF_IMPORT_SMOKE_BIN) $(COFF_DSO_FORWARDER_BIN) $(COFF_MIXED_ARCHIVE_BIN)
+test-coff: lib bin rt-aarch64-windows $(ROUNDTRIP_BIN_COFF) $(COFF_IMPORT_SMOKE_BIN) $(COFF_DSO_FORWARDER_BIN) $(COFF_MIXED_ARCHIVE_BIN) $(COFF_IMAGE_READ_BIN)
$(ROUNDTRIP_BIN_COFF)
$(COFF_IMPORT_SMOKE_BIN)
$(COFF_DSO_FORWARDER_BIN)
$(COFF_MIXED_ARCHIVE_BIN)
+ $(COFF_IMAGE_READ_BIN)
bash test/coff/windows-ucrt-hosted-smoke.sh
bash test/coff/windows-system-dlls-smoke.sh
diff --git a/src/api/object_detect.c b/src/api/object_detect.c
@@ -178,34 +178,67 @@ static KitStatus detect_elf(const u8* d, size_t len, KitTargetSpec* out) {
return KIT_OK;
}
-static KitStatus detect_coff(const u8* d, size_t len, KitTargetSpec* out) {
- u16 machine;
- KitArchKind arch;
- const ObjFormatImpl* fmt;
- const ObjCoffArchOps* ops;
- if (len < 2) return KIT_MALFORMED;
- machine = (u16)d[0] | ((u16)d[1] << 8);
-
- /* Resolve the arch through the COFF format's machine reverse map (which
- * also aliases ARM64EC -> ARM64). The registry models only the
- * link/codegen arches (AMD64 / ARM64); the legacy ABI-classifiable COFF
- * machines it does not carry are mapped explicitly to preserve detection
- * for objects classified as COFF by kit_detect_fmt. */
- fmt = obj_format_lookup(KIT_OBJ_COFF);
- ops = (fmt && fmt->coff_machine) ? fmt->coff_machine(machine) : NULL;
+/* Resolve a COFF Machine number to a KitArchKind through the registry's
+ * coff_machine reverse map (which aliases ARM64EC -> ARM64). The registry
+ * models only the link/codegen arches (AMD64 / ARM64); the legacy
+ * ABI-classifiable machines it does not carry are mapped explicitly to
+ * preserve detection. Returns 1 and writes *out on success; 0 if the
+ * machine is unsupported. Shared by detect_coff (.obj) and detect_pe
+ * (linked image). */
+static int coff_machine_to_arch(u16 machine, KitArchKind* out) {
+ const ObjFormatImpl* fmt = obj_format_lookup(KIT_OBJ_COFF);
+ const ObjCoffArchOps* ops =
+ (fmt && fmt->coff_machine) ? fmt->coff_machine(machine) : NULL;
if (ops) {
- arch = ops->arch;
+ *out = ops->arch;
} else if (machine == 0x014Cu) { /* IMAGE_FILE_MACHINE_I386 */
- arch = KIT_ARCH_X86_32;
+ *out = KIT_ARCH_X86_32;
} else if (machine == 0x01C4u) { /* IMAGE_FILE_MACHINE_ARMNT */
- arch = KIT_ARCH_ARM_32;
+ *out = KIT_ARCH_ARM_32;
} else if (machine == 0x5032u) { /* IMAGE_FILE_MACHINE_RISCV32 */
- arch = KIT_ARCH_RV32;
+ *out = KIT_ARCH_RV32;
} else if (machine == 0x5064u) { /* IMAGE_FILE_MACHINE_RISCV64 */
- arch = KIT_ARCH_RV64;
+ *out = KIT_ARCH_RV64;
} else {
- return KIT_UNSUPPORTED;
+ return 0;
}
+ return 1;
+}
+
+static KitStatus detect_coff(const u8* d, size_t len, KitTargetSpec* out) {
+ u16 machine;
+ KitArchKind arch;
+ if (len < 2) return KIT_MALFORMED;
+ machine = (u16)d[0] | ((u16)d[1] << 8);
+ if (!coff_machine_to_arch(machine, &arch)) return KIT_UNSUPPORTED;
+ detect_target_defaults(out);
+ out->obj = KIT_OBJ_COFF;
+ out->os = KIT_OS_WINDOWS;
+ detect_set_ptr(out, arch);
+ return KIT_OK;
+}
+
+/* PE image (DOS 'MZ' stub + "PE\0\0" signature). Unlike a bare .obj, the
+ * COFF Machine word lives in the file header at e_lfanew+4, not at offset 0
+ * (the DOS stub), so this can't reuse detect_coff's offset-0 read. Routes a
+ * well-formed image to KIT_OBJ_COFF / KIT_OS_WINDOWS; read_coff then
+ * dispatches the 'MZ' magic to read_coff_image. A 'MZ' prefix with no valid
+ * PE signature (a DOS-only stub) is rejected as malformed. */
+static KitStatus detect_pe(const u8* d, size_t len, KitTargetSpec* out) {
+ u32 e_lfanew, pe_sig;
+ u16 machine;
+ KitArchKind arch;
+ if (len < 64) return KIT_MALFORMED; /* DOS header */
+ if (!(d[0] == 'M' && d[1] == 'Z')) return KIT_MALFORMED;
+ e_lfanew = (u32)d[60] | ((u32)d[61] << 8) | ((u32)d[62] << 16) |
+ ((u32)d[63] << 24);
+ /* Need the 4-byte PE signature + the 20-byte IMAGE_FILE_HEADER. */
+ if ((u64)e_lfanew + 4u + 20u > (u64)len) return KIT_MALFORMED;
+ pe_sig = (u32)d[e_lfanew] | ((u32)d[e_lfanew + 1] << 8) |
+ ((u32)d[e_lfanew + 2] << 16) | ((u32)d[e_lfanew + 3] << 24);
+ if (pe_sig != 0x00004550u) return KIT_MALFORMED; /* "PE\0\0" */
+ machine = (u16)d[e_lfanew + 4] | ((u16)d[e_lfanew + 5] << 8);
+ if (!coff_machine_to_arch(machine, &arch)) return KIT_UNSUPPORTED;
detect_target_defaults(out);
out->obj = KIT_OBJ_COFF;
out->os = KIT_OS_WINDOWS;
@@ -282,6 +315,8 @@ KitStatus kit_detect_target(const uint8_t* data, size_t len,
return detect_elf(data, len, out);
#endif
#if KIT_OBJ_COFF_ENABLED
+ case KIT_BIN_PE:
+ return detect_pe(data, len, out);
case KIT_BIN_COFF:
return detect_coff(data, len, out);
#endif
diff --git a/src/api/object_file.c b/src/api/object_file.c
@@ -774,3 +774,44 @@ void kit_obj_rpathiter_free(KitObjRpathIter* it) {
h = it->file->ctx->heap;
h->free(h, it, sizeof(*it));
}
+
+struct KitObjImageRawIter {
+ KitObjFile* file;
+ u32 idx;
+};
+
+KitStatus kit_obj_image_rawiter_new(KitObjFile* f, KitObjImageRawIter** out) {
+ Heap* h;
+ KitObjImageRawIter* it;
+ if (!f || !out) return KIT_INVALID;
+ if (!obj_image(f->ob)) return KIT_NOT_FOUND; /* relocatable: no image */
+ h = f->ctx->heap;
+ it = (KitObjImageRawIter*)h->alloc(h, sizeof(*it),
+ _Alignof(KitObjImageRawIter));
+ if (!it) return KIT_NOMEM;
+ it->file = f;
+ it->idx = 0;
+ *out = it;
+ return KIT_OK;
+}
+
+KitIterResult kit_obj_image_rawiter_next(KitObjImageRawIter* it,
+ KitObjImageRaw* out) {
+ const ObjImage* im;
+ const ObjImageRaw* r;
+ if (!it || !out) return KIT_ITER_ERROR;
+ im = obj_image(it->file->ob);
+ if (it->idx >= obj_image_nraws(im)) return KIT_ITER_END;
+ r = obj_image_raw(im, it->idx++);
+ out->tag = r->tag;
+ out->value = r->value;
+ out->extra = r->extra;
+ return KIT_ITER_ITEM;
+}
+
+void kit_obj_image_rawiter_free(KitObjImageRawIter* it) {
+ Heap* h;
+ if (!it) return;
+ h = it->file->ctx->heap;
+ h->free(h, it, sizeof(*it));
+}
diff --git a/src/obj/coff/read.c b/src/obj/coff/read.c
@@ -7,12 +7,12 @@
* section-definition aux records.
*
* Scope: IMAGE_FILE_MACHINE_AMD64 and IMAGE_FILE_MACHINE_ARM64. PE
- * executables (with a non-zero SizeOfOptionalHeader) are rejected — a
- * future read_coff_pe would handle those. Microsoft "short import"
- * records (Sig1=0, Sig2=0xFFFF) found inside .lib archive members are
- * detected at entry and dispatched to read_coff_short_import, which
- * synthesizes a DSO-shaped ObjBuilder annotated with the providing
- * DLL name via obj_set_coff_import_dll. */
+ * *images* (executables / DLLs, beginning with the DOS 'MZ' stub) are
+ * detected at entry and dispatched to read_coff_image (read_image.c).
+ * Microsoft "short import" records (Sig1=0, Sig2=0xFFFF) found inside
+ * .lib archive members are likewise detected at entry and dispatched to
+ * read_coff_short_import, which synthesizes a DSO-shaped ObjBuilder
+ * annotated with the providing DLL name via obj_set_coff_import_dll. */
#include <string.h>
@@ -21,6 +21,7 @@
#include "core/pool.h"
#include "core/slice.h"
#include "obj/coff/coff.h"
+#include "obj/coff/read_util.h"
#include "obj/format.h"
/* ---- section-header scratch ---- */
@@ -87,44 +88,9 @@ static void resolve_section_name(const char raw[8], const u8* strtab,
*len_out = n;
}
-/* ---- characteristics -> SecKind / SecFlag / SecSem ---- */
-
-static u16 coff_sec_kind(const char* name, u32 nlen, u32 ch) {
- if (ch & IMAGE_SCN_CNT_UNINITIALIZED_DATA) return SEC_BSS;
- if (ch & IMAGE_SCN_CNT_CODE) return SEC_TEXT;
- if (ch & IMAGE_SCN_MEM_EXECUTE) return SEC_TEXT;
- if (nlen >= 7 && memcmp(name, ".debug_", 7) == 0) return SEC_DEBUG;
- /* The MS toolchain spells DWARF section names with a leading ".debug$"
- * (CodeView) — keep ELF-style ".debug_" detection but also treat the
- * MS form as debug. */
- if (nlen >= 7 && memcmp(name, ".debug$", 7) == 0) return SEC_DEBUG;
- if (ch & IMAGE_SCN_CNT_INITIALIZED_DATA) {
- if (ch & IMAGE_SCN_MEM_WRITE) return SEC_DATA;
- return SEC_RODATA;
- }
- return SEC_OTHER;
-}
-
-static u16 coff_sec_flags(const char* name, u32 nlen, u32 ch) {
- u16 f = 0;
- if (ch & IMAGE_SCN_MEM_READ) f |= SF_ALLOC;
- if (ch & IMAGE_SCN_MEM_EXECUTE) f |= SF_EXEC;
- if (ch & IMAGE_SCN_MEM_WRITE) f |= SF_WRITE;
- if (ch & IMAGE_SCN_LNK_COMDAT) f |= SF_GROUP;
- /* TLS sections in PE are spelled ".tls$<suffix>" (e.g. ".tls$", ".tls$ZZZ").
- * There is no characteristics bit for TLS — detection is name-based. */
- if (nlen >= 5 && memcmp(name, ".tls$", 5) == 0) f |= SF_TLS;
- if (nlen == 4 && memcmp(name, ".tls", 4) == 0) f |= SF_TLS;
- return f;
-}
-
-/* Bits 20..23 of Characteristics encode alignment as (log2(align)+1).
- * 0 means "default"; we collapse to align=1 for round-trip purposes. */
-static u32 coff_sec_align(u32 ch) {
- u32 n = (ch & IMAGE_SCN_ALIGN_MASK) >> 20;
- if (n == 0) return 1;
- return 1u << (n - 1u);
-}
+/* characteristics -> SecKind / SecFlag / alignment live in read_util.c
+ * (coff_sec_kind / coff_sec_flags / coff_sec_align), shared with the
+ * image reader. */
/* ---- symbol-name resolution ---- */
@@ -347,6 +313,14 @@ ObjBuilder* read_coff(Compiler* c, const char* name, const u8* data,
return read_coff_short_import(c, name, data, len);
}
+ /* PE image? A linked .exe/.dll begins with the DOS 'MZ' stub, not a bare
+ * IMAGE_FILE_HEADER — dispatch to the image reader, which walks the
+ * DOS -> PE-sig -> file/optional headers. (Placed before the offset-0
+ * machine read below, which assumes a bare header, and before the
+ * optional-header rejection.) */
+ if (len >= 2 && coff_rd_u16(data + 0) == IMAGE_DOS_SIGNATURE)
+ return read_coff_image(c, name, data, len);
+
u16 machine = coff_rd_u16(data + 0);
u16 nsections = coff_rd_u16(data + 2);
/* data + 4: TimeDateStamp (4 bytes, ignored). */
@@ -614,9 +588,7 @@ ObjBuilder* read_coff(Compiler* c, const char* name, const u8* data,
(int)sec_num);
}
- /* WEAK_EXTERNAL primary: aux record carries TagIndex + Characteristics.
- * kit's model has SB_WEAK; the fallback symbol is link-time
- * resolution by name and we drop the explicit index. */
+ /* WEAK_EXTERNAL primary: aux record carries TagIndex + Characteristics. */
if (sclass == IMAGE_SYM_CLASS_WEAK_EXTERNAL) bind = SB_WEAK;
Sym sn =
diff --git a/src/obj/coff/read_dso.c b/src/obj/coff/read_dso.c
@@ -26,55 +26,7 @@
#include "core/pool.h"
#include "core/slice.h"
#include "obj/coff/coff.h"
-
-/* ---- RVA -> file offset ----
- * Walks the section table once per call. Returns 1 on success and
- * fills *off_out; returns 0 if the RVA falls outside every section's
- * [VirtualAddress, VirtualAddress + max(VirtualSize, SizeOfRawData))
- * range or the resulting file offset would exceed `len`. */
-static int rva_to_offset(const u8* shdrs, u16 nsec, u32 rva, size_t len,
- u64* off_out) {
- for (u16 i = 0; i < nsec; ++i) {
- const u8* sh = shdrs + (u64)i * COFF_SECTION_HEADER_SIZE;
- u32 vsize = coff_rd_u32(sh + 8);
- u32 vaddr = coff_rd_u32(sh + 12);
- u32 raw_size = coff_rd_u32(sh + 16);
- u32 raw_ptr = coff_rd_u32(sh + 20);
- /* Some linkers leave VirtualSize == 0 in objects; use raw_size as
- * a fallback so we still resolve RVAs in well-formed images. */
- u32 span = vsize ? vsize : raw_size;
- if (rva >= vaddr && rva < vaddr + span) {
- u64 delta = (u64)(rva - vaddr);
- if (delta >= raw_size) return 0; /* RVA past on-disk data */
- u64 off = (u64)raw_ptr + delta;
- if (off >= len) return 0;
- *off_out = off;
- return 1;
- }
- }
- return 0;
-}
-
-/* Read a NUL-terminated string starting at `off`, bounded by `len`.
- * Returns the string length (excluding NUL); writes the pointer to
- * *out. Returns 0 if off is out of range or the string is not
- * terminated within the file. */
-static u32 read_cstr(const u8* data, size_t len, u64 off, const char** out) {
- if (off >= len) {
- *out = "";
- return 0;
- }
- const char* s = (const char*)(data + off);
- u64 max = (u64)len - off;
- u64 n = 0;
- while (n < max && s[n] != '\0') ++n;
- if (n == max) {
- *out = "";
- return 0;
- } /* unterminated */
- *out = s;
- return (u32)n;
-}
+#include "obj/coff/read_util.h"
ObjBuilder* read_coff_dso(Compiler* c, const char* name, const u8* data,
size_t len, Sym* soname_out) {
@@ -152,7 +104,7 @@ ObjBuilder* read_coff_dso(Compiler* c, const char* name, const u8* data,
}
u64 exp_off;
- if (!rva_to_offset(shdrs, nsec, export_rva, len, &exp_off))
+ if (!coff_rva_to_offset(shdrs, nsec, export_rva, len, &exp_off))
compiler_panic(c, SRCLOC_NONE,
"read_coff_dso: export directory RVA 0x%x out of range",
export_rva);
@@ -172,11 +124,11 @@ ObjBuilder* read_coff_dso(Compiler* c, const char* name, const u8* data,
/* ---- DLL name (soname) ---- */
if (name_rva) {
u64 name_off;
- if (!rva_to_offset(shdrs, nsec, name_rva, len, &name_off))
+ if (!coff_rva_to_offset(shdrs, nsec, name_rva, len, &name_off))
compiler_panic(c, SRCLOC_NONE,
"read_coff_dso: DLL name RVA 0x%x out of range", name_rva);
const char* dll_name;
- u32 nlen = read_cstr(data, len, name_off, &dll_name);
+ u32 nlen = coff_read_cstr(data, len, name_off, &dll_name);
if (nlen && soname_out)
*soname_out =
pool_intern_slice(c->global, (Slice){.s = dll_name, .len = nlen});
@@ -185,13 +137,13 @@ ObjBuilder* read_coff_dso(Compiler* c, const char* name, const u8* data,
/* ---- resolve EAT / ENT / ordinal table once ---- */
u64 eat_off = 0, ent_off = 0, ord_off = 0;
if (num_names) {
- if (!rva_to_offset(shdrs, nsec, eat_rva, len, &eat_off))
+ if (!coff_rva_to_offset(shdrs, nsec, eat_rva, len, &eat_off))
compiler_panic(c, SRCLOC_NONE, "read_coff_dso: EAT RVA 0x%x out of range",
eat_rva);
- if (!rva_to_offset(shdrs, nsec, ent_rva, len, &ent_off))
+ if (!coff_rva_to_offset(shdrs, nsec, ent_rva, len, &ent_off))
compiler_panic(c, SRCLOC_NONE, "read_coff_dso: ENT RVA 0x%x out of range",
ent_rva);
- if (!rva_to_offset(shdrs, nsec, ord_rva, len, &ord_off))
+ if (!coff_rva_to_offset(shdrs, nsec, ord_rva, len, &ord_off))
compiler_panic(c, SRCLOC_NONE,
"read_coff_dso: ordinal table RVA 0x%x out of range",
ord_rva);
@@ -217,9 +169,9 @@ ObjBuilder* read_coff_dso(Compiler* c, const char* name, const u8* data,
(void)func_rva; /* see comment above re: forwarders */
u64 name_off;
- if (!rva_to_offset(shdrs, nsec, nrva, len, &name_off)) continue;
+ if (!coff_rva_to_offset(shdrs, nsec, nrva, len, &name_off)) continue;
const char* nm;
- u32 nlen = read_cstr(data, len, name_off, &nm);
+ u32 nlen = coff_read_cstr(data, len, name_off, &nm);
if (!nlen) continue;
Sym sn = pool_intern_slice(c->global, (Slice){.s = nm, .len = nlen});
diff --git a/src/obj/coff/read_image.c b/src/obj/coff/read_image.c
@@ -0,0 +1,384 @@
+/* PE32+ linked-image reader. Peer of read_elf_image / read_macho_image:
+ * parses a linked Windows executable (.exe) or DLL (.dll) into the neutral
+ * ObjImage view that kit_obj_open / objdump consume — segments, entry point,
+ * image base, dependencies + imports, dynamic symbols (exports + imports),
+ * and dynamic relocations (base relocs). A full section/symbol view is
+ * populated through the ObjBuilder Section table as well, so -h / -s / -d
+ * work the same way they do for ELF / Mach-O images.
+ *
+ * Dispatched from read_coff on the DOS 'MZ' magic (read.c). Handles both
+ * subkinds: IMAGE_FILE_DLL clear -> OBJ_KIND_EXEC, set -> OBJ_KIND_DYN.
+ *
+ * Leniency: truncated *core* headers (DOS / PE sig / file / optional /
+ * section table) panic -> the kit_obj_open setjmp turns that into
+ * KIT_MALFORMED. Malformed *sub-tables* (export / import / base-reloc
+ * directories) are skipped, yielding a partial-but-useful inspection view,
+ * matching read_elf_image / read_macho_image. */
+
+#include <string.h>
+
+#include <kit/cg.h>
+#include <kit/object.h> /* KIT_OBJ_RAW_PE_* reserved tags */
+
+#include "core/arena.h"
+#include "core/heap.h"
+#include "core/pool.h"
+#include "core/slice.h"
+#include "obj/coff/coff.h"
+#include "obj/coff/read_util.h"
+#include "obj/format.h"
+
+static Sym intern(Compiler* c, const char* s, u32 n) {
+ return n ? pool_intern_slice(c->global, (Slice){.s = s, .len = n}) : 0;
+}
+
+/* ---- exports -> dynsyms + soname ----
+ * Mirrors read_coff_dso's export-directory walk, but emits ObjImageSym
+ * entries (defined, value = ImageBase + func RVA) and sets the DLL's own
+ * Name as the image soname. Lenient: any out-of-range sub-table aborts the
+ * export view rather than panicking. */
+static void read_pe_exports(Compiler* c, ObjImage* im, const u8* data,
+ size_t len, const u8* shdrs, u16 nsec,
+ const u8* data_dir, u32 num_dirs, u64 image_base) {
+ if ((u32)IMAGE_DIRECTORY_ENTRY_EXPORT >= num_dirs) return;
+ const u8* dd =
+ data_dir + IMAGE_DIRECTORY_ENTRY_EXPORT * COFF_DATA_DIRECTORY_SIZE;
+ u32 export_rva = coff_rd_u32(dd);
+ u32 export_size = coff_rd_u32(dd + 4);
+ if (!export_rva || !export_size) return;
+
+ u64 exp_off;
+ if (!coff_rva_to_offset(shdrs, nsec, export_rva, len, &exp_off)) return;
+ if (exp_off + COFF_EXPORT_DIR_SIZE > len) return;
+ const u8* ed = data + exp_off;
+ u32 name_rva = coff_rd_u32(ed + 12);
+ u32 num_funcs = coff_rd_u32(ed + 20);
+ u32 num_names = coff_rd_u32(ed + 24);
+ u32 eat_rva = coff_rd_u32(ed + 28);
+ u32 ent_rva = coff_rd_u32(ed + 32);
+ u32 ord_rva = coff_rd_u32(ed + 36);
+
+ /* soname = the DLL's own Name (DT_SONAME / LC_ID_DYLIB analogue). */
+ if (name_rva) {
+ u64 noff;
+ if (coff_rva_to_offset(shdrs, nsec, name_rva, len, &noff)) {
+ const char* dn;
+ u32 dl = coff_read_cstr(data, len, noff, &dn);
+ if (dl) obj_image_set_soname(im, intern(c, dn, dl));
+ }
+ }
+
+ if (!num_names) return;
+ u64 eat_off, ent_off, ord_off;
+ if (!coff_rva_to_offset(shdrs, nsec, eat_rva, len, &eat_off)) return;
+ if (!coff_rva_to_offset(shdrs, nsec, ent_rva, len, &ent_off)) return;
+ if (!coff_rva_to_offset(shdrs, nsec, ord_rva, len, &ord_off)) return;
+ if (ent_off + (u64)num_names * 4u > len ||
+ ord_off + (u64)num_names * 2u > len)
+ return;
+ if (eat_off + (u64)num_funcs * 4u > len) return;
+
+ for (u32 i = 0; i < num_names; ++i) {
+ u32 nrva = coff_rd_u32(data + ent_off + (u64)i * 4u);
+ u16 ord = coff_rd_u16(data + ord_off + (u64)i * 2u);
+ if (ord >= num_funcs) continue; /* malformed; skip */
+ u32 func_rva = coff_rd_u32(data + eat_off + (u64)ord * 4u);
+ u64 noff;
+ if (!coff_rva_to_offset(shdrs, nsec, nrva, len, &noff)) continue;
+ const char* en;
+ u32 el = coff_read_cstr(data, len, noff, &en);
+ if (!el) continue;
+
+ ObjImageSym ds;
+ memset(&ds, 0, sizeof ds);
+ ds.name = intern(c, en, el);
+ ds.bind = SB_GLOBAL;
+ ds.kind = SK_FUNC; /* forwarders point at the export-dir string; still SK_FUNC */
+ ds.section = OBJ_SEC_NONE;
+ ds.value = image_base + func_rva;
+ obj_image_add_dynsym(im, &ds);
+ }
+}
+
+/* ---- imports -> deps + undefined dynsyms ----
+ * Walks the import directory descriptors. Each provider DLL becomes one
+ * ObjImageDep carrying its imported-name list; every by-name import also
+ * lands as an undefined ObjImageSym so -T lists imports like ELF .dynsym.
+ * By-ordinal imports are not named in v1 and are skipped. */
+static void read_pe_imports(Compiler* c, ObjImage* im, const u8* data,
+ size_t len, const u8* shdrs, u16 nsec,
+ const u8* data_dir, u32 num_dirs) {
+ if ((u32)IMAGE_DIRECTORY_ENTRY_IMPORT >= num_dirs) return;
+ const u8* dd =
+ data_dir + IMAGE_DIRECTORY_ENTRY_IMPORT * COFF_DATA_DIRECTORY_SIZE;
+ u32 imp_rva = coff_rd_u32(dd);
+ if (!imp_rva) return;
+ u64 imp_off;
+ if (!coff_rva_to_offset(shdrs, nsec, imp_rva, len, &imp_off)) return;
+
+ for (u32 d = 0;; ++d) {
+ u64 desc = imp_off + (u64)d * COFF_IMPORT_DESCRIPTOR_SIZE;
+ if (desc + COFF_IMPORT_DESCRIPTOR_SIZE > len) break;
+ u32 oft = coff_rd_u32(data + desc + 0); /* OriginalFirstThunk (ILT) */
+ u32 dll_name_rva = coff_rd_u32(data + desc + 12);
+ u32 ft = coff_rd_u32(data + desc + 16); /* FirstThunk (IAT) */
+ if (oft == 0 && dll_name_rva == 0 && ft == 0) break; /* null terminator */
+ if (dll_name_rva == 0) continue;
+
+ u64 noff;
+ if (!coff_rva_to_offset(shdrs, nsec, dll_name_rva, len, &noff)) continue;
+ const char* dll;
+ u32 dll_len = coff_read_cstr(data, len, noff, &dll);
+ if (!dll_len) continue;
+ Sym dep_name = intern(c, dll, dll_len);
+
+ /* Prefer the ILT (OriginalFirstThunk); fall back to the IAT when the
+ * image was bound and the ILT is absent. */
+ u32 thunk_rva = oft ? oft : ft;
+ Sym* imports = NULL;
+ u32 nimports = 0, cap = 0;
+ u64 toff;
+ if (thunk_rva && coff_rva_to_offset(shdrs, nsec, thunk_rva, len, &toff)) {
+ for (u32 t = 0;; ++t) {
+ u64 te = toff + (u64)t * COFF_THUNK_DATA64_SIZE;
+ if (te + COFF_THUNK_DATA64_SIZE > len) break;
+ u64 thunk = coff_rd_u64(data + te);
+ if (thunk == 0) break; /* null-terminated table */
+ if (thunk & IMAGE_ORDINAL_FLAG64) continue; /* by-ordinal: skip (v1) */
+ u32 ibn_rva = (u32)(thunk & 0x7fffffffu);
+ u64 hoff;
+ if (!coff_rva_to_offset(shdrs, nsec, ibn_rva, len, &hoff)) continue;
+ /* IMAGE_IMPORT_BY_NAME: u16 Hint, then NUL-terminated name. */
+ const char* inm;
+ u32 il = coff_read_cstr(data, len, hoff + 2u, &inm);
+ if (!il) continue;
+ Sym isym = intern(c, inm, il);
+
+ if (nimports == cap) {
+ u32 ncap = cap ? cap * 2u : 8u;
+ Sym* grown = arena_array(c->scratch, Sym, ncap);
+ if (nimports) memcpy(grown, imports, sizeof(Sym) * nimports);
+ imports = grown;
+ cap = ncap;
+ }
+ imports[nimports++] = isym;
+
+ ObjImageSym us;
+ memset(&us, 0, sizeof us);
+ us.name = isym;
+ us.bind = SB_GLOBAL;
+ us.kind = SK_NOTYPE; /* PE import descriptors don't distinguish func/data */
+ us.section = OBJ_SEC_NONE;
+ obj_image_add_dynsym(im, &us);
+ }
+ }
+
+ ObjImageDep dep;
+ dep.name = dep_name;
+ dep.imports = imports; /* transient scratch; add_dep deep-copies */
+ dep.nimports = nimports;
+ obj_image_add_dep(im, &dep);
+ }
+}
+
+/* ---- base relocations -> dynrelocs ----
+ * Walks the .reloc base-relocation blocks. Each DIR64/HIGHLOW fixup is a
+ * symbol-less load-bias adjustment, mapped to the arch's RELATIVE kind.
+ * ABSOLUTE entries are block padding and skipped. */
+static void read_pe_basereloc(ObjImage* im, const u8* data, size_t len,
+ const u8* shdrs, u16 nsec, const u8* data_dir,
+ u32 num_dirs, u64 image_base,
+ RelocKind relative_kind) {
+ if ((u32)IMAGE_DIRECTORY_ENTRY_BASERELOC >= num_dirs) return;
+ const u8* dd =
+ data_dir + IMAGE_DIRECTORY_ENTRY_BASERELOC * COFF_DATA_DIRECTORY_SIZE;
+ u32 rel_rva = coff_rd_u32(dd);
+ u32 rel_size = coff_rd_u32(dd + 4);
+ if (!rel_rva || !rel_size) return;
+ u64 rel_off;
+ if (!coff_rva_to_offset(shdrs, nsec, rel_rva, len, &rel_off)) return;
+ u64 end = rel_off + rel_size;
+ if (end > len) end = len;
+
+ u64 pos = rel_off;
+ while (pos + COFF_BASE_RELOCATION_SIZE <= end) {
+ u32 page_rva = coff_rd_u32(data + pos + 0);
+ u32 block_size = coff_rd_u32(data + pos + 4);
+ if (block_size < COFF_BASE_RELOCATION_SIZE) break; /* malformed */
+ if (pos + block_size > end) block_size = (u32)(end - pos);
+ u32 nent = (block_size - COFF_BASE_RELOCATION_SIZE) / 2u;
+ for (u32 e = 0; e < nent; ++e) {
+ u16 ent = coff_rd_u16(data + pos + COFF_BASE_RELOCATION_SIZE + (u64)e * 2u);
+ u32 type = (u32)ent >> 12;
+ u32 off = (u32)ent & 0x0fffu;
+ if (type == IMAGE_REL_BASED_ABSOLUTE) continue; /* padding */
+ if (type != IMAGE_REL_BASED_DIR64 && type != IMAGE_REL_BASED_HIGHLOW)
+ continue;
+ ObjImageReloc dr;
+ memset(&dr, 0, sizeof dr);
+ dr.section = OBJ_SEC_NONE; /* offset is a vaddr */
+ dr.offset = image_base + page_rva + off;
+ dr.kind = relative_kind;
+ obj_image_add_dynreloc(im, &dr);
+ }
+ pos += block_size;
+ }
+}
+
+ObjBuilder* read_coff_image(Compiler* c, const char* name, const u8* data,
+ size_t len) {
+ (void)name;
+
+ /* ---- DOS header + PE signature (truncation panics) ---- */
+ if (len < COFF_DOS_HEADER_SIZE)
+ compiler_panic(c, SRCLOC_NONE,
+ "read_coff_image: input shorter than DOS header");
+ if (coff_rd_u16(data + 0) != IMAGE_DOS_SIGNATURE)
+ compiler_panic(c, SRCLOC_NONE, "read_coff_image: bad DOS magic");
+ u32 e_lfanew = coff_rd_u32(data + 60);
+ u64 nt_end =
+ (u64)e_lfanew + 4u + COFF_FILE_HEADER_SIZE + COFF_OPT_HDR64_SIZE;
+ if (nt_end > len)
+ compiler_panic(c, SRCLOC_NONE,
+ "read_coff_image: PE headers extend past end of file");
+ if (coff_rd_u32(data + e_lfanew) != IMAGE_NT_SIGNATURE)
+ compiler_panic(c, SRCLOC_NONE, "read_coff_image: bad PE signature");
+
+ /* ---- IMAGE_FILE_HEADER ---- */
+ const u8* fh = data + e_lfanew + 4u;
+ u16 machine = coff_rd_u16(fh + 0);
+ u16 nsec = coff_rd_u16(fh + 2);
+ u16 size_of_opt = coff_rd_u16(fh + 16);
+ u16 chars = coff_rd_u16(fh + 18);
+ if (machine != IMAGE_FILE_MACHINE_AMD64 &&
+ machine != IMAGE_FILE_MACHINE_ARM64 &&
+ machine != IMAGE_FILE_MACHINE_ARM64EC)
+ compiler_panic(c, SRCLOC_NONE, "read_coff_image: unsupported machine %#x",
+ (u32)machine);
+ if (size_of_opt < COFF_OPT_HDR64_SIZE)
+ compiler_panic(c, SRCLOC_NONE,
+ "read_coff_image: optional header %u too small for PE32+",
+ (u32)size_of_opt);
+
+ /* ---- IMAGE_OPTIONAL_HEADER64 ---- */
+ const u8* oh = fh + COFF_FILE_HEADER_SIZE;
+ if (coff_rd_u16(oh + 0) != IMAGE_NT_OPTIONAL_HDR64_MAGIC)
+ compiler_panic(c, SRCLOC_NONE, "read_coff_image: not PE32+");
+ u32 entry_rva = coff_rd_u32(oh + 16);
+ u64 image_base = coff_rd_u64(oh + 24);
+ u32 sect_align = coff_rd_u32(oh + 32);
+ u16 subsystem = coff_rd_u16(oh + 68);
+ u16 dllchars = coff_rd_u16(oh + 70);
+ u32 num_dirs = coff_rd_u32(oh + 108);
+ if (num_dirs > COFF_NUM_DATA_DIRECTORIES) num_dirs = COFF_NUM_DATA_DIRECTORIES;
+ const u8* data_dir = oh + COFF_OPT_HDR64_SIZE -
+ COFF_NUM_DATA_DIRECTORIES * COFF_DATA_DIRECTORY_SIZE;
+
+ /* ---- section table ---- */
+ u64 shdrs_off = (u64)e_lfanew + 4u + COFF_FILE_HEADER_SIZE + size_of_opt;
+ if (shdrs_off + (u64)nsec * COFF_SECTION_HEADER_SIZE > len)
+ compiler_panic(c, SRCLOC_NONE,
+ "read_coff_image: section table extends past end of file");
+ const u8* shdrs = data + shdrs_off;
+
+ /* Arch ops resolve the RELATIVE base-reloc kind (machine validated above). */
+ const ObjFormatImpl* fmt = obj_format_lookup(KIT_OBJ_COFF);
+ const ObjCoffArchOps* aops =
+ (fmt && fmt->coff_machine) ? fmt->coff_machine(machine) : NULL;
+ if (!aops)
+ compiler_panic(c, SRCLOC_NONE,
+ "read_coff_image: no arch impl for machine %#x",
+ (u32)machine);
+ RelocKind relative_kind = (aops->arch == KIT_ARCH_X86_64) ? R_X64_RELATIVE
+ : (aops->arch == KIT_ARCH_ARM_64) ? R_AARCH64_RELATIVE
+ : R_X64_RELATIVE;
+
+ ObjBuilder* ob = obj_new(c);
+ if (!ob) compiler_panic(c, SRCLOC_NONE, "read_coff_image: obj_new failed");
+ ObjImage* im = obj_image_ensure(
+ ob, (chars & IMAGE_FILE_DLL) ? OBJ_KIND_DYN : OBJ_KIND_EXEC);
+ if (!im)
+ compiler_panic(c, SRCLOC_NONE, "read_coff_image: obj_image_ensure failed");
+ obj_image_set_base(im, image_base);
+ obj_image_set_entry(im, entry_rva ? image_base + entry_rva : 0);
+
+ /* ---- sections + segments (dual-emit) ---- */
+ for (u16 i = 0; i < nsec; ++i) {
+ const u8* sh = shdrs + (u64)i * COFF_SECTION_HEADER_SIZE;
+ const char* raw = (const char*)sh; /* Name[8], NUL-padded (no long form) */
+ u32 nlen = 0;
+ while (nlen < 8 && raw[nlen] != '\0') ++nlen;
+ u32 vsize = coff_rd_u32(sh + 8);
+ u32 vaddr = coff_rd_u32(sh + 12);
+ u32 rawsize = coff_rd_u32(sh + 16);
+ u32 rawptr = coff_rd_u32(sh + 20);
+ u32 ch = coff_rd_u32(sh + 36);
+
+ Sym sn = intern(c, raw, nlen);
+ u16 kind = coff_sec_kind(raw, nlen, ch);
+ u16 flags = coff_sec_flags(raw, nlen, ch);
+ u32 align = coff_sec_align(ch);
+ int is_bss = (ch & IMAGE_SCN_CNT_UNINITIALIZED_DATA) != 0;
+ u16 sem = is_bss ? SSEM_NOBITS : SSEM_PROGBITS;
+
+ ObjSecId id =
+ obj_section_ex(ob, sn, (SecKind)kind, (SecSem)sem, flags, align, 0u, 0u, 0u);
+ if (id != OBJ_SEC_NONE) {
+ obj_section_set_ext(ob, id, OBJ_EXT_COFF, ch, 0);
+ obj_section_set_addr(ob, id, image_base + vaddr);
+ if (is_bss) {
+ obj_reserve_bss(ob, id, vsize ? vsize : rawsize, align);
+ } else if (rawsize) {
+ /* Images FileAlignment-pad raw data; copy at most VirtualSize, and
+ * clamp leniently to the file length (vs the strict .obj path). */
+ u32 copy = rawsize;
+ if (vsize && vsize < copy) copy = vsize;
+ if ((u64)rawptr + copy > len)
+ copy = (rawptr < len) ? (u32)(len - rawptr) : 0;
+ if (copy) {
+ u8* dst = obj_reserve(ob, id, copy);
+ if (dst) memcpy(dst, data + rawptr, copy);
+ }
+ }
+ }
+
+ ObjSegment seg;
+ memset(&seg, 0, sizeof seg);
+ seg.name = sn;
+ seg.vaddr = image_base + vaddr;
+ seg.vsize = vsize;
+ seg.file_off = rawptr;
+ seg.file_size = rawsize;
+ seg.perms = ((ch & IMAGE_SCN_MEM_READ) ? OBJ_SEG_R : 0u) |
+ ((ch & IMAGE_SCN_MEM_WRITE) ? OBJ_SEG_W : 0u) |
+ ((ch & IMAGE_SCN_MEM_EXECUTE) ? OBJ_SEG_X : 0u);
+ seg.align = sect_align ? sect_align : 1u;
+ obj_image_add_segment(im, &seg);
+ }
+
+ /* ---- raw escape-hatch entries: 16 data dirs + subsystem + dllchars ---- */
+ for (u32 i = 0; i < COFF_NUM_DATA_DIRECTORIES; ++i) {
+ const u8* e = data_dir + (u64)i * COFF_DATA_DIRECTORY_SIZE;
+ ObjImageRaw r;
+ r.tag = i;
+ r.value = (i < num_dirs) ? coff_rd_u32(e) : 0;
+ r.extra = (i < num_dirs) ? coff_rd_u32(e + 4) : 0;
+ obj_image_add_raw(im, &r);
+ }
+ {
+ ObjImageRaw r = {KIT_OBJ_RAW_PE_SUBSYSTEM, subsystem, 0};
+ obj_image_add_raw(im, &r);
+ }
+ {
+ ObjImageRaw r = {KIT_OBJ_RAW_PE_DLLCHARS, dllchars, 0};
+ obj_image_add_raw(im, &r);
+ }
+
+ read_pe_exports(c, im, data, len, shdrs, nsec, data_dir, num_dirs, image_base);
+ read_pe_imports(c, im, data, len, shdrs, nsec, data_dir, num_dirs);
+ read_pe_basereloc(im, data, len, shdrs, nsec, data_dir, num_dirs, image_base,
+ relative_kind);
+
+ obj_finalize(ob);
+ return ob;
+}
diff --git a/src/obj/coff/read_util.c b/src/obj/coff/read_util.c
@@ -0,0 +1,84 @@
+/* Shared PE/COFF reader primitives — see read_util.h. */
+
+#include "obj/coff/read_util.h"
+
+#include <string.h>
+
+#include "obj/obj.h" /* SecKind / SecFlag enums */
+
+int coff_rva_to_offset(const u8* shdrs, u16 nsec, u32 rva, size_t len,
+ u64* off_out) {
+ for (u16 i = 0; i < nsec; ++i) {
+ const u8* sh = shdrs + (u64)i * COFF_SECTION_HEADER_SIZE;
+ u32 vsize = coff_rd_u32(sh + 8);
+ u32 vaddr = coff_rd_u32(sh + 12);
+ u32 raw_size = coff_rd_u32(sh + 16);
+ u32 raw_ptr = coff_rd_u32(sh + 20);
+ /* Some linkers leave VirtualSize == 0 in objects; use raw_size as
+ * a fallback so we still resolve RVAs in well-formed images. */
+ u32 span = vsize ? vsize : raw_size;
+ if (rva >= vaddr && rva < vaddr + span) {
+ u64 delta = (u64)(rva - vaddr);
+ if (delta >= raw_size) return 0; /* RVA past on-disk data */
+ u64 off = (u64)raw_ptr + delta;
+ if (off >= len) return 0;
+ *off_out = off;
+ return 1;
+ }
+ }
+ return 0;
+}
+
+u32 coff_read_cstr(const u8* data, size_t len, u64 off, const char** out) {
+ if (off >= len) {
+ *out = "";
+ return 0;
+ }
+ const char* s = (const char*)(data + off);
+ u64 max = (u64)len - off;
+ u64 n = 0;
+ while (n < max && s[n] != '\0') ++n;
+ if (n == max) {
+ *out = "";
+ return 0;
+ } /* unterminated */
+ *out = s;
+ return (u32)n;
+}
+
+u16 coff_sec_kind(const char* name, u32 nlen, u32 ch) {
+ if (ch & IMAGE_SCN_CNT_UNINITIALIZED_DATA) return SEC_BSS;
+ if (ch & IMAGE_SCN_CNT_CODE) return SEC_TEXT;
+ if (ch & IMAGE_SCN_MEM_EXECUTE) return SEC_TEXT;
+ if (nlen >= 7 && memcmp(name, ".debug_", 7) == 0) return SEC_DEBUG;
+ /* The MS toolchain spells DWARF section names with a leading ".debug$"
+ * (CodeView) — keep ELF-style ".debug_" detection but also treat the
+ * MS form as debug. */
+ if (nlen >= 7 && memcmp(name, ".debug$", 7) == 0) return SEC_DEBUG;
+ if (ch & IMAGE_SCN_CNT_INITIALIZED_DATA) {
+ if (ch & IMAGE_SCN_MEM_WRITE) return SEC_DATA;
+ return SEC_RODATA;
+ }
+ return SEC_OTHER;
+}
+
+u16 coff_sec_flags(const char* name, u32 nlen, u32 ch) {
+ u16 f = 0;
+ if (ch & IMAGE_SCN_MEM_READ) f |= SF_ALLOC;
+ if (ch & IMAGE_SCN_MEM_EXECUTE) f |= SF_EXEC;
+ if (ch & IMAGE_SCN_MEM_WRITE) f |= SF_WRITE;
+ if (ch & IMAGE_SCN_LNK_COMDAT) f |= SF_GROUP;
+ /* TLS sections in PE are spelled ".tls$<suffix>" (e.g. ".tls$", ".tls$ZZZ").
+ * There is no characteristics bit for TLS — detection is name-based. */
+ if (nlen >= 5 && memcmp(name, ".tls$", 5) == 0) f |= SF_TLS;
+ if (nlen == 4 && memcmp(name, ".tls", 4) == 0) f |= SF_TLS;
+ return f;
+}
+
+/* Bits 20..23 of Characteristics encode alignment as (log2(align)+1).
+ * 0 means "default"; we collapse to align=1 for round-trip purposes. */
+u32 coff_sec_align(u32 ch) {
+ u32 n = (ch & IMAGE_SCN_ALIGN_MASK) >> 20;
+ if (n == 0) return 1;
+ return 1u << (n - 1u);
+}
diff --git a/src/obj/coff/read_util.h b/src/obj/coff/read_util.h
@@ -0,0 +1,36 @@
+/* Shared PE/COFF reader primitives. Pure helpers (no ObjBuilder
+ * coupling) used by both the relocatable-object reader (read_coff), the
+ * DLL reader (read_coff_dso), and the linked-image reader
+ * (read_coff_image), so the RVA mapping, bounded string reads, and the
+ * characteristics -> canonical SecKind/SecFlag/align mapping live in one
+ * place. */
+
+#ifndef KIT_OBJ_COFF_READ_UTIL_H
+#define KIT_OBJ_COFF_READ_UTIL_H
+
+#include <stddef.h>
+
+#include "obj/coff/coff.h" /* u8/u16/u32/u64, IMAGE_SCN_*, coff_rd_* */
+
+/* RVA -> file offset via the section table (40-byte ImageSectionHeader
+ * entries, `nsec` of them at `shdrs`). Returns 1 and fills *off_out on
+ * success; 0 if the RVA falls outside every section's
+ * [VirtualAddress, VirtualAddress + max(VirtualSize, SizeOfRawData))
+ * range or the resulting file offset would exceed `len`. */
+int coff_rva_to_offset(const u8* shdrs, u16 nsec, u32 rva, size_t len,
+ u64* off_out);
+
+/* Read a NUL-terminated string at file offset `off`, bounded by `len`.
+ * Returns the length (excluding NUL) and writes the pointer to *out.
+ * Returns 0 (and *out = "") if off is out of range or the string is not
+ * terminated within the file. */
+u32 coff_read_cstr(const u8* data, size_t len, u64 off, const char** out);
+
+/* Characteristics -> canonical SecKind / SecFlag / alignment. Shared by
+ * the .obj reader and the image reader so both classify sections
+ * identically. Returns are SecKind / SecFlag values (widened to u16). */
+u16 coff_sec_kind(const char* name, u32 nlen, u32 ch);
+u16 coff_sec_flags(const char* name, u32 nlen, u32 ch);
+u32 coff_sec_align(u32 ch);
+
+#endif
diff --git a/src/obj/elf/read.c b/src/obj/elf/read.c
@@ -439,6 +439,16 @@ static void read_elf_image(Compiler* c, ObjBuilder* ob, const u8* data,
for (u64 off = 0; off + dyn_size <= dynsz; off += dyn_size) {
u64 tag = elf_rd_addr(dynp + off, is32);
u64 val = elf_rd_addr(dynp + off + (is32 ? 4 : 8), is32);
+ /* Raw .dynamic view (escape hatch): one entry per DT_* tag, the
+ * terminating DT_NULL included, before the NEEDED/SONAME/RPATH
+ * filtering below. */
+ {
+ ObjImageRaw r;
+ r.tag = (u32)tag;
+ r.value = val;
+ r.extra = 0;
+ obj_image_add_raw(im, &r);
+ }
if (tag == DT_NULL) break;
if (tag != DT_NEEDED && tag != DT_SONAME && tag != DT_RPATH &&
tag != DT_RUNPATH)
diff --git a/src/obj/macho/read.c b/src/obj/macho/read.c
@@ -175,6 +175,16 @@ static void read_macho_image(Compiler* c, ObjBuilder* ob, const u8* data,
u32 cmdsize = rd_u32_le(data + pos + 4);
if (cmdsize < 8 || pos + cmdsize > end) break;
+ /* Raw load-command view (escape hatch): one entry per LC_* command,
+ * carrying its file offset and on-disk size. */
+ {
+ ObjImageRaw r;
+ r.tag = cmd;
+ r.value = pos;
+ r.extra = cmdsize;
+ obj_image_add_raw(im, &r);
+ }
+
if (cmd == LC_SEGMENT_64 && cmdsize >= MACHO_SEGCMD64_SIZE) {
const char* segname = (const char*)(data + pos + 8);
u32 seg_len = fixed16_len(segname);
diff --git a/src/obj/obj.c b/src/obj/obj.c
@@ -266,6 +266,8 @@ struct ObjImage {
u32 ndynsyms, cap_dynsyms;
ObjImageReloc* dynrelocs;
u32 ndynrelocs, cap_dynrelocs;
+ ObjImageRaw* raws;
+ u32 nraws, cap_raws;
/* Undefined symbol names a DSO references (interned). Used by the linker's
* --gc-sections pass to keep executable-defined symbols a shared library
* needs (e.g. libc.so.7's `environ` / `__progname`) from being collected. */
@@ -277,8 +279,17 @@ static void obj_image_free_(ObjBuilder* ob) {
ObjImage* im;
if (!ob || !ob->image) return;
im = ob->image;
- /* Dep import-name arrays are caller-owned (interned in the global pool);
- * only the deps vector itself is ours to release. */
+ /* The image owns each dep's imports[] array container (allocated from
+ * im->heap by the PE reader); the Sym values inside stay interned in the
+ * global pool and are not freed here. ELF/Mach-O deps carry imports==NULL. */
+ if (im->deps) {
+ for (u32 i = 0; i < im->ndeps; ++i) {
+ const ObjImageDep* d = &im->deps[i];
+ if (d->imports)
+ im->heap->free(im->heap, (void*)d->imports,
+ sizeof(*d->imports) * d->nimports);
+ }
+ }
if (im->segs)
im->heap->free(im->heap, im->segs, sizeof(*im->segs) * im->cap_segs);
if (im->deps)
@@ -291,6 +302,8 @@ static void obj_image_free_(ObjBuilder* ob) {
if (im->dynrelocs)
im->heap->free(im->heap, im->dynrelocs,
sizeof(*im->dynrelocs) * im->cap_dynrelocs);
+ if (im->raws)
+ im->heap->free(im->heap, im->raws, sizeof(*im->raws) * im->cap_raws);
if (im->undefs)
im->heap->free(im->heap, im->undefs, sizeof(*im->undefs) * im->cap_undefs);
ob->heap->free(ob->heap, im, sizeof(*im));
@@ -336,9 +349,29 @@ void obj_image_add_segment(ObjImage* im, const ObjSegment* seg) {
im->segs[im->nsegs++] = *seg;
}
void obj_image_add_dep(ObjImage* im, const ObjImageDep* dep) {
+ ObjImageDep d;
if (!im || !dep) return;
if (VEC_GROW(im->heap, im->deps, im->cap_deps, im->ndeps + 1)) return;
- im->deps[im->ndeps++] = *dep;
+ d = *dep;
+ /* Deep-copy the imports[] name array into image-heap-owned memory so the
+ * reader may pass a transient (scratch/arena) array; obj_image_free_
+ * releases this copy. The Sym values inside are global-interned and not
+ * owned here. ELF/Mach-O deps carry imports==NULL (nimports==0). */
+ if (d.nimports && dep->imports) {
+ Sym* copy = (Sym*)im->heap->alloc(im->heap, sizeof(Sym) * d.nimports,
+ _Alignof(Sym));
+ if (!copy) {
+ d.imports = NULL;
+ d.nimports = 0;
+ } else {
+ memcpy(copy, dep->imports, sizeof(Sym) * d.nimports);
+ d.imports = copy;
+ }
+ } else {
+ d.imports = NULL;
+ d.nimports = 0;
+ }
+ im->deps[im->ndeps++] = d;
}
void obj_image_add_rpath(ObjImage* im, Sym rpath) {
if (!im) return;
@@ -362,6 +395,11 @@ void obj_image_add_undef(ObjImage* im, Sym name) {
if (VEC_GROW(im->heap, im->undefs, im->cap_undefs, im->nundefs + 1)) return;
im->undefs[im->nundefs++] = name;
}
+void obj_image_add_raw(ObjImage* im, const ObjImageRaw* raw) {
+ if (!im || !raw) return;
+ if (VEC_GROW(im->heap, im->raws, im->cap_raws, im->nraws + 1)) return;
+ im->raws[im->nraws++] = *raw;
+}
ObjKind obj_image_kind(const ObjImage* im) {
return im ? im->kind : OBJ_KIND_REL;
@@ -395,6 +433,10 @@ u32 obj_image_nundefs(const ObjImage* im) { return im ? im->nundefs : 0; }
Sym obj_image_undef(const ObjImage* im, u32 idx) {
return (im && idx < im->nundefs) ? im->undefs[idx] : 0;
}
+u32 obj_image_nraws(const ObjImage* im) { return im ? im->nraws : 0; }
+const ObjImageRaw* obj_image_raw(const ObjImage* im, u32 idx) {
+ return (im && idx < im->nraws) ? &im->raws[idx] : NULL;
+}
void obj_ext_set(ObjBuilder* ob, ObjExtKind kind, void* payload,
ObjExtFreeFn free_fn) {
diff --git a/src/obj/obj.h b/src/obj/obj.h
@@ -961,6 +961,21 @@ typedef struct ObjImageReloc {
RelocKind kind;
} ObjImageReloc;
+/* Raw, format-specific image field that doesn't fit the neutral model.
+ * One flat triple list per image, in the spirit of the per-section
+ * kit_obj_section_format_flags escape hatch: a neutral container with
+ * per-format tag semantics (documented on the public KitObjImageRaw):
+ * PE : data dirs tag=0..15 (index), value=RVA, extra=size;
+ * subsystem tag=KIT_OBJ_RAW_PE_SUBSYSTEM, value=u16;
+ * dllchars tag=KIT_OBJ_RAW_PE_DLLCHARS, value=u16
+ * ELF : .dynamic tag=d_tag, value=d_val, extra=0
+ * Mach-O: load cmds tag=cmd, value=file offset, extra=cmdsize */
+typedef struct ObjImageRaw {
+ u32 tag;
+ u64 value;
+ u64 extra;
+} ObjImageRaw;
+
typedef struct ObjImage ObjImage; /* defined in obj.c */
/* Accessor — NULL on relocatable inputs. */
@@ -977,13 +992,17 @@ void obj_image_set_interp(ObjImage*, Sym interp);
void obj_image_set_soname(ObjImage*, Sym soname);
/* Image table appenders (readers). Each copies its argument by value into a
- * builder-heap-owned vector. The Sym array behind ObjImageDep.imports must
- * outlive the builder (intern into the compiler's global pool). */
+ * builder-heap-owned vector. obj_image_add_dep additionally deep-copies the
+ * ObjImageDep.imports[] name array into image-heap memory, so the reader may
+ * pass a transient (scratch) array; the Sym values themselves must still be
+ * interned in the compiler's global pool. */
void obj_image_add_segment(ObjImage*, const ObjSegment*);
void obj_image_add_dep(ObjImage*, const ObjImageDep*);
void obj_image_add_rpath(ObjImage*, Sym rpath);
void obj_image_add_dynsym(ObjImage*, const ObjImageSym*);
void obj_image_add_dynreloc(ObjImage*, const ObjImageReloc*);
+/* Raw format-specific image fields (see ObjImageRaw). Copied by value. */
+void obj_image_add_raw(ObjImage*, const ObjImageRaw*);
/* Undefined symbol names a DSO references (interned). The linker's
* --gc-sections pass roots executable definitions of these so a shared
* library's back-references (e.g. libc.so.7 → `environ` / `__progname`)
@@ -1008,6 +1027,8 @@ u32 obj_image_ndynrelocs(const ObjImage*);
const ObjImageReloc* obj_image_dynreloc(const ObjImage*, u32 idx);
u32 obj_image_nundefs(const ObjImage*);
Sym obj_image_undef(const ObjImage*, u32 idx);
+u32 obj_image_nraws(const ObjImage*);
+const ObjImageRaw* obj_image_raw(const ObjImage*, u32 idx);
/* ---- file format emitters ---- */
void emit_elf(Compiler*, ObjBuilder*, Writer*);
@@ -1041,6 +1062,17 @@ ObjBuilder* read_coff(Compiler*, const char* name, const u8* data, size_t len);
* synthesized in v1 — almost all real-world imports are by name. */
ObjBuilder* read_coff_dso(Compiler*, const char* name, const u8* data,
size_t len, Sym* soname_out);
+/* PE32+ linked-image reader (peer of read_elf_image / read_macho_image).
+ * Handles both executables (IMAGE_FILE_DLL clear -> OBJ_KIND_EXEC) and
+ * DLLs (set -> OBJ_KIND_DYN), populating the neutral ObjImage: one
+ * segment per PE section, exports -> dynsyms + soname, imports -> deps +
+ * undefined dynsyms, base relocs -> RELATIVE dynrelocs, plus a full
+ * section/symbol view via the ObjBuilder Section table, and the raw
+ * data-directory / subsystem / dllchars escape-hatch entries. Lenient:
+ * malformed sub-tables are skipped; truncated core headers panic.
+ * Dispatched from read_coff on the DOS 'MZ' magic. */
+ObjBuilder* read_coff_image(Compiler*, const char* name, const u8* data,
+ size_t len);
ObjBuilder* read_macho(Compiler*, const char* name, const u8* data, size_t len);
/* Mach-O MH_DYLIB reader. Produces an ObjBuilder containing only the
* dylib's exported symbols (as defined OBJ_SEC_NONE entries — the
diff --git a/test/coff/pe-image-read.c b/test/coff/pe-image-read.c
@@ -0,0 +1,415 @@
+/* PE32+ linked-image reader round-trip (read_coff_image, no external
+ * toolchain).
+ *
+ * Links a tiny PIE executable in memory with kit's own COFF linker — a
+ * .text entry plus a .data slot that takes an absolute (R_ABS64) reference
+ * to an imported ExitProcess from KERNEL32.dll (via a short-import shim) —
+ * then re-opens the emitted bytes through the public kit_obj_open and
+ * asserts the neutral image view the reader populates:
+ * - kind EXEC, nonzero entry / image base
+ * - segments + sections (one per PE section, .text executable)
+ * - dependency KERNEL32.dll carrying the ExitProcess import
+ * - dynamic symbol ExitProcess (undefined import)
+ * - base relocation(s) for the absolute .data pointer (PIE)
+ * - raw escape hatch: 16 data directories + subsystem + dllchars,
+ * IMPORT directory populated
+ *
+ * Runs on every host (the reader is ours); covers both x86_64 and aarch64
+ * Windows targets. */
+
+#include <kit/core.h>
+#include <kit/link.h>
+#include <kit/object.h>
+#include <setjmp.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "core/core.h"
+#include "core/pool.h"
+#include "link/link.h"
+#include "obj/obj.h"
+
+/* ---- short-import wire constants (mirror pe-import-smoke.c). ---- */
+#define SHIM_HEADER_SIZE 20u
+#define SHIM_SYM_CSTR "ExitProcess"
+#define SHIM_DLL_CSTR "KERNEL32.dll"
+#define SHIM_SYM_NUL_LEN 12u /* "ExitProcess\0" */
+#define SHIM_DLL_NUL_LEN 13u /* "KERNEL32.dll\0" */
+#define SHIM_DATA_LEN (SHIM_SYM_NUL_LEN + SHIM_DLL_NUL_LEN)
+#define SHIM_TOTAL_LEN (SHIM_HEADER_SIZE + SHIM_DATA_LEN)
+#define COFF_SHIMP_SIG2 0xFFFFu
+/* TypeFlags = Type=CODE(0) | (NameType=NAME(1) << 2) = 0x0004. */
+#define COFF_SHIMP_TYPEFLAGS 0x0004u
+
+/* ---- env vtables --------------------------------------------------- */
+
+static void* heap_alloc(KitHeap* h, size_t n, size_t a) {
+ (void)h;
+ (void)a;
+ return n ? malloc(n) : NULL;
+}
+static void* heap_realloc(KitHeap* h, void* p, size_t o, size_t n, size_t a) {
+ (void)h;
+ (void)o;
+ (void)a;
+ return realloc(p, n);
+}
+static void heap_free(KitHeap* h, void* p, size_t n) {
+ (void)h;
+ (void)n;
+ free(p);
+}
+static KitHeap g_heap = {heap_alloc, heap_realloc, heap_free, NULL};
+
+static void diag_emit(KitDiagSink* s, KitDiagKind k, KitSrcLoc loc,
+ const char* fmt, va_list ap) {
+ static const char* names[] = {"note", "warning", "error", "fatal"};
+ (void)s;
+ (void)loc;
+ fprintf(stderr, "%s: ", names[k]);
+ vfprintf(stderr, fmt, ap);
+ fputc('\n', stderr);
+}
+static KitDiagSink g_diag = {diag_emit, NULL, 0, 0};
+static KitContext g_ctx;
+
+static int g_failures;
+static const char* g_case = "?";
+#define EXPECT(cond, ...) \
+ do { \
+ if (!(cond)) { \
+ fprintf(stderr, "FAIL [%s] %s:%d: ", g_case, __FILE__, __LINE__); \
+ fprintf(stderr, __VA_ARGS__); \
+ fputc('\n', stderr); \
+ g_failures++; \
+ } \
+ } while (0)
+
+/* ---- target / compiler ------------------------------------------- */
+
+static void target_windows(KitTargetSpec* t, KitArchKind arch) {
+ memset(t, 0, sizeof *t);
+ t->arch = arch;
+ t->os = KIT_OS_WINDOWS;
+ t->obj = KIT_OBJ_COFF;
+ t->ptr_size = 8;
+ t->ptr_align = 8;
+ t->big_endian = false;
+ t->pic = KIT_PIC_PIE;
+ t->code_model = KIT_CM_SMALL;
+}
+
+static Compiler* make_compiler(const KitTargetSpec* t) {
+ KitTargetOptions opts;
+ KitTarget* target = NULL;
+ KitCompiler* cc = NULL;
+ memset(&opts, 0, sizeof opts);
+ opts.spec = *t;
+ if (kit_target_new(&g_ctx, &opts, &target) != KIT_OK || !target) return NULL;
+ if (kit_compiler_new(target, &g_ctx, &cc) != KIT_OK || !cc) {
+ kit_target_free(target);
+ return NULL;
+ }
+ return (Compiler*)cc;
+}
+
+static void free_compiler(Compiler* c) {
+ const KitTarget* target;
+ if (!c) return;
+ target = kit_compiler_target((KitCompiler*)c);
+ kit_compiler_free((KitCompiler*)c);
+ kit_target_free((KitTarget*)target);
+}
+
+/* ---- short-import shim builder ------------------------------------ */
+
+static void build_short_import(uint8_t buf[SHIM_TOTAL_LEN], uint16_t machine) {
+ memset(buf, 0, SHIM_TOTAL_LEN);
+ buf[2] = (uint8_t)(COFF_SHIMP_SIG2 & 0xFF); /* Sig2 = 0xFFFF */
+ buf[3] = (uint8_t)((COFF_SHIMP_SIG2 >> 8) & 0xFF);
+ buf[6] = (uint8_t)(machine & 0xFF);
+ buf[7] = (uint8_t)((machine >> 8) & 0xFF);
+ buf[12] = (uint8_t)(SHIM_DATA_LEN & 0xFFu); /* SizeOfData */
+ buf[13] = (uint8_t)((SHIM_DATA_LEN >> 8) & 0xFFu);
+ buf[18] = (uint8_t)(COFF_SHIMP_TYPEFLAGS & 0xFF);
+ buf[19] = (uint8_t)((COFF_SHIMP_TYPEFLAGS >> 8) & 0xFF);
+ memcpy(buf + SHIM_HEADER_SIZE, SHIM_SYM_CSTR, SHIM_SYM_NUL_LEN);
+ memcpy(buf + SHIM_HEADER_SIZE + SHIM_SYM_NUL_LEN, SHIM_DLL_CSTR,
+ SHIM_DLL_NUL_LEN);
+}
+
+/* ---- program ObjBuilder ------------------------------------------- */
+
+/* mainCRTStartup body: a single return. The exact encoding is irrelevant
+ * to the reader; differ per arch only so the linker sees plausible code. */
+static const uint8_t TEXT_X64[1] = {0xc3}; /* ret */
+static const uint8_t TEXT_AA64[4] = {0xc0, 0x03, 0x5f, 0xd6}; /* ret */
+
+static ObjBuilder* build_program(Compiler* c, KitArchKind arch) {
+ ObjBuilder* ob = obj_new(c);
+ Pool* p = c->global;
+ Sym text_name = pool_intern_slice(p, SLICE_LIT(".text"));
+ Sym data_name = pool_intern_slice(p, SLICE_LIT(".data"));
+ Sym main_name = pool_intern_slice(p, SLICE_LIT("mainCRTStartup"));
+ Sym exit_name = pool_intern_slice(p, SLICE_LIT(SHIM_SYM_CSTR));
+ const uint8_t* text = arch == KIT_ARCH_X86_64 ? TEXT_X64 : TEXT_AA64;
+ u32 text_len = arch == KIT_ARCH_X86_64 ? (u32)sizeof TEXT_X64 : (u32)sizeof TEXT_AA64;
+ ObjSecId tsec = obj_section(ob, text_name, SEC_TEXT, SF_ALLOC | SF_EXEC, 16);
+ ObjSecId dsec = obj_section(ob, data_name, SEC_DATA, SF_ALLOC | SF_WRITE, 8);
+ ObjSymId exit_sym;
+ uint8_t zeros[8] = {0};
+
+ obj_write(ob, tsec, text, text_len);
+ obj_symbol(ob, main_name, SB_GLOBAL, SK_FUNC, tsec, 0, text_len);
+
+ /* .data: an 8-byte absolute pointer to the imported ExitProcess. The
+ * R_ABS64 both forces ExitProcess to be imported and (in a PIE) yields a
+ * base relocation, so the reader's import + base-reloc paths both run. */
+ exit_sym = obj_symbol(ob, exit_name, SB_GLOBAL, SK_UNDEF, OBJ_SEC_NONE, 0, 0);
+ obj_write(ob, dsec, zeros, sizeof zeros);
+ obj_reloc(ob, dsec, 0, R_ABS64, exit_sym, 0);
+
+ obj_finalize(ob);
+ return ob;
+}
+
+/* Link a PE image and copy the emitted bytes into a fresh malloc buffer
+ * (so the reader runs fully independent of the producing compiler).
+ * Returns NULL on failure. */
+static uint8_t* link_pe(Compiler* c, KitArchKind arch, uint16_t machine,
+ size_t* out_len) {
+ ObjBuilder* prog = build_program(c, arch);
+ uint8_t shim[SHIM_TOTAL_LEN];
+ Linker* l;
+ LinkImage* img;
+ KitWriter* w = NULL;
+ const uint8_t* bytes;
+ size_t n = 0;
+ uint8_t* copy = NULL;
+
+ build_short_import(shim, machine);
+
+ l = link_new(c);
+ if (!l) return NULL;
+ link_add_obj(l, prog);
+ (void)link_add_obj_bytes(l, "ExitProcess.lib-member", shim, SHIM_TOTAL_LEN);
+ link_set_entry(l, KIT_SLICE_LIT("mainCRTStartup"));
+ link_set_pie(l, 1);
+ link_set_emit_static_exe(l, 1);
+
+ img = link_resolve(l);
+ if (!img) {
+ link_free(l);
+ return NULL;
+ }
+ if (kit_writer_mem(&g_heap, &w) != KIT_OK || !w) {
+ link_image_free(img);
+ link_free(l);
+ return NULL;
+ }
+ link_emit_image_writer(img, w);
+ bytes = kit_writer_mem_bytes(w, &n);
+ if (bytes && n) {
+ copy = (uint8_t*)malloc(n);
+ if (copy) memcpy(copy, bytes, n);
+ }
+ *out_len = n;
+ kit_writer_close(w);
+ link_image_free(img);
+ link_free(l);
+ return copy;
+}
+
+/* ---- the round-trip assertions ------------------------------------ */
+
+static void run_case(const char* name, KitArchKind arch, uint16_t machine) {
+ Compiler* c;
+ uint8_t* pe;
+ size_t pe_len = 0;
+ KitTargetSpec t;
+ KitObjFile* f = NULL;
+ KitSlice input;
+ KitObjImageInfo info;
+ KitStatus st;
+
+ g_case = name;
+ target_windows(&t, arch);
+ c = make_compiler(&t);
+ if (!c) {
+ EXPECT(0, "make_compiler failed");
+ return;
+ }
+ if (setjmp(c->panic)) {
+ EXPECT(0, "panic while linking PE");
+ compiler_run_cleanups(c);
+ free_compiler(c);
+ return;
+ }
+ pe = link_pe(c, arch, machine, &pe_len);
+ free_compiler(c);
+ if (!pe || !pe_len) {
+ EXPECT(0, "link_pe produced no bytes");
+ free(pe);
+ return;
+ }
+
+ /* Detection should route the image to COFF/Windows. */
+ EXPECT(kit_detect_fmt(pe, pe_len) == KIT_BIN_PE, "detect_fmt != KIT_BIN_PE");
+
+ input.data = pe;
+ input.len = pe_len;
+ st = kit_obj_open(&g_ctx, KIT_SLICE_LIT("image.exe"), &input, &f);
+ EXPECT(st == KIT_OK && f, "kit_obj_open failed (st=%d)", (int)st);
+ if (!f) {
+ free(pe);
+ return;
+ }
+
+ EXPECT(kit_obj_kind(f) == KIT_OBJ_KIND_EXEC, "kind != EXEC (%d)",
+ (int)kit_obj_kind(f));
+
+ st = kit_obj_image_info(f, &info);
+ EXPECT(st == KIT_OK, "image_info failed");
+ EXPECT(info.image_base != 0, "image_base == 0");
+ EXPECT(info.entry > info.image_base, "entry (%llu) not above base (%llu)",
+ (unsigned long long)info.entry, (unsigned long long)info.image_base);
+
+ /* Sections + a .text section. */
+ {
+ KitObjSection idx;
+ EXPECT(kit_obj_nsections(f) > 0, "no sections");
+ EXPECT(kit_obj_section_by_name(f, KIT_SLICE_LIT(".text"), &idx) == KIT_OK,
+ ".text section not found");
+ }
+
+ /* Segments: at least one, with an executable one present. */
+ {
+ KitObjSegIter* it = NULL;
+ KitObjSegInfo seg;
+ int nseg = 0, nexec = 0;
+ EXPECT(kit_obj_segiter_new(f, &it) == KIT_OK, "segiter_new failed");
+ while (it && kit_obj_segiter_next(it, &seg) == KIT_ITER_ITEM) {
+ ++nseg;
+ if (seg.perms & KIT_SEG_X) ++nexec;
+ EXPECT(seg.vaddr >= info.image_base, "segment vaddr below image base");
+ }
+ kit_obj_segiter_free(it);
+ EXPECT(nseg > 0, "no segments");
+ EXPECT(nexec > 0, "no executable segment");
+ }
+
+ /* Dependency KERNEL32.dll carrying the ExitProcess import. */
+ {
+ KitObjDepIter* it = NULL;
+ KitObjDepInfo dep;
+ int found_dll = 0, found_imp = 0;
+ EXPECT(kit_obj_depiter_new(f, &it) == KIT_OK, "depiter_new failed");
+ while (it && kit_obj_depiter_next(it, &dep) == KIT_ITER_ITEM) {
+ if (kit_slice_eq_cstr(dep.name, SHIM_DLL_CSTR)) {
+ found_dll = 1;
+ for (uint32_t i = 0; i < dep.nimports; ++i)
+ if (kit_slice_eq_cstr(dep.imports[i], SHIM_SYM_CSTR)) found_imp = 1;
+ }
+ }
+ kit_obj_depiter_free(it);
+ EXPECT(found_dll, "KERNEL32.dll dependency not found");
+ EXPECT(found_imp, "ExitProcess import not listed under KERNEL32.dll");
+ }
+
+ /* Dynamic symbol ExitProcess (undefined import). */
+ {
+ KitObjSymIter* it = NULL;
+ KitObjSymInfo sym;
+ int found = 0;
+ EXPECT(kit_obj_dynsymiter_new(f, &it) == KIT_OK, "dynsymiter_new failed");
+ while (it && kit_obj_symiter_next(it, &sym) == KIT_ITER_ITEM)
+ if (kit_slice_eq_cstr(sym.name, SHIM_SYM_CSTR)) found = 1;
+ kit_obj_symiter_free(it);
+ EXPECT(found, "ExitProcess not in dynamic symbols");
+ }
+
+ /* Raw escape hatch: 16 data dirs + subsystem + dllchars; IMPORT set. */
+ {
+ KitObjImageRawIter* it = NULL;
+ KitObjImageRaw r;
+ int ndatadir = 0, have_subsys = 0, have_dllchars = 0;
+ uint64_t import_rva = 0;
+ EXPECT(kit_obj_image_rawiter_new(f, &it) == KIT_OK, "rawiter_new failed");
+ while (it && kit_obj_image_rawiter_next(it, &r) == KIT_ITER_ITEM) {
+ if (r.tag < 16) {
+ ++ndatadir;
+ if (r.tag == 1) import_rva = r.value; /* IMAGE_DIRECTORY_ENTRY_IMPORT */
+ } else if (r.tag == KIT_OBJ_RAW_PE_SUBSYSTEM) {
+ have_subsys = 1;
+ EXPECT(r.value == 3, "subsystem != WINDOWS_CUI (%llu)",
+ (unsigned long long)r.value);
+ } else if (r.tag == KIT_OBJ_RAW_PE_DLLCHARS) {
+ have_dllchars = 1;
+ }
+ }
+ kit_obj_image_rawiter_free(it);
+ EXPECT(ndatadir == 16, "expected 16 data directories, saw %d", ndatadir);
+ EXPECT(have_subsys, "subsystem raw entry missing");
+ EXPECT(have_dllchars, "dllcharacteristics raw entry missing");
+ EXPECT(import_rva != 0, "IMPORT data directory RVA is zero");
+ }
+
+ /* Base relocations: the PIE .data absolute pointer needs at least one. */
+ {
+ KitObjRelocIter* it = NULL;
+ KitObjReloc rel;
+ int n = 0;
+ EXPECT(kit_obj_dynreliter_new(f, &it) == KIT_OK, "dynreliter_new failed");
+ while (it && kit_obj_reliter_next(it, &rel) == KIT_ITER_ITEM) ++n;
+ kit_obj_reliter_free(it);
+ EXPECT(n > 0, "no base relocations for PIE image");
+ }
+
+ kit_obj_free(f);
+ free(pe);
+}
+
+int main(int argc, char** argv) {
+ memset(&g_ctx, 0, sizeof g_ctx);
+ g_ctx.heap = &g_heap;
+ g_ctx.diag = &g_diag;
+ g_ctx.now = -1;
+
+ /* Optional: regenerate the committed x86_64 PE objdump fixture (no
+ * asserts). Used to produce test/objdump/x86_64-windows/cases/pe-image.exe
+ * from this same in-memory link, so the non-gated objdump golden is
+ * reproducible. */
+ if (argc > 1) {
+ KitTargetSpec t;
+ Compiler* c;
+ target_windows(&t, KIT_ARCH_X86_64);
+ c = make_compiler(&t);
+ if (c && setjmp(c->panic) == 0) {
+ size_t n = 0;
+ uint8_t* pe = link_pe(c, KIT_ARCH_X86_64, 0x8664u, &n);
+ if (pe && n) {
+ FILE* fp = fopen(argv[1], "wb");
+ if (fp) {
+ fwrite(pe, 1, n, fp);
+ fclose(fp);
+ }
+ fprintf(stderr, "wrote %zu bytes to %s\n", n, argv[1]);
+ }
+ free(pe);
+ }
+ free_compiler(c);
+ return 0;
+ }
+
+ run_case("x86_64-windows", KIT_ARCH_X86_64, 0x8664u);
+ run_case("aarch64-windows", KIT_ARCH_ARM_64, 0xAA64u);
+
+ if (g_failures) {
+ fprintf(stderr, "FAILED %d assertion(s)\n", g_failures);
+ return 1;
+ }
+ fprintf(stderr, "OK pe-image-read\n");
+ return 0;
+}
diff --git a/test/objdump/x86_64-windows/cases/04-pe-sections.sh b/test/objdump/x86_64-windows/cases/04-pe-sections.sh
@@ -1,7 +1,7 @@
-# Golden: objdump -h on a linked PE32+ executable. Asserts the PE
-# section walker fires (since kit_obj_open does not parse PE
-# images) and produces a section table with the canonical headers.
-# Gated on llvm-mingw UCRT availability.
+# Golden: objdump -h on a linked PE32+ executable. PE images now open
+# through kit_obj_open, so -h flows through the neutral dump_sections path
+# (the same "Sections:" header as ELF / Mach-O / COFF .obj), with .text and
+# .idata present. Gated on llvm-mingw UCRT availability.
find_sdk() {
local d
@@ -28,7 +28,7 @@ int main(void) { return 0; }
EOF
"$KIT" cc -target x86_64-windows --sysroot "$SDK" t.c -o t.exe
"$KIT" objdump -h t.exe | awk '
-/Sections \(PE image\)/ {print "found: PE sections"; next}
-/^Idx Name.*VMA.*Size/ {print "found: PE header row"; next}
+/^Sections:/ {print "found: PE sections"; next}
+/^Idx Name.*Size.*Align.*Flags/ {print "found: PE header row"; next}
/^ *[0-9]+ \.text/ {print "section: .text"; next}
/^ *[0-9]+ \.idata/ {print "section: .idata"; next}'
diff --git a/test/objdump/x86_64-windows/cases/pe-image.exe b/test/objdump/x86_64-windows/cases/pe-image.exe
Binary files differ.
diff --git a/test/objdump/x86_64-windows/cases/pe-image.expected b/test/objdump/x86_64-windows/cases/pe-image.expected
@@ -0,0 +1,48 @@
+pe-image.exe: file format coff64-x86_64
+
+architecture: x86_64, flags 0x00000102:
+EXEC_P, D_PAGED
+start address 0x0000000140001000
+format: coff64, sections: 6, symbols: 0
+
+subsystem: 3 (WINDOWS_CUI)
+
+Sections:
+Idx Name Size Align Flags
+ 0 .text 00000008 2**0 CONTENTS,ALLOC,LOAD,READONLY,CODE
+ Characteristics: 0x60000020
+ 1 .rdata 00000010 2**0 CONTENTS,ALLOC,LOAD,READONLY
+ Characteristics: 0x40000040
+ 2 .idata 00000065 2**0 CONTENTS,ALLOC,LOAD,READONLY
+ Characteristics: 0x40000040
+ 3 .data 00000008 2**0 CONTENTS,ALLOC,LOAD,DATA
+ Characteristics: 0xc0000040
+ 4 .reloc 0000000c 2**0 CONTENTS,ALLOC,LOAD,READONLY,DISCARDABLE
+ Characteristics: 0x42000040
+
+DYNAMIC SYMBOL TABLE:
+0000000000000000 g n *UND* 0000000000000000 ExitProcess
+
+
+pe-image.exe: PE32+ private headers
+ Magic: 0x20b (PE32+)
+ Machine: x86_64
+ ImageBase: 0x140000000
+ AddressOfEntryPoint: 0x1000
+ Subsystem: 3 (WINDOWS_CUI)
+ DllCharacteristics: 0x8160
+
+Data Directories:
+ Idx Name RVA Size
+ 1 IMPORT 0x00003000 0x00000028
+ 5 BASERELOC 0x00005000 0x0000000c
+ 12 IAT 0x00003038 0x00000010
+
+The Import Tables:
+ DLL Name: KERNEL32.dll
+ Name: ExitProcess
+
+DYNAMIC RELOCATION RECORDS
+OFFSET TYPE VALUE
+0000000140004000 X64_RELATIVE *ABS*
+
diff --git a/test/objdump/x86_64-windows/cases/pe-image.sh b/test/objdump/x86_64-windows/cases/pe-image.sh
@@ -0,0 +1,14 @@
+# Golden: -f / -h / -p / -T / -R over a committed freestanding x86_64 PE32+
+# executable (pe-image.exe). Exercises the COFF/PE *image* reader and the
+# raw-fields escape hatch end to end through the neutral objdump path:
+# -f EXEC_P / D_PAGED flags, real entry point, Windows subsystem
+# -h the PE section table (CONTENTS/ALLOC/CODE/... + raw Characteristics)
+# -p PE32+ optional-header highlights, data directories, import tables
+# -T the dynamic symbol table (the imported ExitProcess)
+# -R the base-relocation records (X64_RELATIVE for the PIE .data pointer)
+#
+# Committed as a binary (like test/objdump/aarch64/exec.elf) so the golden is
+# stable and decoupled from the code generator. Needs no external toolchain.
+# Regenerate with: build/test/pe-image-read <path> (see test/coff/pe-image-read.c).
+cp "$(dirname "$0")/pe-image.exe" pe-image.exe
+"$KIT" objdump -f -h -p -T -R pe-image.exe