commit 1dae315948770c1020a5231df4d4a7698b48aa7c
parent c52ea923e42a779d36aba62e7d324f737e41cce6
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Tue, 16 Jun 2026 14:33:35 -0700
link: coalesce sections sharing a :phdr; honor multi-phdr sections
Each scripted output section made its own LinkSegment and only phdrs[0]
was consulted. Implement GNU ld phdr semantics:
(a) a section whose primary :phdr matches the currently-open segment
and is vaddr-contiguous coalesces into that one PT_LOAD (extending
its byte buffer); and
(b) each additional :phdr a section names becomes an alias program
header covering just that section's vaddr/file range, with no byte
buffer of its own.
The per-segment primary phdr name is tracked in a function-local array
(no LinkSegment field). Alias segments carry a NULL byte buffer; the ELF
byte writer now skips NULL-buffer segments (their file range is already
on disk via the primary). nseg_max is bumped to count additional phdrs.
Factored a script_apply_phdr helper out of the duplicated phdr-attr
setup.
Adds check_phdr_coalesce_and_multi (red->green). test-elf 41/0,
existing scripted layout checks still green.
Diffstat:
2 files changed, 186 insertions(+), 56 deletions(-)
diff --git a/src/link/link_layout.c b/src/link/link_layout.c
@@ -831,6 +831,32 @@ static u64 eval_link_expr(Linker* l, LinkImage* img, u64 dot,
}
}
+/* Set a segment's phdr attributes (type/filehdr/phdrs/flags) from the named
+ * PHDRS entry `name`. Panics if the name is unknown. `dot`/`outs`/`nouts` are
+ * forwarded to the FLAGS() expression evaluation. */
+static void script_apply_phdr(Linker* l, LinkImage* img,
+ const KitLinkScript* script, LinkSegment* seg,
+ KitSlice name, u64 dot, const ScriptOutInfo* outs,
+ u32 nouts) {
+ const KitLinkPhdr* ph = script_find_phdr(script, name);
+ if (!ph)
+ compiler_panic(l->c, SRCLOC_NONE, "linker script: unknown PHDR '%.*s'",
+ SLICE_ARG(name));
+ seg->phdr_type = ph->type;
+ seg->phdr_filehdr = ph->filehdr ? 1u : 0u;
+ seg->phdr_phdrs = ph->phdrs ? 1u : 0u;
+ if (ph->flags) {
+ int err = 0;
+ seg->phdr_flags = (u32)eval_link_expr(l, img, dot, outs, nouts, ph->flags,
+ &err);
+ if (err)
+ compiler_panic(l->c, SRCLOC_NONE,
+ "linker script: invalid PHDR FLAGS for '%.*s'",
+ SLICE_ARG(ph->name));
+ seg->phdr_flags_set = 1u;
+ }
+}
+
/* Format-aware C-symbol mangling for linker-synthesized boundaries. */
static Sym boundary_name(Linker* l, const char* name) {
return obj_format_c_mangle(l->c, name);
@@ -1138,9 +1164,16 @@ static void link_layout_sections_scripted(Linker* l, LinkImage* img,
* next unapplied top_asn. */
u32 dot_cursor = 0;
+ /* Upper bound on segments: each non-discard section contributes at least
+ * one, plus one per *additional* :phdr it names (a section listing several
+ * phdrs appears under each as its own program header). Coalescing sections
+ * that share a phdr only reduces the count, so this stays an upper bound. */
u32 nseg_max = 0;
- for (si = 0; si < script->nsections; ++si)
- if (!slice_eq_cstr(script->sections[si].name, "/DISCARD/")) ++nseg_max;
+ for (si = 0; si < script->nsections; ++si) {
+ const KitLinkOutputSection* os_ = &script->sections[si];
+ if (slice_eq_cstr(os_->name, "/DISCARD/")) continue;
+ nseg_max += os_->nphdrs ? os_->nphdrs : 1u;
+ }
img->segments =
nseg_max ? (LinkSegment*)h->alloc(h, sizeof(*img->segments) * nseg_max,
_Alignof(LinkSegment))
@@ -1163,6 +1196,22 @@ static void link_layout_sections_scripted(Linker* l, LinkImage* img,
sizeof(*img->segment_bytes_cap) * nseg_max);
}
+ /* Per-segment primary :phdr name (parallel to img->segments while we build
+ * it); a new section naming the same phdr as the currently-open segment and
+ * vaddr-contiguous with it coalesces into that one PT_LOAD. Kept local — no
+ * LinkSegment field needed. open_seg is the index of the coalescable open
+ * segment, or (u32)-1 when none can be extended. */
+ KitSlice* seg_phdr_name = NULL;
+ u32 open_seg = (u32)-1;
+ if (nseg_max) {
+ seg_phdr_name =
+ (KitSlice*)h->alloc(h, sizeof(*seg_phdr_name) * nseg_max,
+ _Alignof(KitSlice));
+ if (!seg_phdr_name)
+ compiler_panic(img->c, SRCLOC_NONE, "link: oom on segment phdr names");
+ memset(seg_phdr_name, 0, sizeof(*seg_phdr_name) * nseg_max);
+ }
+
for (si = 0; si < script->nsections; ++si) {
const KitLinkOutputSection* os = &script->sections[si];
int is_discard = slice_eq_cstr(os->name, "/DISCARD/");
@@ -1408,71 +1457,145 @@ static void link_layout_sections_scripted(Linker* l, LinkImage* img,
continue;
}
- seg = &img->segments[img->nsegments];
- memset(seg, 0, sizeof(*seg));
- seg->id = seg_id;
- seg->flags = SF_ALLOC | perms;
- seg->vaddr = sec_start_dot;
- seg->paddr = lma_start;
- file_cursor = ALIGN_UP(file_cursor, (u64)PAGE_SIZE);
- seg->file_offset = file_cursor;
- seg->mem_size = mem_size_accum;
- seg->file_size = file_size_accum;
- seg->align = align_max;
- seg->nsections = nsec_in_seg;
- if (os->nphdrs) {
- const KitLinkPhdr* ph = script_find_phdr(script, os->phdrs[0]);
- if (!ph)
- compiler_panic(l->c, SRCLOC_NONE,
- "linker script: unknown PHDR '%.*s'",
- SLICE_ARG(os->phdrs[0]));
- seg->phdr_type = ph->type;
- seg->phdr_filehdr = ph->filehdr ? 1u : 0u;
- seg->phdr_phdrs = ph->phdrs ? 1u : 0u;
- if (ph->flags) {
+ /* (a) Coalesce: if this section's PRIMARY :phdr matches the currently-open
+ * segment and the section is vaddr-contiguous with it, extend that segment
+ * into one PT_LOAD instead of opening a new one. A section listing several
+ * phdrs may still coalesce its primary here; its additional phdrs become
+ * alias program headers below covering just this section's range. FLAGS()
+ * / AT() seal a segment (they override the geometry) and never coalesce. */
+ KitSlice primary_phdr = os->nphdrs ? os->phdrs[0] : KIT_SLICE_NULL;
+ int can_coalesce =
+ open_seg != (u32)-1 && primary_phdr.s && seg_phdr_name[open_seg].s &&
+ slice_eq(seg_phdr_name[open_seg], primary_phdr) &&
+ sec_start_dot == img->segments[open_seg].vaddr +
+ img->segments[open_seg].mem_size &&
+ !os->lma && !os->flags;
+
+ /* Per-section file geometry (used by both the primary placement and the
+ * alias program headers, which describe only this section's range). */
+ u64 this_file_off;
+ u64 this_vaddr = sec_start_dot;
+ u64 this_paddr = lma_start;
+
+ if (can_coalesce) {
+ LinkSegment* oseg = &img->segments[open_seg];
+ u64 prev_file = oseg->file_size;
+ u64 new_file = prev_file + file_size_accum;
+ seg = oseg;
+ this_file_off = seg->file_offset + prev_file;
+ seg->mem_size += mem_size_accum;
+ /* This section's bytes follow the open segment's directly (no page
+ * break); rebase its LinkSections into the open segment. */
+ {
+ u32 fi;
+ for (fi = first_section_idx; fi < img->nsections; ++fi) {
+ LinkSection* ls = &img->sections[fi];
+ ls->segment_id = seg->id;
+ ls->file_offset = this_file_off + ls->file_offset;
+ }
+ }
+ file_cursor += file_size_accum;
+ if (file_size_accum && !l->jit_mode) {
+ u8* grown = (u8*)h->realloc(h, img->segment_bytes[open_seg],
+ img->segment_bytes_cap[open_seg],
+ (size_t)new_file, 16);
+ if (!grown)
+ compiler_panic(img->c, SRCLOC_NONE,
+ "link: oom growing coalesced segment bytes");
+ img->segment_bytes[open_seg] = grown;
+ img->segment_bytes_cap[open_seg] = (size_t)new_file;
+ script_pattern_fill(grown + prev_file, (size_t)file_size_accum,
+ fill_value, fill_width);
+ }
+ seg->file_size = new_file;
+ seg->nsections += nsec_in_seg;
+ } else {
+ seg = &img->segments[img->nsegments];
+ memset(seg, 0, sizeof(*seg));
+ seg->id = seg_id;
+ seg->flags = SF_ALLOC | perms;
+ seg->vaddr = sec_start_dot;
+ seg->paddr = lma_start;
+ file_cursor = ALIGN_UP(file_cursor, (u64)PAGE_SIZE);
+ seg->file_offset = file_cursor;
+ this_file_off = seg->file_offset;
+ seg->mem_size = mem_size_accum;
+ seg->file_size = file_size_accum;
+ seg->align = align_max;
+ seg->nsections = nsec_in_seg;
+ /* Primary phdr names the segment's program-header attributes; the name
+ * is recorded so a following same-phdr contiguous section coalesces. */
+ if (os->nphdrs) {
+ script_apply_phdr(l, img, script, seg, os->phdrs[0], dot, outs,
+ script->nsections);
+ seg_phdr_name[img->nsegments] = os->phdrs[0];
+ } else {
+ seg_phdr_name[img->nsegments] = KIT_SLICE_NULL;
+ }
+ if (os->flags) {
int err = 0;
seg->phdr_flags =
(u32)eval_link_expr(l, img, dot, outs, script->nsections,
- ph->flags, &err);
+ os->flags, &err);
if (err)
compiler_panic(l->c, SRCLOC_NONE,
- "linker script: invalid PHDR FLAGS for '%.*s'",
- SLICE_ARG(ph->name));
+ "linker script: invalid FLAGS for section '%.*s'",
+ SLICE_ARG(os->name));
seg->phdr_flags_set = 1u;
}
- }
- if (os->flags) {
- int err = 0;
- seg->phdr_flags =
- (u32)eval_link_expr(l, img, dot, outs, script->nsections, os->flags,
- &err);
- if (err)
- compiler_panic(l->c, SRCLOC_NONE,
- "linker script: invalid FLAGS for section '%.*s'",
- SLICE_ARG(os->name));
- seg->phdr_flags_set = 1u;
- }
- file_cursor += file_size_accum;
- if (file_size_accum && !l->jit_mode) {
- img->segment_bytes[img->nsegments] =
- (u8*)h->alloc(h, (size_t)file_size_accum, 16);
- if (!img->segment_bytes[img->nsegments])
- compiler_panic(img->c, SRCLOC_NONE,
- "link: oom on scripted segment bytes");
- img->segment_bytes_cap[img->nsegments] = (size_t)file_size_accum;
- script_pattern_fill(img->segment_bytes[img->nsegments],
- (size_t)file_size_accum, fill_value, fill_width);
+ file_cursor += file_size_accum;
+ if (file_size_accum && !l->jit_mode) {
+ img->segment_bytes[img->nsegments] =
+ (u8*)h->alloc(h, (size_t)file_size_accum, 16);
+ if (!img->segment_bytes[img->nsegments])
+ compiler_panic(img->c, SRCLOC_NONE,
+ "link: oom on scripted segment bytes");
+ img->segment_bytes_cap[img->nsegments] = (size_t)file_size_accum;
+ script_pattern_fill(img->segment_bytes[img->nsegments],
+ (size_t)file_size_accum, fill_value, fill_width);
+ }
+ {
+ u32 fi;
+ for (fi = first_section_idx; fi < img->nsections; ++fi) {
+ LinkSection* ls = &img->sections[fi];
+ ls->file_offset = seg->file_offset + (ls->file_offset);
+ }
+ }
+ /* A primary segment (phdr-named, no FLAGS/AT) is coalescable by a
+ * following same-phdr contiguous section. No-phdr / FLAGS / AT segments
+ * are sealed (NULL name never matches a phdr-named follower, matching
+ * the pre-coalesce per-section behavior). */
+ open_seg = (os->nphdrs && !os->flags && !os->lma) ? img->nsegments
+ : (u32)-1;
+ img->nsegments++;
}
+ /* (b) Each ADDITIONAL :phdr the section names yields an alias program
+ * header covering only THIS section's vaddr/file range, with no byte
+ * buffer of its own (the bytes are on disk via the primary; the ELF byte
+ * writer skips NULL-buffer segments). */
{
- u32 fi;
- for (fi = first_section_idx; fi < img->nsections; ++fi) {
- LinkSection* ls = &img->sections[fi];
- ls->file_offset = seg->file_offset + (ls->file_offset);
+ u32 pi;
+ for (pi = 1; pi < os->nphdrs; ++pi) {
+ LinkSegment* aseg = &img->segments[img->nsegments];
+ memset(aseg, 0, sizeof(*aseg));
+ aseg->id = (LinkSegmentId)(img->nsegments + 1u);
+ aseg->flags = SF_ALLOC | perms;
+ aseg->vaddr = this_vaddr;
+ aseg->paddr = this_paddr;
+ aseg->file_offset = this_file_off;
+ aseg->mem_size = mem_size_accum;
+ aseg->file_size = file_size_accum;
+ aseg->align = align_max;
+ aseg->nsections = 0; /* aliases own no sections */
+ script_apply_phdr(l, img, script, aseg, os->phdrs[pi], dot, outs,
+ script->nsections);
+ seg_phdr_name[img->nsegments] = os->phdrs[pi];
+ img->segment_bytes[img->nsegments] = NULL;
+ img->segment_bytes_cap[img->nsegments] = 0;
+ img->nsegments++;
}
}
-
- img->nsegments++;
}
/* Apply any trailing dot assignments after the last output section. */
@@ -1505,6 +1628,8 @@ static void link_layout_sections_scripted(Linker* l, LinkImage* img,
h->free(h, claimed, sizeof(*claimed) * ni);
}
if (outs) h->free(h, outs, sizeof(*outs) * script->nsections);
+ if (seg_phdr_name)
+ h->free(h, seg_phdr_name, sizeof(*seg_phdr_name) * nseg_max);
if (region_cursor)
h->free(h, region_cursor, sizeof(*region_cursor) * script->nregions);
if (load_region_cursor)
diff --git a/src/obj/elf/link.c b/src/obj/elf/link.c
@@ -1643,7 +1643,12 @@ void link_emit_elf(LinkImage* img, Writer* w) {
u32 i;
for (i = 0; i < img->nsegments; ++i) {
const LinkSegment* seg = &img->segments[i];
- if (seg->file_size == 0) continue;
+ /* Alias program headers (a section listed under several :phdrs) have no
+ * byte buffer of their own — their file range already lives on disk via
+ * the primary segment, so skip them here (their phdr still describes the
+ * shared file range). */
+ if (seg->file_size == 0 || img->segment_bytes[seg->id - 1] == NULL)
+ continue;
if (cur_off < seg->file_offset) {
write_zeroes(w, (size_t)(seg->file_offset - cur_off));
cur_off = seg->file_offset;