From 8f183c8c185f43db596b6aac1983f97403503592 Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Sat, 17 Oct 2026 12:34:16 +0100 Subject: [PATCH 01/18] MappedFile: slight tweak to node deletion This strategy is a bit simpler and lets us reuse nodes more quickly, at the cost of a single bit (of which we had 17 spare anyway). --- src/link/Coff.zig | 2 +- src/link/Elf2.zig | 2 +- src/link/MachO2.zig | 1 + src/link/MappedFile.zig | 75 +++++++++++++++++++++++++++++------------ 4 files changed, 56 insertions(+), 24 deletions(-) diff --git a/src/link/Coff.zig b/src/link/Coff.zig index 5fede2640f4d8056ca5a5e1163b1a6a0ab1ea253..3b65bdb3b43d9ad78e496030ccf0ade0e864fbca 100644 --- a/src/link/Coff.zig +++ b/src/link/Coff.zig @@ -6100,7 +6100,7 @@ pub fn idle(coff: *Coff) !bool { break :task; } while (coff.mf.updates.pop()) |ni| : (coff.mf.update_prog_node.completeOne()) { - if (ni.pendingDelete(&coff.mf)) continue; + ni.cleanUpdated(&coff.mf); const clean_moved = ni.cleanMoved(&coff.mf); const clean_resized = ni.cleanResized(&coff.mf); const clean_next_moved = ni.cleanNextMoved(&coff.mf); diff --git a/src/link/Elf2.zig b/src/link/Elf2.zig index 2b68bb4683429019083d9cfccb3b05d991a054c4..0f7e44d1d559618370a0d032e2c40525a61b1e78 100644 --- a/src/link/Elf2.zig +++ b/src/link/Elf2.zig @@ -10857,7 +10857,7 @@ pub fn idle(elf: *Elf) link.Error!bool { break :task; } while (elf.mf.updates.pop()) |ni| : (elf.mf.update_prog_node.completeOne()) { - if (ni.pendingDelete(&elf.mf)) continue; + ni.cleanUpdated(&elf.mf); const clean_moved = ni.cleanMoved(&elf.mf); const clean_resized = ni.cleanResized(&elf.mf); const clean_next_moved = ni.cleanNextMoved(&elf.mf); diff --git a/src/link/MachO2.zig b/src/link/MachO2.zig index 96993fd0e9239572d1dbe04c295e51344009acd3..17e7d97d3c8916be857f5279bfde7607dc590134 100644 --- a/src/link/MachO2.zig +++ b/src/link/MachO2.zig @@ -3342,6 +3342,7 @@ pub fn idle(macho: *MachO) link.Error!bool { } while (macho.mf.updates.pop()) |ni| { + ni.cleanUpdated(&macho.mf); const clean_moved = ni.cleanMoved(&macho.mf); const clean_resized = ni.cleanResized(&macho.mf); const clean_next_moved = ni.cleanNextMoved(&macho.mf); diff --git a/src/link/MappedFile.zig b/src/link/MappedFile.zig index 5db69a96c00af9a0a6ad9fc0f987d5a37fab3b9e..e455ba232dc46dac3c3d54e8e57209486ccaa13e 100644 --- a/src/link/MappedFile.zig +++ b/src/link/MappedFile.zig @@ -207,6 +207,7 @@ pub fn init(file: Io.File, gpa: Allocator, io: Io) (Allocator.Error || Io.Cancel .resized = false, .next_moved = false, .has_content = false, + .updated = false, }, .location_payload = switch (root_location) { .small => |small| .{ .small = small }, @@ -287,7 +288,14 @@ pub const Node = extern struct { next_moved: bool, /// Whether this node might contain initialized bytes. has_content: bool, - unused: u17 = 0, + + /// Whether this node is currently in `updates`. Usually this is just equal to whether any + /// of `moved`, `resized`, or `next_moved` is set, but when a node is deleted and later + /// reused, it may still be in `updates`, so preserving this flag through the deletion + /// prevents us from adding it to `updates` a second time. + updated: bool, + + unused: u16 = 0, }; pub const Location = union(enum(u1)) { @@ -494,6 +502,11 @@ pub const Node = extern struct { } } + pub fn cleanUpdated(ni: Node.Index, mf: *MappedFile) void { + assert(ni.get(mf).flags.updated); + ni.get(mf).flags.updated = false; + } + pub fn hasMoved(ni: Node.Index, mf: *const MappedFile) bool { var cur_ni = ni; while (cur_ni != .root) { @@ -518,9 +531,11 @@ pub const Node = extern struct { if (node.prev.unwrap()) |prev_ni| prev_ni.nextMovedAssumeCapacity(mf); if (ni.hasMoved(mf)) return; node.flags.moved = true; - if (node.flags.resized or node.flags.next_moved) return; - mf.updates.appendAssumeCapacity(ni); - mf.update_prog_node.increaseEstimatedTotalItems(1); + if (!node.flags.updated) { + node.flags.updated = true; + mf.updates.appendAssumeCapacity(ni); + mf.update_prog_node.increaseEstimatedTotalItems(1); + } } pub fn hasResized(ni: Node.Index, mf: *const MappedFile) bool { @@ -539,9 +554,11 @@ pub const Node = extern struct { const node = ni.get(mf); if (node.flags.resized) return; node.flags.resized = true; - if (node.flags.moved or node.flags.next_moved) return; - mf.updates.appendAssumeCapacity(ni); - mf.update_prog_node.increaseEstimatedTotalItems(1); + if (!node.flags.updated) { + node.flags.updated = true; + mf.updates.appendAssumeCapacity(ni); + mf.update_prog_node.increaseEstimatedTotalItems(1); + } } pub fn hasNextMoved(ni: Node.Index, mf: *const MappedFile) bool { @@ -560,9 +577,11 @@ pub const Node = extern struct { const node = ni.get(mf); if (!node.flags.enable_next_moved or node.flags.next_moved) return; node.flags.next_moved = true; - if (node.flags.moved or node.flags.resized) return; - mf.updates.appendAssumeCapacity(ni); - mf.update_prog_node.increaseEstimatedTotalItems(1); + if (!node.flags.updated) { + node.flags.updated = true; + mf.updates.appendAssumeCapacity(ni); + mf.update_prog_node.increaseEstimatedTotalItems(1); + } } pub fn alignment(ni: Node.Index, mf: *const MappedFile) Alignment { @@ -647,19 +666,30 @@ pub const Node = extern struct { pub fn delete(ni: Node.Index, gpa: Allocator, mf: *MappedFile) Allocator.Error!void { const node = ni.get(mf); assert(node.first == .none and node.last == .none); // has children - mf.removeNodesFromChildList(gpa, ni, ni); - const updated = node.flags.moved or node.flags.resized or node.flags.next_moved; - node.* = undefined; - node.next = ni.toOptional(); - if (!updated) assert(ni.pendingDelete(mf)); - } - - pub fn pendingDelete(ni: Node.Index, mf: *MappedFile) bool { - const node = ni.get(mf); - if (node.next != ni.toOptional()) return false; - node.next = mf.free_ni; + try mf.removeNodesFromChildList(gpa, ni, ni); + node.* = .{ + .parent = undefined, + .prev = undefined, + .next = mf.free_ni, + .first = undefined, + .last = undefined, + .flags = .{ + .position = .floating, + .alignment = .@"1", + .bubbles_moved = false, + .enable_next_moved = false, + .location_tag = .small, + .moved = false, + .resized = false, + .next_moved = false, + .has_content = false, + // It is important that we preserve this flag! When the node gets reused, this + // prevents it being redundantly added to `mf.updates` if it is already there. + .updated = node.flags.updated, + }, + .location_payload = undefined, + }; mf.free_ni = ni.toOptional(); - return true; } /// Ensures that the size of `ni` is at least `min_size`. Valid for any node. @@ -946,6 +976,7 @@ fn addNode(mf: *MappedFile, gpa: Allocator, opts: struct { .resized = false, .next_moved = false, .has_content = false, + .updated = false, }, .location_payload = switch (location) { .small => |small| .{ .small = small }, -- 2.54.0 From 5be6c62845638e6983b98f2c0c59d3a31a865b65 Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Sat, 17 Oct 2026 12:37:00 +0100 Subject: [PATCH 02/18] Elf2: delete unused `.copied_global` nodes --- src/link/Elf2.zig | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/src/link/Elf2.zig b/src/link/Elf2.zig index 0f7e44d1d559618370a0d032e2c40525a61b1e78..9c494d906183c644cd45d1740d20d229e6adce82 100644 --- a/src/link/Elf2.zig +++ b/src/link/Elf2.zig @@ -308,9 +308,6 @@ const Node = union(enum) { /// emitted a copy relocation. /// /// TODO it would be better to emit these into `.bss` or `.bss.rel.ro`, once we support those. - /// - /// TODO: currently, the `elf.copied_globals` entry may not be there---this case exists because - /// `MappedFile` does not (yet?) support deleting nodes. See logic in `updateGlobalDynamic`. copied_global: Symbol.Global.Index, /// May contain relocations. nav: NavMapIndex, @@ -3335,10 +3332,8 @@ fn updateGlobalDynamic(elf: *Elf, orig_gsi: Symbol.Global.Index, force_apply_rel } else { if (elf.copied_globals.fetchSwapRemove(gsi)) |copied_global_kv| { // We previously made a copy relocation, but now have deemed it unnecessary. + try elf.deleteNode(copied_global_kv.value.node); elf.shndx.rela_dyn.relaDeleteOne(elf, copied_global_kv.value.rela_index); - // TODO: once `MappedFile` has a way to delete a node (so it can re-use the - // space), we should delete `copied_global_kv.value.node`, which is an - // "orphaned" `copied_global` node. apply_relocs = true; } } @@ -3613,10 +3608,8 @@ fn deleteOwnedDynsym(elf: *Elf, gsi: Symbol.Global.Index) std.mem.Allocator.Erro var apply_relocs: bool = false; if (elf.copied_globals.fetchSwapRemove(gsi)) |copied_global_kv| { + try elf.deleteNode(copied_global_kv.value.node); elf.shndx.rela_dyn.relaDeleteOne(elf, copied_global_kv.value.rela_index); - // TODO: once `MappedFile` has a way to delete a node (so it can re-use the - // space), we should delete `copied_global_kv.value.node`, which is an - // "orphaned" `copied_global` node. apply_relocs = true; } if (elf.plt.getIndex(gsi)) |plt_index| { @@ -11444,11 +11437,7 @@ fn flushMoved(elf: *Elf, ni: MappedFile.Node.Index) std.mem.Allocator.Error!void }); }, .copied_global => |global_name| { - const copied_global = elf.copied_globals.getPtr(global_name) orelse { - // TODO: this node is orphaned, which is possible because `MappedFile` does not yet - // support deleting nodes. See logic in `updateGlobalDynamic`. - return; - }; + const copied_global = elf.copied_globals.getPtr(global_name).?; assert(copied_global.node == ni); const new_addr = elf.computeNodeVAddr(ni); @@ -13008,11 +12997,9 @@ pub fn addNodeAssumeCapacity(elf: *Elf, ni: MappedFile.Node.Index, node: Node) M return ni; } -fn deleteNode(elf: *Elf, node: *MappedFile.Node.Index.Optional) std.mem.Allocator.Error!void { - const ni = node.unwrap().?; - try ni.delete(elf.base.comp.gpa, &elf.mf); - elf.nodes.set(@backingInt(ni), .deleted); - node.* = .none; +fn deleteNode(elf: *Elf, node: MappedFile.Node.Index) std.mem.Allocator.Error!void { + try node.delete(elf.base.comp.gpa, &elf.mf); + elf.nodes.set(@backingInt(node), .deleted); } /// If `sym` has a PLT entry, returns the address of that entry (specifically, the address which a -- 2.54.0 From cebeab2671e98de0d3c1d6677ebfa56c5e253077 Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Sat, 17 Oct 2026 12:42:25 +0100 Subject: [PATCH 03/18] Elf2: remove unhelpful TODOs The whole linker suffers from this bug right now (where it naively assumes it will never itself trigger overflow of the address space), we don't need TODOs for it in one specific target machine's code path. --- src/link/Elf2.zig | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/link/Elf2.zig b/src/link/Elf2.zig index 9c494d906183c644cd45d1740d20d229e6adce82..cd911832b05903b1af2e204567a02bb4f1f6e3ff 100644 --- a/src/link/Elf2.zig +++ b/src/link/Elf2.zig @@ -12469,7 +12469,6 @@ fn ensurePltEntry(elf: *Elf, gsi: Symbol.Global.Index) Error!bool { // relocate the PLT entry to point to the .GOT.PLT entry const got_plt_abs = got_plt_section.vaddr(elf) + got_plt_offset; - // TODO: handle overflow gracefully const inst0: *align(1) link.loongarch.J20 = @ptrCast(plt_slice[0..4]); const inst1: *align(1) link.loongarch.K12 = @ptrCast(plt_slice[4..8]); elf.targetStore(inst0, .{ @@ -12635,7 +12634,6 @@ fn flushMovedPltSection(elf: *Elf, which: enum { plt, plt_sec, got_plt }, old_ad const target_slice = plt_slice[plt_offset..]; const got_plt_abs: u64 = got_plt_addr + got_plt_offset; - // TODO: handle overflow gracefully const inst0: *align(1) link.loongarch.J20 = @ptrCast(target_slice[0..4]); const inst1: *align(1) link.loongarch.K12 = @ptrCast(target_slice[4..8]); -- 2.54.0 From ae7911ac25dfa12cd194c3801f996510824848d1 Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Sat, 17 Oct 2026 14:22:30 +0100 Subject: [PATCH 04/18] Elf2: fix `flushMoved` bubbling logic When a loadable segment encounters a "moved" event, it should not propagate that event to child nodes, because they only care about virtual addresses changing and movement of segments in the file does not necessarily trigger a change in virtual address. The same goes for a moved event on a section without `SHF_ALLOC`: the "virtual addresses" of nodes within the section are actually just offsets into the section, so there's no need to ever propagate the event to them. This logic was correct in the past, but it unfortunately looks like I unintentionally regressed it in my early days of working on this linker. This commit fixes that regression, which results in redundant work being avoided and therefore speeds up linking. --- src/link/Elf2.zig | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/link/Elf2.zig b/src/link/Elf2.zig index cd911832b05903b1af2e204567a02bb4f1f6e3ff..9fc4213ff1e4112f108a910b948e5491f8620279 100644 --- a/src/link/Elf2.zig +++ b/src/link/Elf2.zig @@ -5207,7 +5207,7 @@ fn initHeaders( // Must be at least `addr_align` for `elf.ni.phdr` to be placed inside this node .alignment = node_block_align.max(addr_align), .moved = true, - .bubbles_moved = false, + .bubbles_moved = false, // see loadable segment handling in `flushMoved` }), .{ .segment = phndx.rodata }); elf.phdrs.items[phndx.rodata] = .wrap(elf.ni.rodata); @@ -5216,14 +5216,13 @@ fn initHeaders( .alignment = addr_align, // keep in sync with `elf.ni.rodata` alignment above .moved = true, .resized = true, - .bubbles_moved = false, }), .{ .segment = phndx.phdr }); elf.phdrs.items[phndx.phdr] = .wrap(elf.ni.phdr); elf.ni.text = elf.addNodeAssumeCapacity(try elf.ni.elf.addFloatingChild(gpa, &elf.mf, .{ .alignment = node_block_align, .moved = true, - .bubbles_moved = false, + .bubbles_moved = false, // see loadable segment handling in `flushMoved` }), .{ .segment = phndx.text }); elf.phdrs.items[phndx.text] = .wrap(elf.ni.text); @@ -5231,7 +5230,7 @@ fn initHeaders( // Must be at least `addr_align` for `elf.ni.data_rel_ro` to be placed inside this node .alignment = node_block_align.max(addr_align), .moved = true, - .bubbles_moved = false, + .bubbles_moved = false, // see loadable segment handling in `flushMoved` }), .{ .segment = phndx.data }); elf.phdrs.items[phndx.data] = .wrap(elf.ni.data); @@ -5239,7 +5238,7 @@ fn initHeaders( try elf.ni.elf.addFloatingChild(gpa, &elf.mf, .{ .alignment = node_block_align, .moved = true, - .bubbles_moved = false, + .bubbles_moved = false, // see loadable segment handling in `flushMoved` }), .{ .segment = phndx.plt }, )); @@ -5249,7 +5248,6 @@ fn initHeaders( // later (if `have_dynamic_section`). Keep in sync with `elf.ni.data` alignment above. .alignment = node_block_align.max(addr_align), .moved = true, - .bubbles_moved = false, }), .{ .segment = phndx.relro }); elf.phdrs.items[phndx.relro] = .wrap(elf.ni.data_rel_ro); @@ -5258,7 +5256,7 @@ fn initHeaders( try elf.ni.rodata.addFloatingChild(gpa, &elf.mf, .{ .alignment = node_block_align, .moved = true, - .bubbles_moved = false, + .bubbles_moved = false, // see TLS segment handling in `flushMoved` }), .{ .segment = phndx.tls }, )); @@ -5676,7 +5674,6 @@ fn initHeaders( .size = interp.len + 1, .moved = true, .resized = true, - .bubbles_moved = false, }), .{ .segment = phndx.interp }, ); @@ -5698,7 +5695,6 @@ fn initHeaders( try elf.ni.data_rel_ro.addFloatingChild(gpa, &elf.mf, .{ .alignment = addr_align, .moved = true, - .bubbles_moved = false, }), .{ .segment = phndx.dynamic }, ); @@ -5952,7 +5948,6 @@ fn initHeaders( .size = @sizeOf(Dwarf.EhFrameHdr), .alignment = .@"4", .moved = true, - .bubbles_moved = false, }), .{ .segment = phndx.gnu_eh_frame }, ); @@ -9119,7 +9114,7 @@ fn addSection(elf: *Elf, segment_ni: MappedFile.Node.Index, opts: struct { .size = opts.node_align.forward(opts.size), .alignment = opts.addralign.max(opts.node_align), .resized = opts.size > 0, - .bubbles_moved = opts.flags.ALLOC, + .bubbles_moved = opts.flags.ALLOC, // see handling in `flushMoved` }), if (opts.manual_size) .{ .section_manual_size = shndx } else .{ .section = shndx }, )), @@ -11309,6 +11304,9 @@ fn flushMoved(elf: *Elf, ni: MappedFile.Node.Index) std.mem.Allocator.Error!void .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`. + return; }, .DYNAMIC, @@ -11336,6 +11334,12 @@ fn flushMoved(elf: *Elf, ni: MappedFile.Node.Index) std.mem.Allocator.Error!void elf, tbss_addr, ); + + // Do not bubble moved---our child nodes don't care about our virtual + // address changing, because symbols in the TLS segment have values + // defined relative to the base of the segment. (This is also why we + // don't update tbss symbols here.) + return; }, } }, @@ -11388,6 +11392,12 @@ fn flushMoved(elf: *Elf, ni: MappedFile.Node.Index) std.mem.Allocator.Error!void .first_symbol_reloc = elf.eh_frame_hdr_first_symbol_reloc, }); } + + if (!flags.ALLOC) { + // Do not bubble moved---our virtual address hasn't changed (because it's always 0 + // for non-ALLOC sections), so there's nothing for our children to worry about. + return; + } }, .input_section => |isi| { const old_section_addr = isi.ptr(elf).vaddr; -- 2.54.0 From 51cd726f86e0036e9cc19339a70655a149f9d79f Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Sat, 17 Oct 2026 15:28:48 +0100 Subject: [PATCH 05/18] Elf2: more tracy integration Also moves the logic for printing node names for debug purposes (used by the `--debug-link-snapshot` logic) to a formatter so it can be reused. --- src/link/Elf2.zig | 258 +++++++++++++++++++++++++++------------------- 1 file changed, 154 insertions(+), 104 deletions(-) diff --git a/src/link/Elf2.zig b/src/link/Elf2.zig index 9fc4213ff1e4112f108a910b948e5491f8620279..07ad275ac56226ac9ba70baadbb4a8734a30e14c 100644 --- a/src/link/Elf2.zig +++ b/src/link/Elf2.zig @@ -461,6 +461,135 @@ const Node = union(enum) { fn fromAtom(atom: link.File.AtomId) MappedFile.Node.Index { return @fromBackingInt(@backingInt(atom)); } + + fn fmtDebug(ni: MappedFile.Node.Index, elf: *Elf) FormatDebug { + return .{ .ni = ni, .elf = elf }; + } + const FormatDebug = struct { + ni: MappedFile.Node.Index, + elf: *Elf, + pub fn format(ctx: FormatDebug, w: *Io.Writer) Io.Writer.Error!void { + const elf = ctx.elf; + const node = elf.getNode(ctx.ni); + try w.writeAll(@tagName(node)); + switch (node) { + // nodes with no payload + .deleted, + .archive, + .archive_header, + .elf, + .ehdr, + .shdr, + .debug_addr, + .eh_frame_footer, + .debug_str_offsets, + .unit_padding, + .archive_elf_member_header, + => |tag| comptime assert(@TypeOf(tag) == void), + + .archive_input_member => |ii| try w.print("({f}{f})", .{ + ii.path(elf).fmtEscapeString(), + fmtMemberString(ii.member(elf)), + }), + .segment => |phndx| switch (elf.phdrSlice()) { + inline else => |phdr| { + const ph = &phdr[phndx]; + try w.writeByte('('); + const pt = elf.targetLoad(&ph.type); + if (std.enums.tagName(std.elf.PT, pt)) |pt_name| + try w.writeAll(pt_name) + else inline for (@typeInfo(std.elf.PT).@"enum".decl_names) |decl_name| { + const decl_val = @field(std.elf.PT, decl_name); + if (@TypeOf(decl_val) != std.elf.PT) continue; + if (pt == @field(std.elf.PT, decl_name)) break try w.writeAll(decl_name); + } else try w.print("0x{x}", .{pt}); + try w.writeAll(", "); + const pf = elf.targetLoad(&ph.flags); + if (pf.R) try w.writeByte('R'); + if (pf.W) try w.writeByte('W'); + if (pf.X) try w.writeByte('X'); + try w.writeByte(')'); + }, + }, + .section, .section_manual_size => |shndx| try w.print("({q})", .{ + shndx.name(elf).slice(elf), + }), + .input_section => |isi| { + const ii = isi.input(elf); + try w.print("({f}{f}, {q})", .{ + ii.path(elf).fmtEscapeString(), + fmtMemberString(ii.member(elf)), + elf.getNodeShndx(isi.node(elf)).name(elf).slice(elf), + }); + }, + .copied_global => |gsi| try w.print("({q})", .{ + gsi.rawName(elf).slice(elf), + }), + .nav => |nmi| { + const ip = &elf.base.comp.zcu.?.intern_pool; + try w.print("({qf})", .{ + ip.getNav(nmi.nav(elf)).fqn.fmt(ip), + }); + }, + .uav => |umi| { + const zcu = elf.base.comp.zcu.?; + const val: Value = .fromInterned(umi.uavValue(elf)); + try w.print("({f}, {f})", .{ + val.typeOf(zcu).fmt(zcu), + val.fmtValue(zcu), + }); + }, + inline .lazy_code, .lazy_const_data => |lmi| try w.print("({f})", .{ + Type.fromInterned(lmi.lazySymbol(elf).ty).fmt(elf.base.comp.zcu.?), + }), + + .debug_shared => |ss| try w.print("({t})", .{ss}), + + .unit_frame, + .unit_frame_cie, + .unit_debug_info, + .unit_debug_info_header, + .unit_debug_info_footer, + .unit_debug_line, + .unit_debug_line_header, + .unit_debug_rnglists, + => |ui| try w.print("({q})", .{ + ui.mod(&elf.dwarf).fully_qualified_name, + }), + + .const_debug_info => |cpi| switch (cpi.val(&elf.dwarf.const_pool)) { + .generic_poison_type => try w.writeAll("(anytype)"), + else => |val| try w.print("({f})", .{ + Value.fromInterned(val).fmtValue(elf.base.comp.zcu.?), + }), + }, + .global_debug_info => |gi| { + const ip = &elf.base.comp.zcu.?.intern_pool; + try w.print("({qf})", .{ + ip.getNav(gi.nav(&elf.dwarf)).fqn.fmt(ip), + }); + }, + .func_frame_fde, .func_debug_info, .func_debug_line => |fi| { + const ip = &elf.base.comp.zcu.?.intern_pool; + try w.print("({qf})", .{ + ip.getNav(fi.nav(&elf.dwarf)).fqn.fmt(ip), + }); + }, + .decl_debug_info => |di| { + const comp = elf.base.comp; + const zcu = comp.zcu.?; + const ip = &zcu.intern_pool; + const src_inst = di.srcInst(&elf.dwarf); + const path = zcu.fileByIndex(src_inst.resolveFile(ip)).path; + if (src_inst.resolve(ip)) |inst| { + try w.print("({f}, %{d})", .{ path.fmt(comp), inst }); + } else { + try w.print("({f}, lost)", .{path.fmt(comp)}); + } + }, + } + } + }; }; const InputSection = struct { @@ -2985,10 +3114,15 @@ fn addGlobalSymbol(elf: *Elf, opts: Symbol.Global.AddOptions) (error{ /// some operation which requires relocations to be re-applied, it can pass `force_apply_relocs` as /// `true` to guarantee this function to do so. fn updateGlobalDynamic(elf: *Elf, orig_gsi: Symbol.Global.Index, force_apply_relocs: bool) Error!void { + const trace = tracy.trace(@src()); + defer trace.end(); + const gpa = elf.base.comp.gpa; const gsi = orig_gsi.resolveAlias(elf); + trace.addText(gsi.rawName(elf).slice(elf)); + const maybe_alias_gsi = gsi.findAliaser(elf); var apply_relocs: bool = force_apply_relocs; @@ -4132,7 +4266,20 @@ const Symbol = struct { } fn flushMoved(sym_id: Symbol.Id, elf: *Elf, new_value: u64) void { + const trace = tracy.trace(@src()); + defer trace.end(); + const sym_index = sym_id.index(elf); + + trace.addText(name: { + // There isn't a general `name` method on `Symbol.Id` because it would be incorrect + // to ever actually care about the name of a local symbol. + const name: String(.strtab) = switch (elf.symPtr(sym_index)) { + inline else => |sym| @fromBackingInt(elf.targetLoad(&sym.name)), + }; + break :name name.slice(elf); + }); + switch (elf.symPtr(sym_index)) { inline else => |sym| elf.targetStore(&sym.value, @intCast(new_value)), } @@ -10717,6 +10864,9 @@ pub fn idle(elf: *Elf) link.Error!bool { elf.mf.nodes_lock.lock(); defer elf.mf.nodes_lock.unlock(); + const trace = tracy.trace(@src()); + defer trace.end(); + const comp = elf.base.comp; const diags = &comp.link_diags; @@ -11285,6 +11435,7 @@ fn flushElfOffset(elf: *Elf, ni: MappedFile.Node.Index) void { fn flushMoved(elf: *Elf, ni: MappedFile.Node.Index) std.mem.Allocator.Error!void { const trace = tracy.trace(@src()); defer trace.end(); + trace.addTextFmt("{f}", .{Node.fmtDebug(ni, elf)}); switch (elf.getNode(ni)) { .deleted => unreachable, @@ -11904,6 +12055,7 @@ fn allocateSegmentLoadAddress(elf: *Elf, orig_phndx: u32) std.mem.Allocator.Erro fn flushResized(elf: *Elf, ni: MappedFile.Node.Index) std.mem.Allocator.Error!void { const trace = tracy.trace(@src()); defer trace.end(); + trace.addTextFmt("{f}", .{Node.fmtDebug(ni, elf)}); _, const size = ni.location(&elf.mf).resolve(&elf.mf); switch (elf.getNode(ni)) { @@ -12039,6 +12191,7 @@ fn flushResized(elf: *Elf, ni: MappedFile.Node.Index) std.mem.Allocator.Error!vo fn flushPadding(elf: *Elf, ni: MappedFile.Node.Index) std.mem.Allocator.Error!void { const trace = tracy.trace(@src()); defer trace.end(); + trace.addTextFmt("{f}", .{Node.fmtDebug(ni, elf)}); const node = elf.getNode(ni); switch (node) { @@ -12798,111 +12951,8 @@ pub fn printNode( ni: MappedFile.Node.Index, indent: usize, ) Io.Writer.Error!void { - const node = elf.getNode(ni); try w.splatByteAll(' ', indent); - try w.writeAll(@tagName(node)); - switch (node) { - else => {}, - .segment => |phndx| switch (elf.phdrSlice()) { - inline else => |phdr| { - const ph = &phdr[phndx]; - try w.writeByte('('); - const pt = elf.targetLoad(&ph.type); - if (std.enums.tagName(std.elf.PT, pt)) |pt_name| - try w.writeAll(pt_name) - else inline for (@typeInfo(std.elf.PT).@"enum".decl_names) |decl_name| { - const decl_val = @field(std.elf.PT, decl_name); - if (@TypeOf(decl_val) != std.elf.PT) continue; - if (pt == @field(std.elf.PT, decl_name)) break try w.writeAll(decl_name); - } else try w.print("0x{x}", .{pt}); - try w.writeAll(", "); - const pf = elf.targetLoad(&ph.flags); - if (pf.R) try w.writeByte('R'); - if (pf.W) try w.writeByte('W'); - if (pf.X) try w.writeByte('X'); - try w.writeByte(')'); - }, - }, - .section, .section_manual_size => |shndx| try w.print("({s})", .{shndx.name(elf).slice(elf)}), - .input_section => |isi| { - const ii = isi.input(elf); - try w.print("({f}{f}, {s})", .{ - ii.path(elf).fmtEscapeString(), - fmtMemberString(ii.member(elf)), - elf.getNodeShndx(isi.node(elf)).name(elf).slice(elf), - }); - }, - .copied_global => |gsi| try w.print("(copy:{s})", .{gsi.rawName(elf).slice(elf)}), - .nav => |nmi| { - const zcu = elf.base.comp.zcu.?; - const ip = &zcu.intern_pool; - const nav = ip.getNav(nmi.nav(elf)); - try w.print("({f}, {f})", .{ - Type.fromInterned(nav.resolved.?.type).fmt(zcu), - nav.fqn.fmt(ip), - }); - }, - .uav => |umi| { - const zcu = elf.base.comp.zcu.?; - const val: Value = .fromInterned(umi.uavValue(elf)); - try w.print("({f}, {f})", .{ - val.typeOf(zcu).fmt(zcu), - val.fmtValue(zcu), - }); - }, - inline .lazy_code, .lazy_const_data => |lmi| try w.print("({f})", .{ - Type.fromInterned(lmi.lazySymbol(elf).ty).fmt(elf.base.comp.zcu.?), - }), - .debug_shared => |ss| try w.print("({})", .{ss}), - .unit_frame, - .unit_frame_cie, - .unit_debug_info, - .unit_debug_info_header, - .unit_debug_info_footer, - .unit_debug_line, - .unit_debug_line_header, - .unit_debug_rnglists, - => |ui| try w.print("({s})", .{ui.mod(&elf.dwarf).fully_qualified_name}), - .const_debug_info => |cpi| switch (cpi.val(&elf.dwarf.const_pool)) { - .generic_poison_type => try w.writeAll("(anytype)"), - else => |val| try w.print("({f})", .{ - Value.fromInterned(val).fmtValue(elf.base.comp.zcu.?), - }), - }, - .global_debug_info => |gi| { - const zcu = elf.base.comp.zcu.?; - const ip = &zcu.intern_pool; - const nav = ip.getNav(gi.nav(&elf.dwarf)); - try w.writeByte('('); - if (nav.resolved) |resolved| try w.print("{f}, ", .{ - Type.fromInterned(resolved.type).fmt(zcu), - }); - try w.print("{f}", .{nav.fqn.fmt(ip)}); - if (nav.resolved) |resolved| try w.print(", {f}", .{ - Value.fromInterned(resolved.value).fmtValue(zcu), - }); - try w.writeByte(')'); - }, - .func_frame_fde, .func_debug_info, .func_debug_line => |fi| { - const zcu = elf.base.comp.zcu.?; - const ip = &zcu.intern_pool; - const nav = ip.getNav(fi.nav(&elf.dwarf)); - try w.writeByte('('); - if (nav.resolved) |resolved| try w.print("{f}, ", .{ - Type.fromInterned(resolved.type).fmt(zcu), - }); - try w.print("{f})", .{nav.fqn.fmt(ip)}); - }, - .decl_debug_info => |di| { - const comp = elf.base.comp; - const zcu = comp.zcu.?; - const ip = &zcu.intern_pool; - const src_inst = di.srcInst(&elf.dwarf); - try w.print("({f}, ", .{zcu.fileByIndex(src_inst.resolveFile(ip)).path.fmt(comp)}); - if (src_inst.resolve(ip)) |inst| try w.print("%{d}", .{inst}) else try w.writeAll("lost"); - try w.writeByte(')'); - }, - } + try w.print("{f}", .{Node.fmtDebug(ni, elf)}); { const mf_node = &elf.mf.nodes.items[@backingInt(ni)]; const off, const size = mf_node.location().resolve(&elf.mf); -- 2.54.0 From 77340b28c806e30c3d5759abe6883fc35eaed60c Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Sat, 17 Oct 2026 16:39:05 +0100 Subject: [PATCH 06/18] Elf2: stop constantly re-applying DWARF relocations --- src/link/Elf2.zig | 41 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/src/link/Elf2.zig b/src/link/Elf2.zig index 07ad275ac56226ac9ba70baadbb4a8734a30e14c..31cebe9d06d22bf40cbe16943e783f693a42e136 100644 --- a/src/link/Elf2.zig +++ b/src/link/Elf2.zig @@ -1073,10 +1073,14 @@ const dwarf_relocs = struct { const Addr = struct { first_target_reloc: NodeReloc.Index, symbol_relocs: std.ArrayList(SymbolReloc.Index), + /// Index into `symbol_relocs`. + pending_reloc_index: usize, }; const StrOffsets = struct { first_target_reloc: NodeReloc.Index, node_relocs: std.ArrayList(NodeReloc.Index), + /// Index into `node_relocs`. + pending_reloc_index: usize, }; const Unit = struct { frame_cie_first_target_reloc: NodeReloc.Index, @@ -5000,10 +5004,12 @@ fn create( .dwarf_addr = .{ .first_target_reloc = .none, .symbol_relocs = .empty, + .pending_reloc_index = 0, }, .dwarf_str_offsets = .{ .first_target_reloc = .none, .node_relocs = .empty, + .pending_reloc_index = 0, }, .dwarf_units = &.{}, .dwarf_consts = .empty, @@ -11010,6 +11016,9 @@ pub fn idle(elf: *Elf) link.Error!bool { if (elf.bss_moved) { elf.bss_moved = false; + const idle_prog_node = elf.mf.update_prog_node.start(".bss", 0); + defer idle_prog_node.end(); + const data_phndx = elf.getNode(elf.ni.data).segment; const data_offset: u64, const data_vaddr: u64, const data_filesz: u64 = switch (elf.phdrSlice()) { inline else => |phdr| .{ @@ -11040,6 +11049,32 @@ pub fn idle(elf: *Elf) link.Error!bool { break :task; } + if (elf.dwarf_addr.pending_reloc_index < elf.dwarf_addr.symbol_relocs.items.len) { + const idle_prog_node = elf.mf.update_prog_node.start(@tagName(Node.debug_addr), 0); + defer idle_prog_node.end(); + const debug_addr_ni = elf.dwarf.debug_addr.ni.unwrap().?; + const debug_addr_vaddr = elf.getNodeVAddr(debug_addr_ni); + for (elf.dwarf_addr.symbol_relocs.items[elf.dwarf_addr.pending_reloc_index..]) |symbol_ri| { + const symbol_reloc = symbol_ri.get(elf); + assert(symbol_reloc.node.unwrap().? == debug_addr_ni); + symbol_reloc.flushMovedNode(elf, debug_addr_vaddr); + } + elf.dwarf_addr.pending_reloc_index = elf.dwarf_addr.symbol_relocs.items.len; + break :task; + } + if (elf.dwarf_str_offsets.pending_reloc_index < elf.dwarf_str_offsets.node_relocs.items.len) { + const idle_prog_node = elf.mf.update_prog_node.start(@tagName(Node.debug_str_offsets), 0); + defer idle_prog_node.end(); + const debug_str_offsets_ni = elf.dwarf.debug_str_offsets.ni.unwrap().?; + const debug_str_offsets_vaddr = elf.getNodeVAddr(debug_str_offsets_ni); + for (elf.dwarf_str_offsets.node_relocs.items[elf.dwarf_str_offsets.pending_reloc_index..]) |node_ri| { + const node_reloc = node_ri.get(elf); + assert(node_reloc.node.unwrap().? == debug_str_offsets_ni); + node_reloc.flushMovedNode(elf, debug_str_offsets_vaddr); + } + elf.dwarf_str_offsets.pending_reloc_index = elf.dwarf_str_offsets.node_relocs.items.len; + break :task; + } } if (elf.inputs.items.len - elf.input_pending_index > 0) return true; if (elf.input_sections.items.len - elf.input_section_pending_index > 0) return true; @@ -11047,6 +11082,8 @@ pub fn idle(elf: *Elf) link.Error!bool { if (elf.changed_symtab_index.count() > 0) return true; if (elf.mf.updates.items.len > 0) return true; if (elf.bss_moved) return true; + if (elf.dwarf_addr.pending_reloc_index < elf.dwarf_addr.symbol_relocs.items.len) return true; + if (elf.dwarf_str_offsets.pending_reloc_index < elf.dwarf_str_offsets.node_relocs.items.len) return true; return false; } @@ -11177,7 +11214,6 @@ fn genPending(elf: *Elf, pt: Zcu.PerThread) link.Error!void { fn genPendingDebug(elf: *Elf, gpa: std.mem.Allocator) link.Error!void { while (elf.dwarf.debug_addr.map.count() - elf.dwarf.debug_addr.pending_index > 0) { const debug_addr_ni = elf.dwarf.debug_addr.ni.unwrap().?; - try debug_addr_ni.moved(gpa, &elf.mf); var da_nw: link.MappedFile.Node.Writer = undefined; debug_addr_ni.writer(gpa, &elf.mf, &da_nw); defer da_nw.deinit(); @@ -11190,7 +11226,6 @@ fn genPendingDebug(elf: *Elf, gpa: std.mem.Allocator) link.Error!void { } while (elf.dwarf.debug_str_offsets.map.count() - elf.dwarf.debug_str_offsets.pending_index > 0) { const debug_str_offsets_ni = elf.dwarf.debug_str_offsets.ni.unwrap().?; - try debug_str_offsets_ni.moved(gpa, &elf.mf); var dso_nw: link.MappedFile.Node.Writer = undefined; debug_str_offsets_ni.writer(gpa, &elf.mf, &dso_nw); defer dso_nw.deinit(); @@ -11659,6 +11694,7 @@ fn flushMoved(elf: *Elf, ni: MappedFile.Node.Index) std.mem.Allocator.Error!void assert(symbol_reloc.node.unwrap().? == ni); symbol_reloc.flushMovedNode(elf, node_vaddr); } + elf.dwarf_addr.pending_reloc_index = elf.dwarf_addr.symbol_relocs.items.len; }, .debug_str_offsets => { const target_section_offset = elf.computeNodeSectionOffset(ni); @@ -11675,6 +11711,7 @@ fn flushMoved(elf: *Elf, ni: MappedFile.Node.Index) std.mem.Allocator.Error!void assert(node_reloc.node.unwrap().? == ni); node_reloc.flushMovedNode(elf, node_vaddr); } + elf.dwarf_str_offsets.pending_reloc_index = elf.dwarf_str_offsets.node_relocs.items.len; }, .unit_frame_cie => |ui| { const target_section_offset = elf.computeNodeSectionOffset(ni); -- 2.54.0 From 0b4d75a0acd5311edf4f027d77d346bcfdc9b977 Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Sat, 17 Oct 2026 16:40:09 +0100 Subject: [PATCH 07/18] link.Queue: prelink could create idle tasks --- src/link/Queue.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/link/Queue.zig b/src/link/Queue.zig index 81585b91f377efd195b19a07a15cbf20241b1713..391768cf71bd68c47c52fa0f3c9dd3f6cddc4620 100644 --- a/src/link/Queue.zig +++ b/src/link/Queue.zig @@ -196,6 +196,7 @@ fn runLinkTasks(q: *Queue, comp: *Compilation) Io.Cancelable!void { }; } + have_idle_tasks = true; zcu_tasks: while (true) { var task_buf: [128]ZcuTask = undefined; const limit: usize = if (have_idle_tasks) 0 else 1; -- 2.54.0 From d070d85471004dfaaf4e865d64e4405116822d2a Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Sat, 17 Oct 2026 17:06:45 +0100 Subject: [PATCH 08/18] Elf2: simplify relocation application --- src/link/Elf2.zig | 186 ++++++++++++++-------------------------------- 1 file changed, 54 insertions(+), 132 deletions(-) diff --git a/src/link/Elf2.zig b/src/link/Elf2.zig index 31cebe9d06d22bf40cbe16943e783f693a42e136..8e23dd4107d146f5bd4cb4d4743a0c0bbf14c34f 100644 --- a/src/link/Elf2.zig +++ b/src/link/Elf2.zig @@ -1943,28 +1943,20 @@ const SymbolReloc = struct { } }; - fn flushMovedNode(reloc: *SymbolReloc, elf: *Elf, node_vaddr: u64) void { - if (reloc.rela_index.unwrap()) |rela_index| { - // The node has moved, so the offset of the relocation within the section might have - // changed, so update the `offset` field of the `ElfN.Rela` entry. - reloc.relaSection(elf).relaSetOffset(elf, rela_index, node_vaddr + reloc.offset); - } - // This is not just the inverse of the above condition, because if `reloc` is relative - // to the base of this DSO, then `rela_index` is an `R_*_RELATIVE` relocation, but we - // still need to call `SymbolReloc.apply` to update that relocation's addend. - if (elf.ehdrType() != .REL) { - reloc.apply(elf); - } - } - fn apply(reloc: *SymbolReloc, elf: *Elf) void { - assert(elf.ehdrType() != .REL); const node = reloc.node.unwrap() orelse return; // deleted if (node.hasMoved(&elf.mf) or reloc.target.hasMoved(elf)) { // There's no point applying the relocation now, because it will be re-applied by // `flushMoved` at some point anyway. return; } + if (elf.ehdrType() == .REL) { + // All we need to do is update the output relocation. + const rela_index = reloc.rela_index.unwrap().?; + const reloc_addr = elf.getNodeVAddr(node) + reloc.offset; + reloc.relaSection(elf).relaSetOffset(elf, rela_index, reloc_addr); + return; + } switch (reloc.result) { .ok => {}, .overflowed => elf.overflowed_reloc_count -= 1, @@ -2030,7 +2022,11 @@ const SymbolReloc = struct { // is required, meaning we can handle it now and return early. if (reloc.rela_index.unwrap()) |rela_index| switch (elf.classifySymbolValue(reloc.target)) { .static => unreachable, - .dynamic => return, // the relocation happens at runtime + .dynamic => { + // The relocation happens at runtime; we just need to update its offset. + elf.shndx.rela_dyn.relaSetOffset(elf, rela_index, dest_vaddr); + return; + }, .static_relative => { // We have emitted an R_*_RELATIVE relocation to help lower an absolute-address // relocation. The value computed above is valid, but instead of writing it to the @@ -2042,6 +2038,7 @@ const SymbolReloc = struct { } assert(reloc.type.action.simple.cast == .unsigned); assert(reloc.type.action.simple.shift == .@"0"); + elf.shndx.rela_dyn.relaSetOffset(elf, rela_index, dest_vaddr); elf.shndx.rela_dyn.relaSetAddend(elf, rela_index, target_val); return; }, @@ -2122,34 +2119,14 @@ const NodeReloc = struct { } }; - fn flushMovedNode(reloc: *NodeReloc, elf: *Elf, node_vaddr: u64) void { - if (reloc.rela_index.unwrap()) |rela_index| { - assert(elf.ehdrType() == .REL); - // The node has moved, so the offset of the relocation within the section might have - // changed, so update the `offset` field of the `ElfN.Rela` entry. - elf.getNodeShndx(reloc.node.unwrap().?).get(elf).rela.shndx.relaSetOffset(elf, rela_index, node_vaddr + reloc.offset); - } else { - assert(elf.ehdrType() != .REL); - reloc.apply(elf); - } - } - - fn flushMovedTarget(reloc: *NodeReloc, elf: *Elf, target_section_offset: u64) void { - if (reloc.rela_index.unwrap()) |rela_index| { - assert(elf.ehdrType() == .REL); - // The target has moved, so the `addend` field of the `ElfN.Rela` entry needs to be updated. - elf.getNodeShndx(reloc.node.unwrap().?).get(elf).rela.shndx.relaSetAddend(elf, rela_index, target_section_offset +% @as(u64, @bitCast(reloc.addend))); - } else { - assert(elf.ehdrType() != .REL); - reloc.apply(elf); - } - } - fn apply(reloc: *NodeReloc, elf: *Elf) void { const node = reloc.node.unwrap() orelse return; // deleted if (reloc.rela_index.unwrap()) |rela_index| { assert(elf.ehdrType() == .REL); - _ = rela_index; + // Ensure the output relocation is up-to-date. + const rela_shndx = elf.getNodeShndx(reloc.node.unwrap().?).get(elf).rela.shndx; + rela_shndx.relaSetOffset(elf, rela_index, elf.getNodeVAddr(node) + reloc.offset); + rela_shndx.relaSetAddend(elf, rela_index, elf.getNodeVAddr(node) +% @as(u64, @bitCast(reloc.addend))); } else { assert(elf.ehdrType() != .REL); if (node.hasMoved(&elf.mf) or reloc.target.hasMoved(&elf.mf)) { @@ -4308,14 +4285,12 @@ const Symbol = struct { } fn applyTargetRelocs(sym_id: Symbol.Id, elf: *Elf) void { - if (elf.ehdrType() != .REL) { - var ri = sym_id.index(elf).ptr(elf).first_target_reloc; - while (ri != .none) { - const reloc = ri.get(elf); - assert(reloc.target == sym_id); - reloc.apply(elf); - ri = reloc.next; - } + var ri = sym_id.index(elf).ptr(elf).first_target_reloc; + while (ri != .none) { + const reloc = ri.get(elf); + assert(reloc.target == sym_id); + reloc.apply(elf); + ri = reloc.next; } if (elf.got.getIndex(.{ .symbol = sym_id })) |got_index| { @@ -6623,43 +6598,6 @@ fn computeNodeVAddr(elf: *Elf, ni: MappedFile.Node.Index) u64 { const offset, _ = ni.location(&elf.mf).resolve(&elf.mf); return parent_vaddr + offset; } -fn computeNodeSectionOffset(elf: *Elf, ni: MappedFile.Node.Index) u64 { - const parent_ni = ni.parent(&elf.mf).unwrap().?; - const parent_section_offset = parent_section_offset: switch (elf.getNode(parent_ni)) { - .deleted, - .archive, - .archive_header, - .archive_input_member, - .archive_elf_member_header, - .elf, - .ehdr, - .shdr, - .segment, - => unreachable, - .section, .section_manual_size => 0, - .input_section, .copied_global => unreachable, - .nav, .uav, .lazy_code, .lazy_const_data => unreachable, - .debug_shared, .debug_addr, .eh_frame_footer, .debug_str_offsets, .unit_padding => unreachable, - .unit_frame, .unit_debug_info, .unit_debug_line => { - const parent_section_offset, _ = parent_ni.location(&elf.mf).resolve(&elf.mf); - break :parent_section_offset parent_section_offset; - }, - .unit_frame_cie, - .unit_debug_info_header, - .unit_debug_info_footer, - .unit_debug_line_header, - .unit_debug_rnglists, - .const_debug_info, - .global_debug_info, - .func_frame_fde, - .func_debug_info, - .func_debug_line, - .decl_debug_info, - => unreachable, - }; - const offset, _ = ni.location(&elf.mf).resolve(&elf.mf); - return parent_section_offset + offset; -} fn computeNodeElfOffset(elf: *Elf, ni: MappedFile.Node.Index) u64 { return ni.fileLocation(&elf.mf, false).offset - elf.ni.elf.fileLocation(&elf.mf, false).offset; } @@ -6801,7 +6739,6 @@ pub fn resetNodeRelocs(elf: *Elf, ni: MappedFile.Node.Index) void { fn flushMovedNodeRelocs( elf: *Elf, node: MappedFile.Node.Index, - node_vaddr: u64, opts: struct { first_symbol_reloc: SymbolReloc.Index = .none, skip_symbol_relocs: MappedFile.Node.Index.Optional = .none, @@ -6817,7 +6754,7 @@ fn flushMovedNodeRelocs( if (reloc.node == opts.skip_symbol_relocs) continue; break; } - reloc.flushMovedNode(elf, node_vaddr); + reloc.apply(elf); } } @@ -6828,7 +6765,7 @@ fn flushMovedNodeRelocs( if (reloc.node == opts.skip_node_relocs) continue; break; } - reloc.flushMovedNode(elf, node_vaddr); + reloc.apply(elf); } } @@ -9406,6 +9343,7 @@ fn addRelocAssumeCapacity( .rela_index = rela_index.toOptional(), .result = .ok, }); + ri.get(elf).apply(elf); }, .DYN, .EXEC => switch (elf.ehdrMachine()) { .AARCH64 => switch (@"type".AARCH64) { @@ -11053,11 +10991,10 @@ pub fn idle(elf: *Elf) link.Error!bool { const idle_prog_node = elf.mf.update_prog_node.start(@tagName(Node.debug_addr), 0); defer idle_prog_node.end(); const debug_addr_ni = elf.dwarf.debug_addr.ni.unwrap().?; - const debug_addr_vaddr = elf.getNodeVAddr(debug_addr_ni); for (elf.dwarf_addr.symbol_relocs.items[elf.dwarf_addr.pending_reloc_index..]) |symbol_ri| { const symbol_reloc = symbol_ri.get(elf); assert(symbol_reloc.node.unwrap().? == debug_addr_ni); - symbol_reloc.flushMovedNode(elf, debug_addr_vaddr); + symbol_reloc.apply(elf); } elf.dwarf_addr.pending_reloc_index = elf.dwarf_addr.symbol_relocs.items.len; break :task; @@ -11066,11 +11003,10 @@ pub fn idle(elf: *Elf) link.Error!bool { const idle_prog_node = elf.mf.update_prog_node.start(@tagName(Node.debug_str_offsets), 0); defer idle_prog_node.end(); const debug_str_offsets_ni = elf.dwarf.debug_str_offsets.ni.unwrap().?; - const debug_str_offsets_vaddr = elf.getNodeVAddr(debug_str_offsets_ni); for (elf.dwarf_str_offsets.node_relocs.items[elf.dwarf_str_offsets.pending_reloc_index..]) |node_ri| { const node_reloc = node_ri.get(elf); assert(node_reloc.node.unwrap().? == debug_str_offsets_ni); - node_reloc.flushMovedNode(elf, debug_str_offsets_vaddr); + node_reloc.apply(elf); } elf.dwarf_str_offsets.pending_reloc_index = elf.dwarf_str_offsets.node_relocs.items.len; break :task; @@ -11565,7 +11501,7 @@ fn flushMoved(elf: *Elf, ni: MappedFile.Node.Index) std.mem.Allocator.Error!void reloc.apply(elf); } } else if (shndx == elf.shndx.plt) { - elf.flushMovedNodeRelocs(ni, addr, .{ + elf.flushMovedNodeRelocs(ni, .{ .first_symbol_reloc = elf.plt_first_symbol_reloc, }); elf.flushMovedPltSection(.plt, old_addr, addr); @@ -11574,7 +11510,7 @@ fn flushMoved(elf: *Elf, ni: MappedFile.Node.Index) std.mem.Allocator.Error!void } else if (shndx == elf.shndx.plt_sec) { elf.flushMovedPltSection(.plt_sec, old_addr, addr); } else if (shndx == elf.shndx.eh_frame_hdr) { - elf.flushMovedNodeRelocs(ni, addr, .{ + elf.flushMovedNodeRelocs(ni, .{ .first_symbol_reloc = elf.eh_frame_hdr_first_symbol_reloc, }); } @@ -11627,7 +11563,7 @@ fn flushMoved(elf: *Elf, ni: MappedFile.Node.Index) std.mem.Allocator.Error!void } } - elf.flushMovedNodeRelocs(ni, new_section_addr, .{ + elf.flushMovedNodeRelocs(ni, .{ .first_symbol_reloc = isi.ptrConst(elf).first_symbol_reloc, .first_got_reloc = isi.ptrConst(elf).first_got_reloc, }); @@ -11650,7 +11586,7 @@ fn flushMoved(elf: *Elf, ni: MappedFile.Node.Index) std.mem.Allocator.Error!void Symbol.Id.global(gsi).flushMoved(elf, new_addr); } } - elf.flushMovedNodeRelocs(ni, new_addr, .{ + elf.flushMovedNodeRelocs(ni, .{ .first_symbol_reloc = mi.firstSymbolReloc(elf), .skip_symbol_relocs = switch (tag) { else => comptime unreachable, @@ -11664,12 +11600,11 @@ fn flushMoved(elf: *Elf, ni: MappedFile.Node.Index) std.mem.Allocator.Error!void }); }, .debug_shared => |ss| { - const target_section_offset = elf.computeNodeSectionOffset(ni); var target_ri = elf.dwarf_shared.getPtr(ss).first_target_reloc; while (target_ri != .none) { const target_reloc = target_ri.get(elf); assert(target_reloc.target == ni); - target_reloc.flushMovedTarget(elf, target_section_offset); + target_reloc.apply(elf); target_ri = target_reloc.next; } }, @@ -11680,121 +11615,110 @@ fn flushMoved(elf: *Elf, ni: MappedFile.Node.Index) std.mem.Allocator.Error!void .unit_debug_line, => {}, .debug_addr => { - const target_section_offset = elf.computeNodeSectionOffset(ni); var target_ri = elf.dwarf_addr.first_target_reloc; while (target_ri != .none) { const target_reloc = target_ri.get(elf); assert(target_reloc.target == ni); - target_reloc.flushMovedTarget(elf, target_section_offset); + target_reloc.apply(elf); target_ri = target_reloc.next; } - const node_vaddr = elf.computeNodeVAddr(ni); for (elf.dwarf_addr.symbol_relocs.items) |symbol_ri| { const symbol_reloc = symbol_ri.get(elf); assert(symbol_reloc.node.unwrap().? == ni); - symbol_reloc.flushMovedNode(elf, node_vaddr); + symbol_reloc.apply(elf); } elf.dwarf_addr.pending_reloc_index = elf.dwarf_addr.symbol_relocs.items.len; }, .debug_str_offsets => { - const target_section_offset = elf.computeNodeSectionOffset(ni); var target_ri = elf.dwarf_str_offsets.first_target_reloc; while (target_ri != .none) { const target_reloc = target_ri.get(elf); assert(target_reloc.target == ni); - target_reloc.flushMovedTarget(elf, target_section_offset); + target_reloc.apply(elf); target_ri = target_reloc.next; } - const node_vaddr = elf.computeNodeVAddr(ni); for (elf.dwarf_str_offsets.node_relocs.items) |node_ri| { const node_reloc = node_ri.get(elf); assert(node_reloc.node.unwrap().? == ni); - node_reloc.flushMovedNode(elf, node_vaddr); + node_reloc.apply(elf); } elf.dwarf_str_offsets.pending_reloc_index = elf.dwarf_str_offsets.node_relocs.items.len; }, .unit_frame_cie => |ui| { - const target_section_offset = elf.computeNodeSectionOffset(ni); var target_ri = elf.dwarf_units[@backingInt(ui)].frame_cie_first_target_reloc; while (target_ri != .none) { const target_reloc = target_ri.get(elf); assert(target_reloc.target == ni); - target_reloc.flushMovedTarget(elf, target_section_offset); + target_reloc.apply(elf); target_ri = target_reloc.next; } }, .unit_debug_info_header => |ui| { const dwarf_unit = &elf.dwarf_units[@backingInt(ui)]; - const target_section_offset = elf.computeNodeSectionOffset(ni); var target_ri = dwarf_unit.debug_info_header_first_target_reloc; while (target_ri != .none) { const target_reloc = target_ri.get(elf); assert(target_reloc.target == ni); - target_reloc.flushMovedTarget(elf, target_section_offset); + target_reloc.apply(elf); target_ri = target_reloc.next; } - elf.flushMovedNodeRelocs(ni, elf.computeNodeVAddr(ni), .{ + elf.flushMovedNodeRelocs(ni, .{ .first_node_reloc = dwarf_unit.debug_info_header_first_node_reloc, }); }, .unit_debug_info_footer => {}, .unit_debug_line_header => |ui| { const dwarf_unit = &elf.dwarf_units[@backingInt(ui)]; - const target_section_offset = elf.computeNodeSectionOffset(ni); var target_ri = dwarf_unit.debug_line_header_first_target_reloc; while (target_ri != .none) { const target_reloc = target_ri.get(elf); assert(target_reloc.target == ni); - target_reloc.flushMovedTarget(elf, target_section_offset); + target_reloc.apply(elf); target_ri = target_reloc.next; } - elf.flushMovedNodeRelocs(ni, elf.computeNodeVAddr(ni), .{ + elf.flushMovedNodeRelocs(ni, .{ .first_node_reloc = dwarf_unit.debug_line_header_first_node_reloc, }); }, .unit_debug_rnglists => |ui| { const dwarf_unit = &elf.dwarf_units[@backingInt(ui)]; - const target_section_offset = elf.computeNodeSectionOffset(ni); var target_ri = dwarf_unit.debug_rnglists_first_target_reloc; while (target_ri != .none) { const target_reloc = target_ri.get(elf); assert(target_reloc.target == ni); - target_reloc.flushMovedTarget(elf, target_section_offset); + target_reloc.apply(elf); target_ri = target_reloc.next; } - const node_vaddr = elf.computeNodeVAddr(ni); for (dwarf_unit.debug_rnglists_symbol_relocs.items) |symbol_ri| { const symbol_reloc = symbol_ri.get(elf); assert(symbol_reloc.node.unwrap().? == ni); - symbol_reloc.flushMovedNode(elf, node_vaddr); + symbol_reloc.apply(elf); } }, .const_debug_info => |cpi| { const dwarf_const = &elf.dwarf_consts.get(cpi).?; - const target_section_offset = elf.computeNodeSectionOffset(ni); var target_ri = dwarf_const.debug_info_first_target_reloc; while (target_ri != .none) { const target_reloc = target_ri.get(elf); assert(target_reloc.target == ni); - target_reloc.flushMovedTarget(elf, target_section_offset); + target_reloc.apply(elf); target_ri = target_reloc.next; } - elf.flushMovedNodeRelocs(ni, elf.computeNodeVAddr(ni), .{ + elf.flushMovedNodeRelocs(ni, .{ .first_symbol_reloc = dwarf_const.debug_info_first_symbol_reloc, .first_node_reloc = dwarf_const.debug_info_first_node_reloc, }); }, .global_debug_info => |gi| { const dwarf_global = &elf.dwarf_globals.items[@backingInt(gi)]; - const target_section_offset = elf.computeNodeSectionOffset(ni); var target_ri = dwarf_global.debug_info_first_target_reloc; while (target_ri != .none) { const target_reloc = target_ri.get(elf); assert(target_reloc.target == ni); - target_reloc.flushMovedTarget(elf, target_section_offset); + target_reloc.apply(elf); target_ri = target_reloc.next; } - elf.flushMovedNodeRelocs(ni, elf.computeNodeVAddr(ni), .{ + elf.flushMovedNodeRelocs(ni, .{ .first_symbol_reloc = dwarf_global.debug_info_first_symbol_reloc, .first_node_reloc = dwarf_global.debug_info_first_node_reloc, }); @@ -11810,22 +11734,21 @@ fn flushMoved(elf: *Elf, ni: MappedFile.Node.Index) std.mem.Allocator.Error!void elf.dwarf.updateEhFrameFde(ni.slice(&elf.mf), offset); }, } - elf.flushMovedNodeRelocs(ni, elf.computeNodeVAddr(ni), .{ + elf.flushMovedNodeRelocs(ni, .{ .first_symbol_reloc = dwarf_func.frame_fde_first_symbol_reloc, .first_node_reloc = dwarf_func.frame_fde_first_node_reloc, }); }, .func_debug_info => |fi| { const dwarf_func = &elf.dwarf_funcs.items[@backingInt(fi)]; - const target_section_offset = elf.computeNodeSectionOffset(ni); var target_ri = dwarf_func.debug_info_first_target_reloc; while (target_ri != .none) { const target_reloc = target_ri.get(elf); assert(target_reloc.target == ni); - target_reloc.flushMovedTarget(elf, target_section_offset); + target_reloc.apply(elf); target_ri = target_reloc.next; } - elf.flushMovedNodeRelocs(ni, elf.computeNodeVAddr(ni), .{ + elf.flushMovedNodeRelocs(ni, .{ .first_symbol_reloc = dwarf_func.debug_info_first_symbol_reloc, .skip_symbol_relocs = if (elf.navs.getPtr(fi.nav(&elf.dwarf))) |nav| nav.lsi.index().ptr(elf).node @@ -11837,7 +11760,7 @@ fn flushMoved(elf: *Elf, ni: MappedFile.Node.Index) std.mem.Allocator.Error!void }, .func_debug_line => |fi| { const dwarf_func = &elf.dwarf_funcs.items[@backingInt(fi)]; - elf.flushMovedNodeRelocs(ni, elf.computeNodeVAddr(ni), .{ + elf.flushMovedNodeRelocs(ni, .{ .first_symbol_reloc = dwarf_func.debug_line_first_symbol_reloc, .first_node_reloc = dwarf_func.debug_line_first_node_reloc, .skip_node_relocs = fi.get(&elf.dwarf).debug_info_ni, @@ -11845,15 +11768,14 @@ fn flushMoved(elf: *Elf, ni: MappedFile.Node.Index) std.mem.Allocator.Error!void }, .decl_debug_info => |di| { const dwarf_decl = &elf.dwarf_decls.get(di).?; - const target_section_offset = elf.computeNodeSectionOffset(ni); var target_ri = dwarf_decl.debug_info_first_target_reloc; while (target_ri != .none) { const target_reloc = target_ri.get(elf); assert(target_reloc.target == ni); - target_reloc.flushMovedTarget(elf, target_section_offset); + target_reloc.apply(elf); target_ri = target_reloc.next; } - elf.flushMovedNodeRelocs(ni, elf.computeNodeVAddr(ni), .{ + elf.flushMovedNodeRelocs(ni, .{ .first_node_reloc = dwarf_decl.debug_info_first_node_reloc, }); }, -- 2.54.0 From 4317e1b12a084284a3cd3bf660c49cd42a627266 Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Sat, 17 Oct 2026 21:40:42 +0100 Subject: [PATCH 09/18] Elf2: fix dynamic symbol edge cases, add missing error reporting ...and fix our symbol binding logic to be more standard. --- src/link/Dwarf2.zig | 37 +-- src/link/Elf2.zig | 617 +++++++++++++++++++++++++----------------- test/debugger/storage | 4 - 3 files changed, 375 insertions(+), 283 deletions(-) diff --git a/src/link/Dwarf2.zig b/src/link/Dwarf2.zig index 28887a5318395cb05b8ce64bfd8f932dfd359a1d..3c73609eb7bf663637ef432f198960a98f53ef68 100644 --- a/src/link/Dwarf2.zig +++ b/src/link/Dwarf2.zig @@ -2101,11 +2101,9 @@ pub fn updateExtern( dwarf: *Dwarf, pt: Zcu.PerThread, di_nw: *link.MappedFile.Node.Writer, - arch: std.Target.Cpu.Arch, - si: link.File.SymbolId, global: InternPool.Nav.Index, ) link.Error!void { - dwarf.updateExternInner(pt, di_nw, arch, si, global) catch |err| switch (err) { + dwarf.updateExternInner(pt, di_nw, global) catch |err| switch (err) { else => |e| return e, error.WriteFailed => return dwarf.reportWriteError(di_nw), }; @@ -2114,20 +2112,17 @@ fn updateExternInner( dwarf: *Dwarf, pt: Zcu.PerThread, di_nw: *link.MappedFile.Node.Writer, - arch: std.Target.Cpu.Arch, - si: link.File.SymbolId, global: InternPool.Nav.Index, ) link.EmitError!void { const zcu = pt.zcu; const ip = &zcu.intern_pool; const nav = ip.getNav(global); log.debug("updateExtern({f})", .{nav.fqn.fmt(ip)}); - const nav_align, const nav_const, const nav_threadlocal, const nav_extern = nav_resolved: { + const nav_align, const nav_const, const nav_extern = nav_resolved: { const nav_resolved = nav.resolved.?; break :nav_resolved .{ nav_resolved.@"align", nav_resolved.@"const", - nav_resolved.@"threadlocal", ip.indexToKey(nav_resolved.value).@"extern", }; }; @@ -2162,7 +2157,7 @@ fn updateExternInner( try di_w.writeByte(if (decl.is_pub) DW.ACCESS.public else DW.ACCESS.private); try dwarf.strx(di_nw, nav.name.toSlice(ip)); } else { - try dwarf.abbrevCode(di_nw, .decl_var); + try dwarf.abbrevCode(di_nw, .decl_extern_var); try dwarf.refType(pt, di_nw, .fromInterned(zcu.fileRootType(inst_info.file))); try di_w.writeInt(u32, decl.src_line + 1, dwarf.endian); try di_w.writeUleb128(decl.src_column + 1); @@ -2173,7 +2168,6 @@ fn updateExternInner( try dwarf.strx(di_nw, nav_extern.name.toSlice(ip)); if (maybe_func_type) |func_type| { try dwarf.refType(pt, di_nw, .fromInterned(func_type.return_type)); - try dwarf.addrSym(di_nw, si, 0); try di_w.writeByte(@intFromBool(Type.fromInterned(func_type.return_type).isNoReturn(zcu))); for (func_type.param_types.get(ip)) |param_type| { try dwarf.abbrevCode(di_nw, .extern_param); @@ -2182,23 +2176,13 @@ fn updateExternInner( if (func_type.is_var_args) try dwarf.abbrevCode(di_nw, .is_var_args); if (!is_empty) try di_w.writeUleb128(@backingInt(AbbrevCode.null)); } else { - const addr: Loc = .{ .addr_sym = .{ .si = si } }; - const loc: Loc = if (nav_threadlocal) switch (arch) { - .x86_64 => .{ .form_tls_address = &addr }, - else => unreachable, - } else addr; const type_offset = di_w.end; if (nav_const) try dwarf.secOffsetPlaceholder(di_w) else try dwarf.refType(pt, di_nw, .fromInterned(nav_extern.ty)); - try dwarf.exprLoc(di_nw, loc); try di_w.writeUleb128(nav_align.toByteUnits() orelse Type.fromInterned(nav_extern.ty).abiAlignment(zcu).toByteUnits().?); - switch (nav_extern.source) { - .builtin => {}, - .syntax => try di_w.writeByte(@intFromBool(decl.linkage != .normal)), - } if (nav_const) { try dwarf.secOffsetFinish(di_nw, type_offset, di_nw.ni, di_w.end); try dwarf.abbrevCode(di_nw, .is_const); @@ -4800,6 +4784,7 @@ pub const AbbrevCode = enum { decl_func_generic, decl_extern_empty_func, decl_extern_func, + decl_extern_var, decl_specification_empty_struct, decl_specification_struct, type_decl_specification_empty_struct, @@ -5285,7 +5270,6 @@ pub const AbbrevCode = enum { .attrs = decl_attrs ++ .{ .{ .linkage_name, .strx }, .{ .type, .ref_addr }, - .{ .low_pc, .addr }, .{ .external, .flag_present }, .{ .noreturn, .flag }, }, @@ -5296,11 +5280,19 @@ pub const AbbrevCode = enum { .attrs = decl_attrs ++ .{ .{ .linkage_name, .strx }, .{ .type, .ref_addr }, - .{ .low_pc, .addr }, .{ .external, .flag_present }, .{ .noreturn, .flag }, }, }, + .decl_extern_var = .{ + .tag = .variable, + .attrs = decl_attrs ++ .{ + .{ .linkage_name, .strx }, + .{ .type, .ref_addr }, + .{ .alignment, .udata }, + .{ .external, .flag_present }, + }, + }, .decl_specification_empty_struct = .{ .tag = .structure_type, .attrs = decl_specification_attrs, @@ -5995,7 +5987,6 @@ pub const AbbrevCode = enum { .{ .ZIG_parent, .ref_addr }, .{ .linkage_name, .strx }, .{ .type, .ref_addr }, - .{ .low_pc, .addr }, .{ .external, .flag_present }, .{ .noreturn, .flag }, }, @@ -6007,7 +5998,6 @@ pub const AbbrevCode = enum { .{ .ZIG_parent, .ref_addr }, .{ .linkage_name, .strx }, .{ .type, .ref_addr }, - .{ .low_pc, .addr }, .{ .external, .flag_present }, .{ .noreturn, .flag }, }, @@ -6018,7 +6008,6 @@ pub const AbbrevCode = enum { .{ .ZIG_parent, .ref_addr }, .{ .linkage_name, .strx }, .{ .type, .ref_addr }, - .{ .location, .exprloc }, .{ .alignment, .udata }, .{ .external, .flag_present }, }, diff --git a/src/link/Elf2.zig b/src/link/Elf2.zig index 8e23dd4107d146f5bd4cb4d4743a0c0bbf14c34f..b1d61ed0ceeff127256914fa67e97fa70d9d1474 100644 --- a/src/link/Elf2.zig +++ b/src/link/Elf2.zig @@ -244,7 +244,7 @@ section_by_name: std.array_hash_map.Auto(String(.shstrtab), void), changed_symtab_index: std.array_hash_map.Auto(Symbol.Global.Index, void), /// Counts how many relocations are currently in `.rela.dyn` which would require a `DT_TEXTREL` /// entry in the `.dynamic` section. This allows adding `DT_TEXTREL` to the output `.dynamic` -/// section in `flush` only when it is actually necessary. See also `nodeWantsDsoRelocation`. +/// section in `flush` only when it is actually necessary. See also `nodeAllowsDynamicReloc`. textrel_count: u32, dwarf: Dwarf, @@ -259,6 +259,8 @@ dwarf_decls: std.array_hash_map.Auto(Dwarf.Decl.Index, dwarf_relocs.Decl), overflowed_reloc_count: u32, misaligned_reloc_count: u32, +illegal_dynamic_type_reloc_count: u32, +illegal_dynamic_section_reloc_count: u32, const_prog_node: std.Progress.Node, input_prog_node: std.Progress.Node, @@ -1409,7 +1411,13 @@ const SymbolReloc = struct { /// relocation entry. The entry will be removed if we discover a definition which allows us to /// statically resolve the relocation. rela_index: Section.RelaIndex.Optional, - result: enum(u8) { ok, overflowed, misaligned }, + result: enum(u8) { + ok, + overflowed, + misaligned, + illegal_dynamic_type, + illegal_dynamic_section, + }, /// Determines the section in which this relocation will be placed if it is outstanding. /// @@ -1961,6 +1969,10 @@ const SymbolReloc = struct { .ok => {}, .overflowed => elf.overflowed_reloc_count -= 1, .misaligned => elf.misaligned_reloc_count -= 1, + .illegal_dynamic_type, .illegal_dynamic_section => { + // This status won't change until a call to `updateDynamicReloc`. + return; + }, } if (reloc.applyInner(elf)) { @branchHint(.likely); @@ -2071,6 +2083,8 @@ const SymbolReloc = struct { .ok => {}, .overflowed => elf.overflowed_reloc_count -= 1, .misaligned => elf.misaligned_reloc_count -= 1, + .illegal_dynamic_type => elf.illegal_dynamic_type_reloc_count -= 1, + .illegal_dynamic_section => elf.illegal_dynamic_section_reloc_count -= 1, } reloc.* = undefined; @@ -2084,14 +2098,211 @@ const SymbolReloc = struct { reloc.relaSection(elf).relaDeleteOne(elf, rela_index); switch (elf.ehdrType()) { .REL => {}, - .EXEC, .DYN => switch (elf.nodeWantsDsoRelocation(reloc.node.unwrap().?)) { - .no => unreachable, // there *was* a dynamic relocation! - .yes => {}, - .yes_textrel => elf.textrel_count -= 1, + .EXEC, .DYN => switch (elf.nodeAllowsDynamicReloc(reloc.node.unwrap().?)) { + .not_alloc => unreachable, // there *was* a dynamic relocation! + .ok => {}, + .ok_textrel => elf.textrel_count -= 1, }, } reloc.rela_index = .none; } + + fn updateDynamicReloc(reloc: *SymbolReloc, elf: *Elf) Error!void { + assert(elf.ehdrType() != .REL); + const reloc_node = reloc.node.unwrap().?; // deleted + + // Delete any current output relocation or error. + switch (reloc.result) { + .ok, .overflowed, .misaligned => { + reloc.deleteOutputRel(elf); + }, + .illegal_dynamic_type => { + elf.illegal_dynamic_type_reloc_count -= 1; + reloc.result = .ok; + }, + .illegal_dynamic_section => { + elf.illegal_dynamic_section_reloc_count -= 1; + reloc.result = .ok; + }, + } + + assert(reloc.rela_index == .none); + + const symbol_class = elf.classifySymbolValue(reloc.target); + + switch (reloc.target.unwrap()) { + .local => {}, + .global => |gsi| switch (symbol_class) { + .static => gsi.ptr(elf).status.any_static_target_relocs = true, + .static_relative => gsi.ptr(elf).status.any_static_relative_target_relocs = true, + .dynamic => gsi.ptr(elf).status.any_dynamic_target_relocs = true, + }, + } + + const want_dynamic_reloc: enum { + /// `reloc` cannot be statically resolved, but is not an allowed dynamic relocation. + illegal, + relative, + abs_addr, + dtp_off, + tp_off, + size32, + size64, + } = switch (symbol_class) { + .static => return, + + .static_relative => switch (reloc.type.target) { + // We need a runtime relocation if the relocation depends on our load address, which + // is the case iff the relocation resolves to the absolute value of a symbol. Relocs + // which are PC-relative, or which resolve to something other than a symbol address + // (e.g. a symbol's size), are fine. + + .pltrel, + .rel, + .size, + .dtpoff, + .tpoff, + => return, + + .abs, .pltabs => switch (reloc.type.action.simple.dest.isAddr(elf)) { + true => .relative, + false => .illegal, + }, + + .special => switch (reloc.type.action.special) { + .larch_pcala_hi20, + .larch_pcala64_lo20, + .larch_pcala64_hi12, + .larch_b21, + .larch_b26, + .larch_call36, + .sparc_wdisp10, + .sparc_wdisp16, + .sparc_le_hix22, + => return, + + .sparc_olo10, + .sparc_hix22, + => .illegal, + }, + }, + + .dynamic => switch (reloc.type.target) { + // PLT relocations targeting dynamic symbols actually target that symbol's PLT + // entry, so we should emit an `R_*_RELATIVE` relocation instead. + .pltabs => switch (reloc.type.action.simple.dest.isAddr(elf)) { + true => .relative, + false => .illegal, + }, + // ...although PC-relative PLT relocations don't even need that that! + .pltrel => return, + // PC-relative relocations are not valid dynamic relocations. + .rel => .illegal, + // On the few targets supporting size relocations, they are valid at runtime. + .size => switch (reloc.type.action.simple.dest) { + .@"32" => .size32, + .@"64" => .size64, + else => .illegal, + }, + // Absolute addresses and TLS offsets can be lowered at runtime provided they + // are address-sized. + .dtpoff => switch (reloc.type.action.simple.dest.isAddr(elf)) { + true => .dtp_off, + false => .illegal, + }, + .tpoff => switch (reloc.type.action.simple.dest.isAddr(elf)) { + true => .tp_off, + false => .illegal, + }, + .abs => switch (reloc.type.action.simple.dest.isAddr(elf)) { + true => .abs_addr, + false => .illegal, + }, + + .special => switch (reloc.type.action.special) { + .larch_b21, + .larch_b26, + .larch_call36, + .sparc_wdisp10, + .sparc_wdisp16, + => return, + + .larch_pcala_hi20, + .larch_pcala64_lo20, + .larch_pcala64_hi12, + .sparc_olo10, + .sparc_hix22, + .sparc_le_hix22, + => .illegal, + }, + }, + }; + + const is_textrel: bool = switch (elf.nodeAllowsDynamicReloc(reloc_node)) { + .not_alloc => { + // The section containing the relocation is not loaded into memory. These sections + // have slightly different rules. In other ELF linkers, they basically just let any + // relocation through at any time---they will bind to a local symbol definition if + // it exists, or just to 0 otherwise. I think that's a little scary though, so until + // I come across a compelling reason for us to mimic that behaviour, let's be more + // conservative and only allow what seems necessary: + const allow: bool = switch (want_dynamic_reloc) { + // Relocations which target the value/[d]tpoff of a symbol with a local + // definition, and which write their result to a full address-sized word, have + // clear use cases (e.g. DWARF uses these), so let them through. + .relative, + .abs_addr, + .dtp_off, + .tp_off, + => switch (reloc.target.unwrap()) { + .local => true, + .global => |gsi| gsi.resolveAlias(elf).defined(elf), + }, + + // Everything else I'm going to disallow for now. Remember that we're already in + // the dynamic case here, so this isn't disallowing (e.g.) all size relocations + // in these sections, just size relocations *targeting dynamic symbols*! + .size32, .size64, .illegal => false, + }; + if (!allow) { + // ...but in this case we actually do need to trigger an error. + reloc.result = .illegal_dynamic_section; + elf.illegal_dynamic_section_reloc_count += 1; + } + return; + }, + .ok => false, + .ok_textrel => true, + }; + + const rel_type: MachineRelocType = rel: switch (want_dynamic_reloc) { + .illegal => { + reloc.result = .illegal_dynamic_type; + elf.illegal_dynamic_type_reloc_count += 1; + return; + }, + .relative => .relative(elf), + .abs_addr => .absAddr(elf), + .dtp_off => .dtpOff(elf), + .tp_off => .tpOff(elf), + .size32 => MachineRelocType.size32(elf) orelse continue :rel .illegal, + .size64 => MachineRelocType.size64(elf) orelse continue :rel .illegal, + }; + const target_dynsym: u32 = switch (want_dynamic_reloc) { + .illegal => unreachable, // handled above + .relative => 0, + else => reloc.target.unwrap().global.dynsymIndex(elf).?, + }; + + try elf.shndx.rela_dyn.relaEnsureAdditionalCapacity(elf, 1); + reloc.rela_index = elf.shndx.rela_dyn.relaAddOneAssumeCapacity(elf, .{ + .type = rel_type, + .offset = elf.getNodeVAddr(reloc_node) + reloc.offset, + .raw_sym_index = target_dynsym, + .addend = reloc.addend.normal, + }).toOptional(); + if (is_textrel) elf.textrel_count += 1; + } }; /// A relocation targeting an arbitrary node (within a section) with a fixed addend. @@ -2909,9 +3120,7 @@ fn addGlobalSymbol(elf: *Elf, opts: Symbol.Global.AddOptions) (error{ } } - if (elf.mergeGlobalSymbolVisibility(gsi, opts.visibility)) { - force_apply_relocs = true; - } + elf.mergeGlobalSymbolVisibility(gsi, opts.visibility); // There's already a symbol with this name, we just need to potentially set its value, and // to update its bind and visibility. @@ -3167,14 +3376,23 @@ fn updateGlobalDynamic(elf: *Elf, orig_gsi: Symbol.Global.Index, force_apply_rel }, }; - const omit_dynsym: bool = omit: { - if (elf.shndx.dynsym == .UNDEF) break :omit true; - break :omit switch (visibility) { - .HIDDEN, .INTERNAL => true, - .DEFAULT, .PROTECTED => false, - }; + const have_dynsym: bool = switch (elf.shndx.dynsym) { + .UNDEF => false, + else => switch (visibility) { + .HIDDEN, .INTERNAL => false, + // An undefined protected dynamic symbol would be pointless, because protected symbols + // don't bind to external definitions; so only add to dynsym if we have a definition. + .PROTECTED => gsi.defined(elf), + // Executables conventionally do not put symbols in their dynamic symbol table unless a + // definition is seen, while dynamic libraries just chuck 'em all in. + .DEFAULT => switch (elf.base.comp.config.output_mode) { + .Exe => gsi.defined(elf) or maybe_dso_global != null, + .Lib => true, + .Obj => unreachable, // we have a dynamic symbol table + }, + }, }; - if (omit_dynsym) { + if (!have_dynsym) { if (try elf.deleteOwnedDynsym(gsi)) { apply_relocs = true; } @@ -3201,9 +3419,14 @@ fn updateGlobalDynamic(elf: *Elf, orig_gsi: Symbol.Global.Index, force_apply_rel if (elf.ehdrType() != .REL) { assert(elf.classifySymbolValue(.global(gsi)) != .dynamic); } - gsi.deleteDynamicTargetRelocs(elf); + + if (try gsi.updateDynamicTargetRelocs(elf)) { + apply_relocs = true; + } if (maybe_alias_gsi) |alias_gsi| { - alias_gsi.deleteDynamicTargetRelocs(elf); + if (try alias_gsi.updateDynamicTargetRelocs(elf)) { + apply_relocs = true; + } } if (apply_relocs) { @@ -3462,10 +3685,12 @@ fn updateGlobalDynamic(elf: *Elf, orig_gsi: Symbol.Global.Index, force_apply_rel _ = elf.unknown_globals.swapRemove(gsi); } - if (elf.classifySymbolValue(.global(gsi)) != .dynamic) { - gsi.deleteDynamicTargetRelocs(elf); - if (maybe_alias_gsi) |alias_gsi| { - alias_gsi.deleteDynamicTargetRelocs(elf); + if (try gsi.updateDynamicTargetRelocs(elf)) { + apply_relocs = true; + } + if (maybe_alias_gsi) |alias_gsi| { + if (try alias_gsi.updateDynamicTargetRelocs(elf)) { + apply_relocs = true; } } @@ -3588,15 +3813,12 @@ fn setGlobalSymbolBind( /// undefined---their visibility values are combined to determine the resulting visibility. /// /// Every call to this function *must* be followed by a call to `updateGlobalDynamic` since changes -/// to symbol visibility can affect the dynamic symbol table entry. If this function returns `true`, -/// then that `updateGlobalDynamic` call must have the `force_apply_relocs` argument set to `true`, -/// because this symbol's classification has changed (it is no longer a dynamic symbol) so -/// relocations targeting the symbol must be re-applied. +/// to symbol visibility can affect the dynamic symbol table entry. fn mergeGlobalSymbolVisibility( elf: *Elf, gsi: Symbol.Global.Index, other_visibility: std.elf.STV, -) bool { +) void { const old_visibility: std.elf.STV = switch (elf.symPtr(gsi.ptr(elf).symtab_index)) { inline else => |sym| elf.targetLoad(&sym.other).visibility, }; @@ -3628,13 +3850,13 @@ fn mergeGlobalSymbolVisibility( ), } - if (elf.ehdrType() == .REL) { - return false; - } + if (elf.ehdrType() == .REL) return; // We also need to deal with a concept I call "symbol demotion". If a symbol is `STV_HIDDEN` or // `STV_INTERNAL`, and we're emitting an ELF module (executable or shared object, as opposed to // a relocatable), then the symbol should be "demoted" to binding `STB_LOCAL` in the output. + // TODO: after the latest batch of refactors, I'm unsure if this logic should be here anymore. I + // think it should probably live in `updateGlobalDynamic` instead. const old_demoted: bool = switch (old_visibility) { .INTERNAL, .HIDDEN => true, .DEFAULT, .PROTECTED => false, @@ -3652,16 +3874,6 @@ fn mergeGlobalSymbolVisibility( } elf.moveDemotedGlobal(gsi); } - - const old_protected: bool = switch (old_visibility) { - .PROTECTED, .INTERNAL, .HIDDEN => true, - .DEFAULT => false, - }; - const new_protected: bool = switch (new_visibility) { - .PROTECTED, .INTERNAL, .HIDDEN => true, - .DEFAULT => false, - }; - return old_protected != new_protected; } /// If a symbol which was STB_GLOBAL/STB_WEAK becomes STB_LOCAL (see `mergeGlobalSymbolVisibility`), /// the symbol must be moved from the "globals" part of the symtab to the "locals" part, because ELF @@ -4038,85 +4250,52 @@ const Symbol = struct { return null; } - /// Scans through all relocations targeting `gsi`, deletes their dynamic relocation - /// entries, and adds `R_*_RELATIVE` relocation entries as needed. + /// Updates the dynamic relocations associated with relocations targeting `gsi`, adding + /// or removing them as needed based on the current classification of `gsi`. /// - /// Asserts we are creating a DSO. - fn deleteDynamicTargetRelocs(gsi: Symbol.Global.Index, elf: *Elf) void { - if (elf.shndx.dynamic == .UNDEF) return; + /// Returns `true` if any relocations were updated, in which case relocations targeting + /// `gsi` must be re-applied by the caller. + fn updateDynamicTargetRelocs(gsi: Symbol.Global.Index, elf: *Elf) Error!bool { + if (elf.shndx.dynamic == .UNDEF) return false; const sym_id: Symbol.Id = .global(gsi); + switch (elf.classifySymbolValue(sym_id)) { .static => { if (!gsi.ptr(elf).status.any_dynamic_target_relocs and !gsi.ptr(elf).status.any_static_relative_target_relocs) { - return; + return false; } - gsi.ptr(elf).status.any_dynamic_target_relocs = false; - gsi.ptr(elf).status.any_static_relative_target_relocs = false; - gsi.ptr(elf).status.any_static_target_relocs = true; }, .static_relative => { if (!gsi.ptr(elf).status.any_dynamic_target_relocs and !gsi.ptr(elf).status.any_static_target_relocs) { - return; + return false; + } + }, + .dynamic => { + if (!gsi.ptr(elf).status.any_static_target_relocs and + !gsi.ptr(elf).status.any_static_relative_target_relocs) + { + return false; } - gsi.ptr(elf).status.any_dynamic_target_relocs = false; - gsi.ptr(elf).status.any_static_target_relocs = false; - gsi.ptr(elf).status.any_static_relative_target_relocs = true; }, - // TODO: this function needs to support a symbol becoming dynamic which wasn't - // previously dynamic (and when it does, the function should be renamed to - // `updateTargetOutputRelocs`). But for now, this isn't supported. - .dynamic => unreachable, } + + gsi.ptr(elf).status.any_static_target_relocs = false; + gsi.ptr(elf).status.any_static_relative_target_relocs = false; + gsi.ptr(elf).status.any_dynamic_target_relocs = false; + var ri = sym_id.index(elf).ptr(elf).first_target_reloc; while (ri != .none) { const reloc = ri.get(elf); assert(reloc.target == sym_id); - reloc.deleteOutputRel(elf); + try reloc.updateDynamicReloc(elf); ri = reloc.next; } - switch (elf.classifySymbolValue(sym_id)) { - .static => return, - .static_relative => {}, - .dynamic => unreachable, - } - // We removed the symbol relocations, now add R_*_RELATIVE relocations where needed. - ri = sym_id.index(elf).ptr(elf).first_target_reloc; - while (ri != .none) { - const reloc = ri.get(elf); - ri = reloc.next; - assert(reloc.target == sym_id); - switch (reloc.type.target) { - // Only relocations which resolve to absolute addresses require runtime - // `R_*_RELATIVE` relocations. - .special, - .pltrel, - .rel, - .dtpoff, - .tpoff, - .size, - => continue, - .abs, .pltabs => {}, - } - if (!reloc.type.action.simple.dest.isAddr(elf)) continue; - const node = reloc.node.unwrap().?; - switch (elf.nodeWantsDsoRelocation(node)) { - .no => continue, - .yes_textrel => elf.textrel_count += 1, - .yes => {}, - } - // There is capacity for a relocation because we just deleted one earlier. - reloc.rela_index = elf.shndx.rela_dyn.relaAddOneAssumeCapacity(elf, .{ - .type = .relative(elf), - .offset = elf.getNodeVAddr(node) + reloc.offset, - .raw_sym_index = 0, - .addend = 0, - }).toOptional(); - } + return true; } const Optional = enum(u32) { @@ -4370,61 +4549,53 @@ fn classifySymbolValue(elf: *Elf, sym: Symbol.Id) enum { .EXEC => false, }; - if (elf.shndx.dynamic == .UNDEF) { - // This is a static non-PIE executable---every symbol has a statically known value. - return .static; - } - const resolved_sym = sym.resolveAlias(elf); - const shndx: Section.Index, const visibility: std.elf.STV = switch (elf.symPtr(resolved_sym.index(elf))) { + const symtab_shndx: Section.Index, const visibility: std.elf.STV = switch (elf.symPtr(resolved_sym.index(elf))) { inline else => |sym_ptr| .{ .fromSection(elf.targetLoad(&sym_ptr.shndx)), elf.targetLoad(&sym_ptr.other).visibility, }, }; - switch (resolved_sym.unwrap()) { - .local => { - assert(shndx != .UNDEF); + const shndx: Section.Index = switch (resolved_sym.unwrap()) { + .local => shndx: { + assert(symtab_shndx != .UNDEF); assert(visibility == .DEFAULT); + break :shndx symtab_shndx; }, - .global => |gsi| if (visibility == .DEFAULT and comp.config.output_mode != .Exe) { - // An unprotected symbol in a DSO which is not an executable is subject to runtime - // preemption, so a dynamic relocation is required for it even if we have a definition. - return .dynamic; - } else if (elf.copied_globals.contains(gsi)) { - // This becomes a locally-defined symbol in `.data`. - return if (runtime_load_addr) .static_relative else .static; - }, - } - - return switch (shndx) { - .UNDEF => switch (visibility) { - .DEFAULT => if (comp.config.link_mode == .static and comp.config.output_mode == .Exe) { - assert(comp.config.pie); // non-PIE static exe should not have a `.dynamic` section - // This is a static PIE---we'll see a definition at some point, so there'd be no - // point in adding an invalid dynamic relocation (the only valid dynamic relation - // type in a static PIE is `R_*_RELATIVE`). - return .static; - } else .dynamic, // external symbol - - // If the symbol *cannot* be external, then there's no point making a dynamic relocation - // now---if linking succeeds we won't need anything more than perhaps an `R_*_RELATIVE`. - .INTERNAL, .HIDDEN, .PROTECTED => .static, - }, + .global => |gsi| shndx: { + const shndx: Section.Index = switch (symtab_shndx) { + .UNDEF => if (elf.copied_globals.contains(gsi)) .data else .UNDEF, + else => |shndx| shndx, + }; - .ABS => .static, + // Check for dynamic symbols before falling into the static path. + if (gsi.dynsymIndex(elf) != null and visibility == .DEFAULT) { + if (shndx == .UNDEF) { + // There is no local definition of this symbol. + return .dynamic; + } else if (comp.config.output_mode != .Exe) { + // There is a local definition, but it is preemptible at runtime. + return .dynamic; + } + } - else => if (runtime_load_addr and - shndx.flags(elf).ALLOC and - !shndx.flags(elf).TLS) - { - return .static_relative; - } else { - return .static; + break :shndx shndx; }, }; + + if (runtime_load_addr and + shndx != .UNDEF and + shndx != .ABS and + shndx.flags(elf).ALLOC and + !shndx.flags(elf).TLS) + { + // This symbol's value is a virtual address relative to a runtime-known load address. + return .static_relative; + } + + return .static; } pub fn symbolForAtom(elf: *Elf, atom: link.File.AtomId) link.File.SymbolId { @@ -4994,6 +5165,8 @@ fn create( .overflowed_reloc_count = 0, .misaligned_reloc_count = 0, + .illegal_dynamic_type_reloc_count = 0, + .illegal_dynamic_section_reloc_count = 0, .const_prog_node = .none, .input_prog_node = .none, @@ -9701,18 +9874,21 @@ fn addSymbolRelocAssumeCapacityInner( ) Error!void { assert(elf.ehdrType() != .REL); - const rela_index: Section.RelaIndex.Optional = r: { - if (elf.shndx.dynamic == .UNDEF) break :r .none; - - // If we emit a runtime relocation entry, its `offset` is a virtual address, so we need to - // determine the vaddr of `node`. - const node_vaddr = elf.getNodeVAddr(node); - - // If this is `true`, we will try to create a copy relocation for the target symbol if it is - // not locally defined. If the relocation value is always computed from the target symbol's - // value (even for an external target symbol), and if the target symbol might be of type - // STT_OBJECT, this should probably be `true`. - const try_copy_reloc: bool = switch (@"type".target) { + request_static_target: { + if (elf.shndx.dynamic == .UNDEF) break :request_static_target; + const gsi: Symbol.Global.Index = switch (target.unwrap()) { + .local => break :request_static_target, + .global => |gsi| gsi, + }; + // The target symbol may be dynamic, in which case its value cannot be resolved statically. + // However, there are strategies (such as copy relocations) the linker can employ which can + // effectively convert dynamic symbols into static symbols. If statically resolving this + // relocation would require knowing the symbol value, we can set a flag so that those + // strategies are used when possible to make this relocation statically resolvable. + // + // (This `switch` should return `false` for TLS relocations, because we can't do anything + // about TLS symbols being dynamic.) + const want_static_value: bool = switch (@"type".target) { .rel, .abs => true, .pltrel, @@ -9741,92 +9917,13 @@ fn addSymbolRelocAssumeCapacityInner( => false, }, }; - if (try_copy_reloc) switch (target.unwrap()) { - .local => {}, - .global => |gsi| if (!gsi.ptr(elf).status.want_static_value) { - gsi.ptr(elf).status.want_static_value = true; - try elf.updateGlobalDynamic(gsi, false); - }, - }; + if (!want_static_value) break :request_static_target; - classify: switch (elf.classifySymbolValue(target)) { - .static => { - switch (target.unwrap()) { - .local => {}, - .global => |gsi| gsi.ptr(elf).status.any_static_target_relocs = true, - } - break :r .none; - }, - .static_relative => { - switch (target.unwrap()) { - .local => {}, - .global => |gsi| gsi.ptr(elf).status.any_static_relative_target_relocs = true, - } - switch (@"type".target) { - // Only relocations which resolve to absolute addresses require runtime - // `R_*_RELATIVE` relocations. - .special, - .pltrel, - .rel, - .dtpoff, - .tpoff, - .size, - => break :r .none, - - .abs, .pltabs => {}, - } - if (!@"type".action.simple.dest.isAddr(elf)) break :r .none; - switch (elf.nodeWantsDsoRelocation(node)) { - .no => break :r .none, - .yes => {}, - .yes_textrel => elf.textrel_count += 1, - } - break :r elf.shndx.rela_dyn.relaAddOneAssumeCapacity(elf, .{ - .type = .relative(elf), - .offset = node_vaddr + offset, - .raw_sym_index = 0, - .addend = 0, - }).toOptional(); - }, - .dynamic => { - target.unwrap().global.ptr(elf).status.any_dynamic_target_relocs = true; - const dynamic_reloc_type: MachineRelocType = switch (@"type".target) { - // PLT relocations targeting dynamic symbols actually target that symbol's PLT - // entry, so we should emit an `R_*_RELATIVE` relocation instead. - .pltabs => continue :classify .static_relative, - // ...although PC-relative PLT relocations don't even need that! - .pltrel => break :r .none, - // Weird sizes or computations are not supported as runtime relocations. - .special => break :r .none, - // Relative addresses are not supported as runtime relocations. - .rel => break :r .none, - - // On the few targets supporting size relocations, they are valid at runtime. - .size => switch (@"type".action.simple.dest) { - .@"32" => MachineRelocType.size32(elf) orelse break :r .none, - .@"64" => MachineRelocType.size64(elf) orelse break :r .none, - else => break :r .none, - }, - // Absolute addresses and TLS offsets can be lowered at runtime provided they - // are address-sized. - .dtpoff => if (@"type".action.simple.dest.isAddr(elf)) .dtpOff(elf) else break :r .none, - .tpoff => if (@"type".action.simple.dest.isAddr(elf)) .tpOff(elf) else break :r .none, - .abs => if (@"type".action.simple.dest.isAddr(elf)) .absAddr(elf) else break :r .none, - }; - switch (elf.nodeWantsDsoRelocation(node)) { - .no => break :r .none, - .yes => {}, - .yes_textrel => elf.textrel_count += 1, - } - break :r elf.shndx.rela_dyn.relaAddOneAssumeCapacity(elf, .{ - .type = dynamic_reloc_type, - .offset = node_vaddr + offset, - .raw_sym_index = target.unwrap().global.dynsymIndex(elf).?, - .addend = addend.normal, - }).toOptional(); - }, + if (!gsi.ptr(elf).status.want_static_value) { + gsi.ptr(elf).status.want_static_value = true; + try elf.updateGlobalDynamic(gsi, false); } - }; + } const ri: SymbolReloc.Index = @fromBackingInt(@intCast(elf.symbol_relocs.items.len)); const first_target_reloc = &target.index(elf).ptr(elf).first_target_reloc; @@ -9841,12 +9938,13 @@ fn addSymbolRelocAssumeCapacityInner( .type = @"type", .next = next, .prev = .none, - .rela_index = rela_index, + .rela_index = .none, .result = .ok, }); if (@"type".dependsOnTlsSize(elf)) { elf.tls_size_symbol_relocs.putAssumeCapacityNoClobber(ri, {}); } + try ri.get(elf).updateDynamicReloc(elf); // Actually apply the new relocation! ri.get(elf).apply(elf); @@ -10177,18 +10275,19 @@ fn updateGotEntry(elf: *Elf, got_index: usize) void { }; } -/// If `node` cannot contain runtime relocations, returns `.no`. +/// If `node` cannot contain dynamic relocations because it is not loaded into memory at runtime, +/// returns `.not_alloc`. /// -/// If `node` can contain runtime relocations, returns `.yes_textrel` if such a relocation requires -/// the presence of a `DT_TEXTREL` dynamic entry, or `.yes` otherwise. -fn nodeWantsDsoRelocation(elf: *Elf, node: MappedFile.Node.Index) enum { yes, yes_textrel, no } { +/// Otherwise, returns `.ok_textrel` if placing a dynamic relocation in this node requires the +/// presence of a `DT_TEXTREL` dynamic entry, or `.ok` otherwise. +fn nodeAllowsDynamicReloc(elf: *Elf, node: MappedFile.Node.Index) enum { ok, ok_textrel, not_alloc } { const shndx = elf.getNodeShndx(node); const shf: std.elf.SHF = switch (elf.shdrPtr(shndx)) { inline else => |shdr| elf.targetLoad(&shdr.flags).shf, }; - if (!shf.ALLOC) return .no; - if (!shf.WRITE) return .yes_textrel; - return .yes; + if (!shf.ALLOC) return .not_alloc; + if (!shf.WRITE) return .ok_textrel; + return .ok; } pub fn updateNav(elf: *Elf, pt: Zcu.PerThread, nav_index: InternPool.Nav.Index) link.Error!void { @@ -10209,15 +10308,8 @@ fn updateNavInner(elf: *Elf, pt: Zcu.PerThread, nav_index: InternPool.Nav.Index) const nav = ip.getNav(nav_index); const mod = zcu.fileByIndex(nav.srcInst(ip).resolveFile(ip)).mod.?; switch (ip.indexToKey(nav.resolved.?.value)) { - .@"extern" => |@"extern"| { + .@"extern" => { if (mod.strip or elf.ehdrMachine() != .X86_64) return; - const si = try elf.externSymbol(.{ - .name = @"extern".name.toSlice(ip), - .lib_name = @"extern".lib_name.toSlice(ip), - .type = elf.navType(nav.resolved.?), - .linkage = @"extern".linkage, - .visibility = @"extern".visibility, - }); const dwarf_gi = try elf.dwarf.getGlobal(nav_index); const dwarf_global = dwarf_gi.get(&elf.dwarf); const debug_info_ni = dwarf_global.debug_info_ni.unwrap().?; @@ -10226,10 +10318,7 @@ fn updateNavInner(elf: *Elf, pt: Zcu.PerThread, nav_index: InternPool.Nav.Index) debug_info_ni.writer(gpa, &elf.mf, &di_nw); defer di_nw.deinit(); elf.resetNodeRelocs(debug_info_ni); - try elf.dwarf.updateExtern(pt, &di_nw, switch (elf.ehdrMachine()) { - else => unreachable, - .X86_64 => .x86_64, - }, si, nav_index); + try elf.dwarf.updateExtern(pt, &di_nw, nav_index); }, else => if (Type.fromInterned(nav.resolved.?.type).hasRuntimeBits(zcu)) { const nmi = try elf.navMapIndex(zcu, nav_index); @@ -10757,10 +10846,28 @@ fn flushInner( defer elf.mf.nodes_lock.unlock(); if (elf.overflowed_reloc_count > 0) { - diags.addError("failed to apply {d} relocations: overflow", .{elf.overflowed_reloc_count}); + diags.addError( + "failed to apply {d} relocations: overflow", + .{elf.overflowed_reloc_count}, + ); } if (elf.misaligned_reloc_count > 0) { - diags.addError("failed to apply {d} relocations: misaligned value", .{elf.misaligned_reloc_count}); + diags.addError( + "failed to apply {d} relocations: misaligned value", + .{elf.misaligned_reloc_count}, + ); + } + if (elf.illegal_dynamic_type_reloc_count > 0) { + diags.addError( + "refusing to emit {d} dynamic relocations with illegal types", + .{elf.illegal_dynamic_type_reloc_count}, + ); + } + if (elf.illegal_dynamic_section_reloc_count > 0) { + diags.addError( + "refusing to emit {d} dynamic relocations in non-allocated sections", + .{elf.illegal_dynamic_section_reloc_count}, + ); } if (elf.archive) |*archive| { diff --git a/test/debugger/storage b/test/debugger/storage index 4cd4c55f5dece6bcb454b6db6a0101ae1c60500f..cb715854876e1e8cd84bc76ecb0fe6a4d79679bc 100644 --- a/test/debugger/storage +++ b/test/debugger/storage @@ -67,13 +67,9 @@ Process #... stopped (u64) global_threadlocal1 = 0xb4d643528c042121 (u64) global_threadlocal2 = 0x43faea1cf5ad7a22 (u64) extern_const = 0x1b0c91ea0470b0b2 -(u64) extern_const = 0x1b0c91ea0470b0b2 (u64) extern_var = 0x19bc17b3f0b61ebc -(u64) extern_var = 0x19bc17b3f0b61ebc -(u64) extern_threadlocal1 = 0x3034c4ce967ed64d (u64) extern_threadlocal1 = 0x3034c4ce967ed64d (u64) extern_threadlocal2 = 0xfd330ab00b4bc5eb -(u64) extern_threadlocal2 = 0xfd330ab00b4bc5eb (lldb) frame variable --show-all-children --show-types --format hex -- param1 param2 param3 param4 param5 param6 param7 param8 local_comptime_val 'local_comptime_ptr.@"0"' local_comptime_ptr[0] local_const local_var (u64) param1 = 0x6a607e08125c7e00 (u64) param2 = 0x98944cb2a45a8b51 -- 2.54.0 From 7deea319501cee74a46de704521b25ebccfc8e6f Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Sun, 18 Oct 2026 09:51:17 +0100 Subject: [PATCH 10/18] Elf2: improve entry point handling * report undefined entry point symbol * report entry point symbol only defined in input DSO * allow entry point symbol to resolve to default symbol version --- src/link/Elf2.zig | 59 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/src/link/Elf2.zig b/src/link/Elf2.zig index b1d61ed0ceeff127256914fa67e97fa70d9d1474..49916ce2c6bacb512268a581a42e682bb5619959 100644 --- a/src/link/Elf2.zig +++ b/src/link/Elf2.zig @@ -246,6 +246,7 @@ changed_symtab_index: std.array_hash_map.Auto(Symbol.Global.Index, void), /// entry in the `.dynamic` section. This allows adding `DT_TEXTREL` to the output `.dynamic` /// section in `flush` only when it is actually necessary. See also `nodeAllowsDynamicReloc`. textrel_count: u32, +entry_gsi: Symbol.Global.Index.Optional, dwarf: Dwarf, dwarf_shared: std.enums.EnumArray(Dwarf.SharedSection, dwarf_relocs.Shared), @@ -3073,6 +3074,8 @@ fn addGlobalSymbol(elf: *Elf, opts: Symbol.Global.AddOptions) (error{ } || Error)!Symbol.Global.Index { _ = opts.lib_name; // TODO + log.debug("addGlobalSymbol({q})", .{opts.name}); + const gpa = elf.base.comp.gpa; try elf.symtab.ensureUnusedCapacity(gpa, 1); @@ -5138,6 +5141,7 @@ fn create( .section_by_name = .empty, .changed_symtab_index = .empty, .textrel_count = 0, + .entry_gsi = undefined, .dwarf = .init(&elf.base, switch (comp.config.debug_format) { .strip => .@"32", // for .eh_frame @@ -6561,6 +6565,31 @@ fn initHeaders( .code_view => unreachable, } + // If we have an entry point symbol, create the undefined reference now. + elf.entry_gsi = entry: switch (elf.options.entry) { + .default => switch (comp.config.output_mode) { + .Exe => continue :entry .enabled, + .Lib, .Obj => continue :entry .disabled, + }, + .enabled => continue :entry .{ .named = "_start" }, + .disabled => .none, + .named => |sym_name| .wrap(elf.addGlobalSymbol(.{ + .node = .none, + .name = sym_name, + .value = 0, + .size = 0, + .type = .NOTYPE, + .bind = .strong, + .visibility = .DEFAULT, + .shndx = .UNDEF, + }) catch |err| switch (err) { + error.MultipleDefinitions => unreachable, // shndx is undef + error.MultipleDefaultVersions => unreachable, // not a versioned symbol + error.UndefinedDefaultVersion => unreachable, // not a versioned symbol + else => |e| return e, + }), + }; + assert(elf.nodes.len == expected_nodes_len); assert(elf.shdrs.items.len == shnum - 1); // -1 to exclude SHN_UNDEF @@ -10884,20 +10913,22 @@ fn flushInner( elf.flushDynamic(); const entry_addr: u64 = entry: { - const sym_name: []const u8 = name: switch (elf.options.entry) { - .default => switch (comp.config.output_mode) { - .Exe => continue :name .enabled, - .Lib, .Obj => continue :name .disabled, - }, - .disabled => break :entry 0, - .enabled => "_start", - .named => |named| named, - }; - const gsi = elf.globalByName(.{ - .name = sym_name, - .version = null, - }) orelse break :entry 0; - break :entry Symbol.Id.global(gsi).value(elf); + const entry_gsi = elf.entry_gsi.unwrap() orelse break :entry 0; + + // Even though we already reported undefined globals earlier, we also need to check here + // whether the entry point is defined. There are two reasons for this: + // + // * If we are emitting something other than an executable, we skipped the undefined globals + // check, but there could still be an entry point (e.g. due to `-fentry=foo` on the CLI). + // + // * The entry point might be defined in an input DSO, which would satisfy the undefined + // symbol reference but is insufficient here: the entry point needs a local definition. + + if (!entry_gsi.resolveAlias(elf).defined(elf)) { + return diags.fail("undefined entry point symbol '{s}'", .{entry_gsi.name(elf)}); + } + + break :entry Symbol.Id.global(entry_gsi).value(elf); }; switch (elf.ehdrPtr()) { inline else => |ehdr| elf.targetStore(&ehdr.entry, @intCast(entry_addr)), -- 2.54.0 From 149a4fb28c16f13935ed1e178499ba738c881386 Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Sun, 18 Oct 2026 09:51:28 +0100 Subject: [PATCH 11/18] Elf2: tiny error handling fix --- src/link/Elf2.zig | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/link/Elf2.zig b/src/link/Elf2.zig index 49916ce2c6bacb512268a581a42e682bb5619959..3255cad5820a5cbc9fd58e497a482e41ea83e5cb 100644 --- a/src/link/Elf2.zig +++ b/src/link/Elf2.zig @@ -10936,9 +10936,12 @@ fn flushInner( try elf.mf.flush(); - if (elf.options.enable_link_snapshots) - elf.dumpStderr() catch |err| - return diags.fail("dumping link snapshot failed: {t}", .{err}); + if (elf.options.enable_link_snapshots) { + elf.dumpStderr() catch |err| switch (err) { + error.Canceled => |e| return e, + else => |e| return diags.fail("dumping link snapshot failed: {t}", .{e}), + }; + } } pub fn idle(elf: *Elf) link.Error!bool { -- 2.54.0 From f83d28d5c61ae408369be03b0ce3bbbf9c079874 Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Sun, 18 Oct 2026 13:57:23 +0100 Subject: [PATCH 12/18] 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 From c3b8abfc26e5b5e3de79fc4270d4540588541c02 Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Sun, 18 Oct 2026 21:14:03 +0100 Subject: [PATCH 13/18] std.Build.Cache: fix diagnostic printing --- lib/std/Build/Cache.zig | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/std/Build/Cache.zig b/lib/std/Build/Cache.zig index c4d1c106022d436b7b32295c627c0898797b8906..d8f79c54ece069301e065b1bd172fea4e15cb885 100644 --- a/lib/std/Build/Cache.zig +++ b/lib/std/Build/Cache.zig @@ -1727,7 +1727,14 @@ pub const Manifest = struct { pub fn format(this: @This(), w: *Io.Writer) Io.Writer.Error!void { switch (this) { .none => return w.writeAll("none"), - else => |err, tag| return w.print("{t}: {t}", .{ tag, err }), + + .open_file, + .open_dir, + .stat_file, + .stat_dir, + .read_file, + .read_dir, + => |err, tag| return w.print("{t}: {t}", .{ tag, err }), } } }; -- 2.54.0 From 19c4660c77130e78503e2e7e9e4ad2ec8e0985d6 Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Sun, 18 Oct 2026 21:15:29 +0100 Subject: [PATCH 14/18] Maker: fix error handling bugs in `configure` --- lib/compiler/Maker.zig | 62 +++++++++++++++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/lib/compiler/Maker.zig b/lib/compiler/Maker.zig index 66ba521c040d736e679af5bf58773bfcecfc55d2..607132eaeaed88a5b115e3783cfa918969873b65 100644 --- a/lib/compiler/Maker.zig +++ b/lib/compiler/Maker.zig @@ -1101,6 +1101,9 @@ pub fn main(init: process.Init.Minimal) !void { if (help_menu) printUsageWithoutConfiguration(&graph, "build.zig compilation failure"); break :c true; }, + error.StdoutWriteFailed => { + fatal("failed writing to stdout: {t}", .{stdout_writer_allocation.err.?}); + }, else => |e| w: { log.err("configuration failed: {t}", .{e}); break :w false; @@ -1163,7 +1166,14 @@ const ConfigureOptions = struct { error_bundle: ?*ErrorBundle = null, }; -fn configure(graph: *Graph, options: ConfigureOptions) !ScannedConfig { +const ConfigureError = Allocator.Error || Io.Cancelable || error{ + Unexpected, + AlreadyReported, + FailedButCacheIntact, + StdoutWriteFailed, +}; + +fn configure(graph: *Graph, options: ConfigureOptions) ConfigureError!ScannedConfig { const configure_argv = options.configure_argv; const gpa = graph.cache.gpa; const io = graph.io; @@ -1328,7 +1338,19 @@ fn configure(graph: *Graph, options: ConfigureOptions) !ScannedConfig { defer job_queue.deinit(); if (options.system_pkg_dir_path == null) { - try http_client.initDefaultProxies(arena, &graph.environ_map); + http_client.initDefaultProxies(arena, &graph.environ_map) catch |err| switch (err) { + error.NameTooLong, + error.InvalidHostName, + error.InvalidPort, + error.InvalidFormat, + error.UnexpectedCharacter, + error.UriMissingHost, + => |e| { + log.err("failed to initialize HTTP client: {t}", .{e}); + return error.AlreadyReported; + }, + else => |e| return e, + }; } try job_queue.all_fetches.ensureUnusedCapacity(gpa, 1); @@ -1410,10 +1432,10 @@ fn configure(graph: *Graph, options: ConfigureOptions) !ScannedConfig { .writer = initStdoutWriter(io), .mode = try .detect(io, .stdout(), NO_COLOR, CLICOLOR_FORCE), }, arena, graph.global_cache_root, &job_queue, &fetch, &fork_set) catch |err| switch (err) { - error.WriteFailed => return stdout_writer_allocation.err.?, + error.WriteFailed => return error.StdoutWriteFailed, else => |e| return e, }; - try stdout_writer_allocation.flush(); + stdout_writer_allocation.flush() catch return error.StdoutWriteFailed; process.exit(0); } @@ -1444,11 +1466,17 @@ fn configure(graph: *Graph, options: ConfigureOptions) !ScannedConfig { .root_dir = graph.local_cache_root, .sub_path = try arena.print("o/{s}/dependencies.zig", .{&hex_digest}), }; - var atomic_file = try dependencies_zig_path.root_dir.handle.createFileAtomic( + var atomic_file = dependencies_zig_path.root_dir.handle.createFileAtomic( io, dependencies_zig_path.sub_path, .{ .make_path = true, .replace = true }, - ); + ) catch |err| switch (err) { + error.Canceled => |e| return e, + else => |e| { + log.err("failed to write {qf}: {t}", .{ dependencies_zig_path, e }); + return error.AlreadyReported; + }, + }; defer atomic_file.deinit(io); atomic_file.file.writeStreamingAll(io, dependencies_source.items) catch |err| fatal("writing dependencies.zig contents: {t}", .{err}); @@ -1596,11 +1624,17 @@ fn configure(graph: *Graph, options: ConfigureOptions) !ScannedConfig { .root_dir = graph.local_cache_root, .sub_path = try arena.print("tmp" ++ Dir.path.sep_str ++ "{x}", .{randInt(io, u64)}), }; - const config_tmp_file: Io.File = try config_tmp_path.root_dir.handle.createFile( + const config_tmp_file: Io.File = config_tmp_path.root_dir.handle.createFile( io, config_tmp_path.sub_path, .{ .read = true, .exclusive = true }, - ); + ) catch |err| switch (err) { + error.Canceled => |e| return e, + else => |e| { + log.err("failed to create {qf}: {t}", .{ config_tmp_path, e }); + return error.AlreadyReported; + }, + }; defer config_tmp_file.close(io); const term = term: { @@ -1663,11 +1697,19 @@ fn configure(graph: *Graph, options: ConfigureOptions) !ScannedConfig { if (config_man) |man| for (configuration.path_deps) |path_dep| { const path = try confPathDepToCachePath(arena, graph, &configuration, path_dep); - try man.addDiscoveredPath(.{ + var diag: Cache.Manifest.AddDiscoveredPathDiagnostic = undefined; + man.addDiscoveredPath(.{ .discovered_path = .{ .unresolved = path }, .handle = if (path_dep.flags.is_directory) .{ .dir = null } else .{ .file = null }, .metadata_only = path_dep.flags.metadata_only, - }); + .diagnostic = &diag, + }) catch |err| switch (err) { + error.FileSystemFailure => { + log.err("failed to register dependency on {qf}: {f}", .{ path, diag }); + return error.AlreadyReported; + }, + else => |e| return e, + }; }; // If it is poisoned, there is no point in moving it to cached -- 2.54.0 From 067f8dcb375aae6cfe1fdc66cf70372067382c49 Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Sun, 18 Oct 2026 22:49:16 +0100 Subject: [PATCH 15/18] std.Build.Cache: support depending on nonexistent file/directory --- lib/compiler/Maker.zig | 8 +- lib/compiler/Maker/Step.zig | 17 +++- lib/std/Build.zig | 10 ++- lib/std/Build/Cache.zig | 151 +++++++++++++++++++++--------------- 4 files changed, 119 insertions(+), 67 deletions(-) diff --git a/lib/compiler/Maker.zig b/lib/compiler/Maker.zig index 607132eaeaed88a5b115e3783cfa918969873b65..e532c2467ec5140c26b6e4585b689263db3f1dee 100644 --- a/lib/compiler/Maker.zig +++ b/lib/compiler/Maker.zig @@ -2336,6 +2336,7 @@ fn cacheCatOne(input_hex_digest: []const u8, contents: []const u8, writer: *Io.W try file_obj.field("digest", @as([]const u8, &hex_digest), .{}); if (file.flags.is_directory) try file_obj.field("directory", true, .{}); if (file.flags.metadata_only) try file_obj.field("metadata", true, .{}); + if (!file.flags.exists) try file_obj.field("exists", false, .{}); try file_obj.field("prefix", file.flags.prefix, .{}); try file_obj.field("path", path, .{}); try file_obj.end(); @@ -3370,7 +3371,12 @@ fn printStepStatus(maker: *Maker, step_index: Configuration.Step.Index, stderr: try writer.writeAll(" invalid_manifest"); try stderr.setColor(.green); }, - .path_deleted, .directory_status_changed, .metadata_changed, .contents_changed => |path, tag| { + .path_created, + .path_deleted, + .directory_status_changed, + .metadata_changed, + .contents_changed, + => |path, tag| { try stderr.setColor(.blue); try writer.print(" {t}: {s}", .{ tag, path }); try stderr.setColor(.green); diff --git a/lib/compiler/Maker/Step.zig b/lib/compiler/Maker/Step.zig index 791d5b402cb21a526a46bca54eeadcc8b79c7e9d..9ce6f3cb50b618c7488c6043911921deeebb6336 100644 --- a/lib/compiler/Maker/Step.zig +++ b/lib/compiler/Maker/Step.zig @@ -84,6 +84,7 @@ const ResultCacheStatus = union(enum) { incomplete_manifest, invalid_manifest, path_deleted: [*:0]const u8, + path_created: [*:0]const u8, directory_status_changed: [*:0]const u8, metadata_changed: [*:0]const u8, contents_changed: [*:0]const u8, @@ -815,7 +816,13 @@ fn setResultCacheStatus(s: *Step, man: *const Cache.Manifest, status: Cache.Mani .hit => .hit, .incomplete_manifest => .incomplete_manifest, .invalid_manifest => .invalid_manifest, - inline .path_deleted, .directory_status_changed, .metadata_changed, .contents_changed => |off, tag| @unionInit( + + inline .path_created, + .path_deleted, + .directory_status_changed, + .metadata_changed, + .contents_changed, + => |off, tag| @unionInit( ResultCacheStatus, @tagName(tag), try gpa.dupeSentinel(u8, off.path(man.contents.items), 0), @@ -827,7 +834,13 @@ fn setResultCacheStatus(s: *Step, man: *const Cache.Manifest, status: Cache.Mani fn clearResultCacheStatus(s: *Step, gpa: Allocator) void { switch (s.result_cache_status) { .hit, .incomplete_manifest, .invalid_manifest => {}, - .path_deleted, .directory_status_changed, .metadata_changed, .contents_changed => |path| { + + .path_created, + .path_deleted, + .directory_status_changed, + .metadata_changed, + .contents_changed, + => |path| { gpa.free(std.mem.sliceTo(path, 0)); }, } diff --git a/lib/std/Build.zig b/lib/std/Build.zig index 7e52047b126e83bf914d67a024e061bac0bbd573..a3c38262f3ea0da7317776fa73bb45d548c06eed 100644 --- a/lib/std/Build.zig +++ b/lib/std/Build.zig @@ -2643,9 +2643,11 @@ pub fn systemIntegrationOption( /// Indicates that the build.zig logic depends on a particular file's contents. /// -/// If the file is created, deleted, or has its contents changed, the configure -/// phase will be repeated. If the inode or mtime change, but the file contents -/// remain the same, it will not cause the configure logic to be repeated. +/// The file must exist when this function is called. +/// +/// If the file is deleted, or has its contents changed, the configure phase +/// will be repeated. If the inode or mtime change, but the file contents remain +/// the same, it will not cause the configure logic to be repeated. /// /// This is an alternative to `Graph.poisonCache` that avoids making every invocation /// of `zig build` into a cache miss. @@ -2695,6 +2697,8 @@ pub fn dependOnFileMetadata(b: *Build, lazy_path: LazyPath) void { /// Indicates that the build.zig logic depends on a particular directory's entries. /// +/// The directory must exist when this function is called. +/// /// This is an alternative to `Graph.poisonCache` that avoids making every invocation /// of `zig build` into a cache miss. /// diff --git a/lib/std/Build/Cache.zig b/lib/std/Build/Cache.zig index d8f79c54ece069301e065b1bd172fea4e15cb885..43a28b1d504dd86e887b82006314c6a3a10bcc9f 100644 --- a/lib/std/Build/Cache.zig +++ b/lib/std/Build/Cache.zig @@ -56,7 +56,7 @@ pub fn prefixes(cache: *const Cache) []const Directory { return cache.prefixes_buffer[0..cache.prefixes_len]; } -const PrefixIndex = u6; +const PrefixIndex = u3; const PrefixedPath = struct { prefix: PrefixIndex, @@ -453,7 +453,9 @@ pub const Manifest = struct { pub const Flags = packed struct(u8) { is_directory: bool, metadata_only: bool, + exists: bool, prefix: PrefixIndex, + _: u2 = 0, }; /// Prefixes path names in encoded directory contents. Starts numbering @@ -576,6 +578,7 @@ pub const Manifest = struct { /// Returns true if the stat was changed. Updates the `file` with the new stat value. fn setStatChanged(file: *File, m: *Manifest, s: Stat) Io.Cancelable!bool { + assert(file.flags.exists); if (s.size == file.size and s.mtime.nanoseconds == file.mtime and s.inode == file.inode) @@ -693,12 +696,12 @@ pub const Manifest = struct { }; /// Add a file or directory path as a dependency of process being cached. - /// When `hit` is called, the contents will be checked to ensure + /// When `check` is called, the contents will be checked to ensure /// that it matches the contents from previous times. /// /// The contents of the input file may be requested and subsequently /// obtained via methods of the returned `InputPath.Index` after calling - /// `hit`. + /// `check`. /// /// Contents of a directory are considered to be the sorted list of file /// names of direct entries, separated by null byte. Each file name is @@ -726,6 +729,7 @@ pub const Manifest = struct { .prefix = new_prefix, .is_directory = is_directory, .metadata_only = options.metadata_only, + .exists = true, // modified later if necessary }; const gop = m.files.getOrPutAssumeCapacityContext(new_file_offset, .{ .contents = m.contents.items }); @@ -841,6 +845,7 @@ pub const Manifest = struct { hit, incomplete_manifest, invalid_manifest, + path_created: File.Offset, path_deleted: File.Offset, directory_status_changed: File.Offset, metadata_changed: File.Offset, @@ -853,7 +858,7 @@ pub const Manifest = struct { pub fn format(this: @This(), w: *Io.Writer) Io.Writer.Error!void { switch (this.check_result) { .hit, .incomplete_manifest, .invalid_manifest => return w.writeAll(@tagName(this.check_result)), - .path_deleted, .directory_status_changed, .metadata_changed, .contents_changed => |off| { + .path_created, .path_deleted, .directory_status_changed, .metadata_changed, .contents_changed => |off| { const path = off.path(this.manifest.contents.items); return w.print("{t} {s}", .{ this.check_result, path }); }, @@ -1192,38 +1197,36 @@ pub const Manifest = struct { }; if (!input_path.have_stat and stat_path_ok) { // Since this is an input file, FileNotFound counts as a failure, not a cache miss. - const actual_stat = switch (input_path.getHandle(input_file)) { - .none => parent_dir.statFile(io, file_path, .{}) catch |err| switch (err) { - error.Canceled => |e| return e, - else => |e| return fail(diag, .{ .file_stat = .{ - .file_offset = file_off, - .err = e, - } }), - }, - .file => |opened_file| opened_file.stat(io) catch |err| switch (err) { - error.Canceled => |e| return e, - else => |e| return fail(diag, .{ .file_stat = .{ - .file_offset = file_off, - .err = e, - } }), - }, - .dir => |opened_dir| opened_dir.stat(io) catch |err| switch (err) { - error.Canceled => |e| return e, - else => |e| return fail(diag, .{ .file_stat = .{ - .file_offset = file_off, - .err = e, - } }), - }, + const stat_result = switch (input_path.getHandle(input_file)) { + .none => parent_dir.statFile(io, file_path, .{}), + .file => |opened_file| opened_file.stat(io), + .dir => |opened_dir| opened_dir.stat(io), }; - const actual_is_directory = actual_stat.kind == .directory; - if (actual_is_directory != input_file.flags.is_directory) { - // Since this is an input file, this is a failure, not a cache miss. - return fail(diag, .{ .file_stat = .{ + if (stat_result) |actual_stat| { + const actual_is_directory = actual_stat.kind == .directory; + if (actual_is_directory != input_file.flags.is_directory) { + // Since this is an input file, this is a failure, not a cache miss. + return fail(diag, .{ .file_stat = .{ + .file_offset = file_off, + .err = if (actual_is_directory) error.IsDir else error.NotDir, + } }); + } + try input_file.setStat(m, .init(actual_stat)); + } else |err| switch (err) { + error.Canceled => |e| return e, + + error.FileNotFound => |e| if (input_file.flags.metadata_only) { + input_file.flags.exists = false; + } else return fail(diag, .{ .file_stat = .{ .file_offset = file_off, - .err = if (actual_is_directory) error.IsDir else error.NotDir, - } }); + .err = e, + } }), + + else => |e| return fail(diag, .{ .file_stat = .{ + .file_offset = file_off, + .err = e, + } }), } - try input_file.setStat(m, .init(actual_stat)); } if (input_file.flags.metadata_only) { @@ -1369,6 +1372,9 @@ pub const Manifest = struct { // space to compute the digest in case stat changed, in order to compare with `disk_file.digest`. if (input_path.have_stat) { + if (!disk_file.flags.exists) { + return .{ .path_created = file_off }; + } const changed = try disk_file.setStatChanged(m, input_file.stat()); if (!changed) return .hit; } @@ -1376,29 +1382,28 @@ pub const Manifest = struct { if (disk_file.flags.metadata_only) { if (!input_path.have_stat) { // Since this is an input file, FileNotFound counts as a failure, not a cache miss. - const actual_stat = switch (input_path.getHandle(input_file)) { - .none => parent_dir.statFile(io, file_path, .{}) catch |err| switch (err) { - error.Canceled => |e| return e, - else => |e| return fail(diag, .{ .file_stat = .{ - .file_offset = file_off, - .err = e, - } }), - }, - .file => |opened_file| opened_file.stat(io) catch |err| switch (err) { - error.Canceled => |e| return e, - else => |e| return fail(diag, .{ .file_stat = .{ - .file_offset = file_off, - .err = e, - } }), - }, - .dir => |opened_dir| opened_dir.stat(io) catch |err| switch (err) { - error.Canceled => |e| return e, - else => |e| return fail(diag, .{ .file_stat = .{ + const stat_result = switch (input_path.getHandle(input_file)) { + .none => parent_dir.statFile(io, file_path, .{}), + .file => |opened_file| opened_file.stat(io), + .dir => |opened_dir| opened_dir.stat(io), + }; + const actual_stat = stat_result catch |err| switch (err) { + error.Canceled => |e| return e, + error.FileNotFound => |e| switch (disk_file.flags.exists) { + false => return .hit, + true => return fail(diag, .{ .file_stat = .{ .file_offset = file_off, .err = e, } }), }, + else => |e| return fail(diag, .{ .file_stat = .{ + .file_offset = file_off, + .err = e, + } }), }; + if (!disk_file.flags.exists) { + return .{ .path_created = file_off }; + } // In the other case (have_stat=true), this check is redundant since we already asserted the flags are // identical, which contains the prefix. const actual_is_directory = actual_stat.kind == .directory; @@ -1559,7 +1564,10 @@ pub const Manifest = struct { if (file.flags.metadata_only) { const actual_stat = parent_dir.statFile(io, file_path, .{}) catch |err| switch (err) { - error.FileNotFound => return .{ .path_deleted = file_off }, + error.FileNotFound => switch (file.flags.exists) { + false => return .hit, + true => return .{ .path_deleted = file_off }, + }, error.Canceled => |e| return e, else => |e| return fail(diag, .{ .file_stat = .{ .file_offset = file_off, @@ -1567,6 +1575,10 @@ pub const Manifest = struct { } }), }; + if (!file.flags.exists) { + return .{ .path_created = file_off }; + } + const actual_is_directory = actual_stat.kind == .directory; if (actual_is_directory != file.flags.is_directory) return .{ .directory_status_changed = file_off }; @@ -1576,6 +1588,8 @@ pub const Manifest = struct { return .hit; } + assert(file.flags.exists); // file not existing can only be tracked with metadata_only + if (file.flags.is_directory) { const opened_dir = parent_dir.openDir(io, file_path, .{ .iterate = true, @@ -1774,6 +1788,7 @@ pub const Manifest = struct { .prefix = new_prefix, .is_directory = is_directory, .metadata_only = options.metadata_only, + .exists = true, // modified later if necessary }; const gop = m.files.getOrPutAssumeCapacityContext(new_file_offset, .{ .contents = m.contents.items }); @@ -1817,18 +1832,25 @@ pub const Manifest = struct { } else { const dir = cache.prefixes()[prefix].handle; const sub_path = file_offset.path(m.contents.items); - const handle = dir.openDir(io, sub_path, .{ + if (dir.openDir(io, sub_path, .{ .access_sub_paths = false, .iterate = true, - }) catch |err| switch (err) { + })) |handle| { + defer handle.close(io); + try populateDirectory(m, header, need_stat, handle, options.contents, metadata_only, options.diagnostic); + } else |err| switch (err) { error.Canceled => |e| return e, + error.FileNotFound => |e| if (options.metadata_only) { + header.flags.exists = false; + } else { + if (options.diagnostic) |d| d.* = .{ .open_dir = e }; + return error.FileSystemFailure; + }, else => |e| { if (options.diagnostic) |d| d.* = .{ .open_dir = e }; return error.FileSystemFailure; }, - }; - defer handle.close(io); - try populateDirectory(m, header, need_stat, handle, options.contents, metadata_only, options.diagnostic); + } }, .file => |opt_handle| if (opt_handle) |handle| { @@ -1836,15 +1858,22 @@ pub const Manifest = struct { } else { const dir = cache.prefixes()[prefix].handle; const sub_path = file_offset.path(m.contents.items); - const handle = dir.openFile(io, sub_path, .{ .mode = .read_only }) catch |err| switch (err) { + if (dir.openFile(io, sub_path, .{ .mode = .read_only })) |handle| { + defer handle.close(io); + try populateFile(m, header, need_stat, handle, options.contents, metadata_only, options.diagnostic); + } else |err| switch (err) { error.Canceled => |e| return e, + error.FileNotFound => |e| if (options.metadata_only) { + header.flags.exists = false; + } else { + if (options.diagnostic) |d| d.* = .{ .open_file = e }; + return error.FileSystemFailure; + }, else => |e| { if (options.diagnostic) |d| d.* = .{ .open_file = e }; return error.FileSystemFailure; }, - }; - defer handle.close(io); - try populateFile(m, header, need_stat, handle, options.contents, metadata_only, options.diagnostic); + } }, } } -- 2.54.0 From f67c5e92fc010b4fd43638d9ff9c996de014155d Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Sun, 18 Oct 2026 21:16:17 +0100 Subject: [PATCH 16/18] build.zig: add file/directory dependencies searching for config.h --- build.zig | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/build.zig b/build.zig index e6c41d78c3f675ab8bcaeb267e4c35c1dad674a2..39bb16c0a0b03001e30797a19f33be2e92e764d0 100644 --- a/build.zig +++ b/build.zig @@ -352,8 +352,9 @@ pub fn build(b: *std.Build) !void { const cmake_cfg = if (static_llvm) null else blk: { const io = b.graph.io; const cwd: Io.Dir = .cwd(); - if (findConfigH(b, config_h_path_option)) |config_h_path| { - const file_contents = cwd.readFileAlloc(io, config_h_path, arena, .limited(max_config_h_bytes)) catch unreachable; + if (try findConfigH(b, config_h_path_option)) |config_h_path| { + b.dependOnFileContents(b.graph.cwdRelativePath(config_h_path)); + const file_contents = try cwd.readFileAlloc(io, config_h_path, arena, .limited(max_config_h_bytes)); break :blk parseConfigH(b, file_contents); } else { std.log.warn("config.h could not be located automatically", .{}); @@ -1187,7 +1188,7 @@ const CMakeConfig = struct { const max_config_h_bytes = 1 * 1024 * 1024; -fn findConfigH(b: *std.Build, config_h_path_option: ?[]const u8) ?[]const u8 { +fn findConfigH(b: *std.Build, config_h_path_option: ?[]const u8) std.mem.Allocator.Error!?[]const u8 { const arena = b.graph.arena; const io = b.graph.io; const cwd: Io.Dir = .cwd(); @@ -1205,22 +1206,26 @@ fn findConfigH(b: *std.Build, config_h_path_option: ?[]const u8) ?[]const u8 { var check_dir = fs.path.dirname(b.graph.zig_exe).?; while (true) { - var dir = cwd.openDir(io, check_dir, .{}) catch unreachable; + var dir = cwd.openDir(io, check_dir, .{}) catch |err| { + std.debug.panic("failed to open {q}: {t}", .{ check_dir, err }); + }; defer dir.close(io); // Check if config.h is present in dir var config_h_or_err = dir.openFile(io, "config.h", .{}); if (config_h_or_err) |*file| { file.close(io); - return fs.path.join( + return try fs.path.join( arena, &[_][]const u8{ check_dir, "config.h" }, - ) catch unreachable; - } else |e| switch (e) { + ); + } else |err| switch (err) { error.FileNotFound => {}, - else => unreachable, + else => |e| std.debug.panic("failed to check for config.h in {q}: {t}", .{ check_dir, e }), } + b.dependOnFileMetadata(b.graph.cwdRelativePath(check_dir).path(b, "config.h")); + // Check if we reached the source root by looking for .git, and bail if so var git_dir_or_err = dir.openDir(io, ".git", .{}); if (git_dir_or_err) |*git_dir| { -- 2.54.0 From f7626b95269bc3baad2c46e4d3b7acc16221c424 Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Mon, 19 Oct 2026 11:04:01 +0100 Subject: [PATCH 17/18] Elf2: don't emulate a ZCU input object when building a static library In this case, there is a *literal* ZCU input object in the static library itself. This was causing a safety check to occasionally trip in safe compiler builds. First noticed when debugging an unrelated issue: see https://codeberg.org/ziglang/zig/issues/37327#issuecomment-25643513. --- src/link/Elf2.zig | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/src/link/Elf2.zig b/src/link/Elf2.zig index a6ae8fa69970cfa4060f31adf3516cc58cfae187..a585287261a2d73f365a4cb22ff89f041ddf7e2b 100644 --- a/src/link/Elf2.zig +++ b/src/link/Elf2.zig @@ -8904,24 +8904,27 @@ fn prelinkInner(elf: *Elf) Error!void { if (comp.zcu) |_| self_hosted_codegen: { if (comp.config.use_llvm) break :self_hosted_codegen; - // We're using self-hosted codegen---add an input representing the Zig "object". - try elf.inputs.ensureUnusedCapacity(gpa, 1); - const zcu_name = try std.fmt.allocPrint(gpa, "{s}_zcu", .{comp.root_name}); - defer gpa.free(zcu_name); - const zcu_file_symbol = try elf.addLocalSymbol(.{ - .node = .none, - .name = zcu_name, - .value = 0, - .size = 0, - .type = .FILE, - .shndx = .ABS, - }); - elf.inputs.addOneAssumeCapacity().* = .{ - .path = elf.base.emit, - .member = null, - .extra = .{ .file_symbol = zcu_file_symbol }, - }; - elf.input_pending_index += 1; + if (elf.archive == null) { + // Self-hosted codegen is logically emitting a relocatable which is then immediately + // being linked into the output file, so add that fictional Zig object file input. + try elf.inputs.ensureUnusedCapacity(gpa, 1); + const zcu_name = try std.fmt.allocPrint(gpa, "{s}_zcu", .{comp.root_name}); + defer gpa.free(zcu_name); + const zcu_file_symbol = try elf.addLocalSymbol(.{ + .node = .none, + .name = zcu_name, + .value = 0, + .size = 0, + .type = .FILE, + .shndx = .ABS, + }); + elf.inputs.addOneAssumeCapacity().* = .{ + .path = elf.base.emit, + .member = null, + .extra = .{ .file_symbol = zcu_file_symbol }, + }; + elf.input_pending_index += 1; + } switch (elf.shndx.debug_addr) { .UNDEF => {}, -- 2.54.0 From 6a5ce4aa99dc5c3ca86187845de24bd521644056 Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Mon, 19 Oct 2026 13:36:50 +0100 Subject: [PATCH 18/18] link.Dwarf: match new Dwarf2 extern representation A previous commit in this branch changed how `link.Dwarf2` represents `extern`s. The legacy `link.Dwarf` implementation, used by the old ELF linker, did not have this change, which led to deviations in debugger output. This commit brings them back into sync. --- src/link/Dwarf.zig | 53 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/src/link/Dwarf.zig b/src/link/Dwarf.zig index a9b309e1d71e2c03e71e7e1addf5055a74d965d2..7803ca1888a5748d2a2121b73c79df5527d2d019 100644 --- a/src/link/Dwarf.zig +++ b/src/link/Dwarf.zig @@ -2662,7 +2662,6 @@ fn initWipNavInner( try wip_nav.strp(@"extern".name.toSlice(ip)); try wip_nav.refType(.fromInterned(if (maybe_func_type) |func_type| func_type.return_type else @"extern".ty)); if (maybe_func_type) |func_type| { - try wip_nav.infoAddrSym(sym_index, 0); try diw.writeByte(@intFromBool(Type.fromInterned(func_type.return_type).isNoReturn(zcu))); if (func_type.param_types.len > 0 or func_type.is_var_args) { for (func_type.param_types.get(ip)) |param_type| { @@ -2672,7 +2671,7 @@ fn initWipNavInner( if (func_type.is_var_args) try wip_nav.abbrevCode(.is_var_args); try diw.writeUleb128(@backingInt(AbbrevCode.null)); } - } else try wip_nav.infoExprLoc(.{ .addr_reloc = sym_index }); + } }, .syntax => switch (ip.isFunctionType(@"extern".ty)) { false => continue :nav_val .{ .undef = @"extern".ty }, @@ -2690,7 +2689,6 @@ fn initWipNavInner( }, &nav, inst_info.file, &decl); try wip_nav.strp(@"extern".name.toSlice(ip)); try wip_nav.refType(.fromInterned(func_type.return_type)); - try wip_nav.infoAddrSym(sym_index, 0); try diw.writeByte(@intFromBool(Type.fromInterned(func_type.return_type).isNoReturn(zcu))); if (func_type.param_types.len > 0 or func_type.is_var_args) { for (func_type.param_types.get(ip)) |param_type| { @@ -2817,13 +2815,19 @@ fn initWipNavInner( else => { const diw = &wip_nav.debug_info.writer; try wip_nav.declCommon(.{ - .decl = .decl_var, + .decl = switch (decl.linkage) { + .@"extern" => .decl_extern_var, + else => .decl_var, + }, .decl_specification = switch (decl.kind) { .unnamed_test, .@"test", .decltest, .@"comptime" => unreachable, .@"const" => .decl_specification_const, .@"var" => .decl_specification_var, }, - .decl_instance = .decl_instance_var, + .decl_instance = switch (decl.linkage) { + .@"extern" => .decl_instance_extern_var, + else => .decl_instance_var, + }, }, &nav, inst_info.file, &decl); try wip_nav.strp(switch (decl.linkage) { .normal => nav.fqn, @@ -2842,20 +2846,24 @@ fn initWipNavInner( .unnamed_test, .@"test", .decltest, .@"comptime" => unreachable, .@"const" => { const const_ty_reloc_index = try wip_nav.refForward(); - try wip_nav.infoExprLoc(loc); + if (decl.linkage != .@"extern") try wip_nav.infoExprLoc(loc); try diw.writeUleb128(nav.resolved.?.@"align".toByteUnits() orelse ty.abiAlignment(zcu).toByteUnits().?); - try diw.writeByte(@intFromBool(decl.linkage != .normal)); + if (decl.linkage != .@"extern") { + try diw.writeByte(@intFromBool(decl.linkage != .normal)); + } wip_nav.finishForward(const_ty_reloc_index); try wip_nav.abbrevCode(.is_const); try wip_nav.refType(ty); }, .@"var" => { try wip_nav.refType(ty); - try wip_nav.infoExprLoc(loc); + if (decl.linkage != .@"extern") try wip_nav.infoExprLoc(loc); try diw.writeUleb128(nav.resolved.?.@"align".toByteUnits() orelse ty.abiAlignment(zcu).toByteUnits().?); - try diw.writeByte(@intFromBool(decl.linkage != .normal)); + if (decl.linkage != .@"extern") { + try diw.writeByte(@intFromBool(decl.linkage != .normal)); + } }, } }, @@ -5138,6 +5146,7 @@ const AbbrevCode = enum { decl_union, decl_packed_union, decl_var, + decl_extern_var, decl_const, decl_const_runtime_bits, decl_const_comptime_state, @@ -5160,6 +5169,7 @@ const AbbrevCode = enum { decl_instance_union, decl_instance_packed_union, decl_instance_var, + decl_instance_extern_var, decl_instance_const, decl_instance_const_runtime_bits, decl_instance_const_comptime_state, @@ -5360,6 +5370,15 @@ const AbbrevCode = enum { .{ .external, .flag }, }, }, + .decl_extern_var = .{ + .tag = .variable, + .attrs = decl_abbrev_common_attrs ++ .{ + .{ .linkage_name, .strp }, + .{ .type, .ref_addr }, + .{ .alignment, .udata }, + .{ .external, .flag_present }, + }, + }, .decl_const = .{ .tag = .constant, .attrs = decl_abbrev_common_attrs ++ .{ @@ -5443,7 +5462,6 @@ const AbbrevCode = enum { .attrs = decl_abbrev_common_attrs ++ .{ .{ .linkage_name, .strp }, .{ .type, .ref_addr }, - .{ .low_pc, .addr }, .{ .external, .flag_present }, .{ .noreturn, .flag }, }, @@ -5454,7 +5472,6 @@ const AbbrevCode = enum { .attrs = decl_abbrev_common_attrs ++ .{ .{ .linkage_name, .strp }, .{ .type, .ref_addr }, - .{ .low_pc, .addr }, .{ .external, .flag_present }, .{ .noreturn, .flag }, }, @@ -5536,6 +5553,15 @@ const AbbrevCode = enum { .{ .external, .flag }, }, }, + .decl_instance_extern_var = .{ + .tag = .variable, + .attrs = decl_instance_abbrev_common_attrs ++ .{ + .{ .linkage_name, .strp }, + .{ .location, .exprloc }, + .{ .alignment, .udata }, + .{ .external, .flag_present }, + }, + }, .decl_instance_const = .{ .tag = .constant, .attrs = decl_instance_abbrev_common_attrs ++ .{ @@ -5619,7 +5645,6 @@ const AbbrevCode = enum { .attrs = decl_instance_abbrev_common_attrs ++ .{ .{ .linkage_name, .strp }, .{ .type, .ref_addr }, - .{ .low_pc, .addr }, .{ .external, .flag_present }, .{ .noreturn, .flag }, }, @@ -5630,7 +5655,6 @@ const AbbrevCode = enum { .attrs = decl_instance_abbrev_common_attrs ++ .{ .{ .linkage_name, .strp }, .{ .type, .ref_addr }, - .{ .low_pc, .addr }, .{ .external, .flag_present }, .{ .noreturn, .flag }, }, @@ -6048,7 +6072,6 @@ const AbbrevCode = enum { .{ .ZIG_parent, .ref_addr }, .{ .linkage_name, .strp }, .{ .type, .ref_addr }, - .{ .low_pc, .addr }, .{ .external, .flag_present }, .{ .noreturn, .flag }, }, @@ -6060,7 +6083,6 @@ const AbbrevCode = enum { .{ .ZIG_parent, .ref_addr }, .{ .linkage_name, .strp }, .{ .type, .ref_addr }, - .{ .low_pc, .addr }, .{ .external, .flag_present }, .{ .noreturn, .flag }, }, @@ -6071,7 +6093,6 @@ const AbbrevCode = enum { .{ .ZIG_parent, .ref_addr }, .{ .linkage_name, .strp }, .{ .type, .ref_addr }, - .{ .location, .exprloc }, .{ .external, .flag_present }, }, }, -- 2.54.0