kit

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

commit 0e43fcea5cb98d166ee610ebda16d422d714b718
parent 70caa5263acefdad2e9fd1d2190f10a681c0b165
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Tue, 16 Jun 2026 17:57:46 -0700

link: advance location counter through input sections for interior script symbol assignments

A linker-script output-section body executes its commands in GNU-ld source
order: an input-section glob (*(.data*)) advances the location counter '.',
and a following 'sym = .' or '. = expr' must see the advanced dot. The
applicator was instead applying ALL section-body assignments in one loop
BEFORE placing any input sections, so a trailing '_edata = .' captured the
section-start dot and collapsed onto a leading '_sdata = .'. A Cortex-M
.data copy loop (while (_sdata < _edata)) then copied zero bytes, leaving
initialized globals unset.

Stamp a source-order body_seq onto each input match and body assignment in
the parser (the injected ': ALIGN(N)' header is body_seq 0 and still runs
before the section base is captured). In layout, merge-walk inputs and
assignments by body_seq so interior symbols capture '.' at their textual
position; a trailing '. = ALIGN(N)' now also extends the section span (and
its interior file padding when it follows emitted PROGBITS).

Adds check_body_symbols_after_glob: two data globs with _sdata/_mid/_edata
markers and a '. = ALIGN(16)' before the end marker, asserting each captures
the advanced dot. test-link 124/0, test-elf 41/0, test-macho 80/0.

Diffstat:
Minclude/kit/link.h | 12++++++++++++
Msrc/link/link_layout.c | 202++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Msrc/link/link_script.c | 24+++++++++++++++++++++---
Mtest/link/link_script_test.c | 161+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 316 insertions(+), 83 deletions(-)

diff --git a/include/kit/link.h b/include/kit/link.h @@ -87,6 +87,11 @@ typedef struct KitLinkInputMatch { const KitSlice* exclude_file_patterns; uint32_t nexclude_file_patterns; int keep; + /* Source-order position of this `*(...)` within its enclosing output-section + * body, shared with KitLinkAssignment.body_seq. Lets the applicator execute + * input-section placement and interior `sym = .` / `. = expr` assignments in + * GNU-ld source order, so a symbol after a glob captures the advanced `.`. */ + uint32_t body_seq; } KitLinkInputMatch; typedef enum KitLinkAsnKind { @@ -106,6 +111,13 @@ typedef struct KitLinkAssignment { * assignments (e.g. `. = ALIGN(N)` between two sections) with output * sections in source order. Only meaningful for top_asns; 0 otherwise. */ uint32_t seq; + /* Source-order position of this assignment WITHIN an output-section body, + * shared with KitLinkInputMatch.body_seq, so the applicator interleaves + * interior `sym = .` / `. = expr` with `*(...)` input placement in GNU-ld + * source order. Only meaningful for KitLinkOutputSection.asns; 0 otherwise. + * The injected `: ALIGN(N)` header assignment is body_seq 0 and is applied + * before any input section (it sets the section base). */ + uint32_t body_seq; } KitLinkAssignment; typedef struct KitLinkOutputSection { diff --git a/src/link/link_layout.c b/src/link/link_layout.c @@ -1331,94 +1331,136 @@ static void link_layout_sections_scripted(Linker* l, LinkImage* img, SLICE_ARG(os->name)); fill_value = fv; } + /* Body command 0: the injected `: ALIGN(N)` header (if any) sets the + * section base before any content is placed. Apply only body_seq-0 + * assignments here, then capture sec_start_dot. The remaining body + * commands (input-section globs and interior `sym = .` / `. = expr` + * assignments) execute below in true source order. */ for (k = 0; k < os->nasns; ++k) { - apply_asn(l, img, &dot, outs, script->nsections, &os->asns[k]); + if (os->asns[k].body_seq == 0) + apply_asn(l, img, &dot, outs, script->nsections, &os->asns[k]); } sec_start_dot = dot; + /* Interleave input-section placement and interior assignments by their + * source-order body_seq. GNU ld executes section-body commands in order, + * so a `sym = .` after a `*(.data*)` glob captures `.` AFTER the glob's + * content, and a `. = ALIGN(N)` after the last glob extends the section. + * Both os->asns (body_seq >= 1) and os->inputs are stamped in ascending + * source order, so a single forward cursor over each suffices. */ { - u32 mi; - for (mi = 0; mi < os->ninputs; ++mi) { - const KitLinkInputMatch* im = &os->inputs[mi]; - for (ii = 0; ii < LinkInputs_count(&l->inputs); ++ii) { - ObjBuilder* ob = LinkInputs_at(&l->inputs, ii)->obj; - InputMap* m = &img->input_maps[ii]; - for (j = 1; j < obj_section_count(ob); ++j) { - const Section* s; - const char* nm; - u32 align; - u32 first = 0, count = 1, ai; - int has_atoms; - if (claimed[ii][j]) continue; - if (m->comdat_discarded[j]) continue; - s = obj_section_get(ob, j); - if (!s || !link_section_kept(s)) continue; - { - nm = pool_slice(l->c->global, s->name).s; - } - if (!nm) continue; - if (!input_match_section(l, ii, im, nm)) continue; - - align = subalign_val ? (u32)subalign_val : (s->align ? s->align : 1u); - has_atoms = link_input_section_has_atoms(m, j); - if (has_atoms) link_input_section_atoms(m, j, &first, &count); - for (ai = 0; ai < count; ++ai) { - ObjAtomId aid = - has_atoms ? m->section_atom_ids[first + ai] : OBJ_ATOM_NONE; - const ObjAtom* atom = has_atoms ? obj_atom_get(ob, aid) : NULL; - u64 ofs; - LinkSection* ls; - LinkSectionId lsid; - u64 obj_offset; - u64 size; - if (has_atoms) { - if (!atom || atom->removed) continue; - if (!link_gc_atom_live_get(g, ii, aid)) continue; - obj_offset = atom->offset; - size = atom->size; - } else if (!link_gc_live_get(g, ii, j)) { - continue; - } else { - obj_offset = 0u; - size = link_section_size_for_link(s); + u32 ak = 0; + u32 mi = 0; + /* Skip past the body_seq-0 header assignments already applied above. */ + while (ak < os->nasns && os->asns[ak].body_seq == 0) ++ak; + for (;;) { + int have_asn = ak < os->nasns; + int have_in = mi < os->ninputs; + if (!have_asn && !have_in) break; + /* Apply the assignment whose body_seq is strictly before the next + * pending input command (or all trailing assignments when no inputs + * remain). */ + if (have_asn && + (!have_in || os->asns[ak].body_seq < os->inputs[mi].body_seq)) { + apply_asn(l, img, &dot, outs, script->nsections, &os->asns[ak]); + /* A `. = expr` that advances dot past the placed content extends the + * section span (GNU ld grows the output section to the new dot). The + * gap also becomes interior file padding when it follows emitted + * PROGBITS bytes in a loadable section (file_size_accum already + * nonzero); a gap before any file content stays bss-like. */ + if (dot - sec_start_dot > mem_size_accum) + mem_size_accum = dot - sec_start_dot; + if (!os->noload && file_size_accum && + dot - sec_start_dot > file_size_accum) + file_size_accum = dot - sec_start_dot; + ++ak; + continue; + } + { + const KitLinkInputMatch* im = &os->inputs[mi]; + for (ii = 0; ii < LinkInputs_count(&l->inputs); ++ii) { + ObjBuilder* ob = LinkInputs_at(&l->inputs, ii)->obj; + InputMap* m = &img->input_maps[ii]; + for (j = 1; j < obj_section_count(ob); ++j) { + const Section* s; + const char* nm; + u32 align; + u32 first = 0, count = 1, ai; + int has_atoms; + if (claimed[ii][j]) continue; + if (m->comdat_discarded[j]) continue; + s = obj_section_get(ob, j); + if (!s || !link_section_kept(s)) continue; + { + nm = pool_slice(l->c->global, s->name).s; + } + if (!nm) continue; + if (!input_match_section(l, ii, im, nm)) continue; + + align = + subalign_val ? (u32)subalign_val : (s->align ? s->align : 1u); + has_atoms = link_input_section_has_atoms(m, j); + if (has_atoms) link_input_section_atoms(m, j, &first, &count); + for (ai = 0; ai < count; ++ai) { + ObjAtomId aid = + has_atoms ? m->section_atom_ids[first + ai] : OBJ_ATOM_NONE; + const ObjAtom* atom = has_atoms ? obj_atom_get(ob, aid) : NULL; + u64 ofs; + LinkSection* ls; + LinkSectionId lsid; + u64 obj_offset; + u64 size; + if (has_atoms) { + if (!atom || atom->removed) continue; + if (!link_gc_atom_live_get(g, ii, aid)) continue; + obj_offset = atom->offset; + size = atom->size; + } else if (!link_gc_live_get(g, ii, j)) { + continue; + } else { + obj_offset = 0u; + size = link_section_size_for_link(s); + } + if (align > align_max) align_max = align; + dot = ALIGN_UP(dot, (u64)align); + ofs = dot; + + lsid = (LinkSectionId)(img->nsections + 1u); + ls = &img->sections[img->nsections++]; + memset(ls, 0, sizeof(*ls)); + ls->id = lsid; + ls->input_id = LinkInputs_at(&l->inputs, ii)->id; + ls->obj_section_id = j; + ls->obj_atom_id = aid; + ls->segment_id = seg_id; + ls->obj_offset = obj_offset; + ls->vaddr = ofs; + ls->size = size; + ls->flags = s->flags; + ls->align = align; + ls->name = s->name; + /* 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 (!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; } - if (align > align_max) align_max = align; - dot = ALIGN_UP(dot, (u64)align); - ofs = dot; - - lsid = (LinkSectionId)(img->nsections + 1u); - ls = &img->sections[img->nsections++]; - memset(ls, 0, sizeof(*ls)); - ls->id = lsid; - ls->input_id = LinkInputs_at(&l->inputs, ii)->id; - ls->obj_section_id = j; - ls->obj_atom_id = aid; - ls->segment_id = seg_id; - ls->obj_offset = obj_offset; - ls->vaddr = ofs; - ls->size = size; - ls->flags = s->flags; - ls->align = align; - ls->name = s->name; - /* 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 (!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; + claimed[ii][j] = 1; } - claimed[ii][j] = 1; } + ++mi; } } } diff --git a/src/link/link_script.c b/src/link/link_script.c @@ -867,10 +867,22 @@ static int parse_file_qualified_matchers(LSP* p, VecMatch* inputs, } static int parse_section_body(LSP* p, VecMatch* inputs, VecAsn* asns, - VecAssert* asserts, KitLinkExpr** fill_out) { + VecAssert* asserts, KitLinkExpr** fill_out, + u32* body_seq) { + u32 prev_in = inputs->n; + u32 prev_as = asns->n; if (expect_ch(p, '{')) return 1; for (;;) { int ch; + /* Stamp the source-order body position onto every input match and + * assignment pushed by the previous iteration, then bump for the next + * command. One body command (a `*(...)`, a `sym = expr`, a `. = expr`) + * may push several input matches (`*(.a .b .c)`) but they all belong to + * the same command and share its body_seq. The applicator merge-walks + * inputs+asns by body_seq to execute them in true source order. */ + while (prev_in < inputs->n) inputs->p[prev_in++].body_seq = *body_seq; + while (prev_as < asns->n) asns->p[prev_as++].body_seq = *body_seq; + ++*body_seq; skip_ws(p); if (p->err) return 1; ch = peek_ch(p); @@ -1038,12 +1050,18 @@ static int parse_output_section(LSP* p, const char* name_buf, size_t name_len, } /* Section header alignment is encoded as the first asn — applicator - * pulls it before processing inputs. */ + * pulls it before processing inputs. It runs at body_seq 0 (the section + * base); body commands start at body_seq 1. */ if (align_n) { if (push_dot_align(p, &asns, align_n)) goto fail; + asns.p[asns.n - 1].body_seq = 0; } - if (parse_section_body(p, &inputs, &asns, asserts, &fill)) goto fail; + { + u32 body_seq = 1; + if (parse_section_body(p, &inputs, &asns, asserts, &fill, &body_seq)) + goto fail; + } for (;;) { int ch; diff --git a/test/link/link_script_test.c b/test/link/link_script_test.c @@ -891,12 +891,173 @@ static void check_recursion_depth_guard(void) { if (s) kit_link_script_free(&g_u.ctx, s); } +/* Build an input with two writable data sections (.data = 4 bytes, .data.more + * = 8 bytes) plus a _start in .text, so a section body can glob each + * separately and place interior `sym = .` markers between them. */ +static KitObjBuilder* build_two_data_input(KitCompiler* c) { + static const uint8_t text_bytes[4] = {0xc0, 0x03, 0x5f, 0xd6}; + static const uint8_t data_bytes[4] = {1, 2, 3, 4}; + static const uint8_t data2_bytes[8] = {5, 6, 7, 8, 9, 10, 11, 12}; + KitObjBuilder* ob = NULL; + KitObjSection text = KIT_SECTION_NONE; + KitObjSection data = KIT_SECTION_NONE; + KitObjSection data2 = KIT_SECTION_NONE; + KitObjSymbol sym = KIT_OBJ_SYMBOL_NONE; + KitObjSectionDesc d; + KitObjSymbolDesc sd; + + if (kit_obj_builder_new(c, &ob) != KIT_OK || !ob) { + EXPECT(0, "obj builder allocation"); + return NULL; + } + memset(&d, 0, sizeof(d)); + d.name = kit_sym_intern(c, KIT_SLICE_LIT(".text")); + d.kind = KIT_SEC_TEXT; + d.flags = KIT_SF_ALLOC | KIT_SF_EXEC; + d.align = 4; + if (kit_obj_builder_section(ob, &d, &text) != KIT_OK || + kit_obj_builder_write(ob, text, text_bytes, sizeof(text_bytes)) != KIT_OK) + goto fail; + + memset(&d, 0, sizeof(d)); + d.name = kit_sym_intern(c, KIT_SLICE_LIT(".data")); + d.kind = KIT_SEC_DATA; + d.flags = KIT_SF_ALLOC | KIT_SF_WRITE; + d.align = 4; + if (kit_obj_builder_section(ob, &d, &data) != KIT_OK || + kit_obj_builder_write(ob, data, data_bytes, sizeof(data_bytes)) != KIT_OK) + goto fail; + + memset(&d, 0, sizeof(d)); + d.name = kit_sym_intern(c, KIT_SLICE_LIT(".data.more")); + d.kind = KIT_SEC_DATA; + d.flags = KIT_SF_ALLOC | KIT_SF_WRITE; + d.align = 4; + if (kit_obj_builder_section(ob, &d, &data2) != KIT_OK || + kit_obj_builder_write(ob, data2, data2_bytes, sizeof(data2_bytes)) != + KIT_OK) + goto fail; + + memset(&sd, 0, sizeof(sd)); + sd.name = kit_sym_intern(c, KIT_SLICE_LIT("_start")); + sd.bind = KIT_SB_GLOBAL; + sd.kind = KIT_SK_FUNC; + sd.section = text; + sd.value = 0; + sd.size = sizeof(text_bytes); + if (kit_obj_builder_symbol(ob, &sd, &sym) != KIT_OK) goto fail; + + if (kit_obj_builder_finalize(ob) != KIT_OK) goto fail; + return ob; +fail: + EXPECT(0, "build two-data input"); + kit_obj_builder_free(ob); + return NULL; +} + +/* Bug fix: section-body commands execute in source order, so a `sym = .` after + * an input-section glob captures the location counter ADVANCED past the glob's + * content, and `. = ALIGN(N)` after the last glob extends the section. Before + * the fix every body assignment was evaluated against the section-start dot, so + * _sdata == _mid == _edata. This drives the exact bare-metal `.data` copy-loop + * idiom (_sdata/_edata bracketing initialized data) plus an interior midpoint + * marker between two globs. */ +static void check_body_symbols_after_glob(void) { + static const char script_text[] = + "OUTPUT_FORMAT(\"elf64-littleaarch64\")\n" + "OUTPUT_ARCH(aarch64)\n" + "ENTRY(_start)\n" + "MEMORY {\n" + " FLASH (rx) : ORIGIN = 0x10000, LENGTH = 64K\n" + " SRAM (rwx) : ORIGIN = 0x20000000, LENGTH = 64K\n" + "}\n" + "SECTIONS {\n" + " .text : { *(.text .text.*) } > FLASH\n" + " .data : ALIGN(4) {\n" + " _sdata = .;\n" + " *(.data)\n" + " _mid = .;\n" + " *(.data.more)\n" + " . = ALIGN(16);\n" + " _edata = .;\n" + " } > SRAM AT> FLASH\n" + "}\n"; + KitTargetSpec target = + kit_unit_target(KIT_ARCH_ARM_64, KIT_OS_LINUX, KIT_OBJ_ELF); + KitCompiler* c = NULL; + KitObjBuilder* ob = NULL; + KitLinkScript* s = NULL; + KitLinkSession* sess = NULL; + KitWriter* w = NULL; + KitObjFile* f = NULL; + KitSlice script; + KitSlice bytes; + KitLinkSessionOptions opts; + KitObjSymInfo sdata, mid, edata; + size_t len = 0; + + script.s = script_text; + script.len = sizeof(script_text) - 1u; + EXPECT(kit_link_script_parse(&g_u.ctx, script, &s) == KIT_OK && s, + "body-symbol script parses"); + if (!s) goto done; + EXPECT(kit_unit_compiler_new(&g_u, target, &c) == KIT_OK && c, + "compiler allocation"); + if (!c) goto done; + ob = build_two_data_input(c); + if (!ob) goto done; + memset(&opts, 0, sizeof(opts)); + opts.output_kind = KIT_LINK_OUTPUT_EXE; + opts.linker_script = s; + EXPECT(kit_link_session_new(c, &opts, &sess) == KIT_OK && sess, + "link session allocation"); + if (!sess) goto done; + EXPECT(kit_link_session_add_obj(sess, ob) == KIT_OK, "add input object"); + EXPECT(kit_writer_mem(&g_u.heap, &w) == KIT_OK && w, "memory writer"); + if (!w) goto done; + EXPECT(kit_link_session_emit(sess, w) == KIT_OK, "emit scripted image"); + bytes.data = kit_writer_mem_bytes(w, &len); + bytes.len = len; + if (!bytes.data || bytes.len == 0) goto done; + EXPECT(kit_obj_open(&g_u.ctx, KIT_SLICE_LIT("<body-symbols>"), &bytes, &f) == + KIT_OK && + f, + "open scripted image"); + if (!f) goto done; + + EXPECT(kit_obj_symbol_by_name(f, KIT_SLICE_LIT("_sdata"), &sdata) == KIT_OK, + "_sdata defined"); + EXPECT(kit_obj_symbol_by_name(f, KIT_SLICE_LIT("_mid"), &mid) == KIT_OK, + "_mid defined"); + EXPECT(kit_obj_symbol_by_name(f, KIT_SLICE_LIT("_edata"), &edata) == KIT_OK, + "_edata defined"); + + /* .data is placed at SRAM origin (4-aligned). The leading marker captures the + * base; `.data` (4 bytes) advances to base+4; `.data.more` (8 bytes, + * already 4-aligned) ends at base+12; `. = ALIGN(16)` rounds to base+16. */ + EXPECT(sdata.value == 0x20000000u, "_sdata == .data start (section base)"); + EXPECT(mid.value == 0x20000004u, "_mid == base + sizeof(.data) after glob"); + EXPECT(edata.value == 0x20000010u, + "_edata == ALIGN(16) of base + .data + .data.more"); + EXPECT(sdata.value != edata.value, + "interior symbols around globs are not collapsed to the section start"); + +done: + if (f) kit_obj_free(f); + if (w) kit_writer_close(w); + if (sess) kit_link_session_free(sess); + if (ob) kit_obj_builder_free(ob); + if (s) kit_link_script_free(&g_u.ctx, s); + if (c) kit_compiler_free(c); +} + 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_body_symbols_after_glob(); check_multibyte_fill(); check_phdr_coalesce_and_multi(); check_bss_tail_no_coalesce_skew();