commit a3a93d53b51655c1a9a307c1f658d018ef4dfd28
parent 439a6d35ca041e458890b3949375adfd71862667
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Wed, 17 Jun 2026 06:23:09 -0700
Add ihex and srec image output
Diffstat:
6 files changed, 681 insertions(+), 25 deletions(-)
diff --git a/doc/plan/MCU.md b/doc/plan/MCU.md
@@ -0,0 +1,194 @@
+# Plan: Microcontroller development (STM32 Cortex-M, Espressif RISC-V)
+
+## Status — 2026-06-17 — first last-mile image formats landed
+
+Kit's core compile→link→object pipeline is already in good shape for bare-metal
+MCU work — the hard part (an embedded-grade linker with full `MEMORY`/`SECTIONS`/
+`AT>` linker-script support, `--gc-sections`, freestanding `ET_EXEC`) is done,
+and both 32-bit backends produce correct -O0/-O1 code. The gaps are concentrated
+in **last-mile workflow** (startup stubs, target docs, on-target debug) and a
+few **defaults footguns**. Intel HEX and Motorola S-record output are now
+covered by `kit image --format ...` and `kit objcopy -O ...`.
+
+This doc owns the MCU **workflow integration** roadmap. The ARM **backend**
+follow-ons that MCU work depends on (hard-float, `.ARM.attributes`, GNU numeric
+local labels, the kit-compiled reset stub) are tracked in
+[ARM32.md](ARM32.md) — referenced here by MCU impact, not duplicated. The RV32
+backend contract lives in [../RUNTIME.md](../RUNTIME.md); the linker-script
+surface in [LINKER.md](LINKER.md) / [../LINK.md](../LINK.md).
+
+## Scope
+
+**In scope (the two named ecosystems, minus Xtensa):**
+
+- **STMicro STM32** on Cortex-M3 (ARMv7-M) and Cortex-M4/M4F/M7 (ARMv7E-M):
+ STM32 F1/L1/F2 today (soft-float, correct — these have no FPU); F3/F4/L4/G4/
+ F7/H7 compile soft-float now and gain the FPU once ARM hard-float lands.
+- **Espressif RISC-V** parts: ESP32-C2/C3 (RV32IMC), C6/H2 (RV32IMAC), P4
+ (RV32IMAFC). The C extension is already supported, which is what these need.
+
+**Explicitly out of scope (for now):**
+
+- **ARMv6-M / Cortex-M0 / M0+** (STM32 F0/L0/G0/C0). The backend emits
+ ARMv7-M-only ops (hardware `SDIV`/`UDIV`, `MOVW`/`MOVT`, wide branches, full
+ IT) that ARMv6-M lacks. Deferred — see the ARM32.md follow-on list.
+- **Xtensa** (classic ESP32 / ESP32-S2 / S3, ESP8266). No backend; out of scope.
+ Espressif support here means their RISC-V silicon only.
+- **ARMv8-M / TrustZone** (Cortex-M23/M33; STM32 L5/U5/H5). Untested, unmodeled.
+
+## §0 — Target coverage matrix
+
+| Vendor / family | Core (ISA) | Status |
+|---|---|---|
+| STM32 F1/L1/F2 | Cortex-M3 (ARMv7-M) | **Works** — soft-float, correct (no FPU on part) |
+| STM32 F3/F4/L4/G4 | Cortex-M4(F) (ARMv7E-M) | Compiles **soft-float only**; FPv4-SP idle until §4 |
+| STM32 F7/H7 | Cortex-M7 (ARMv7E-M) | Same — soft-float; no FPU/DP-FPU use |
+| STM32 F0/L0/G0/C0 | Cortex-M0/M0+ (ARMv6-M) | **Out of scope** (ARMv7-M-only insns) |
+| STM32 L5/U5/H5 | Cortex-M33 (ARMv8-M) | **Out of scope** (untested) |
+| ESP32-C2 / -C3 | RV32IMC | **Works** — soft-float by default now (§1); add `-march=rv32imc` to drop the `A` ext |
+| ESP32-C6 / -H2 | RV32IMAC | **Works** (`-march=rv32imac`) |
+| ESP32-P4 | RV32IMAFC | **Works**; single-float hard, double soft |
+| ESP32 / -S2 / -S3, ESP8266 | Xtensa | **Out of scope** (no backend) |
+
+## §1 — Footguns to fix first (cheap, high-impact)
+
+These are correctness/UX traps that bite before any feature work and are nearly
+free to fix.
+
+- [x] **RV32 default profile no longer assumes an FPU.** The rv32 default was
+ `rv32imafc` / `ilp32f` (hardware single-float on by default), so a forgotten
+ `-march`/`-mabi` silently emitted `FLW`/`FSW` that fault on a no-FPU part
+ (ESP32-C3 = rv32imc). Flipped to **`rv32imac` / `ilp32` soft-float**
+ (`rv64_target_feature_defaults` in `src/arch/riscv/arch.c` no longer sets
+ `F` for rv32). The FPU is opt-in via `-march=rv32imafc -mabi=ilp32f` (the
+ explicit flag fully overrides defaults and still emits `fadd.s` — verified).
+ Because the static predefined-macro table can encode only one float profile
+ (and `rt/lib/coro` keys on `__riscv_flen`), the float macros
+ (`__riscv_float_abi_*`, `__riscv_flen`, `__riscv_fdiv`, `__riscv_fsqrt`) are
+ now emitted per resolved `float_abi` by a new `ArchImpl.float_predefines`
+ hook (`kit_compiler_arch_float_predefines`), so the preprocessor always
+ agrees with the codegen ABI — this also fixed a latent bug where the soft
+ runtime's coro layer was built with `__riscv_flen=32` (FP save/restore in
+ soft code) and where rv64 `lp64` soft builds got `__riscv_float_abi_double`.
+ Tested: `test-smoke-rv32` (ilp32 + ilp32f lanes, -O0/-O1, qemu) 7/0,
+ `test-cg-api`, `test-abi-classify`, `test-smoke-rv64` all green.
+- [ ] **Document the canonical invocations per part.** A short table (this part →
+ `-target … -march=… -mabi=…`) in the user docs, so the C3/C6/P4 ISA-string
+ differences are spelled out rather than rediscovered.
+- [ ] **RV32 atomics without the `A` extension.** ESP32-C2/C3 are RV32I**M**C
+ (no `A`); C6/H2/P4 have `A`. Kit declares `__riscv_atomic` and provides
+ 4-byte lock-free / 8-byte spinlock fallbacks. On a no-`A` single-core part
+ the lock-free 4-byte path needs verification (it should degrade to the
+ runtime helper, not emit `lr.w`/`sc.w`). Confirm `-march=rv32imc` routes
+ atomics through the runtime, and that the spinlock fallback is interrupt-
+ safe enough for single-core use. (`src/cg/atomic.c`, `../RUNTIME.md:116`.)
+
+## §2 — Flashing: object → image formats
+
+The flash workflow needs an image format the flasher ingests. Today
+`objcopy -O binary` works (`driver/cmd/objcopy.c:226`, via
+`KIT_IMAGE_FORMAT_BIN`) and this implementation adds Intel HEX / S-record support.
+
+- [x] **`objcopy -O ihex`** (Intel HEX). Many ST-Link/third-party flashers and
+ bootloaders consume `.hex`. Bounded: a new encoder alongside the BIN path
+ in `src/obj/image.c` (records: data / extended-linear-address / EOF;
+ checksum per line) plus the `-O` token in `driver/cmd/objcopy.c:206`.
+- [x] **`objcopy -O srec`** (Motorola S-record). Same shape (S0/S1-3/S7-9),
+ parallel encoder. Lower priority than ihex but cheap once ihex exists.
+- [ ] **Espressif note (no work):** `esptool.py` consumes a plain ELF (or
+ `elf2image`) and produces the bootloader/partition/app image with its
+ header + checksum itself. Kit's freestanding `ET_EXEC` ELF is sufficient
+ for the C-series — no custom Espressif image format needed. Verify the
+ round-trip and document it; do not build an image packer.
+
+## §3 — Startup: crt0 / vector table
+
+The runtime ships **no `crt0`** by design (`../RUNTIME.md:240`), so today every
+firmware hand-writes the reset handler, vector table, `.data` copy-from-flash,
+`.bss` zero, and stack setup. Correct reference stubs already exist in the test
+harness (`test/smoke/arm32.sh:70`, `test/smoke/rv32.sh`,
+`test/link/harness/start.c`) — the work is promoting a *reusable, overridable*
+startup into the shippable runtime so users start from working code, not a blank
+page.
+
+- [ ] **Generic Cortex-M startup + minimal vector table** — reset handler that
+ sets MSP from `_estack`, copies `.data` (LMA→VMA), zeroes `.bss`, runs
+ `.init_array`, calls `main`; a weak default vector table with overridable
+ handler slots. Overridable so a vendor table wins when present.
+- [ ] **Generic RV32 startup** — set `sp`/`gp`, the TLS TCB + `tp` prologue
+ (`../RUNTIME.md:248`), `.data`/`.bss`, `.init_array`, `main`. Single-thread
+ TLS-as-static is fine for MCU.
+- [ ] **A starter linker script per regime** — a parameterized `MEMORY{FLASH,RAM}`
+ + `SECTIONS{}` template (with `_sdata`/`_edata`/`_sbss`/`_ebss`/`_estack`,
+ `.isr_vector` KEEP, `.data` `AT>FLASH`) that the §3 startup matches. Kit's
+ linker already supports every directive this needs (`src/link/link_script.c`).
+- [ ] Depends on (ARM): the **kit-compiled reset stub** in ARM32.md (currently
+ clang-assembled; blocked on a few `kit as` forms). When that lands, the
+ ARM startup above is fully kit-built end to end.
+
+## §4 — ARM hard-float (FPv4-SP / FPv5)
+
+Cortex-M4F/M7 (STM32 F4/F7/H7 — very common) have FPUs that current soft-float
+codegen leaves idle. This is an **ARM backend** item, owned by
+[ARM32.md](ARM32.md) ("Hard-float (FPv4-SP, `arm-none-eabihf`): VFP codegen +
+softfp/hard axis"). Listed here only for MCU prioritization: it is the largest
+single performance lever for the STM32 mid/high range. Needs the VFP register
+file, hard-float AAPCS variant, and `-mfloat-abi`/`-mfpu` plumbing.
+
+## §5 — SDK / toolchain interop
+
+To use kit *alongside* vendor SDKs (CMSIS, STM32 HAL, ESP-IDF) rather than fully
+standalone, kit objects must link cleanly with GNU-toolchain libraries.
+
+- [ ] **`.ARM.attributes`** — without the build-attributes section, GNU `ld`
+ rejects mixing kit `.o` with vendor `.a` on ABI grounds. Owned by ARM32.md
+ (`src/obj/elf/emit.c`); a hard gate for SDK interop on ARM.
+- [ ] **GNU numeric local labels (`1:`/`1f`/`1b`) in `kit as`** — needed to
+ assemble typical vendor startup `.s` and the AEABI helper layer. Owned by
+ ARM32.md.
+- [ ] **Vendor linker-script compatibility.** Kit's linker-script parser is a
+ GNU-ld *subset*; it rejects `GROUP`/`INPUT`/`INCLUDE`/`STARTUP`/`SEARCH_DIR`
+ (`src/link/link_script.c:1683`). Real STM32CubeIDE / ESP-IDF `.ld` scripts
+ use some of these. Run kit's linker against a representative set of vendor
+ scripts, then add the directives that actually appear (likely `INCLUDE`
+ and `GROUP` first). Track the concrete misses in LINKER.md.
+
+## §6 — On-target debug
+
+`kit dbg` is an **in-process JIT** debugger (`KitDebugSession`); it cannot speak
+the GDB remote serial protocol to an OpenOCD/J-Link/probe-rs server, so it can't
+debug on real silicon today.
+
+- [ ] **Pragmatic path (verify, near-free):** kit already produces ELF + DWARF
+ for Thumb-2/RV32 (`make test-dwarf`). Confirm stock `arm-none-eabi-gdb` /
+ `riscv32-elf-gdb` + OpenOCD load kit's DWARF and step kit-built firmware
+ correctly. If that works, on-target debug is *unblocked* via external gdb
+ and the rest is optional.
+- [ ] **Native path (larger, separate effort):** a `kit dbg target remote :3333`
+ GDB-remote *client* mode so kit's own debugger drives hardware. Defer until
+ the external-gdb path is confirmed working; this is a real subsystem, not a
+ quick add. Coordinate with [DEBUG.md](DEBUG.md).
+
+## §7 — Runtime polish (optional, demand-driven)
+
+- [ ] **Float `printf`** — the callback formatter is integer-only
+ (`rt/lib/stdio/printf.c`). `%f`/`%g` is a common embedded ask (and a common
+ *explicit refusal* for size). Make it an opt-in lane, not the default.
+- [ ] **Thin semihosting helper** — a small `kit/semihosting.h` wrapping
+ `SYS_WRITE0`/`SYS_EXIT` (`BKPT #0xAB` on ARM, `ebreak` sequence on RV32) so
+ users don't hand-roll the trap. The printf callback already plugs into
+ this. Useful for QEMU bring-up and early board debug.
+
+## Ordering
+
+Roughly by leverage-per-effort:
+
+1. §1 footguns (RV32 soft-float default, per-part doc) — hours, prevents silent
+ bad images.
+2. §2 `objcopy -O ihex` — small, unblocks STM32 flashing.
+3. §3 startup + starter linker script — removes the biggest "blank page" barrier.
+4. §6 verify external-gdb on-target debug — near-free if it works.
+5. §4 ARM hard-float (via ARM32.md) — biggest perf lever for STM32 F4/F7/H7.
+6. §5 SDK interop (`.ARM.attributes`, GNU labels, vendor `.ld`) — gates using
+ kit with existing vendor code.
+7. §7 polish, §6 native remote-debug — demand-driven.
diff --git a/driver/cmd/image.c b/driver/cmd/image.c
@@ -49,6 +49,10 @@ void driver_help_image(void) {
" --format rom fixed-size flat binary (requires "
"--pad-to)\n"
" --format sections concatenate --section bytes in order\n"
+ " --format ihex Intel HEX records from loadable "
+ "segments\n"
+ " --format srec Motorola S-records from loadable "
+ "segments\n"
" --from segments derive bytes from loadable segments "
"(default)\n"
" --from sections derive bytes from named sections\n"
@@ -214,6 +218,14 @@ static int parse_format(const char* s, uint32_t* out) {
*out = KIT_IMAGE_FORMAT_SECTIONS;
return 0;
}
+ if (driver_streq(s, "ihex") || driver_streq(s, "ihex8")) {
+ *out = KIT_IMAGE_FORMAT_IHEX;
+ return 0;
+ }
+ if (driver_streq(s, "srec") || driver_streq(s, "s19")) {
+ *out = KIT_IMAGE_FORMAT_SREC;
+ return 0;
+ }
if (driver_streq(s, "elf")) {
*out = KIT_IMAGE_FORMAT_ELF;
return 0;
@@ -455,6 +467,24 @@ static void meta_put_selection(MetaBuf* mb, KitObjFile* of,
}
}
+static const char* image_format_name(uint32_t format) {
+ switch ((KitImageFormat)format) {
+ case KIT_IMAGE_FORMAT_ROM:
+ return "rom";
+ case KIT_IMAGE_FORMAT_SECTIONS:
+ return "sections";
+ case KIT_IMAGE_FORMAT_ELF:
+ return "elf";
+ case KIT_IMAGE_FORMAT_IHEX:
+ return "ihex";
+ case KIT_IMAGE_FORMAT_SREC:
+ return "srec";
+ case KIT_IMAGE_FORMAT_BIN:
+ default:
+ return "bin";
+ }
+}
+
static int write_metadata(DriverEnv* env, const KitContext* ctx, KitObjFile* of,
const ImageCliOpts* o, const KitImageReport* report,
const char* path) {
@@ -487,10 +517,7 @@ static int write_metadata(DriverEnv* env, const KitContext* ctx, KitObjFile* of,
/* Keys are emitted in a fixed order so the sidecar is byte-stable. */
meta_putf(&mb, "{\n");
meta_putf(&mb, " \"format\": \"%s\",\n",
- o->image.format == KIT_IMAGE_FORMAT_ROM ? "rom"
- : o->image.format == KIT_IMAGE_FORMAT_SECTIONS ? "sections"
- : o->image.format == KIT_IMAGE_FORMAT_ELF ? "elf"
- : "bin");
+ image_format_name(o->image.format));
meta_putf(&mb, " \"source\": \"%s\",\n",
from_sections ? "sections" : "segments");
meta_putf(&mb, " \"target\": \"%s\",\n", triple);
diff --git a/driver/cmd/objcopy.c b/driver/cmd/objcopy.c
@@ -64,7 +64,7 @@ void driver_help_objcopy(void) {
" -O BFDNAME emit as a different format. "
"Recognized\n"
" names: elf*, mach-o / macho*, coff*, "
- "wasm*, binary\n"
+ "wasm*, binary, ihex, srec\n"
"\n"
"EXIT CODES\n"
" 0 success 1 I/O or strip error 2 bad "
@@ -117,7 +117,8 @@ typedef struct CopyOpts {
/* Format conversion */
int have_output_fmt;
KitObjFmt output_fmt;
- int output_binary;
+ int output_image;
+ KitImageFormat output_image_format;
/* I/O */
const char* input;
const char* output;
@@ -228,6 +229,14 @@ static int is_binary_fmt_name(const char* name) {
driver_streq(name, "raw");
}
+static int is_ihex_fmt_name(const char* name) {
+ return driver_streq(name, "ihex") || driver_streq(name, "ihex8");
+}
+
+static int is_srec_fmt_name(const char* name) {
+ return driver_streq(name, "srec") || driver_streq(name, "s19");
+}
+
static int has_object_transforms(const CopyOpts* opts) {
return opts->op != COPY_OP_NONE || opts->nremove || opts->nonly ||
opts->nrename_sec || opts->nadd || opts->nupdate || opts->nredef ||
@@ -452,14 +461,15 @@ static int copy_one_object(DriverEnv* env, const KitContext* ctx,
return rc;
}
-static int copy_one_binary(const KitContext* ctx, const char* input_name,
- const KitSlice* input, const char* output_path) {
+static int copy_one_image(const KitContext* ctx, const CopyOpts* opts,
+ const char* input_name, const KitSlice* input,
+ const char* output_path) {
KitImageOptions iopts;
KitWriter* w = NULL;
KitStatus st;
memset(&iopts, 0, sizeof iopts);
- iopts.format = KIT_IMAGE_FORMAT_BIN;
+ iopts.format = opts->output_image_format;
iopts.from = KIT_IMAGE_FROM_SEGMENTS;
iopts.addr = KIT_IMAGE_ADDR_VADDR;
iopts.fill = 0;
@@ -526,7 +536,20 @@ int driver_objcopy(int argc, char** argv) {
}
++i;
if (is_binary_fmt_name(argv[i])) {
- opts.output_binary = 1;
+ opts.output_image = 1;
+ opts.output_image_format = KIT_IMAGE_FORMAT_BIN;
+ opts.have_output_fmt = 0;
+ continue;
+ }
+ if (is_ihex_fmt_name(argv[i])) {
+ opts.output_image = 1;
+ opts.output_image_format = KIT_IMAGE_FORMAT_IHEX;
+ opts.have_output_fmt = 0;
+ continue;
+ }
+ if (is_srec_fmt_name(argv[i])) {
+ opts.output_image = 1;
+ opts.output_image_format = KIT_IMAGE_FORMAT_SREC;
opts.have_output_fmt = 0;
continue;
}
@@ -537,7 +560,7 @@ int driver_objcopy(int argc, char** argv) {
goto done;
}
opts.have_output_fmt = 1;
- opts.output_binary = 0;
+ opts.output_image = 0;
continue;
}
matched = take_value(&i, argc, argv, "--remove-section", &val);
@@ -664,9 +687,10 @@ int driver_objcopy(int argc, char** argv) {
}
out_path = opts.output ? opts.output : opts.input;
- if (opts.output_binary && has_object_transforms(&opts)) {
+ if (opts.output_image && has_object_transforms(&opts)) {
driver_errf(OBJCOPY_TOOL,
- "-O binary cannot be combined with object transform options");
+ "image output format cannot be combined with object transform "
+ "options");
rc = 2;
goto done;
}
@@ -680,8 +704,8 @@ int driver_objcopy(int argc, char** argv) {
input.data = in_fd.data;
input.len = in_fd.size;
- if (opts.output_binary) {
- rc = copy_one_binary(&ctx, opts.input, &input, out_path);
+ if (opts.output_image) {
+ rc = copy_one_image(&ctx, &opts, opts.input, &input, out_path);
} else {
rc = copy_one_object(&env, &ctx, opts.input, &input, &opts, out_path);
}
diff --git a/include/kit/image.h b/include/kit/image.h
@@ -16,6 +16,8 @@ typedef enum KitImageFormat {
KIT_IMAGE_FORMAT_ROM,
KIT_IMAGE_FORMAT_SECTIONS,
KIT_IMAGE_FORMAT_ELF,
+ KIT_IMAGE_FORMAT_IHEX,
+ KIT_IMAGE_FORMAT_SREC,
} KitImageFormat;
typedef enum KitImageSource {
diff --git a/src/obj/image.c b/src/obj/image.c
@@ -92,6 +92,34 @@ static int name_in_list(KitSlice name, const KitSlice* list, uint32_t n) {
return 0;
}
+#define KIT_IHEX_DATA_MAX 16u
+#define KIT_SREC_DATA_MAX 16u
+
+static const char kHex[] = "0123456789ABCDEF";
+
+static void line_append_hex_u8(char* line, size_t* n, uint8_t v) {
+ line[(*n)++] = kHex[(v >> 4) & 0xfu];
+ line[(*n)++] = kHex[v & 0xfu];
+}
+
+static void line_append_hex_u16(char* line, size_t* n, uint16_t v) {
+ line_append_hex_u8(line, n, (uint8_t)(v >> 8));
+ line_append_hex_u8(line, n, (uint8_t)v);
+}
+
+static void line_append_hex_u64_be(char* line, size_t* n, uint64_t v,
+ uint32_t nbytes) {
+ uint32_t i;
+ uint32_t shift = (nbytes - 1u) * 8u;
+ for (i = 0; i < nbytes; ++i, shift -= 8u) {
+ line_append_hex_u8(line, n, (uint8_t)(v >> shift));
+ }
+}
+
+static KitStatus write_line(KitWriter* out, const char* data, size_t n) {
+ return (kit_writer_write(out, data, n) == KIT_OK) ? KIT_OK : KIT_IO;
+}
+
static KitSlice drop_pt_prefix(KitSlice s) {
if (s.len > 3 && ascii_lower((unsigned char)s.s[0]) == 'p' &&
ascii_lower((unsigned char)s.s[1]) == 't' && s.s[2] == '_') {
@@ -629,6 +657,300 @@ static KitStatus write_layout(const KitContext* ctx, const ImageRange* ranges,
return kit_writer_status(out);
}
+static KitStatus check_32bit_text_image_range(const KitContext* ctx,
+ const KitImageReport* report,
+ const char* name) {
+ if (report->base > UINT32_MAX ||
+ report->size > ((uint64_t)UINT32_MAX + 1u) - report->base) {
+ kit_ctx_diagf(ctx, "image: %s cannot emit above 32-bit addresses", name);
+ return KIT_UNSUPPORTED;
+ }
+ return KIT_OK;
+}
+
+static KitStatus emit_ihex_ela_record(KitWriter* out, uint16_t upper) {
+ uint8_t payload[2];
+ char line[32];
+ uint32_t sum;
+ size_t n = 0;
+
+ payload[0] = (uint8_t)(upper >> 8);
+ payload[1] = (uint8_t)(upper & 0xffu);
+ sum = 2u + 0u + 0u + 4u + payload[0] + payload[1];
+
+ line[n++] = ':';
+ line_append_hex_u8(line, &n, 2u);
+ line_append_hex_u16(line, &n, 0u);
+ line_append_hex_u8(line, &n, 4u);
+ line_append_hex_u8(line, &n, payload[0]);
+ line_append_hex_u8(line, &n, payload[1]);
+ line_append_hex_u8(line, &n, (uint8_t)(~sum + 1u));
+ line[n++] = '\n';
+
+ return write_line(out, line, n);
+}
+
+static KitStatus emit_ihex_data_record(KitWriter* out, uint16_t addr,
+ const uint8_t* data, size_t len) {
+ uint32_t sum;
+ size_t i;
+ char line[64];
+ size_t n = 0;
+
+ if (len > KIT_IHEX_DATA_MAX || !len) return KIT_INVALID;
+
+ sum = (uint32_t)len;
+ sum += (uint32_t)(addr >> 8);
+ sum += (uint32_t)(addr & 0xffu);
+ sum += 0u;
+ line[n++] = ':';
+ line_append_hex_u8(line, &n, (uint8_t)len);
+ line_append_hex_u16(line, &n, addr);
+ line_append_hex_u8(line, &n, 0u);
+ for (i = 0; i < len; ++i) {
+ line_append_hex_u8(line, &n, data[i]);
+ sum += data[i];
+ }
+ line_append_hex_u8(line, &n, (uint8_t)(~sum + 1u));
+ line[n++] = '\n';
+ return write_line(out, line, n);
+}
+
+static KitStatus emit_ihex_eof_record(KitWriter* out) {
+ char line[16];
+ size_t n = 0;
+
+ line[n++] = ':';
+ line_append_hex_u8(line, &n, 0u);
+ line_append_hex_u16(line, &n, 0u);
+ line_append_hex_u8(line, &n, 1u);
+ line_append_hex_u8(line, &n, 0xffu);
+ line[n++] = '\n';
+ return write_line(out, line, n);
+}
+
+static KitStatus emit_ihex_blob(const KitContext* ctx, KitWriter* out,
+ uint64_t addr, const uint8_t* src,
+ uint64_t len, uint8_t fill, int as_fill,
+ uint16_t* current_upper) {
+ uint8_t chunk[KIT_IHEX_DATA_MAX];
+ uint16_t upper;
+ uint32_t chunk_len;
+ uint64_t at;
+ KitStatus st;
+
+ while (len) {
+ if (addr > UINT32_MAX) {
+ kit_ctx_diagf(ctx, "image: ihex cannot emit above 32-bit addresses");
+ return KIT_UNSUPPORTED;
+ }
+ upper = (uint16_t)(addr >> 16);
+ if (*current_upper != upper) {
+ st = emit_ihex_ela_record(out, upper);
+ if (st != KIT_OK) return st;
+ *current_upper = upper;
+ }
+ at = addr & 0xffffu;
+ chunk_len = (uint32_t)(0x10000ull - at);
+ if (chunk_len > len) chunk_len = (uint32_t)len;
+ if (chunk_len > KIT_IHEX_DATA_MAX) chunk_len = KIT_IHEX_DATA_MAX;
+ if (as_fill) {
+ memset(chunk, fill, (size_t)chunk_len);
+ } else {
+ memcpy(chunk, src, (size_t)chunk_len);
+ src += chunk_len;
+ }
+ st = emit_ihex_data_record(out, (uint16_t)addr, chunk, (size_t)chunk_len);
+ if (st != KIT_OK) return st;
+ addr += chunk_len;
+ len -= chunk_len;
+ }
+ return KIT_OK;
+}
+
+static KitStatus write_ihex_records(const KitContext* ctx, const ImageRange* ranges,
+ uint32_t nranges, const KitImageOptions* opts,
+ const KitImageReport* report,
+ KitWriter* out) {
+ uint64_t cur = report->base;
+ uint64_t pad = 0;
+ uint16_t current_upper = 0xffffu;
+ uint32_t i;
+ KitStatus st;
+
+ for (i = 0; i < nranges; ++i) {
+ const ImageRange* r = &ranges[i];
+ uint64_t hole = r->addr - cur;
+ if (hole) {
+ st = emit_ihex_blob(ctx, out, cur, NULL, hole, opts->fill, 1,
+ ¤t_upper);
+ if (st != KIT_OK) return st;
+ }
+ st = emit_ihex_blob(ctx, out, r->addr, r->data, r->size, 0, 0,
+ ¤t_upper);
+ if (st != KIT_OK) return st;
+ cur = r->end;
+ }
+ if (report->size > cur - report->base) {
+ pad = report->size - (cur - report->base);
+ st = emit_ihex_blob(ctx, out, cur, NULL, pad, opts->fill, 1,
+ ¤t_upper);
+ if (st != KIT_OK) return st;
+ }
+ return emit_ihex_eof_record(out);
+}
+
+static KitStatus srec_data_type(const KitContext* ctx, uint64_t addr, char* type,
+ uint32_t* addr_bytes) {
+ if (addr <= 0xffffu) {
+ *type = '1';
+ *addr_bytes = 2u;
+ return KIT_OK;
+ }
+ if (addr <= 0xffffffu) {
+ *type = '2';
+ *addr_bytes = 3u;
+ return KIT_OK;
+ }
+ if (addr <= 0xffffffffu) {
+ *type = '3';
+ *addr_bytes = 4u;
+ return KIT_OK;
+ }
+ kit_ctx_diagf(ctx,
+ "image: srec cannot emit above 32-bit addresses (got 0x%llx)",
+ (unsigned long long)addr);
+ return KIT_UNSUPPORTED;
+}
+
+static KitStatus emit_srec_data_record(KitWriter* out, char type, uint32_t addr_bytes,
+ uint64_t addr, const uint8_t* data,
+ size_t len) {
+ uint8_t sum;
+ size_t i;
+ char line[128];
+ size_t n = 0;
+ uint8_t count;
+
+ if (addr_bytes < 2u || addr_bytes > 4u) return KIT_INVALID;
+ if (len > KIT_SREC_DATA_MAX) return KIT_INVALID;
+ if (!data && len != 0u) return KIT_INVALID;
+
+ count = (uint8_t)(addr_bytes + len + 1u);
+ sum = count;
+
+ line[n++] = 'S';
+ line[n++] = (char)type;
+ line_append_hex_u8(line, &n, count);
+ line_append_hex_u64_be(line, &n, addr, addr_bytes);
+ for (i = 0; i < addr_bytes; ++i) {
+ sum += (uint8_t)(addr >> (8u * (addr_bytes - 1u - i)));
+ }
+ if (data) {
+ for (i = 0; i < len; ++i) {
+ line_append_hex_u8(line, &n, data[i]);
+ sum += data[i];
+ }
+ }
+ line_append_hex_u8(line, &n, (uint8_t)(~sum));
+ line[n++] = '\n';
+ return write_line(out, line, n);
+}
+
+static KitStatus emit_srec_blob(const KitContext* ctx, KitWriter* out, uint64_t addr,
+ const uint8_t* src, uint64_t len, uint8_t fill,
+ int as_fill) {
+ uint8_t chunk[KIT_SREC_DATA_MAX];
+ size_t chunk_len_u;
+ uint64_t chunk_len;
+ uint64_t block_size;
+ uint64_t at;
+ char type;
+ uint32_t addr_bytes;
+ KitStatus st;
+
+ while (len) {
+ st = srec_data_type(ctx, addr, &type, &addr_bytes);
+ if (st != KIT_OK) return st;
+ at = addr & (((uint64_t)1u << (addr_bytes * 8u)) - 1u);
+ block_size = (uint64_t)1u << (addr_bytes * 8u);
+ chunk_len = block_size - at;
+ if (chunk_len > len) chunk_len = len;
+ if (chunk_len > KIT_SREC_DATA_MAX) chunk_len = KIT_SREC_DATA_MAX;
+ chunk_len_u = (size_t)chunk_len;
+ if (as_fill) {
+ memset(chunk, fill, chunk_len_u);
+ } else {
+ memcpy(chunk, src, chunk_len_u);
+ src += chunk_len_u;
+ }
+ st = emit_srec_data_record(out, type, addr_bytes, addr, chunk, chunk_len_u);
+ if (st != KIT_OK) return st;
+ addr += chunk_len;
+ len -= chunk_len;
+ }
+ return KIT_OK;
+}
+
+static char srec_start_type(uint64_t addr) {
+ if (addr <= 0xffffu) return '9';
+ if (addr <= 0xffffffu) return '8';
+ return '7';
+}
+
+static uint32_t srec_start_addr_bytes(uint64_t addr) {
+ if (addr <= 0xffffu) return 2u;
+ if (addr <= 0xffffffu) return 3u;
+ return 4u;
+}
+
+static KitStatus write_srec_records(const KitContext* ctx, const ImageRange* ranges,
+ uint32_t nranges, const KitImageOptions* opts,
+ const KitImageReport* report, uint64_t entry,
+ KitWriter* out) {
+ const uint8_t header_name[] = "KIT";
+ uint64_t cur;
+ uint64_t pad = 0;
+ uint32_t i;
+ KitStatus st;
+
+ if (emit_srec_data_record(out, '0', 2u, 0u, header_name,
+ sizeof(header_name) - 1u) != KIT_OK) {
+ return KIT_IO;
+ }
+
+ if (nranges == 0) return KIT_NOT_FOUND;
+ cur = report->base;
+ for (i = 0; i < nranges; ++i) {
+ const ImageRange* r = &ranges[i];
+ uint64_t hole = r->addr - cur;
+ if (hole) {
+ st = emit_srec_blob(ctx, out, cur, NULL, hole, opts->fill, 1);
+ if (st != KIT_OK) return st;
+ }
+ st = emit_srec_blob(ctx, out, r->addr, r->data, r->size, 0u, 0);
+ if (st != KIT_OK) return st;
+ cur = r->end;
+ }
+ if (report->size > cur - report->base) {
+ pad = report->size - (cur - report->base);
+ st = emit_srec_blob(ctx, out, cur, NULL, pad, opts->fill, 1);
+ if (st != KIT_OK) return st;
+ }
+ if (entry > UINT32_MAX) {
+ kit_ctx_diagf(ctx,
+ "image: srec cannot emit above 32-bit addresses (got 0x%llx)",
+ (unsigned long long)entry);
+ return KIT_UNSUPPORTED;
+ }
+ if (emit_srec_data_record(out, srec_start_type(entry),
+ srec_start_addr_bytes(entry), entry, NULL, 0u) !=
+ KIT_OK) {
+ return KIT_IO;
+ }
+ return KIT_OK;
+}
+
/* Validation pass: --require-entry / --require-symbol / --require-section /
* --no-dynamic. Runs against the opened object before any bytes are written. */
static KitStatus validate_object(const KitContext* ctx, KitObjFile* obj,
@@ -731,6 +1053,7 @@ KitStatus obj_emit_image(const KitContext* ctx, KitObjFile* obj,
KitStatus st;
int from_sections;
uint64_t seg_mem_end = 0;
+ uint64_t srec_entry = 0;
uint8_t image_hdr[64];
const uint8_t* image_hdr_p = NULL;
@@ -748,7 +1071,8 @@ KitStatus obj_emit_image(const KitContext* ctx, KitObjFile* obj,
format = (KitImageFormat)opts->format;
if (format != KIT_IMAGE_FORMAT_BIN && format != KIT_IMAGE_FORMAT_ROM &&
- format != KIT_IMAGE_FORMAT_SECTIONS && format != KIT_IMAGE_FORMAT_ELF) {
+ format != KIT_IMAGE_FORMAT_SECTIONS && format != KIT_IMAGE_FORMAT_IHEX &&
+ format != KIT_IMAGE_FORMAT_SREC && format != KIT_IMAGE_FORMAT_ELF) {
kit_ctx_diagf(ctx, "image: unknown output format");
return KIT_INVALID;
}
@@ -777,20 +1101,23 @@ KitStatus obj_emit_image(const KitContext* ctx, KitObjFile* obj,
return KIT_INVALID;
}
- /* Source selection. Three formats, two byte sources:
- * bin : load segments only.
- * sections : named sections only (the format IS the concatenation).
- * rom : load segments by default, or named sections when a --section
- * list (or --from sections) is supplied.
+ /* Source selection. Five formats, two byte sources:
+ * bin/srec/ihex : load segments only.
+ * rom : load segments by default, or named sections when a
+ * --section list (or --from sections) is supplied.
+ * sections : named sections only (the format IS the concatenation).
* --section is the section-order list; it implies the section source for the
- * formats that allow it and is rejected for bin. */
- if (format == KIT_IMAGE_FORMAT_BIN && opts->nsection_order) {
+ * formats that allow it and is rejected for bin / ihex / srec. */
+ if ((format == KIT_IMAGE_FORMAT_BIN || format == KIT_IMAGE_FORMAT_IHEX ||
+ format == KIT_IMAGE_FORMAT_SREC) &&
+ opts->nsection_order) {
kit_ctx_diagf(ctx,
"image: --section is only valid with --format sections or "
"--format rom");
return KIT_INVALID;
}
- if (format == KIT_IMAGE_FORMAT_BIN &&
+ if ((format == KIT_IMAGE_FORMAT_BIN || format == KIT_IMAGE_FORMAT_IHEX ||
+ format == KIT_IMAGE_FORMAT_SREC) &&
opts->from == KIT_IMAGE_FROM_SECTIONS) {
kit_ctx_diagf(ctx,
"image: --from sections requires --format sections or rom");
@@ -847,7 +1174,25 @@ KitStatus obj_emit_image(const KitContext* ctx, KitObjFile* obj,
}
}
if (st == KIT_OK) {
- st = write_layout(ctx, ranges, nranges, opts, &report, image_hdr_p, out);
+ if (format == KIT_IMAGE_FORMAT_IHEX) {
+ st = check_32bit_text_image_range(ctx, &report, "ihex");
+ if (st == KIT_OK)
+ st = write_ihex_records(ctx, ranges, nranges, opts, &report, out);
+ } else if (format == KIT_IMAGE_FORMAT_SREC) {
+ KitObjImageInfo info;
+ if (kit_obj_image_info(obj, &info) == KIT_OK) srec_entry = info.entry;
+ st = check_32bit_text_image_range(ctx, &report, "srec");
+ if (st == KIT_OK && srec_entry > UINT32_MAX) {
+ kit_ctx_diagf(ctx,
+ "image: srec cannot emit above 32-bit addresses");
+ st = KIT_UNSUPPORTED;
+ }
+ if (st == KIT_OK)
+ st = write_srec_records(ctx, ranges, nranges, opts, &report, srec_entry,
+ out);
+ } else {
+ 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
@@ -189,6 +189,18 @@ same_file image-paddr-matches-vaddr "$work/kernel.bin" "$work/kernel.paddr.bin"
run_ok objcopy-binary "$KIT" objcopy -O binary "$work/kernel.elf" \
"$work/kernel.objcopy.bin"
same_file image-objcopy-binary "$work/kernel.bin" "$work/kernel.objcopy.bin"
+run_ok image-ihex "$KIT" image --format ihex "$work/kernel.elf" \
+ -o "$work/kernel.hex"
+run_ok objcopy-ihex "$KIT" objcopy -O ihex "$work/kernel.elf" \
+ "$work/kernel.objcopy.hex"
+same_file image-objcopy-ihex "$work/kernel.hex" "$work/kernel.objcopy.hex"
+contains image-ihex-eof "$work/kernel.hex" ":00000001FF"
+run_ok image-srec "$KIT" image --format srec "$work/kernel.elf" \
+ -o "$work/kernel.srec"
+run_ok objcopy-srec "$KIT" objcopy -O srec "$work/kernel.elf" \
+ "$work/kernel.objcopy.srec"
+same_file image-objcopy-srec "$work/kernel.srec" "$work/kernel.objcopy.srec"
+contains image-srec-header "$work/kernel.srec" "S00600004B495411"
if [ -s "$work/kernel.bin" ]; then
ok image-bin-nonempty
else
@@ -213,6 +225,58 @@ contains image-pad-fill-byte "$work/kernel.pad.hex" \
run_fail image-max-size "$KIT" image --format bin --max-size 1 \
"$work/kernel.elf" -o "$work/kernel.too-small.bin"
+# Text image formats must carry full 32-bit addresses. This fixture is placed
+# above 16 MiB so Intel HEX needs a non-trivial extended-linear-address record;
+# the checksum pins the full 16-bit high address (0x0102), not just its low byte.
+cat > "$work/highaddr.s" <<'EOF'
+.globl _start
+.section .text
+_start:
+ ret
+ .byte 0x11, 0x22, 0x33, 0x44
+EOF
+cat > "$work/highaddr.lds" <<'EOF'
+ENTRY(_start)
+SECTIONS {
+ . = 0x010203f0;
+ .text : ALIGN(1) { *(.text .text.*) }
+ /DISCARD/ : { *(.note.*) *(.comment) *(.eh_frame) }
+}
+EOF
+run_ok image-text-high-build "$KIT" build-exe -target x86_64-none-elf \
+ -nostdlib -static -no-pie -e _start -T "$work/highaddr.lds" \
+ "$work/highaddr.s" -o "$work/highaddr.elf"
+run_ok image-ihex-high "$KIT" image --format ihex "$work/highaddr.elf" \
+ -o "$work/highaddr.hex"
+contains image-ihex-high-ela "$work/highaddr.hex" ":020000040102F7"
+run_ok objcopy-ihex-high "$KIT" objcopy -O ihex "$work/highaddr.elf" \
+ "$work/highaddr.objcopy.hex"
+same_file image-objcopy-ihex-high "$work/highaddr.hex" \
+ "$work/highaddr.objcopy.hex"
+run_ok image-srec-high "$KIT" image --format srec "$work/highaddr.elf" \
+ -o "$work/highaddr.srec"
+contains image-srec-high-address "$work/highaddr.srec" "010203F0"
+run_ok objcopy-srec-high "$KIT" objcopy -O srec "$work/highaddr.elf" \
+ "$work/highaddr.objcopy.srec"
+same_file image-objcopy-srec-high "$work/highaddr.srec" \
+ "$work/highaddr.objcopy.srec"
+
+cat > "$work/over32.lds" <<'EOF'
+ENTRY(_start)
+SECTIONS {
+ . = 0x100010000;
+ .text : ALIGN(1) { *(.text .text.*) }
+ /DISCARD/ : { *(.note.*) *(.comment) *(.eh_frame) }
+}
+EOF
+run_ok image-text-over32-build "$KIT" build-exe -target x86_64-none-elf \
+ -nostdlib -static -no-pie -e _start -T "$work/over32.lds" \
+ "$work/highaddr.s" -o "$work/over32.elf"
+run_fail image-ihex-over32 "$KIT" image --format ihex "$work/over32.elf" \
+ -o "$work/over32.hex"
+run_fail image-srec-over32 "$KIT" image --format srec "$work/over32.elf" \
+ -o "$work/over32.srec"
+
# ---- image: --format sections ---------------------------------------------
# Concatenate named sections in DECLARED order. The fixture's .text is a lone
# `ret` (0xc3) and .rodata is 0x11223344, so the concatenation and its reverse