zig

fork of https://codeberg.org/ziglang/zig
Log | Files | Refs | README | LICENSE

commit ae1130ab2090ce25cd709c89749da2e3cd161d2f (tree)
parent 587030e75440d26a52cc65e58d14a66a4f459096
Author: kcbanner <kcbanner@gmail.com>
Date:   Fri,  5 Jun 2026 01:55:35 -0400

Coff: stub out merging and handle special case sections

- Parse / flush /MERGE arguments, impl is incomplete
- Fixup recovering addends not sign extending
- Add .fptable section when linking msvc libc - this needs to be a separate section as it gets marked read-only at runtime
- Pseudo sections prefer to use the exact section name if it exists already (to support .fptable)

Diffstat:
Msrc/link/Coff.zig | 248+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
1 file changed, 193 insertions(+), 55 deletions(-)

diff --git a/src/link/Coff.zig b/src/link/Coff.zig @@ -60,6 +60,8 @@ string_bytes: std.ArrayList(u8), section_table: std.AutoArrayHashMapUnmanaged(String, Section), pseudo_section_table: std.array_hash_map.Auto(String, Symbol.Index), object_section_table: std.array_hash_map.Auto(String, Symbol.Index), +section_merges: std.AutoArrayHashMapUnmanaged(String, String), +section_merge_pending_index: u32, symbols: std.ArrayList(Symbol), globals: std.array_hash_map.Auto(GlobalName, Symbol.Index), global_pending_index: u32, @@ -93,6 +95,8 @@ pub const archive_end_of_header = "`\n"; pub const imp_prefix = "__imp_"; +const header_name_max_len = @typeInfo(@FieldType(std.coff.SectionHeader, "name")).array.len; + /// This is the start of a Portable Executable (PE) file. /// It starts with a MS-DOS header followed by a MS-DOS stub program. /// This data does not change so we include it as follows in all binaries. @@ -797,6 +801,7 @@ pub const String = enum(u32) { @".dtors" = 57, @".dtors$ZZZ" = 64, @".bss" = 75, + @".fptable" = 80, _, pub const Optional = enum(u32) { @@ -811,6 +816,7 @@ pub const String = enum(u32) { @".dtors" = @intFromEnum(String.@".dtors"), @".dtors$ZZZ" = @intFromEnum(String.@".dtors$ZZZ"), @".bss" = @intFromEnum(String.@".bss"), + @".fptable" = @intFromEnum(String.@".fptable"), none = std.math.maxInt(u32), _, @@ -1242,11 +1248,6 @@ pub const Reloc = extern struct { .ADDR32, .ADDR32NB, .SECREL, - => std.mem.readInt( - u32, - loc_slice[0..4], - target_endian, - ), .REL32, .REL32_1, .REL32_2, @@ -1263,11 +1264,6 @@ pub const Reloc = extern struct { else => |kind| @panic(@tagName(kind)), .ABSOLUTE => 0, .DIR16, - => std.mem.readInt( - u16, - loc_slice[0..2], - target_endian, - ), .REL16, => std.mem.readInt( i16, @@ -1277,11 +1273,6 @@ pub const Reloc = extern struct { .DIR32, .DIR32NB, .SECREL, - => std.mem.readInt( - u32, - loc_slice[0..4], - target_endian, - ), .REL32, => std.mem.readInt( i32, @@ -1553,6 +1544,8 @@ fn create( .section_table = .empty, .pseudo_section_table = .empty, .object_section_table = .empty, + .section_merges = .empty, + .section_merge_pending_index = 0, .symbols = .empty, .globals = .empty, .global_pending_index = 0, @@ -1698,7 +1691,7 @@ fn initHeaders( const file_align: std.mem.Alignment = comptime .fromByteUnits(default_file_alignment); const is_image = coff.isImage(); const is_archive = coff.isArchive(); - + const target = &comp.root_mod.resolved_target.result; const optional_header_size: u16 = if (is_image) switch (magic) { _ => unreachable, inline else => |ct_magic| @sizeOf(@field(std.coff.OptionalHeader, @tagName(ct_magic))), @@ -1713,12 +1706,14 @@ fn initHeaders( // Sections expected_nodes_len += 4; - if (is_image) + if (is_image) { // Pseudo-sections and import / export table - expected_nodes_len += 9 - else - // Symbol table - expected_nodes_len += 2; + expected_nodes_len += 9; + if (comp.config.link_libc and target.abi == .msvc) + expected_nodes_len += 1; + } else + // Symbol table + expected_nodes_len += 2; // TLS section if (comp.config.any_non_single_threaded) { @@ -2004,9 +1999,10 @@ fn initHeaders( try coff.symbols.ensureTotalCapacity(gpa, Symbol.Index.known_count); assert(coff.addSymbolAssumeCapacity() == .null); + // TODO: How do we tell MappedFile not to allocate physical space for these? // TODO: Could have a node flag 'virtual' that can never have slice* called on it or fileLocation - + // TODO: Instead of it's own section, we can place .bss as a pseudo-section at the end of .text in the extra space assert(try coff.addSection(.@".bss", .{ .CNT_UNINITIALIZED_DATA = true, .MEM_READ = true, @@ -2028,6 +2024,18 @@ fn initHeaders( }) == .text); if (is_image) { + if (comp.config.link_libc and target.abi == .msvc) { + // This section contains a function pointer table used by control flow guard: + // https://learn.microsoft.com/en-us/windows/win32/secbp/control-flow-guard + // The page containing it is set to PAGE_READONLY during startup, so this can't + // be merged into .data this protection would overlap writable memory. + _ = try coff.addSection(.@".fptable", .{ + .CNT_INITIALIZED_DATA = true, + .MEM_READ = true, + .MEM_WRITE = true, + }); + } + coff.import_table.ni = try coff.mf.addLastChildNode( gpa, (try coff.objectSectionMapIndex( @@ -2199,7 +2207,8 @@ pub fn startProgress(coff: *Coff, prog_node: std.Progress.Node) void { coff.synth_prog_node = prog_node.start("Synthetics", count: { var count = coff.globals.count() - coff.global_pending_index + - coff.late_globals.items.len - coff.late_globals_pending_index; + coff.late_globals.items.len - coff.late_globals_pending_index + + coff.section_merges.count() - coff.section_merge_pending_index; for (&coff.lazy.values) |*lazy| count += lazy.map.count() - lazy.pending_index; break :count count; @@ -2562,7 +2571,8 @@ fn getString(coff: *Coff, string: []const u8) String.Optional { fn getOrPutSymbolName(coff: *Coff, name: []const u8, opt_string: ?String) !SymbolTable.SymbolName { assert(!coff.isImage()); const gpa = coff.base.comp.gpa; - return if (name.len > 8) name: { + + return if (name.len > header_name_max_len) name: { const string = opt_string orelse try coff.getOrPutString(name); const string_gop = try coff.symbol_table.strings.getOrPut(gpa, string); if (!string_gop.found_existing) { @@ -3202,8 +3212,6 @@ const ObjectSectionAttributes = packed struct { initialized: bool = false, uninitialized: bool = false, - // TODO: Include init / not init flags? - pub fn fromFlags(flags: std.coff.SectionHeader.Flags) ObjectSectionAttributes { return .{ .read = flags.MEM_READ, @@ -3244,26 +3252,22 @@ fn pseudoSectionMapIndex( const gpa = coff.base.comp.gpa; const pseudo_section_gop = try coff.pseudo_section_table.getOrPut(gpa, name); const psmi: Node.PseudoSectionMapIndex = @enumFromInt(pseudo_section_gop.index); - const sn = if (!pseudo_section_gop.found_existing) sn: { - const default_parent: Symbol.Index = if (attributes.uninitialized) - .bss - else if (attributes.execute) - .text - else if (attributes.write) - .data - else - .rdata; + const parent_sn = if (!pseudo_section_gop.found_existing) sn: { + const effective_name = coff.section_merges.get(name) orelse name; + const parent = if (coff.section_table.get(effective_name)) |existing_sec| + existing_sec.si + else if (coff.isImage()) parent: { + const parent: Symbol.Index = if (attributes.uninitialized) + .bss + else if (attributes.execute) + .text + else if (attributes.write) + .data + else + .rdata; - const parent = if (coff.isImage() or std.mem.eql( - u8, - name.toSlice(coff), - default_parent.knownString().toSlice(coff).?, - )) - default_parent - else if (coff.section_table.get(name)) |section| - section.si - else - try coff.addSection(name, attributes.asFlags()); + break :parent parent; + } else try coff.addSection(effective_name, attributes.asFlags()); try coff.nodes.ensureUnusedCapacity(gpa, 1); try coff.symbols.ensureUnusedCapacity(gpa, 1); @@ -3282,9 +3286,9 @@ fn pseudoSectionMapIndex( try coff.verifyParentSectionAttributes( .pseudo, - sn.name(coff), + parent_sn.name(coff), name, - .fromFlags(sn.header(coff).flags), + .fromFlags(parent_sn.header(coff).flags), attributes, ); @@ -3614,6 +3618,8 @@ fn loadObject( const target_endian = coff.targetEndian(); const is_archive = coff.isArchive(); assert(!coff.isObj()); + // We want to evaluate new merges as we see them in .drectve sections to avoid redundant work + assert(coff.section_merge_pending_index == coff.section_merges.count()); log.debug("loadObject({f}{f})", .{ path.fmtEscapeString(), fmtMemberNameString(member_name) }); @@ -3826,10 +3832,15 @@ fn loadObject( var num_global_symbols: u32 = 0; var pending_symbols: std.AutoArrayHashMapUnmanaged(u32, PendingSymbol) = .empty; defer pending_symbols.deinit(gpa); - if (!is_archive) try pending_symbols.ensureUnusedCapacity(gpa, header.number_of_symbols); + var section_merges: std.ArrayList(struct { + from: String, + to: String, + }) = .empty; + defer section_merges.deinit(gpa); + // Discover symbol names and COMDAT symbol mappings var symbol_i: u32 = 0; while (symbol_i < header.number_of_symbols) { @@ -4081,15 +4092,48 @@ fn loadObject( ); } else if (std.ascii.startsWithIgnoreCase(arg, "/guardsym:")) { // TODO: https://learn.microsoft.com/en-us/windows/win32/secbp/pe-metadata - } else if (std.ascii.startsWithIgnoreCase(arg, "/merge:")) { + } else if (std.ascii.startsWithIgnoreCase(arg, "/merge:")) merge: { var split = std.mem.splitScalar(u8, arg["/merge:".len..], '='); const from = split.first(); const to = split.next() orelse return diags.failParse(path, "malformed .drectve argument: '{s}'", .{arg}); + if (to.len > header_name_max_len) + return diags.failParse( + path, + "/merge .drectve target exceeds max length of {d}: '{s}'", + .{ header_name_max_len, arg }, + ); + if (std.mem.eql(u8, from, to)) break :merge; + + try coff.ensureManyUnusedStringCapacity(2, from.len + to.len + 2); + const from_str = coff.getOrPutStringAssumeCapacity(from); + const to_str = coff.getOrPutStringAssumeCapacity(to); + + { + var iter = to_str; + while (coff.section_merges.get(iter)) |next_to| { + if (next_to == from_str) + return diags.failParse( + path, + "/merge .drectve argument would create a cycle: {s}={s} leads to {s}={s}", + .{ from, to, iter.toSlice(coff), to }, + ); + + iter = next_to; + } + } - // TODO: Override the parent selection for generated sections below - _ = from; - _ = to; + try coff.section_merges.ensureUnusedCapacity(gpa, 1); + const gop = coff.section_merges.getOrPutAssumeCapacity(from_str); + if (!gop.found_existing) { + coff.synth_prog_node.increaseEstimatedTotalItems(1); + gop.value_ptr.* = to_str; + } else if (gop.value_ptr.* != to_str) + return diags.failParse( + path, + "conflicting /merge .drectve arguments: first seen as {s}={s}, now seen as {s}={s}", + .{ from, gop.value_ptr.toSlice(coff), from, to }, + ); } else if (std.ascii.startsWithIgnoreCase(arg, "/disallowlib:")) { const lib_name = arg["/disallowlib:".len..]; // TODO: Track these and issue error in prelink if any match @@ -4250,6 +4294,9 @@ fn loadObject( }; } + while (coff.section_merge_pending_index < coff.section_merges.count()) : (coff.section_merge_pending_index += 1) + try coff.flushSectionMerge(coff.section_merge_pending_index); + // Resolve pending associations, create parent sections var num_included_sections: u16 = 0; var num_included_symbols: u32 = 0; @@ -4276,6 +4323,11 @@ fn loadObject( .pending => unreachable, } + // TODO: Until we support sorting .pdata, we shouldn't merge these in, the result would be invalid + const section_name = section.name.toSlice(coff); + if (std.mem.startsWith(u8, section_name, ".pdata")) + continue; + num_included_sections += 1; num_included_symbols += section.num_symbols; num_included_relocs += section.header.number_of_relocations; @@ -4293,7 +4345,7 @@ fn loadObject( try coff.input_sections.ensureUnusedCapacity(gpa, num_included_sections); for (sections) |*section| { - if (section.comdat_result != .include) continue; + if (section.parent_si == .null) continue; const ni = try coff.mf.addLastChildNode(gpa, section.parent_si.node(coff), .{ .size = section.header.size_of_raw_data, @@ -4457,7 +4509,7 @@ fn loadObject( const relocation_size = std.coff.Relocation.sizeOf(); for (sections) |section| { - if (section.comdat_result != .include) continue; + if (section.si == .null) continue; const loc_sym = section.si.get(coff); assert(loc_sym.loc_relocs == .none); @@ -5481,6 +5533,26 @@ pub fn flush( pub fn idle(coff: *Coff, tid: Zcu.PerThread.Id) !bool { const comp = coff.base.comp; task: { + while (coff.section_merge_pending_index < coff.section_merges.count()) { + defer coff.section_merge_pending_index += 1; + const sub_prog_node = coff.synth_prog_node.start( + coff.section_merges.keys()[coff.section_merge_pending_index].toSlice(coff), + 0, + ); + defer sub_prog_node.end(); + coff.flushSectionMerge(coff.section_merge_pending_index) catch |err| switch (err) { + //error.OutOfMemory => |e| return e, + else => |e| return comp.link_diags.fail( + "linker failed to merge section {s} into {s}: {t}", + .{ + coff.section_merges.keys()[coff.section_merge_pending_index].toSlice(coff), + coff.section_merges.values()[coff.section_merge_pending_index].toSlice(coff), + e, + }, + ), + }; + break :task; + } while (coff.pending_uavs.pop()) |pending_uav| { const sub_prog_node = coff.idleProgNode(tid, coff.const_prog_node, .{ .uav = pending_uav.key }); defer sub_prog_node.end(); @@ -5655,7 +5727,8 @@ pub fn idle(coff: *Coff, tid: Zcu.PerThread.Id) !bool { try coff.flushMember(pending_mi.key); break :task; } - // TODO: This and the next task ideally only run once, as it's wasteful otherwise + // TODO: All the sort / shrink tasks ideally run only once - otherwise it's wasteful + // Defer until exports_complete? if (coff.export_table.pending_sort) { defer coff.export_table.pending_sort = false; const sub_prog_node = coff.idleProgNode( @@ -5694,6 +5767,7 @@ pub fn idle(coff: *Coff, tid: Zcu.PerThread.Id) !bool { break :task; } } + if (coff.section_merge_pending_index < coff.section_merges.count()) return true; if (coff.pending_uavs.count() > 0) return true; if (coff.pending_input != null) return true; if (coff.inputs_complete and coff.globals.count() > coff.global_pending_index) return true; @@ -6805,6 +6879,70 @@ fn flushExportsSort(coff: *Coff) void { }); } +fn flushSectionMerge(coff: *Coff, index: u32) !void { + assert(coff.isImage()); + const from = coff.section_merges.keys()[index]; + const to = coff.section_merges.values()[index]; + assert(from != to); + + log.debug("flushSectionMerge({s}->{s})", .{ from.toSlice(coff), to.toSlice(coff) }); + + const opt_to_sec = coff.section_table.getPtr(to); + if (coff.section_table.getPtr(from)) |from_sec| { + const from_sym = from_sec.si.get(coff); + if (opt_to_sec) |to_sec| { + const to_sym = to_sec.si.get(coff); + + // TODO: Create a pseudo-section named `from` in `to`, copy `from_sec` ni into that pseudo section + // TODO: Update .section_number for all contained syms + // TODO: Remove `from_sec` from section table (set size = 0 and can do it in flushResized?). + // This is non-trivial as we can't leave holes in the section table. + // TODO: Merge section flags + _ = to_sym; + + return coff.base.comp.link_diags.fail("TODO implement section to section merge", .{}); + } else if (coff.pseudo_section_table.get(to)) |to_ps_si| { + const to_sym = to_ps_si.get(coff); + if (from_sym.section_number == to_sym.section_number) + return; + + // TODO: Same as above, except place `from` into a node in `to_psmi`'s parent + return coff.base.comp.link_diags.fail("TODO implement section to pseudosection merge", .{}); + } + + // If `to` doesn't exist, /MERGE is defined as renaming `from` to `to`. + // No other path will create image-level sections, so we can safely rename this now + const from_name = &from_sec.si.get(coff).section_number.header(coff).name; + const to_slice = to.toSlice(coff); + @memcpy(from_name[0..to_slice.len], to_slice); + @memset(from_name[to_slice.len..], 0); + } else if (coff.pseudo_section_table.getIndex(from)) |from_index| { + const from_psmi: Node.PseudoSectionMapIndex = @enumFromInt(from_index); + const from_sym = from_psmi.symbol(coff).get(coff); + if (opt_to_sec) |to_sec| { + const to_sym = to_sec.si.get(coff); + if (from_sym.section_number == to_sym.section_number) + return; + + // TODO: Move from_psmi's node into to_sec + // TODO: Update .section_number for all contained syms + // TODO: Merge section flags + + return coff.base.comp.link_diags.fail("TODO implement pseudosection to section merge", .{}); + } else if (coff.pseudo_section_table.get(to)) |to_ps_si| { + const to_sym = to_ps_si.get(coff); + if (from_sym.section_number == to_sym.section_number) + return; + + // TODO: Same as above, but move from_psmi's node after to_psmi's node in its parent + + return coff.base.comp.link_diags.fail("TODO implement pseudosection to pseudosection merge", .{}); + } + + // Renaming pseudo-sections have no effect on the output, so this is a no-op. + } +} + fn virtualSlide(coff: *Coff, start_section_index: usize, start_rva: u32) !void { var rva = start_rva; for (