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:
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();