kit

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

commit 36ae51be96a81e40ad3b4235fec8ee1fc3895195
parent 9cfa2d235c1d428fafc81a54d3808d37b0617204
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Tue, 16 Jun 2026 16:42:34 -0700

kit image: synthesize flat-kernel Image headers (arm64/riscv); plan cpio/initramfs

Add `--image-header[=arm64|riscv|auto]` to `kit image` (bin/rom): overlay a
64-byte flat-kernel Image header onto the first loadable segment, preserving the
author's entry branch (code0/code1) and filling the metadata tail
deterministically — text_offset, image_size, flags, and the per-arch
magic/version. image_size is the in-memory span INCLUDING BSS, computed from
KitObjSegInfo.vsize over all selected loadable segments (BSS-only file_size==0
segments included; the byte ranges skip them but the span must not). AUTO infers
the arch from the object's machine.

Boot-semantic fields are explicit opt-ins (--image-endian, --image-page-size
[arm64-only], --image-text-offset); kit fills magic/version/image_size but
invents no boot policy. The overlay keeps the output the same size as a plain
`bin`, so the pass-through path (author writes the full header) stays byte-exact.

- include/kit/image.h: KitImageHeader enum, options, report.{mem_size,image_header}
- src/obj/image.c: BSS-inclusive memory span; header build + arch resolve + overlay
- driver/cmd/image.c: flags, help, metadata sidecar (image_header/image_size)
- test/tools/run.sh: 23 cases — arm64/riscv synthesis, AUTO, pass-through,
  determinism, BSS-counted image_size, magic/version placement, negatives
- doc/plan/KERNEL.md: cpio/initramfs packaging plan (newc, gzip+lz4 only, no
  zstd/xz, compression as a `kit cpio` flag); Image-header section + checklist
- TODO.md: note kit-assembler symbol-difference (`_end - _start`) gap

Diffstat:
MTODO.md | 18++++++++++++++++++
Mdoc/plan/KERNEL.md | 121+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdriver/cmd/image.c | 101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Minclude/kit/image.h | 27+++++++++++++++++++++++++++
Msrc/obj/image.c | 186+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Mtest/tools/run.sh | 190+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 631 insertions(+), 12 deletions(-)

diff --git a/TODO.md b/TODO.md @@ -39,3 +39,21 @@ live in `doc/plan/PERF.md`, not here.) them. NB: streaming each section's `Buf` chunks instead of flatten-to-one `write()` was tried and is *slower* (more syscalls) — keep the single big write; only the dedup is wanted here. + +## Assembler: symbol-minus-symbol differences + +- **`.quad _end - _start` is rejected** by kit's own assembler with + `asm: cannot subtract symbol from constant` (`src/asm/asm.c` `parse_add`, + ~line 391). Only `sym - constant` and `sym - .` (PC-relative) are handled; + a `sym1 - sym2` difference (a link-time-resolvable constant when both are in + one section, or a relocation pair otherwise) is not. GNU as supports it. This + hides on hosts where the only path that assembles such sources is clang: e.g. + `test/link/cases/35_linker_script_kernel/entry.aa64.S` uses + `.quad _end - _start` for its Image-header `image_size`, but that fixture's + R-lane assembles with clang and its kit-assembled E-lane is qemu-gated (skipped + on macOS). It surfaces the moment kit's assembler is driven directly + (`kit build-exe`/`kit as`) on such a source — notably an author-written flat + kernel `Image` header that computes `image_size = _end - _start` (see + doc/plan/KERNEL.md). Fix in `parse_add`: when both operands carry a symbol, + record a symbol-difference expression and lower it to a constant (same section) + or a relocation pair, rather than panicking. diff --git a/doc/plan/KERNEL.md b/doc/plan/KERNEL.md @@ -226,6 +226,51 @@ kit image --format bin --from segments --segment PT_LOAD \ Optional later embedded formats, such as Intel HEX, S-record, or UF2, are out of the first kernel-focused pass. +### Flat kernel `Image` (arm64 / riscv64) + +QEMU's `-kernel` path on arm64 and riscv consumes the flat `Image` format: a raw +loadable binary prefixed with a fixed **64-byte header** the kernel's loader +reads to place and size the image. The first 32 bytes are common to both arches: + +| offset | field | notes | +|--------|-------------------|---------------------------------------------------------| +| 0 | `code0` (u32) | first instruction (branch to entry; `"MZ"` low half if EFI) | +| 4 | `code1` (u32) | second instruction | +| 8 | `text_offset` | u64 LE — load offset from a 2 MiB-aligned base | +| 16 | `image_size` | u64 LE — effective image size **including BSS** | +| 24 | `flags` | u64 LE — bit 0 endianness; arm64 bits 1-2 page size, bit 3 placement | + +The tails differ. arm64: three reserved u64s, `magic = "ARM\x64"` (`0x644d5241`) +at offset 56, then a u32 PE-offset slot. riscv64: a `version` u32 (currently +`0x2`), reserved words, a deprecated `"RISCV\0\0\0"` magic at 48, and +`magic2 = "RSC\x05"` (`0x05435352`) at 56. + +There are two ways to support this, and they have different costs: + +- **Author-owned header (pass-through) — already supported.** In the + Linux-native workflow the kernel's own startup code emits the 64-byte header + (`head.S` places `code0`/`code1` and declares `image_size` from linker + symbols). This matches the project stance that kernel startup is the author's + responsibility. kit needs **nothing new**: `kit image --format bin` / + `objcopy -O binary` copy PT_LOAD bytes verbatim, so the leading header survives + byte-exact and the result is a loadable `Image`. Only a doc note and a fixture + that asserts the header bytes and size are required. + +- **Kit-synthesized header — small add, the part worth doing.** The one field + that is awkward for authors is `image_size`, the in-memory footprint including + BSS, which kit already knows: `KitObjSegInfo.vsize` gives each loadable + segment's memory size (the current emitter lays out `file_size` only and + ignores it). So the add is: a ~64-byte header emitter, a new + `--format arm64-image` / `--format riscv-image` (or an + `--image-header=arm64|riscv` modifier on `--format bin`), and `vsize`-based + computation of `image_size` and the memory span. + +Because `flags` encodes boot semantics (endianness, page size, placement), +header synthesis is an explicit opt-in, never a default. Those fields are +surfaced as explicit options (e.g. `--image-endian`, `--image-page-size`, +`--image-text-offset`) rather than invented; kit fills magic/version and +`image_size` deterministically and does not choose a boot policy. + ### Image flags Selection: @@ -299,6 +344,43 @@ ROM-style emission should: - leave target-specific checksums or reset-vector conventions as future, explicit options. +## Initramfs and archive packaging + +The kernel-boot pipeline needs one packaging format kit does not yet emit: the +SVR4 `newc` **cpio** archive the Linux kernel unpacks as its initramfs. An +initramfs is a `cpio -H newc` archive (magic `070701`, or `070702` for the CRC +variant), optionally compressed, that the kernel's built-in extractor reads at +boot; the early-microcode convention is just an uncompressed cpio concatenated +ahead of the compressed main archive. This is an archive format, not a boot +protocol — the direct analogue of the existing `ar` and tar paths — so it +belongs in kit's byte-utility tool family, not the image emitter. kit packages +and inspects the archive; it does not build, mount, or boot it, and does not +invent its contents. + +Add a `kit cpio` tool, reusing the `src/dist/tar.c` patterns and the public +`kit/compress.h` codecs: + +- `newc` (SVR4 "portable") format only — the format the kernel requires. The + legacy `bin`/`odc` cpio formats are out of scope. +- Create from a directory or explicit file list, list (`-t`), and extract + (`-i`), with deterministic ordering, normalized mode/uid/gid/mtime, and the + closing `TRAILER!!!` record. Regular files, directories, and symlinks first; + special/device nodes (which need explicit major/minor) can come later via a + manifest if a use case appears. +- Concatenation: build and accept already-concatenated archives so early-init + cpio segments can be assembled and inspected. +- Compression as a flag on `kit cpio` (e.g. `--compress=gzip|lz4`, with short + `-z` / `--lz4`) that build-then-compresses in one step. An initramfs is just a + compressed `newc` archive, so the flag is the whole story — no separate + `initramfs` tool is warranted. gzip and lz4 **only**, the two initramfs + compressors kit already ships; zstd and xz are out of scope, with a clear + diagnostic rather than pretend support. On read (`-t` / `-i`), `-d` and + auto-detection let a compressed archive round-trip with no separate + decompress step. + +Gate the tool in `driver/main.c` (`KIT_TOOL_CPIO_ENABLED`) alongside the other +archive utilities. + ## Implementation shape Add a shared image-emission layer rather than burying policy in `objcopy`: @@ -441,3 +523,42 @@ metadata accuracy, and cc/ld report parity). The checklist below is closed out. goldens for a pinned link). - [x] Negative tests: region overflow, discarded sections, dynamic artifacts, NOLOAD-with-PROGBITS, and cross-arch freestanding rejection. + +### Newly scoped: initramfs and `Image` packaging + +Added on top of the closed-out Phase 1-5 work; not yet started. + +Initramfs / cpio (archive packaging, sibling to `ar`): + +- [ ] `kit cpio` (newc/SVR4 only, magic `070701`/`070702`): create / list (`-t`) + / extract (`-i`) with deterministic ordering and normalized metadata, closing + `TRAILER!!!`; reuse `src/dist/tar.c` patterns. +- [ ] Archive concatenation for early-init segments (build + accept concatenated + inputs). +- [ ] Compression as a `kit cpio` flag (`--compress=gzip|lz4`, `-z`/`--lz4`; + `-d`/auto-detect on read) — gzip + lz4 only, clear diagnostic for zstd/xz. An + initramfs is just a compressed newc archive, so no separate `initramfs` tool. +- [ ] Tool gating in `driver/main.c` (`KIT_TOOL_CPIO_ENABLED`) + a round-trip + fixture (pack → list → unpack, byte-deterministic) and a kernel-unpack shape + check. + +Flat kernel `Image` header (arm64 / riscv64) — landed via the `--image-header` +modifier on `--format bin`/`rom` (not separate formats); it overlays the first +64 bytes of the first loadable segment rather than prepending, so the entry +branch (code0/code1) is preserved and the output stays the same size as a plain +`bin`. + +- [x] Pass-through: an author-emitted 64-byte header survives + `kit image --format bin` byte-exact (magic + image_size preserved); covered by + the `image-hdr-pt-*` fixtures. +- [x] `--image-header=arm64|riscv|auto` (AUTO infers the arch from the object's + machine): synthesizes the 64-byte header; `image_size` is the in-memory span + **including BSS**, computed from `KitObjSegInfo.vsize` over all selected + loadable segments (BSS-only `file_size==0` segments included — the byte ranges + skip them, the span must not). +- [x] Explicit boot-semantic options (`--image-endian`, `--image-page-size` + [arm64-only], `--image-text-offset`); magic/version filled deterministically; + no invented boot policy. The sub-options error without `--image-header`. +- [x] Per-arch fixtures (aarch64 / riscv64) in `test/tools/run.sh`: synthesized + header is self-consistent (magic at 56, riscv version 2 at 32, `image_size` + counts the BSS), deterministic, and the entry branch is preserved. diff --git a/driver/cmd/image.c b/driver/cmd/image.c @@ -75,6 +75,16 @@ void driver_help_image(void) { " --pad-to SIZE pad final size to SIZE\n" " --max-size SIZE reject output larger than SIZE\n" "\n" + "KERNEL IMAGE HEADER (bin/rom; preserves the entry branch, fills the\n" + "metadata tail — the first segment must reserve a 64-byte header)\n" + " --image-header[=arm64|riscv|auto] synthesize a flat-kernel " + "Image header\n" + " --image-text-offset N header text_offset field (default: 0)\n" + " --image-endian little|big header endianness flag (default: " + "little)\n" + " --image-page-size 4k|16k|64k arm64 page-size flag (default: " + "unspecified)\n" + "\n" "VALIDATION\n" " --require-entry fail unless the image declares an " "entry\n" @@ -239,6 +249,25 @@ static int parse_addr(const char* s, uint32_t* out) { return -1; } +static int parse_image_header(const char* s, uint32_t* out) { + if (driver_streq(s, "arm64") || driver_streq(s, "aarch64") || + driver_streq(s, "aa64")) { + *out = KIT_IMAGE_HEADER_ARM64; + return 0; + } + if (driver_streq(s, "riscv") || driver_streq(s, "riscv64") || + driver_streq(s, "rv64") || driver_streq(s, "riscv32") || + driver_streq(s, "rv32")) { + *out = KIT_IMAGE_HEADER_RISCV; + return 0; + } + if (driver_streq(s, "auto")) { + *out = KIT_IMAGE_HEADER_AUTO; + return 0; + } + return -1; +} + /* Append cstr `name` to a growable KitSlice list, re-aliasing the matching * KitImageOptions pointer at *aliased_ptr to the (possibly reallocated) * storage and bumping *count. */ @@ -500,6 +529,13 @@ static int write_metadata(DriverEnv* env, const KitContext* ctx, KitObjFile* of, (unsigned long long)report->payload_size); meta_putf(&mb, " \"output_size\": %llu,\n", (unsigned long long)report->size); + if (report->image_header != KIT_IMAGE_HEADER_NONE) { + meta_putf(&mb, " \"image_header\": \"%s\",\n", + report->image_header == KIT_IMAGE_HEADER_RISCV ? "riscv" + : "arm64"); + meta_putf(&mb, " \"image_size\": %llu,\n", + (unsigned long long)report->mem_size); + } meta_putf(&mb, " \"ranges\": %u,\n", (unsigned)report->nranges); meta_putf(&mb, " \"max_hole\": %llu,\n", (unsigned long long)report->max_hole); @@ -754,6 +790,63 @@ int driver_image(int argc, char** argv) { o.image.have_max_size = true; continue; } + /* Bare --image-header infers the arch from the object; --image-header=VALUE + * names it. The space form is intentionally not accepted so a following + * input path is never mistaken for the value. */ + if (driver_streq(a, "--image-header")) { + o.image.image_header = KIT_IMAGE_HEADER_AUTO; + continue; + } + if (driver_strneq(a, "--image-header=", 15)) { + if (parse_image_header(a + 15, &o.image.image_header) != 0) { + driver_errf(IMAGE_TOOL, "unknown image header: %s", a + 15); + goto done; + } + continue; + } + matched = take_value(&i, argc, argv, "--image-text-offset", &val); + if (matched < 0) goto missing_value; + if (matched) { + if (parse_size(val, &o.image.image_text_offset) != 0) { + driver_errf(IMAGE_TOOL, "invalid --image-text-offset value: %s", val); + goto done; + } + o.image.have_image_text_offset = true; + continue; + } + matched = take_value(&i, argc, argv, "--image-endian", &val); + if (matched < 0) goto missing_value; + if (matched) { + if (driver_streq(val, "little") || driver_streq(val, "le")) { + o.image.image_big_endian = false; + } else if (driver_streq(val, "big") || driver_streq(val, "be")) { + o.image.image_big_endian = true; + } else { + driver_errf(IMAGE_TOOL, "invalid --image-endian value: %s", val); + goto done; + } + continue; + } + matched = take_value(&i, argc, argv, "--image-page-size", &val); + if (matched < 0) goto missing_value; + if (matched) { + uint64_t bytes; + if (parse_size(val, &bytes) != 0) { + driver_errf(IMAGE_TOOL, "invalid --image-page-size value: %s", val); + goto done; + } + if (bytes == 4096u) + o.image.image_page_size_kib = 4; + else if (bytes == 16384u) + o.image.image_page_size_kib = 16; + else if (bytes == 65536u) + o.image.image_page_size_kib = 64; + else { + driver_errf(IMAGE_TOOL, "--image-page-size must be 4k, 16k, or 64k"); + goto done; + } + continue; + } if (a[0] == '-' && a[1] != '\0') { driver_errf(IMAGE_TOOL, "unknown option: %s", a); goto done; @@ -777,6 +870,14 @@ int driver_image(int argc, char** argv) { goto done; } } + if (o.image.image_header == KIT_IMAGE_HEADER_NONE && + (o.image.have_image_text_offset || o.image.image_big_endian || + o.image.image_page_size_kib)) { + driver_errf(IMAGE_TOOL, + "--image-text-offset/--image-endian/--image-page-size require " + "--image-header"); + goto done; + } if (driver_load_bytes(&env.file_io, IMAGE_TOOL, o.input, &load, &input) != 0) { rc = 1; diff --git a/include/kit/image.h b/include/kit/image.h @@ -29,6 +29,19 @@ typedef enum KitImageAddrKind { KIT_IMAGE_ADDR_LMA, } KitImageAddrKind; +/* Flat-kernel `Image` header to synthesize over a `bin`/`rom` image. The first + * 8 bytes of the first loadable segment (the author's entry branch, code0/code1) + * are preserved; kit fills the 56-byte metadata tail deterministically: + * text_offset, image_size (the in-memory span incl. BSS), flags, and the + * per-arch magic/version. The author's first segment must reserve those 56 + * bytes. AUTO picks arm64/riscv from the object's machine. */ +typedef enum KitImageHeader { + KIT_IMAGE_HEADER_NONE = 0, + KIT_IMAGE_HEADER_ARM64, + KIT_IMAGE_HEADER_RISCV, + KIT_IMAGE_HEADER_AUTO, +} KitImageHeader; + typedef struct KitImageOptions { uint32_t format; /* KitImageFormat */ uint32_t from; /* KitImageSource */ @@ -80,6 +93,13 @@ typedef struct KitImageOptions { const KitSlice* require_sections; uint32_t nrequire_sections; bool no_dynamic; /* reject dynamic linking artifacts (interp/PLT-GOT/dynamic) */ + + /* Flat-kernel `Image` header synthesis (bin/rom, segment source only). */ + uint32_t image_header; /* KitImageHeader; NONE disables the whole block */ + bool have_image_text_offset; + uint64_t image_text_offset; /* header text_offset field; default 0 */ + bool image_big_endian; /* header flags bit 0; default little-endian */ + uint32_t image_page_size_kib; /* arm64 flags bits 1-2: 0 unspecified / 4/16/64 */ } KitImageOptions; typedef struct KitImageReport { @@ -87,6 +107,13 @@ typedef struct KitImageReport { uint64_t size; uint64_t payload_size; uint64_t max_hole; + /* In-memory span of the selected loadable segments (incl. trailing BSS), + * measured from `base`. This is the value written to a synthesized Image + * header's image_size field. */ + uint64_t mem_size; + /* The Image header kind actually written (KitImageHeader), resolved from + * AUTO; KIT_IMAGE_HEADER_NONE when no header was synthesized. */ + uint32_t image_header; uint32_t nranges; bool had_holes; /* True when the report describes a section concatenation rather than a diff --git a/src/obj/image.c b/src/obj/image.c @@ -17,7 +17,7 @@ typedef struct ImageRange { const uint8_t* data; uint64_t size; uint64_t addr; - uint64_t end; + uint64_t end; /* addr + on-disk size (file end) */ KitSlice name; uint32_t order; } ImageRange; @@ -52,6 +52,18 @@ static int u64_align_up(uint64_t v, uint64_t align, uint64_t* out) { return u64_add(v, align - rem, out); } +static void put_u32le(uint8_t* p, uint32_t v) { + p[0] = (uint8_t)v; + p[1] = (uint8_t)(v >> 8); + p[2] = (uint8_t)(v >> 16); + p[3] = (uint8_t)(v >> 24); +} + +static void put_u64le(uint8_t* p, uint64_t v) { + put_u32le(p, (uint32_t)v); + put_u32le(p + 4, (uint32_t)(v >> 32)); +} + static int ascii_lower(int c) { return (c >= 'A' && c <= 'Z') ? c + ('a' - 'A') : c; } @@ -194,18 +206,20 @@ static void ranges_sort(ImageRange* r, uint32_t n) { static KitStatus collect_ranges(const KitContext* ctx, KitObjFile* obj, const KitSlice* bytes, const KitImageOptions* opts, - ImageRange** ranges_out, - uint32_t* nranges_out, uint32_t* cap_out) { + ImageRange** ranges_out, uint32_t* nranges_out, + uint32_t* cap_out, uint64_t* mem_end_out) { KitObjSegIter* it = NULL; KitObjSegInfo seg; ImageRange* ranges = NULL; uint32_t nranges = 0, cap = 0, order = 0; + uint64_t mem_end_max = 0; KitStatus st; KitObjFmt fmt; *ranges_out = NULL; *nranges_out = 0; *cap_out = 0; + *mem_end_out = 0; if (kit_obj_kind(obj) == KIT_OBJ_KIND_REL) { kit_ctx_diagf(ctx, "image: input is relocatable; linked image required"); @@ -225,6 +239,17 @@ static KitStatus collect_ranges(const KitContext* ctx, KitObjFile* obj, ++order; continue; } + /* Memory-span accounting runs for every selected loadable segment, + * including BSS-only ones (file_size == 0) that contribute no bytes but do + * extend the in-memory footprint an Image header's image_size must report. + * vsize (>= file_size) carries the trailing BSS. */ + { + uint64_t maddr, mend; + uint64_t msize = seg.vsize >= seg.file_size ? seg.vsize : seg.file_size; + if (u64_sub_bias(seg_addr(&seg, opts), opts->bias, &maddr) && + u64_add(maddr, msize, &mend) && mend > mem_end_max) + mem_end_max = mend; + } if (seg.file_size == 0) { ++order; continue; @@ -271,6 +296,7 @@ static KitStatus collect_ranges(const KitContext* ctx, KitObjFile* obj, *ranges_out = ranges; *nranges_out = nranges; *cap_out = cap; + *mem_end_out = mem_end_max; return KIT_OK; } @@ -281,7 +307,7 @@ static KitStatus collect_ranges(const KitContext* ctx, KitObjFile* obj, static KitStatus collect_sections(const KitContext* ctx, KitObjFile* obj, const KitImageOptions* opts, ImageRange** ranges_out, uint32_t* nranges_out, - uint32_t* cap_out) { + uint32_t* cap_out, uint64_t* mem_end_out) { ImageRange* ranges = NULL; uint32_t nranges = 0, cap = 0; uint64_t cursor = 0; @@ -291,6 +317,7 @@ static KitStatus collect_sections(const KitContext* ctx, KitObjFile* obj, *ranges_out = NULL; *nranges_out = 0; *cap_out = 0; + *mem_end_out = 0; if (opts->nsection_order == 0) { kit_ctx_diagf(ctx, "image: --format sections requires at least one " @@ -361,13 +388,13 @@ static KitStatus collect_sections(const KitContext* ctx, KitObjFile* obj, *ranges_out = ranges; *nranges_out = nranges; *cap_out = cap; + *mem_end_out = cursor; /* concatenation has no separate memory image */ return KIT_OK; } static KitStatus compute_layout(const KitContext* ctx, const ImageRange* ranges, - uint32_t nranges, - const KitImageOptions* opts, - KitImageReport* report) { + uint32_t nranges, const KitImageOptions* opts, + uint64_t seg_mem_end, KitImageReport* report) { uint64_t base = opts->have_base ? opts->base : ranges[0].addr; uint64_t cur = base; uint64_t payload = 0; @@ -420,6 +447,9 @@ static KitStatus compute_layout(const KitContext* ctx, const ImageRange* ranges, report->size = cur - base; report->payload_size = payload; report->max_hole = max_hole; + /* In-memory span from the image base. seg_mem_end covers BSS-only segments + * the byte ranges skip; fall back to the file span if it is somehow lower. */ + report->mem_size = seg_mem_end > base ? seg_mem_end - base : cur - base; report->nranges = nranges; report->had_holes = had_holes ? true : false; @@ -464,9 +494,105 @@ static KitStatus write_bytes(KitWriter* out, const uint8_t* data, size_t n) { return n ? kit_writer_write(out, data, n) : KIT_OK; } +/* Resolve a requested Image-header kind (possibly AUTO) to a concrete arch from + * the object's machine. */ +static KitStatus resolve_image_header(const KitContext* ctx, KitObjFile* obj, + uint32_t want, KitImageHeader* out) { + KitTargetSpec spec; + if (want == KIT_IMAGE_HEADER_ARM64 || want == KIT_IMAGE_HEADER_RISCV) { + *out = (KitImageHeader)want; + return KIT_OK; + } + spec = kit_obj_target(obj); + switch (spec.arch) { + case KIT_ARCH_ARM_64: + *out = KIT_IMAGE_HEADER_ARM64; + return KIT_OK; + case KIT_ARCH_RV64: + case KIT_ARCH_RV32: + *out = KIT_IMAGE_HEADER_RISCV; + return KIT_OK; + default: + break; + } + kit_ctx_diagf(ctx, "image: --image-header could not infer the architecture; " + "pass --image-header=arm64 or =riscv"); + return KIT_INVALID; +} + +/* Fill a 64-byte flat-kernel Image header. code0/code1 (the first 8 bytes of the + * first loadable segment, i.e. the author's entry branch) are preserved; the + * 56-byte metadata tail is synthesized deterministically. */ +static KitStatus build_image_header(const KitContext* ctx, KitImageHeader kind, + const ImageRange* first, + const KitImageReport* report, + const KitImageOptions* opts, + uint8_t hdr[64]) { + uint64_t flags = 0; + uint64_t text_offset = + opts->have_image_text_offset ? opts->image_text_offset : 0; + + if (first->addr != report->base) { + kit_ctx_diagf(ctx, "image: --image-header needs the first loadable segment " + "at the image base (no leading hole)"); + return KIT_INVALID; + } + if (first->size < 64) { + kit_ctx_diagf(ctx, "image: --image-header needs the first loadable segment " + "to reserve a 64-byte header"); + return KIT_INVALID; + } + + if (opts->image_big_endian) flags |= 1u; + if (opts->image_page_size_kib) { + if (kind != KIT_IMAGE_HEADER_ARM64) { + kit_ctx_diagf(ctx, + "image: --image-page-size applies only to the arm64 header"); + return KIT_INVALID; + } + switch (opts->image_page_size_kib) { + case 4: + flags |= (uint64_t)1u << 1; + break; + case 16: + flags |= (uint64_t)2u << 1; + break; + case 64: + flags |= (uint64_t)3u << 1; + break; + default: + kit_ctx_diagf(ctx, + "image: --image-page-size must be 4, 16, or 64 (KiB)"); + return KIT_INVALID; + } + } + + memset(hdr, 0, 64); + memcpy(hdr, first->data, 8); /* code0/code1: preserve the entry branch */ + put_u64le(hdr + 8, text_offset); + put_u64le(hdr + 16, report->mem_size); + put_u64le(hdr + 24, flags); + if (kind == KIT_IMAGE_HEADER_ARM64) { + /* res2..res4 zero; magic "ARM\x64" at 56; PE-offset slot (60) zero. */ + hdr[56] = 0x41; + hdr[57] = 0x52; + hdr[58] = 0x4d; + hdr[59] = 0x64; + } else { + /* version 2 at 32; reserved/deprecated-magic zero; magic2 "RSC\x05" at 56. */ + put_u32le(hdr + 32, 0x00000002u); + hdr[56] = 0x52; + hdr[57] = 0x53; + hdr[58] = 0x43; + hdr[59] = 0x05; + } + return KIT_OK; +} + static KitStatus write_layout(const KitContext* ctx, const ImageRange* ranges, uint32_t nranges, const KitImageOptions* opts, - const KitImageReport* report, KitWriter* out) { + const KitImageReport* report, + const uint8_t* image_hdr, KitWriter* out) { uint64_t cur = report->base; uint32_t i; for (i = 0; i < nranges; ++i) { @@ -476,6 +602,17 @@ static KitStatus write_layout(const KitContext* ctx, const ImageRange* ranges, kit_ctx_diagf(ctx, "image: failed to write hole fill"); return KIT_IO; } + /* The synthesized header overlays the first 64 bytes of the first range + * (validated >= 64 and at the base); the remaining bytes follow verbatim. */ + if (i == 0 && image_hdr) { + if (write_bytes(out, image_hdr, 64) != KIT_OK || + write_bytes(out, r->data + 64, (size_t)(r->size - 64)) != KIT_OK) { + kit_ctx_diagf(ctx, "image: failed to write header + segment bytes"); + return KIT_IO; + } + cur = r->end; + continue; + } if (write_bytes(out, r->data, (size_t)r->size) != KIT_OK) { kit_ctx_diagf(ctx, "image: failed to write segment bytes"); return KIT_IO; @@ -593,6 +730,9 @@ KitStatus obj_emit_image(const KitContext* ctx, KitObjFile* obj, KitImageFormat format; KitStatus st; int from_sections; + uint64_t seg_mem_end = 0; + uint8_t image_hdr[64]; + const uint8_t* image_hdr_p = NULL; if (!ctx || !ctx->heap || !obj || !object_bytes || !out) return KIT_INVALID; if (!object_bytes->data && object_bytes->len) return KIT_INVALID; @@ -670,22 +810,44 @@ KitStatus obj_emit_image(const KitContext* ctx, KitObjFile* obj, "image: --format rom requires an explicit size via --pad-to"); return KIT_INVALID; } + if (opts->image_header != KIT_IMAGE_HEADER_NONE) { + if (format != KIT_IMAGE_FORMAT_BIN && format != KIT_IMAGE_FORMAT_ROM) { + kit_ctx_diagf(ctx, "image: --image-header requires --format bin or rom"); + return KIT_INVALID; + } + if (from_sections) { + kit_ctx_diagf( + ctx, "image: --image-header is incompatible with a section source"); + return KIT_INVALID; + } + } st = validate_object(ctx, obj, opts); if (st != KIT_OK) return st; if (from_sections) { - st = collect_sections(ctx, obj, opts, &ranges, &nranges, &cap); + st = collect_sections(ctx, obj, opts, &ranges, &nranges, &cap, &seg_mem_end); } else { - st = collect_ranges(ctx, obj, object_bytes, opts, &ranges, &nranges, &cap); + st = collect_ranges(ctx, obj, object_bytes, opts, &ranges, &nranges, &cap, + &seg_mem_end); } if (st == KIT_OK) { memset(&report, 0, sizeof report); report.sections_concat = from_sections ? true : false; - st = compute_layout(ctx, ranges, nranges, opts, &report); + st = compute_layout(ctx, ranges, nranges, opts, seg_mem_end, &report); + } + if (st == KIT_OK && opts->image_header != KIT_IMAGE_HEADER_NONE) { + KitImageHeader kind; + st = resolve_image_header(ctx, obj, opts->image_header, &kind); + if (st == KIT_OK) + st = build_image_header(ctx, kind, &ranges[0], &report, opts, image_hdr); + if (st == KIT_OK) { + image_hdr_p = image_hdr; + report.image_header = (uint32_t)kind; + } } if (st == KIT_OK) { - st = write_layout(ctx, ranges, nranges, opts, &report, out); + st = write_layout(ctx, ranges, nranges, opts, &report, image_hdr_p, out); } if (st == KIT_OK && report_out) *report_out = report; ranges_free(ctx, ranges, cap); diff --git a/test/tools/run.sh b/test/tools/run.sh @@ -325,5 +325,195 @@ run_fail image-meta-remove-all "$KIT" image --format bin \ -o "$work/sel.rm.bin" # ==== END kernel-FZ Bug 10 =================================================== +# ---- image: flat-kernel Image header (--image-header) ---------------------- +# A freestanding kernel whose first 64 bytes are an Image header AREA: an entry +# branch in the first 8 bytes (code0/code1) and 56 reserved bytes the emitter +# fills. A 4 KiB .bss makes the in-memory span exceed the file bytes, so a +# correct image_size must count BSS. The skeleton leaves the metadata zero so a +# plain `--format bin` (raw) is a witness that the emitter, not the author, +# wrote the magic and size. +cat > "$work/khdr_a64.S" <<'EOF' + .section .text, "ax" + .globl _start +_start: + b kstart /* 0x00 code0: branch over the header */ + .long 0 /* 0x04 code1 */ + .quad 0 /* 0x08 text_offset (kit fills) */ + .quad 0 /* 0x10 image_size (kit fills) */ + .quad 0 /* 0x18 flags (kit fills) */ + .quad 0 /* 0x20 res2 */ + .quad 0 /* 0x28 res3 */ + .quad 0 /* 0x30 res4 */ + .long 0 /* 0x38 magic (kit fills) */ + .long 0 /* 0x3c res5 */ +kstart: + ret + .section .data, "aw" + .byte 0x42 + .section .bss, "aw", %nobits + .balign 16 + .skip 4096 +EOF +cat > "$work/khdr_a64.lds" <<'EOF' +ENTRY(_start) +SECTIONS { + . = 0x40080000; + .text : ALIGN(8) { *(.text .text.*) } + .data : ALIGN(8) { *(.data .data.*) } + .bss : ALIGN(16) { *(.bss .bss.*) } + _end = .; + /DISCARD/ : { *(.note.*) *(.comment) *(.eh_frame) } +} +EOF +run_ok image-hdr-a64-build "$KIT" build-exe -target aarch64-none-elf \ + -nostdlib -nostartfiles -static -no-pie -e _start -T "$work/khdr_a64.lds" \ + "$work/khdr_a64.S" -o "$work/khdr_a64.elf" +run_ok image-hdr-a64 "$KIT" image --format bin --image-header=arm64 \ + --metadata "$work/khdr_a64.json" "$work/khdr_a64.elf" -o "$work/khdr_a64.img" +# Plain bin is the witness for code0/code1, total size, and the empty skeleton. +run_ok image-hdr-a64-raw "$KIT" image --format bin "$work/khdr_a64.elf" \ + -o "$work/khdr_a64.raw" +# The entry branch (code0/code1) is preserved verbatim. +head -c 8 "$work/khdr_a64.img" > "$work/a64.code0" +head -c 8 "$work/khdr_a64.raw" > "$work/a64.code0.raw" +same_file image-hdr-a64-code0-preserved "$work/a64.code0.raw" "$work/a64.code0" +# Overlay, not prepend: same length as plain bin. +if [ "$(wc -c < "$work/khdr_a64.img")" = "$(wc -c < "$work/khdr_a64.raw")" ]; then + ok image-hdr-a64-overlay-size +else + not_ok image-hdr-a64-overlay-size +fi +# arm64 magic "ARM\x64" lands at offset 56; the skeleton (raw) is still zero +# there, proving the emitter synthesized it. +tail -c +57 "$work/khdr_a64.img" | head -c 4 | "$KIT" xxd -p > "$work/a64.magic" +contains image-hdr-a64-magic "$work/a64.magic" "41524d64" +tail -c +57 "$work/khdr_a64.raw" | head -c 4 | "$KIT" xxd -p > "$work/a64.rawmagic" +contains image-hdr-a64-skeleton-zero "$work/a64.rawmagic" "00000000" +# image_size (header offset 16) is written and counts the 4 KiB BSS: it exceeds +# the file image and the metadata mirrors it. +tail -c +17 "$work/khdr_a64.img" | head -c 8 | "$KIT" xxd -p > "$work/a64.szf.hex" +if [ "$(cat "$work/a64.szf.hex")" != "0000000000000000" ]; then + ok image-hdr-a64-size-written +else + not_ok image-hdr-a64-size-written +fi +a64_imgsz=$(grep '"image_size"' "$work/khdr_a64.json" | tr -dc '0-9') +a64_filesz=$(wc -c < "$work/khdr_a64.img" | tr -d ' ') +if [ "$a64_imgsz" -ge 4096 ] && [ "$a64_imgsz" -gt "$a64_filesz" ]; then + ok image-hdr-a64-size-counts-bss +else + echo "image_size=$a64_imgsz file=$a64_filesz" > "$work/a64sz.diag" + not_ok image-hdr-a64-size-counts-bss "$work/a64sz.diag" +fi +# Determinism: identical bytes on a second run. +run_ok image-hdr-a64-again "$KIT" image --format bin --image-header=arm64 \ + "$work/khdr_a64.elf" -o "$work/khdr_a64.img2" +same_file image-hdr-a64-deterministic "$work/khdr_a64.img" "$work/khdr_a64.img2" + +# riscv64: same shape, exercised through AUTO arch inference (bare flag). The +# riscv tail differs — version 2 at offset 32 and magic2 "RSC\x05" at 56. +cat > "$work/khdr_rv.S" <<'EOF' + .section .text, "ax" + .globl _start +_start: + j kstart /* 0x00 code0 */ + .long 0 /* 0x04 code1 */ + .quad 0 /* 0x08 text_offset */ + .quad 0 /* 0x10 image_size */ + .quad 0 /* 0x18 flags */ + .long 0 /* 0x20 version (kit fills) */ + .long 0 /* 0x24 res1 */ + .quad 0 /* 0x28 res2 */ + .quad 0 /* 0x30 deprecated magic */ + .long 0 /* 0x38 magic2 (kit fills) */ + .long 0 /* 0x3c res3 */ +kstart: + ret + .section .data, "aw" + .byte 0x42 + .section .bss, "aw", %nobits + .balign 16 + .skip 8192 +EOF +cat > "$work/khdr_rv.lds" <<'EOF' +ENTRY(_start) +SECTIONS { + . = 0x80200000; + .text : ALIGN(8) { *(.text .text.*) } + .data : ALIGN(8) { *(.data .data.*) } + .bss : ALIGN(16) { *(.bss .bss.*) } + _end = .; + /DISCARD/ : { *(.note.*) *(.comment) *(.eh_frame) } +} +EOF +run_ok image-hdr-rv-build "$KIT" build-exe -target riscv64-none-elf \ + -nostdlib -nostartfiles -static -no-pie -e _start -T "$work/khdr_rv.lds" \ + "$work/khdr_rv.S" -o "$work/khdr_rv.elf" +run_ok image-hdr-rv-auto "$KIT" image --format bin --image-header \ + --metadata "$work/khdr_rv.json" "$work/khdr_rv.elf" -o "$work/khdr_rv.img" +contains image-hdr-rv-auto-detected "$work/khdr_rv.json" '"image_header": "riscv"' +tail -c +57 "$work/khdr_rv.img" | head -c 4 | "$KIT" xxd -p > "$work/rv.magic" +contains image-hdr-rv-magic2 "$work/rv.magic" "52534305" +tail -c +33 "$work/khdr_rv.img" | head -c 4 | "$KIT" xxd -p > "$work/rv.ver" +contains image-hdr-rv-version "$work/rv.ver" "02000000" +rv_imgsz=$(grep '"image_size"' "$work/khdr_rv.json" | tr -dc '0-9') +if [ "$rv_imgsz" -ge 8192 ]; then + ok image-hdr-rv-size-counts-bss +else + echo "image_size=$rv_imgsz" > "$work/rvsz.diag" + not_ok image-hdr-rv-size-counts-bss "$work/rvsz.diag" +fi + +# Pass-through: an author who writes the full header keeps it byte-exact through +# a plain `--format bin` (no --image-header). image_size = _end - _start and the +# magic are the author's own bytes, untouched by the emitter. +cat > "$work/khdr_pt.S" <<'EOF' + .section .text, "ax" + .globl _start +_start: + b kstart + .long 0 + .quad 0x80000 /* text_offset */ + .quad 0x4000 /* image_size (author-written constant) */ + .quad 0xa /* flags: 4K, LE */ + .quad 0 + .quad 0 + .quad 0 + .ascii "ARM\x64" /* magic */ + .long 0 +kstart: + ret + .section .bss, "aw", %nobits + .balign 16 + .skip 256 +EOF +run_ok image-hdr-pt-build "$KIT" build-exe -target aarch64-none-elf \ + -nostdlib -nostartfiles -static -no-pie -e _start -T "$work/khdr_a64.lds" \ + "$work/khdr_pt.S" -o "$work/khdr_pt.elf" +run_ok image-hdr-pt-bin "$KIT" image --format bin "$work/khdr_pt.elf" \ + -o "$work/khdr_pt.bin" +tail -c +57 "$work/khdr_pt.bin" | head -c 4 | "$KIT" xxd -p > "$work/pt.magic" +contains image-hdr-pt-magic-preserved "$work/pt.magic" "41524d64" +tail -c +17 "$work/khdr_pt.bin" | head -c 8 | "$KIT" xxd -p > "$work/pt.sz.hex" +if [ "$(cat "$work/pt.sz.hex")" != "0000000000000000" ]; then + ok image-hdr-pt-size-preserved +else + not_ok image-hdr-pt-size-preserved +fi +run_ok image-hdr-pt-again "$KIT" image --format bin "$work/khdr_pt.elf" \ + -o "$work/khdr_pt.bin2" +same_file image-hdr-pt-deterministic "$work/khdr_pt.bin" "$work/khdr_pt.bin2" + +# Negative: a section source has no memory model, AUTO needs a known arch, the +# page-size flag is arm64-only, and the header sub-options need --image-header. +run_fail image-hdr-rejects-sections "$KIT" image --format sections \ + --section .text --image-header=arm64 "$work/kernel.elf" -o "$work/h.bad.bin" +run_fail image-hdr-auto-unknown-arch "$KIT" image --format bin --image-header \ + "$work/kernel.elf" -o "$work/h.bad.bin" +run_fail image-hdr-page-size-riscv "$KIT" image --format bin --image-header=riscv \ + --image-page-size 16k "$work/khdr_rv.elf" -o "$work/h.bad.bin" +run_fail image-hdr-opts-need-header "$KIT" image --format bin \ + --image-text-offset 0x80000 "$work/kernel.elf" -o "$work/h.bad.bin" + kit_summary tools-driver kit_exit