authorgravatar for kcbanner@gmail.comCasey Banner <kcbanner@gmail.com> 2026-06-05 01:55:35-04:00
committergravatar for kcbanner@gmail.comCasey Banner <kcbanner@gmail.com> 2026-06-23 00:26:55-04:00
logae1130ab2090ce25cd709c89749da2e3cd161d2f
tree89539967389655e1b22bf236197d7b793f5ab704
parent587030e75440d26a52cc65e58d14a66a4f459096

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)

1 files changed, 193 insertions(+), 55 deletions(-)

src/link/Coff.zig+193-55
......@@ -60,6 +60,8 @@ string_bytes: std.ArrayList(u8),
6060section_table: std.AutoArrayHashMapUnmanaged(String, Section),
6161pseudo_section_table: std.array_hash_map.Auto(String, Symbol.Index),
6262object_section_table: std.array_hash_map.Auto(String, Symbol.Index),
63section_merges: std.AutoArrayHashMapUnmanaged(String, String),
64section_merge_pending_index: u32,
6365symbols: std.ArrayList(Symbol),
6466globals: std.array_hash_map.Auto(GlobalName, Symbol.Index),
6567global_pending_index: u32,
......@@ -93,6 +95,8 @@ pub const archive_end_of_header = "`\n";
9395
9496pub const imp_prefix = "__imp_";
9597
98const header_name_max_len = @typeInfo(@FieldType(std.coff.SectionHeader, "name")).array.len;
99
96100/// This is the start of a Portable Executable (PE) file.
97101/// It starts with a MS-DOS header followed by a MS-DOS stub program.
98102/// This data does not change so we include it as follows in all binaries.
......@@ -797,6 +801,7 @@ pub const String = enum(u32) {
797801 @".dtors" = 57,
798802 @".dtors$ZZZ" = 64,
799803 @".bss" = 75,
804 @".fptable" = 80,
800805 _,
801806
802807 pub const Optional = enum(u32) {
......@@ -811,6 +816,7 @@ pub const String = enum(u32) {
811816 @".dtors" = @intFromEnum(String.@".dtors"),
812817 @".dtors$ZZZ" = @intFromEnum(String.@".dtors$ZZZ"),
813818 @".bss" = @intFromEnum(String.@".bss"),
819 @".fptable" = @intFromEnum(String.@".fptable"),
814820 none = std.math.maxInt(u32),
815821 _,
816822
......@@ -1242,11 +1248,6 @@ pub const Reloc = extern struct {
12421248 .ADDR32,
12431249 .ADDR32NB,
12441250 .SECREL,
1245 => std.mem.readInt(
1246 u32,
1247 loc_slice[0..4],
1248 target_endian,
1249 ),
12501251 .REL32,
12511252 .REL32_1,
12521253 .REL32_2,
......@@ -1263,11 +1264,6 @@ pub const Reloc = extern struct {
12631264 else => |kind| @panic(@tagName(kind)),
12641265 .ABSOLUTE => 0,
12651266 .DIR16,
1266 => std.mem.readInt(
1267 u16,
1268 loc_slice[0..2],
1269 target_endian,
1270 ),
12711267 .REL16,
12721268 => std.mem.readInt(
12731269 i16,
......@@ -1277,11 +1273,6 @@ pub const Reloc = extern struct {
12771273 .DIR32,
12781274 .DIR32NB,
12791275 .SECREL,
1280 => std.mem.readInt(
1281 u32,
1282 loc_slice[0..4],
1283 target_endian,
1284 ),
12851276 .REL32,
12861277 => std.mem.readInt(
12871278 i32,
......@@ -1553,6 +1544,8 @@ fn create(
15531544 .section_table = .empty,
15541545 .pseudo_section_table = .empty,
15551546 .object_section_table = .empty,
1547 .section_merges = .empty,
1548 .section_merge_pending_index = 0,
15561549 .symbols = .empty,
15571550 .globals = .empty,
15581551 .global_pending_index = 0,
......@@ -1698,7 +1691,7 @@ fn initHeaders(
16981691 const file_align: std.mem.Alignment = comptime .fromByteUnits(default_file_alignment);
16991692 const is_image = coff.isImage();
17001693 const is_archive = coff.isArchive();
1701
1694 const target = &comp.root_mod.resolved_target.result;
17021695 const optional_header_size: u16 = if (is_image) switch (magic) {
17031696 _ => unreachable,
17041697 inline else => |ct_magic| @sizeOf(@field(std.coff.OptionalHeader, @tagName(ct_magic))),
......@@ -1713,12 +1706,14 @@ fn initHeaders(
17131706 // Sections
17141707 expected_nodes_len += 4;
17151708
1716 if (is_image)
1709 if (is_image) {
17171710 // Pseudo-sections and import / export table
1718 expected_nodes_len += 9
1719 else
1720 // Symbol table
1721 expected_nodes_len += 2;
1711 expected_nodes_len += 9;
1712 if (comp.config.link_libc and target.abi == .msvc)
1713 expected_nodes_len += 1;
1714 } else
1715 // Symbol table
1716 expected_nodes_len += 2;
17221717
17231718 // TLS section
17241719 if (comp.config.any_non_single_threaded) {
......@@ -2004,9 +1999,10 @@ fn initHeaders(
20041999
20052000 try coff.symbols.ensureTotalCapacity(gpa, Symbol.Index.known_count);
20062001 assert(coff.addSymbolAssumeCapacity() == .null);
2002
20072003 // TODO: How do we tell MappedFile not to allocate physical space for these?
20082004 // TODO: Could have a node flag 'virtual' that can never have slice* called on it or fileLocation
2009
2005 // TODO: Instead of it's own section, we can place .bss as a pseudo-section at the end of .text in the extra space
20102006 assert(try coff.addSection(.@".bss", .{
20112007 .CNT_UNINITIALIZED_DATA = true,
20122008 .MEM_READ = true,
......@@ -2028,6 +2024,18 @@ fn initHeaders(
20282024 }) == .text);
20292025
20302026 if (is_image) {
2027 if (comp.config.link_libc and target.abi == .msvc) {
2028 // This section contains a function pointer table used by control flow guard:
2029 // https://learn.microsoft.com/en-us/windows/win32/secbp/control-flow-guard
2030 // The page containing it is set to PAGE_READONLY during startup, so this can't
2031 // be merged into .data this protection would overlap writable memory.
2032 _ = try coff.addSection(.@".fptable", .{
2033 .CNT_INITIALIZED_DATA = true,
2034 .MEM_READ = true,
2035 .MEM_WRITE = true,
2036 });
2037 }
2038
20312039 coff.import_table.ni = try coff.mf.addLastChildNode(
20322040 gpa,
20332041 (try coff.objectSectionMapIndex(
......@@ -2199,7 +2207,8 @@ pub fn startProgress(coff: *Coff, prog_node: std.Progress.Node) void {
21992207 coff.synth_prog_node = prog_node.start("Synthetics", count: {
22002208 var count =
22012209 coff.globals.count() - coff.global_pending_index +
2202 coff.late_globals.items.len - coff.late_globals_pending_index;
2210 coff.late_globals.items.len - coff.late_globals_pending_index +
2211 coff.section_merges.count() - coff.section_merge_pending_index;
22032212
22042213 for (&coff.lazy.values) |*lazy| count += lazy.map.count() - lazy.pending_index;
22052214 break :count count;
......@@ -2562,7 +2571,8 @@ fn getString(coff: *Coff, string: []const u8) String.Optional {
25622571fn getOrPutSymbolName(coff: *Coff, name: []const u8, opt_string: ?String) !SymbolTable.SymbolName {
25632572 assert(!coff.isImage());
25642573 const gpa = coff.base.comp.gpa;
2565 return if (name.len > 8) name: {
2574
2575 return if (name.len > header_name_max_len) name: {
25662576 const string = opt_string orelse try coff.getOrPutString(name);
25672577 const string_gop = try coff.symbol_table.strings.getOrPut(gpa, string);
25682578 if (!string_gop.found_existing) {
......@@ -3202,8 +3212,6 @@ const ObjectSectionAttributes = packed struct {
32023212 initialized: bool = false,
32033213 uninitialized: bool = false,
32043214
3205 // TODO: Include init / not init flags?
3206
32073215 pub fn fromFlags(flags: std.coff.SectionHeader.Flags) ObjectSectionAttributes {
32083216 return .{
32093217 .read = flags.MEM_READ,
......@@ -3244,26 +3252,22 @@ fn pseudoSectionMapIndex(
32443252 const gpa = coff.base.comp.gpa;
32453253 const pseudo_section_gop = try coff.pseudo_section_table.getOrPut(gpa, name);
32463254 const psmi: Node.PseudoSectionMapIndex = @enumFromInt(pseudo_section_gop.index);
3247 const sn = if (!pseudo_section_gop.found_existing) sn: {
3248 const default_parent: Symbol.Index = if (attributes.uninitialized)
3249 .bss
3250 else if (attributes.execute)
3251 .text
3252 else if (attributes.write)
3253 .data
3254 else
3255 .rdata;
3255 const parent_sn = if (!pseudo_section_gop.found_existing) sn: {
3256 const effective_name = coff.section_merges.get(name) orelse name;
3257 const parent = if (coff.section_table.get(effective_name)) |existing_sec|
3258 existing_sec.si
3259 else if (coff.isImage()) parent: {
3260 const parent: Symbol.Index = if (attributes.uninitialized)
3261 .bss
3262 else if (attributes.execute)
3263 .text
3264 else if (attributes.write)
3265 .data
3266 else
3267 .rdata;
32563268
3257 const parent = if (coff.isImage() or std.mem.eql(
3258 u8,
3259 name.toSlice(coff),
3260 default_parent.knownString().toSlice(coff).?,
3261 ))
3262 default_parent
3263 else if (coff.section_table.get(name)) |section|
3264 section.si
3265 else
3266 try coff.addSection(name, attributes.asFlags());
3269 break :parent parent;
3270 } else try coff.addSection(effective_name, attributes.asFlags());
32673271
32683272 try coff.nodes.ensureUnusedCapacity(gpa, 1);
32693273 try coff.symbols.ensureUnusedCapacity(gpa, 1);
......@@ -3282,9 +3286,9 @@ fn pseudoSectionMapIndex(
32823286
32833287 try coff.verifyParentSectionAttributes(
32843288 .pseudo,
3285 sn.name(coff),
3289 parent_sn.name(coff),
32863290 name,
3287 .fromFlags(sn.header(coff).flags),
3291 .fromFlags(parent_sn.header(coff).flags),
32883292 attributes,
32893293 );
32903294
......@@ -3614,6 +3618,8 @@ fn loadObject(
36143618 const target_endian = coff.targetEndian();
36153619 const is_archive = coff.isArchive();
36163620 assert(!coff.isObj());
3621 // We want to evaluate new merges as we see them in .drectve sections to avoid redundant work
3622 assert(coff.section_merge_pending_index == coff.section_merges.count());
36173623
36183624 log.debug("loadObject({f}{f})", .{ path.fmtEscapeString(), fmtMemberNameString(member_name) });
36193625
......@@ -3826,10 +3832,15 @@ fn loadObject(
38263832 var num_global_symbols: u32 = 0;
38273833 var pending_symbols: std.AutoArrayHashMapUnmanaged(u32, PendingSymbol) = .empty;
38283834 defer pending_symbols.deinit(gpa);
3829
38303835 if (!is_archive)
38313836 try pending_symbols.ensureUnusedCapacity(gpa, header.number_of_symbols);
38323837
3838 var section_merges: std.ArrayList(struct {
3839 from: String,
3840 to: String,
3841 }) = .empty;
3842 defer section_merges.deinit(gpa);
3843
38333844 // Discover symbol names and COMDAT symbol mappings
38343845 var symbol_i: u32 = 0;
38353846 while (symbol_i < header.number_of_symbols) {
......@@ -4081,15 +4092,48 @@ fn loadObject(
40814092 );
40824093 } else if (std.ascii.startsWithIgnoreCase(arg, "/guardsym:")) {
40834094 // TODO: https://learn.microsoft.com/en-us/windows/win32/secbp/pe-metadata
4084 } else if (std.ascii.startsWithIgnoreCase(arg, "/merge:")) {
4095 } else if (std.ascii.startsWithIgnoreCase(arg, "/merge:")) merge: {
40854096 var split = std.mem.splitScalar(u8, arg["/merge:".len..], '=');
40864097 const from = split.first();
40874098 const to = split.next() orelse
40884099 return diags.failParse(path, "malformed .drectve argument: '{s}'", .{arg});
4100 if (to.len > header_name_max_len)
4101 return diags.failParse(
4102 path,
4103 "/merge .drectve target exceeds max length of {d}: '{s}'",
4104 .{ header_name_max_len, arg },
4105 );
4106 if (std.mem.eql(u8, from, to)) break :merge;
4107
4108 try coff.ensureManyUnusedStringCapacity(2, from.len + to.len + 2);
4109 const from_str = coff.getOrPutStringAssumeCapacity(from);
4110 const to_str = coff.getOrPutStringAssumeCapacity(to);
4111
4112 {
4113 var iter = to_str;
4114 while (coff.section_merges.get(iter)) |next_to| {
4115 if (next_to == from_str)
4116 return diags.failParse(
4117 path,
4118 "/merge .drectve argument would create a cycle: {s}={s} leads to {s}={s}",
4119 .{ from, to, iter.toSlice(coff), to },
4120 );
4121
4122 iter = next_to;
4123 }
4124 }
40894125
4090 // TODO: Override the parent selection for generated sections below
4091 _ = from;
4092 _ = to;
4126 try coff.section_merges.ensureUnusedCapacity(gpa, 1);
4127 const gop = coff.section_merges.getOrPutAssumeCapacity(from_str);
4128 if (!gop.found_existing) {
4129 coff.synth_prog_node.increaseEstimatedTotalItems(1);
4130 gop.value_ptr.* = to_str;
4131 } else if (gop.value_ptr.* != to_str)
4132 return diags.failParse(
4133 path,
4134 "conflicting /merge .drectve arguments: first seen as {s}={s}, now seen as {s}={s}",
4135 .{ from, gop.value_ptr.toSlice(coff), from, to },
4136 );
40934137 } else if (std.ascii.startsWithIgnoreCase(arg, "/disallowlib:")) {
40944138 const lib_name = arg["/disallowlib:".len..];
40954139 // TODO: Track these and issue error in prelink if any match
......@@ -4250,6 +4294,9 @@ fn loadObject(
42504294 };
42514295 }
42524296
4297 while (coff.section_merge_pending_index < coff.section_merges.count()) : (coff.section_merge_pending_index += 1)
4298 try coff.flushSectionMerge(coff.section_merge_pending_index);
4299
42534300 // Resolve pending associations, create parent sections
42544301 var num_included_sections: u16 = 0;
42554302 var num_included_symbols: u32 = 0;
......@@ -4276,6 +4323,11 @@ fn loadObject(
42764323 .pending => unreachable,
42774324 }
42784325
4326 // TODO: Until we support sorting .pdata, we shouldn't merge these in, the result would be invalid
4327 const section_name = section.name.toSlice(coff);
4328 if (std.mem.startsWith(u8, section_name, ".pdata"))
4329 continue;
4330
42794331 num_included_sections += 1;
42804332 num_included_symbols += section.num_symbols;
42814333 num_included_relocs += section.header.number_of_relocations;
......@@ -4293,7 +4345,7 @@ fn loadObject(
42934345 try coff.input_sections.ensureUnusedCapacity(gpa, num_included_sections);
42944346
42954347 for (sections) |*section| {
4296 if (section.comdat_result != .include) continue;
4348 if (section.parent_si == .null) continue;
42974349
42984350 const ni = try coff.mf.addLastChildNode(gpa, section.parent_si.node(coff), .{
42994351 .size = section.header.size_of_raw_data,
......@@ -4457,7 +4509,7 @@ fn loadObject(
44574509
44584510 const relocation_size = std.coff.Relocation.sizeOf();
44594511 for (sections) |section| {
4460 if (section.comdat_result != .include) continue;
4512 if (section.si == .null) continue;
44614513
44624514 const loc_sym = section.si.get(coff);
44634515 assert(loc_sym.loc_relocs == .none);
......@@ -5481,6 +5533,26 @@ pub fn flush(
54815533pub fn idle(coff: *Coff, tid: Zcu.PerThread.Id) !bool {
54825534 const comp = coff.base.comp;
54835535 task: {
5536 while (coff.section_merge_pending_index < coff.section_merges.count()) {
5537 defer coff.section_merge_pending_index += 1;
5538 const sub_prog_node = coff.synth_prog_node.start(
5539 coff.section_merges.keys()[coff.section_merge_pending_index].toSlice(coff),
5540 0,
5541 );
5542 defer sub_prog_node.end();
5543 coff.flushSectionMerge(coff.section_merge_pending_index) catch |err| switch (err) {
5544 //error.OutOfMemory => |e| return e,
5545 else => |e| return comp.link_diags.fail(
5546 "linker failed to merge section {s} into {s}: {t}",
5547 .{
5548 coff.section_merges.keys()[coff.section_merge_pending_index].toSlice(coff),
5549 coff.section_merges.values()[coff.section_merge_pending_index].toSlice(coff),
5550 e,
5551 },
5552 ),
5553 };
5554 break :task;
5555 }
54845556 while (coff.pending_uavs.pop()) |pending_uav| {
54855557 const sub_prog_node = coff.idleProgNode(tid, coff.const_prog_node, .{ .uav = pending_uav.key });
54865558 defer sub_prog_node.end();
......@@ -5655,7 +5727,8 @@ pub fn idle(coff: *Coff, tid: Zcu.PerThread.Id) !bool {
56555727 try coff.flushMember(pending_mi.key);
56565728 break :task;
56575729 }
5658 // TODO: This and the next task ideally only run once, as it's wasteful otherwise
5730 // TODO: All the sort / shrink tasks ideally run only once - otherwise it's wasteful
5731 // Defer until exports_complete?
56595732 if (coff.export_table.pending_sort) {
56605733 defer coff.export_table.pending_sort = false;
56615734 const sub_prog_node = coff.idleProgNode(
......@@ -5694,6 +5767,7 @@ pub fn idle(coff: *Coff, tid: Zcu.PerThread.Id) !bool {
56945767 break :task;
56955768 }
56965769 }
5770 if (coff.section_merge_pending_index < coff.section_merges.count()) return true;
56975771 if (coff.pending_uavs.count() > 0) return true;
56985772 if (coff.pending_input != null) return true;
56995773 if (coff.inputs_complete and coff.globals.count() > coff.global_pending_index) return true;
......@@ -6805,6 +6879,70 @@ fn flushExportsSort(coff: *Coff) void {
68056879 });
68066880}
68076881
6882fn flushSectionMerge(coff: *Coff, index: u32) !void {
6883 assert(coff.isImage());
6884 const from = coff.section_merges.keys()[index];
6885 const to = coff.section_merges.values()[index];
6886 assert(from != to);
6887
6888 log.debug("flushSectionMerge({s}->{s})", .{ from.toSlice(coff), to.toSlice(coff) });
6889
6890 const opt_to_sec = coff.section_table.getPtr(to);
6891 if (coff.section_table.getPtr(from)) |from_sec| {
6892 const from_sym = from_sec.si.get(coff);
6893 if (opt_to_sec) |to_sec| {
6894 const to_sym = to_sec.si.get(coff);
6895
6896 // TODO: Create a pseudo-section named `from` in `to`, copy `from_sec` ni into that pseudo section
6897 // TODO: Update .section_number for all contained syms
6898 // TODO: Remove `from_sec` from section table (set size = 0 and can do it in flushResized?).
6899 // This is non-trivial as we can't leave holes in the section table.
6900 // TODO: Merge section flags
6901 _ = to_sym;
6902
6903 return coff.base.comp.link_diags.fail("TODO implement section to section merge", .{});
6904 } else if (coff.pseudo_section_table.get(to)) |to_ps_si| {
6905 const to_sym = to_ps_si.get(coff);
6906 if (from_sym.section_number == to_sym.section_number)
6907 return;
6908
6909 // TODO: Same as above, except place `from` into a node in `to_psmi`'s parent
6910 return coff.base.comp.link_diags.fail("TODO implement section to pseudosection merge", .{});
6911 }
6912
6913 // If `to` doesn't exist, /MERGE is defined as renaming `from` to `to`.
6914 // No other path will create image-level sections, so we can safely rename this now
6915 const from_name = &from_sec.si.get(coff).section_number.header(coff).name;
6916 const to_slice = to.toSlice(coff);
6917 @memcpy(from_name[0..to_slice.len], to_slice);
6918 @memset(from_name[to_slice.len..], 0);
6919 } else if (coff.pseudo_section_table.getIndex(from)) |from_index| {
6920 const from_psmi: Node.PseudoSectionMapIndex = @enumFromInt(from_index);
6921 const from_sym = from_psmi.symbol(coff).get(coff);
6922 if (opt_to_sec) |to_sec| {
6923 const to_sym = to_sec.si.get(coff);
6924 if (from_sym.section_number == to_sym.section_number)
6925 return;
6926
6927 // TODO: Move from_psmi's node into to_sec
6928 // TODO: Update .section_number for all contained syms
6929 // TODO: Merge section flags
6930
6931 return coff.base.comp.link_diags.fail("TODO implement pseudosection to section merge", .{});
6932 } else if (coff.pseudo_section_table.get(to)) |to_ps_si| {
6933 const to_sym = to_ps_si.get(coff);
6934 if (from_sym.section_number == to_sym.section_number)
6935 return;
6936
6937 // TODO: Same as above, but move from_psmi's node after to_psmi's node in its parent
6938
6939 return coff.base.comp.link_diags.fail("TODO implement pseudosection to pseudosection merge", .{});
6940 }
6941
6942 // Renaming pseudo-sections have no effect on the output, so this is a no-op.
6943 }
6944}
6945
68086946fn virtualSlide(coff: *Coff, start_section_index: usize, start_rva: u32) !void {
68096947 var rva = start_rva;
68106948 for (