kit

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

commit 6b470e4919a9d24103069c8beeb741ac0bf9dc9d
parent 3f7a01dcdb627b6713ad17682130a09824bdce21
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Tue, 16 Jun 2026 14:19:26 -0700

link: honor (NOLOAD) output sections for PROGBITS content

A (NOLOAD) output section must contribute zero file bytes and present a
NOBITS-style segment regardless of the input sections' PROGBITS/NOBITS
semantics. Mark every placed unit NOBITS when the output section is
(NOLOAD), keep file_size_accum at 0, and teach link_emit_segment_bytes
to honor the output-section decision (ls->sem) so the byte copy skips
NOLOAD sections (whose segment has no file buffer).

Adds check_noload_progbits (red->green).

Diffstat:
Msrc/link/link_layout.c | 17++++++++++++++---
Mtest/link/link_script_test.c | 292+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 306 insertions(+), 3 deletions(-)

diff --git a/src/link/link_layout.c b/src/link/link_layout.c @@ -1289,14 +1289,20 @@ static void link_layout_sections_scripted(Linker* l, LinkImage* img, ls->flags = s->flags; ls->align = align; ls->name = s->name; - ls->sem = (s->kind == SEC_BSS) ? SSEM_NOBITS : s->sem; + /* A (NOLOAD) output section produces no file bytes regardless of + * the input section's PROGBITS/NOBITS semantics: mark every + * placed unit NOBITS so the byte-copy (link_emit_segment_bytes) + * and the segment shape below both treat it as bss. */ + ls->sem = (os->noload || s->kind == SEC_BSS) ? SSEM_NOBITS + : s->sem; ls->file_offset = ofs - sec_start_dot; ls->input_offset = ls->file_offset; map_placed_unit(m, j, aid, lsid); dot += ls->size; mem_size_accum = dot - sec_start_dot; - if (ls->sem != SSEM_NOBITS) file_size_accum = dot - sec_start_dot; + if (!os->noload && ls->sem != SSEM_NOBITS) + file_size_accum = dot - sec_start_dot; perms |= (s->flags & (SF_EXEC | SF_WRITE | SF_TLS)); ++nsec_in_seg; } @@ -1580,7 +1586,12 @@ void link_emit_segment_bytes(Linker* l, LinkImage* img) { const Section* s = obj_section_get(ob, ls->obj_section_id); LinkSegment* seg = &img->segments[ls->segment_id - 1]; u8* dst; - if (!s || s->sem == SSEM_NOBITS || s->kind == SEC_BSS) continue; + /* ls->sem carries the OUTPUT-section decision (e.g. (NOLOAD) forces + * NOBITS even for PROGBITS input), so check it too — the segment it + * lives in has no file bytes to copy into. */ + if (!s || s->sem == SSEM_NOBITS || s->kind == SEC_BSS || + ls->sem == SSEM_NOBITS) + continue; if (ls->size == 0) continue; dst = img->segment_bytes[seg->id - 1] + (size_t)(ls->file_offset - seg->file_offset); diff --git a/test/link/link_script_test.c b/test/link/link_script_test.c @@ -211,6 +211,72 @@ fail: return NULL; } +/* Parse `script_text`, link `build_layout_input()` against it, and open the + * emitted image. On success *out_f / *out_w / *out_c / *out_s / *out_ob own + * resources the caller must release (use emit_cleanup). Returns 1 on success, + * 0 if any stage failed (an EXPECT already fired). */ +typedef struct EmitFixture { + KitCompiler* c; + KitObjBuilder* ob; + KitLinkScript* s; + KitLinkSession* sess; + KitWriter* w; + KitObjFile* f; +} EmitFixture; + +static void emit_cleanup(EmitFixture* fx) { + if (fx->f) kit_obj_free(fx->f); + if (fx->w) kit_writer_close(fx->w); + if (fx->sess) kit_link_session_free(fx->sess); + if (fx->ob) kit_obj_builder_free(fx->ob); + if (fx->s) kit_link_script_free(&g_u.ctx, fx->s); + if (fx->c) kit_compiler_free(fx->c); + memset(fx, 0, sizeof(*fx)); +} + +static int emit_layout(const char* script_text, size_t script_len, + EmitFixture* fx) { + KitTargetSpec target = + kit_unit_target(KIT_ARCH_ARM_64, KIT_OS_LINUX, KIT_OBJ_ELF); + KitLinkSessionOptions opts; + KitSlice script; + KitSlice bytes; + size_t len = 0; + + memset(fx, 0, sizeof(*fx)); + script.s = script_text; + script.len = script_len; + EXPECT(kit_link_script_parse(&g_u.ctx, script, &fx->s) == KIT_OK && fx->s, + "layout script parses"); + if (!fx->s) return 0; + EXPECT(kit_unit_compiler_new(&g_u, target, &fx->c) == KIT_OK && fx->c, + "compiler allocation"); + if (!fx->c) return 0; + fx->ob = build_layout_input(fx->c); + if (!fx->ob) return 0; + memset(&opts, 0, sizeof(opts)); + opts.output_kind = KIT_LINK_OUTPUT_EXE; + opts.linker_script = fx->s; + EXPECT(kit_link_session_new(fx->c, &opts, &fx->sess) == KIT_OK && fx->sess, + "link session allocation"); + if (!fx->sess) return 0; + EXPECT(kit_link_session_add_obj(fx->sess, fx->ob) == KIT_OK, + "add input object"); + EXPECT(kit_writer_mem(&g_u.heap, &fx->w) == KIT_OK && fx->w, "memory writer"); + if (!fx->w) return 0; + EXPECT(kit_link_session_emit(fx->sess, fx->w) == KIT_OK, + "emit scripted image"); + bytes.data = kit_writer_mem_bytes(fx->w, &len); + bytes.len = len; + EXPECT(bytes.data && bytes.len > 0, "scripted image bytes"); + if (!bytes.data || bytes.len == 0) return 0; + EXPECT(kit_obj_open(&g_u.ctx, KIT_SLICE_LIT("<scripted-layout>"), &bytes, + &fx->f) == KIT_OK && + fx->f, + "open scripted image"); + return fx->f != NULL; +} + static void check_scripted_layout_vma_lma(void) { static const char script_text[] = "OUTPUT_FORMAT(\"elf64-littleaarch64\")\n" @@ -314,10 +380,236 @@ done: if (c) kit_compiler_free(c); } +/* Item 1: a PROGBITS-content output section marked (NOLOAD) must contribute + * zero file bytes and present a NOBITS-style segment (file_size 0, vsize > 0). */ +static void check_noload_progbits(void) { + static const char script_text[] = + "OUTPUT_FORMAT(\"elf64-littleaarch64\")\n" + "OUTPUT_ARCH(aarch64)\n" + "ENTRY(_start)\n" + "SECTIONS {\n" + " . = 0x10000;\n" + " .text : { *(.text .text.*) }\n" + " .shadow (NOLOAD) : { *(.data .data.*) }\n" + "}\n"; + EmitFixture fx; + KitObjSegIter* it = NULL; + int saw_shadow = 0; + uint64_t text_end = 0; + + if (!emit_layout(script_text, sizeof(script_text) - 1u, &fx)) goto done; + EXPECT(kit_obj_segiter_new(fx.f, &it) == KIT_OK && it, "segment iterator"); + if (!it) goto done; + for (;;) { + KitObjSegInfo seg; + KitIterResult r = kit_obj_segiter_next(it, &seg); + if (r == KIT_ITER_END) break; + if (r != KIT_ITER_ITEM) break; + if (!kit_slice_eq_cstr(seg.name, "LOAD")) continue; + if (seg.file_size == 4u && seg.perms == (KIT_SEG_R | KIT_SEG_X)) + text_end = seg.vaddr + seg.vsize; + /* the shadow segment carries the .data bytes (4) as pure bss */ + if (seg.vsize == 4u && seg.file_size == 0u && + (seg.perms & KIT_SEG_X) == 0) { + saw_shadow = 1; + EXPECT(seg.file_size == 0u, "NOLOAD section contributes no file bytes"); + EXPECT(seg.vsize == 4u, "NOLOAD section keeps memory size"); + } + } + EXPECT(text_end != 0u, "saw .text LOAD before shadow"); + EXPECT(saw_shadow, "saw NOLOAD shadow segment with zero file size"); + +done: + if (it) kit_obj_segiter_free(it); + emit_cleanup(&fx); +} + +/* Item 2: an inter-section `. = ALIGN(0x1000)` written between two output + * sections must advance the location counter at its textual position so the + * following section lands at the aligned address. */ +static void check_intersection_dot_align(void) { + static const char script_text[] = + "OUTPUT_FORMAT(\"elf64-littleaarch64\")\n" + "OUTPUT_ARCH(aarch64)\n" + "ENTRY(_start)\n" + "SECTIONS {\n" + " . = 0x10000;\n" + " .text : { *(.text .text.*) }\n" + " . = ALIGN(0x1000);\n" + " .data : { *(.data .data.*) }\n" + "}\n"; + EmitFixture fx; + KitObjSegIter* it = NULL; + int saw_text = 0; + int saw_data = 0; + + if (!emit_layout(script_text, sizeof(script_text) - 1u, &fx)) goto done; + EXPECT(kit_obj_segiter_new(fx.f, &it) == KIT_OK && it, "segment iterator"); + if (!it) goto done; + for (;;) { + KitObjSegInfo seg; + KitIterResult r = kit_obj_segiter_next(it, &seg); + if (r == KIT_ITER_END) break; + if (r != KIT_ITER_ITEM) break; + if (!kit_slice_eq_cstr(seg.name, "LOAD")) continue; + if (seg.vaddr == 0x10000u) saw_text = 1; + if (seg.perms & KIT_SEG_W) { + saw_data = 1; + EXPECT(seg.vaddr == 0x11000u, + ".data lands at . = ALIGN(0x1000) after .text"); + } + } + EXPECT(saw_text, "saw .text at 0x10000"); + EXPECT(saw_data, "saw .data aligned to 0x1000"); + +done: + if (it) kit_obj_segiter_free(it); + emit_cleanup(&fx); +} + +/* Item 3: a multi-byte fill value must be laid down as a repeating big-endian + * pattern across an in-section gap rather than truncated to its low byte. */ +static void check_multibyte_fill(void) { + /* .text has 4 bytes; a `. = . + 8` gap then 4 more bytes (.data). The gap + * is part of the .text output section and must be filled with the FILL + * pattern (0x12345678, big-endian, repeated). */ + static const char script_text[] = + "OUTPUT_FORMAT(\"elf64-littleaarch64\")\n" + "OUTPUT_ARCH(aarch64)\n" + "ENTRY(_start)\n" + "SECTIONS {\n" + " . = 0x10000;\n" + " .text : {\n" + " *(.text .text.*)\n" + " . = . + 8;\n" + " *(.data .data.*)\n" + " FILL(0x12345678)\n" + " }\n" + "}\n"; + EmitFixture fx; + KitObjSegIter* it = NULL; + KitSlice img; + size_t len = 0; + uint64_t seg_off = 0; + int found = 0; + + if (!emit_layout(script_text, sizeof(script_text) - 1u, &fx)) goto done; + EXPECT(kit_obj_segiter_new(fx.f, &it) == KIT_OK && it, "segment iterator"); + if (!it) goto done; + for (;;) { + KitObjSegInfo seg; + KitIterResult r = kit_obj_segiter_next(it, &seg); + if (r == KIT_ITER_END) break; + if (r != KIT_ITER_ITEM) break; + if (!kit_slice_eq_cstr(seg.name, "LOAD")) continue; + if (seg.vaddr == 0x10000u) { + seg_off = seg.file_off; + found = 1; + EXPECT(seg.file_size == 16u, ".text segment spans text+gap+data"); + } + } + EXPECT(found, "found .text segment"); + if (!found) goto done; + img.data = kit_writer_mem_bytes(fx.w, &len); + img.len = len; + EXPECT(img.data && (size_t)(seg_off + 16) <= len, "segment bytes in range"); + if (img.data && (size_t)(seg_off + 16) <= len) { + const uint8_t* p = (const uint8_t*)img.data + seg_off; + /* bytes [4..12) are the 8-byte gap, filled big-endian repeating + * 12 34 56 78 12 34 56 78 */ + static const uint8_t want[8] = {0x12, 0x34, 0x56, 0x78, + 0x12, 0x34, 0x56, 0x78}; + EXPECT(memcmp(p + 4, want, 8) == 0, + "gap filled with repeated big-endian FILL pattern"); + } + +done: + if (it) kit_obj_segiter_free(it); + emit_cleanup(&fx); +} + +/* Item 4: (a) two output sections naming the same :phdr coalesce into a single + * PT_LOAD; (b) a section listing several phdrs appears under each named + * segment. */ +static void check_phdr_coalesce_and_multi(void) { + static const char script_text[] = + "OUTPUT_FORMAT(\"elf64-littleaarch64\")\n" + "OUTPUT_ARCH(aarch64)\n" + "ENTRY(_start)\n" + "PHDRS {\n" + " text PT_LOAD FLAGS(5);\n" + " data PT_LOAD FLAGS(6);\n" + "}\n" + "SECTIONS {\n" + " . = 0x10000;\n" + " .text : { *(.text .text.*) } :text\n" + " .also : { *(.data .data.*) } :text :data\n" + "}\n"; + EmitFixture fx; + KitObjSegIter* it = NULL; + uint32_t nload = 0; + uint32_t n_rx = 0; /* PT_LOAD with R|X (the coalesced text phdr) */ + uint32_t n_rw = 0; /* PT_LOAD with R|W (the data phdr) */ + + if (!emit_layout(script_text, sizeof(script_text) - 1u, &fx)) goto done; + EXPECT(kit_obj_segiter_new(fx.f, &it) == KIT_OK && it, "segment iterator"); + if (!it) goto done; + for (;;) { + KitObjSegInfo seg; + KitIterResult r = kit_obj_segiter_next(it, &seg); + if (r == KIT_ITER_END) break; + if (r != KIT_ITER_ITEM) break; + if (!kit_slice_eq_cstr(seg.name, "LOAD")) continue; + ++nload; + if (seg.perms == (KIT_SEG_R | KIT_SEG_X)) ++n_rx; + if (seg.perms == (KIT_SEG_R | KIT_SEG_W)) ++n_rw; + } + /* .text and .also both name :text -> one R|X PT_LOAD; .also also names + * :data -> a second PT_LOAD with the data flags. So exactly 2 PT_LOAD. */ + EXPECT(nload == 2, "two sections sharing :text coalesce; :data adds one"); + EXPECT(n_rx == 1, "single coalesced R|X text segment"); + EXPECT(n_rw == 1, ".also also appears under the :data segment"); + +done: + if (it) kit_obj_segiter_free(it); + emit_cleanup(&fx); +} + +/* Item 5: deeply nested parentheses must fail cleanly (diagnostic) rather than + * overflow the parser/eval stack. */ +static void check_recursion_depth_guard(void) { + /* 512 nested parens around an int — well past the 256 limit. */ + char buf[2048]; + size_t pos = 0; + unsigned i; + KitLinkScript* s = NULL; + KitSlice text; + static const char prefix[] = "SECTIONS {\n . = "; + static const char suffix[] = ";\n}\n"; + memcpy(buf + pos, prefix, sizeof(prefix) - 1); + pos += sizeof(prefix) - 1; + for (i = 0; i < 512; ++i) buf[pos++] = '('; + buf[pos++] = '0'; + for (i = 0; i < 512; ++i) buf[pos++] = ')'; + memcpy(buf + pos, suffix, sizeof(suffix) - 1); + pos += sizeof(suffix) - 1; + text.s = buf; + text.len = pos; + /* must return an error, not crash, and not produce a script */ + EXPECT(kit_link_script_parse(&g_u.ctx, text, &s) != KIT_OK, + "deeply nested parens rejected, no crash"); + if (s) kit_link_script_free(&g_u.ctx, s); +} + int main(void) { kit_unit_init(&g_u); check_kernel_script_subset(); check_scripted_layout_vma_lma(); + check_noload_progbits(); + check_intersection_dot_align(); + check_multibyte_fill(); + check_phdr_coalesce_and_multi(); + check_recursion_depth_guard(); kit_unit_summary(&g_u, "link_script_test"); return kit_unit_status(&g_u); }