authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2026-08-13 22:42:01+01:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2026-08-29 12:24:14+01:00
log56c1b0871c396155ca865b4f3531f6a7f17b35a0
tree23c5b23099d5e9d4d31cad681e058a76297a07d4
parent5a625d5f3cb094242e6a310082c74f1b6461d606
signaturelock-open Commit is signed but in an unrecognized format.

Elf2: keep the data segment at the end on SPARC

SPARC has some strange relocations which are PC-relative, but generate an unsigned offset. These relocations are very frequently used to get a pointer to the GOT. As a result, SPARC generally requires that the GOT has a greater virtual address than all function code. Helpfully, this is a requirement which the old ABI neglects to actually define. The SPARC Compliance Definition 2.4.1 includes one "note" in its explanation of code models vaguely alluding to the fact that certain section orderings "may" be necessary; and later (when discussing the GOT) there is an *example* asm snippet which is stated to "assume" that the offset to the GOT is positive. Neither of these is phrased as to impose any particular requirement on the linker, and in fact, I believe both are intended to be non-normative text! Nonetheless, this requirement exists in practice, and we can't really get around it---before this patch, attempting to link any SPARC64 code with `Elf2` would just result in thousands of relocation errors. So let's follow this rule by forcing the "mutable data" segment, which holds the GOT, to be the last segment in the virtual address space, and therefore after all code (which is in the "text" segment). My spidey senses tell me that some other targets will probably end up having some stupid segment ordering requirements too, so I've pulled the check for whether to require this (currently just checking whether the target machine is `EM_SPARCV9`) into its own function returning an enum.

1 files changed, 142 insertions(+), 53 deletions(-)

src/link/Elf2.zig+142-53
...@@ -3558,48 +3558,55 @@ fn initHeaders(...@@ -3558,48 +3558,55 @@ fn initHeaders(
3558 .EXEC, .DYN => {},3558 .EXEC, .DYN => {},
3559 }3559 }
3560 var phnum: u32 = 0;3560 var phnum: u32 = 0;
3561 break :ph .{ .{3561 break :ph .{
3562 .phdr = phndx: {3562 .{
3563 defer phnum += 1;3563 .phdr = phndx: {
3564 break :phndx phnum;3564 defer phnum += 1;
3565 },3565 break :phndx phnum;
3566 .interp = if (maybe_interp) |_| phndx: {3566 },
3567 defer phnum += 1;3567 .interp = if (maybe_interp) |_| phndx: {
3568 break :phndx phnum;3568 defer phnum += 1;
3569 } else undefined,3569 break :phndx phnum;
3570 .rodata = phndx: {3570 } else undefined,
3571 defer phnum += 1;3571 .rodata = phndx: {
3572 break :phndx phnum;3572 defer phnum += 1;
3573 },3573 break :phndx phnum;
3574 .text = phndx: {3574 },
3575 defer phnum += 1;3575 .text = phndx: {
3576 break :phndx phnum;3576 defer phnum += 1;
3577 },3577 break :phndx phnum;
3578 .data = phndx: {3578 },
3579 defer phnum += 1;3579 .plt = if (plt.got_plt == null) phndx: {
3580 break :phndx phnum;3580 defer phnum += 1;
3581 },3581 break :phndx phnum;
3582 .plt = if (plt.got_plt == null) phndx: {3582 } else undefined,
3583 defer phnum += 1;3583 // `data` must be assigned after all other loadable segments so that it has the greatest
3584 break :phndx phnum;3584 // phndx of any loadable segment. This is so that `targetSegmentLoadAddressRestrictions`
3585 } else undefined,3585 // can be obeyed (specifically, the `.data_last` restriction, needed on SPARC).
3586 .tls = if (comp.config.any_non_single_threaded) phndx: {3586 .data = phndx: {
3587 defer phnum += 1;3587 defer phnum += 1;
3588 break :phndx phnum;3588 break :phndx phnum;
3589 } else undefined,3589 },
3590 .dynamic = if (have_dynamic_section) phndx: {3590 .tls = if (comp.config.any_non_single_threaded) phndx: {
3591 defer phnum += 1;3591 defer phnum += 1;
3592 break :phndx phnum;3592 break :phndx phnum;
3593 } else undefined,3593 } else undefined,
3594 .relro = phndx: {3594 .dynamic = if (have_dynamic_section) phndx: {
3595 defer phnum += 1;3595 defer phnum += 1;
3596 break :phndx phnum;3596 break :phndx phnum;
3597 },3597 } else undefined,
3598 .gnu_stack = phndx: {3598 .relro = phndx: {
3599 defer phnum += 1;3599 defer phnum += 1;
3600 break :phndx phnum;3600 break :phndx phnum;
3601 },
3602 .gnu_stack = phndx: {
3603 defer phnum += 1;
3604 break :phndx phnum;
3605 },
3601 },3606 },
3602 }, phnum };3607 // (I don't actually want the trailing comma below, but a `zig fmt` bug forces it.)
3608 phnum,
3609 };
3603 };3610 };
36043611
3605 const expected_nodes_len = @as(usize, if (is_archive) 3 else 0) + // .archive, .archive_header, .archive_elf_footer3612 const expected_nodes_len = @as(usize, if (is_archive) 3 else 0) + // .archive, .archive_header, .archive_elf_footer
...@@ -4539,6 +4546,22 @@ fn initHeaders(...@@ -4539,6 +4546,22 @@ fn initHeaders(
4539 break :str try elf.string(.dynstr, slice);4546 break :str try elf.string(.dynstr, slice);
4540 },4547 },
4541 };4548 };
4549
4550 if (@"type" != .REL) switch (elf.targetSegmentLoadAddressRestrictions()) {
4551 .none => {},
4552 .data_last => switch (elf.phdrSlice()) {
4553 inline else => |phdr| {
4554 // Ensure that the segment after `.data` (if any) is not a loadable segment.
4555 const next_phndx = phndx.data + 1;
4556 if (next_phndx < phdr.len) {
4557 switch (elf.targetLoad(&phdr[next_phndx].type)) {
4558 .NULL, .LOAD => unreachable, // data segment should be the last loadable segment
4559 else => {},
4560 }
4561 }
4562 },
4563 },
4564 };
4542}4565}
45434566
4544pub fn startProgress(elf: *Elf, prog_node: std.Progress.Node) void {4567pub fn startProgress(elf: *Elf, prog_node: std.Progress.Node) void {
...@@ -4888,7 +4911,31 @@ fn targetDynsymHashInfo(elf: *const Elf) DynsymHashInfo {...@@ -4888,7 +4911,31 @@ fn targetDynsymHashInfo(elf: *const Elf) DynsymHashInfo {
4888 // TODO: Alpha and S390x will need to use either `."@4"` or `.@"8"` depending on `elf.identClass()`.4911 // TODO: Alpha and S390x will need to use either `."@4"` or `.@"8"` depending on `elf.identClass()`.
4889 };4912 };
4890}4913}
4891pub fn targetLoad(elf: *const Elf, ptr: anytype) @typeInfo(@TypeOf(ptr)).pointer.child {4914/// Specifies any restrictions the current target has regarding how segments are ordered in the
4915/// virtual address space. Most targets do not have any such restrictions.
4916fn targetSegmentLoadAddressRestrictions(elf: *const Elf) enum {
4917 none,
4918 /// The "mutable data" segment must be the last loadable segment in the virtual address space.
4919 data_last,
4920} {
4921 return switch (elf.ehdrMachine()) {
4922 .AARCH64,
4923 .PPC64,
4924 .RISCV,
4925 .X86_64,
4926 .LOONGARCH,
4927 => .none,
4928
4929 // SPARC uses `R_SPARC_PC{10,22}` relocations to construct pointers to the GOT, but these
4930 // relocations write an *unsigned* PC-relative offset. This cannot even be worked around by
4931 // using a larger code model, because the crt `_start` assembly always uses these specific
4932 // relocations. Therefore, to avoid relocation errors, all code must appear before the GOT
4933 // in the virtual address space. The easiest way for us to do that is to ensure that the
4934 // "mutable data" segment, containing the GOT, is the last segment in the address space.
4935 .SPARCV9 => .data_last,
4936 };
4937}
4938fn targetLoad(elf: *const Elf, ptr: anytype) @typeInfo(@TypeOf(ptr)).pointer.child {
4892 const pointer_ty = @typeInfo(@TypeOf(ptr)).pointer;4939 const pointer_ty = @typeInfo(@TypeOf(ptr)).pointer;
4893 const Child = pointer_ty.child;4940 const Child = pointer_ty.child;
4894 const alignment = pointer_ty.attrs.@"align" orelse @alignOf(Child);4941 const alignment = pointer_ty.attrs.@"align" orelse @alignOf(Child);
...@@ -8114,6 +8161,18 @@ fn allocateSegmentLoadAddress(elf: *Elf, orig_phndx: u32) std.mem.Allocator.Erro...@@ -8114,6 +8161,18 @@ fn allocateSegmentLoadAddress(elf: *Elf, orig_phndx: u32) std.mem.Allocator.Erro
8114 const page_align = elf.targetPageAlign();8161 const page_align = elf.targetPageAlign();
8115 const node_align = segment_ni.alignment(&elf.mf);8162 const node_align = segment_ni.alignment(&elf.mf);
8116 const ph_align = page_align.max(node_align);8163 const ph_align = page_align.max(node_align);
8164
8165 // If we determine that the segment's virtual address needs to move, then it's a good idea to
8166 // make it less likely that it needs to move *again* in the future, because it is expensive to
8167 // change a segment's load address (a lot of re-flushing is necessary). To do that, we reserve
8168 // more virtual address space than we need (multiplying the actual size by this value). That
8169 // way, there will usually be padding between segments which they can grow into.
8170 //
8171 // TODO: we might want to decrease this multiplier, or even omit it entirely, in cases where
8172 // virtual address space is constrained. For instance, 32-bit targets, or targets where short
8173 // PC-relative relocations between segments are common.
8174 const reserve_size_multiplier = 4;
8175
8117 switch (elf.phdrSlice()) {8176 switch (elf.phdrSlice()) {
8118 inline else => |phdr| {8177 inline else => |phdr| {
8119 const offset = elf.targetLoad(&phdr[orig_phndx].offset);8178 const offset = elf.targetLoad(&phdr[orig_phndx].offset);
...@@ -8172,15 +8231,46 @@ fn allocateSegmentLoadAddress(elf: *Elf, orig_phndx: u32) std.mem.Allocator.Erro...@@ -8172,15 +8231,46 @@ fn allocateSegmentLoadAddress(elf: *Elf, orig_phndx: u32) std.mem.Allocator.Erro
8172 // backwards to the start of the page.8231 // backwards to the start of the page.
8173 const next_page_vaddr = std.mem.alignBackward(u64, next_vaddr, page_align.toByteUnits());8232 const next_page_vaddr = std.mem.alignBackward(u64, next_vaddr, page_align.toByteUnits());
81748233
8175 // If we're at the same vaddr we started at, then all we're worried about is the8234 // Check if the segment fits here. We apply `reserve_size_multiplier`, but only if
8176 // segment fitting here. However, if we've already changed our virtual address, then8235 // the segment is already known to be moving---making it easier to grow in-place is
8177 // we might as well try to reserve a bit *more* virtual address space while we're at8236 // the whole point of the multiplier!
8178 // it, because changing virtual address is quite disruptive (we need to re-flush a8237 {
8179 // lot of stuff!) and giving ourselves more space will make it less likely to happen8238 const target_size = if (vaddr == orig_vaddr) size else size * reserve_size_multiplier;
8180 // again.8239 if (vaddr + target_size <= next_page_vaddr) {
8181 const target_size = if (vaddr == orig_vaddr) size else size * 4;8240 break; // hooray, we fit here!
8182 if (vaddr + target_size <= next_page_vaddr) {8241 }
8183 break; // hooray, we fit here!8242 }
8243
8244 const next_ni = elf.phdrs.items[next_phndx].unwrap().?;
8245
8246 // This segment don't fit here, but before deciding how to proceed, we need to
8247 // consider any target-specific restrictions we are subject to.
8248 switch (elf.targetSegmentLoadAddressRestrictions()) {
8249 .none => {},
8250 .data_last => if (next_ni == elf.ni.data) {
8251 // We can't leapfrog over the data segment. Instead, that segment just needs
8252 // to be shifted forwards to make space for us, and we'll then `break` with
8253 // our current vaddr.
8254
8255 if (next_phndx + 1 < phdr.len) switch (elf.targetLoad(&phdr[next_phndx + 1].type)) {
8256 .NULL, .LOAD => unreachable, // data segment should be the last loadable segment
8257 else => {},
8258 };
8259
8260 const free_vaddr = vaddr + size * reserve_size_multiplier;
8261
8262 const next_align = page_align.max(next_ni.alignment(&elf.mf));
8263 const next_offset = elf.targetLoad(&next_ph.offset);
8264 const next_new_vaddr = next_align.forward(free_vaddr) + next_offset % next_align.toByteUnits();
8265
8266 // This logic for updating the data segment's vaddr is identical to how we
8267 // will update the vaddr of `phndx` when we break from the loop.
8268 elf.targetStore(&next_ph.vaddr, @intCast(next_new_vaddr));
8269 elf.targetStore(&next_ph.paddr, @intCast(next_new_vaddr));
8270 try next_ni.childrenMoved(elf.base.comp.gpa, &elf.mf);
8271
8272 break;
8273 },
8184 }8274 }
81858275
8186 // We don't fit here, so shift ourselves forward (i.e. swap with `next_phndx`). But8276 // We don't fit here, so shift ourselves forward (i.e. swap with `next_phndx`). But
...@@ -8192,8 +8282,7 @@ fn allocateSegmentLoadAddress(elf: *Elf, orig_phndx: u32) std.mem.Allocator.Erro...@@ -8192,8 +8282,7 @@ fn allocateSegmentLoadAddress(elf: *Elf, orig_phndx: u32) std.mem.Allocator.Erro
81928282
8193 // Now just swap the phdrs and update our `phndx`.8283 // Now just swap the phdrs and update our `phndx`.
8194 std.mem.swap(@TypeOf(next_ph.*), &phdr[phndx], next_ph);8284 std.mem.swap(@TypeOf(next_ph.*), &phdr[phndx], next_ph);
8195 const next_ni = elf.phdrs.items[next_phndx];8285 elf.phdrs.items[phndx] = .wrap(next_ni);
8196 elf.phdrs.items[phndx] = next_ni;
8197 elf.nodes.items(.data)[@backingInt(next_ni)] = .{ .segment = phndx };8286 elf.nodes.items(.data)[@backingInt(next_ni)] = .{ .segment = phndx };
8198 elf.phdrs.items[next_phndx] = .wrap(segment_ni);8287 elf.phdrs.items[next_phndx] = .wrap(segment_ni);
8199 elf.nodes.items(.data)[@backingInt(segment_ni)] = .{ .segment = @intCast(next_phndx) };8288 elf.nodes.items(.data)[@backingInt(segment_ni)] = .{ .segment = @intCast(next_phndx) };