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:
| M | TODO.md | | | 18 | ++++++++++++++++++ |
| M | doc/plan/KERNEL.md | | | 121 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | driver/cmd/image.c | | | 101 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | include/kit/image.h | | | 27 | +++++++++++++++++++++++++++ |
| M | src/obj/image.c | | | 186 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------ |
| M | test/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