commit adf6261f0dc335fc1fb5d38afb5d9b5a4cf066ec
parent 8dbe9e8841b691663cce45b6a526d2a9906109eb
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Tue, 16 Jun 2026 15:25:23 -0700
link: --section-start/-Tdata/-Tbss land at the EXACT requested vaddr (Bug 6)
link_apply_section_starts set sec->vaddr = ss->addr - img_base pre-shift, but
the ELF emitter then adds headers_load (the ehdr+phdrs page) in
shift_image_addresses and img_base in the final p_vaddr, so the section landed
at ss->addr + headers_load (e.g. --section-start=.text=0x500000 -> 0x504000).
The below-base ternary (want = addr>=base ? addr-base : addr) also re-added
the base for addresses under it.
Approach A (defer into the emitter): link_apply_section_starts now only
validates + warns at layout time; the ELF emitter's new apply_section_starts
runs AFTER shift_image_addresses and BEFORE apply_all_relocs, computing
delta = ss->addr - (img_base + post_shift_vaddr) and shifting the matched
section's whole segment (segment + its sections + the symbols and reloc sites
resolved into them) uniformly. The delta absorbs headers_load AND img_base with
no special-casing, including below-base requests, so the FINAL p_vaddr / entry
/ symbol addresses equal ss->addr exactly. -Ttext is unchanged (it sets the
ET_EXEC image base; the entry stays at base + headers page).
Removed the now-unused link_static_image_base helper; corrected the link.h
contract comment and the buildcmds section-start comment/assertions.
Tests: fx2-ss-* assert exact .text/.data vaddr + _start symbol + entry, a
below-base pin, and the ld path; the pre-existing be-section-start asserts are
tightened from a 0x50 prefix to the exact 0x500000.
Diffstat:
4 files changed, 95 insertions(+), 49 deletions(-)
diff --git a/include/kit/link.h b/include/kit/link.h
@@ -266,11 +266,14 @@ typedef struct KitLinkDefsym {
} KitLinkDefsym;
/* `--section-start=.name=ADDR` (and the `-Tdata`/`-Tbss` shorthands, which
- * the driver folds in as `.data` / `.bss`): force an output section's runtime
- * address. Applied as a post-layout vaddr override on the matching section and
- * its containing segment. Honored on the default (non-scripted) static layout;
- * a linker script pins addresses itself, so these are ignored when a script is
- * in force. */
+ * the driver folds in as `.data` / `.bss`): force an output section's *exact*
+ * final runtime address (GNU-ld semantics). Applied by the format emitter after
+ * the headers load-page and image base are known, by shifting the matched
+ * section's whole segment so the section lands precisely at ADDR (intra-segment
+ * offsets and symbol addresses move with it). Honored on the default
+ * (non-scripted) static layout; a linker script pins addresses itself, so these
+ * are ignored when a script is in force. (Distinct from `-Ttext`, which sets the
+ * ET_EXEC image base — the entry then sits at base + the ehdr+phdrs page.) */
typedef struct KitLinkSectionStart {
KitSlice name;
uint64_t addr;
diff --git a/src/link/link_layout.c b/src/link/link_layout.c
@@ -2038,28 +2038,17 @@ static void link_apply_defsyms(Linker* l, LinkImage* img) {
}
}
-/* The static ET_EXEC runtime base the ELF emitter will add to image-relative
- * vaddrs. Mirrors src/obj/elf/link.c's img_base rule so --section-start can
- * interpret its argument as an absolute runtime address (it subtracts this
- * base to recover the image-relative vaddr the layout / emit pass uses). PIE
- * and scripted images keep base 0. */
-#define LINK_IMAGE_BASE_STATIC 0x400000ULL
-static u64 link_static_image_base(const LinkImage* img) {
- if (img->pie || img->scripted) return 0ULL;
- if (img->text_base_set) return img->text_base;
- return LINK_IMAGE_BASE_STATIC;
-}
-
-/* --section-start / -Tdata / -Tbss: post-layout vaddr override for a named
- * output section and its containing segment. The requested address is an
- * absolute runtime address (GNU-ld semantics); it is converted to the
- * image-relative vaddr space layout/emit use by subtracting the static image
- * base. Ignored under a script (which pins addresses itself). Best-effort
- * under the bucket layout: it shifts the matched section's segment so the
- * section lands at the requested address, keeping intra-segment offsets. */
+/* --section-start / -Tdata / -Tbss: force a named output section's *final*
+ * runtime address to exactly the requested value (GNU-ld semantics). The actual
+ * placement is deferred to the format emitter (ELF: link_apply_section_starts
+ * in src/obj/elf/link.c, applied AFTER shift_image_addresses so the headers
+ * load-page and the runtime image base both fall out of the delta) — at layout
+ * time we only validate, since the image-relative vaddr here is not yet in the
+ * final coordinate system. Ignored under a script (which pins addresses
+ * itself). This pass emits the diagnostics (it has Linker / link_warn); the
+ * matched section's segment is shifted by the emitter to land it exactly. */
static void link_apply_section_starts(Linker* l, LinkImage* img) {
u32 i, j;
- u64 base;
if (l->script) {
if (l->nsection_starts)
link_warn(l,
@@ -2067,11 +2056,8 @@ static void link_apply_section_starts(Linker* l, LinkImage* img) {
"script");
return;
}
- base = link_static_image_base(img);
for (i = 0; i < l->nsection_starts; ++i) {
const KitLinkSectionStart* ss = &l->section_starts[i];
- /* The requested absolute address, expressed in image-relative space. */
- u64 want = ss->addr >= base ? ss->addr - base : ss->addr;
int matched = 0;
if (!ss->name.s || ss->name.len == 0) continue;
for (j = 0; j < img->nsections; ++j) {
@@ -2081,23 +2067,6 @@ static void link_apply_section_starts(Linker* l, LinkImage* img) {
memcmp(nm.s, ss->name.s, nm.len) != 0)
continue;
matched = 1;
- if (sec->segment_id != LINK_SEG_NONE &&
- sec->segment_id <= img->nsegments) {
- LinkSegment* seg = &img->segments[sec->segment_id - 1];
- u64 delta = want - sec->vaddr;
- u32 k;
- /* Shift the whole segment so this section lands at the requested
- * address; other sections in the segment keep their relative
- * placement. */
- seg->vaddr += delta;
- seg->paddr += delta;
- for (k = 0; k < img->nsections; ++k) {
- if (img->sections[k].segment_id == seg->id)
- img->sections[k].vaddr += delta;
- }
- } else {
- sec->vaddr = want;
- }
}
if (!matched)
link_warn(l, "link: --section-start: no output section named '%.*s'",
diff --git a/src/obj/elf/link.c b/src/obj/elf/link.c
@@ -343,6 +343,74 @@ static void shift_image_addresses(LinkImage* img, u64 delta) {
}
}
+/* --section-start / -Tdata / -Tbss: force the FINAL runtime address of a named
+ * output section to exactly the requested value (GNU-ld semantics). Applied
+ * here, AFTER shift_image_addresses, so the requested address is compared
+ * against the section's final p_vaddr (img_base + post-shift vaddr); the delta
+ * absorbs both the headers load-page (headers_load) and the runtime image base
+ * (img_base) with no special-casing, including below-base requests. The
+ * matched section's whole segment is shifted uniformly so intra-segment offsets
+ * and symbol addresses stay consistent, mirroring shift_image_addresses. Must
+ * run before apply_all_relocs so reloc sites see the pinned addresses. Scripted
+ * images pin addresses via the script and carry no pins here. */
+static void apply_section_starts(LinkImage* img, u64 img_base) {
+ Linker* l = img->linker;
+ u32 i, j;
+ if (!l || img->scripted) return;
+ for (i = 0; i < l->nsection_starts; ++i) {
+ const KitLinkSectionStart* ss = &l->section_starts[i];
+ LinkSegmentId target_seg = LINK_SEG_NONE;
+ i64 delta = 0;
+ if (!ss->name.s || ss->name.len == 0) continue;
+ /* Find the matched output section and compute the shift that lands its
+ * final address on ss->addr. (A name-less warning was already issued at
+ * layout time by link_apply_section_starts.) */
+ for (j = 0; j < img->nsections; ++j) {
+ LinkSection* sec = &img->sections[j];
+ Slice nm = sec->name ? pool_slice(img->c->global, sec->name) : SLICE_NULL;
+ if (!nm.s || nm.len != ss->name.len ||
+ memcmp(nm.s, ss->name.s, nm.len) != 0)
+ continue;
+ if (sec->file_only) continue;
+ if (sec->segment_id != LINK_SEG_NONE &&
+ sec->segment_id <= img->nsegments) {
+ target_seg = sec->segment_id;
+ delta = (i64)ss->addr - (i64)(img_base + sec->vaddr);
+ } else {
+ /* No containing segment (rare): pin the section's vaddr directly so
+ * img_base + vaddr == ss->addr. */
+ sec->vaddr = ss->addr - img_base;
+ }
+ break;
+ }
+ if (target_seg == LINK_SEG_NONE || delta == 0) continue;
+ /* Shift the whole segment (segment + its sections + the symbols and reloc
+ * sites resolved into them) so the pinned section lands exactly, keeping
+ * every relative placement within the segment. */
+ img->segments[target_seg - 1].vaddr += (u64)delta;
+ img->segments[target_seg - 1].paddr += (u64)delta;
+ for (j = 0; j < img->nsections; ++j)
+ if (img->sections[j].segment_id == target_seg)
+ img->sections[j].vaddr += (u64)delta;
+ for (j = 0; j < LinkSyms_count(&img->syms); ++j) {
+ LinkSymbol* s = LinkSyms_at(&img->syms, j);
+ if (s->kind == SK_ABS || !s->defined) continue;
+ if (s->section_id == LINK_SEC_NONE || s->section_id > img->nsections)
+ continue;
+ if (img->sections[s->section_id - 1].segment_id == target_seg)
+ s->vaddr += (u64)delta;
+ }
+ for (j = 0; j < LinkRelocs_count(&img->relocs); ++j) {
+ LinkRelocApply* r = LinkRelocs_at(&img->relocs, j);
+ if (r->link_section_id == LINK_SEC_NONE ||
+ r->link_section_id > img->nsections)
+ continue;
+ if (img->sections[r->link_section_id - 1].segment_id == target_seg)
+ r->write_vaddr += (u64)delta;
+ }
+ }
+}
+
/* AArch64 ELF ABI: the per-thread TLS block starts at TP + 16 bytes
* (the TCB sits ahead of the TLS image). RISC-V psABI normally points
* tp at the start of the TLS image; the kit harness's start.c
@@ -1003,6 +1071,10 @@ void link_emit_elf(LinkImage* img, Writer* w) {
shift_image_file_offsets(img, headers_load);
else
shift_image_addresses(img, headers_load);
+ /* --section-start / -Tdata / -Tbss: pin chosen sections to their exact final
+ * runtime addresses now that vaddrs are post-shift (and before relocs see
+ * them). No-op for scripted images. */
+ apply_section_starts(img, img_base);
apply_all_relocs(img, img_base);
/* ---- write .dynamic body + re-serialize .rela.dyn (PIE only) ----
diff --git a/test/buildcmds/run.sh b/test/buildcmds/run.sh
@@ -350,13 +350,15 @@ run_fail be-defsym-bad "$KIT" build-exe -target x86_64-linux -nostdlib -static \
contains be-defsym-bad-diag "$work/be-defsym-bad.err" "NAME=EXPR"
# ---- --section-start / -Tdata / -Tbss: per-section address overrides -------
-# These are accepted (best-effort placement; -Ttext / a linker script give
-# exact addresses). Assert acceptance + determinism rather than an exact vaddr.
+# The requested absolute address is the section's EXACT final runtime address
+# (GNU-ld semantics). (-Ttext is the ET_EXEC header-page model — img_base plus
+# the ehdr+phdrs page — so it is intentionally NOT exact; see the FX2 block at
+# the end for the dedicated exact-vaddr coverage.)
run_ok be-section-start "$KIT" build-exe -target x86_64-linux -nostdlib \
-static -no-pie --section-start=.text=0x500000 -Tdata 0x600000 \
--map secstart.map kstart.c -o secstart.elf
-contains be-section-start-text secstart.map "vaddr=0x50"
-contains be-section-start-data secstart.map "vaddr=0x60"
+contains be-section-start-text secstart.map "vaddr=0x500000"
+contains be-section-start-data secstart.map "vaddr=0x600000"
run_ok ld-tbss "$KIT" ld -nostdlib -static -no-pie -Tdata 0x600000 \
kstart.o -o tbss.elf
run_ok ld-section-start "$KIT" ld -nostdlib -static -no-pie \