From f83d28d5c61ae408369be03b0ce3bbbf9c079874 Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Sun, 18 Oct 2026 13:57:23 +0100 Subject: [PATCH] Elf2: rework segment vaddr allocation logic The main goal here was to fix a source of non-determinism leading to unreproducible output binaries. However, I also ended up simplifying the logic considerably by determining that the ability for segments to jump over one another was of little to no practical benefit. This was because by doing so, the segment would intrude on the ability of a different segment to grow in-place; so while it may limit the need for further segment movement right away, it made it more likely to occur in the near future, and these effects largely canceled out. Therefore, the linker now preserves the order of segments at all times. This makes SPARC less of a special case, and might make it slightly easier to design our alternative to linker scripts in the future. --- src/link/Elf2.zig | 395 +++++++++++++++++++++------------------------- 1 file changed, 178 insertions(+), 217 deletions(-) diff --git a/src/link/Elf2.zig b/src/link/Elf2.zig index 3255cad5820a5cbc9fd58e497a482e41ea83e5cb..a6ae8fa69970cfa4060f31adf3516cc58cfae187 100644 --- a/src/link/Elf2.zig +++ b/src/link/Elf2.zig @@ -5376,9 +5376,10 @@ fn initHeaders( defer phnum += 1; break :phndx phnum; } else undefined, - // `data` must be assigned after all other loadable segments so that it has the greatest - // phndx of any loadable segment. This is so that `targetSegmentLoadAddressRestrictions` - // can be obeyed (specifically, the `.data_last` restriction, needed on SPARC). + // `data` must be assigned after all other loadable segments so that it has the last + // phndx of the loadable segments. This causes `updateSegmentLoadAddresses` to put + // that segment's virtual address after the others, which is a de-facto requirement + // on SPARC. More details are in a comment in `updateSegmentLoadAddresses`. .data = phndx: { defer phnum += 1; break :phndx phnum; @@ -5654,25 +5655,10 @@ fn initHeaders( const ElfN = ct_class.ElfN(); const target_endian = elf.targetEndian(); - populate_phdrs: { - // Initially we will give every `PT_LOAD` segment this address. When we re-allocate - // segments in the virtual address space in `flushMoved` and `flushResized`, we will - // move some segments to higher addresses to prevent overlap. This address therefore - // becomes the image's "base address"; i.e. the first `PT_LOAD` segment will start - // at this address. The base address could eventually end up higher than this due to - // how we re-allocate the address space, but never lower. - const base_vaddr: u64 = switch (@"type") { - .REL => break :populate_phdrs, - .DYN => 0, - .EXEC => switch (machine) { - .AARCH64 => 0x200000, - .LOONGARCH => 0x10000, - .PPC64 => 0x10000000, - .RISCV => 0x10000, - .SPARCV9 => 0x100000, - .X86_64 => 0x200000, - }, - }; + if (@"type" != .REL) { + // Initially we will give every `PT_LOAD` segment the base address. They will be + // moved to non-overlapping addresses by `updateSegmentLoadAddresses`. + const base_vaddr: u64 = elf.baseLoadableSegmentVAddr(); // All `PT_LOAD` segments are given this `.@"align"`. However, to avoid bloating the // binary, their *nodes* are not aligned to this boundary---ELF only requires that @@ -6621,21 +6607,7 @@ fn initHeaders( }, }; - if (@"type" != .REL) switch (elf.targetSegmentLoadAddressRestrictions()) { - .none => {}, - .data_last => switch (elf.phdrSlice()) { - inline else => |phdr| { - // Ensure that the segment after `.data` (if any) is not a loadable segment. - const next_phndx = phndx.data + 1; - if (next_phndx < phdr.len) { - switch (elf.targetLoad(&phdr[next_phndx].type)) { - .NULL, .LOAD => unreachable, // data segment should be the last loadable segment - else => {}, - } - } - }, - }, - }; + try elf.updateSegmentLoadAddresses(); } pub fn startProgress(elf: *Elf, prog_node: std.Progress.Node) void { @@ -7035,6 +7007,21 @@ fn ehdrType(elf: *const Elf) EhdrType { } } +fn baseLoadableSegmentVAddr(elf: *const Elf) u64 { + return switch (elf.ehdrType()) { + .REL => unreachable, + .DYN => 0, + .EXEC => switch (elf.ehdrMachine()) { + .AARCH64 => 0x200000, + .LOONGARCH => 0x10000, + .PPC64 => 0x10000000, + .RISCV => 0x10000, + .SPARCV9 => 0x100000, + .X86_64 => 0x200000, + }, + }; +} + fn targetPtrSize(elf: *const Elf) u8 { return elf.identClass().size(); } @@ -7153,30 +7140,6 @@ fn targetDynsymHashInfo(elf: *const Elf) DynsymHashInfo { // TODO: Alpha and S390x will need to use either `."@4"` or `.@"8"` depending on `elf.identClass()`. }; } -/// Specifies any restrictions the current target has regarding how segments are ordered in the -/// virtual address space. Most targets do not have any such restrictions. -fn targetSegmentLoadAddressRestrictions(elf: *const Elf) enum { - none, - /// The "mutable data" segment must be the last loadable segment in the virtual address space. - data_last, -} { - return switch (elf.ehdrMachine()) { - .AARCH64, - .PPC64, - .RISCV, - .X86_64, - .LOONGARCH, - => .none, - - // SPARC uses `R_SPARC_PC{10,22}` relocations to construct pointers to the GOT, but these - // relocations write an *unsigned* PC-relative offset. This cannot even be worked around by - // using a larger code model, because the crt `_start` assembly always uses these specific - // relocations. Therefore, to avoid relocation errors, all code must appear before the GOT - // in the virtual address space. The easiest way for us to do that is to ensure that the - // "mutable data" segment, containing the GOT, is the last segment in the address space. - .SPARCV9 => .data_last, - }; -} fn targetLoad(elf: *const Elf, ptr: anytype) @typeInfo(@TypeOf(ptr)).pointer.child { const pointer_ty = @typeInfo(@TypeOf(ptr)).pointer; const Child = pointer_ty.child; @@ -7606,6 +7569,7 @@ pub fn loadInput(elf: *Elf, input: link.Input) (link.Error || error{BadMagic})!v .{elf.mf.io_err.?}, ), }; + try elf.updateSegmentLoadAddresses(); } fn loadInputInner(elf: *Elf, input: link.Input) (Error || error{BadMagic})!void { const comp = elf.base.comp; @@ -8885,6 +8849,7 @@ pub fn prelink(elf: *Elf, prog_node: std.Progress.Node) link.Error!void { error.MappedFileIo => return diags.fail("failed to write output file: {t}", .{elf.mf.io_err.?}), else => |e| return e, }; + try elf.updateSegmentLoadAddresses(); } fn prelinkInner(elf: *Elf) Error!void { const comp = elf.base.comp; @@ -9038,6 +9003,7 @@ pub fn zcuFilesReady(elf: *Elf, zcu: *Zcu) link.Error!void { .{elf.mf.io_err.?}, ), }; + try elf.updateSegmentLoadAddresses(); } fn zcuFilesReadyInner(elf: *Elf, zcu: *Zcu) Error!void { const gpa = zcu.gpa; @@ -10327,6 +10293,7 @@ pub fn updateNav(elf: *Elf, pt: Zcu.PerThread, nav_index: InternPool.Nav.Index) .{elf.mf.io_err.?}, ), }; + try elf.updateSegmentLoadAddresses(); } fn updateNavInner(elf: *Elf, pt: Zcu.PerThread, nav_index: InternPool.Nav.Index) Error!void { const comp = elf.base.comp; @@ -10416,6 +10383,7 @@ pub fn updateContainerType( .{elf.mf.io_err.?}, ), }; + try elf.updateSegmentLoadAddresses(); } fn updateContainerTypeInner( elf: *Elf, @@ -10557,6 +10525,7 @@ pub fn updateFunc( .{elf.mf.io_err.?}, ), }; + try elf.updateSegmentLoadAddresses(); } fn updateFuncInner( elf: *Elf, @@ -10804,6 +10773,7 @@ pub fn lostTracking( elf.mf.io_err.?, }), }; + try elf.updateSegmentLoadAddresses(); } pub fn updateErrorData(elf: *Elf, pt: Zcu.PerThread) link.Error!void { @@ -10813,6 +10783,7 @@ pub fn updateErrorData(elf: *Elf, pt: Zcu.PerThread) link.Error!void { }); if (elf.dwarf.const_pool.getIfExists(.anyerror_type)) |cpi| try elf.updateConstInner(pt, cpi, .anyerror_type, .complete); + try elf.updateSegmentLoadAddresses(); } pub fn flush( @@ -10860,6 +10831,8 @@ fn flushInner( try elf.prepareDynamic(); + try elf.updateSegmentLoadAddresses(); + while (try elf.idle()) {} assert(elf.input_pending_index == elf.inputs.items.len); @@ -11566,9 +11539,10 @@ fn flushMoved(elf: *Elf, ni: MappedFile.Node.Index) std.mem.Allocator.Error!void else => unreachable, .NULL, .LOAD => { - try elf.allocateSegmentLoadAddress(phndx); - // Do not bubble moved---our child nodes only care if our virtual - // address changes, which is handled by `updateSegmentLoadAddress`. + // Our address changes are handled by `updateSegmentLoadAddresses`, so + // there's nothing we need to do here. Don't bubble moved, because our + // child nodes only care if our virtual address changes, which is + // handled by that other function. return; }, @@ -11987,166 +11961,156 @@ fn flushMovedNobitsSymbols( } } -/// Updates `vaddr` and `paddr` of the segment header, and marks everything in the segment as moved. -fn updateSegmentLoadAddress( - elf: *Elf, - segment_ni: MappedFile.Node.Index, - new_vaddr: u64, -) std.mem.Allocator.Error!void { - const phndx = elf.getNode(segment_ni).segment; - - switch (elf.phdrSlice()) { - inline else => |phdr| { - assert(new_vaddr != elf.targetLoad(&phdr[phndx].vaddr)); - elf.targetStore(&phdr[phndx].vaddr, @intCast(new_vaddr)); - elf.targetStore(&phdr[phndx].paddr, @intCast(new_vaddr)); - }, - } - - try segment_ni.childrenMoved(elf.base.comp.gpa, &elf.mf); - if (segment_ni == elf.ni.data) elf.bss_moved = true; -} - -/// Given the index of a `PT_LOAD`/`PT_NULL` segment, assumes that the phdr's `offset` and `filesz` -/// have been updated as needed by the caller, and updates the `@"align"`, `vaddr`, `paddr`, and -/// `memsz` fields of the segment, in order to place it at a valid virtual address. +/// If any loadable segments have invalid virtual addresses, assigns them new valid addresses and +/// queues the necessary idle tasks (e.g. re-applying relocations targeting those segments). /// -/// TODO: this function is currently a source of non-determinism in the linker, because handling the -/// moving or resizing of a segment could reorder them and thereby affect how we handle *future* -/// changes to segments. -fn allocateSegmentLoadAddress(elf: *Elf, orig_phndx: u32) std.mem.Allocator.Error!void { - const segment_ni = elf.phdrs.items[orig_phndx].unwrap().?; - assert(elf.getNode(segment_ni).segment == orig_phndx); +/// This function must be called at the end of every link task in case tghe task invalidated segment +/// addresses (which can happen in response to any node movement/resize, or any section resize). It +/// cannot be called from `idle` as this function does not have the required idempotency guarantee +/// so calling it non-deterministically it would lead to unreproducible output files. +fn updateSegmentLoadAddresses(elf: *Elf) std.mem.Allocator.Error!void { + if (elf.ehdrType() == .REL) return; + const page_align = elf.targetPageAlign(); - const node_align = segment_ni.alignment(&elf.mf); - const ph_align = page_align.max(node_align); - - // If we determine that the segment's virtual address needs to move, then it's a good idea to - // make it less likely that it needs to move *again* in the future, because it is expensive to - // change a segment's load address (a lot of re-flushing is necessary). To do that, we reserve - // more virtual address space than we need (multiplying the actual size by this value). That - // way, there will usually be padding between segments which they can grow into. - // - // TODO: we might want to decrease this multiplier, or even omit it entirely, in cases where - // virtual address space is constrained. For instance, 32-bit targets, or targets where short - // PC-relative relocations between segments are common. - const reserve_size_multiplier = 4; switch (elf.phdrSlice()) { inline else => |phdr| { - const offset = elf.targetLoad(&phdr[orig_phndx].offset); - const filesz = elf.targetLoad(&phdr[orig_phndx].filesz); - - const orig_vaddr = elf.targetLoad(&phdr[orig_phndx].vaddr); - assert(elf.targetLoad(&phdr[orig_phndx].paddr) == orig_vaddr); - - const bss_size = if (segment_ni == elf.ni.data) - elf.shndx.bss.size(elf) - else - 0; - const memsz = filesz + bss_size; - - var vaddr: u64 = orig_vaddr; - - elf.targetStore(&phdr[orig_phndx].@"align", @intCast(ph_align.toByteUnits())); - elf.targetStore(&phdr[orig_phndx].memsz, @intCast(memsz)); - elf.targetStore(&phdr[orig_phndx].type, if (memsz > 0) .LOAD else .NULL); - - // First, we will shift the virtual address as needed in order to maintain the required - // property that vaddr is congruent to offset modulo the phdr alignment. - { - // Compute the candidate address by undoing the current offset and then re-offsetting - vaddr = std.mem.alignBackward(u64, vaddr, ph_align.toByteUnits()) + offset % ph_align.toByteUnits(); - // If `node_align` is greater than `page_align`, the address we just set might be in - // the previous segment. The first page we "own" is the one in which the old vaddr - // resides, so check against that. - const first_good_vaddr = std.mem.alignBackward(u64, orig_vaddr, page_align.toByteUnits()); - if (vaddr < first_good_vaddr) { - // Yep, we crossed into the previous segment's pages, so correct for that by - // offsetting our address by another `ph_align`. - vaddr += ph_align.toByteUnits(); - assert(vaddr >= first_good_vaddr); + // Somewhere in the phdrs is a contiguous sequence of all "loadable" segments (segments + // with a `type` of `PT_NULL` or `PT_LOAD`); find the start of that sequence. In theory + // we could memoize this, but there's no real need since the number of phdrs is tiny. + const load_phdrs_start = for (phdr, 0..) |*ph, phndx| { + switch (elf.targetLoad(&ph.type)) { + .NULL, .LOAD => break phndx, + else => continue, } - } + } else unreachable; // we always create loadable (`PT_NULL`/`PT_LOAD`) segments - // If our size has changed, or if the address shift above caused our "end" address to - // cross a page boundary, then we might be overlapping with the next segment's pages. In - // that case, we will jump past that segment and give ourselves a new address after it. - // We'll need to repeat this for every loadable phdr after us, until we're no longer - // overlapping anything. - var phndx = orig_phndx; - for (phdr[orig_phndx + 1 ..], orig_phndx + 1..) |*next_ph, next_phndx| { - switch (elf.targetLoad(&next_ph.type)) { + // Update these fields for each loadable segment: + // + // * `memsz` + // * `type` (`PT_NULL` vs `PT_LOAD`) + // * `align` + // * `offset` (in theory this should happen in `updateElfOffset`, but this logic needs + // to track that value, so it's easier to just populate it here instead) + // + // ...and check if the current `vaddr` values for the loadable segments are valid (i.e. + // compatible with the segments' file offsets and address ranges do not overlap). + var vaddrs_valid: bool = true; + var first_free_vaddr: u64 = elf.baseLoadableSegmentVAddr(); + const load_phdrs_end: usize = for ( + phdr[load_phdrs_start..], + elf.phdrs.items[load_phdrs_start..], + load_phdrs_start.., + ) |*ph, segment_oni, phndx| { + switch (elf.targetLoad(&ph.type)) { .NULL, .LOAD => {}, - else => { - // All loadable segments have contiguous indices, so this indicates we have - // become the last loadable segment, meaning we definitely don't overlap any - // other loadable segment. - break; - }, + else => break phndx, // reached end of loadable segments } + const segment_ni = segment_oni.unwrap().?; // all loadable segments have nodes - const next_vaddr = elf.targetLoad(&next_ph.vaddr); - // Find the first virtual address which the next phdr "owns" by aligning its vaddr - // backwards to the start of the page. - const next_page_vaddr = std.mem.alignBackward(u64, next_vaddr, page_align.toByteUnits()); + // We need to know the segment's file offset here anyway, so we may as well just + // populate the phdr `offset` field now instead of in `flushElfOffset`. + const ph_offset = elf.computeNodeElfOffset(segment_ni); + elf.targetStore(&ph.offset, @intCast(ph_offset)); - // Check if the segment fits here. We apply `reserve_size_multiplier`, but only if - // the segment is already known to be moving---making it easier to grow in-place is - // the whole point of the multiplier! - { - const target_size = if (vaddr == orig_vaddr) memsz else memsz * reserve_size_multiplier; - if (vaddr + target_size <= next_page_vaddr) { - break; // hooray, we fit here! + // The alignment of the segment affects which virtual addresses are valid. + const ph_align = segment_ni.alignment(&elf.mf).max(page_align); + elf.targetStore(&ph.@"align", @intCast(ph_align.toByteUnits())); + + // The size of the segment in memory is equal to the size of the segment in the file + // plus the size of any `SHT_NOBITS` section(s) at the end of the segment. + const ph_memsz: u64 = memsz: { + _, const file_size: u64 = segment_ni.location(&elf.mf).resolve(&elf.mf); + if (segment_ni == elf.ni.data) { + break :memsz file_size + elf.shndx.bss.size(elf); + } else { + break :memsz file_size; } + }; + elf.targetStore(&ph.memsz, @intCast(ph_memsz)); + elf.targetStore(&ph.type, if (ph_memsz == 0) .NULL else .LOAD); + + // Check if the existing virtual address of the segment is valid. + const ph_vaddr = elf.targetLoad(&ph.vaddr); + assert(elf.targetLoad(&ph.paddr) == ph_vaddr); + if (ph_vaddr < first_free_vaddr or + ph_offset % ph_align.toByteUnits() != ph_vaddr % ph_align.toByteUnits()) + { + @branchHint(.unlikely); + vaddrs_valid = false; } - const next_ni = elf.phdrs.items[next_phndx].unwrap().?; + // The next segment's pages must not overlap our own. + first_free_vaddr = page_align.forward(ph_vaddr + ph_memsz); + } else phdr.len; - // This segment don't fit here, but before deciding how to proceed, we need to - // consider any target-specific restrictions we are subject to. - switch (elf.targetSegmentLoadAddressRestrictions()) { - .none => {}, - .data_last => if (next_ni == elf.ni.data) { - // We can't leapfrog over the data segment. Instead, that segment just needs - // to be shifted forwards to make space for us, and we'll then `break` with - // our current vaddr. + // There should be no more loadable segments after the contiguous range. + for (phdr[load_phdrs_end..]) |*ph| switch (elf.targetLoad(&ph.type)) { + .NULL, .LOAD => unreachable, // non-contiguous loadable segment + else => {}, + }; - if (next_phndx + 1 < phdr.len) switch (elf.targetLoad(&phdr[next_phndx + 1].type)) { - .NULL, .LOAD => unreachable, // data segment should be the last loadable segment - else => {}, - }; - - const free_vaddr = vaddr + memsz * reserve_size_multiplier; - - const next_align = page_align.max(next_ni.alignment(&elf.mf)); - const next_offset = elf.targetLoad(&next_ph.offset); - const next_new_vaddr = next_align.forward(free_vaddr) + next_offset % next_align.toByteUnits(); - - try elf.updateSegmentLoadAddress(next_ni, next_new_vaddr); - - break; - }, - } - - // We don't fit here, so shift ourselves forward (i.e. swap with `next_phndx`). But - // first we need to adjust `vaddr` to come after it. - const next_size = elf.targetLoad(&next_ph.memsz); - // Instead of putting ourselves right after `next_ph`, we'll go a bit later in the - // address space so that `next_ph` has address space to grow into (like above). - vaddr = ph_align.forward(@intCast(next_vaddr + next_size * 4)) + offset % ph_align.toByteUnits(); - - // Now just swap the phdrs and update our `phndx`. - std.mem.swap(@TypeOf(next_ph.*), &phdr[phndx], next_ph); - elf.phdrs.items[phndx] = .wrap(next_ni); - elf.nodes.items(.data)[@backingInt(next_ni)] = .{ .segment = phndx }; - elf.phdrs.items[next_phndx] = .wrap(segment_ni); - elf.nodes.items(.data)[@backingInt(segment_ni)] = .{ .segment = @intCast(next_phndx) }; - phndx = @intCast(next_phndx); + if (vaddrs_valid) { + @branchHint(.likely); + return; } - if (vaddr != orig_vaddr) { - try elf.updateSegmentLoadAddress(segment_ni, vaddr); + // It's time to actually allocate virtual addresses for all segments. Our current + // strategy for this preserves the order of segments in memory, but this isn't a hard + // requirement in general. + // + // However, a warning! If this logic *is* ever changed to re-order segments, it may need + // to abide by target-specific restrictions. For instance, on SPARC, because the GOT is + // usually accessed with the unsigned relocations `R_SPARC_PC{10,22}`, it is effectively + // a requirement for all code to have a lower virtual address than the GOT, which means + // that the "text" segment must be ordered before the "data" segment on that target. + + const gpa = elf.base.comp.gpa; + + // If we determine that a segment's virtual address needs to move, then it's a good idea + // to space it out a bit from the surrounding segments, making it less likely that more + // segment moves are necessary in the future (because every segment movement invalidates + // a lot of idle work). We do that by multiplying each segment's size by this multiplier + // and treating that amount of space as "reserved" for the segment to grow into. + // + // TODO: we might want to decrease this multiplier, or even omit it entirely, in cases + // where the virtual address space is constrained; for instance, 32-bit targets, or + // targets where short PC-relative relocations between segments are common. + const reserve_size_multiplier = 4; + + first_free_vaddr = elf.baseLoadableSegmentVAddr(); + var preferred_vaddr = first_free_vaddr; + + for (load_phdrs_start..load_phdrs_end) |phndx| { + const ph_align: Alignment = .fromByteUnits(elf.targetLoad(&phdr[phndx].@"align")); + assert(ph_align.compare(.gte, page_align)); + const ph_offset = elf.targetLoad(&phdr[phndx].offset); + const ph_memsz = elf.targetLoad(&phdr[phndx].memsz); + + // If the existing address is valid, then don't do anything. + const ph_vaddr: u64 = elf.targetLoad(&phdr[phndx].vaddr); + if (ph_vaddr >= first_free_vaddr and + ph_vaddr % ph_align.toByteUnits() == ph_offset % ph_align.toByteUnits()) + { + // The current virtual address is already valid, so just stay put. Perhaps the + // next segment will have to move, but the only alternative was *us* moving! + first_free_vaddr = page_align.forward(ph_vaddr + ph_memsz); + preferred_vaddr = page_align.forward(ph_vaddr + ph_memsz * reserve_size_multiplier); + continue; + } + + // Otherwise, put ourselves at a new address just after the previous segment. + const new_vaddr = ph_align.forward(preferred_vaddr) + ph_offset % ph_align.toByteUnits(); + assert(new_vaddr != ph_vaddr); // `ph_vaddr` was not valid + + elf.targetStore(&phdr[phndx].vaddr, @intCast(new_vaddr)); + elf.targetStore(&phdr[phndx].paddr, @intCast(new_vaddr)); + const segment_ni = elf.phdrs.items[phndx].unwrap().?; + try segment_ni.childrenMoved(gpa, &elf.mf); + if (segment_ni == elf.ni.data) elf.bss_moved = true; + + first_free_vaddr = page_align.forward(new_vaddr + ph_memsz); + preferred_vaddr = page_align.forward(new_vaddr + ph_memsz * reserve_size_multiplier); } }, } @@ -12184,7 +12148,6 @@ fn flushResized(elf: *Elf, ni: MappedFile.Node.Index) std.mem.Allocator.Error!vo else => unreachable, .NULL, .LOAD => { elf.targetStore(&ph.filesz, @intCast(size)); - try elf.allocateSegmentLoadAddress(phndx); if (ni == elf.ni.data) elf.bss_moved = true; }, .DYNAMIC, .INTERP, .PHDR, .GNU_EH_FRAME, .GNU_RELRO => { @@ -12197,7 +12160,11 @@ fn flushResized(elf: *Elf, ni: MappedFile.Node.Index) std.mem.Allocator.Error!vo elf.targetStore(&ph.filesz, @intCast(size)); - // Our memsz must include the `.tbss` section. + // Our memsz must include the `.tbss` section. After adding that in, we will + // realign to the TLS segment alignment. This is not strictly required, but + // the TLS runtime will pad the module's TLS block to the segment alignment + // anyway, so it's simpler for us to just make the memsz aligned so that our + // memsz matches the size of the module's TLS block. const tls_align: Alignment = .fromByteUnits(elf.targetLoad(&ph.@"align")); elf.targetStore(&ph.memsz, @intCast( tls_align.forward(size + elf.shndx.tbss.size(elf)), @@ -12948,6 +12915,7 @@ pub fn updateExports( }; } try elf.genPending(pt); + try elf.updateSegmentLoadAddresses(); } fn updateExportInner(elf: *Elf, export_index: Zcu.Export.Index) Error!void { const zcu = elf.base.comp.zcu.?; @@ -13122,15 +13090,8 @@ fn ensureSegmentAligned(elf: *Elf, start_phndx: u32, min_align: Alignment) Error switch (elf.phdrSlice()) { inline else => |phdr| switch (elf.targetLoad(&phdr[phndx].type)) { .NULL, .LOAD => { - // The `@"align"` field is managed by `allocateSegmentLoadAddress`. - // - // It's very likely that the node was moved and/or resized when we realigned it - // just above, but it is possible that it was not moved *but* still has an - // unaligned virtual address. In that case, we need to ensure the segment's - // virtual address range will be recomputed. - if (!min_align.check(@intCast(elf.targetLoad(&phdr[phndx].vaddr)))) { - try seg_ni.moved(gpa, &elf.mf); - } + // The `@"align"` field is managed by `updateSegmentLoadAddresses`, which will + // be called at the end of the current link task. }, else => elf.targetStore(&phdr[phndx].@"align", @intCast(@max( elf.targetLoad(&phdr[phndx].@"align"), -- 2.54.0