kit

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

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:
Adoc/plan/MCU.md | 194+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdriver/cmd/image.c | 35+++++++++++++++++++++++++++++++----
Mdriver/cmd/objcopy.c | 46+++++++++++++++++++++++++++++++++++-----------
Minclude/kit/image.h | 2++
Msrc/obj/image.c | 365++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mtest/tools/run.sh | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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, + &current_upper); + if (st != KIT_OK) return st; + } + st = emit_ihex_blob(ctx, out, r->addr, r->data, r->size, 0, 0, + &current_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, + &current_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