zig

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

commit cd3288a3f6e48ee381196a6d6437309472c513fe (tree)
parent 914b8f26b03944ac58900aefb3ab21fc988e6c7e
Author: Andrew Kelley <andrew@ziglang.org>
Date:   Thu, 25 Jun 2026 19:20:51 +0200

Merge pull request 'Coff linker enhancements, new linker testing framework, and COFF objdump implementation' (#35674) from kcbanner/zig:coff_linker_wip into master

Reviewed-on: https://codeberg.org/ziglang/zig/pulls/35674
Reviewed-by: Jacob Young <jacobly@ziglang.org>
Reviewed-by: Andrew Kelley <andrew@ziglang.org>

Diffstat:
Mbuild.zig | 12+++++++++---
Mlib/compiler/Maker/Step/Run.zig | 101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mlib/compiler/configurer.zig | 8++++++++
Mlib/compiler/objdump.zig | 1716++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mlib/compiler/resinator/cvtres.zig | 2+-
Mlib/std/Build/Configuration.zig | 6+++++-
Mlib/std/Build/Step/Run.zig | 9+++++++++
Mlib/std/coff.zig | 308+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Mlib/std/meta.zig | 9+++++++++
Mlib/std/multi_array_list.zig | 2+-
Mlib/std/start.zig | 2+-
Mlib/std/zig.zig | 4----
Msrc/Compilation.zig | 102++++++++++++++++++++++++++++++++++++++++---------------------------------------
Msrc/codegen/x86_64/Emit.zig | 55+++++++++++++++++++++++++++++++++++++------------------
Msrc/crash_report.zig | 29+++++++++++++++++++++++++++++
Msrc/libs/mingw.zig | 39+++++++++++++++++++++++----------------
Msrc/libs/mingw/implib.zig | 2+-
Msrc/link.zig | 94++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Msrc/link/Coff.zig | 6499++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Msrc/link/Elf2.zig | 26+++++++++++---------------
Msrc/link/MappedFile.zig | 379+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Msrc/main.zig | 4++--
Atest/link.zig | 341+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/link/exports.zig | 9+++++++++
Atest/link/mingw.zig | 48++++++++++++++++++++++++++++++++++++++++++++++++
Atest/link/snapshots/.gitattributes | 1+
Atest/link/snapshots/abs-symbol.x86_64.dmp | 1+
Atest/link/snapshots/dynamic-lib-code.implib-x86_64-windows.dmp | 32++++++++++++++++++++++++++++++++
Atest/link/snapshots/dynamic-lib-code.windows.dmp | 13+++++++++++++
Atest/link/snapshots/dynamic-lib-data.dmp | 14++++++++++++++
Atest/link/snapshots/dynamic-lib-data.implib-x86_64-windows.dmp | 41+++++++++++++++++++++++++++++++++++++++++
Atest/link/snapshots/static-lib.llvm.dmp | 15+++++++++++++++
Atest/link/snapshots/static-lib.no-llvm.dmp | 12++++++++++++
Atest/link/snapshots/tls.llvm.dmp | 9+++++++++
Atest/link/snapshots/tls.no-llvm.dmp | 7+++++++
Atest/src/Link.zig | 281+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/standalone/shared_library/build.zig | 46++++++++++++++++++++++++++++++++++++++++++----
Mtest/standalone/shared_library/mathtest.zig | 2++
Mtest/standalone/shared_library/test.c | 9+++++++++
Mtest/standalone/static_c_lib/build.zig | 88++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Mtest/tests.zig | 221++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
41 files changed, 9753 insertions(+), 845 deletions(-)

diff --git a/build.zig b/build.zig @@ -256,7 +256,6 @@ pub fn build(b: *std.Build) !void { const is_debug = optimize == .Debug; const enable_debug_extensions = b.option(bool, "debug-extensions", "Enable commands and options useful for debugging the compiler") orelse is_debug; const enable_logging = b.option(bool, "log", "Enable debug logging with --debug-log") orelse is_debug; - const enable_link_snapshots = b.option(bool, "link-snapshot", "Whether to enable linker state snapshots") orelse false; const opt_version_string = b.option([]const u8, "version-string", "Override Zig version string. Default is to find out with git."); const version_slice = if (opt_version_string) |version| version else v: { @@ -372,7 +371,6 @@ pub fn build(b: *std.Build) !void { exe_options.addOption(bool, "enable_debug_extensions", enable_debug_extensions); exe_options.addOption(bool, "enable_logging", enable_logging); - exe_options.addOption(bool, "enable_link_snapshots", enable_link_snapshots); exe_options.addOption(bool, "enable_tracy", tracy != null); exe_options.addOption(bool, "enable_tracy_callstack", tracy_callstack); exe_options.addOption(bool, "enable_tracy_allocation", tracy_allocation); @@ -629,6 +627,15 @@ pub fn build(b: *std.Build) !void { .skip_llvm = skip_llvm, .max_rss = 3_300_000_000, })); + test_step.dependOn(tests.addLinkTests(b, .{ + .test_target_filters = test_target_filters, + .test_filters = test_filters, + .optimize_modes = optimize_modes, + .skip_non_native = skip_non_native, + .skip_windows = skip_windows, + .skip_llvm = skip_llvm, + .max_rss = 100_000_000, + })); test_step.dependOn(tests.addStackTraceTests(b, test_filters, skip_non_native)); test_step.dependOn(tests.addErrorTraceTests(b, test_filters, optimize_modes, skip_non_native)); test_step.dependOn(tests.addCliTests(b)); @@ -724,7 +731,6 @@ fn addWasiUpdateStep(b: *std.Build, version: [:0]const u8) !void { exe_options.addOption(std.SemanticVersion, "semver", semver); exe_options.addOption(bool, "enable_debug_extensions", false); exe_options.addOption(bool, "enable_logging", false); - exe_options.addOption(bool, "enable_link_snapshots", false); exe_options.addOption(bool, "enable_tracy", false); exe_options.addOption(bool, "enable_tracy_callstack", false); exe_options.addOption(bool, "enable_tracy_allocation", false); diff --git a/lib/compiler/Maker/Step/Run.zig b/lib/compiler/Maker/Step/Run.zig @@ -2118,6 +2118,67 @@ fn runCommand( }); } } + const snapshots: []const ?struct { + path: Cache.Path, + result: enum { stderr, stdout }, + } = &.{ + if (conf_run.expect_stderr_snapshot.value) |path| .{ + .path = try maker.resolveLazyPathIndex(arena, path, run_index), + .result = .stderr, + } else null, + if (conf_run.expect_stdout_snapshot.value) |path| .{ + .path = try maker.resolveLazyPathIndex(arena, path, run_index), + .result = .stdout, + } else null, + }; + for (snapshots) |opt_snapshot| { + const snapshot = opt_snapshot orelse continue; + + const file = snapshot.path.root_dir.handle.openFile(io, snapshot.path.sub_path, .{}) catch |err| + return step.fail(maker, "unable to open snapshot file {f}: {t}", .{ snapshot.path, err }); + defer file.close(io); + + var file_reader = file.reader(io, &.{}); + const snapshot_contents = file_reader.interface.allocRemaining(gpa, .unlimited) catch |err| + return step.fail(maker, "unable to read snapshot file {f}: {t}", .{ snapshot.path, err }); + defer gpa.free(snapshot_contents); + + const result = switch (snapshot.result) { + .stderr => generic_result.stderr.?, + .stdout => generic_result.stdout.?, + }; + if (std.mem.findDiff(u8, snapshot_contents, result)) |diff_index| { + var diff_line_number: usize = 1; + + for (snapshot_contents[0..diff_index]) |value| { + if (value == '\n') diff_line_number += 1; + } + + return step.fail(maker, + \\ + \\========= snapshot file: ========= + \\{f} + \\========= contained: ============= + \\{s} + \\========= {t} output was: ======== + \\{s} + \\================================== + \\first difference on line {d}: + \\expected: + \\{f} + \\found: + \\{f} + , .{ + snapshot.path, + snapshot_contents, + snapshot.result, + result, + diff_line_number, + fmtSnapshotIndicatorLine(snapshot_contents, diff_index), + fmtSnapshotIndicatorLine(result, diff_index), + }); + } + } }, else => { // On failure, report captured stderr like normal standard error output. @@ -2131,6 +2192,38 @@ fn runCommand( } } +const FmtIndicatorLine = struct { + buf: []const u8, + index: usize, +}; + +fn fmtSnapshotIndicatorLine(buf: []const u8, index: usize) std.fmt.Alt( + FmtIndicatorLine, + snapshotIndicatorLine, +) { + return .{ .data = .{ .buf = buf, .index = index } }; +} + +fn snapshotIndicatorLine(line: FmtIndicatorLine, w: *std.Io.Writer) std.Io.Writer.Error!void { + const line_begin_index = if (std.mem.lastIndexOfScalar(u8, line.buf[0..line.index], '\n')) |line_begin| + line_begin + 1 + else + 0; + const line_end_index = if (std.mem.findScalar(u8, line.buf[line.index..], '\n')) |line_end| + (line.index + line_end) + else + line.buf.len; + + try w.writeAll(line.buf[line_begin_index..line_end_index]); + try w.writeByte('\n'); + try w.splatByteAll(' ', line_end_index - line_begin_index); + try w.writeByte('\n'); + if (line.index >= line.buf.len) + try w.writeAll("^ (end of file)") + else + try w.print("^ ('\\x{x:0>2}')\n", .{line.buf[line.index]}); +} + const EvalGenericResult = struct { term: process.Child.Term, stdout: ?[]const u8, @@ -2301,11 +2394,15 @@ fn setColorEnvironmentVariables( } fn checksContainStdout(conf_run: *const Configuration.Step.Run) bool { - return conf_run.expect_stdout_exact.value != null or conf_run.expect_stdout_match.slice.len != 0; + return conf_run.expect_stdout_exact.value != null or + conf_run.expect_stdout_match.slice.len != 0 or + conf_run.expect_stdout_snapshot.value != null; } fn checksContainStderr(conf_run: *const Configuration.Step.Run) bool { - return conf_run.expect_stderr_exact.value != null or conf_run.expect_stderr_match.slice.len != 0; + return conf_run.expect_stderr_exact.value != null or + conf_run.expect_stderr_match.slice.len != 0 or + conf_run.expect_stderr_snapshot.value != null; } /// If `path` is absolute, return it unchanged. If `make_absolute` is true, make it absolute. diff --git a/lib/compiler/configurer.zig b/lib/compiler/configurer.zig @@ -1011,6 +1011,8 @@ fn serialize(b: *std.Build, wc: *Configuration.Wip, writer: *Io.Writer) !void { status: Configuration.Step.Run.ExpectTermStatus, value: u32, } = null; + var expect_stderr_snapshot: ?Configuration.LazyPath.Index = null; + var expect_stdout_snapshot: ?Configuration.LazyPath.Index = null; switch (run.stdio) { .check => |checks| for (checks.items) |check| switch (check) { .expect_stderr_exact => |bytes| expect_stderr_exact = try wc.addBytes(bytes), @@ -1027,6 +1029,8 @@ fn serialize(b: *std.Build, wc: *Configuration.Wip, writer: *Io.Writer) !void { .stopped => |x| .{ .status = .stopped, .value = @intFromEnum(x) }, .unknown => |x| .{ .status = .unknown, .value = x }, }, + .expect_stderr_snapshot => |path| expect_stderr_snapshot = try s.addLazyPath(path), + .expect_stdout_snapshot => |path| expect_stdout_snapshot = try s.addLazyPath(path), }, else => {}, } @@ -1066,6 +1070,8 @@ fn serialize(b: *std.Build, wc: *Configuration.Wip, writer: *Io.Writer) !void { .expect_stdout_match = expect_stdout_match.items.len != 0, .expect_term = expect_term != null, .expect_term_status = if (expect_term) |t| t.status else .exited, + .expect_stderr_snapshot = expect_stderr_snapshot != null, + .expect_stdout_snapshot = expect_stdout_snapshot != null, }, .file_inputs = .{ .slice = try s.initLazyPathList(run.file_inputs.items) }, .args = .{ .slice = try s.initArgsList(run.argv.items) }, @@ -1088,6 +1094,8 @@ fn serialize(b: *std.Build, wc: *Configuration.Wip, writer: *Io.Writer) !void { .expect_stdout_exact = .{ .value = if (expect_stdout_exact) |bytes| bytes else null }, .expect_stderr_match = .{ .slice = expect_stderr_match.items }, .expect_stdout_match = .{ .slice = expect_stdout_match.items }, + .expect_stderr_snapshot = .{ .value = expect_stderr_snapshot orelse null }, + .expect_stdout_snapshot = .{ .value = expect_stdout_snapshot orelse null }, .stdin = .{ .u = switch (run.stdin) { .none => .none, .bytes => |bytes| .{ .bytes = try wc.addBytes(bytes) }, diff --git a/lib/compiler/objdump.zig b/lib/compiler/objdump.zig @@ -4,19 +4,149 @@ const fatal = std.process.fatal; const mem = std.mem; const assert = std.debug.assert; +const builtin = @import("builtin"); +const native_endian = builtin.cpu.arch.endian(); + var stdout_buffer: [4000]u8 = undefined; +const Options = struct { + exports: bool, + exports_sort: bool, + file_headers: bool, + imports: bool, + input_path: []const u8, + member_filters: []const []const u8 = &.{}, + member_headers: bool, + elements: std.enums.EnumArray(Element, bool), + redact: std.enums.EnumArray(FieldKind, bool), + relocs: bool, + section_filters: []const []const u8 = &.{}, + section_headers: bool, + symbol_filters: []const []const u8 = &.{}, + strings: bool, + symbols: bool, + tls: bool, + + // Coff-specific + linker_member: ?std.coff.ArchiveMemberHeader.Kind, +}; + +const FieldKind = enum { + va, + rva, + ord, + size, +}; + +const Element = enum { + @"file-type", + @"header-name", + @"member-path", + newlines, + @"table-header", +}; + pub fn main(init: std.process.Init) !void { const io = init.io; const args = try init.minimal.args.toSlice(init.arena.allocator()); + const arena = init.arena.allocator(); - var opt_input_path: ?[]const u8 = null; var i: usize = 1; + + var opt_exports: ?bool = null; + var opt_exports_sort: ?bool = null; + var opt_file_headers: ?bool = null; + var opt_imports: ?bool = null; + var opt_input_path: ?[]const u8 = null; + var opt_linker_member: ?std.coff.ArchiveMemberHeader.Kind = null; + var opt_member_headers: ?bool = null; + var any_elements = false; + var elements: ?@FieldType(Options, "elements") = null; + var redact: @FieldType(Options, "redact") = .initFill(false); + var opt_relocs: ?bool = null; + var opt_section_headers: ?bool = null; + var opt_strings: ?bool = null; + var opt_symbols: ?bool = null; + var opt_tls: ?bool = null; + var section_filters: std.ArrayList([]const u8) = .empty; + var symbol_filters: std.ArrayList([]const u8) = .empty; + var member_filters: std.ArrayList([]const u8) = .empty; while (i < args.len) : (i += 1) { const arg = args[i]; if (mem.startsWith(u8, arg, "-")) { if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) { return Io.File.stdout().writeStreamingAll(io, usage); + } else if (mem.eql(u8, arg, "--all-headers")) { + opt_file_headers = true; + opt_linker_member = .second_linker; + opt_member_headers = true; + opt_section_headers = true; + opt_symbols = true; + opt_relocs = true; + } else if (mem.startsWith(u8, arg, "--exports")) { + opt_exports = true; + opt_linker_member = .second_linker; + if (mem.eql(u8, arg["--exports".len..], "=sort")) + opt_exports_sort = true; + } else if (mem.eql(u8, arg, "--file-headers")) { + opt_file_headers = true; + } else if (mem.eql(u8, arg, "--imports")) { + opt_imports = true; + } else if (mem.startsWith(u8, arg, "--linker-member")) { + if (mem.eql(u8, arg["--linker-member".len..], "=1")) + opt_linker_member = .first_linker + else if (mem.eql(u8, arg["--linker-member".len..], "=longnames")) + opt_linker_member = .longnames + else + opt_linker_member = .second_linker; + } else if (mem.eql(u8, arg, "--member-headers")) { + opt_member_headers = true; + } else if (mem.startsWith(u8, arg, "--elements=")) { + any_elements = true; + var split = std.mem.splitScalar(u8, arg["--elements=".len..], ','); + while (split.next()) |element| { + const kind, const add = if (element.len > 0 and element[0] == '-') + .{ element[1..], false } + else + .{ element, true }; + + if (elements == null) elements = .initFill(false); + if (std.meta.stringToEnum(Element, kind)) |format_kind| { + elements.?.set(format_kind, add); + } else if (std.mem.eql(u8, kind, "all")) { + elements.? = .initFill(add); + } else { + fatal("unrecognized element: '{s}'", .{kind}); + } + } + } else if (mem.startsWith(u8, arg, "--only-member=")) { + (try member_filters.addOne(arena)).* = try arena.dupe(u8, arg["--only-member=".len..]); + } else if (mem.startsWith(u8, arg, "--only-section=")) { + (try section_filters.addOne(arena)).* = try arena.dupe(u8, arg["--only-section=".len..]); + } else if (mem.startsWith(u8, arg, "--only-symbol=")) { + (try symbol_filters.addOne(arena)).* = try arena.dupe(u8, arg["--only-symbol=".len..]); + } else if (mem.startsWith(u8, arg, "--redact=")) { + const kind = arg["--redact=".len..]; + if (std.meta.stringToEnum(FieldKind, kind)) |field_kind| { + redact.set(field_kind, true); + } else if (std.mem.eql(u8, kind, "all")) { + redact = .initFill(true); + } else { + fatal("unrecognized redaction kind: {s}", .{kind}); + } + } else if (mem.eql(u8, arg, "--relocs")) { + opt_relocs = true; + } else if (mem.eql(u8, arg, "--section-headers")) { + opt_section_headers = true; + } else if (mem.eql(u8, arg, "-s") or mem.eql(u8, arg, "--snapshot")) { + elements = .initFill(false); + redact = .initFill(true); + } else if (mem.eql(u8, arg, "--strings")) { + opt_strings = true; + } else if (mem.eql(u8, arg, "--symbols")) { + opt_symbols = true; + } else if (mem.eql(u8, arg, "--tls")) { + opt_tls = true; } else { fatal("unrecognized argument: {s}", .{arg}); } @@ -27,42 +157,134 @@ pub fn main(init: std.process.Init) !void { } } - const input_path = opt_input_path orelse fatal("missing input file path positional argument", .{}); + const opts: Options = .{ + .input_path = opt_input_path orelse fatal("missing input file path positional argument", .{}), + .exports = opt_exports orelse false, + .exports_sort = opt_exports_sort orelse false, + .file_headers = opt_file_headers orelse false, + .imports = opt_imports orelse false, + .linker_member = opt_linker_member, + .member_filters = member_filters.items, + .member_headers = opt_member_headers orelse false, + .elements = elements orelse .initFill(true), + .redact = redact, + .relocs = opt_relocs orelse false, + .section_filters = section_filters.items, + .section_headers = opt_section_headers orelse false, + .strings = opt_strings orelse false, + .symbol_filters = symbol_filters.items, + .symbols = opt_symbols orelse false, + .tls = opt_tls orelse false, + }; - var file = std.Io.Dir.cwd().openFile(io, input_path, .{}) catch |err| - fatal("failed to open {s}: {t}", .{ input_path, err }); + var file = std.Io.Dir.cwd().openFile(io, opts.input_path, .{}) catch |err| + fatal("failed to open {s}: {t}", .{ opts.input_path, err }); defer file.close(io); - var buffer: [4000]u8 = undefined; + var buffer: [4096]u8 = undefined; var file_reader = file.reader(io, &buffer); var stdout_writer = std.Io.File.stdout().writerStreaming(io, &stdout_buffer); - dump(&file_reader.interface, &stdout_writer.interface) catch |err| switch (err) { + + const ctx: DumpContext = .{ + .gpa = init.gpa, + .opts = &opts, + .fr = &file_reader, + .w = &stdout_writer.interface, + }; + + dump(&ctx) catch |err| switch (err) { error.ReadFailed => return file_reader.err.?, error.WriteFailed => return stdout_writer.err.?, - error.UnknownFile => fatal("unrecognized file: {s}", .{input_path}), + error.UnknownFile => fatal("unrecognized file: {s}", .{opts.input_path}), + error.ParseFailure => {}, else => |e| return e, }; try stdout_writer.flush(); } -fn dump(r: *Io.Reader, w: *Io.Writer) !void { +fn dump(d: *const DumpContext) !void { + const r = &d.fr.interface; try r.fill(4); elf: { if (!mem.eql(u8, r.buffered()[0..4], std.elf.MAGIC)) break :elf; - return elf.dump(r, w); + return elf.dump(r, d.w); } macho: { if (mem.readInt(u32, r.buffered()[0..4], .little) != std.macho.MH_MAGIC_64) break :macho; - return macho.dump(r, w); + return macho.dump(r, d.w); } wasm: { comptime assert(std.wasm.magic.len == 4); if (!mem.eql(u8, r.buffered()[0..4], &std.wasm.magic)) break :wasm; - return wasm.dump(r, w); + return wasm.dump(r, d.w); + } + coff: { + const ext = std.fs.path.extension(d.opts.input_path); + const basename = std.fs.path.basename(d.opts.input_path); + if (std.mem.eql(u8, ext, ".exe") or std.mem.eql(u8, ext, ".dll")) { + if (!mem.eql(u8, r.buffered()[0..2], "MZ")) break :coff; + try r.discardAll(std.coff.pe_pointer_offset); + const sig_offset = try r.takeInt(u32, .little); + try d.fr.seekTo(sig_offset); + const sig = try r.take(4); + + if (!std.mem.eql(u8, sig, std.coff.pe_signature)) { + try d.w.print("invalid PE signature: {x}", .{sig}); + return error.ParseFailure; + } + + if (d.element(.@"file-type")) { + try d.w.print("{s}: PE/COFF image\n\n", .{basename}); + if (d.element(.newlines)) try d.w.writeByte('\n'); + } + + return coff.dumpObject(d, true, basename); + } else if (std.mem.eql(u8, ext, ".lib")) { + r.fill(std.coff.archive_signature.len) catch break :coff; + if (!mem.eql(u8, r.buffered()[0..std.coff.archive_signature.len], std.coff.archive_signature)) break :coff; + if (d.element(.@"file-type")) { + try d.w.print("{s}: COFF archive\n", .{basename}); + if (d.element(.newlines)) try d.w.writeByte('\n'); + } + + return coff.dumpArchive(d); + } else if (std.mem.eql(u8, ext, ".obj")) { + if (d.element(.@"file-type")) { + try d.w.print("{s}: COFF object\n", .{basename}); + if (d.element(.newlines)) try d.w.writeByte('\n'); + } + + return coff.dumpObject(d, false, basename); + } } return error.UnknownFile; } +const DumpContext = struct { + gpa: std.mem.Allocator, + opts: *const Options, + fr: *Io.File.Reader, + w: *Io.Writer, + + fn element(self: *const DumpContext, e: Element) bool { + return self.opts.elements.get(e); + } + + fn redacted(self: *const DumpContext, opt_kind: ?FieldKind) bool { + const kind = opt_kind orelse return false; + return self.opts.redact.get(kind); + } + + fn failParse( + ctx: *const DumpContext, + comptime fmt: []const u8, + args: anytype, + ) noreturn { + std.log.err("error parsing '{s}'", .{std.fs.path.basename(ctx.opts.input_path)}); + fatal(fmt, args); + } +}; + const elf = struct { fn dump(r: *Io.Reader, w: *Io.Writer) !void { _ = r; @@ -84,10 +306,1478 @@ const wasm = struct { } }; +const coff = struct { + const DIRECTORY_ENTRY = std.coff.IMAGE.DIRECTORY_ENTRY; + + const Section = struct { + header: std.coff.SectionHeader, + name: []const u8, + + fn rvaFileOffset(section: *const Section, rva: u32) !u32 { + if (rva < section.header.virtual_address or + rva >= section.header.virtual_address + section.header.size_of_raw_data) + return error.OutOfBounds; + + return section.header.pointer_to_raw_data + (rva - section.header.virtual_address); + } + }; + + const ArchiveHeader = struct { + name: []const u8, + date: u40, + user_id: u20, + group_id: u20, + file_mode: u24, + size: u34, + + pub fn fromRaw(d: *const DumpContext, raw_header: *const std.coff.ArchiveMemberHeader, opt_longnames: ?[]const u8) @This() { + const name = raw_header.parseName(opt_longnames) catch |err| switch (err) { + error.BadName => d.failParse("malformed member name: '{s}'", .{&raw_header.name}), + error.NoLongNames => d.failParse("member uses a long name, but there was no longnames member", .{}), + }; + + return .{ + .name = name, + .date = raw_header.parseDate() catch |err| + d.failParse("unable to parse date '{s}' in member '{s}': {t}", .{ raw_header.date, name, err }), + .user_id = raw_header.parseUserId() catch |err| + d.failParse("unable to parse user_id '{s}' in member '{s}': {t}", .{ raw_header.user_id, name, err }), + .group_id = raw_header.parseGroupId() catch |err| + d.failParse("unable to parse group_id '{s}' in member '{s}': {t}", .{ raw_header.group_id, name, err }), + .file_mode = raw_header.parseFileMode() catch |err| + d.failParse("unable to parse file_mode '{s}' in member '{s}': {t}", .{ raw_header.file_mode, name, err }), + .size = raw_header.parseSize() catch |err| + d.failParse("unable to parse size '{s}' in member '{s}': {t}", .{ raw_header.size, name, err }), + }; + } + }; + + fn dumpArchive(d: *const DumpContext) !void { + const gpa = d.gpa; + const fr = d.fr; + const w = d.w; + + const r = &fr.interface; + r.toss(std.coff.archive_signature.len); + + const Member = struct { + offset: u32, + order: ?u32, + }; + + var members: std.ArrayList(Member) = .empty; + defer members.deinit(gpa); + var symbol_member_indices: std.ArrayList(u32) = .empty; + defer symbol_member_indices.deinit(gpa); + + var opt_expected_kind: ?std.coff.ArchiveMemberHeader.Kind = .first_linker; + var opt_longnames: ?[]const u8 = null; + defer if (opt_longnames) |l| gpa.free(l); + + var pos = fr.logicalPos(); + const size = try fr.getSize(); + while (pos < size) : (pos = fr.logicalPos()) { + if ((pos & 1) != 0) try r.discardAll(1); + const raw_header = try r.takeStruct(std.coff.ArchiveMemberHeader, .little); + const header: ArchiveHeader = .fromRaw(d, &raw_header, opt_longnames); + + if (!std.mem.eql(u8, &raw_header.end_of_header, std.coff.archive_end_of_header)) + return d.failParse("malformed end-of-header field in member '{s}': {x}", .{ header.name, raw_header.end_of_header }); + + const dump_header = + (d.opts.member_headers and filterMatches(d.opts.member_filters, header.name)) or + (d.opts.linker_member == opt_expected_kind); + + if (dump_header) + try dumpArchiveHeader(d, &header, @intCast(pos)); + + const member_end = fr.logicalPos() + header.size; + if (member_end > size) + return d.failParse("out-of-bounds length 0x{x} in member '{s}'", .{ header.size, header.name }); + + if (opt_expected_kind) |expected_kind| switch (expected_kind) { + .first_linker => { + if (!std.mem.eql(u8, header.name, "/")) + return d.failParse("expected first linker member, found '{s}'", .{header.name}); + + const num_symbols = try r.takeInt(u32, .big); + if (dump_header) + try w.print( + \\{t: >16} type + \\ | {d} symbols + \\ + , .{ expected_kind, num_symbols }); + + if (d.opts.linker_member == .first_linker) { + if (d.element(.@"table-header")) + try w.writeAll( + \\ + \\Archive symbols: + \\& Member Symbol + \\ + ); + + const offsets = try r.readAlloc(gpa, num_symbols * 4); + defer gpa.free(offsets); + + for (0..num_symbols) |symbol_i| { + const symbol = r.takeDelimiter(0) catch |err| + return d.failParse("unable to read first linker member string table: {t}", .{err}); + + if (!filterMatches(d.opts.symbol_filters, symbol.?)) + continue; + + const offset = std.mem.readInt(u32, offsets[symbol_i * 4 ..][0..4], .big); + try w.print("{f} {s}\n", .{ + fmtIntField(d, offset, .{ .kind = .va }), + symbol.?, + }); + } + } + if (dump_header and d.element(.newlines)) try w.writeByte('\n'); + + try fr.seekTo(member_end); + opt_expected_kind = .second_linker; + continue; + }, + .second_linker => { + if (!std.mem.eql(u8, header.name, "/")) + return d.failParse("expected second linker member, found '{s}'", .{header.name}); + + const num_members = try r.takeInt(u32, .little); + pos = fr.logicalPos(); + if (pos + num_members * @sizeOf(u32) > member_end) + return d.failParse("invalid member count 0x{x} in second linker member", .{num_members}); + + try members.ensureTotalCapacity(gpa, num_members); + for (0..num_members) |_| + members.addOneAssumeCapacity().* = .{ + .offset = try r.takeInt(u32, .little), + .order = null, + }; + + const num_symbols = try r.takeInt(u32, .little); + pos = fr.logicalPos(); + if (pos + num_symbols * @sizeOf(u16) > member_end) + return d.failParse("invalid symbol count 0x{x} in second linker member", .{num_symbols}); + + if (dump_header) + try w.print( + \\{t: >16} type + \\ | {f} symbols + \\ | {f} members + \\ + , .{ + expected_kind, + fmtIntField(d, num_symbols, .{ .kind = .size, .width = .auto }), + fmtIntField(d, num_members, .{ .kind = .size, .width = .auto }), + }); + + try symbol_member_indices.ensureTotalCapacity(gpa, num_symbols); + for (0..num_symbols) |order| { + const index = (try r.takeInt(u16, .little)) - 1; + if (index >= members.items.len) + return d.failParse("invalid member index 0x{x} in seconds linker member indices array", .{index}); + + symbol_member_indices.addOneAssumeCapacity().* = index; + + if (members.items[index].order == null) + members.items[index].order = @intCast(order); + } + + if (d.opts.exports and d.opts.exports_sort) { + std.sort.pdq(Member, members.items, {}, struct { + fn lessThan(ctx: void, lhs: Member, rhs: Member) bool { + _ = ctx; + if (lhs.order == null and rhs.order == null) + return lhs.offset < rhs.offset + else if (lhs.order) |lhs_order| + return if (rhs.order) |rhs_order| lhs_order < rhs_order else false + else if (rhs.order) |rhs_order| + return if (lhs.order) |lhs_order| lhs_order < rhs_order else true + else + unreachable; + } + }.lessThan); + } + + if (d.opts.linker_member == .second_linker) { + if (d.element(.@"table-header")) + try w.writeAll( + \\ + \\Archive Symbols: + \\& Member Symbol + \\ + ); + + pos = fr.logicalPos(); + var symbol_i: u32 = 0; + while (pos < member_end and symbol_i < num_symbols) : ({ + pos = fr.logicalPos(); + symbol_i += 1; + }) { + const symbol_name = if (r.takeDelimiter(0) catch |err| switch (err) { + error.StreamTooLong => null, + else => |e| return e, + }) |n| n else return d.failParse("unterminated string found in second linker member", .{}); + + if (!filterMatches(d.opts.symbol_filters, symbol_name)) + continue; + + try w.print("{f} {s}\n", .{ + fmtIntField( + d, + members.items[symbol_member_indices.items[symbol_i]].offset, + .{ .kind = .va }, + ), + symbol_name, + }); + } + + if (symbol_i != num_symbols) + return d.failParse( + " expected {d} entries in second linker member string table, but found {d}", + .{ num_symbols, symbol_i }, + ); + } + + if (d.element(.newlines)) try w.writeByte('\n'); + try fr.seekTo(member_end); + opt_expected_kind = .longnames; + continue; + }, + .longnames => { + // This member is optional + if (std.mem.eql(u8, header.name, "//")) { + opt_longnames = try r.readAlloc(gpa, header.size); + if (dump_header) + try w.print("{t: >16} type\n", .{expected_kind}); + + if (d.opts.linker_member == .longnames) { + if (d.element(.@"table-header")) + try w.print( + \\ + \\Longnames (0x{x} bytes): + \\ + , .{opt_longnames.?.len}); + + var lr = Io.Reader.fixed(opt_longnames.?); + while (try lr.takeDelimiter(0)) |str| { + try w.writeAll(str); + try w.writeByte('\n'); + } + } + + if (d.element(.newlines)) try w.writeByte('\n'); + } + + opt_expected_kind = null; + break; + }, + else => unreachable, + }; + } + + if (opt_expected_kind) |expected_kind| switch (expected_kind) { + .first_linker => d.failParse("missing first linker member", .{}), + .second_linker => d.failParse("missing second linker member", .{}), + else => {}, + }; + + for (members.items, 0..) |member, member_i| { + fr.seekTo(member.offset) catch |err| + d.failParse("unable to read member {d} at offset 0x{x}: {t}", .{ member_i, member.offset, err }); + + const raw_header = try r.takeStruct(std.coff.ArchiveMemberHeader, .little); + const header: ArchiveHeader = .fromRaw(d, &raw_header, opt_longnames); + if (!filterMatches(d.opts.member_filters, header.name)) continue; + + const member_sig = try r.peek(4); + const machine: std.coff.IMAGE.FILE.MACHINE = + @enumFromInt(std.mem.readInt(u16, member_sig[0..2], .little)); + const sig = std.mem.readInt(u16, member_sig[2..4], .little); + + const is_imp_lib = machine == std.coff.IMAGE.FILE.MACHINE.UNKNOWN and sig == 0xffff; + if (d.opts.member_headers) + try dumpArchiveHeader(d, &header, member.offset); + + if (d.opts.member_headers or (d.opts.exports and is_imp_lib)) { + if (is_imp_lib) { + const imp_header = try r.takeStruct(std.coff.ImportHeader, .little); + const sym_name = (try r.takeDelimiter(0)).?; + const imp_dll = (try r.takeDelimiter(0)).?; + + if (!filterMatches(d.opts.symbol_filters, sym_name)) + continue; + + if (d.element(.@"header-name")) + try w.writeAll("\nImport header:\n"); + + try dumpHeader(d, std.coff.ImportHeader, &imp_header, struct { + pub fn sig1(_: *const DumpContext, _: *const std.coff.ImportHeader) !void {} + pub fn sig2(_: *const DumpContext, _: *const std.coff.ImportHeader) !void {} + pub fn types(id: *const DumpContext, h: *const std.coff.ImportHeader) !void { + try id.w.print( + \\{t: >16} import_type + \\{t: >16} name_type + \\ + , .{ h.types.type, h.types.name_type }); + } + }); + + const imp_name = imp_name: switch (imp_header.types.name_type) { + .NAME_NOPREFIX, + .NAME_UNDECORATE, + => |tag| { + var imp_name = std.mem.trimStart(u8, sym_name, "?@_"); + if (tag == .NAME_UNDECORATE) + imp_name = std.mem.sliceTo(imp_name, '@'); + break :imp_name imp_name; + }, + else => sym_name, + }; + + try w.print( + \\ symbol name | {s} + \\ import name | {s} + \\ dll | {s} + \\ + , .{ + sym_name, + imp_name, + imp_dll, + }); + } else { + try w.writeAll(" COFF object type\n"); + } + if (d.element(.newlines)) try w.writeByte('\n'); + } + + if (is_imp_lib) continue; + if (d.opts.section_headers or + d.opts.file_headers or + d.opts.relocs or + d.opts.strings or + d.opts.symbols) + { + const member_name = if (d.element(.@"member-path")) + header.name + else + std.fs.path.basename(header.name); + + if (d.element(.@"file-type")) { + try w.print("{s}({s}): COFF object\n", .{ + std.fs.path.basename(d.opts.input_path), + member_name, + }); + if (d.element(.newlines)) try w.writeByte('\n'); + } + try dumpObject(d, false, member_name); + } + } + } + + fn dumpObject( + d: *const DumpContext, + is_image: bool, + obj_name: []const u8, + ) !void { + const gpa = d.gpa; + const fr = d.fr; + const w = d.w; + + const file_location = fr.logicalPos(); + const r = &fr.interface; + const header = r.takeStruct(std.coff.Header, .little) catch |err| + return d.failParse("unable to read COFF header: {t}", .{err}); + + if (d.opts.file_headers) { + if (d.element(.@"header-name")) try w.writeAll("COFF Header:\n"); + try dumpHeader(d, std.coff.Header, &header, struct {}); + if (d.element(.newlines)) try w.writeByte('\n'); + } + + switch (header.machine) { + _ => return d.failParse("unknown machine type: {x}", .{header.machine}), + else => {}, + } + + var known_dirs: [DIRECTORY_ENTRY.len]std.coff.ImageDataDirectory = undefined; + const needs_data_dirs = + d.opts.exports or + d.opts.imports or + d.opts.tls; + + const ImageInfo = struct { + data_dirs: []const std.coff.ImageDataDirectory, + magic: std.coff.OptionalHeader.Magic, + image_base: u64, + }; + + const image_info: ?ImageInfo = if (header.size_of_optional_header > 0) image_info: { + if (!d.opts.file_headers and !needs_data_dirs) { + try fr.seekBy(header.size_of_optional_header); + break :image_info null; + } + + if (d.opts.file_headers and d.element(.@"header-name")) + try w.writeAll("COFF Optional Header:\n"); + + const magic: std.coff.OptionalHeader.Magic = @enumFromInt(try r.peekInt(u16, .little)); + const num_directory_entries, const image_base = switch (magic) { + inline .PE32, .@"PE32+" => |v| num_data_dirs: { + const OptionalHeader = if (v == .PE32) + std.coff.OptionalHeader.PE32 + else + std.coff.OptionalHeader.@"PE32+"; + + const optional_header = r.takeStruct(OptionalHeader, .little) catch |err| + return d.failParse("unable to read optional header: {t}", .{err}); + + if (d.opts.file_headers) { + try dumpHeader(d, OptionalHeader, &optional_header, struct { + pub fn base_of_code(id: *const DumpContext, h: *const std.coff.OptionalHeader) !void { + const base = @as(*const OptionalHeader, @ptrCast(@alignCast(h))).image_base; + try dumpRvaField(id, @src().fn_name, h.base_of_code, base); + } + + pub fn address_of_entry_point(id: *const DumpContext, h: *const std.coff.OptionalHeader) !void { + const base = @as(*const OptionalHeader, @ptrCast(@alignCast(h))).image_base; + try dumpRvaField(id, @src().fn_name, h.base_of_code, base); + } + + pub fn major_linker_version(id: *const DumpContext, h: *const std.coff.OptionalHeader) !void { + try dumpVersionField(id.w, "linker_version", h.major_linker_version, h.minor_linker_version); + } + pub fn minor_linker_version(_: *const DumpContext, _: *const std.coff.OptionalHeader) !void {} + + pub fn major_operating_system_version(id: *const DumpContext, h: *const OptionalHeader) !void { + try dumpVersionField( + id.w, + "operating_system_version", + h.major_operating_system_version, + h.minor_operating_system_version, + ); + } + pub fn minor_operating_system_version(_: *const DumpContext, _: *const OptionalHeader) !void {} + + pub fn major_image_version(id: *const DumpContext, h: *const OptionalHeader) !void { + try dumpVersionField(id.w, "image_version", h.major_image_version, h.minor_image_version); + } + pub fn minor_image_version(_: *const DumpContext, _: *const OptionalHeader) !void {} + + pub fn major_subsystem_version(id: *const DumpContext, h: *const OptionalHeader) !void { + try dumpVersionField(id.w, "subsystem_version", h.major_subsystem_version, h.minor_subsystem_version); + } + pub fn minor_subsystem_version(_: *const DumpContext, _: *const OptionalHeader) !void {} + }); + if (d.element(.newlines)) try w.writeByte('\n'); + } + + break :num_data_dirs .{ + optional_header.number_of_rva_and_sizes, + optional_header.image_base, + }; + }, + else => return d.failParse("invalid optional header magic number: {x}", .{magic}), + }; + + if (d.opts.file_headers and d.element(.@"header-name")) + try w.writeAll("Data Directories:\n"); + + for (0..num_directory_entries) |dir_i| { + const dir = r.takeStruct(std.coff.ImageDataDirectory, .little) catch |err| + return d.failParse("unable to read data directory {x}: {t}", .{ dir_i, err }); + + if (dir_i < known_dirs.len) + known_dirs[dir_i] = dir; + + if (d.opts.file_headers) + try w.print( + "{x: >16} {x: >8} {t}\n", + .{ dir.virtual_address, dir.size, @as(DIRECTORY_ENTRY, @enumFromInt(dir_i)) }, + ); + } + if (d.opts.file_headers and d.element(.newlines)) try w.writeByte('\n'); + + break :image_info .{ + .data_dirs = known_dirs[0..@min(known_dirs.len, num_directory_entries)], + .magic = magic, + .image_base = image_base, + }; + } else if (is_image) { + return d.failParse("image did not contain an optional header", .{}); + } else null; + + // Section names in images don't use the string table, as they must fit inline in the header + const load_string_table = (d.opts.strings or !is_image) and header.pointer_to_symbol_table > 0; + const string_table = if (load_string_table) string_table: { + const pos = fr.logicalPos(); + fr.seekTo(file_location + header.pointer_to_symbol_table + header.number_of_symbols * std.coff.Symbol.sizeOf()) catch |err| + return d.failParse("unable to seek to string table: {t}", .{err}); + + const string_table_len = r.peekInt(u32, .little) catch |err| + return d.failParse("unable to read string table length: {t}", .{err}); + + const table = r.readAlloc(gpa, string_table_len) catch |err| + return d.failParse("unable to read string table: {t}", .{err}); + + try fr.seekTo(pos); + break :string_table table; + } else &.{}; + defer gpa.free(string_table); + + if (d.opts.strings) { + if (d.element(.@"table-header")) + try w.print( + \\String Table (0x{x} bytes): + \\ + , .{string_table.len}); + + var sr = Io.Reader.fixed(string_table[@sizeOf(u32)..]); + while (try sr.takeDelimiter(0)) |str| { + try w.writeAll(str); + try w.writeByte('\n'); + } + + if (d.element(.newlines)) try w.writeByte('\n'); + } + + var sections: std.ArrayList(Section) = .empty; + defer sections.deinit(gpa); + var sections_with_data: u16 = 0; + + const load_sections = + d.opts.section_headers or + d.opts.symbols or + d.opts.relocs or + needs_data_dirs; + + if (load_sections) { + if (d.opts.section_headers and d.element(.@"table-header")) + try w.print( + \\Sections in '{s}': + \\Num Name RVA Virt Size Data Size & Data & Relocs & Lines # Relocs # Lines Flags + \\ + , .{obj_name}); + + try sections.resize(gpa, header.number_of_sections); + for (sections.items, 0..) |*section, section_i| { + section.header = r.takeStruct(std.coff.SectionHeader, .little) catch |err| + return d.failParse("unable to read section header {x}: {t}", .{ section_i, err }); + section.name = headerName(&section.header.name, string_table) catch |err| switch (err) { + error.Overflow, + error.InvalidCharacter, + => return d.failParse("unable to parse section name offset '{s}': {t}", .{ + section.name, + err, + }), + error.OutOfBounds => return d.failParse("section name offset '{s}' was out of bounds (>= {x})", .{ + section.name, + string_table.len, + }), + }; + + sections_with_data += @intFromBool(section.header.size_of_raw_data > 0); + if (d.opts.section_headers) { + if (!filterMatches(d.opts.section_filters, section.name)) continue; + const raw_name = std.mem.sliceTo(&section.header.name, 0); + try w.print( + "{x: >3} {s: <8} {f} {f} {f} {f} {f} {f} {f} {f} {x:0>8} |", + .{ + section_i + 1, + raw_name, + fmtIntField(d, section.header.virtual_address, .{ .kind = .va }), + fmtIntField(d, section.header.virtual_size, .{ .kind = .size, .width = .{ .explicit = 9 } }), + fmtIntField(d, section.header.size_of_raw_data, .{ .kind = .size, .width = .{ .explicit = 9 } }), + fmtIntField(d, section.header.pointer_to_raw_data, .{ .kind = .va }), + fmtIntField(d, section.header.pointer_to_relocations, .{ .kind = .va }), + fmtIntField(d, section.header.pointer_to_linenumbers, .{ .kind = .va }), + fmtIntField(d, section.header.number_of_relocations, .{ .kind = .va }), + fmtIntField(d, section.header.number_of_linenumbers, .{ .kind = .va }), + @as(u32, @bitCast(section.header.flags)), + }, + ); + + try dumpFlags(w, "{s}", std.coff.SectionHeader.Flags, &section.header.flags, 1); + if (section.name.len > 8) + try w.print("\n | {s}", .{section.name}); + + try w.writeByte('\n'); + } + } + + if (d.opts.section_headers and d.element(.newlines)) try w.writeByte('\n'); + } + + var symbols: std.ArrayList(struct { + name: []const u8, + section_number: std.coff.SectionNumber, + }) = .empty; + defer symbols.deinit(gpa); + + var name_arena: std.heap.ArenaAllocator = .init(gpa); + defer name_arena.deinit(); + + if (d.opts.relocs) + try symbols.ensureUnusedCapacity(gpa, header.number_of_symbols); + + if (d.opts.symbols or d.opts.relocs) { + if (header.pointer_to_symbol_table > 0) { + fr.seekTo(file_location + header.pointer_to_symbol_table) catch |err| + return d.failParse("unable to seek to symbol table: {t}", .{err}); + + if (d.opts.symbols and d.element(.@"table-header")) + try w.print( + \\Symbols in '{s}': + \\ Ord Value Sect Type Storage Name + \\ + , .{obj_name}); + + const symbol_size = std.coff.Symbol.sizeOf(); + var symbol_i: u32 = 0; + while (symbol_i < header.number_of_symbols) { + var symbol: std.coff.Symbol = undefined; + const symbol_bytes = r.take(symbol_size) catch |err| + return d.failParse("unable to read symbol {x}: {t}", .{ symbol_i, err }); + + @memcpy(std.mem.asBytes(&symbol)[0..symbol_size], symbol_bytes); + if (native_endian != .little) + std.mem.byteSwapAllFields(std.coff.Symbol, &symbol); + + const aux_symbols = if (symbol.number_of_aux_symbols > 0) + try r.take(symbol_size * symbol.number_of_aux_symbols) + else + &.{}; + defer symbol_i += symbol.number_of_aux_symbols + 1; + + const name = if (std.mem.eql(u8, symbol.name[0..4], "\x00\x00\x00\x00")) name: { + const index = std.mem.readInt(u32, symbol.name[4..], .little); + if (index >= string_table.len) + return d.failParse("invalid name offset for symbol {x} ({x} >= {x})", .{ + symbol_i, + index, + string_table.len, + }); + break :name std.mem.sliceTo(string_table[index..], 0); + } else try name_arena.allocator().dupe(u8, std.mem.sliceTo(&symbol.name, 0)); + + if (d.opts.relocs) + symbols.appendNTimesAssumeCapacity(.{ + .name = name, + .section_number = symbol.section_number, + }, 1 + symbol.number_of_aux_symbols); + + if (!d.opts.symbols or !filterMatches(d.opts.symbol_filters, name)) + continue; + + try w.print("{f} {x:0>8} ", .{ + fmtIntField(d, @as(u16, @intCast(symbol_i)), .{ .kind = .ord }), + symbol.value, + }); + try switch (symbol.section_number) { + .UNDEFINED => w.writeAll("UNDEF"), + .ABSOLUTE => w.writeAll(" ABS"), + .DEBUG => w.writeAll("DEBUG"), + else => |v| { + const backing = @intFromEnum(v); + const fmt = "{x: >5}"; + if (backing >= 0) + try w.print(fmt, .{@as(u15, @intCast(backing))}) + else + try w.print(fmt, .{backing}); + }, + }; + + try w.print("{t: >5}", .{symbol.type.base_type}); + if (switch (symbol.type.complex_type) { + .NULL => " ", + .POINTER => "* ", + .FUNCTION => "()", + .ARRAY => "[]", + else => null, + }) |suffix| try w.writeAll(suffix) else try w.print("{x}", .{symbol.type.complex_type}); + + try w.print("{t: >16} | {s}\n", .{ symbol.storage_class, name }); + + for (0..symbol.number_of_aux_symbols) |aux_i| { + _ = aux_i; + try w.writeAll(" |"); + + if (symbol.storage_class == .EXTERNAL and + symbol.type == std.coff.SymType{ + .complex_type = .FUNCTION, + .base_type = .NULL, + } and + @intFromEnum(symbol.section_number) > 0) + { + try w.writeAll("TODO function aux symbol"); + } else if (symbol.type == std.coff.SymType{ + .complex_type = .FUNCTION, + .base_type = .NULL, + } and + (std.mem.eql(u8, name, ".bf") or std.mem.eql(u8, name, ".ef"))) + { + try w.writeAll("TODO bf / ef aux symbol"); + } else if (symbol.storage_class == .WEAK_EXTERNAL and symbol.section_number == .UNDEFINED) { + if (symbol.value != 0) + return d.failParse( + "invalid value 0x{x} for weak external symbol 0x{x}", + .{ symbol.value, symbol_i }, + ); + + var weak_external: std.coff.WeakExternalDefinition = undefined; + @memcpy(std.mem.asBytes(&weak_external)[0..symbol_size], aux_symbols[0..symbol_size]); + if (native_endian != .little) + std.mem.byteSwapAllFields(std.coff.WeakExternalDefinition, &weak_external); + + if (weak_external.tag_index >= header.number_of_symbols) + return d.failParse( + "invalid tag_index 0x{x} for weak external symbol 0x{x}", + .{ weak_external.tag_index, symbol_i }, + ); + + if (d.redacted(.ord)) + try w.print(" Weak External [falls back to relative ordinal {x:0>8} via {t}]", .{ + @as(i64, weak_external.tag_index) - symbol_i, + weak_external.flag, + }) + else + try w.print(" Weak External [falls back to ordinal {x:0>8} via {t}]", .{ + weak_external.tag_index, + weak_external.flag, + }); + } else if (symbol.storage_class == .FILE) { + if (!std.mem.eql(u8, name, ".file")) { + try w.print(" !! unexpected symbol name '{s}' for file symbol 0x{x}", .{ name, symbol_i }); + continue; + } + + const filename = std.mem.sliceTo(aux_symbols, 0); + try w.print(" File '{s}'", .{filename}); + break; + } else if (symbol.storage_class == .STATIC and + symbol.type == std.coff.SymType{ + .complex_type = .NULL, + .base_type = .NULL, + } and + symbol.value == 0 and + switch (symbol.section_number) { + .UNDEFINED, .DEBUG, .ABSOLUTE => false, + else => |sn| @intFromEnum(sn) > 0, + }) + { + const section_i: u15 = @intCast(@intFromEnum(symbol.section_number) - 1); + try w.writeAll(" Section "); + + if (section_i >= sections.items.len) { + try w.print(" !! invalid section number: {x}", .{section_i}); + continue; + } + + var section_def: std.coff.SectionDefinition = undefined; + @memcpy(std.mem.asBytes(&section_def)[0..symbol_size], aux_symbols[0..symbol_size]); + if (native_endian != .little) + std.mem.byteSwapAllFields(std.coff.SectionDefinition, &section_def); + + const section = &sections.items[section_i]; + if (section_def.number_of_relocations != section.header.number_of_relocations) { + try w.print( + " !! relocation count did not match section header: {d} vs {d}", + .{ section_def.number_of_relocations, section.header.number_of_relocations }, + ); + continue; + } + + if (section_def.number_of_linenumbers != section.header.number_of_linenumbers) { + try w.print( + " !! line number count did not match section header: {d} vs {d}", + .{ section_def.number_of_linenumbers, section.header.number_of_linenumbers }, + ); + continue; + } + + try w.print(" [size {f} chksum {x:0>8} relocs {x:0>4} lines {x:0>4}]", .{ + fmtIntField(d, section_def.length, .{ .kind = .size, .zero_fill = true }), + section_def.checksum, + section_def.number_of_relocations, + section_def.number_of_linenumbers, + }); + + switch (section_def.selection) { + .NONE => {}, + else => |selection| { + try w.print(" COMDAT({t}", .{selection}); + if (selection == .ASSOCIATIVE) + try w.print("->{x}", .{section_def.number}); + try w.writeAll(")"); + }, + } + } + + try w.writeByte('\n'); + } + } + + if (d.opts.symbols and d.element(.newlines)) try w.writeByte('\n'); + } else if (d.opts.symbols) { + try w.writeAll("No symbol table found\n"); + } + } + + if (d.opts.relocs) { + const relocation_size = std.coff.Relocation.sizeOf(); + + for (sections.items, 0..) |section, section_i| { + if (section.header.pointer_to_relocations == 0) continue; + + if (d.element(.@"table-header")) + try w.print( + \\Relocs for section {x} '{s}' in {s}: + \\ Offset Type Symbol -> Sect Name + \\ + , .{ section_i + 1, section.name, obj_name }); + + fr.seekTo(file_location + section.header.pointer_to_relocations) catch |err| + return d.failParse("unable to seek to section {x} relocation table: {t}", .{ section_i + 1, err }); + + for (0..section.header.number_of_relocations) |reloc_i| { + var reloc: std.coff.Relocation = undefined; + @memcpy(std.mem.asBytes(&reloc)[0..relocation_size], try r.take(relocation_size)); + if (native_endian != .little) + std.mem.byteSwapAllFields(std.coff.Relocation, &reloc); + + const sym = &symbols.items[reloc.symbol_table_index]; + if (!filterMatches(d.opts.symbol_filters, sym.name)) + continue; + + try w.print("{f} ", .{ + fmtIntField(d, reloc.virtual_address, .{ .kind = .va, .zero_fill = true }), + }); + switch (header.machine) { + _ => unreachable, + inline else => |m| switch (m.RelocationType()) { + void => try w.writeAll("(unknown arch)"), + else => |RelocationType| try w.print( + "{t: <17} ", + .{@as(RelocationType, @enumFromInt(reloc.type))}, + ), + }, + } + + if (reloc.symbol_table_index >= symbols.items.len) + return d.failParse( + "reloc {x} in section {x} has out-of-bounds symbol index {x}", + .{ reloc_i, section_i + 1, reloc.symbol_table_index }, + ); + + try w.print("{f} {f} | {s}\n", .{ + fmtIntField(d, reloc.symbol_table_index, .{ .kind = .ord }), + fmtSectionNumber(sym.section_number), + sym.name, + }); + } + if (d.element(.newlines)) try w.writeByte('\n'); + } + } + + // Sections indices with raw data, sorted by RVA + const rva_index = if (needs_data_dirs) rva_index: { + const rva_index = try gpa.alloc(u16, sections_with_data); + var indices_i: u16 = 0; + for (sections.items, 0..) |*section, i| { + if (section.header.size_of_raw_data == 0) continue; + rva_index[indices_i] = @intCast(i); + indices_i += 1; + } + + const Context = struct { + indices: []u16, + sections: []const Section, + + pub fn lessThan(ctx: @This(), lhs: usize, rhs: usize) bool { + return ctx.sections[ctx.indices[lhs]].header.virtual_address < + ctx.sections[ctx.indices[rhs]].header.virtual_address; + } + + pub fn swap(ctx: @This(), lhs: usize, rhs: usize) void { + std.mem.swap(u16, &ctx.indices[lhs], &ctx.indices[rhs]); + } + }; + + std.sort.pdqContext(0, rva_index.len, Context{ + .indices = rva_index, + .sections = sections.items, + }); + + break :rva_index rva_index; + } else &.{}; + defer gpa.free(rva_index); + + if (d.opts.exports) exports: { + if (try seekToDataDirectory( + d, + rva_index, + sections.items, + (image_info orelse { + try w.writeAll("COFF objects do not contain an export data directory"); + break :exports; + }).data_dirs, + .EXPORT, + )) |section_index| { + const export_dir = r.takeStruct(std.coff.ExportDirectoryTable, .little) catch |err| + return d.failParse("unable to read export directory: {t}", .{err}); + + try w.print("Export directory:\n", .{}); + try dumpHeader(d, std.coff.ExportDirectoryTable, &export_dir, struct { + pub fn major_version(id: *const DumpContext, h: *const std.coff.ExportDirectoryTable) !void { + try dumpVersionField(id.w, "version", h.major_version, h.minor_version); + } + pub fn minor_version(_: *const DumpContext, _: *const std.coff.ExportDirectoryTable) !void {} + }); + + const section = sections.items[section_index]; + const name_loc = section.rvaFileOffset(export_dir.name_rva) catch + return d.failParse( + "export name rva 0x{x} was not within the export section", + .{export_dir.name_rva}, + ); + + const eat_loc = section.rvaFileOffset(export_dir.export_address_table_rva) catch + return d.failParse( + "export address table rva 0x{x} was not within the export section", + .{export_dir.export_address_table_rva}, + ); + + const name_pointer_loc = section.rvaFileOffset(export_dir.name_pointer_table_rva) catch + return d.failParse( + "export name pointer table rva 0x{x} was not within the export section", + .{export_dir.name_pointer_table_rva}, + ); + + const ord_loc = section.rvaFileOffset(export_dir.ordinal_table_rva) catch + return d.failParse( + "export ordinal table rva 0x{x} was not within the export section", + .{export_dir.ordinal_table_rva}, + ); + + // All the variable length fields should be contained within this directory. + // Read it entirely to avoid needing to seek per-name when iterating. + const dir = image_info.?.data_dirs[@intFromEnum(DIRECTORY_ENTRY.EXPORT)]; + const dir_end_rva = dir.virtual_address + dir.size; + const dir_loc = fr.logicalPos(); + const dir_slice = try r.readAlloc(gpa, dir.size); + defer gpa.free(dir_slice); + + const dll_name = std.mem.sliceTo(dir_slice[name_loc - dir_loc ..], 0); + if (d.element(.@"table-header")) + try w.print( + \\ + \\Exports from {s}: + \\ Ord Hint RVA Name + \\ + , .{dll_name}); + + const name_pointers = dir_slice[name_pointer_loc - dir_loc ..][0 .. export_dir.number_of_names * @sizeOf(u32)]; + const ords = dir_slice[ord_loc - dir_loc ..][0 .. export_dir.number_of_names * @sizeOf(u16)]; + const addrs = dir_slice[eat_loc - dir_loc ..][0 .. export_dir.number_of_entries * @sizeOf(u32)]; + const name_rva_to_offset = dir.virtual_address + @sizeOf(std.coff.ExportDirectoryTable); + for (0..export_dir.number_of_names) |name_i| { + const name_rva = std.mem.readInt(u32, name_pointers[name_i * @sizeOf(u32) ..][0..@sizeOf(u32)], .little); + const name = std.mem.sliceTo(dir_slice[name_rva - name_rva_to_offset ..], 0); + if (!filterMatches(d.opts.symbol_filters, name)) + continue; + + const ord = std.mem.readInt(u16, ords[name_i * @sizeOf(u16) ..][0..@sizeOf(u16)], .little); + const addr = std.mem.readInt(u32, addrs[@as(u32, ord) * @sizeOf(u32) ..][0..@sizeOf(u32)], .little); + + try w.print("{f} {f} ", .{ + fmtIntField(d, @as(u16, @intCast(export_dir.ordinal_base + ord)), .{ .kind = .ord }), + fmtIntField(d, @as(u16, @intCast(name_i)), .{ .kind = .ord }), + }); + const is_forwarder = addr >= dir.virtual_address and addr < dir_end_rva; + if (is_forwarder) { + try w.writeAll("forwards"); + } else { + try w.print("{f}", .{fmtIntField(d, addr, .{ .kind = .rva })}); + } + + try w.print(" | {s}", .{name}); + if (is_forwarder) + try w.print(" -> {s}", .{std.mem.sliceTo(dir_slice[addr - name_rva_to_offset ..], 0)}); + try w.writeByte('\n'); + } + } + } + + if (d.opts.imports) imports: { + if (try seekToDataDirectory( + d, + rva_index, + sections.items, + (image_info orelse { + try w.writeAll("COFF objects do not contain an import data directory"); + break :imports; + }).data_dirs, + .IMPORT, + )) |_| { + const Entry = std.coff.ImportDirectoryEntry; + var directory_entries: std.ArrayList(Entry) = .empty; + defer directory_entries.deinit(gpa); + while (true) { + const entry = r.takeStruct(Entry, .little) catch |err| + return d.failParse( + "unable to read import directory entry {x}: {t}", + .{ directory_entries.items.len, err }, + ); + + if (std.mem.allEqual(u8, std.mem.asBytes(&entry), 0)) break; + (try directory_entries.addOne(gpa)).* = entry; + } + + for (directory_entries.items) |entry| { + const name_section = sectionContainingRva( + rva_index, + sections.items, + entry.name_rva, + ) orelse + return d.failParse( + "import directory entry name rva 0x{x} was not found in any section", + .{entry.name_rva}, + ); + + const name_loc = sections.items[name_section].rvaFileOffset( + entry.name_rva, + ) catch unreachable; + fr.seekTo(name_loc) catch |err| + return d.failParse( + "unable to seek to import directory entry name at 0x{x}: {t}", + .{ name_loc, err }, + ); + + const dll_name = (try r.takeDelimiter(0)).?; + + if (d.element(.@"header-name")) + try w.print("Import table entry for {s}:\n", .{dll_name}); + try dumpHeader(d, Entry, &entry, struct {}); + + if (d.element(.@"table-header")) + try w.print( + \\ + \\ Ord Hint Name + \\ + , .{}); + + const ilt_section = sectionContainingRva( + rva_index, + sections.items, + entry.import_lookup_table_rva, + ) orelse + return d.failParse( + "import directory entry ilt rva 0x{x} was not found in any section", + .{entry.import_lookup_table_rva}, + ); + + const ilt_loc = sections.items[ilt_section].rvaFileOffset( + entry.import_lookup_table_rva, + ) catch unreachable; + fr.seekTo(ilt_loc) catch |err| + return d.failParse( + "unable to seek to import directory ilt at 0x{x}: {t}", + .{ ilt_loc, err }, + ); + + switch (image_info.?.magic) { + _ => try w.writeAll("(unknown magic)"), + inline else => |m| { + const TableEntry = std.coff.ImportLookupTableEntry(m); + const null_entry: TableEntry = @bitCast(@as(@typeInfo(TableEntry).@"struct".backing_integer.?, 0)); + + var ilt_entries: std.ArrayList(TableEntry) = .empty; + defer ilt_entries.deinit(gpa); + while (true) { + const table_entry = r.takeStruct(TableEntry, .little) catch |err| + return d.failParse( + "unable to read ilt entry {s}:{x}: {t}", + .{ dll_name, ilt_entries.items.len, err }, + ); + if (table_entry == null_entry) break; + (try ilt_entries.addOne(gpa)).* = table_entry; + } + + for (ilt_entries.items, 0..) |ilt_entry, ilt_entry_i| { + if (ilt_entry.is_ordinal) { + try w.print("{x: >4}", .{ilt_entry.payload.ordinal.ordinal}); + } else { + const hint_section = sectionContainingRva( + rva_index, + sections.items, + ilt_entry.payload.hint_name_rva, + ) orelse + return d.failParse( + "import directory ilt entry 0x{x}'s hint rva 0x{x} was not found in any section", + .{ ilt_entry_i, ilt_entry.payload.hint_name_rva }, + ); + + const hint_loc = sections.items[hint_section].rvaFileOffset( + ilt_entry.payload.hint_name_rva, + ) catch unreachable; + fr.seekTo(hint_loc) catch |err| + return d.failParse( + "unable to seek to ilt entry 0x{x}'s hint at 0x{x}: {t}", + .{ ilt_entry_i, hint_loc, err }, + ); + + const hint = r.takeInt(u16, .little) catch |err| + return d.failParse( + "unable to read import directory ilt entry 0x{x}'s hint: {t}", + .{ ilt_entry_i, err }, + ); + + const name = r.takeDelimiter(0) catch |err| + return d.failParse( + "unable to read import directory ilt entry 0x{x}'s name: {t}", + .{ ilt_entry_i, err }, + ); + + try w.print(" {x: >4} | {s}\n", .{ hint, name.? }); + } + } + if (d.element(.newlines)) try w.writeByte('\n'); + }, + } + } + } + } + + if (d.opts.tls) tls: { + if (try seekToDataDirectory( + d, + rva_index, + sections.items, + (image_info orelse { + try w.writeAll("COFF objects do not contain a TLS data directory"); + break :tls; + }).data_dirs, + .TLS, + )) |_| { + switch (image_info.?.magic) { + _ => try w.writeAll("(unknown magic)"), + inline else => |m| { + const TlsDirectoryEntry = std.coff.TlsDirectoryEntry(m); + const tls_entry = r.takeStruct(TlsDirectoryEntry, .little) catch |err| + return d.failParse("unable to read tls directory: {t}", .{err}); + + try w.writeAll("TLS Directory:\n"); + try dumpHeader(d, TlsDirectoryEntry, &tls_entry, struct {}); + + try w.writeAll(" | "); + if (tls_entry.characteristics.alignment == .NONE) { + try w.writeAll("Alignment not specified"); + } else { + try w.print( + "Alignment: {d}", + .{tls_entry.characteristics.alignment.toByteUnits().?}, + ); + } + + try w.writeAll( + \\ + \\ + \\TLS Callbacks: + \\ Address + \\ + ); + + const callbacks_rva: u32 = @intCast(tls_entry.callbacks_va - image_info.?.image_base); + const section_index = sectionContainingRva( + rva_index, + sections.items, + callbacks_rva, + ) orelse + return d.failParse( + "tls callbacks rva 0x{x} was not found in any section", + .{callbacks_rva}, + ); + + const callbacks_loc = sections.items[section_index] + .rvaFileOffset(callbacks_rva) catch unreachable; + + fr.seekTo(callbacks_loc) catch |err| + return d.failParse( + "unable to seek to tls callbacks array at offset 0x{x}: {t}", + .{ callbacks_loc, err }, + ); + + while (true) { + const callback_va = r.takeInt(@FieldType(TlsDirectoryEntry, "callbacks_va"), .little) catch |err| + return d.failParse( + "unable to read tls callbacks array: {t}", + .{err}, + ); + + try w.print("{f}\n", .{fmtIntField(d, callback_va, .{ .kind = .va })}); + if (callback_va == 0) break; + } + if (d.element(.newlines)) try w.writeByte('\n'); + }, + } + } + } + } + + fn seekToDataDirectory( + d: *const DumpContext, + rva_index: []const u16, + sections: []const Section, + data_dirs: []const std.coff.ImageDataDirectory, + entry: DIRECTORY_ENTRY, + ) !?u16 { + if (@intFromEnum(entry) < data_dirs.len) blk: { + const rva = data_dirs[@intFromEnum(entry)].virtual_address; + if (rva == 0) break :blk; + + const section_index = sectionContainingRva(rva_index, sections, rva) orelse + return d.failParse( + "{t} directory rva 0x{x} was not found in any section", + .{ entry, rva }, + ); + + const file_offset = sections[section_index].rvaFileOffset(rva) catch unreachable; + d.fr.seekTo(file_offset) catch |err| + return d.failParse( + "unable to seek to {t} directory at offset 0x{x}: {t}", + .{ entry, file_offset, err }, + ); + + return section_index; + } + + try d.w.print("{t} directory was not present in optional header\n", .{entry}); + return null; + } + + fn sectionContainingRva( + /// Indices into `sections` sorted by rva + indices: []const u16, + sections: []const Section, + rva: u32, + ) ?u16 { + const Context = struct { + rva: u32, + sections: []const Section, + + fn order(ctx: @This(), section_index: u16) std.math.Order { + const h = &ctx.sections[section_index].header; + if (ctx.rva < h.virtual_address) return .lt; + const end = h.virtual_address + h.size_of_raw_data; + if (ctx.rva >= end) return .gt; + return .eq; + } + }; + + const indices_index = std.sort.binarySearch(u16, indices, Context{ + .rva = rva, + .sections = sections, + }, Context.order) orelse return null; + return @intCast(indices[indices_index]); + } + + fn headerName(raw: *const [8]u8, string_table: []const u8) ![]const u8 { + return if (raw[0] == '/') name: { + const name_offset = try std.fmt.parseUnsigned(u24, std.mem.sliceTo(raw[1..], 0), 10); + if (name_offset >= string_table.len) + return error.OutOfBounds; + + break :name std.mem.sliceTo(string_table[name_offset..], 0); + } else std.mem.sliceTo(raw, 0); + } + + fn fmtSectionNumber(section_number: std.coff.SectionNumber) std.fmt.Alt(std.coff.SectionNumber, sectionNumberString) { + return .{ .data = section_number }; + } + + fn sectionNumberString(section_number: std.coff.SectionNumber, w: *std.Io.Writer) std.Io.Writer.Error!void { + try switch (section_number) { + .UNDEFINED => w.writeAll("UNDEF"), + .ABSOLUTE => w.writeAll(" ABS"), + .DEBUG => w.writeAll("DEBUG"), + else => |v| { + const backing = @intFromEnum(v); + const fmt = "{x: >5}"; + if (backing >= 0) + try w.print(fmt, .{@as(u15, @intCast(backing))}) + else + try w.print(fmt, .{backing}); + }, + }; + } + + const FormatIntField = struct { + val: ?u64, + width: ?usize, + zero_fill: bool, + }; + + fn fmtIntField( + d: *const DumpContext, + val: anytype, + params: struct { + kind: ?FieldKind = null, + width: union(enum) { + fit_max, + auto, + explicit: usize, + } = .fit_max, + zero_fill: bool = false, + }, + ) std.fmt.Alt(FormatIntField, intFieldString) { + return .{ + .data = .{ + .val = if (d.redacted(params.kind)) null else val, + .width = switch (params.width) { + .fit_max => @typeInfo(@TypeOf(val)).int.bits / 4, + .auto => null, + .explicit => |w| w, + }, + .zero_fill = params.zero_fill, + }, + }; + } + + fn intFieldString(field: FormatIntField, w: *std.Io.Writer) std.Io.Writer.Error!void { + if (field.val) |val| { + try w.printInt(val, 16, .lower, .{ + .width = field.width, + .alignment = .right, + .fill = if (field.zero_fill) '0' else ' ', + }); + } else try w.splatByteAll('x', field.width orelse 1); + } + + fn dumpFlags(w: *Io.Writer, comptime fmt: []const u8, comptime T: type, flags: *const T, cols: u32) !void { + const s = @typeInfo(T).@"struct"; + inline for (s.field_names, s.field_types) |field_name, field_type| { + if (field_type == bool and @field(flags, field_name)) { + try w.splatByteAll(' ', cols); + try w.print(fmt, .{field_name}); + } + } + } + + fn dumpArchiveHeader(d: *const DumpContext, header: *const ArchiveHeader, pos: u32) !void { + if (d.element(.@"header-name")) + try d.w.print("Archive member at offset 0x{x}: '{s}'\n", .{ pos, header.name }); + try dumpHeader(d, ArchiveHeader, header, struct { + pub fn name(_: *const DumpContext, _: *const ArchiveHeader) !void {} + pub fn file_mode(id: *const DumpContext, h: *const ArchiveHeader) !void { + try id.w.print("{o: >16} file_mode\n", .{h.file_mode}); + } + }); + } + + fn fieldKind(name: []const u8) ?FieldKind { + if (std.mem.endsWith(u8, name, "_rva")) + return .rva; + if (std.mem.endsWith(u8, name, "_va") or + std.mem.endsWith(u8, name, "_address") or + std.mem.startsWith(u8, name, "pointer_")) + return .va; + if (std.mem.startsWith(u8, name, "number_") or + std.mem.startsWith(u8, name, "size")) + return .size; + if (std.mem.startsWith(u8, name, "hint")) + return .ord; + return null; + } + + fn dumpHeader( + d: *const DumpContext, + comptime T: type, + header: *const T, + Custom: type, + ) !void { + const s = @typeInfo(T).@"struct"; + inline for (s.field_names, s.field_types) |field_name, field_type| { + const val = &@field(header, field_name); + if (@hasDecl(Custom, field_name)) { + try @field(Custom, field_name)(d, header); + } else { + switch (@typeInfo(field_type)) { + .int => try d.w.print("{f} {s}\n", .{ fmtIntField(d, val.*, .{ + .kind = comptime fieldKind(field_name), + .width = .{ .explicit = 16 }, + }), field_name }), + .@"enum" => try d.w.print("{x: >16} {s} ({t})\n", .{ val.*, field_name, val.* }), + .@"struct" => |s_field| { + switch (s_field.layout) { + .auto, + .@"extern", + => try dumpHeader(d, field_type, val, Custom), + .@"packed" => { + try d.w.print("{x: >16} {s}\n", .{ @as(s_field.backing_integer.?, @bitCast(val.*)), field_name }); + try dumpFlags(d.w, "| {s}\n", field_type, val, 15); + }, + } + }, + else => unreachable, + } + } + } + } + + fn dumpVersionField(w: *Io.Writer, name: []const u8, major: anytype, minor: anytype) !void { + try w.print("{d: >13}.{x:0<2} {s}\n", .{ major, minor, name }); + } + + fn dumpRvaField(d: *const DumpContext, name: []const u8, rva: u64, base: u64) !void { + try d.w.print("{f} {s} ({f})\n", .{ + fmtIntField(d, rva, .{ .kind = .rva }), + name, + fmtIntField(d, base + rva, .{ .kind = .va }), + }); + } +}; + +fn filterMatches(filters: []const []const u8, val: []const u8) bool { + return for (filters) |filter| { + if (std.mem.containsAtLeast(u8, val, 1, filter)) break true; + } else filters.len == 0; +} + const usage = \\Usage: zig objdump [options] file \\ \\Options: - \\ -h, --help Print this help and exit - \\ + \\ -h, --help Print this help and exit + \\ --all-headers Alias for --file-headers --linker-member=2 --member-headers --section-headers --relocs --symbols + \\ --exports[=sort] Display exported symbols. + \\ In the case of COFF import libraries, displays the symbol list and import headers. + \\ Specify =sort to optionally sort the import headers by symbol name. + \\ --file-headers Display file-format specific headers + \\ --imports Display imported symbols + \\ --linker-member[=1|2|longnames] (Coff) Display contents of the specified archive linker member (default 2) + \\ --member-headers Display archive member headers + \\ --elements=[e1],[e2],-[e3],... Select which formatting elements are displayed. Intended for snapshot testing. + \\ file-type File type summary + \\ header-name Name that precedes a header block + \\ member-path Display full member paths. If removed, only basenames will be used. + \\ newlines Newlines between output sections + \\ table-header Table headers with column names + \\ all (default) All of the above + \\ --only-member=[name] Only consider archive members names that contain [name]. Can be specified multiple times. + \\ --only-section=[name] Only consider section names that contain [name]. Can be specified multiple times. + \\ --only-symbol=[name] Only consider symbol names that contain [name]. Can be specified multiple times. + \\ --redact=[kind] Redact the specified field kind. Intended for snapshot testing. + \\ rva Relative virtual addresses + \\ va Virtual addresses and file offsets + \\ ord Symbol ordinals / hints + \\ size Sizes and lengths + \\ all All of the above + \\ --relocs Display relocations + \\ -s, --snapshot Alias for --redact=all --elements=-all + \\ --section-headers Display section headers + \\ --strings Display string tables + \\ --symbols Display symbol tables + \\ --tls Display TLS information ; diff --git a/lib/compiler/resinator/cvtres.zig b/lib/compiler/resinator/cvtres.zig @@ -383,7 +383,7 @@ pub fn writeCoff( fn writeSymbol(writer: *std.Io.Writer, symbol: std.coff.Symbol) !void { try writer.writeAll(&symbol.name); try writer.writeInt(u32, symbol.value, .little); - try writer.writeInt(u16, @intFromEnum(symbol.section_number), .little); + try writer.writeInt(i16, @intFromEnum(symbol.section_number), .little); try writer.writeInt(u8, @intFromEnum(symbol.type.base_type), .little); try writer.writeInt(u8, @intFromEnum(symbol.type.complex_type), .little); try writer.writeInt(u8, @intFromEnum(symbol.storage_class), .little); diff --git a/lib/std/Build/Configuration.zig b/lib/std/Build/Configuration.zig @@ -587,6 +587,8 @@ pub const Step = extern struct { expect_stderr_match: Storage.FlagLengthPrefixedList(.flags2, .expect_stderr_match, Bytes), expect_stdout_match: Storage.FlagLengthPrefixedList(.flags2, .expect_stdout_match, Bytes), expect_term_value: Storage.FlagOptional(.flags2, .expect_term, u32), + expect_stdout_snapshot: Storage.FlagOptional(.flags2, .expect_stdout_snapshot, LazyPath.Index), + expect_stderr_snapshot: Storage.FlagOptional(.flags2, .expect_stderr_snapshot, LazyPath.Index), pub const CapturedStream = extern struct { generated_file: GeneratedFileIndex, @@ -686,7 +688,9 @@ pub const Step = extern struct { expect_stdout_match: bool, expect_term: bool, expect_term_status: ExpectTermStatus, - _: u25 = 0, + expect_stdout_snapshot: bool, + expect_stderr_snapshot: bool, + _: u23 = 0, }; }; diff --git a/lib/std/Build/Step/Run.zig b/lib/std/Build/Step/Run.zig @@ -133,6 +133,8 @@ pub const StdIo = union(enum) { expect_stdout_exact: []const u8, expect_stdout_match: []const u8, expect_term: process.Child.Term, + expect_stderr_snapshot: std.Build.LazyPath, + expect_stdout_snapshot: std.Build.LazyPath, }; }; @@ -682,6 +684,13 @@ pub fn addCheck(run: *Run, new_check: StdIo.Check) void { .check => |*checks| checks.append(b.allocator, new_check) catch @panic("OOM"), else => @panic("illegal call to addCheck: conflicting helper method calls. Suggest to directly set stdio field of Run instead"), } + + switch (new_check) { + .expect_stderr_snapshot, + .expect_stdout_snapshot, + => |file| run.addFileInput(file), + else => {}, + } } pub fn captureStdErr(run: *Run, options: CapturedStdIo.Options) std.Build.LazyPath { diff --git a/lib/std/coff.zig b/lib/std/coff.zig @@ -2,6 +2,12 @@ const std = @import("std.zig"); const assert = std.debug.assert; const mem = std.mem; +pub const archive_signature = "!<arch>\n"; +pub const archive_end_of_header = "`\n"; + +pub const pe_signature = "PE\x00\x00"; +pub const pe_pointer_offset = 0x3C; + pub const Header = extern struct { /// The number that identifies the type of target machine. machine: IMAGE.FILE.MACHINE, @@ -367,6 +373,36 @@ pub const DebugType = enum(u32) { _, }; +pub fn TlsDirectoryEntry(comptime magic: std.coff.OptionalHeader.Magic) type { + return switch (magic) { + _ => comptime unreachable, + .PE32 => extern struct { + raw_data_start_va: u32, + raw_data_end_va: u32, + tls_index_va: u32, + callbacks_va: u32, + size_of_zero_fill: u32, + characteristics: packed struct(u32) { + _reserved_0: u19, + alignment: SectionHeader.Flags.Align, + _reserved_1: u9, + }, + }, + .@"PE32+" => extern struct { + raw_data_start_va: u64, + raw_data_end_va: u64, + tls_index_va: u64, + callbacks_va: u64, + size_of_zero_fill: u32, + characteristics: packed struct(u32) { + _reserved_0: u19, + alignment: SectionHeader.Flags.Align, + _reserved_1: u9, + }, + }, + }; +} + pub const ImportDirectoryEntry = extern struct { /// The RVA of the import lookup table. /// This table contains a name or ordinal for each import. @@ -389,56 +425,28 @@ pub const ImportDirectoryEntry = extern struct { import_address_table_rva: u32, }; -pub const ImportLookupEntry32 = struct { - pub const ByName = packed struct(u32) { - name_table_rva: u31, - flag: u1 = 0, +pub fn ImportLookupTableEntry(comptime magic: std.coff.OptionalHeader.Magic) type { + const Payload = packed union(u31) { + ordinal: packed struct(u31) { + ordinal: u16, + _: u15 = 0, + }, + hint_name_rva: u31, }; - pub const ByOrdinal = packed struct(u32) { - ordinal_number: u16, - unused: u15 = 0, - flag: u1 = 1, + return switch (magic) { + _ => comptime unreachable, + .PE32 => packed struct(u32) { + payload: Payload, + is_ordinal: bool, + }, + .@"PE32+" => packed struct(u64) { + payload: Payload, + _: u32 = 0, + is_ordinal: bool, + }, }; - - const mask = 0x80000000; - - pub fn getImportByName(raw: u32) ?ByName { - if (mask & raw != 0) return null; - return @as(ByName, @bitCast(raw)); - } - - pub fn getImportByOrdinal(raw: u32) ?ByOrdinal { - if (mask & raw == 0) return null; - return @as(ByOrdinal, @bitCast(raw)); - } -}; - -pub const ImportLookupEntry64 = struct { - pub const ByName = packed struct(u64) { - name_table_rva: u31, - unused: u32 = 0, - flag: u1 = 0, - }; - - pub const ByOrdinal = packed struct(u64) { - ordinal_number: u16, - unused: u47 = 0, - flag: u1 = 1, - }; - - const mask = 0x8000000000000000; - - pub fn getImportByName(raw: u64) ?ByName { - if (mask & raw != 0) return null; - return @as(ByName, @bitCast(raw)); - } - - pub fn getImportByOrdinal(raw: u64) ?ByOrdinal { - if (mask & raw == 0) return null; - return @as(ByOrdinal, @bitCast(raw)); - } -}; +} /// Every name ends with a NULL byte. IF the NULL byte does not fall on /// 2byte boundary, the entry structure is padded to ensure 2byte alignment. @@ -452,6 +460,50 @@ pub const ImportHintNameEntry = extern struct { name: [1]u8, }; +pub const ExportDirectoryTable = extern struct { + /// Reserved + flags: u32, + + /// Creation time of this table + time_date_stamp: u32, + + major_version: u16, + minor_version: u16, + + /// The address of an ASCII string that contains the name of the DLL. + /// This address is relative to the image base. + name_rva: u32, + + /// The ordinal of the first export in this image + ordinal_base: u32, + + /// Number of entries in the export address table + number_of_entries: u32, + + /// Number of entries in the name pointer table and ordinal table + number_of_names: u32, + + export_address_table_rva: u32, + name_pointer_table_rva: u32, + ordinal_table_rva: u32, +}; + +pub const ExportAddressTableEntry = extern struct { + /// If this address is within the export section, then this is the address of the export + /// Otherwise, this is the address of a string that specfies a symbol in another DLL: + /// <dll name>.<export name> + /// <dll name>.#<export ordinal> + export_or_forwarder_rva: u32, +}; + +pub const ExportNamePointerTableEntry = extern struct { + name_rva: u32, +}; + +pub const ExportOrdinalTableEntry = extern struct { + unbiased_ordinal: u16, +}; + pub const SectionHeader = extern struct { name: [8]u8, virtual_size: u32, @@ -610,11 +662,15 @@ pub const SectionHeader = extern struct { std.debug.assert(std.math.isPowerOfTwo(n)); return @enumFromInt(@ctz(n) + 1); } + + pub fn alignment(a: Align) ?std.mem.Alignment { + return .fromByteUnitsOptional(a.toByteUnits() orelse null); + } }; }; }; -pub const Symbol = struct { +pub const Symbol = extern struct { name: [8]u8, value: u32, section_number: SectionNumber, @@ -622,7 +678,7 @@ pub const Symbol = struct { storage_class: StorageClass, number_of_aux_symbols: u8, - pub fn sizeOf() usize { + pub fn sizeOf() comptime_int { return 18; } @@ -639,18 +695,18 @@ pub const Symbol = struct { } }; -pub const SectionNumber = enum(u16) { +pub const SectionNumber = enum(i16) { /// The symbol record is not yet assigned a section. /// A value of zero indicates that a reference to an external symbol is defined elsewhere. /// A value of non-zero is a common symbol with a size that is specified by the value. UNDEFINED = 0, /// The symbol has an absolute (non-relocatable) value and is not an address. - ABSOLUTE = 0xffff, + ABSOLUTE = -1, /// The symbol provides general type or debugging information but does not correspond to a section. /// Microsoft tools use this setting along with .file records (storage class FILE). - DEBUG = 0xfffe, + DEBUG = -2, _, }; @@ -822,7 +878,7 @@ pub const StorageClass = enum(u8) { _, }; -pub const FunctionDefinition = struct { +pub const FunctionDefinition = extern struct { /// The symbol-table index of the corresponding .bf (begin function) symbol record. tag_index: u32, @@ -841,7 +897,7 @@ pub const FunctionDefinition = struct { unused: [2]u8, }; -pub const SectionDefinition = struct { +pub const SectionDefinition = extern struct { /// The size of section data; the same as SizeOfRawData in the section header. length: u32, @@ -863,7 +919,7 @@ pub const SectionDefinition = struct { unused: [3]u8, }; -pub const FileDefinition = struct { +pub const FileDefinition = extern struct { /// An ANSI string that gives the name of the source file. /// This is padded with nulls if it is less than the maximum length. file_name: [18]u8, @@ -874,7 +930,7 @@ pub const FileDefinition = struct { } }; -pub const WeakExternalDefinition = struct { +pub const WeakExternalDefinition = extern struct { /// The symbol-table index of sym2, the symbol to be linked if sym1 is not found. tag_index: u32, @@ -885,7 +941,7 @@ pub const WeakExternalDefinition = struct { unused: [10]u8, - pub fn sizeOf() usize { + pub fn sizeOf() comptime_int { return 18; } }; @@ -933,7 +989,7 @@ pub const ComdatSelection = enum(u8) { _, }; -pub const DebugInfoDefinition = struct { +pub const DebugInfoDefinition = extern struct { unused_1: [4]u8, /// The actual ordinal line number (1, 2, 3, and so on) within the source file, corresponding to the .bf or .ef record. @@ -971,13 +1027,10 @@ pub const Coff = struct { // The lifetime of `data` must be longer than the lifetime of the returned Coff pub fn init(data: []const u8, is_loaded: bool) error{ EndOfStream, MissingPEHeader }!Coff { - const pe_pointer_offset = 0x3C; - const pe_magic = "PE\x00\x00"; - if (data.len < pe_pointer_offset + 4) return error.EndOfStream; const header_offset = mem.readInt(u32, data[pe_pointer_offset..][0..4], .little); if (data.len < header_offset + 4) return error.EndOfStream; - const is_image = mem.eql(u8, data[header_offset..][0..4], pe_magic); + const is_image = mem.eql(u8, data[header_offset..][0..4], pe_signature); const coff: Coff = .{ .data = data, @@ -1348,6 +1401,10 @@ pub const Relocation = extern struct { virtual_address: u32, symbol_table_index: u32, type: u16, + + pub fn sizeOf() comptime_int { + return 10; + } }; pub const IMAGE = struct { @@ -1465,6 +1522,36 @@ pub const IMAGE = struct { _, /// AXP 64 (Same as Alpha 64) pub const AXP64: IMAGE.FILE.MACHINE = .ALPHA64; + + pub fn RelocationType(comptime machine: IMAGE.FILE.MACHINE) type { + return switch (machine) { + .AMD64, + => REL.AMD64, + .ARM, + .ARMNT, + => REL.ARM, + .ARM64, + .ARM64EC, + .ARM64X, + => REL.ARM64, + .I386 => REL.I386, + .IA64 => REL.IA64, + .M32R => REL.M32R, + .MIPS16, + .MIPSFPU, + .MIPSFPU16, + => REL.MIPS, + .POWERPC, + .POWERPCFP, + => REL.PPC, + .SH3, + .SH3DSP, + .SH4, + .SH5, + => REL.SH, + else => void, + }; + } }; }; @@ -1919,3 +2006,100 @@ pub const IMAGE = struct { }; }; }; + +pub const ArchiveMemberHeader = extern struct { + /// Left-justified '/' terminated member name + name: [16]u8, + /// Left-justified ASCII decimal: seconds since January 1st, 1970 + date: [12]u8, + /// Left-justified ASCII decimal: user id + user_id: [6]u8, + /// Left-justified ASCII decimal: group id + group_id: [6]u8, + /// Left-justified ASCII octal: file mode + file_mode: [8]u8, + /// Left-justified ASCII decimal: size of the member following this header, + /// not including the size of this header. + size: [10]u8, + /// The literal string '`\n' + end_of_header: [2]u8, + + /// Extracts the name of the member by either reading it directly from + /// the header, or by finding it inside the longnames member, if provided. + pub fn parseName( + self: *const ArchiveMemberHeader, + opt_longnames: ?[]const u8, + ) ![]const u8 { + const trim = std.mem.trimEnd(u8, &self.name, &.{' '}); + + if (trim.len == 0) return error.BadName; + return if (trim[0] == '/') name: { + if (trim.len == 1 or + trim.len == 2 and trim[1] == '/') + break :name trim; + + const offset = std.fmt.parseUnsigned(u50, trim[1..], 10) catch + return error.BadName; + + if (opt_longnames) |longnames| { + if (offset >= longnames.len) return error.BadName; + break :name std.mem.sliceTo(longnames[@intCast(offset)..], 0); + } else return error.NoLongNames; + } else if (trim[trim.len - 1] == '/') + trim[0 .. trim.len - 1] + else + return error.BadName; + } + + fn parseField(field: []const u8, T: type, base: u8) !T { + if (std.mem.allEqual(u8, field, ' ')) return 0; + if (field[0] == '-') + return @bitCast(try std.fmt.parseInt( + @Int(.signed, @typeInfo(T).int.bits), + std.mem.trimEnd(u8, field, &.{' '}), + base, + )); + + return std.fmt.parseUnsigned(T, std.mem.trimEnd(u8, field, &.{' '}), base); + } + + pub fn parseDate(self: *const ArchiveMemberHeader) !u40 { + return parseField(&self.date, u40, 10); + } + + pub fn parseUserId(self: *const ArchiveMemberHeader) !u20 { + return parseField(&self.user_id, u20, 10); + } + + pub fn parseGroupId(self: *const ArchiveMemberHeader) !u20 { + return parseField(&self.group_id, u20, 10); + } + + pub fn parseFileMode(self: *const ArchiveMemberHeader) !u20 { + return parseField(&self.group_id, u20, 8); + } + + pub fn parseSize(self: *const ArchiveMemberHeader) !u34 { + return parseField(&self.size, u34, 10); + } + + pub const Kind = enum { + first_linker, + second_linker, + longnames, + coff, + import, + }; +}; + +pub const LineNumber = extern struct { + type: extern union { + symbol_table_index: u32, + virtual_address: u32, + }, + line_number: u16, + + pub fn sizeOf() comptime_int { + return 6; + } +}; diff --git a/lib/std/meta.zig b/lib/std/meta.zig @@ -499,6 +499,15 @@ test DeclEnum { try expectEqualEnum(enum {}, DeclEnum(D)); } +pub fn BareUnion(comptime T: type) type { + const u = switch (@typeInfo(T)) { + .@"union" => |u| u, + else => @compileError("expected union type, found '" ++ @typeName(T) ++ "'"), + }; + + return @Union(u.layout, null, u.field_names, u.field_types[0..], u.field_attrs[0..]); +} + pub fn Tag(comptime T: type) type { return switch (@typeInfo(T)) { .@"enum" => |info| info.tag_type, diff --git a/lib/std/multi_array_list.zig b/lib/std/multi_array_list.zig @@ -44,7 +44,7 @@ pub fn MultiArrayList(comptime T: type) type { const Elem = switch (@typeInfo(T)) { .@"struct" => T, .@"union" => |u| struct { - pub const Bare = @Union(u.layout, null, u.field_names, u.field_types[0..], u.field_attrs[0..]); + pub const Bare = std.meta.BareUnion(T); pub const Tag = u.tag_type orelse @compileError("MultiArrayList does not support untagged unions"); tags: Tag, diff --git a/lib/std/start.zig b/lib/std/start.zig @@ -93,7 +93,7 @@ fn DllMainCRTStartup( fdwReason: std.os.windows.DWORD, lpReserved: std.os.windows.LPVOID, ) callconv(.winapi) std.os.windows.BOOL { - if (!builtin.single_threaded and !builtin.link_libc) { + if (!builtin.single_threaded) { _ = @import("os/windows/tls.zig"); } diff --git a/lib/std/zig.zig b/lib/std/zig.zig @@ -996,14 +996,11 @@ pub const EmitArtifact = enum { docs, pdb, h, - compiler_rt_dyn_lib, /// If using `Server` to communicate with the compiler, it will place requested artifacts in /// paths under the output directory, where those paths are named according to this function. /// Returned string is allocated with `gpa` and owned by the caller. pub fn cacheName(ea: EmitArtifact, gpa: Allocator, opts: BinNameOptions) Allocator.Error![]const u8 { - // hack for stage2_x86_64 + coff. See Coff.flush. - if (ea == .compiler_rt_dyn_lib) return "compiler_rt.dll"; const suffix: []const u8 = switch (ea) { .bin => return binNameAlloc(gpa, opts), .@"asm" => ".s", @@ -1013,7 +1010,6 @@ pub const EmitArtifact = enum { .docs => "-docs", .pdb => ".pdb", .h => ".h", - .compiler_rt_dyn_lib => unreachable, }; return std.fmt.allocPrint(gpa, "{s}{s}", .{ opts.root_name, suffix }); } diff --git a/src/Compilation.zig b/src/Compilation.zig @@ -223,8 +223,6 @@ compiler_rt_lib: ?CrtFile = null, /// Populated when we build the compiler_rt_obj object. A Job to build this is indicated /// by setting `queued_jobs.compiler_rt_obj` and resolved before calling linker.flush(). compiler_rt_obj: ?CrtFile = null, -/// hack for stage2_x86_64 + coff -compiler_rt_dyn_lib: ?CrtFile = null, /// Populated when we build the libfuzzer static library. A Job to build this /// is indicated by setting `queued_jobs.fuzzer_lib` and resolved before /// calling linker.flush(). @@ -287,8 +285,6 @@ emit_llvm_bc: ?[]const u8, emit_docs: ?[]const u8, const QueuedJobs = struct { - /// hack for stage2_x86_64 + coff - compiler_rt_dyn_lib: bool = false, compiler_rt_lib: bool = false, compiler_rt_obj: bool = false, ubsan_rt_lib: bool = false, @@ -1781,7 +1777,7 @@ fn addModuleTableToCacheHash( } } -const RtStrat = enum { none, lib, obj, zcu, dyn_lib }; +const RtStrat = enum { none, lib, obj, zcu }; pub const CreateDiagnostic = union(enum) { export_table_import_table_conflict, @@ -1902,12 +1898,6 @@ pub fn create(gpa: Allocator, arena: Allocator, io: Io, diag: *CreateDiagnostic, }; if (have_zcu and (!need_llvm or use_llvm)) { if (output_mode == .Obj) break :s .zcu; - switch (target_util.zigBackend(target, use_llvm)) { - else => {}, - .stage2_aarch64, .stage2_x86_64 => if (target.ofmt == .coff) { - break :s if (is_exe_or_dyn_lib and build_options.have_llvm) .dyn_lib else .zcu; - }, - } } if (need_llvm and !build_options.have_llvm) break :s .none; // impossible to build without llvm if (is_exe_or_dyn_lib) break :s .lib; @@ -2628,11 +2618,6 @@ pub fn create(gpa: Allocator, arena: Allocator, io: Io, diag: *CreateDiagnostic, log.debug("queuing a job to build compiler_rt_obj", .{}); comp.queued_jobs.compiler_rt_obj = true; }, - .dyn_lib => { - // hack for stage2_x86_64 + coff - log.debug("queuing a job to build compiler_rt_dyn_lib", .{}); - comp.queued_jobs.compiler_rt_dyn_lib = true; - }, } switch (comp.ubsan_rt_strat) { @@ -2645,7 +2630,6 @@ pub fn create(gpa: Allocator, arena: Allocator, io: Io, diag: *CreateDiagnostic, log.debug("queuing a job to build ubsan_rt_obj", .{}); comp.queued_jobs.ubsan_rt_obj = true; }, - .dyn_lib => unreachable, // hack for compiler_rt only } switch (comp.zigc_strat) { @@ -2654,7 +2638,7 @@ pub fn create(gpa: Allocator, arena: Allocator, io: Io, diag: *CreateDiagnostic, log.debug("queuing a job to build libzigc", .{}); comp.queued_jobs.zigc_lib = true; }, - .obj, .dyn_lib => unreachable, // only available as a static library or inside an existing ZCU + .obj => unreachable, // only available as a static library or inside an existing ZCU } if (is_exe_or_dyn_lib and comp.config.any_fuzz) { @@ -2713,7 +2697,6 @@ pub fn destroy(comp: *Compilation) void { if (comp.zigc_static_lib) |*crt_file| crt_file.deinit(gpa, io); if (comp.compiler_rt_lib) |*crt_file| crt_file.deinit(gpa, io); if (comp.compiler_rt_obj) |*crt_file| crt_file.deinit(gpa, io); - if (comp.compiler_rt_dyn_lib) |*crt_file| crt_file.deinit(gpa, io); if (comp.fuzzer_lib) |*crt_file| crt_file.deinit(gpa, io); if (comp.glibc_so_files) |*glibc_file| { @@ -4492,22 +4475,16 @@ fn performAllTheWork( comp.link_queue.finishZcuQueue(comp); - // This has to happen after the main semantic analysis loop because it is possible for Sema to + // Main thread work is all done, now just wait for all async work. + try misc_group.await(io); + + // This has to happen again after the main semantic analysis loop because it is possible for Sema to // call `addLinkLib` and hence add more items to `comp.windows_libs`. - for (comp.windows_libs.keys()[comp.windows_libs_num_done..]) |link_lib| { - mingw.buildImportLib(comp, link_lib) catch |err| { - // TODO Surface more error details. - comp.lockAndSetMiscFailure( - .windows_import_lib, - "unable to generate DLL import .lib file for {s}: {t}", - .{ link_lib, err }, - ); - }; - } + for (comp.windows_libs.keys()[comp.windows_libs_num_done..]) |lib_name| + misc_group.async(io, buildMingwImportLib, .{ comp, lib_name, false, main_progress_node }); comp.windows_libs_num_done = @intCast(comp.windows_libs.count()); - - // Main thread work is all done, now just wait for all async work. try misc_group.await(io); + comp.link_queue.wait(io); } @@ -4566,24 +4543,6 @@ fn dispatchPrelinkWork(comp: *Compilation, main_progress_node: std.Progress.Node }); } - // hack for stage2_x86_64 + coff - if (comp.queued_jobs.compiler_rt_dyn_lib and comp.compiler_rt_dyn_lib == null) { - prelink_group.async(io, buildRt, .{ - comp, - "compiler_rt.zig", - "compiler_rt", - .Lib, - .dynamic, - .compiler_rt, - main_progress_node, - RtOptions{ - .checks_valgrind = true, - .allow_lto = false, - }, - &comp.compiler_rt_dyn_lib, - }); - } - if (comp.queued_jobs.fuzzer_lib and comp.fuzzer_lib == null) { prelink_group.async(io, buildRt, .{ comp, @@ -4727,6 +4686,16 @@ fn dispatchPrelinkWork(comp: *Compilation, main_progress_node: std.Progress.Node }); } + while (comp.windows_libs_num_done < comp.windows_libs.count()) { + prelink_group.async(io, buildMingwImportLib, .{ + comp, + comp.windows_libs.keys()[comp.windows_libs_num_done], + true, + main_progress_node, + }); + comp.windows_libs_num_done += 1; + } + prelink_group.await(io) catch |err| switch (err) { error.Canceled => unreachable, // see swapCancelProtection above }; @@ -5412,6 +5381,39 @@ fn buildMingwCrtFile(comp: *Compilation, crt_file: mingw.CrtFile, prog_node: std } } +fn buildMingwImportLib(comp: *Compilation, lib_name: []const u8, is_prelink: bool, prog_node: std.Progress.Node) void { + const crt_file_path = mingw.buildImportLib(comp, lib_name, prog_node) catch |err| switch (err) { + // TODO: This isn't actually true for self-hosted + // In the non-prelink case we will end up putting foo.lib onto the linker line and letting the linker + // use its library paths to look for libraries and report any problems. + error.DefNotFound => return if (is_prelink) { + comp.lockAndSetMiscFailure( + .windows_import_lib, + "definition not found for required mingw DLL import .lib {s}", + .{lib_name}, + ); + }, + // TODO Surface more error details. + else => |e| return comp.lockAndSetMiscFailure( + .windows_import_lib, + "unable to generate mingw DLL import .lib file for {s}: {t}", + .{ lib_name, e }, + ), + }; + + if (is_prelink) + comp.queuePrelinkTasks(&.{.{ + .load_archive = .{ + .path = crt_file_path, + .must_link = false, + }, + }}) catch |err| comp.lockAndSetMiscFailure( + .windows_import_lib, + "unable to queue prelink task for mingw import lib {f}: {t}", + .{ crt_file_path, err }, + ); +} + fn buildWasiLibcCrtFile(comp: *Compilation, crt_file: wasi_libc.CrtFile, prog_node: std.Progress.Node) void { if (wasi_libc.buildCrtFile(comp, crt_file, prog_node)) |_| { comp.queued_jobs.wasi_libc_crt_file[@intFromEnum(crt_file)] = false; diff --git a/src/codegen/x86_64/Emit.zig b/src/codegen/x86_64/Emit.zig @@ -113,6 +113,7 @@ pub fn emitMir(emit: *Emit) Error!void { .default => true, .hidden, .protected => false, }, + .is_dll_import = @"extern".is_dll_import, .force_pcrel_direct = switch (@"extern".relocation) { .any => false, .pcrel => true, @@ -154,20 +155,13 @@ pub fn emitMir(emit: *Emit) Error!void { @enumFromInt(try elf_file.getGlobalSymbol(extern_func.toSlice(&emit.lower.mir).?, null)) else if (emit.bin_file.cast(.elf2)) |elf| try elf.externSymbol(.{ .name = extern_func.toSlice(&emit.lower.mir).?, - .lib_name = switch (comp.compiler_rt_strat) { - .none, .lib, .obj, .zcu => null, - .dyn_lib => "compiler_rt", - }, + .lib_name = null, .type = .FUNC, }) else if (emit.bin_file.cast(.macho)) |macho_file| @enumFromInt(try macho_file.getGlobalSymbol(extern_func.toSlice(&emit.lower.mir).?, null)) - else if (emit.bin_file.cast(.coff2)) |coff| @enumFromInt(@intFromEnum(try coff.globalSymbol( - extern_func.toSlice(&emit.lower.mir).?, - switch (comp.compiler_rt_strat) { - .none, .lib, .obj, .zcu => null, - .dyn_lib => "compiler_rt", - }, - ))) else return emit.fail("external symbol unimplemented for {s}", .{@tagName(emit.bin_file.tag)}), + else if (emit.bin_file.cast(.coff2)) |coff| @enumFromInt(@intFromEnum(try coff.globalSymbol(.{ + .name = extern_func.toSlice(&emit.lower.mir).?, + }))) else return emit.fail("external symbol unimplemented for {s}", .{@tagName(emit.bin_file.tag)}), .is_extern = true, } }, }, @@ -179,7 +173,13 @@ pub fn emitMir(emit: *Emit) Error!void { switch (lowered_inst.encoding.mnemonic) { .call => { reloc.target = .{ .branch = target }; - try emit.encodeInst(lowered_inst, reloc_info); + if (target.is_dll_import and emit.bin_file.cast(.coff2) != null) { + try emit.encodeInst(try .new(.none, .call, &.{ + .{ .mem = .initRip(.ptr, 0) }, + }, emit.lower.target), reloc_info); + } else { + try emit.encodeInst(lowered_inst, reloc_info); + } continue :lowered_inst; }, else => {}, @@ -255,7 +255,25 @@ pub fn emitMir(emit: *Emit) Error!void { else => unreachable, } } else if (emit.bin_file.cast(.coff2)) |_| { - switch (lowered_inst.encoding.mnemonic) { + if (target.is_dll_import) switch (lowered_inst.encoding.mnemonic) { + .lea => try emit.encodeInst(try .new(.none, .mov, &.{ + lowered_inst.ops[0], + .{ .mem = .initRip(.ptr, 0) }, + }, emit.lower.target), reloc_info), + .mov => { + try emit.encodeInst(try .new(.none, .mov, &.{ + lowered_inst.ops[0], + .{ .mem = .initRip(.ptr, 0) }, + }, emit.lower.target), reloc_info); + try emit.encodeInst(try .new(.none, .mov, &.{ + lowered_inst.ops[0], + .{ .mem = .initSib(lowered_inst.ops[reloc.op_index].mem.sib.ptr_size, .{ .base = .{ + .reg = lowered_inst.ops[0].reg.to64(), + } }) }, + }, emit.lower.target), &.{}); + }, + else => unreachable, + } else switch (lowered_inst.encoding.mnemonic) { .lea => try emit.encodeInst(try .new(.none, .lea, &.{ lowered_inst.ops[0], .{ .mem = .initRip(.none, 0) }, @@ -374,7 +392,7 @@ pub fn emitMir(emit: *Emit) Error!void { .op_index = 1, .target = .{ .symbol = .{ .symbol = @enumFromInt(@intFromEnum( - try coff.globalSymbol("__tls_index", null), + try coff.globalSymbol(.{ .name = "__tls_index" }), )), .is_extern = false, } }, @@ -409,7 +427,7 @@ pub fn emitMir(emit: *Emit) Error!void { .op_index = 1, .target = .{ .symbol = .{ .symbol = @enumFromInt(@intFromEnum( - try coff.globalSymbol("_tls_index", null), + try coff.globalSymbol(.{ .name = "_tls_index" }), )), .is_extern = false, } }, @@ -725,6 +743,7 @@ const RelocInfo = struct { const Symbol = struct { symbol: link.File.SymbolId, is_extern: bool, + is_dll_import: bool = false, force_pcrel_direct: bool = false, }; }; @@ -816,7 +835,7 @@ fn encodeInst(emit: *Emit, lowered_inst: Instruction, reloc_info: []const RelocI @enumFromInt(@intFromEnum(emit.atom_id)), end_offset - 4, @enumFromInt(@intFromEnum(target.symbol)), - reloc.off, + .{ .known = reloc.off }, .{ .AMD64 = .REL32 }, ) else unreachable, .branch => |target| if (emit.bin_file.cast(.elf)) |elf_file| { @@ -854,7 +873,7 @@ fn encodeInst(emit: *Emit, lowered_inst: Instruction, reloc_info: []const RelocI @enumFromInt(@intFromEnum(emit.atom_id)), end_offset - 4, @enumFromInt(@intFromEnum(target.symbol)), - reloc.off, + .{ .known = reloc.off }, .{ .AMD64 = .REL32 }, ) else return emit.fail("TODO implement {s} reloc for {s}", .{ @tagName(reloc.target), @tagName(emit.bin_file.tag), @@ -912,7 +931,7 @@ fn encodeInst(emit: *Emit, lowered_inst: Instruction, reloc_info: []const RelocI @enumFromInt(@intFromEnum(emit.atom_id)), end_offset - 4, @enumFromInt(@intFromEnum(target.symbol)), - reloc.off, + .{ .known = reloc.off }, .{ .AMD64 = .SECREL }, ) else return emit.fail("TODO implement {s} reloc for {s}", .{ @tagName(reloc.target), @tagName(emit.bin_file.tag), diff --git a/src/crash_report.zig b/src/crash_report.zig @@ -84,6 +84,25 @@ pub const CodegenFunc = if (enabled) struct { pub fn stop(_: InternPool.Index) void {} }; +pub const LinkerOp = if (enabled) struct { + lf: *link.File, + tid: Zcu.PerThread.Id, + threadlocal var current: ?LinkerOp = null; + pub fn start(lf: *link.File, tid: Zcu.PerThread.Id) void { + std.debug.assert(current == null); + current = .{ .lf = lf, .tid = tid }; + } + pub fn stop(lf: *link.File, tid: Zcu.PerThread.Id) void { + std.debug.assert(current.?.lf == lf and current.?.tid == tid); + current = null; + } +} else struct { + const current: ?noreturn = null; + // Dummy implementation + pub fn start(_: *link.File, _: Zcu.PerThread.Id) void {} + pub fn stop(_: *link.File, _: Zcu.PerThread.Id) void {} +}; + fn dumpCrashContext() Io.Writer.Error!void { const S = struct { /// In the case of recursive panics or segfaults, don't print the context for a second time. @@ -111,6 +130,15 @@ fn dumpCrashContext() Io.Writer.Error!void { try w.print("Generating function '{f}'\n\n", .{func_fqn.fmt(&cg.zcu.intern_pool)}); } else if (AnalyzeBody.current) |anal| { try dumpCrashContextSema(anal, w, &S.crash_heap); + } else if (LinkerOp.current) |linker_op| { + try w.writeAll("Linker snapshot:\n"); + switch (try linker_op.lf.dump(w, linker_op.tid)) { + .unimplemented => try w.writeAll("(backend does not support link snapshots)"), + .needs_extensions => try w.writeAll("(build with -Ddebug-extensions to dump linker state)"), + .disabled => try w.writeAll("(run with --debug-link-snapshot to dump linker state)"), + .enabled => {}, + } + try w.writeAll("\n\n"); } else { try w.writeAll("(no context)\n\n"); } @@ -185,6 +213,7 @@ const Zir = std.zig.Zir; const Sema = @import("Sema.zig"); const Zcu = @import("Zcu.zig"); +const link = @import("link.zig"); const InternPool = @import("InternPool.zig"); const dev = @import("dev.zig"); const print_zir = @import("print_zir.zig"); diff --git a/src/libs/mingw.zig b/src/libs/mingw.zig @@ -207,9 +207,14 @@ fn addCrtCcArgs( }); } -pub fn buildImportLib(comp: *Compilation, lib_name: []const u8) !void { +pub fn buildImportLib(comp: *Compilation, lib_name: []const u8, prog_node: std.Progress.Node) !Cache.Path { dev.check(.build_import_lib); + log.debug("buildImportLib({s})", .{lib_name}); + + const sub_node = prog_node.start(lib_name, 0); + defer sub_node.end(); + const gpa = comp.gpa; const io = comp.io; @@ -218,12 +223,7 @@ pub fn buildImportLib(comp: *Compilation, lib_name: []const u8) !void { const arena = arena_allocator.allocator(); const def_file_path = findDef(arena, io, comp.getTarget(), comp.dirs.zig_lib, lib_name) catch |err| switch (err) { - error.FileNotFound => { - log.debug("no {s}.def file available to make a DLL import {s}.lib", .{ lib_name, lib_name }); - // In this case we will end up putting foo.lib onto the linker line and letting the linker - // use its library paths to look for libraries and report any problems. - return; - }, + error.FileNotFound => return error.DefNotFound, else => |e| return e, }; // Only .def.in files need preprocessing @@ -263,14 +263,16 @@ pub fn buildImportLib(comp: *Compilation, lib_name: []const u8) !void { comp.mutex.lockUncancelable(io); defer comp.mutex.unlock(io); try comp.crt_files.ensureUnusedCapacity(gpa, 1); + + const crt_file_path: Cache.Path = .{ + .root_dir = comp.dirs.global_cache, + .sub_path = sub_path, + }; comp.crt_files.putAssumeCapacityNoClobber(final_lib_basename, .{ - .full_object_path = .{ - .root_dir = comp.dirs.global_cache, - .sub_path = sub_path, - }, + .full_object_path = crt_file_path, .lock = man.toOwnedLock(), }); - return; + return crt_file_path; } const digest = man.final(); @@ -294,6 +296,9 @@ pub fn buildImportLib(comp: *Compilation, lib_name: []const u8) !void { } const members = members: { + const members_node = sub_node.start("Members", 0); + defer members_node.end(); + const input = switch (def_needs_preprocessing) { true => pp: { var aw: Io.Writer.Allocating = .init(gpa); @@ -357,13 +362,15 @@ pub fn buildImportLib(comp: *Compilation, lib_name: []const u8) !void { comp.mutex.lockUncancelable(io); defer comp.mutex.unlock(io); + const crt_file_path: Cache.Path = .{ + .root_dir = comp.dirs.global_cache, + .sub_path = lib_final_path, + }; try comp.crt_files.putNoClobber(gpa, final_lib_basename, .{ - .full_object_path = .{ - .root_dir = comp.dirs.global_cache, - .sub_path = lib_final_path, - }, + .full_object_path = crt_file_path, .lock = man.toOwnedLock(), }); + return crt_file_path; } pub fn libExists( diff --git a/src/libs/mingw/implib.zig b/src/libs/mingw/implib.zig @@ -1012,7 +1012,7 @@ fn getShortImport( fn writeSymbol(writer: *std.Io.Writer, symbol: std.coff.Symbol) !void { try writer.writeAll(&symbol.name); try writer.writeInt(u32, symbol.value, .little); - try writer.writeInt(u16, @intFromEnum(symbol.section_number), .little); + try writer.writeInt(i16, @intFromEnum(symbol.section_number), .little); try writer.writeInt(u8, @intFromEnum(symbol.type.base_type), .little); try writer.writeInt(u8, @intFromEnum(symbol.type.complex_type), .little); try writer.writeInt(u8, @intFromEnum(symbol.storage_class), .little); diff --git a/src/link.zig b/src/link.zig @@ -25,6 +25,7 @@ const Package = @import("Package.zig"); const dev = @import("dev.zig"); const target_util = @import("target.zig"); const codegen = @import("codegen.zig"); +const crash_report = @import("crash_report.zig"); pub const aarch64 = @import("link/aarch64.zig"); pub const LdScript = @import("link/LdScript.zig"); @@ -481,7 +482,7 @@ pub const File = struct { rpath_list: []const []const u8, /// Zig compiler development linker flags. - /// Enable dumping of linker's state as JSON. + /// Enable dumping of linker's state. enable_link_snapshots: bool, /// Darwin-specific linker flags: @@ -790,6 +791,7 @@ pub const File = struct { assert(base.comp.zcu.?.llvm_object == null); const nav = pt.zcu.intern_pool.getNav(nav_index); assert(nav.resolved.?.value != .none); + switch (base.tag) { .lld => unreachable, .plan9 => unreachable, @@ -924,6 +926,9 @@ pub const File = struct { /// Commit pending changes and write headers. Takes into account final output mode. /// `arena` has the lifetime of the call to `Compilation.update`. pub fn flush(base: *File, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node) Error!void { + crash_report.LinkerOp.start(base, tid); + defer crash_report.LinkerOp.stop(base, tid); + const comp = base.comp; const io = comp.io; if (comp.clang_preprocessor_mode == .yes or comp.clang_preprocessor_mode == .pch) { @@ -975,6 +980,10 @@ pub const File = struct { export_indices: []const Zcu.Export.Index, ) Error!void { assert(base.comp.zcu.?.llvm_object == null); + + crash_report.LinkerOp.start(base, pt.tid); + defer crash_report.LinkerOp.stop(base, pt.tid); + switch (base.tag) { .lld => unreachable, .plan9 => unreachable, @@ -1006,6 +1015,7 @@ pub const File = struct { /// Never called when LLVM is codegenning the ZCU. pub fn getNavVAddr(base: *File, pt: Zcu.PerThread, nav_index: InternPool.Nav.Index, reloc_info: RelocInfo) Error!u64 { assert(base.comp.zcu.?.llvm_object == null); + switch (base.tag) { .lld => unreachable, .c => unreachable, @@ -1027,6 +1037,7 @@ pub const File = struct { decl_align: InternPool.Alignment, ) Error!SymbolId { assert(base.comp.zcu.?.llvm_object == null); + switch (base.tag) { .lld => unreachable, .c => unreachable, @@ -1043,6 +1054,7 @@ pub const File = struct { /// Never called when LLVM is codegenning the ZCU. pub fn getUavVAddr(base: *File, decl_val: InternPool.Index, reloc_info: RelocInfo) Error!u64 { assert(base.comp.zcu.?.llvm_object == null); + switch (base.tag) { .lld => unreachable, .c => unreachable, @@ -1063,6 +1075,7 @@ pub const File = struct { name: InternPool.NullTerminatedString, ) void { assert(base.comp.zcu.?.llvm_object == null); + switch (base.tag) { .lld => unreachable, .plan9 => unreachable, @@ -1077,6 +1090,31 @@ pub const File = struct { } } + pub const DumpResult = enum { + unimplemented, + needs_extensions, + disabled, + enabled, + }; + + pub fn dump(base: *File, w: *Io.Writer, tid: Zcu.PerThread.Id) !DumpResult { + if (!build_options.enable_debug_extensions) return .not_built; + switch (base.tag) { + .elf, + .macho, + .c, + .wasm, + .spirv, + .plan9, + .lld, + => return .unimplemented, + inline else => |tag| { + dev.check(tag.devFeature()); + return @as(*tag.Type(), @fieldParentPtr("base", base)).dump(w, tid); + }, + } + } + /// Opens a path as an object file and parses it into the linker. fn openLoadObject(base: *File, path: Path) anyerror!void { if (base.tag == .lld) return; @@ -1178,8 +1216,9 @@ pub const File = struct { pub fn loadInput(base: *File, input: Input) anyerror!void { if (base.tag == .lld) return; assert(!base.post_prelink); + switch (base.tag) { - inline .elf, .elf2, .wasm, .spirv => |tag| { + inline .coff2, .elf, .elf2, .wasm, .spirv => |tag| { dev.check(tag.devFeature()); return @as(*tag.Type(), @fieldParentPtr("base", base)).loadInput(input); }, @@ -1441,7 +1480,8 @@ pub fn doPrelinkTask(comp: *Compilation, task: PrelinkTask) void { const target = &comp.root_mod.resolved_target.result; const flags = target_util.libcFullLinkFlags(target); - const crt_dir = comp.libc_installation.?.crt_dir.?; + const libc_installation = comp.libc_installation.?; + const crt_dir = libc_installation.crt_dir.?; const sep = std.fs.path.sep_str; for (flags) |flag| { assert(mem.startsWith(u8, flag, "-l")); @@ -1493,6 +1533,54 @@ pub fn doPrelinkTask(comp: *Compilation, task: PrelinkTask) void { }, } } + + if (target.os.tag == .windows and target.abi == .msvc) { + const inputs: []const struct { + dir: enum { crt, msvc_lib, kernel32_lib }, + name: []const u8, + } = switch (comp.config.link_mode) { + .dynamic => &.{ + .{ .dir = .msvc_lib, .name = "msvcrt.lib" }, + .{ .dir = .msvc_lib, .name = "vcruntime.lib" }, + .{ .dir = .msvc_lib, .name = "legacy_stdio_definitions.lib" }, + .{ .dir = .crt, .name = "ucrt.lib" }, + .{ .dir = .kernel32_lib, .name = "kernel32.lib" }, + .{ .dir = .kernel32_lib, .name = "ntdll.lib" }, + }, + .static => &.{ + .{ .dir = .msvc_lib, .name = "libcmt.lib" }, + .{ .dir = .msvc_lib, .name = "libvcruntime.lib" }, + .{ .dir = .msvc_lib, .name = "legacy_stdio_definitions.lib" }, + .{ .dir = .crt, .name = "libucrt.lib" }, + .{ .dir = .kernel32_lib, .name = "kernel32.lib" }, + .{ .dir = .kernel32_lib, .name = "ntdll.lib" }, + }, + }; + + for (inputs) |lib| { + const path = Path.initCwd( + std.fmt.allocPrint(comp.arena, "{s}" ++ sep ++ "{s}", .{ + switch (lib.dir) { + .crt => crt_dir, + .msvc_lib => libc_installation.msvc_lib_dir.?, + .kernel32_lib => libc_installation.kernel32_lib_dir.?, + }, + lib.name, + }) catch return diags.setAllocFailure(), + ); + if (std.mem.endsWith(u8, lib.name, "lib")) { + base.openLoadArchive(path, false) catch |err| switch (err) { + error.LinkFailure => return, // error reported via diags + else => |e| diags.addParseError(path, "failed to parse archive: {s}", .{@errorName(e)}), + }; + } else { + base.openLoadObject(path) catch |err| switch (err) { + error.LinkFailure => return, // error reported via diags + else => |e| diags.addParseError(path, "failed to parse object: {s}", .{@errorName(e)}), + }; + } + } + } }, .load_object => |path| { const prog_node = comp.link_prog_node.start("Parse Object", 0); diff --git a/src/link/Coff.zig b/src/link/Coff.zig @@ -17,11 +17,40 @@ const target_util = @import("../target.zig"); const Type = @import("../Type.zig"); const Value = @import("../Value.zig"); const Zcu = @import("../Zcu.zig"); +const ModuleDefinition = @import("../libs/mingw/def.zig").ModuleDefinition; +const implib = @import("../libs/mingw/implib.zig"); +const Path = std.Build.Cache.Path; base: link.File, +options: link.File.OpenOptions, mf: MappedFile, nodes: std.MultiArrayList(Node), +members: std.ArrayList(Member), +pending_members: std.array_hash_map.Auto(Member.Index, void), +lib_string_table: std.ArrayList(String), +lib_string_len: u32, +long_names_table: LongNamesTable, import_table: ImportTable, +export_table: ExportTable, +symbol_table: SymbolTable, +inputs: std.array_hash_map.Custom(std.Build.Cache.Path, void, std.Build.Cache.Path.TableAdapter, false), +input_archives: std.ArrayList(InputArchive), +input_archive_members: std.ArrayList(InputArchive.Member), +input_archive_symbols: std.ArrayList(InputArchive.Member.Symbol), +input_archive_symbol_indices: std.array_hash_map.Auto(String, InputArchive.SearchList), +pending_input: ?InputArchive.Member.Index, +pending_default_libs: std.ArrayList(struct { + path: []const u8, + ioi: InputObject.Index, +}), +alternate_names: std.array_hash_map.Auto(String, String), +input_objects: std.ArrayList(InputObject), +input_symbols: std.ArrayList(struct { si: Symbol.Index, name: String }), +input_sections: std.ArrayList(Node.InputSection), +input_section_pending_index: u32, +inputs_complete: bool, +exports_complete: bool, +pending_special_symbol: SpecialSymbol, strings: std.HashMapUnmanaged( u32, void, @@ -29,11 +58,13 @@ strings: std.HashMapUnmanaged( std.hash_map.default_max_load_percentage, ), string_bytes: std.ArrayList(u8), -image_section_table: std.ArrayList(Symbol.Index), +section_table: std.array_hash_map.Auto(String, Section), pseudo_section_table: std.array_hash_map.Auto(String, Symbol.Index), object_section_table: std.array_hash_map.Auto(String, Symbol.Index), -symbol_table: std.ArrayList(Symbol), -globals: std.array_hash_map.Auto(GlobalName, Symbol.Index), +section_merges: std.array_hash_map.Auto(String, String), +section_merge_pending_index: u32, +symbols: std.ArrayList(Symbol), +globals: std.array_hash_map.Auto(String, Global), global_pending_index: u32, navs: std.array_hash_map.Auto(InternPool.Nav.Index, Symbol.Index), uavs: std.array_hash_map.Auto(InternPool.Index, Symbol.Index), @@ -45,8 +76,13 @@ pending_uavs: std.array_hash_map.Auto(Node.UavMapIndex, struct { alignment: InternPool.Alignment, }), relocs: std.ArrayList(Reloc), +first_free_reloc: Reloc.Index, +last_free_reloc: Reloc.Index, const_prog_node: std.Progress.Node, synth_prog_node: std.Progress.Node, +symbol_prog_node: std.Progress.Node, +member_prog_node: std.Progress.Node, +input_prog_node: std.Progress.Node, pub const default_file_alignment: u16 = 0x200; pub const default_size_of_stack_reserve: u32 = 0x1000000; @@ -54,6 +90,17 @@ pub const default_size_of_stack_commit: u32 = 0x1000; pub const default_size_of_heap_reserve: u32 = 0x100000; pub const default_size_of_heap_commit: u32 = 0x1000; +pub const imp_prefix = "__imp_"; + +const header_name_max_len = @typeInfo(@FieldType(std.coff.SectionHeader, "name")).array.len; + +const Error = link.Error || error{MappedFileIo}; +const LoadInputError = Error || + Io.File.SeekError || + Io.File.Reader.SizeError || + Io.Reader.Error || + MappedFile.Error; + /// 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. @@ -134,25 +181,53 @@ pub const msdos_stub: [120]u8 = .{ pub const Node = union(enum) { file, header, + /// Images and archives only. signature, + /// Archives only. + archive_member_header: Member.Index, + archive_member: Member.Index, + coff_header, + + /// Image only optional_header, data_directories, + section_table, + + /// Archives and objects only + symbol_table, + string_table, + relocation_table: Symbol.SectionNumber, + relocation_table_entry: Reloc.Index, + image_section: Symbol.Index, + /// Images only import_directory_table, import_lookup_table: ImportTable.Index, import_address_table: ImportTable.Index, import_hint_name_table: ImportTable.Index, + /// Images only + export_directory_table, + export_address_table, + export_name_pointer_table, + export_ordinal_table, + export_name_table, + pseudo_section: PseudoSectionMapIndex, object_section: ObjectSectionMapIndex, - global: GlobalMapIndex, + input_section: InputSection.Index, + import_thunk: GlobalMapIndex, nav: NavMapIndex, uav: UavMapIndex, lazy_code: LazyMapRef.Index(.code), lazy_const_data: LazyMapRef.Index(.const_data), + builtin: Symbol.Index, + + /// Takes the place of a known node index when that node is not present in the output + placeholder, pub const PseudoSectionMapIndex = enum(u32) { _, @@ -179,14 +254,30 @@ pub const Node = union(enum) { }; pub const GlobalMapIndex = enum(u32) { + none, _, - pub fn globalName(gmi: GlobalMapIndex, coff: *const Coff) GlobalName { - return coff.globals.keys()[@intFromEnum(gmi)]; + pub fn wrap(i: ?u32) GlobalMapIndex { + return @enumFromInt((i orelse return .none) + 1); + } + + pub fn unwrap(gmi: GlobalMapIndex) ?u32 { + return switch (gmi) { + .none => null, + _ => @intFromEnum(gmi) - 1, + }; + } + + pub fn name(gmi: GlobalMapIndex, coff: *const Coff) String { + return coff.globals.keys()[gmi.unwrap().?]; } pub fn symbol(gmi: GlobalMapIndex, coff: *const Coff) Symbol.Index { - return coff.globals.values()[@intFromEnum(gmi)]; + return coff.globals.values()[gmi.unwrap().?].si; + } + + pub fn libName(gmi: GlobalMapIndex, coff: *const Coff) String.Optional { + return coff.globals.values()[gmi.unwrap().?].lib_name; } }; @@ -214,6 +305,47 @@ pub const Node = union(enum) { } }; + const InputSection = struct { + ioi: InputObject.Index, + si: Symbol.Index, + comdat_si: Symbol.Index, + file_location: MappedFile.Node.FileLocation, + first_li: Node.InputSection.LocalIndex, + crc: u32, + + pub const Index = enum(u32) { + _, + + pub fn inputSection(isi: Index, coff: *const Coff) *InputSection { + return &coff.input_sections.items[@intFromEnum(isi)]; + } + + pub fn input(isi: Index, coff: *const Coff) InputObject.Index { + return coff.input_sections.items[@intFromEnum(isi)].ioi; + } + + pub fn fileLocation(isi: Index, coff: *const Coff) MappedFile.Node.FileLocation { + return coff.input_sections.items[@intFromEnum(isi)].file_location; + } + + pub fn symbol(isi: Index, coff: *const Coff) Symbol.Index { + return coff.input_sections.items[@intFromEnum(isi)].si; + } + + pub fn firstSymbol(isi: Index, coff: *const Coff) LocalIndex { + return coff.input_sections.items[@intFromEnum(isi)].first_li; + } + }; + + const LocalIndex = enum(u32) { + _, + + pub fn name(isli: LocalIndex, coff: *const Coff) String { + return coff.input_symbols.items[@intFromEnum(isli)].name; + } + }; + }; + pub const LazyMapRef = struct { kind: link.File.LazySymbol.Kind, index: u32, @@ -253,6 +385,14 @@ pub const Node = union(enum) { file, header, signature, + first_linker_member_header, + first_linker_member, + second_linker_member_header, + second_linker_member, + longnames_member_header, + longnames_member, + zcu_member_header, + zcu_member, coff_header, optional_header, data_directories, @@ -270,14 +410,338 @@ pub const Node = union(enum) { } }; +pub const InputArchive = struct { + path: std.Build.Cache.Path, + + const Index = enum(u32) { + _, + + pub fn path(iai: InputArchive.Index, coff: *Coff) std.Build.Cache.Path { + return coff.input_archives.items[@intFromEnum(iai)].path; + } + }; + + pub const Member = struct { + iai: InputArchive.Index, + name: String, + content: union(enum) { + // This range includes the member header + object: MappedFile.Node.FileLocation, + import: struct { + symbol_name: String, + lib_name: String, + // Either ordinal or hint, depending on value of name_type + import_ordinal_hint: u16, + type: std.coff.ImportType, + name_type: std.coff.ImportNameType, + }, + }, + flags: packed struct { + // Set if an attempt was made to load this member + is_loaded: bool, + }, + + const Index = enum(u32) { + _, + + pub fn member(iami: InputArchive.Member.Index, coff: *Coff) *InputArchive.Member { + return &coff.input_archive_members.items[@intFromEnum(iami)]; + } + }; + + pub const Symbol = struct { + iami: InputArchive.Member.Index, + // Set to its own index to indicate its the last in the list + next: InputArchive.Member.Symbol.Index, + + const Index = enum(u32) { + _, + }; + }; + }; + + pub const SearchList = struct { + first: InputArchive.Member.Symbol.Index, + last: InputArchive.Member.Symbol.Index, + }; +}; + +pub const InputObject = struct { + path: std.Build.Cache.Path, + member_name: ?[]const u8, + source_name: String.Optional, + + pub const Index = enum(u32) { + _, + + pub fn path(ioi: Index, coff: *const Coff) std.Build.Cache.Path { + return coff.input_objects.items[@intFromEnum(ioi)].path; + } + + pub fn memberName(ioi: Index, coff: *const Coff) ?[]const u8 { + return coff.input_objects.items[@intFromEnum(ioi)].member_name; + } + }; +}; + +pub const Member = struct { + kind: std.coff.ArchiveMemberHeader.Kind, + header_ni: MappedFile.Node.Index, + content_ni: MappedFile.Node.Index, + first_linker_indices: std.array_hash_map.Auto(struct { + mi: Member.Index, + name: String, + }, FirstLinkerIndex), + + pub const Index = enum(u16) { + first, + second, + longnames, + _, + + const known_count = @typeInfo(Index).@"enum".field_names.len; + + pub fn get(member_index: Member.Index, coff: *Coff) *Member { + return &coff.members.items[@intFromEnum(member_index)]; + } + }; + + pub const FirstLinkerIndex = enum(u32) { + _, + }; + + pub fn headerPtr(member: *Member, coff: *Coff) *std.coff.ArchiveMemberHeader { + return @ptrCast(@alignCast(member.header_ni.slice(&coff.mf))); + } + + /// Sets `name` as the name field of this member's header, either directly (if it's short enough), + /// or by creating an entry in the longnames member and storing a reference to that entry. + pub fn initHeader(member: *Member, coff: *Coff, name: []const u8, timestamp: u32) !void { + const max_name_len = @typeInfo(@FieldType(std.coff.ArchiveMemberHeader, "name")).array.len; + const opt_name_offset = if (name.len >= max_name_len) offset: { + const gpa = coff.base.comp.gpa; + const entries_ctx = LongNamesTable.Adapter{ .coff = coff }; + const gop = try coff.long_names_table.entries.getOrPutAdapted( + gpa, + name, + entries_ctx, + ); + + if (!gop.found_existing) { + errdefer _ = coff.export_table.entries.pop(); + + _, const old_size = Node.known.longnames_member.location(&coff.mf).resolve(&coff.mf); + const new_size = old_size + name.len + 1; + assert(new_size < comptime try std.math.powi(u64, 10, max_name_len - 1)); + + try Node.known.longnames_member.resize(&coff.mf, gpa, new_size); + const name_table_slice = Node.known.longnames_member.slice(&coff.mf); + const name_slice = name_table_slice[@intCast(old_size)..][0 .. name.len + 1]; + @memcpy(name_slice[0..name.len], name); + name_slice[name.len] = 0; + + gop.value_ptr.* = .{ + .offset = old_size, + .len = name.len, + }; + } + + break :offset gop.value_ptr.offset; + } else null; + + const header = member.headerPtr(coff); + if (opt_name_offset) |name_offset| { + header.name[0] = '/'; + storeHeaderDecimalStr(header.name[1..], name_offset); + } else { + @memcpy(header.name[0..name.len], name); + header.name[name.len] = '/'; + const padding = max_name_len - name.len - 1; + @memset(header.name[max_name_len - padding ..], ' '); + } + + storeHeaderDecimalStr(&header.date, timestamp); + + // Matching the Microsoft behaviour of emitting blanks for these fields + header.user_id = @splat(' '); + header.group_id = @splat(' '); + + // file_mode is actually octal, but we only ever write 0 to it + storeHeaderDecimalStr(&header.file_mode, 0); + if (!member.content_ni.hasResized(&coff.mf)) + storeHeaderDecimalStr( + &header.size, + member.content_ni.location(&coff.mf).resolve(&coff.mf)[1], + ); + + @memcpy(&header.end_of_header, std.coff.archive_end_of_header); + } + + pub fn storeHeaderDecimalStr(field_ptr: anytype, value: u64) void { + const array_info = @typeInfo(@typeInfo(@TypeOf(field_ptr)).pointer.child).array; + assert(array_info.child == u8); + assert(value < comptime try std.math.powi(u64, 10, array_info.len)); + _ = std.fmt.printInt(field_ptr, value, 10, .lower, .{ + .width = array_info.len, + .alignment = .left, + .fill = ' ', + }); + } + + pub fn loadHeaderDecimalStr(field_ptr: anytype, value: u64) void { + const array_info = @typeInfo(@typeInfo(@TypeOf(field_ptr)).pointer.child).array; + assert(array_info.child == u8); + assert(value < comptime try std.math.powi(u64, 10, array_info.len)); + _ = std.fmt.printInt(field_ptr, value, 10, .lower, .{ + .width = array_info.len, + .alignment = .left, + .fill = ' ', + }); + } +}; + +pub const LongNamesTable = struct { + ni: MappedFile.Node.Index = .none, + entries: std.array_hash_map.Auto(void, Entry), + + pub const Entry = struct { + offset: u64, + len: u64, + }; + + const Adapter = struct { + coff: *Coff, + + pub fn eql(adapter: Adapter, lhs_key: []const u8, _: void, rhs_index: usize) bool { + assert(adapter.coff.isArchive()); + const longnames_slice = Node.known.longnames_member.slice(&adapter.coff.mf); + const rhs = adapter.coff.long_names_table.entries.values()[rhs_index]; + return std.mem.eql(u8, longnames_slice[@intCast(rhs.offset)..][0..@intCast(rhs.len)], lhs_key); + } + + pub fn hash(_: Adapter, key: []const u8) u32 { + assert(std.mem.indexOfScalar(u8, key, 0) == null); + return std.array_hash_map.hashString(key); + } + }; +}; + +pub const SymbolTable = struct { + ni: MappedFile.Node.Index, + strings_ni: MappedFile.Node.Index, + strings: std.array_hash_map.Auto(String, StringIndex), + symbols: std.array_hash_map.Auto(Symbol.Index, SymbolTable.Index), + pending_symbol_index: u32, + + // Resizing the symbol table node has the result of accumulating padding + // between the last symbol in the symbol table node and the start of the + // string table node, due to the shifting method when resizing the parent in MappedFile. + // The spec requires the string table begin immediately after the last symbol, + // so we compact the symbol table node and move the string table back if needed. + pending_shrink: bool, + + pub const StringIndex = enum(u32) { + _, + }; + + pub const SymbolName = union(enum) { + short: []const u8, + long: StringIndex, + + pub fn store(name: SymbolName, coff: *const Coff, field: *[8]u8) void { + switch (name) { + .short => |s| { + @memcpy(field[0..s.len], s); + @memset(field[s.len..], 0); + }, + .long => |l| { + @memset(field[0..4], 0); + std.mem.writePackedInt(u32, field[4..], 0, @intFromEnum(l), coff.targetEndian()); + }, + } + } + }; + + // Symbol.Index does not map 1:1 with SymbolTable.Index: + // - Not all symbols need a symbol table entry + // - A variable number of auxiliary entries may trail each symbol + pub const Index = enum(u32) { + none, + _, + + pub fn wrap(i: u32) Index { + return @enumFromInt(i + 1); + } + + pub fn unwrap(sti: Index) ?u32 { + return switch (sti) { + .none => null, + _ => @intFromEnum(sti) - 1, + }; + } + }; +}; + +pub const ExportTable = struct { + ni: MappedFile.Node.Index, + export_directory_table_ni: MappedFile.Node.Index, + export_address_table_si: Symbol.Index, + name_pointer_table_ni: MappedFile.Node.Index, + ordinal_table_ni: MappedFile.Node.Index, + name_table_ni: MappedFile.Node.Index, + entries: std.array_hash_map.Auto(void, Entry), + pending_sort: bool = false, + + pub const Entry = struct { + si: Symbol.Index, + name_index: u32, + name_len: u32, + export_address_table_ri: Reloc.Index, + }; + + const Adapter = struct { + coff: *Coff, + + pub fn eql(adapter: Adapter, lhs_key: []const u8, _: void, rhs_index: usize) bool { + const coff = adapter.coff; + const name_table_slice = coff.export_table.name_table_ni.slice(&coff.mf); + const rhs = coff.export_table.entries.values()[rhs_index]; + return std.mem.eql(u8, name_table_slice[rhs.name_index..][0..rhs.name_len], lhs_key); + } + + pub fn hash(_: Adapter, key: []const u8) u32 { + assert(std.mem.indexOfScalar(u8, key, 0) == null); + return std.array_hash_map.hashString(key); + } + }; + + pub const Ordinal = enum(u16) { + _, + + pub fn get(export_index: ExportTable.Ordinal, coff: *Coff) *Entry { + return &coff.export_table.entries.values()[@intFromEnum(export_index)]; + } + }; +}; + pub const ImportTable = struct { ni: MappedFile.Node.Index, entries: std.array_hash_map.Auto(void, Entry), + iat_symbol_indices: std.array_hash_map.Auto(struct { + iti: ImportTable.Index, + name: String.Optional, + // If name == .none this is the ordinal, otherwise the hint + ordinal_hint: u16, + }, u32), pub const Entry = struct { import_lookup_table_ni: MappedFile.Node.Index, import_address_table_si: Symbol.Index, import_hint_name_table_ni: MappedFile.Node.Index, + // All .iat_ptr globals that reference this table. + // This is separate from `iat_symbol_indices` because multiple symbols + // can reference to the same iat entry, after name demangling. + import_address_table_symbols: std.ArrayList(Symbol.Index), len: u32, hint_name_len: u32, }; @@ -314,13 +778,32 @@ pub const String = enum(u32) { @".rdata" = 13, @".text" = 20, @".tls$" = 26, + @".edata" = 32, + @".ctors" = 39, + @".ctors$ZZZ" = 46, + @".dtors" = 57, + @".dtors$ZZZ" = 64, + @".bss" = 75, + @".fptable" = 80, + @".tls" = 89, + @".thunks" = 94, _, pub const Optional = enum(u32) { @".data" = @intFromEnum(String.@".data"), + @".idata" = @intFromEnum(String.@".idata"), @".rdata" = @intFromEnum(String.@".rdata"), @".text" = @intFromEnum(String.@".text"), @".tls$" = @intFromEnum(String.@".tls$"), + @".edata" = @intFromEnum(String.@".edata"), + @".ctors" = @intFromEnum(String.@".ctors"), + @".ctors$ZZZ" = @intFromEnum(String.@".ctors$ZZZ"), + @".dtors" = @intFromEnum(String.@".dtors"), + @".dtors$ZZZ" = @intFromEnum(String.@".dtors$ZZZ"), + @".bss" = @intFromEnum(String.@".bss"), + @".fptable" = @intFromEnum(String.@".fptable"), + @".tls" = @intFromEnum(String.@".tls"), + @".thunks" = @intFromEnum(String.@".thunks"), none = std.math.maxInt(u32), _, @@ -346,20 +829,176 @@ pub const String = enum(u32) { } }; -pub const GlobalName = struct { name: String, lib_name: String.Optional }; +pub const Section = struct { + si: Symbol.Index, + relocation_table_ni: MappedFile.Node.Index, + + pub const RelocationIndex = enum(u16) { + none, + _, + + pub fn wrap(i: ?u16) RelocationIndex { + return @enumFromInt((i orelse return .none) + 1); + } + + pub fn unwrap(sri: RelocationIndex) ?u16 { + return switch (sri) { + .none => null, + _ => @intFromEnum(sri) - 1, + }; + } + + pub fn entry( + sri: RelocationIndex, + coff: *Coff, + sn: Symbol.SectionNumber, + ) ?*align(2) std.coff.Relocation { + if (sri == .none) return null; + const table_slice = sn.section(coff).relocation_table_ni.slice(&coff.mf); + return @ptrCast(@alignCast(&table_slice[@as(u32, sri.unwrap().?) * std.coff.Relocation.sizeOf()])); + } + }; +}; + +pub const Global = struct { + si: Symbol.Index, + lib_name: String.Optional, +}; + +pub const WeakExternalStrat = enum(u3) { + none, + no_library, + library, + alias, + anti_dependency, + + pub fn fromFlag(flag: std.coff.WeakExternalFlag) WeakExternalStrat { + return switch (flag) { + .SEARCH_NOLIBRARY => .no_library, + .SEARCH_LIBRARY => .library, + .SEARCH_ALIAS => .alias, + .ANTI_DEPENDENCY => .anti_dependency, + _ => unreachable, + }; + } +}; + +const SpecialSymbol = enum { + entry, + tls, + none, +}; pub const Symbol = struct { ni: MappedFile.Node.Index, rva: u32, - size: u32, + value: std.meta.BareUnion(Symbol.Value), + extra: std.meta.BareUnion(Symbol.Extra), + flags: packed struct(u16) { + value_tag: ValueTag, + extra_tag: ExtraTag, + type: Symbol.Type, + dll_storage_class: DllStorageClass, + weak_external_strat: WeakExternalStrat, + _: u5 = 0, + }, /// Relocations contained within this symbol loc_relocs: Reloc.Index, /// Relocations targeting this symbol target_relocs: Reloc.Index, section_number: SectionNumber, - unused0: u32 = 0, - unused1: u32 = 0, - unused2: u16 = 0, + gmi: Node.GlobalMapIndex, + + pub const DllStorageClass = enum(u2) { + default, + dllimport, + dllexport, + }; + + pub const Type = enum(u2) { + unknown, + code, + data, + }; + + const ValueTag = enum(u2) { + none, + node_offset, + weak_alias_si, + weak_alias_name, + }; + + pub const Value = union(ValueTag) { + none, + /// The offset of the symbol within its node. Used with symbols that + /// don't create their own nodes: .input_section, .import_address_table + /// Images only. + node_offset: u32, + /// Images: the weak alias that should replace this symbol if it is not resolved. + /// Objects: he target of a weak external that hasn't been assigned an sti yet. + /// Globals only. + weak_alias_si: Symbol.Index, + /// For weak externals that have an alias that is also an undef + /// external, this is the name of the alias global that should + /// be generated and resolved if this symbol is not resolved. + /// Globals only, images only. + weak_alias_name: String, + }; + + const ExtraTag = enum(u2) { + size, + isli, + next_alias_si, + }; + + pub const Extra = union(ExtraTag) { + // The size of the symbol + size: u32, + /// Only valid when .ni == .input_section and .value_tag == .node_offset + isli: Node.InputSection.LocalIndex, + /// The next symbol in the list of aliases of this symbol. + next_alias_si: Symbol.Index, + }; + + pub fn setValue(sym: *Symbol, value: Symbol.Value) void { + sym.flags.value_tag = std.meta.activeTag(value); + sym.value = switch (sym.flags.value_tag) { + inline else => |t| @unionInit( + @FieldType(Symbol, "value"), + @tagName(t), + @field(value, @tagName(t)), + ), + }; + } + + pub fn setExtra(sym: *Symbol, extra: Symbol.Extra) void { + sym.flags.extra_tag = std.meta.activeTag(extra); + sym.extra = switch (sym.flags.extra_tag) { + inline else => |t| @unionInit( + @FieldType(Symbol, "extra"), + @tagName(t), + @field(extra, @tagName(t)), + ), + }; + } + + pub fn nodeOffset(sym: *const Symbol, coff: *Coff) u32 { + return switch (sym.flags.value_tag) { + .node_offset => offset: { + assert(switch (coff.getNode(sym.ni)) { + // Separate nodes are not created for these entries per-symbol + .input_section, .import_address_table => true, + else => false, + }); + break :offset sym.value.node_offset; + }, + else => 0, + }; + } + + pub fn size(sym: *const Symbol) u32 { + return if (sym.flags.extra_tag == .size) sym.extra.size else 0; + } pub const SectionNumber = enum(i16) { UNDEFINED = 0, @@ -371,8 +1010,20 @@ pub const Symbol = struct { return @intCast(@intFromEnum(sn) - 1); } + fn hasIndex(sn: SectionNumber) bool { + return @intFromEnum(sn) > 0; + } + pub fn symbol(sn: SectionNumber, coff: *const Coff) Symbol.Index { - return coff.image_section_table.items[sn.toIndex()]; + return sn.section(coff).si; + } + + pub fn name(sn: SectionNumber, coff: *const Coff) String { + return coff.section_table.keys()[sn.toIndex()]; + } + + pub fn section(sn: SectionNumber, coff: *const Coff) *Section { + return &coff.section_table.values()[sn.toIndex()]; } pub fn header(sn: SectionNumber, coff: *Coff) *std.coff.SectionHeader { @@ -382,6 +1033,7 @@ pub const Symbol = struct { pub const Index = enum(u32) { null, + bss, data, rdata, text, @@ -390,7 +1042,12 @@ pub const Symbol = struct { const known_count = @typeInfo(Index).@"enum".field_names.len; pub fn get(si: Symbol.Index, coff: *Coff) *Symbol { - return &coff.symbol_table.items[@intFromEnum(si)]; + return &coff.symbols.items[@intFromEnum(si)]; + } + + pub fn unwrap(si: Symbol.Index) ?Symbol.Index { + if (si == .null) return null; + return si; } pub fn node(si: Symbol.Index, coff: *Coff) MappedFile.Node.Index { @@ -399,37 +1056,92 @@ pub const Symbol = struct { return ni; } - pub fn flushMoved(si: Symbol.Index, coff: *Coff) void { - const sym = si.get(coff); - sym.rva = coff.computeNodeRva(sym.ni); - si.applyLocationRelocs(coff); - si.applyTargetRelocs(coff); + pub fn sti(si: Symbol.Index, coff: *Coff) SymbolTable.Index { + assert(!coff.isImage()); + return coff.symbol_table.symbols.get(si) orelse .none; + } + + pub fn next(si: Symbol.Index) Symbol.Index { + return @enumFromInt(@intFromEnum(si) + 1); + } + + pub fn knownString(si: Symbol.Index) String.Optional { + return switch (si) { + .null, _ => .none, + inline else => |tag| @field(String.Optional, "." ++ @tagName(tag)), + }; } - pub fn applyLocationRelocs(si: Symbol.Index, coff: *Coff) void { - for (coff.relocs.items[@intFromEnum(si.get(coff).loc_relocs)..]) |*reloc| { - if (reloc.loc != si) break; - reloc.apply(coff); + pub fn flushMoved(si: Symbol.Index, coff: *Coff) !void { + const sym = si.get(coff); + sym.rva = coff.computeNodeRva(sym.ni) + sym.nodeOffset(coff); + try si.applyLocationRelocs(coff); + try si.applyTargetRelocs(coff, .none); + + var alias_sym = sym; + while (alias_sym.flags.extra_tag == .next_alias_si) { + const alias_si = alias_sym.extra.next_alias_si; + alias_sym = alias_si.get(coff); + assert(alias_sym.ni == sym.ni); + alias_sym.rva = sym.rva; + try alias_si.applyTargetRelocs(coff, .none); } } - pub fn applyTargetRelocs(si: Symbol.Index, coff: *Coff) void { - var ri = si.get(coff).target_relocs; + pub fn flushSymbolTableIndex(si: Symbol.Index, coff: *Coff) void { + const sym = si.get(coff); + const index = si.sti(coff).unwrap().?; + var ri = sym.target_relocs; while (ri != .none) { const reloc = ri.get(coff); assert(reloc.target == si); - reloc.apply(coff); + if (reloc.sri.entry(coff, reloc.loc.get(coff).section_number)) |entry| + coff.targetStore(&entry.symbol_table_index, index); + ri = reloc.next; + } + } + + pub fn applyLocationRelocs(si: Symbol.Index, coff: *Coff) !void { + const sym = si.get(coff); + switch (sym.loc_relocs) { + .none => {}, + else => |loc_relocs| { + for (coff.relocs.items[@intFromEnum(loc_relocs)..]) |*reloc| { + if (reloc.loc != si) break; + if (reloc.sri.entry(coff, sym.section_number)) |entry| coff.targetStore( + &entry.virtual_address, + @intCast(coff.computeSymbolSectionOffset(sym, .image) + reloc.offset), + ); + try reloc.apply(coff); + } + }, + } + } + + pub fn applyTargetRelocs(si: Symbol.Index, coff: *Coff, end: Reloc.Index) !void { + const sym = si.get(coff); + + var ri = sym.target_relocs; + while (ri != end) { + const reloc = ri.get(coff); + assert(reloc.target == si); + try reloc.apply(coff); ri = reloc.next; } } pub fn deleteLocationRelocs(si: Symbol.Index, coff: *Coff) void { const sym = si.get(coff); - for (coff.relocs.items[@intFromEnum(sym.loc_relocs)..]) |*reloc| { - if (reloc.loc != si) break; - reloc.delete(coff); + switch (sym.loc_relocs) { + .none => {}, + else => |loc_relocs| { + for (coff.relocs.items[@intFromEnum(loc_relocs)..]) |*reloc| { + if (reloc.loc != si) break; + reloc.delete(coff); + } + sym.loc_relocs = .none; + }, } - sym.loc_relocs = .none; } }; @@ -439,14 +1151,24 @@ pub const Symbol = struct { }; pub const Reloc = extern struct { - type: Reloc.Type, + offset: u64, + addend: i64, + type: Reloc.Type, + sri: Section.RelocationIndex, prev: Reloc.Index, next: Reloc.Index, loc: Symbol.Index, target: Symbol.Index, - unused: u32, - offset: u64, - addend: i64, + flags: packed struct(u8) { + /// Indicates the addend is not known and should be recovered from the location itself. + /// COFF relocation tables don't encode the addend, only the location. + recover_addend: bool, + /// Set if this reloc is in the free list. + /// When set, `prev` / `next` refer to other relocs in the free list. + /// All other fields are undefined. + free: bool, + _: u6 = 0, + }, pub const Type = extern union { AMD64: std.coff.IMAGE.REL.AMD64, @@ -464,135 +1186,319 @@ pub const Reloc = extern struct { none = std.math.maxInt(u32), _, - pub fn get(si: Reloc.Index, coff: *Coff) *Reloc { - return &coff.relocs.items[@intFromEnum(si)]; + pub fn wrap(i: ?u32) Reloc.Index { + return @enumFromInt((i orelse return .none) + 1); + } + + pub fn get(ri: Reloc.Index, coff: *Coff) *Reloc { + return &coff.relocs.items[@intFromEnum(ri)]; } }; - pub fn apply(reloc: *const Reloc, coff: *Coff) void { + pub fn apply(reloc: *Reloc, coff: *Coff) !void { const loc_sym = reloc.loc.get(coff); switch (loc_sym.ni) { .none => return, else => |ni| if (ni.hasMoved(&coff.mf)) return, } - const target_sym = reloc.target.get(coff); - switch (target_sym.ni) { - .none => return, - else => |ni| if (ni.hasMoved(&coff.mf)) return, - } + const loc_slice = loc_sym.ni.slice(&coff.mf)[@intCast(reloc.offset)..]; - const target_rva = target_sym.rva +% @as(u64, @bitCast(reloc.addend)); const target_endian = coff.targetEndian(); - switch (coff.targetLoad(&coff.headerPtr().machine)) { - else => |machine| @panic(@tagName(machine)), - .AMD64 => switch (reloc.type.AMD64) { - else => |kind| @panic(@tagName(kind)), - .ABSOLUTE => {}, - .ADDR64 => std.mem.writeInt( - u64, - loc_slice[0..8], - coff.optionalHeaderField(.image_base) + target_rva, - target_endian, - ), - .ADDR32 => std.mem.writeInt( - u32, - loc_slice[0..4], - @intCast(coff.optionalHeaderField(.image_base) + target_rva), - target_endian, - ), - .ADDR32NB => std.mem.writeInt( - u32, - loc_slice[0..4], - @intCast(target_rva), - target_endian, - ), - .REL32 => std.mem.writeInt( - i32, - loc_slice[0..4], - @intCast(@as(i64, @bitCast(target_rva -% (loc_sym.rva + reloc.offset + 4)))), - target_endian, - ), - .REL32_1 => std.mem.writeInt( - i32, - loc_slice[0..4], - @intCast(@as(i64, @bitCast(target_rva -% (loc_sym.rva + reloc.offset + 5)))), - target_endian, - ), - .REL32_2 => std.mem.writeInt( - i32, - loc_slice[0..4], - @intCast(@as(i64, @bitCast(target_rva -% (loc_sym.rva + reloc.offset + 6)))), - target_endian, - ), - .REL32_3 => std.mem.writeInt( - i32, - loc_slice[0..4], - @intCast(@as(i64, @bitCast(target_rva -% (loc_sym.rva + reloc.offset + 7)))), - target_endian, - ), - .REL32_4 => std.mem.writeInt( - i32, - loc_slice[0..4], - @intCast(@as(i64, @bitCast(target_rva -% (loc_sym.rva + reloc.offset + 8)))), - target_endian, - ), - .REL32_5 => std.mem.writeInt( - i32, - loc_slice[0..4], - @intCast(@as(i64, @bitCast(target_rva -% (loc_sym.rva + reloc.offset + 9)))), - target_endian, - ), - .SECREL => std.mem.writeInt( - u32, - loc_slice[0..4], - coff.computeNodeSectionOffset(target_sym.ni), - target_endian, - ), - }, - .I386 => switch (reloc.type.I386) { - else => |kind| @panic(@tagName(kind)), - .ABSOLUTE => {}, - .DIR16 => std.mem.writeInt( - u16, - loc_slice[0..2], - @intCast(coff.optionalHeaderField(.image_base) + target_rva), - target_endian, - ), - .REL16 => std.mem.writeInt( - i16, - loc_slice[0..2], - @intCast(@as(i64, @bitCast(target_rva -% (loc_sym.rva + reloc.offset + 2)))), - target_endian, - ), - .DIR32 => std.mem.writeInt( - u32, - loc_slice[0..4], - @intCast(coff.optionalHeaderField(.image_base) + target_rva), - target_endian, - ), - .DIR32NB => std.mem.writeInt( - u32, - loc_slice[0..4], - @intCast(target_rva), - target_endian, - ), - .REL32 => std.mem.writeInt( - i32, - loc_slice[0..4], - @intCast(@as(i64, @bitCast(target_rva -% (loc_sym.rva + reloc.offset + 4)))), - target_endian, - ), - .SECREL => std.mem.writeInt( - u32, - loc_slice[0..4], - coff.computeNodeSectionOffset(target_sym.ni), - target_endian, - ), - }, + const target_machine = coff.targetLoad(&coff.headerPtr().machine); + + if (!coff.isImage()) { + assert(!reloc.flags.recover_addend); + switch (target_machine) { + else => |machine| @panic(@tagName(machine)), + .AMD64 => switch (reloc.type.AMD64) { + else => |kind| @panic(@tagName(kind)), + .ABSOLUTE => {}, + .ADDR64 => std.mem.writeInt( + u64, + loc_slice[0..8], + @intCast(reloc.addend), + target_endian, + ), + .ADDR32, + .ADDR32NB, + .SECREL, + => std.mem.writeInt( + u32, + loc_slice[0..4], + @intCast(reloc.addend), + target_endian, + ), + .REL32, + .REL32_1, + .REL32_2, + .REL32_3, + .REL32_4, + .REL32_5, + => std.mem.writeInt( + i32, + loc_slice[0..4], + @intCast(reloc.addend), + target_endian, + ), + }, + .I386 => switch (reloc.type.I386) { + else => |kind| @panic(@tagName(kind)), + .ABSOLUTE => {}, + .DIR16, + => std.mem.writeInt( + u16, + loc_slice[0..2], + @intCast(reloc.addend), + target_endian, + ), + .REL16, + => std.mem.writeInt( + i16, + loc_slice[0..2], + @intCast(reloc.addend), + target_endian, + ), + .DIR32, + .DIR32NB, + .SECREL, + => std.mem.writeInt( + u32, + loc_slice[0..4], + @intCast(reloc.addend), + target_endian, + ), + .REL32, + => std.mem.writeInt( + i32, + loc_slice[0..4], + @intCast(reloc.addend), + target_endian, + ), + }, + } + + return; + } else if (reloc.flags.recover_addend) { + reloc.flags.recover_addend = false; + reloc.addend = switch (target_machine) { + else => |machine| @panic(@tagName(machine)), + .AMD64 => switch (reloc.type.AMD64) { + else => |kind| @panic(@tagName(kind)), + .ABSOLUTE => 0, + .ADDR64 => @bitCast(std.mem.readInt( + u64, + loc_slice[0..8], + target_endian, + )), + .ADDR32, + .ADDR32NB, + .SECREL, + .REL32, + .REL32_1, + .REL32_2, + .REL32_3, + .REL32_4, + .REL32_5, + => std.mem.readInt( + i32, + loc_slice[0..4], + target_endian, + ), + }, + .I386 => switch (reloc.type.I386) { + else => |kind| @panic(@tagName(kind)), + .ABSOLUTE => 0, + .DIR16, + .REL16, + => std.mem.readInt( + i16, + loc_slice[0..2], + target_endian, + ), + .DIR32, + .DIR32NB, + .SECREL, + .REL32, + => std.mem.readInt( + i32, + loc_slice[0..4], + target_endian, + ), + }, + }; + } + + const target_sym = reloc.target.get(coff); + const is_abs = switch (target_sym.ni) { + .none => if (target_sym.section_number == .ABSOLUTE) true else return, + else => |ni| if (ni.hasMoved(&coff.mf)) return else false, + }; + + const target_rva = target_sym.rva +% @as(u64, @bitCast(reloc.addend)); + if (is_abs) { + switch (target_machine) { + else => |machine| @panic(@tagName(machine)), + .AMD64 => switch (reloc.type.AMD64) { + // TODO: Could wait to report these later, in reportUndefs -> reportRelocErrs, + // so that this function doesn't return an err + else => |kind| return coff.base.comp.link_diags.fail( + "absolute symbol '{s}' targeted by invalid relocation type: {t}", + .{ target_sym.gmi.name(coff).toSlice(coff), kind }, + ), + .ABSOLUTE => {}, + .ADDR64 => std.mem.writeInt( + u64, + loc_slice[0..8], + target_rva, + target_endian, + ), + .ADDR32 => std.mem.writeInt( + u32, + loc_slice[0..4], + @intCast(target_rva), + target_endian, + ), + }, + .I386 => switch (reloc.type.I386) { + else => |kind| return coff.base.comp.link_diags.fail( + "absolute symbol '{s}' targeted by invalid relocation type: {t}", + .{ target_sym.gmi.name(coff).toSlice(coff), kind }, + ), + .ABSOLUTE => {}, + .DIR16 => std.mem.writeInt( + u16, + loc_slice[0..2], + @intCast(target_rva), + target_endian, + ), + .DIR32 => std.mem.writeInt( + u32, + loc_slice[0..4], + @intCast(target_rva), + target_endian, + ), + }, + } + } else { + switch (target_machine) { + else => |machine| @panic(@tagName(machine)), + .AMD64 => switch (reloc.type.AMD64) { + else => |kind| @panic(@tagName(kind)), + .ABSOLUTE => {}, + .ADDR64 => std.mem.writeInt( + u64, + loc_slice[0..8], + coff.optionalHeaderField(.image_base) + target_rva, + target_endian, + ), + .ADDR32 => std.mem.writeInt( + u32, + loc_slice[0..4], + @intCast(coff.optionalHeaderField(.image_base) + target_rva), + target_endian, + ), + .ADDR32NB => std.mem.writeInt( + u32, + loc_slice[0..4], + @intCast(target_rva), + target_endian, + ), + .REL32 => std.mem.writeInt( + i32, + loc_slice[0..4], + @intCast(@as(i64, @bitCast(target_rva -% (loc_sym.rva + reloc.offset + 4)))), + target_endian, + ), + .REL32_1 => std.mem.writeInt( + i32, + loc_slice[0..4], + @intCast(@as(i64, @bitCast(target_rva -% (loc_sym.rva + reloc.offset + 5)))), + target_endian, + ), + .REL32_2 => std.mem.writeInt( + i32, + loc_slice[0..4], + @intCast(@as(i64, @bitCast(target_rva -% (loc_sym.rva + reloc.offset + 6)))), + target_endian, + ), + .REL32_3 => std.mem.writeInt( + i32, + loc_slice[0..4], + @intCast(@as(i64, @bitCast(target_rva -% (loc_sym.rva + reloc.offset + 7)))), + target_endian, + ), + .REL32_4 => std.mem.writeInt( + i32, + loc_slice[0..4], + @intCast(@as(i64, @bitCast(target_rva -% (loc_sym.rva + reloc.offset + 8)))), + target_endian, + ), + .REL32_5 => std.mem.writeInt( + i32, + loc_slice[0..4], + @intCast(@as(i64, @bitCast(target_rva -% (loc_sym.rva + reloc.offset + 9)))), + target_endian, + ), + .SECREL => std.mem.writeInt( + u32, + loc_slice[0..4], + @intCast(coff.computeSymbolSectionOffset(target_sym, .pseudo) + reloc.addend), + target_endian, + ), + }, + .I386 => switch (reloc.type.I386) { + else => |kind| @panic(@tagName(kind)), + .ABSOLUTE => {}, + .DIR16 => std.mem.writeInt( + u16, + loc_slice[0..2], + @intCast(coff.optionalHeaderField(.image_base) + target_rva), + target_endian, + ), + .REL16 => std.mem.writeInt( + i16, + loc_slice[0..2], + @intCast(@as(i64, @bitCast(target_rva -% (loc_sym.rva + reloc.offset + 2)))), + target_endian, + ), + .DIR32 => std.mem.writeInt( + u32, + loc_slice[0..4], + @intCast(coff.optionalHeaderField(.image_base) + target_rva), + target_endian, + ), + .DIR32NB => std.mem.writeInt( + u32, + loc_slice[0..4], + @intCast(target_rva), + target_endian, + ), + .REL32 => std.mem.writeInt( + i32, + loc_slice[0..4], + @intCast(@as(i64, @bitCast(target_rva -% (loc_sym.rva + reloc.offset + 4)))), + target_endian, + ), + .SECREL => std.mem.writeInt( + u32, + loc_slice[0..4], + @intCast(coff.computeSymbolSectionOffset(target_sym, .pseudo) + reloc.addend), + target_endian, + ), + }, + } } } pub fn delete(reloc: *Reloc, coff: *Coff) void { + if (reloc.sri != .none) { + // TODO: Need to remove this from the COFF relocation table (maybe removeswap?) + // TODO: If this was the last reloc causing something to be in the symbol table, we should remove + // the symbol table entry (and unset sti). That will require flushSymbolTableIndex on the + // swapped symbol if we exchange indices + @panic("TODO implement symbol table reloc deletions"); + } + switch (reloc.prev) { .none => { const target = reloc.target.get(coff); @@ -605,7 +1511,24 @@ pub const Reloc = extern struct { .none => {}, else => |next| next.get(coff).prev = reloc.prev, } + reloc.* = undefined; + reloc.flags = .{ + .recover_addend = false, + .free = true, + }; + + const ri: Reloc.Index = .wrap(@intCast(reloc - coff.relocs.items.ptr)); + if (coff.last_free_reloc == .none) { + assert(coff.first_free_reloc == .none); + coff.first_free_reloc = ri; + coff.last_free_reloc = ri; + } else { + coff.last_free_reloc.get(coff).next = ri; + reloc.prev = coff.last_free_reloc; + reloc.next = .none; + coff.last_free_reloc = ri; + } } comptime { @@ -639,14 +1562,6 @@ fn create( assert(target.ofmt == .coff); if (target.cpu.arch.endian() != comptime targetEndian(undefined)) return error.UnsupportedCOFFArchitecture; - const is_image = switch (comp.config.output_mode) { - .Exe => true, - .Lib => switch (comp.config.link_mode) { - .static => false, - .dynamic => true, - }, - .Obj => false, - }; const machine = target.toCoffMachine(); const timestamp: u32 = 0; const major_subsystem_version = options.major_subsystem_version orelse 6; @@ -689,18 +1604,61 @@ fn create( .allow_shlib_undefined = false, .stack_size = 0, }, + .options = options, .mf = try .init(file, comp.gpa, io), .nodes = .empty, + .members = .empty, + .pending_members = .empty, + .lib_string_table = .empty, + .lib_string_len = 0, + .long_names_table = .{ + .entries = .empty, + }, .import_table = .{ .ni = .none, .entries = .empty, + .iat_symbol_indices = .empty, + }, + .export_table = .{ + .ni = .none, + .export_directory_table_ni = .none, + .export_address_table_si = .null, + .name_pointer_table_ni = .none, + .ordinal_table_ni = .none, + .name_table_ni = .none, + .entries = .empty, + }, + .symbol_table = .{ + .ni = .none, + .strings_ni = .none, + .strings = .empty, + .symbols = .empty, + .pending_symbol_index = 0, + .pending_shrink = false, }, + .inputs = .empty, + .input_archives = .empty, + .input_archive_members = .empty, + .input_archive_symbols = .empty, + .input_archive_symbol_indices = .empty, + .pending_input = null, + .pending_default_libs = .empty, + .alternate_names = .empty, + .input_objects = .empty, + .input_symbols = .empty, + .input_sections = .empty, + .input_section_pending_index = 0, + .inputs_complete = false, + .exports_complete = false, + .pending_special_symbol = .entry, .strings = .empty, .string_bytes = .empty, - .image_section_table = .empty, + .section_table = .empty, .pseudo_section_table = .empty, .object_section_table = .empty, - .symbol_table = .empty, + .section_merges = .empty, + .section_merge_pending_index = 0, + .symbols = .empty, .globals = .empty, .global_pending_index = 0, .navs = .empty, @@ -711,8 +1669,13 @@ fn create( }), .pending_uavs = .empty, .relocs = .empty, + .first_free_reloc = .none, + .last_free_reloc = .none, .const_prog_node = .none, .synth_prog_node = .none, + .symbol_prog_node = .none, + .member_prog_node = .none, + .input_prog_node = .none, }; errdefer coff.deinit(); @@ -725,14 +1688,20 @@ fn create( } try coff.initHeaders( - is_image, machine, timestamp, major_subsystem_version, minor_subsystem_version, magic, + if (options.subsystem) |s| switch (s) { + .console => .WINDOWS_CUI, + .windows => .WINDOWS_GUI, + else => return error.UnsupportedCOFFSubsystem, + } else .WINDOWS_CUI, section_align, + std.fs.path.basename(path.sub_path), ); + try coff.initBuiltins(); return coff; } @@ -740,13 +1709,31 @@ pub fn deinit(coff: *Coff) void { const gpa = coff.base.comp.gpa; coff.mf.deinit(gpa); coff.nodes.deinit(gpa); + coff.pending_members.deinit(gpa); + coff.lib_string_table.deinit(gpa); + coff.long_names_table.entries.deinit(gpa); coff.import_table.entries.deinit(gpa); + coff.import_table.iat_symbol_indices.deinit(gpa); + coff.export_table.entries.deinit(gpa); + coff.symbol_table.strings.deinit(gpa); + coff.symbol_table.symbols.deinit(gpa); + coff.inputs.deinit(gpa); + coff.input_archives.deinit(gpa); + coff.input_archive_members.deinit(gpa); + coff.input_archive_symbols.deinit(gpa); + coff.input_archive_symbol_indices.deinit(gpa); + for (coff.pending_default_libs.items) |l| gpa.free(l.path); + coff.pending_default_libs.deinit(gpa); + coff.alternate_names.deinit(gpa); + coff.input_objects.deinit(gpa); + coff.input_symbols.deinit(gpa); + coff.input_sections.deinit(gpa); coff.strings.deinit(gpa); coff.string_bytes.deinit(gpa); - coff.image_section_table.deinit(gpa); + coff.section_table.deinit(gpa); coff.pseudo_section_table.deinit(gpa); coff.object_section_table.deinit(gpa); - coff.symbol_table.deinit(gpa); + coff.symbols.deinit(gpa); coff.globals.deinit(gpa); coff.navs.deinit(gpa); coff.uavs.deinit(gpa); @@ -756,21 +1743,65 @@ pub fn deinit(coff: *Coff) void { coff.* = undefined; } +fn isImage(coff: *const Coff) bool { + const comp = coff.base.comp; + return switch (comp.config.output_mode) { + .Exe => true, + .Lib => switch (comp.config.link_mode) { + .static => false, + .dynamic => true, + }, + .Obj => false, + }; +} + +fn isArchive(coff: *const Coff) bool { + const comp = coff.base.comp; + return switch (comp.config.output_mode) { + .Exe => false, + .Lib => switch (comp.config.link_mode) { + .static => true, + .dynamic => false, + }, + .Obj => false, + }; +} + +fn isExe(coff: *const Coff) bool { + return coff.base.comp.config.output_mode == .Exe; +} + +fn isObj(coff: *const Coff) bool { + return coff.base.comp.config.output_mode == .Obj; +} + +fn hasCoffHeader(coff: *const Coff) bool { + return coff.base.comp.zcu != null or !coff.isArchive(); +} + +fn sectionParent(coff: *Coff) MappedFile.Node.Index { + assert(coff.hasCoffHeader()); + return if (coff.isArchive()) Node.known.zcu_member else Node.known.file; +} + fn initHeaders( coff: *Coff, - is_image: bool, machine: std.coff.IMAGE.FILE.MACHINE, timestamp: u32, major_subsystem_version: u16, minor_subsystem_version: u16, magic: std.coff.OptionalHeader.Magic, + subsystem: std.coff.Subsystem, section_align: std.mem.Alignment, + file_name: []const u8, ) !void { const comp = coff.base.comp; const gpa = comp.gpa; const target_endian = coff.targetEndian(); 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))), @@ -780,33 +1811,120 @@ fn initHeaders( else 0; - const expected_nodes_len = Node.known_count + 6 + - @as(usize, @intFromBool(comp.config.any_non_single_threaded)) * 2; + var expected_nodes_len: usize = Node.known_count; + if (coff.hasCoffHeader()) { + // Sections + expected_nodes_len += 4; + + if (is_image) { + // Pseudo-sections and import / export table + 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) { + if (!is_image) expected_nodes_len += 1; + expected_nodes_len += 1; + } + } + defer assert(coff.nodes.len == expected_nodes_len); + try coff.nodes.ensureTotalCapacity(gpa, expected_nodes_len); coff.nodes.appendAssumeCapacity(.file); const header_ni = Node.known.header; - assert(header_ni == try coff.mf.addOnlyChildNode(gpa, .root, .{ + assert(header_ni == try coff.mf.addOnlyChildNode(gpa, Node.known.file, .{ .alignment = coff.mf.flags.block_size, .fixed = true, })); coff.nodes.appendAssumeCapacity(.header); const signature_ni = Node.known.signature; - assert(signature_ni == try coff.mf.addOnlyChildNode(gpa, header_ni, .{ - .size = (if (is_image) msdos_stub.len else 0) + "PE\x00\x00".len, + assert(signature_ni == try coff.mf.addLastChildNode(gpa, if (is_image or !is_archive) header_ni else Node.known.file, .{ + .size = if (is_image) + msdos_stub.len + std.coff.pe_signature.len + else if (is_archive) + std.coff.archive_signature.len + else + 0, .alignment = .@"4", .fixed = true, })); coff.nodes.appendAssumeCapacity(.signature); - { - const signature_slice = signature_ni.slice(&coff.mf); - if (is_image) @memcpy(signature_slice[0..msdos_stub.len], &msdos_stub); - @memcpy(signature_slice[signature_slice.len - 4 ..], "PE\x00\x00"); + + const signature_slice = signature_ni.slice(&coff.mf); + if (is_image) { + @memcpy(signature_slice[0..msdos_stub.len], &msdos_stub); + @memcpy(signature_slice[signature_slice.len - std.coff.pe_signature.len ..], std.coff.pe_signature); + } else if (is_archive) { + @memcpy(signature_slice, std.coff.archive_signature); } + const opt_coff_parent_ni = if (is_archive) parent: { + const initial_member_count = Member.Index.known_count + @intFromBool(comp.zcu != null); + try coff.members.ensureTotalCapacity(gpa, initial_member_count); + + assert(Member.Index.first == try coff.addMemberAssumeCapacity(.first_linker, @sizeOf(u32))); + coff.targetStore(coff.firstLinkerMemberNumSymbolsPtr(), 0); + + assert(Member.Index.second == try coff.addMemberAssumeCapacity(.second_linker, 2 * @sizeOf(u32))); + coff.targetStore(coff.secondLinkerMemberNumMembersPtr(), 0); + coff.targetStore(coff.secondLinkerMemberNumSymbolsPtr(), 0); + + assert(Member.Index.longnames == try coff.addMemberAssumeCapacity(.longnames, 0)); + + const first_linker_member = Member.Index.first.get(coff); + const second_linker_member = Member.Index.second.get(coff); + const longnames_member = Member.Index.longnames.get(coff); + + try first_linker_member.initHeader(coff, "", timestamp); + try second_linker_member.initHeader(coff, "", timestamp); + try longnames_member.initHeader(coff, "/", timestamp); + + if (comp.zcu) |zcu| { + const zcu_mi = try coff.addMemberAssumeCapacity(.coff, @sizeOf(std.coff.Header)); + const zcu_member = zcu_mi.get(coff); + try zcu_member.initHeader(coff, zcu.main_mod.fully_qualified_name, timestamp); + + break :parent zcu_member.content_ni; + } + + // These placeholder nodes are placed before the first member - if there are + // no other members then the last linker member (longnames) needs to expand + // to fill the padding at the end of the file. + assert(Node.known.zcu_member_header == try coff.mf.addNodeAfter(gpa, Node.known.header, .{})); + assert(Node.known.zcu_member == try coff.mf.addNodeAfter(gpa, Node.known.header, .{})); + coff.nodes.appendAssumeCapacity(.placeholder); + coff.nodes.appendAssumeCapacity(.placeholder); + + break :parent null; + } else parent: { + // TODO: Not ideal to have this many placeholder nodes - use two distinct `Node.known` types? + while (true) { + const placeholder_ni = try coff.mf.addLastChildNode(gpa, Node.known.file, .{}); + coff.nodes.appendAssumeCapacity(.placeholder); + if (placeholder_ni == Node.known.zcu_member) break; + } + + break :parent Node.known.header; + }; + + const coff_parent_ni = opt_coff_parent_ni orelse { + // If we're not generating any code, no more known nodes are used + while (coff.nodes.len < Node.known_count) { + _ = try coff.mf.addNodeAfter(gpa, Node.known.header, .{}); + coff.nodes.appendAssumeCapacity(.placeholder); + } + + return; + }; + const coff_header_ni = Node.known.coff_header; - assert(coff_header_ni == try coff.mf.addLastChildNode(gpa, header_ni, .{ + assert(coff_header_ni == try coff.mf.addLastChildNode(gpa, coff_parent_ni, .{ .size = @sizeOf(std.coff.Header), .alignment = .@"4", .fixed = true, @@ -834,7 +1952,7 @@ fn initHeaders( } const optional_header_ni = Node.known.optional_header; - assert(optional_header_ni == try coff.mf.addLastChildNode(gpa, header_ni, .{ + assert(optional_header_ni == try coff.mf.addLastChildNode(gpa, coff_parent_ni, .{ .size = optional_header_size, .alignment = .@"4", .fixed = true, @@ -876,7 +1994,7 @@ fn initHeaders( .size_of_image = 0, .size_of_headers = 0, .checksum = 0, - .subsystem = .WINDOWS_CUI, + .subsystem = subsystem, .dll_flags = .{ .HIGH_ENTROPY_VA = true, .DYNAMIC_BASE = true, @@ -925,7 +2043,7 @@ fn initHeaders( .size_of_image = 0, .size_of_headers = 0, .checksum = 0, - .subsystem = .WINDOWS_CUI, + .subsystem = subsystem, .dll_flags = .{ .HIGH_ENTROPY_VA = true, .DYNAMIC_BASE = true, @@ -946,13 +2064,13 @@ fn initHeaders( } const data_directories_ni = Node.known.data_directories; - assert(data_directories_ni == try coff.mf.addLastChildNode(gpa, header_ni, .{ + assert(data_directories_ni == try coff.mf.addLastChildNode(gpa, coff_parent_ni, .{ .size = data_directories_size, .alignment = .@"4", .fixed = true, })); coff.nodes.appendAssumeCapacity(.data_directories); - { + if (is_image) { const data_directories = coff.dataDirectorySlice(); @memset(data_directories, .{ .virtual_address = 0, .size = 0 }); if (target_endian != native_endian) std.mem.byteSwapAllFields( @@ -962,7 +2080,7 @@ fn initHeaders( } const section_table_ni = Node.known.section_table; - assert(section_table_ni == try coff.mf.addLastChildNode(gpa, header_ni, .{ + assert(section_table_ni == try coff.mf.addLastChildNode(gpa, coff_parent_ni, .{ .alignment = .@"4", .fixed = true, })); @@ -970,65 +2088,292 @@ fn initHeaders( assert(coff.nodes.len == Node.known_count); - try coff.symbol_table.ensureTotalCapacity(gpa, Symbol.Index.known_count); - coff.symbol_table.addOneAssumeCapacity().* = .{ - .ni = .none, - .rva = 0, - .size = 0, - .loc_relocs = .none, - .target_relocs = .none, - .section_number = .UNDEFINED, - }; - assert(try coff.addSection(".data", .{ + if (!is_image) { + // TODO: These two nodes could be inside one movable node? + coff.symbol_table.ni = try coff.mf.addLastChildNode(gpa, coff_parent_ni, .{ + .alignment = .@"2", + .fixed = true, + .moved = true, + }); + coff.nodes.appendAssumeCapacity(.symbol_table); + + coff.symbol_table.strings_ni = try coff.mf.addLastChildNode(gpa, coff_parent_ni, .{ + .size = @sizeOf(u32), + .fixed = true, + .resized = true, + }); + coff.nodes.appendAssumeCapacity(.string_table); + coff.targetStore(coff.symbolTableStringLenPtr(), @sizeOf(u32)); + } + + 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 .bss? + // TODO: Could have a node flag 'virtual' that can never have slice* or fileLocation called on it + // TODO: Instead of it's own section, 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, + .MEM_WRITE = true, + }) == .bss); + assert(try coff.addSection(.@".data", .{ .CNT_INITIALIZED_DATA = true, .MEM_READ = true, .MEM_WRITE = true, }) == .data); - assert(try coff.addSection(".rdata", .{ + assert(try coff.addSection(.@".rdata", .{ .CNT_INITIALIZED_DATA = true, .MEM_READ = true, }) == .rdata); - assert(try coff.addSection(".text", .{ + assert(try coff.addSection(.@".text", .{ .CNT_CODE = true, .MEM_EXECUTE = true, .MEM_READ = true, }) == .text); - coff.import_table.ni = try coff.mf.addLastChildNode( - gpa, - (try coff.objectSectionMapIndex( - .@".idata", + 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, + }); + } + + // TODO: Lazily initialize this instead, avoid the extra logic for this in flushMoved / flushResized + coff.import_table.ni = try coff.mf.addLastChildNode( + gpa, + (try coff.objectSectionMapIndex( + .@".idata", + coff.mf.flags.block_size, + .{ .read = true, .initialized = true }, + )).symbol(coff).node(coff), + .{ .alignment = .@"4" }, + ); + coff.nodes.appendAssumeCapacity(.import_directory_table); + + coff.export_table.ni = (try coff.pseudoSectionMapIndex( + .@".edata", + .of(std.coff.ExportDirectoryTable), + .{ .read = true, .initialized = true }, + )).symbol(coff).node(coff); + + coff.export_table.export_directory_table_ni = try coff.mf.addLastChildNode( + gpa, + coff.export_table.ni, + .{ + .size = @sizeOf(std.coff.ExportDirectoryTable) + file_name.len + 1, + .moved = true, + .fixed = true, + }, + ); + coff.nodes.appendAssumeCapacity(.export_directory_table); + + const name_index = @sizeOf(std.coff.ExportDirectoryTable); + const table_slice = coff.export_table.export_directory_table_ni.slice(&coff.mf); + @memcpy(table_slice[name_index..][0..file_name.len], file_name[0..file_name.len]); + @memset(table_slice[name_index + file_name.len ..], 0); + + const export_address_table_ni = try coff.mf.addLastChildNode(gpa, coff.export_table.ni, .{ + .alignment = .of(std.coff.ExportAddressTableEntry), + .moved = true, + }); + coff.nodes.appendAssumeCapacity(.export_address_table); + + try coff.symbols.ensureUnusedCapacity(gpa, 1); + coff.export_table.export_address_table_si = coff.addSymbolAssumeCapacity(); + + const export_address_table_sym = coff.export_table.export_address_table_si.get(coff); + export_address_table_sym.ni = export_address_table_ni; + assert(export_address_table_sym.loc_relocs == .none); + export_address_table_sym.loc_relocs = @enumFromInt(coff.relocs.items.len); + export_address_table_sym.section_number = + coff.getNode(coff.export_table.ni).pseudo_section.symbol(coff).get(coff).section_number; + + coff.export_table.name_pointer_table_ni = try coff.mf.addLastChildNode(gpa, coff.export_table.ni, .{ + .alignment = .of(std.coff.ExportNamePointerTableEntry), + .moved = true, + }); + coff.nodes.appendAssumeCapacity(.export_name_pointer_table); + + coff.export_table.ordinal_table_ni = try coff.mf.addLastChildNode(gpa, coff.export_table.ni, .{ + .alignment = .of(std.coff.ExportOrdinalTableEntry), + .moved = true, + }); + coff.nodes.appendAssumeCapacity(.export_ordinal_table); + + coff.export_table.name_table_ni = try coff.mf.addLastChildNode(gpa, coff.export_table.ni, .{ + .alignment = .of(u8), + .moved = true, + }); + coff.nodes.appendAssumeCapacity(.export_name_table); + + const export_directory_table = coff.exportDirectoryTable(); + export_directory_table.* = .{ + .flags = 0, + .time_date_stamp = timestamp, + .major_version = 0, + .minor_version = 0, + .name_rva = 0, + .ordinal_base = 1, + .number_of_entries = 0, + .number_of_names = 0, + .export_address_table_rva = 0, + .name_pointer_table_rva = 0, + .ordinal_table_rva = 0, + }; + if (target_endian != native_endian) + std.mem.byteSwapAllFields(std.coff.ExportDirectoryTable, export_directory_table); + } + + if (comp.config.any_non_single_threaded) { + if (!is_image) + _ = try coff.addSection(.@".tls$", .{ + .CNT_INITIALIZED_DATA = true, + .MEM_READ = true, + .MEM_WRITE = true, + }); + + // While tls variables allocated at runtime are writable, the template itself is not. + // In images, the template is in a .tls pseudo section in .rdata. + // In objects / archives, this section is part of the above .tls$ section. The suffix + // is maintained so merging can occur with other input tls symbols when linked later. + _ = try coff.pseudoSectionMapIndex( + if (is_image) .@".tls" else .@".tls$", coff.mf.flags.block_size, - .{ .read = true }, - )).symbol(coff).node(coff), - .{ .alignment = .@"4", .moved = true }, - ); - coff.nodes.appendAssumeCapacity(.import_directory_table); + .{ .read = true, .write = !is_image, .initialized = true }, + ); + } +} - // While tls variables allocated at runtime are writable, the template itself is not - if (comp.config.any_non_single_threaded) _ = try coff.objectSectionMapIndex( - .@".tls$", - coff.mf.flags.block_size, - .{ .read = true }, - ); +pub fn initBuiltins(coff: *Coff) !void { + const comp = coff.base.comp; + const gpa = comp.gpa; + const target = &comp.root_mod.resolved_target.result; + if (coff.isImage()) { + const si = try coff.globalSymbol(.{ .name = "__ImageBase", .type = .data }); + const sym = si.get(coff); + sym.ni = Node.known.header; + } + + defer coff.flushSectionMerges() catch unreachable; + if (coff.isImage() and target.isMinGW() and comp.config.link_libc) { + try coff.symbols.ensureUnusedCapacity(gpa, 8); + try coff.globals.ensureUnusedCapacity(gpa, 2); + try coff.nodes.ensureUnusedCapacity(gpa, 8); + try coff.section_merges.ensureUnusedCapacity(gpa, 2); + + const lists: []const struct { global: []const u8, start: String, end: String } = &.{ + .{ .global = "__CTOR_LIST__", .start = .@".ctors", .end = .@".ctors$ZZZ" }, + .{ .global = "__DTOR_LIST__", .start = .@".dtors", .end = .@".dtors$ZZZ" }, + }; + + // We need to explicitly merge these into .rdata as in objects they can be marked + // as MEM_WRITE, and would have mismatced section flags. + try coff.section_merges.put(gpa, .@".ctors", .@".rdata"); + try coff.section_merges.put(gpa, .@".dtors", .@".rdata"); + + for (lists) |list| { + const addr_info = coff.targetAddrInfo(); + + // Any .(c|d)tor$(.*) input sections will merge in between these sections + const start_osmi = try coff.objectSectionMapIndex( + list.start, + addr_info.alignment, + .{ .read = true, .initialized = true }, + ); + const end_osmi = try coff.objectSectionMapIndex( + list.end, + addr_info.alignment, + .{ .read = true, .initialized = true }, + ); + + // Additional nodes are used here, instead of just adding the sentinel + // directly to the section data, since once input sections are added + // as children, they would overwrite that data. + const start_sym = start_osmi.symbol(coff).get(coff); + const list_len_si = try coff.globalSymbol(.{ .name = list.global, .type = .data }); + const list_len_sym = list_len_si.get(coff); + list_len_sym.setExtra(.{ .size = addr_info.size }); + list_len_sym.ni = try coff.mf.addFirstChildNode(gpa, start_sym.ni, .{ + .size = addr_info.size, + .fixed = true, + }); + coff.nodes.appendAssumeCapacity(.{ .builtin = list_len_si }); + list_len_sym.section_number = start_sym.section_number; + + const start_slice = list_len_sym.ni.slice(&coff.mf); + switch (addr_info.magic) { + _ => unreachable, + inline .PE32, .@"PE32+" => |t| { + const addr: *TargetAddr(t) = @ptrCast(@alignCast(start_slice)); + // For __CTOR_LIST__ -1 indicates that the list is null terminated. + // For __DTOR_LIST__, this value is ignored, the list is always null terminated + coff.targetStore(addr, std.math.maxInt(TargetAddr(t))); + }, + } + + const end_sym = end_osmi.symbol(coff).get(coff); + const list_end_si = coff.addSymbolAssumeCapacity(); + const list_end_sym = list_end_si.get(coff); + list_end_sym.setExtra(.{ .size = addr_info.size }); + list_end_sym.ni = try coff.mf.addFirstChildNode(gpa, end_sym.ni, .{ + .size = addr_info.size, + .fixed = true, + }); + coff.nodes.appendAssumeCapacity(.{ .builtin = list_end_si }); + list_end_sym.section_number = start_sym.section_number; - assert(coff.nodes.len == expected_nodes_len); + @memset(list_end_sym.ni.slice(&coff.mf), 0); + + try list_len_si.flushMoved(coff); + try list_end_si.flushMoved(coff); + } + } } pub fn startProgress(coff: *Coff, prog_node: std.Progress.Node) void { prog_node.increaseEstimatedTotalItems(3); coff.const_prog_node = prog_node.start("Constants", coff.pending_uavs.count()); coff.synth_prog_node = prog_node.start("Synthetics", count: { - var count = coff.globals.count() - coff.global_pending_index; + var count = + coff.globals.count() - coff.global_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; }); + if (!isImage(coff)) { + prog_node.increaseEstimatedTotalItems(2); + coff.symbol_prog_node = prog_node.start( + "Symbols", + coff.symbol_table.symbols.count() - coff.symbol_table.pending_symbol_index, + ); + coff.member_prog_node = prog_node.start("Members", coff.pending_members.count()); + } + coff.input_prog_node = prog_node.start( + "Inputs", + coff.input_sections.items.len - coff.input_section_pending_index, + ); coff.mf.update_prog_node = prog_node.start("Relocations", coff.mf.updates.items.len); } pub fn endProgress(coff: *Coff) void { coff.mf.update_prog_node.end(); coff.mf.update_prog_node = .none; + coff.input_prog_node.end(); + coff.input_prog_node = .none; + if (!coff.isImage()) { + coff.member_prog_node.end(); + coff.member_prog_node = .none; + coff.symbol_prog_node.end(); + coff.symbol_prog_node = .none; + } coff.synth_prog_node.end(); coff.synth_prog_node = .none; coff.const_prog_node.end(); @@ -1044,10 +2389,20 @@ fn computeNodeRva(coff: *Coff, ni: MappedFile.Node.Index) u32 { .file, .header, .signature, + .archive_member_header, + .archive_member, .coff_header, .optional_header, .data_directories, .section_table, + .export_name_table, + .placeholder, + .symbol_table, + .string_table, + .relocation_table, + .relocation_table_entry, + .input_section, + .builtin, => unreachable, .image_section => |si| si, .import_directory_table => break :parent_rva coff.targetLoad( @@ -1062,9 +2417,21 @@ fn computeNodeRva(coff: *Coff, ni: MappedFile.Node.Index) u32 { .import_hint_name_table => |import_index| break :parent_rva coff.targetLoad( &coff.importDirectoryEntryPtr(import_index).name_rva, ), + .export_directory_table => break :parent_rva coff.targetLoad( + &coff.dataDirectoryPtr(.EXPORT).virtual_address, + ), + .export_address_table => break :parent_rva coff.targetLoad( + &coff.exportDirectoryTable().export_address_table_rva, + ), + .export_name_pointer_table => break :parent_rva coff.targetLoad( + &coff.exportDirectoryTable().name_pointer_table_rva, + ), + .export_ordinal_table => break :parent_rva coff.targetLoad( + &coff.exportDirectoryTable().ordinal_table_rva, + ), inline .pseudo_section, .object_section, - .global, + .import_thunk, .nav, .uav, .lazy_code, @@ -1076,24 +2443,55 @@ fn computeNodeRva(coff: *Coff, ni: MappedFile.Node.Index) u32 { const offset, _ = ni.location(&coff.mf).resolve(&coff.mf); return @intCast(parent_rva + offset); } -fn computeNodeSectionOffset(coff: *Coff, ni: MappedFile.Node.Index) u32 { - var section_offset: u32 = 0; - var parent_ni = ni; + +fn computeSymbolSectionOffset( + coff: *Coff, + sym: *const Symbol, + relative_to: enum { image, pseudo }, +) u32 { + var section_offset: u32 = sym.nodeOffset(coff); + var parent_ni = sym.ni; while (true) { const offset, _ = parent_ni.location(&coff.mf).resolve(&coff.mf); section_offset += @intCast(offset); parent_ni = parent_ni.parent(&coff.mf); switch (coff.getNode(parent_ni)) { else => unreachable, - .image_section, .pseudo_section => return section_offset, - .object_section => {}, + .image_section => break, + .pseudo_section => if (relative_to == .pseudo) break, + .object_section, + => {}, } } + + return section_offset; } pub inline fn targetEndian(_: *const Coff) std.lang.Endian { return .little; } + +fn targetAddrInfo(coff: *Coff) struct { + size: u8, + alignment: std.mem.Alignment, + magic: std.coff.OptionalHeader.Magic, +} { + const magic = coff.targetLoad(&coff.optionalHeaderStandardPtr().magic); + switch (magic) { + _ => unreachable, + .PE32 => return .{ .size = 4, .alignment = .@"4", .magic = magic }, + .@"PE32+" => return .{ .size = 8, .alignment = .@"8", .magic = magic }, + } +} + +fn TargetAddr(comptime magic: std.coff.OptionalHeader.Magic) type { + return switch (magic) { + _ => comptime unreachable, + .PE32 => u32, + .@"PE32+" => u64, + }; +} + fn targetLoad(coff: *const Coff, ptr: anytype) @typeInfo(@TypeOf(ptr)).pointer.child { const Child = @typeInfo(@TypeOf(ptr)).pointer.child; return switch (@typeInfo(Child)) { @@ -1122,9 +2520,55 @@ fn targetStore(coff: *const Coff, ptr: anytype, val: @typeInfo(@TypeOf(ptr)).poi } pub fn headerPtr(coff: *Coff) *std.coff.Header { + assert(coff.hasCoffHeader()); return @ptrCast(@alignCast(Node.known.coff_header.slice(&coff.mf))); } +pub fn firstLinkerMemberNumSymbolsPtr(coff: *Coff) *u32 { + assert(coff.isArchive()); + return @ptrCast(@alignCast(Node.known.first_linker_member.slice(&coff.mf))); +} + +pub fn firstLinkerMemberOffsetsSlice(coff: *Coff) []u32 { + const len = std.mem.toNative(u32, coff.firstLinkerMemberNumSymbolsPtr().*, .big); + return @ptrCast(@alignCast(Node.known.first_linker_member.slice(&coff.mf)[@sizeOf(u32)..][0 .. len * @sizeOf(u32)])); +} + +pub fn secondLinkerMemberNumMembersPtr(coff: *Coff) *align(2) u32 { + assert(coff.isArchive()); + return @ptrCast(@alignCast(Node.known.second_linker_member.slice(&coff.mf))); +} + +pub fn secondLinkerMemberOffsetsSlice(coff: *Coff) []align(2) u32 { + const num_members = coff.targetLoad(coff.secondLinkerMemberNumMembersPtr()); + return @ptrCast(@alignCast( + Node.known.second_linker_member.slice(&coff.mf)[@sizeOf(u32)..][0 .. num_members * @sizeOf(u32)], + )); +} + +pub fn secondLinkerMemberNumSymbolsPtr(coff: *Coff) *align(2) u32 { + const num_members = coff.targetLoad(coff.secondLinkerMemberNumMembersPtr()); + return @ptrCast(@alignCast( + Node.known.second_linker_member.slice(&coff.mf)[(1 + num_members) * @sizeOf(u32) ..], + )); +} + +pub fn secondLinkerMemberIndicesSlice(coff: *Coff) []u16 { + const num_members = coff.targetLoad(coff.secondLinkerMemberNumMembersPtr()); + const num_symbols = coff.targetLoad(coff.secondLinkerMemberNumSymbolsPtr()); + return @ptrCast(@alignCast( + Node.known.second_linker_member.slice(&coff.mf)[(2 + num_members) * @sizeOf(u32) ..][0 .. num_symbols * @sizeOf(u16)], + )); +} + +pub fn secondLinkerMemberStringsSlice(coff: *Coff) []u8 { + const num_members = coff.targetLoad(coff.secondLinkerMemberNumMembersPtr()); + const num_symbols = coff.targetLoad(coff.secondLinkerMemberNumSymbolsPtr()); + return @ptrCast(@alignCast( + Node.known.second_linker_member.slice(&coff.mf)[(2 + num_members) * @sizeOf(u32) + num_symbols * @sizeOf(u16) ..], + )); +} + pub fn optionalHeaderStandardPtr(coff: *Coff) *std.coff.OptionalHeader { return @ptrCast(@alignCast( Node.known.optional_header.slice(&coff.mf)[0..@sizeOf(std.coff.OptionalHeader)], @@ -1136,6 +2580,7 @@ pub const OptionalHeaderPtr = union(std.coff.OptionalHeader.Magic) { @"PE32+": *std.coff.OptionalHeader.@"PE32+", }; pub fn optionalHeaderPtr(coff: *Coff) OptionalHeaderPtr { + assert(coff.isImage()); const slice = Node.known.optional_header.slice(&coff.mf); return switch (coff.targetLoad(&coff.optionalHeaderStandardPtr().magic)) { _ => unreachable, @@ -1150,6 +2595,7 @@ pub fn optionalHeaderField( coff: *Coff, comptime field: std.meta.FieldEnum(std.coff.OptionalHeader.@"PE32+"), ) @FieldType(std.coff.OptionalHeader.@"PE32+", @tagName(field)) { + assert(coff.isImage()); return switch (coff.optionalHeaderPtr()) { inline else => |optional_header| coff.targetLoad(&@field(optional_header, @tagName(field))), }; @@ -1158,6 +2604,7 @@ pub fn optionalHeaderField( pub fn dataDirectorySlice( coff: *Coff, ) *[std.coff.IMAGE.DIRECTORY_ENTRY.len]std.coff.ImageDataDirectory { + assert(coff.isImage()); return @ptrCast(@alignCast(Node.known.data_directories.slice(&coff.mf))); } pub fn dataDirectoryPtr( @@ -1168,10 +2615,48 @@ pub fn dataDirectoryPtr( } pub fn sectionTableSlice(coff: *Coff) []std.coff.SectionHeader { - return @ptrCast(@alignCast(Node.known.section_table.slice(&coff.mf))); + return @ptrCast(@alignCast( + Node.known.section_table.slice(&coff.mf)[0 .. coff.section_table.count() * @sizeOf(std.coff.SectionHeader)], + )); +} + +pub fn symbolTableEntryStoragePtr(coff: *Coff, index: u32) *[std.coff.Symbol.sizeOf()]u8 { + assert(!coff.isImage()); + const offset = index * std.coff.Symbol.sizeOf(); + return @ptrCast(@alignCast(coff.symbol_table.ni.slice(&coff.mf)[offset..][0..std.coff.Symbol.sizeOf()])); +} + +pub fn symbolTableEntryPtr(coff: *Coff, sti: SymbolTable.Index) ?*align(2) std.coff.Symbol { + if (sti.unwrap()) |index| + return @ptrCast(@alignCast(symbolTableEntryStoragePtr(coff, index))) + else + return null; +} + +pub fn symbolTableSectionAuxEntryPtr(coff: *Coff, sti: SymbolTable.Index) ?*align(2) std.coff.SectionDefinition { + if (symbolTableEntryPtr(coff, sti)) |entry| { + assert(entry.storage_class == .STATIC and entry.number_of_aux_symbols == 1); + return @ptrCast(@alignCast(symbolTableEntryStoragePtr(coff, sti.unwrap().? + 1))); + } else { + return null; + } +} + +pub fn symbolTableWeakExternalAuxEntryPtr(coff: *Coff, sti: SymbolTable.Index) ?*align(2) std.coff.WeakExternalDefinition { + if (symbolTableEntryPtr(coff, sti)) |entry| { + assert(entry.storage_class == .WEAK_EXTERNAL and entry.number_of_aux_symbols == 1); + return @ptrCast(@alignCast(symbolTableEntryStoragePtr(coff, sti.unwrap().? + 1))); + } else { + return null; + } +} + +pub fn symbolTableStringLenPtr(coff: *Coff) *align(1) u32 { + return @ptrCast(@alignCast(coff.symbol_table.strings_ni.slice(&coff.mf)[0..@sizeOf(u32)])); } pub fn importDirectoryTableSlice(coff: *Coff) []std.coff.ImportDirectoryEntry { + assert(coff.isImage()); return @ptrCast(@alignCast(coff.import_table.ni.slice(&coff.mf))); } pub fn importDirectoryEntryPtr( @@ -1181,16 +2666,40 @@ pub fn importDirectoryEntryPtr( return &coff.importDirectoryTableSlice()[@intFromEnum(import_index)]; } +pub fn exportDirectoryTable(coff: *Coff) *std.coff.ExportDirectoryTable { + return @ptrCast(@alignCast(coff.export_table.export_directory_table_ni.slice(&coff.mf))); +} + +pub fn exportNamePointerTableSlice(coff: *Coff) []std.coff.ExportNamePointerTableEntry { + const debug = coff.export_table.name_pointer_table_ni.slice(&coff.mf); + _ = debug; + + return @ptrCast(@alignCast(coff.export_table.name_pointer_table_ni.slice(&coff.mf))); +} + +pub fn exportOrdinalTableSlice(coff: *Coff) []std.coff.ExportOrdinalTableEntry { + return @ptrCast(@alignCast(coff.export_table.ordinal_table_ni.slice(&coff.mf))); +} + fn addSymbolAssumeCapacity(coff: *Coff) Symbol.Index { - defer coff.symbol_table.addOneAssumeCapacity().* = .{ + defer coff.symbols.addOneAssumeCapacity().* = .{ .ni = .none, .rva = 0, - .size = 0, + .value = .{ .none = {} }, + .extra = .{ .size = 0 }, + .flags = .{ + .value_tag = .none, + .extra_tag = .size, + .type = .unknown, + .dll_storage_class = .default, + .weak_external_strat = .none, + }, .loc_relocs = .none, .target_relocs = .none, .section_number = .UNDEFINED, + .gmi = .none, }; - return @enumFromInt(coff.symbol_table.items.len); + return @enumFromInt(coff.symbols.items.len); } fn initSymbolAssumeCapacity(coff: *Coff) !Symbol.Index { @@ -1205,12 +2714,55 @@ fn getOrPutString(coff: *Coff, string: []const u8) !String { fn getOrPutOptionalString(coff: *Coff, string: ?[]const u8) !String.Optional { return (try coff.getOrPutString(string orelse return .none)).toOptional(); } +fn getString(coff: *Coff, string: []const u8) String.Optional { + if (coff.strings.getKeyAdapted( + string, + std.hash_map.StringIndexAdapter{ .bytes = &coff.string_bytes }, + )) |key| + return @as(String, @enumFromInt(key)).toOptional() + else + return .none; +} + +/// If the name does not fit in the symbol header, adds it to the symbol table string table. +/// If the caller knows this name already has a String associated with it, they can avoid +/// a redundant call to `getOrPutString` by specifying `opt_string`. +/// The lifetime of the return value matches that of `name`. +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 > 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) { + const string_index = coff.symbol_table.strings_ni.location(&coff.mf).resolve(&coff.mf)[1]; + string_gop.value_ptr.* = @enumFromInt(string_index); + + try coff.symbol_table.strings_ni.resize(&coff.mf, gpa, string_index + name.len + 1); + const slice = coff.symbol_table.strings_ni.slice(&coff.mf); + @memcpy(slice[@intCast(string_index)..][0..name.len], name); + slice[@intCast(string_index + name.len)] = 0; + } + + break :name .{ .long = string_gop.value_ptr.* }; + } else .{ .short = name }; +} +/// `len` does not include null terminators fn ensureUnusedStringCapacity(coff: *Coff, len: usize) !void { const gpa = coff.base.comp.gpa; try coff.strings.ensureUnusedCapacityContext(gpa, 1, .{ .bytes = &coff.string_bytes }); try coff.string_bytes.ensureUnusedCapacity(gpa, len + 1); } + +/// `total_len` includes null terminators +fn ensureManyUnusedStringCapacity(coff: *Coff, num_strings: u32, total_len: usize) !void { + const gpa = coff.base.comp.gpa; + try coff.strings.ensureUnusedCapacityContext(gpa, num_strings, .{ .bytes = &coff.string_bytes }); + try coff.string_bytes.ensureUnusedCapacity(gpa, total_len + num_strings); +} + fn getOrPutStringAssumeCapacity(coff: *Coff, string: []const u8) String { const gop = coff.strings.getOrPutAssumeCapacityAdapted( string, @@ -1225,18 +2777,78 @@ fn getOrPutStringAssumeCapacity(coff: *Coff, string: []const u8) String { return @enumFromInt(gop.key_ptr.*); } -pub fn globalSymbol(coff: *Coff, name: []const u8, lib_name: ?[]const u8) !Symbol.Index { - const gpa = coff.base.comp.gpa; - try coff.symbol_table.ensureUnusedCapacity(gpa, 1); - const sym_gop = try coff.globals.getOrPut(gpa, .{ - .name = try coff.getOrPutString(name), - .lib_name = try coff.getOrPutOptionalString(lib_name), - }); +const GlobalOptions = struct { + name: []const u8, + lib_name: ?[]const u8 = null, + type: Symbol.Type = .unknown, + dll_storage_class: Symbol.DllStorageClass = .default, +}; + +fn getOrPutGlobalSymbol( + coff: *Coff, + opts: GlobalOptions, +) !std.array_hash_map.Auto(String, Global).GetOrPutResult { + const comp = coff.base.comp; + const gpa = comp.gpa; + try coff.symbols.ensureUnusedCapacity(gpa, 1); + + const lib_name: String.Optional = if (opts.lib_name) |lib_name| lib_name: { + const is_libc = std.zig.target.isLibCLibName(&comp.root_mod.resolved_target.result, lib_name); + if (is_libc) { + // This is guaranteed by Sema.handleExternLibName + if (!comp.config.link_libc) unreachable; + + // TODO: The user has requested this symbol come from libc, but this logic allows + // it to come from anywhere. We need to know what inputs are libc inputs, + // and set a flag to only search them for this symbol. + break :lib_name .none; + } + + break :lib_name (try coff.getOrPutString(lib_name)).toOptional(); + } else .none; + + const sym_gop = try coff.globals.getOrPut(gpa, try coff.getOrPutString(opts.name)); if (!sym_gop.found_existing) { - sym_gop.value_ptr.* = coff.addSymbolAssumeCapacity(); + const si = coff.addSymbolAssumeCapacity(); + const sym = si.get(coff); + sym.gmi = .wrap(@intCast(sym_gop.index)); + sym.flags.type = opts.type; + sym.flags.dll_storage_class = opts.dll_storage_class; + sym_gop.value_ptr.* = .{ + .si = si, + .lib_name = lib_name, + }; coff.synth_prog_node.increaseEstimatedTotalItems(1); + + log.debug("globalSymbol({s}, {?s}) = {d}", .{ opts.name, opts.lib_name, si }); + } + + return sym_gop; +} + +fn getDefinedGlobal(coff: *Coff, name: []const u8) Symbol.Index { + if (coff.globals.get( + coff.getString(name).unwrap() orelse return .null, + )) |global| if (global.si.get(coff).ni != .none) return global.si; + return .null; +} + +pub fn globalSymbol(coff: *Coff, opts: GlobalOptions) !Symbol.Index { + const gop = try coff.getOrPutGlobalSymbol(opts); + return gop.value_ptr.si; +} + +pub fn pendingSymbolTableEntry(coff: *Coff, si: Symbol.Index) !void { + assert(!coff.isImage()); + const sym = si.get(coff); + + assert(sym.ni != .none or sym.gmi != .none); + const gpa = coff.base.comp.gpa; + const gop = try coff.symbol_table.symbols.getOrPut(gpa, si); + if (!gop.found_existing) { + coff.symbol_prog_node.increaseEstimatedTotalItems(1); + gop.value_ptr.* = .none; } - return sym_gop.value_ptr.*; } fn navSection( @@ -1247,13 +2859,13 @@ fn navSection( const ip = &zcu.intern_pool; const default: String, const attributes: ObjectSectionAttributes = if (nav_resolved.@"threadlocal" and coff.base.comp.config.any_non_single_threaded) .{ - .@".tls$", .{ .read = true, .write = true }, + .@".tls$", .{ .read = true, .write = true, .initialized = true }, } else if (ip.isFunctionType(nav_resolved.type)) .{ .@".text", .{ .read = true, .execute = true }, } else if (nav_resolved.@"const") .{ - .@".rdata", .{ .read = true }, + .@".rdata", .{ .read = true, .initialized = true }, } else .{ - .@".data", .{ .read = true, .write = true }, + .@".data", .{ .read = true, .write = true, .initialized = true }, }; return (try coff.objectSectionMapIndex( @@ -1270,7 +2882,7 @@ fn navSection( } fn navMapIndex(coff: *Coff, zcu: *Zcu, nav_index: InternPool.Nav.Index) !Node.NavMapIndex { const gpa = zcu.gpa; - try coff.symbol_table.ensureUnusedCapacity(gpa, 1); + try coff.symbols.ensureUnusedCapacity(gpa, 1); const sym_gop = try coff.navs.getOrPut(gpa, nav_index); if (!sym_gop.found_existing) sym_gop.value_ptr.* = coff.addSymbolAssumeCapacity(); return @enumFromInt(sym_gop.index); @@ -1278,17 +2890,20 @@ fn navMapIndex(coff: *Coff, zcu: *Zcu, nav_index: InternPool.Nav.Index) !Node.Na pub fn navSymbol(coff: *Coff, zcu: *Zcu, nav_index: InternPool.Nav.Index) !Symbol.Index { const ip = &zcu.intern_pool; const nav = ip.getNav(nav_index); - if (nav.getExtern(ip)) |@"extern"| return coff.globalSymbol( - @"extern".name.toSlice(ip), - @"extern".lib_name.toSlice(ip), - ); + if (nav.getExtern(ip)) |@"extern"| return coff.globalSymbol(.{ + .name = @"extern".name.toSlice(ip), + .lib_name = @"extern".lib_name.toSlice(ip), + // TODO: Threadlocal as well? + .type = if (ip.isFunctionType(nav.resolved.?.type)) .code else .data, + .dll_storage_class = if (@"extern".is_dll_import) .dllimport else .default, + }); const nmi = try coff.navMapIndex(zcu, nav_index); return nmi.symbol(coff); } fn uavMapIndex(coff: *Coff, uav_val: InternPool.Index) !Node.UavMapIndex { const gpa = coff.base.comp.gpa; - try coff.symbol_table.ensureUnusedCapacity(gpa, 1); + try coff.symbols.ensureUnusedCapacity(gpa, 1); const sym_gop = try coff.uavs.getOrPut(gpa, uav_val); if (!sym_gop.found_existing) sym_gop.value_ptr.* = coff.addSymbolAssumeCapacity(); return @enumFromInt(sym_gop.index); @@ -1300,7 +2915,7 @@ pub fn uavSymbol(coff: *Coff, uav_val: InternPool.Index) !Symbol.Index { pub fn lazySymbol(coff: *Coff, lazy: link.File.LazySymbol) !Symbol.Index { const gpa = coff.base.comp.gpa; - try coff.symbol_table.ensureUnusedCapacity(gpa, 1); + try coff.symbols.ensureUnusedCapacity(gpa, 1); const sym_gop = try coff.lazy.getPtr(lazy.kind).map.getOrPut(gpa, lazy.ty); if (!sym_gop.found_existing) { sym_gop.value_ptr.* = try coff.initSymbolAssumeCapacity(); @@ -1314,7 +2929,7 @@ pub fn getNavVAddr( pt: Zcu.PerThread, nav: InternPool.Nav.Index, reloc_info: link.File.RelocInfo, -) !u64 { +) link.Error!u64 { return coff.getVAddr(reloc_info, try coff.navSymbol(pt.zcu, nav)); } @@ -1322,30 +2937,426 @@ pub fn getUavVAddr( coff: *Coff, uav: InternPool.Index, reloc_info: link.File.RelocInfo, -) !u64 { +) link.Error!u64 { return coff.getVAddr(reloc_info, try coff.uavSymbol(uav)); } -pub fn getVAddr(coff: *Coff, reloc_info: link.File.RelocInfo, target_si: Symbol.Index) !u64 { +pub fn getVAddr(coff: *Coff, reloc_info: link.File.RelocInfo, target_si: Symbol.Index) link.Error!u64 { try coff.addReloc( @enumFromInt(@intFromEnum(reloc_info.parent.atom_index)), reloc_info.offset, target_si, - reloc_info.addend, + .{ .known = reloc_info.addend }, switch (coff.targetLoad(&coff.headerPtr().machine)) { else => unreachable, .AMD64 => .{ .AMD64 = .ADDR64 }, .I386 => .{ .I386 = .DIR32 }, }, ); - return coff.optionalHeaderField(.image_base) + target_si.get(coff).rva; + + var vaddr: u64 = target_si.get(coff).rva; + if (coff.isImage()) vaddr += coff.optionalHeaderField(.image_base); + return vaddr; +} + +/// Caller guarantees there is capacity for one member and two nodes +fn addMemberAssumeCapacity(coff: *Coff, kind: std.coff.ArchiveMemberHeader.Kind, size: u64) !Member.Index { + const comp = coff.base.comp; + const gpa = comp.gpa; + + // TODO: These two nodes could to be inside a movable node if kind == .coff|.import + const header_ni = try coff.mf.addLastChildNode(gpa, Node.known.file, .{ + .size = @sizeOf(std.coff.ArchiveMemberHeader), + .alignment = .@"2", + .fixed = true, + .moved = true, + }); + + const content_ni = try coff.mf.addLastChildNode(gpa, Node.known.file, .{ + // The actual alignment required by the spec is 2, but to allow aligned access to + // the various COFF data structures in-place during linking we overalign + .alignment = switch (kind) { + .coff => .@"4", + else => .@"2", + }, + .size = size, + .resized = size > 0, + .fixed = true, + }); + + const mi: Member.Index = @enumFromInt(coff.members.items.len); + coff.members.appendAssumeCapacity(.{ + .kind = kind, + .header_ni = header_ni, + .content_ni = content_ni, + .first_linker_indices = .empty, + }); + + coff.nodes.appendAssumeCapacity(.{ .archive_member_header = mi }); + coff.nodes.appendAssumeCapacity(.{ .archive_member = mi }); + + switch (kind) { + .first_linker, .second_linker, .longnames => {}, + else => { + const new_num_members = coff.members.items.len - Member.Index.known_count; + coff.targetStore( + coff.secondLinkerMemberNumMembersPtr(), + @intCast(new_num_members), + ); + + const old_size = Node.known.second_linker_member.location(&coff.mf).resolve(&coff.mf)[1]; + const old_header_size = new_num_members * @sizeOf(u32); + const trailing_size: usize = @intCast(old_size - old_header_size); + try Node.known.second_linker_member.resize(&coff.mf, gpa, old_size + @sizeOf(u32)); + + const slice = Node.known.second_linker_member.slice(&coff.mf); + @memmove( + slice[old_header_size + @sizeOf(u32) ..][0..trailing_size], + slice[old_header_size..][0..trailing_size], + ); + + // Offset will be written by flushMoved on header_ni + }, + } + + switch (kind) { + .first_linker, + .longnames, + .import, + => {}, + .second_linker, + .coff, + => { + try coff.pending_members.ensureTotalCapacity( + gpa, + coff.pending_members.capacity() + 1, + ); + coff.member_prog_node.increaseEstimatedTotalItems(1); + }, + } + + return mi; +} + +fn appendMemberSymbolString( + coff: *Coff, + strings_ni: MappedFile.Node.Index, + new_size: u64, + name: []const u8, + offset: u64, +) !void { + try strings_ni.resize(&coff.mf, coff.base.comp.gpa, new_size); + const name_slice = strings_ni.slice(&coff.mf)[offset..][0 .. name.len + 1]; + @memcpy(name_slice[0..name.len], name); + name_slice[name.len] = 0; +} + +fn ensureMemberSymbol(coff: *Coff, mi: Member.Index, name: String) !void { + const gpa = coff.base.comp.gpa; + const member = mi.get(coff); + assert(member.kind == .coff); + + const gop = try member.first_linker_indices.getOrPut(gpa, .{ .mi = mi, .name = name }); + if (gop.found_existing) return; + + const mfli: Member.FirstLinkerIndex = blk: { + const num_symbols_ptr = coff.firstLinkerMemberNumSymbolsPtr(); + const num_symbols = std.mem.toNative(u32, num_symbols_ptr.*, .big); + num_symbols_ptr.* = std.mem.nativeTo(u32, num_symbols + 1, .big); + break :blk @enumFromInt(num_symbols); + }; + + gop.value_ptr.* = mfli; + + // Linker member fields are not modeled as nodes because MappedFile + // can't guarantee that they will be tightly packed after resizing + + const name_slice = name.toSlice(coff); + const new_string_table_size: u32 = @intCast(coff.lib_string_len + name_slice.len + 1); + defer coff.lib_string_len = new_string_table_size; + + { + const old_header_size: usize = @intCast(@sizeOf(u32) + @intFromEnum(mfli) * @sizeOf(u32)); + const new_header_size: usize = @intCast(old_header_size + @sizeOf(u32)); + try Node.known.first_linker_member.resize(&coff.mf, gpa, new_header_size + new_string_table_size); + + const slice = Node.known.first_linker_member.slice(&coff.mf); + @memmove(slice[new_header_size..][0..coff.lib_string_len], slice[old_header_size..][0..coff.lib_string_len]); + @memcpy(slice[new_header_size + coff.lib_string_len ..][0..name_slice.len], name_slice[0..name_slice.len]); + slice[new_header_size + coff.lib_string_len + name_slice.len] = 0; + + // New offset entry is written in flushMember + } + + { + const num_members = coff.targetLoad(coff.secondLinkerMemberNumMembersPtr()); + const old_header_size = 2 * @sizeOf(u32) + num_members * @sizeOf(u32) + @intFromEnum(mfli) * @sizeOf(u16); + const new_header_size = old_header_size + @sizeOf(u16); + try Node.known.second_linker_member.resize(&coff.mf, gpa, new_header_size + new_string_table_size); + + const old_needs_sort = coff.pending_members.get(Member.Index.second) != null; + const needs_sort = old_needs_sort or (if (coff.lib_string_table.items.len > 0) + std.mem.lessThan( + u8, + name_slice, + coff.lib_string_table.items[coff.lib_string_table.items.len - 1].toSlice(coff), + ) + else + false); + + try coff.lib_string_table.append(gpa, name); + + const slice = Node.known.second_linker_member.slice(&coff.mf); + coff.targetStore(coff.secondLinkerMemberNumSymbolsPtr(), @intFromEnum(mfli) + 1); + if (!needs_sort) { + @memmove(slice[new_header_size..][0..coff.lib_string_len], slice[old_header_size..][0..coff.lib_string_len]); + @memcpy(slice[new_header_size + coff.lib_string_len ..][0..name_slice.len], name_slice[0..name_slice.len]); + slice[new_header_size + coff.lib_string_len + name_slice.len] = 0; + } else if (!old_needs_sort) { + // The entire string table is rebuilt in flushMember after sorting + coff.pending_members.putAssumeCapacity(Member.Index.second, {}); + } + + // Indices in this table are 1-based + const index_ptr: *u16 = @ptrCast(@alignCast(slice[old_header_size..])); + coff.targetStore(index_ptr, @intCast(@intFromEnum(mi) - Member.Index.known_count + 1)); + } + + coff.pending_members.putAssumeCapacity(mi, {}); + coff.member_prog_node.increaseEstimatedTotalItems(1); +} + +fn flushSymbolTableEntry(coff: *Coff, index: u32, pt: Zcu.PerThread) !void { + assert(!coff.isImage()); + const gpa = coff.base.comp.gpa; + + const si = coff.symbol_table.symbols.keys()[index]; + const sti = &coff.symbol_table.symbols.values()[index]; + + const sym = si.get(coff); + assert(sym.ni != .none or sym.gmi != .none); + + const entry = coff.symbolTableEntryPtr(sti.*) orelse entry: { + var buf: [15]u8 = undefined; + const symbol_name, const num_aux_symbols: u8, const complex_type: std.coff.ComplexType = + if (sym.gmi != .none) blk: { + const name = sym.gmi.name(coff); + break :blk .{ + try coff.getOrPutSymbolName(name.toSlice(coff), name), + @intFromBool(sym.flags.weak_external_strat != .none), + if (Symbol.Index.text.get(coff).section_number == sym.section_number) + .FUNCTION + else + .NULL, + }; + } else blk: switch (coff.getNode(sym.ni)) { + .image_section => .{ + try coff.getOrPutSymbolName(&sym.section_number.header(coff).name, null), + 1, + .NULL, + }, + .nav => |nmi| { + const zcu = coff.base.comp.zcu.?; + const ip = &zcu.intern_pool; + const nav = ip.getNav(nmi.navIndex(coff)); + break :blk .{ + try coff.getOrPutSymbolName(nav.fqn.toSlice(ip), null), + 0, + if (ip.isFunctionType(nav.resolved.?.type)) .FUNCTION else .NULL, + }; + }, + .uav => |umi| { + var w = Io.Writer.fixed(&buf); + w.print("__anon_{x}", .{umi.uavValue(coff)}) catch unreachable; + break :blk .{ + try coff.getOrPutSymbolName(w.buffered(), null), + 0, + .NULL, + }; + }, + inline .lazy_code, .lazy_const_data => |mi, tag| { + const lazy_sym = mi.lazySymbol(coff); + const name = try std.fmt.allocPrint(gpa, "__lazy_{s}_{f}", .{ + @tagName(lazy_sym.kind), + Type.fromInterned(lazy_sym.ty).fmt(pt), + }); + defer gpa.free(name); + + const string = try coff.getOrPutString(name); + break :blk .{ + try coff.getOrPutSymbolName(string.toSlice(coff), string), + 0, + if (tag == .lazy_code) .FUNCTION else .NULL, + }; + }, + else => { + log.err("TODO implement symbol table init for {s} ({d})", .{ @tagName(coff.getNode(sym.ni)), si }); + unreachable; + }, + }; + + const old_num_symbols = coff.targetLoad(&coff.headerPtr().number_of_symbols); + const new_num_symbols = old_num_symbols + 1 + num_aux_symbols; + coff.targetStore(&coff.headerPtr().number_of_symbols, new_num_symbols); + + try coff.symbol_table.ni.resize(&coff.mf, gpa, new_num_symbols * std.coff.Symbol.sizeOf()); + + sti.* = .wrap(old_num_symbols); + si.flushSymbolTableIndex(coff); + + const entry = coff.symbolTableEntryPtr(sti.*).?; + symbol_name.store(coff, &entry.name); + + entry.section_number = @enumFromInt(@intFromEnum(sym.section_number)); + entry.type = .{ + .complex_type = complex_type, + .base_type = .NULL, + }; + + entry.storage_class = if (sym.gmi != .none) + .EXTERNAL + else if (sym.flags.extra_tag == .next_alias_si) storage: { + var alias_sym = sym; + const weak_external = while (alias_sym.flags.extra_tag == .next_alias_si) { + const alias_si = alias_sym.extra.next_alias_si; + alias_sym = alias_si.get(coff); + assert(alias_sym.ni == sym.ni); + if (alias_sym.flags.weak_external_strat != .none) + break true; + } else false; + break :storage if (weak_external) .EXTERNAL else .STATIC; + } else .STATIC; + + entry.number_of_aux_symbols = num_aux_symbols; + if (coff.targetEndian() != native_endian) + std.mem.byteSwapAllFieldsAligned(std.coff.Symbol, .@"2", entry); + + if (num_aux_symbols > 0) aux_init: { + if (sym.gmi != .none) { + entry.section_number = .UNDEFINED; + entry.storage_class = .WEAK_EXTERNAL; + + const tag_index = sym.value.weak_alias_si.sti(coff).unwrap().?; + const aux_ptr = coff.symbolTableWeakExternalAuxEntryPtr(sti.*).?; + aux_ptr.* = .{ + .tag_index = tag_index, + .flag = switch (sym.flags.weak_external_strat) { + .none => unreachable, + .no_library => .SEARCH_NOLIBRARY, + .library => .SEARCH_LIBRARY, + .alias => .SEARCH_ALIAS, + .anti_dependency => .ANTI_DEPENDENCY, + }, + .unused = @splat(0), + }; + if (coff.targetEndian() != native_endian) + std.mem.byteSwapAllFieldsAligned(std.coff.WeakExternalDefinition, .@"2", aux_ptr); + + break :aux_init; + } else switch (coff.getNode(sym.ni)) { + .image_section => |sec_si| { + assert(si == sec_si); + const header = sym.section_number.header(coff); + const aux_ptr = coff.symbolTableSectionAuxEntryPtr(sti.*).?; + aux_ptr.* = .{ + .length = @intCast(sym.ni.location(&coff.mf).resolve(&coff.mf)[1]), + .number_of_relocations = header.number_of_relocations, + .number_of_linenumbers = header.number_of_linenumbers, + .checksum = 0, + .number = 0, + .selection = .NONE, + .unused = @splat(0), + }; + if (coff.targetEndian() != native_endian) + std.mem.byteSwapAllFieldsAligned(std.coff.SectionDefinition, .@"2", aux_ptr); + + break :aux_init; + }, + else => {}, + } + + unreachable; + } + + break :entry entry; + }; + + coff.targetStore(&entry.value, switch (sym.section_number) { + .UNDEFINED => if (entry.storage_class == .WEAK_EXTERNAL) 0 else sym.size(), + .ABSOLUTE, + .DEBUG, + => unreachable, + else => switch (coff.getNode(sym.ni)) { + .image_section => 0, + else => coff.computeSymbolSectionOffset(sym, .image), + }, + }); + + log.debug("flushSymbolTableEntry({d}) = {d}", .{ si, sti.* }); +} + +fn flushInputMember(coff: *Coff, iami: InputArchive.Member.Index) !void { + const member = iami.member(coff); + assert(!member.flags.is_loaded); + defer member.flags.is_loaded = true; + switch (member.content) { + .import => unreachable, + .object => |file_location| { + if (file_location.size == 0) return; + const comp = coff.base.comp; + const io = comp.io; + const path = member.iai.path(coff); + const file = try path.root_dir.handle.openFile(io, path.sub_path, .{}); + defer file.close(io); + var buffer: [4096]u8 = undefined; + var fr = file.reader(io, &buffer); + const offset = file_location.offset + @sizeOf(std.coff.ArchiveMemberHeader); + try fr.seekTo(offset); + log.debug("flushInputMember({f}({s}))", .{ path, member.name.toSlice(coff) }); + try coff.loadObject(path, member.name.toSlice(coff), &fr, .{ + .offset = offset, + .size = file_location.size, + }); + }, + } +} + +fn flushInputSection(coff: *Coff, isi: Node.InputSection.Index) !void { + const file_loc = isi.fileLocation(coff); + if (file_loc.size == 0) return; + const comp = coff.base.comp; + const io = comp.io; + const gpa = comp.gpa; + const ioi = isi.input(coff); + const path = ioi.path(coff); + const file = try path.root_dir.handle.openFile(io, path.sub_path, .{}); + defer file.close(io); + var fr = file.reader(io, &.{}); + try fr.seekTo(file_loc.offset); + var nw: MappedFile.Node.Writer = undefined; + const si = isi.symbol(coff); + si.node(coff).writer(&coff.mf, gpa, &nw); + defer nw.deinit(); + log.debug("flushInputSection({f}{f}, {s}, {d}, n{d})", .{ + path, + fmtMemberNameString(ioi.memberName(coff)), + si.get(coff).section_number.name(coff).toSlice(coff), + si, + si.node(coff), + }); + if (try nw.interface.sendFileAll(&fr, .limited(@intCast(file_loc.size))) != file_loc.size) + return error.EndOfStream; + try si.applyLocationRelocs(coff); } -fn addSection(coff: *Coff, name: []const u8, flags: std.coff.SectionHeader.Flags) !Symbol.Index { +fn addSection(coff: *Coff, name: String, flags: std.coff.SectionHeader.Flags) !Symbol.Index { + assert(coff.hasCoffHeader()); + const gpa = coff.base.comp.gpa; try coff.nodes.ensureUnusedCapacity(gpa, 1); - try coff.image_section_table.ensureUnusedCapacity(gpa, 1); - try coff.symbol_table.ensureUnusedCapacity(gpa, 1); + try coff.section_table.ensureUnusedCapacity(gpa, 1); + try coff.symbols.ensureUnusedCapacity(gpa, 1); + if (!isImage(coff)) try coff.symbol_table.symbols.ensureUnusedCapacity(gpa, 1); const coff_header = coff.headerPtr(); const section_index = coff.targetLoad(&coff_header.number_of_sections); @@ -1356,21 +3367,32 @@ fn addSection(coff: *Coff, name: []const u8, flags: std.coff.SectionHeader.Flags gpa, @sizeOf(std.coff.SectionHeader) * section_table_len, ); - const ni = try coff.mf.addLastChildNode(gpa, .root, .{ + + const ni = try coff.mf.addLastChildNode(gpa, coff.sectionParent(), .{ .alignment = coff.mf.flags.block_size, .moved = true, .bubbles_moved = false, }); + const si = coff.addSymbolAssumeCapacity(); - coff.image_section_table.appendAssumeCapacity(si); + coff.section_table.putAssumeCapacity(name, .{ + .si = si, + .relocation_table_ni = .none, + }); coff.nodes.appendAssumeCapacity(.{ .image_section = si }); const section_table = coff.sectionTableSlice(); - const virtual_size = coff.optionalHeaderField(.section_alignment); - const rva: u32 = switch (section_index) { - 0 => @intCast(Node.known.header.location(&coff.mf).resolve(&coff.mf)[1]), - else => coff.image_section_table.items[section_index - 1].get(coff).rva + - coff.targetLoad(&section_table[section_index - 1].virtual_size), - }; + + const virtual_size, const rva = if (coff.isImage()) block: { + const virtual_size = coff.optionalHeaderField(.section_alignment); + const rva: u32 = switch (section_index) { + 0 => @intCast(Node.known.header.location(&coff.mf).resolve(&coff.mf)[1]), + else => coff.section_table.values()[section_index - 1].si.get(coff).rva + + coff.targetLoad(&section_table[section_index - 1].virtual_size), + }; + + break :block .{ virtual_size, rva }; + } else .{ 0, 0 }; + { const sym = si.get(coff); sym.ni = ni; @@ -1390,16 +3412,24 @@ fn addSection(coff: *Coff, name: []const u8, flags: std.coff.SectionHeader.Flags .number_of_linenumbers = 0, .flags = flags, }; - @memcpy(section.name[0..name.len], name); - @memset(section.name[name.len..], 0); if (coff.targetEndian() != native_endian) std.mem.byteSwapAllFields(std.coff.SectionHeader, section); - switch (coff.optionalHeaderPtr()) { - inline else => |optional_header| coff.targetStore( - &optional_header.size_of_image, - @intCast(rva + virtual_size), - ), + + const name_slice = name.toSlice(coff); + if (coff.isImage()) { + @memcpy(section.name[0..name_slice.len], name_slice); + @memset(section.name[name_slice.len..], 0); + switch (coff.optionalHeaderPtr()) { + inline else => |optional_header| coff.targetStore( + &optional_header.size_of_image, + @intCast(rva + virtual_size), + ), + } + } else { + (try coff.getOrPutSymbolName(name_slice, name)).store(coff, &section.name); + try coff.pendingSymbolTableEntry(si); } + return si; } @@ -1412,7 +3442,40 @@ const ObjectSectionAttributes = packed struct { nocache: bool = false, discard: bool = false, remove: bool = false, + initialized: bool = false, + uninitialized: bool = false, + + pub fn fromFlags(flags: std.coff.SectionHeader.Flags) ObjectSectionAttributes { + return .{ + .read = flags.MEM_READ, + .write = flags.MEM_WRITE, + .execute = flags.MEM_EXECUTE, + .shared = flags.MEM_SHARED, + .nopage = flags.MEM_NOT_PAGED, + .nocache = flags.MEM_NOT_CACHED, + .discard = flags.MEM_DISCARDABLE, + .remove = flags.LNK_REMOVE, + .initialized = flags.CNT_INITIALIZED_DATA, + .uninitialized = flags.CNT_UNINITIALIZED_DATA, + }; + } + + pub fn asFlags(attr: ObjectSectionAttributes) std.coff.SectionHeader.Flags { + return .{ + .MEM_READ = attr.read, + .MEM_WRITE = attr.write, + .MEM_EXECUTE = attr.execute, + .MEM_SHARED = attr.shared, + .MEM_NOT_PAGED = attr.nopage, + .MEM_NOT_CACHED = attr.nocache, + .MEM_DISCARDABLE = attr.discard, + .LNK_REMOVE = attr.remove, + .CNT_INITIALIZED_DATA = attr.uninitialized, + .CNT_UNINITIALIZED_DATA = attr.uninitialized, + }; + } }; + fn pseudoSectionMapIndex( coff: *Coff, name: String, @@ -1422,15 +3485,25 @@ 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); - if (!pseudo_section_gop.found_existing) { - const parent: Symbol.Index = 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; + + break :parent parent; + } else try coff.addSection(effective_name, attributes.asFlags()); + try coff.nodes.ensureUnusedCapacity(gpa, 1); - try coff.symbol_table.ensureUnusedCapacity(gpa, 1); + try coff.symbols.ensureUnusedCapacity(gpa, 1); const ni = try coff.mf.addLastChildNode(gpa, parent.node(coff), .{ .alignment = alignment }); const si = coff.addSymbolAssumeCapacity(); pseudo_section_gop.value_ptr.* = si; @@ -1441,9 +3514,30 @@ fn pseudoSectionMapIndex( assert(sym.loc_relocs == .none); sym.loc_relocs = @enumFromInt(coff.relocs.items.len); coff.nodes.appendAssumeCapacity(.{ .pseudo_section = psmi }); - } + break :sn sym.section_number; + } else pseudo_section_gop.value_ptr.get(coff).section_number; + + try coff.verifyParentSectionAttributes( + parent_sn, + name, + .pseudo, + .fromFlags(parent_sn.header(coff).flags), + attributes, + ); + return psmi; } + +fn objectSectionParentName(coff: *Coff, name: []const u8) []const u8 { + // In images we want to sort object sections into the final root section name. + // Otherwise, we want to keep the full name so that this sort can occur correctly when + // the object is finally linked into an image. + return if (coff.isImage()) + name[0 .. std.mem.indexOfScalar(u8, name, '$') orelse name.len] + else + name; +} + fn objectSectionMapIndex( coff: *Coff, name: String, @@ -1451,16 +3545,23 @@ fn objectSectionMapIndex( attributes: ObjectSectionAttributes, ) !Node.ObjectSectionMapIndex { const gpa = coff.base.comp.gpa; + const name_slice = name.toSlice(coff); + // TODO: Should this be a section merge instead? + const effective_attributes = if (coff.isImage() and std.mem.startsWith(u8, name_slice, ".tls")) attr: { + // In images, the .tls section is a read-only template + var attr = attributes; + attr.write = false; + break :attr attr; + } else attributes; + const object_section_gop = try coff.object_section_table.getOrPut(gpa, name); const osmi: Node.ObjectSectionMapIndex = @enumFromInt(object_section_gop.index); - if (!object_section_gop.found_existing) { - try coff.ensureUnusedStringCapacity(name.toSlice(coff).len); - const name_slice = name.toSlice(coff); - const parent = (try coff.pseudoSectionMapIndex(coff.getOrPutStringAssumeCapacity( - name_slice[0 .. std.mem.indexOfScalar(u8, name_slice, '$') orelse name_slice.len], - ), alignment, attributes)).symbol(coff); + const sym = if (!object_section_gop.found_existing) sym: { + try coff.ensureUnusedStringCapacity(name_slice.len); + const parent_name = coff.getOrPutStringAssumeCapacity(coff.objectSectionParentName(name_slice)); + const parent = (try coff.pseudoSectionMapIndex(parent_name, alignment, effective_attributes)).symbol(coff); try coff.nodes.ensureUnusedCapacity(gpa, 1); - try coff.symbol_table.ensureUnusedCapacity(gpa, 1); + try coff.symbols.ensureUnusedCapacity(gpa, 1); const parent_ni = parent.node(coff); var prev_ni: MappedFile.Node.Index = .none; var next_it = parent_ni.children(&coff.mf); @@ -1492,30 +3593,219 @@ fn objectSectionMapIndex( assert(sym.loc_relocs == .none); sym.loc_relocs = @enumFromInt(coff.relocs.items.len); coff.nodes.appendAssumeCapacity(.{ .object_section = osmi }); + break :sym sym; + } else object_section_gop.value_ptr.get(coff); + + const parent_ni = sym.ni.parent(&coff.mf); + const parent_alignment = parent_ni.alignment(&coff.mf); + if (alignment.compare(.gt, parent_alignment)) { + log.debug("realignParent({s}, {d}) {d}->{d}", .{ name.toSlice(coff), parent_ni, parent_alignment, alignment }); + try parent_ni.realign(&coff.mf, gpa, alignment, .{ .set_alignment = true }); + } + + const old_alignment = sym.ni.alignment(&coff.mf); + if (alignment.compare(.gt, old_alignment)) { + log.debug("realignObject({s}) {d}->{d}", .{ name.toSlice(coff), old_alignment, alignment }); + try sym.ni.realign(&coff.mf, gpa, alignment, .{ .set_alignment = true }); } + + try coff.verifyParentSectionAttributes( + sym.section_number, + name, + .object, + .fromFlags(sym.section_number.header(coff).flags), + effective_attributes, + ); + return osmi; } +fn verifyParentSectionAttributes( + coff: *Coff, + parent: Symbol.SectionNumber, + child_name: String, + child_kind: enum { pseudo, object }, + parent_attrs: ObjectSectionAttributes, + child_attrs: ObjectSectionAttributes, +) !void { + if (parent_attrs == child_attrs) return; + + const was_merged = switch (child_kind) { + .pseudo => coff.section_merges.contains(child_name), + .object => if (coff.getString( + coff.objectSectionParentName(child_name.toSlice(coff)), + ).unwrap()) |pseudo_name| + coff.section_merges.contains(pseudo_name) + else + false, + }; + + // The section was intentionally merged by the user or builtin rule + if (was_merged) return; + + const BackingT = @typeInfo(ObjectSectionAttributes).@"struct".backing_integer.?; + const num_notes = @popCount(@as(BackingT, @bitCast(parent_attrs)) ^ @as(BackingT, @bitCast(child_attrs))); + var err = try coff.base.comp.link_diags.addErrorWithNotes(num_notes); + try err.addMsg("{t} section '{s}' was placed in parent section '{s}' with mismatched flags", .{ + child_kind, + child_name.toSlice(coff), + parent.name(coff).toSlice(coff), + }); + + inline for (comptime std.meta.fieldNames(ObjectSectionAttributes)) |field| { + if (@field(child_attrs, field) != @field(parent_attrs, field)) { + err.addNote("flags.{s} was {d} in {s}, but {d} in {s}", .{ + field, + @intFromBool(@field(child_attrs, field)), + child_name.toSlice(coff), + @intFromBool(@field(parent_attrs, field)), + parent.name(coff).toSlice(coff), + }); + } + } + + return error.AlreadyReported; +} + +const RelocAddend = union(enum) { + known: i64, + /// Relocs tables in input objects don't include the addend. + /// The value needs to be recovered from the reloc location. + pending: void, +}; + +// TODO: There should be an API where the caller can indicate how many contiguous relocs they need +// and it should attempt to allocate these from from the free list if available. We can cache +// the run length of each segment on Reloc when `free` is set. pub fn addReloc( coff: *Coff, loc_si: Symbol.Index, offset: u64, target_si: Symbol.Index, - addend: i64, + addend: RelocAddend, + @"type": Reloc.Type, +) link.Error!void { + const diags = &coff.base.comp.link_diags; + try coff.ensureUnusedRelocCapacity(loc_si, 1); + coff.addRelocAssumeCapacity(loc_si, offset, target_si, addend, @"type") catch |err| switch (err) { + error.MappedFileIo => return diags.fail( + "failed to write output file: {t}", + .{coff.mf.io_err.?}, + ), + else => |e| return e, + }; +} + +fn ensureUnusedRelocCapacity(coff: *Coff, loc_si: Symbol.Index, len: usize) !void { + const gpa = coff.base.comp.gpa; + try coff.relocs.ensureUnusedCapacity(gpa, len); + if (isImage(coff)) return; + switch (loc_si.get(coff).section_number) { + .UNDEFINED, .ABSOLUTE, .DEBUG => {}, + else => |loc_sn| { + const section = loc_sn.section(coff); + if (section.relocation_table_ni == .none) + try coff.nodes.ensureUnusedCapacity(gpa, 1); + }, + } +} + +fn addRelocAssumeCapacity( + coff: *Coff, + loc_si: Symbol.Index, + offset: u64, + target_si: Symbol.Index, + addend: RelocAddend, @"type": Reloc.Type, ) !void { const gpa = coff.base.comp.gpa; const target = target_si.get(coff); + const ri: Reloc.Index = @enumFromInt(coff.relocs.items.len); - (try coff.relocs.addOne(gpa)).* = .{ + log.debug("addReloc({d}@{d}+0x{x} -> {d}@{d}+0x{x}{s}) = {d}", .{ + loc_si, + loc_si.get(coff).section_number, + offset, + target_si, + target_si.get(coff).section_number, + if (addend == .pending) 0 else addend.known, + if (addend == .pending) "p" else "k", + ri, + }); + + const sri: Section.RelocationIndex = if (isImage(coff)) + .none + else switch (loc_si.get(coff).section_number) { + .UNDEFINED, + .ABSOLUTE, + .DEBUG, + => .none, + else => |loc_sn| sri: { + // The target may not have a node yet, or it could be an extern that will never + // have a node. In that case, flushGlobal will create the symbol table entry. + const existing_sti = target_si.sti(coff); + const sti: SymbolTable.Index = if (existing_sti != .none) + existing_sti + else if (target.ni != .none) sti: { + try coff.pendingSymbolTableEntry(target_si); + break :sti .none; + } else .none; + + const sri: Section.RelocationIndex = blk: { + const section = loc_sn.section(coff); + const header = loc_sn.header(coff); + const old_num_relocations = coff.targetLoad(&header.number_of_relocations); + const new_num_relocations = old_num_relocations + 1; + const new_size = @as(u32, new_num_relocations) * std.coff.Relocation.sizeOf(); + + coff.targetStore(&header.number_of_relocations, new_num_relocations); + if (coff.symbolTableSectionAuxEntryPtr(loc_sn.symbol(coff).sti(coff))) |aux_ptr| + coff.targetStore(&aux_ptr.number_of_relocations, new_num_relocations); + + if (section.relocation_table_ni == .none) { + section.relocation_table_ni = try coff.mf.addLastChildNode( + gpa, + coff.sectionParent(), + .{ + .size = new_size, + .alignment = .@"2", + .moved = true, + .resized = true, + }, + ); + coff.nodes.appendAssumeCapacity(.{ .relocation_table = loc_sn }); + } else { + try section.relocation_table_ni.resize(&coff.mf, gpa, new_size); + } + + // TODO: These need to allocate from a free list, once deleting relocs from the table is supported + break :blk .wrap(old_num_relocations); + }; + + const entry = sri.entry(coff, loc_sn).?; + if (sti.unwrap()) |index| coff.targetStore(&entry.symbol_table_index, index); + + // applyLocationRelocs updates `virtual_address` + // flushSymbolTableIndex updates `symbol_table_index` + coff.targetStore(&entry.type, @bitCast(@"type")); + + break :sri sri; + }, + }; + + coff.relocs.addOneAssumeCapacity().* = .{ .type = @"type", .prev = .none, .next = target.target_relocs, .loc = loc_si, .target = target_si, - .unused = 0, + .sri = sri, .offset = offset, - .addend = addend, + .addend = if (addend == .pending) 0 else addend.known, + .flags = .{ + .recover_addend = addend == .pending, + .free = false, + }, }; switch (target.target_relocs) { .none => {}, @@ -1524,15 +3814,1641 @@ pub fn addReloc( target.target_relocs = ri; } +fn failLoadInput( + coff: *Coff, + err: LoadInputError, + fr: *Io.File.Reader, + path: std.Build.Cache.Path, +) link.Error { + const diags = &coff.base.comp.link_diags; + switch (err) { + else => |e| return e, + error.MappedFileIo => return diags.fail( + "failed to write output file: {t}", + .{coff.mf.io_err.?}, + ), + error.EndOfStream => return diags.failParse( + path, + "unexpected eof", + .{}, + ), + error.AccessDenied, + error.Unexpected, + error.Unseekable, + => |e| return diags.fail( + "failed to read \"{f}\": {t}", + .{ path.fmtEscapeString(), e }, + ), + error.PermissionDenied, + error.SystemResources, + error.Streaming, + => |e| return diags.fail( + "failed to stat \"{f}\": {t}", + .{ path.fmtEscapeString(), e }, + ), + error.ReadFailed => switch (fr.err.?) { + error.Canceled => |e| return e, + else => |e| return diags.fail( + "failed to read \"{f}\": {t}", + .{ path.fmtEscapeString(), e }, + ), + }, + } +} + +pub fn loadInput(coff: *Coff, input: link.Input) link.Error!void { + const comp = coff.base.comp; + const io = comp.io; + + const path = input.path() orelse unreachable; + const gop = try coff.inputs.getOrPut(comp.gpa, path); + if (gop.found_existing) return; + errdefer _ = coff.inputs.swapRemove(path); + + var buf: [4096]u8 = undefined; + switch (input) { + .object => |object| { + var fr = object.file.reader(io, &buf); + coff.loadObject(object.path, null, &fr, .{ + .offset = fr.logicalPos(), + .size = fr.getSize() catch |err| + return coff.failLoadInput(err, &fr, object.path), + }) catch |err| return coff.failLoadInput(err, &fr, object.path); + }, + .archive => |archive| { + var fr = archive.file.reader(io, &buf); + coff.loadArchive(archive.path, &fr) catch |err| + return coff.failLoadInput(err, &fr, archive.path); + }, + .res => |res| { + var fr = res.file.reader(io, &buf); + coff.loadRes(res.path, &fr) catch |err| + return coff.failLoadInput(err, &fr, res.path); + }, + .dso => |dso| { + var fr = dso.file.reader(io, &buf); + coff.loadDll(dso.path, &fr) catch |err| + return coff.failLoadInput(err, &fr, dso.path); + }, + .dso_exact => unreachable, + } +} + +fn fmtMemberNameString(memberName: ?[]const u8) std.fmt.Alt(?[]const u8, memberNameStringEscape) { + return .{ .data = memberName }; +} + +fn memberNameStringEscape(memberName: ?[]const u8, w: *std.Io.Writer) std.Io.Writer.Error!void { + try w.print("({f})", .{std.zig.fmtString(memberName orelse return)}); +} + +fn inputSectionHeaderNameSlice( + coff: *Coff, + header: *const std.coff.SectionHeader, + string_table: []const u8, + path: std.Build.Cache.Path, + section_i: usize, +) ![]const u8 { + const diags = &coff.base.comp.link_diags; + return if (header.name[0] == '/') name: { + const offset_str = std.mem.sliceTo(header.name[1..], 0); + const name_offset = std.fmt.parseUnsigned(u24, offset_str, 10) catch + return diags.failParse(path, "ill-formed section name in section {d}: '{s}'", .{ + section_i, + header.name[0 .. offset_str.len + 1], + }); + + if (name_offset > string_table.len) + return diags.failParse(path, "out-of-bounds section name offset in section {d}: {d}", .{ section_i, name_offset }); + + break :name std.mem.sliceTo(string_table[name_offset..], 0); + } else std.mem.sliceTo(&header.name, 0); +} + +fn loadObject( + coff: *Coff, + path: std.Build.Cache.Path, + member_name: ?[]const u8, + fr: *Io.File.Reader, + fl: MappedFile.Node.FileLocation, +) LoadInputError!void { + const comp = coff.base.comp; + const gpa = comp.gpa; + const diags = &comp.link_diags; + const r = &fr.interface; + const target = &comp.root_mod.resolved_target.result; + 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) }); + + const header = try r.peekStruct(std.coff.Header, .little); + if (header.machine != target.toCoffMachine()) + return diags.failParse(path, "machine mismatch: expected {t}, found {t}", .{ + target.toCoffMachine(), + header.machine, + }); + if (header.number_of_sections == 0) return; + if (@sizeOf(std.coff.Header) + @as(usize, header.number_of_sections) * @sizeOf(std.coff.SectionHeader) > fl.size) + return diags.failParse(path, "invalid section table", .{}); + const unexpected_header_flags: []const std.meta.FieldEnum(std.coff.Header.Flags) = &.{ + .RELOCS_STRIPPED, + .EXECUTABLE_IMAGE, + .AGGRESSIVE_WS_TRIM, + .RESERVED, + .BYTES_REVERSED_LO, + .DLL, + .BYTES_REVERSED_HI, + }; + inline for (unexpected_header_flags) |flag| + if (@field(header.flags, @tagName(flag))) + return diags.failParse(path, "unexpected flag set: {t}", .{flag}); + + if (header.size_of_optional_header != 0) + return diags.failParse(path, "unexpected optional header", .{}); + + const symbol_table_len = header.number_of_symbols * std.coff.Symbol.sizeOf(); + const symbol_table_end = header.pointer_to_symbol_table + symbol_table_len; + // String table length (which includes the length field) immediately trails the symbol table + if (symbol_table_end + @sizeOf(u32) > fl.size) + return diags.failParse(path, "bad symbol table location", .{}); + + try fr.seekTo(fl.offset + symbol_table_end); + const string_table_len = try r.peekInt(u32, target_endian); + if (string_table_len < @sizeOf(u32) or + symbol_table_end + string_table_len > fl.size) + return diags.failParse(path, "bad string table length: 0x{x}", .{string_table_len}); + + const ioi: InputObject.Index = @enumFromInt(coff.input_objects.items.len); + try coff.input_objects.ensureUnusedCapacity(gpa, 1); + const input = coff.input_objects.addOneAssumeCapacity(); + input.* = .{ + .path = path, + .member_name = if (member_name) |m| try gpa.dupe(u8, m) else null, + .source_name = .none, + }; + + const string_table = string_table: { + const string_table = try gpa.alloc(u8, string_table_len); + errdefer gpa.free(string_table); + try r.readSliceAll(string_table); + break :string_table string_table; + }; + defer gpa.free(string_table); + + try coff.ensureManyUnusedStringCapacity( + header.number_of_sections + header.number_of_symbols, + header.number_of_sections * 9 + + header.number_of_symbols * 9 + + string_table_len - @sizeOf(u32), + ); + + const PendingSymbolIndex = enum(u32) { + none, + _, + + pub fn wrap(i: ?u32) @This() { + return @enumFromInt((i orelse return .none) + 1); + } + + pub fn unwrap(i: @This()) ?u32 { + return switch (i) { + .none => null, + _ => @intFromEnum(i) - 1, + }; + } + }; + + const PendingInputSection = struct { + header: std.coff.SectionHeader, + name: String, + si: Symbol.Index, + parent_si: Symbol.Index, + psi: PendingSymbolIndex, + num_symbols: u32, + comdat: std.coff.ComdatSelection, + comdat_psi: PendingSymbolIndex, + comdat_crc: u32, + comdat_association: Symbol.SectionNumber, + comdat_result: union(enum) { + pending, + // Root of the association chain + pending_association: Symbol.SectionNumber, + include, + skip, + }, + }; + + const sections: []PendingInputSection = if (coff.isImage()) sections: { + const sections = try gpa.alloc(PendingInputSection, header.number_of_sections); + errdefer gpa.free(sections); + + try fr.seekTo(fl.offset + @sizeOf(std.coff.Header)); + for (sections, 0..) |*section, section_i| { + section.* = .{ + .header = try r.takeStruct(std.coff.SectionHeader, target_endian), + .name = undefined, + .si = .null, + .parent_si = .null, + .psi = .none, + .num_symbols = 0, + .comdat = .NONE, + .comdat_psi = .none, + .comdat_crc = 0, + .comdat_association = .UNDEFINED, + .comdat_result = .pending, + }; + + const section_name_slice = if (section.header.name[0] == '/') name: { + const offset_str = std.mem.sliceTo(section.header.name[1..], 0); + const name_offset = std.fmt.parseUnsigned(u24, offset_str, 10) catch + return diags.failParse(path, "ill-formed section name offset in section {d}: '{s}'", .{ + section_i, + section.header.name[0 .. offset_str.len + 1], + }); + + if (name_offset > string_table.len) + return diags.failParse( + path, + "out-of-bounds section name offset in section {d}: {d}", + .{ section_i, name_offset }, + ); + + break :name std.mem.sliceTo(string_table[name_offset..], 0); + } else std.mem.sliceTo(&section.header.name, 0); + section.name = coff.getOrPutStringAssumeCapacity(section_name_slice); + + if (section.header.pointer_to_linenumbers + + @as(u32, section.header.number_of_linenumbers) * std.coff.LineNumber.sizeOf() > fl.size) + return diags.failParse(path, "bad line numbers location in section {d} `{s}`", .{ + section_i, + section_name_slice, + }); + + if (section.header.pointer_to_relocations + + @as(u32, section.header.number_of_relocations) * std.coff.Relocation.sizeOf() > fl.size) + return diags.failParse(path, "bad relocations location in section {d} `{s}`", .{ + section_i, + section_name_slice, + }); + + if (section.header.pointer_to_raw_data + section.header.size_of_raw_data > fl.size) + return diags.failParse(path, "bad raw data location in section {d} `{s}`", .{ + section_i, + section_name_slice, + }); + } + + break :sections sections; + } else &.{}; + defer gpa.free(sections); + + const mi = if (is_archive) mi: { + try coff.nodes.ensureUnusedCapacity(gpa, 2); + try coff.members.ensureUnusedCapacity(gpa, 1); + const path_str = try path.toString(gpa); + defer gpa.free(path_str); + + const mi = try coff.addMemberAssumeCapacity(.coff, fl.size); + const member = mi.get(coff); + try member.initHeader(coff, path_str, header.time_date_stamp); + + { + // TODO: This should be deferred to an idle task (but resize it here!) + var nw: MappedFile.Node.Writer = undefined; + member.content_ni.writer(&coff.mf, gpa, &nw); + defer nw.deinit(); + + try fr.seekTo(fl.offset); + const written = nw.interface.sendFileAll(fr, .limited64(fl.size)) catch |err| switch (err) { + error.WriteFailed => return nw.err.?, + else => |e| return e, + }; + + if (written != fl.size) return error.EndOfStream; + } + + break :mi mi; + } else undefined; + + try fr.seekTo(fl.offset + header.pointer_to_symbol_table); + const symbol_size = std.coff.Symbol.sizeOf(); + + const PendingSymbol = struct { + name: String, + value: union(enum) { + // Size of the section + section: u32, + // If section is absolute, the symbol value. + // Otherwise, offset within the section. + static: u32, + // If section is undefined, the symbol size. + // If section is absolute, the symbol value. + // Otherwise offset within the section. + external: u32, + // The index of the target symbol of this weak external + weak_external: u32, + // Trails .weak_external + weak_external_aux: WeakExternalStrat, + }, + section_number: Symbol.SectionNumber, + si: Symbol.Index, + // If a weak external targets this symbol, the index of the weak external + weak_external_psi: PendingSymbolIndex, + }; + + var num_global_symbols: u32 = 0; + var pending_symbols: std.array_hash_map.Auto(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; + var num_included_symbols: u32 = 0; + while (symbol_i < header.number_of_symbols) { + var symbol: std.coff.Symbol = undefined; + @memcpy(std.mem.asBytes(&symbol)[0..symbol_size], try r.take(symbol_size)); + if (target_endian != native_endian) + std.mem.byteSwapAllFields(std.coff.Symbol, &symbol); + + const aux_symbols = if (symbol.number_of_aux_symbols > 0) + try r.take(symbol_size * symbol.number_of_aux_symbols) + else + &.{}; + defer symbol_i += symbol.number_of_aux_symbols + 1; + + const name = std.mem.sliceTo(if (std.mem.eql(u8, symbol.name[0..4], "\x00\x00\x00\x00")) name: { + const index = std.mem.readInt(u32, symbol.name[4..], target_endian); + if (index >= string_table.len) + return diags.failParse(path, "bad string offset for symbol 0x{x}", .{symbol_i}); + break :name string_table[index..]; + } else &symbol.name, 0); + + if (is_archive) { + if (switch (symbol.storage_class) { + .WEAK_EXTERNAL => true, + .EXTERNAL => symbol.section_number != .UNDEFINED, + else => false, + }) try coff.ensureMemberSymbol(mi, coff.getOrPutStringAssumeCapacity(name)); + + continue; + } + + switch (symbol.section_number) { + .UNDEFINED, .DEBUG, .ABSOLUTE => {}, + else => |sn| if (@intFromEnum(sn) > sections.len) + return diags.failParse(path, "out-of-bounds section number {d} in symbol 0x{x}", .{ sn, symbol_i }), + } + + const psi: PendingSymbolIndex = .wrap(@intCast(pending_symbols.count())); + const section_number: Symbol.SectionNumber = @enumFromInt(@intFromEnum(symbol.section_number)); + + const values: []const @FieldType(PendingSymbol, "value") = pending_symbols: switch (symbol.storage_class) { + .STATIC, .LABEL => |storage_class| switch (section_number) { + // TODO: Do we need to do anything with @feat.00? + // https://llvm.org/doxygen/namespacellvm_1_1COFF.html#aeffa16735e18df727a173beaf748c392 + .UNDEFINED, + .DEBUG, + => &.{}, + .ABSOLUTE => &.{.{ .static = symbol.value }}, + else => |sn| { + const section = &sections[sn.toIndex()]; + + // Section symbol + const is_section = storage_class == .STATIC and + symbol.value == 0 and + symbol.type == std.coff.SymType{ + .complex_type = .NULL, + .base_type = .NULL, + } and + symbol.number_of_aux_symbols > 0; + + if (is_section) { + if (symbol.number_of_aux_symbols > 1) + return diags.failParse(path, "invalid number of aux symbols for section symbol 0x{x}: {d}", .{ + symbol_i, + symbol.number_of_aux_symbols, + }); + + var section_def: std.coff.SectionDefinition = undefined; + @memcpy(std.mem.asBytes(&section_def)[0..symbol_size], aux_symbols[0..symbol_size]); + if (target_endian != native_endian) + std.mem.byteSwapAllFields(std.coff.SectionDefinition, &section_def); + + if (section_def.number_of_relocations != section.header.number_of_relocations) + return diags.failParse( + path, + "section aux symbol 0x{x} for '{s}' relocation count did not match section header: {d} vs {d}", + .{ symbol_i + 1, name, section_def.number_of_relocations, section.header.number_of_relocations }, + ); + + if (section_def.number_of_linenumbers != section.header.number_of_linenumbers) + return diags.failParse( + path, + "section aux symbol 0x{x} for '{s}' line number count did not match section header: {d} vs {d}", + .{ symbol_i + 1, name, section_def.number_of_linenumbers, section.header.number_of_linenumbers }, + ); + + if (section.header.flags.LNK_COMDAT) { + if (section_def.selection == .ASSOCIATIVE) { + if (section_def.number == 0 or section_def.number > sections.len) + return diags.failParse( + path, + "section aux symbol 0x{x} for '{s}' contained an invalid associated section number: 0x{x}", + .{ symbol_i + 1, name, section_def.number }, + ); + + section.comdat_association = @enumFromInt(section_def.number); + } + + section.comdat = section_def.selection; + section.comdat_crc = section_def.checksum; + } + + section.psi = psi; + } + + break :pending_symbols &.{if (is_section) + .{ .section = section.header.size_of_raw_data } + else + .{ .static = symbol.value }}; + }, + }, + .WEAK_EXTERNAL => switch (symbol.section_number) { + .UNDEFINED => { + if (symbol.value != 0) + return diags.failParse( + path, + "invalid value {d} for weak external symbol 0x{x}", + .{ symbol.value, symbol_i }, + ); + + var weak_external: std.coff.WeakExternalDefinition = undefined; + @memcpy(std.mem.asBytes(&weak_external)[0..symbol_size], aux_symbols[0..symbol_size]); + if (target_endian != native_endian) + std.mem.byteSwapAllFields(std.coff.WeakExternalDefinition, &weak_external); + + if (weak_external.tag_index >= header.number_of_symbols) + return diags.failParse( + path, + "invalid tag_index 0x{x} for weak external symbol 0x{x}", + .{ weak_external.tag_index, symbol_i }, + ); + + break :pending_symbols switch (weak_external.flag) { + else => |flag| &.{ + .{ .weak_external = weak_external.tag_index }, + .{ .weak_external_aux = WeakExternalStrat.fromFlag(flag) }, + }, + _ => return diags.failParse( + path, + "encountered unknown weak external characteristic 0x{x} for symbol 0x{x}", + .{ weak_external.flag, symbol_i }, + ), + }; + }, + else => |sn| return diags.failParse( + path, + "invalid section number {d} for weak external symbol 0x{x}", + .{ sn, symbol_i }, + ), + }, + .EXTERNAL => switch (section_number) { + .UNDEFINED, + .ABSOLUTE, + => &.{.{ .external = symbol.value }}, + .DEBUG => return diags.failParse( + path, + "unexpected external symbol 0x{x} in DEBUG section: '{s}'", + .{ symbol_i, name }, + ), + else => &.{.{ .external = symbol.value }}, + }, + .FILE => { + if (!std.mem.eql(u8, name, ".file")) + return diags.failParse( + path, + "unexpected symbol name '{s}' for file symbol 0x{x}", + .{ name, symbol_i }, + ); + + var file: std.coff.FileDefinition = undefined; + @memcpy(std.mem.asBytes(&file)[0..symbol_size], aux_symbols[0..symbol_size]); + + input.source_name = (try coff.getOrPutString(file.getFileName())).toOptional(); + break :pending_symbols &.{}; + }, + else => |storage_class| return diags.failParse( + path, + "TODO handle storage class {t} for symbol 0x{x}", + .{ storage_class, symbol_i }, + ), + }; + + for (values, 0..) |value, i| { + if (section_number == .ABSOLUTE) + num_included_symbols += 1; + + switch (value) { + .section => {}, + .static, + .external, + .weak_external, + => { + num_global_symbols += 1; + if (section_number.hasIndex()) { + const section = &sections[section_number.toIndex()]; + section.num_symbols += 1; + if (section.header.flags.LNK_COMDAT and section.comdat_psi == .none) + section.comdat_psi = psi; + } + }, + .weak_external_aux => {}, + } + + const symbol_name = coff.getOrPutStringAssumeCapacity(name); + pending_symbols.putAssumeCapacity(symbol_i + @as(u32, @intCast(i)), .{ + .name = symbol_name, + .value = value, + .section_number = section_number, + .si = .null, + .weak_external_psi = .none, + }); + } + } + + try coff.globals.ensureUnusedCapacity(gpa, num_global_symbols); + for (sections) |*section| { + if (section.header.flags.LNK_INFO) { + if (std.mem.eql(u8, &section.header.name, ".drectve")) { + try fr.seekTo(fl.offset + section.header.pointer_to_raw_data); + // TODO: Don't really want an additional buffer here, but want to limit to size_of_raw_data + var buf: [128]u8 = undefined; + var section_r = r.limited(.limited(section.header.size_of_raw_data), &buf); + while (section_r.interface.takeDelimiter(' ') catch |err| switch (err) { + error.StreamTooLong => return diags.failParse(path, "unexpectedly long .drectve argument", .{}), + else => |e| return e, + }) |arg| { + // Microsoft tools emit 3 space characters into this section even with /Zl + if (arg.len == 0) continue; + + if (std.ascii.startsWithIgnoreCase(arg, "-exclude-symbols:")) { + // TODO: When implementing mingw auto-exports (if at all?), track this to not export this symbol + } else if (std.ascii.startsWithIgnoreCase(arg, "/include:")) { + _ = try coff.globalSymbol(.{ .name = arg["/include:".len..] }); + } else if (std.ascii.startsWithIgnoreCase(arg, "/alternatename:")) { + var split = std.mem.splitScalar(u8, arg["/alternatename:".len..], '='); + const orig = split.first(); + const alt = split.next() orelse + return diags.failParse(path, "malformed .drectve argument: '{s}'", .{arg}); + + try coff.ensureManyUnusedStringCapacity(2, orig.len + alt.len + 2); + const orig_str = coff.getOrPutStringAssumeCapacity(orig); + const alt_str = coff.getOrPutStringAssumeCapacity(alt); + const gop = try coff.alternate_names.getOrPut(gpa, orig_str); + if (!gop.found_existing) { + log.debug("alternateName({s}={s})", .{ orig, alt }); + gop.value_ptr.* = alt_str; + } else if (gop.value_ptr.* != alt_str) + return diags.failParse( + path, + "conflicting /alternatename .drectve arguments: first seen as {s}={s}, now seen as {s}={s}", + .{ orig, gop.value_ptr.toSlice(coff), orig, alt }, + ); + } 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:")) 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; + } + } + + 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 + _ = lib_name; + } else if (std.ascii.startsWithIgnoreCase(arg, "/defaultlib:")) { + const lib_path = arg["/defaultlib:".len..]; + const trim = std.mem.trim(u8, lib_path, "\""); + if (lib_path.len == trim.len or lib_path.len - 2 == trim.len) { + if (!comp.config.link_libc or comp.libc_installation == null) + return diags.failParse(path, "encountered /DEFAULTLIB .drectve argument when libc was not available: {s}", .{arg}); + + (try coff.pending_default_libs.addOne(gpa)).* = .{ + .path = try gpa.dupe(u8, lib_path), + .ioi = ioi, + }; + } else return diags.failParse( + path, + "malformed /DEFAULTLIB .drectve argument: `{s}`", + .{arg}, + ); + } else return diags.failParse(path, "unsupported argument in .drectve section: `{s}`", .{arg}); + } + } + + section.comdat_result = .skip; + continue; + } + + if (section.header.flags.LNK_REMOVE or + section.header.flags.MEM_DISCARDABLE) + { + // TODO: Convert .debug$* sections into PDB + section.comdat_result = .skip; + continue; + } + + section.comdat_result = comdat: switch (section.comdat) { + .NONE => .include, + .ASSOCIATIVE => { + // Associative COMDAT sections have no COMDAT symbol. + // They are linked if the assocated section is linked. + var iter = section; + var iter_sn = iter.comdat_association; + while (iter.comdat == .ASSOCIATIVE) { + iter = &sections[iter_sn.toIndex()]; + iter_sn = iter.comdat_association; + if (iter == section) + return diags.failParse( + path, + "circular COMDAT association loop detected, starting at symbol 0x{x}", + .{pending_symbols.keys()[section.psi.unwrap().?]}, + ); + } + + assert(iter != section); + break :comdat switch (iter.comdat_result) { + .pending => .{ .pending_association = iter_sn }, + else => |iter_result| iter_result, + }; + }, + else => |comdat| { + const psi = section.comdat_psi.unwrap() orelse section.psi.unwrap().?; + const symbol = &pending_symbols.values()[psi]; + const si = existing: switch (symbol.value) { + .weak_external => unreachable, + .weak_external_aux => unreachable, + .static => break :comdat .include, + .section => { + assert(section.comdat_psi == .none); + if (coff.object_section_table.get(section.name)) |si| + break :existing si + else if (coff.pseudo_section_table.get(section.name)) |si| + break :existing si + else if (coff.section_table.get(section.name)) |s| + break :existing s.si + else + break :comdat .include; + }, + .external => { + const global_gop = try coff.getOrPutGlobalSymbol(.{ + .name = symbol.name.toSlice(coff), + }); + + // TODO: What if the same symbol is incorrectly defined twice in this obj? + // Would need to mark this global as pending, or notice it later when .ni != none + if (!global_gop.found_existing or global_gop.value_ptr.si.get(coff).ni == .none) { + symbol.si = global_gop.value_ptr.si; + break :comdat .include; + } + + break :existing global_gop.value_ptr.si; + }, + }; + + const index = pending_symbols.keys()[psi]; + switch (comdat) { + .NODUPLICATES => return coff.failMultipleDefinitions( + path, + member_name, + symbol.name, + index, + si, + .duplicate, + ), + .ANY => { + symbol.si = si; + break :comdat .skip; + }, + .SAME_SIZE => { + // TODO: Verify that this node isn't resized after creation + _, const size = si.get(coff).ni.location(&coff.mf).resolve(&coff.mf); + if (size == section.header.size_of_raw_data) { + symbol.si = si; + break :comdat .skip; + } + + return coff.failMultipleDefinitions( + path, + member_name, + symbol.name, + index, + si, + .{ .size = .{ .a = size, .b = section.header.size_of_raw_data } }, + ); + }, + .EXACT_MATCH => { + const sym = si.get(coff); + const existing_crc = switch (coff.getNode(sym.ni)) { + .input_section => |isi| isi.inputSection(coff).crc, + else => std.hash.crc.Crc32Jamcrc.hash(sym.ni.sliceConst(&coff.mf)), + }; + + if (existing_crc == section.comdat_crc) { + symbol.si = si; + break :comdat .skip; + } + + return coff.failMultipleDefinitions( + path, + member_name, + symbol.name, + index, + si, + .{ .crc = .{ .a = existing_crc, .b = section.comdat_crc } }, + ); + }, + .LARGEST => { + // TODO: Resize existing .ni and replace with this section's contents + // TODO: This will be tricky, what to do about existing InputSection? + unreachable; + }, + .NONE, .ASSOCIATIVE, _ => unreachable, + } + }, + }; + } + + try coff.flushSectionMerges(); + + // Resolve pending associations, create parent sections + var num_included_sections: u16 = 0; + var num_included_relocs: u32 = 0; + for (sections) |*section| { + comdat: switch (section.comdat_result) { + .pending_association => |root_assoc_sn| { + const root_result = sections[root_assoc_sn.toIndex()].comdat_result; + assert(root_result != .pending_association); + section.comdat_result = root_result; + continue :comdat root_result; + }, + .include => {}, + .skip => { + assert(switch (section.comdat) { + .NONE, .ASSOCIATIVE => true, + else => if (section.comdat_psi.unwrap()) |psi| + pending_symbols.values()[psi].si != .null + else + pending_symbols.values()[section.psi.unwrap().?].si != .null, + }); + continue; + }, + .pending => unreachable, + } + + // 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; + + section.parent_si = (try coff.objectSectionMapIndex( + section.name, + section.header.flags.ALIGN.alignment() orelse .@"1", + .fromFlags(section.header.flags), + )).symbol(coff); + } + + try coff.nodes.ensureUnusedCapacity(gpa, num_included_sections); + try coff.relocs.ensureUnusedCapacity(gpa, num_included_relocs); + try coff.symbols.ensureUnusedCapacity(gpa, num_included_symbols + num_included_sections); + try coff.input_sections.ensureUnusedCapacity(gpa, num_included_sections); + + for (sections) |*section| { + 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, + .alignment = section.header.flags.ALIGN.alignment() orelse .@"1", + .moved = true, + }); + coff.nodes.appendAssumeCapacity(.{ .input_section = @enumFromInt(coff.input_sections.items.len) }); + + section.si = coff.addSymbolAssumeCapacity(); + if (section.psi.unwrap()) |psi| + pending_symbols.values()[psi].si = section.si; + + const sym = section.si.get(coff); + sym.ni = ni; + sym.section_number = section.parent_si.get(coff).section_number; + + coff.input_sections.addOneAssumeCapacity().* = .{ + .ioi = ioi, + .si = section.si, + .file_location = .{ + .offset = fl.offset + section.header.pointer_to_raw_data, + .size = section.header.size_of_raw_data, + }, + .first_li = @enumFromInt(coff.input_symbols.items.len), + .crc = section.comdat_crc, + .comdat_si = if (section.comdat_psi.unwrap()) |psi| + pending_symbols.values()[psi].si + else + .null, + }; + + log.debug( + "addInputSection({s}, 0x{x}) = {d}@{d}", + .{ section.name.toSlice(coff), section.comdat_crc, section.si, sym.section_number }, + ); + coff.synth_prog_node.increaseEstimatedTotalItems(1); + } + + for (pending_symbols.values(), pending_symbols.keys(), 0..) |*symbol, index, i| { + switch (symbol.value) { + .weak_external_aux => continue, + else => {}, + } + + defer log.debug("addInputSymbol({s}, 0x{x}@{d}, {t}=0x{x}) = n{d} {d}@{d}", .{ + symbol.name.toSlice(coff), + index, + symbol.section_number, + symbol.value, + switch (symbol.value) { + .weak_external_aux => unreachable, + inline else => |v| v, + }, + symbol.si.get(coff).ni, + symbol.si, + symbol.si.get(coff).section_number, + }); + + const section = switch (symbol.section_number) { + .UNDEFINED => switch (symbol.value) { + .section, + .static, + .weak_external_aux, + => unreachable, + .external => { + if (symbol.weak_external_psi.unwrap()) |weak_external_i| { + // If the alias itself is an undef external, we need to wait until flushing the weak + // external global before creating a global for the alias, as another input could + // still provide the weak external. + const weak_sym = pending_symbols.values()[weak_external_i].si.get(coff); + weak_sym.setValue(.{ .weak_alias_name = symbol.name }); + weak_sym.flags.weak_external_strat = pending_symbols.values()[weak_external_i + 1].value.weak_external_aux; + } + + // Deferred until referenced by a reloc in this object. + // vcruntime.lib defines symbols like this (ie. memcpy_$fo$) that are not referenced + continue; + }, + .weak_external => |alias_index| { + const global_gop = try coff.getOrPutGlobalSymbol(.{ .name = symbol.name.toSlice(coff) }); + symbol.si = global_gop.value_ptr.si; + if (!global_gop.found_existing or symbol.si.get(coff).ni == .none) { + const sym = symbol.si.get(coff); + const alias = pending_symbols.getPtr(alias_index) orelse + return diags.failParse( + path, + "weak external 0x{x} {s}{f} targets unknown symbol index 0x{x}", + .{ + index, + symbol.name.toSlice(coff), + fmtMemberNameString(member_name), + alias_index, + }, + ); + + if (alias.si == .null and alias_index > index) { + // Resolve this once we see alias + alias.weak_external_psi = .wrap(@intCast(i)); + } else { + sym.setValue(if (alias.si.unwrap()) |alias_si| .{ + .weak_alias_si = alias_si, + } else .{ + .weak_alias_name = alias.name, + }); + sym.flags.weak_external_strat = pending_symbols.values()[i + 1].value.weak_external_aux; + } + } + + continue; + }, + }, + .ABSOLUTE => { + const value = sym: switch (symbol.value) { + .static => |value| { + symbol.si = coff.addSymbolAssumeCapacity(); + break :sym value; + }, + .external => |value| { + const global_gop = try coff.getOrPutGlobalSymbol(.{ .name = symbol.name.toSlice(coff) }); + symbol.si = global_gop.value_ptr.si; + if (global_gop.found_existing) + return coff.failMultipleDefinitions( + path, + member_name, + symbol.name, + index, + global_gop.value_ptr.si, + .none, + ); + break :sym value; + }, + else => unreachable, + }; + + const sym = symbol.si.get(coff); + sym.rva = value; + sym.section_number = .ABSOLUTE; + continue; + }, + .DEBUG => continue, + else => |sn| &sections[sn.toIndex()], + }; + + if (section.si == .null) + continue; + + if (symbol.si == .null) { + switch (symbol.value) { + .section => unreachable, + .static => { + symbol.si = coff.addSymbolAssumeCapacity(); + }, + .external => { + assert(index != section.comdat_psi.unwrap()); + const global_gop = try coff.getOrPutGlobalSymbol(.{ .name = symbol.name.toSlice(coff) }); + symbol.si = global_gop.value_ptr.si; + + const sym = symbol.si.get(coff); + if (global_gop.found_existing and sym.ni != .none) + return coff.failMultipleDefinitions( + path, + member_name, + symbol.name, + index, + global_gop.value_ptr.si, + .none, + ); + }, + .weak_external, + .weak_external_aux, + => unreachable, + } + + if (section.comdat_psi.unwrap() == @as(u32, @intCast(i))) + coff.getNode(section.si.get(coff).ni).input_section.inputSection(coff).comdat_si = symbol.si; + } + + if (symbol.weak_external_psi.unwrap()) |weak_external_i| { + assert(symbol.si != .null); + const weak_sym = pending_symbols.values()[weak_external_i].si.get(coff); + weak_sym.setValue(.{ .weak_alias_si = symbol.si }); + weak_sym.flags.weak_external_strat = pending_symbols.values()[weak_external_i + 1].value.weak_external_aux; + } + + if (section.si != symbol.si) { + const sym = symbol.si.get(coff); + assert(sym.ni == .none); + sym.ni = section.si.get(coff).ni; + switch (symbol.value) { + .section => |v| sym.setExtra(.{ .size = v }), + .static => |v| sym.setValue(.{ .node_offset = v }), + .external => |v| switch (symbol.section_number) { + .UNDEFINED, .ABSOLUTE, .DEBUG => unreachable, + else => sym.setValue(.{ .node_offset = v }), + }, + .weak_external, + .weak_external_aux, + => unreachable, + } + + sym.section_number = section.si.get(coff).section_number; + } + } + + const relocation_size = std.coff.Relocation.sizeOf(); + for (sections) |section| { + if (section.si == .null) continue; + + const loc_sym = section.si.get(coff); + assert(loc_sym.loc_relocs == .none); + loc_sym.loc_relocs = @enumFromInt(coff.relocs.items.len); + + if (section.header.number_of_relocations == 0) continue; + + try fr.seekTo(fl.offset + section.header.pointer_to_relocations); + for (0..section.header.number_of_relocations) |reloc_i| { + var reloc: std.coff.Relocation = undefined; + @memcpy(std.mem.asBytes(&reloc)[0..relocation_size], try r.take(relocation_size)); + if (target_endian != native_endian) + std.mem.byteSwapAllFields(std.coff.Relocation, &reloc); + + const symbol = pending_symbols.getPtr(reloc.symbol_table_index) orelse + return diags.failParse( + path, + "relocation 0x{x} in section '{s}' of {f}{f} targets invalid symbol index 0x{x}", + .{ + reloc_i, + section.name.toSlice(coff), + path.fmtEscapeString(), + fmtMemberNameString(member_name), + reloc.symbol_table_index, + }, + ); + + if (symbol.si == .null) { + assert(symbol.section_number == .UNDEFINED); + switch (symbol.value) { + .external => |size| { + const global_gop = try coff.getOrPutGlobalSymbol(.{ .name = symbol.name.toSlice(coff) }); + symbol.si = global_gop.value_ptr.si; + if (!global_gop.found_existing or symbol.si.get(coff).ni == .none) { + const sym = symbol.si.get(coff); + sym.setExtra(.{ .size = @max(sym.size(), size) }); + } + }, + else => unreachable, + } + } + + assert(symbol.si != .null); + try coff.addReloc( + section.si, + reloc.virtual_address - section.header.virtual_address, + symbol.si, + .pending, + @bitCast(reloc.type), + ); + } + } + + // Set up contiguous symbol ranges in `input_symbols` for both symbols we just created, + // and symbols that were previously created as undefined, but we just defined. + const SortContext = struct { + v: []const PendingSymbol, + + pub fn lessThan(ctx: @This(), a_index: usize, b_index: usize) bool { + const lhs = &ctx.v[a_index]; + const rhs = &ctx.v[b_index]; + if (lhs.section_number == rhs.section_number) + return @intFromEnum(lhs.si) < @intFromEnum(rhs.si); + return @intFromEnum(lhs.section_number) < @intFromEnum(rhs.section_number); + } + }; + + pending_symbols.sortUnstable(SortContext{ .v = pending_symbols.values() }); + + try coff.input_symbols.ensureUnusedCapacity(gpa, num_included_symbols + num_included_sections); + var prev_sn: Symbol.SectionNumber = .DEBUG; + var include_section = false; + for (pending_symbols.values()) |symbol| { + // The symbol may have not been included, or it's an undefined external / aux + if (symbol.si == .null or symbol.si.get(coff).ni == .none) continue; + + if (prev_sn != symbol.section_number) { + prev_sn = symbol.section_number; + if (symbol.section_number.hasIndex()) { + const section = &sections[symbol.section_number.toIndex()]; + include_section = section.comdat_result == .include; + if (include_section) { + const isi = coff.getNode(section.si.get(coff).ni).input_section; + isi.inputSection(coff).first_li = @enumFromInt(coff.input_symbols.items.len); + } + } + } + + if (include_section) { + assert(coff.getNode(symbol.si.get(coff).ni) == .input_section); + symbol.si.get(coff).setExtra(.{ .isli = @enumFromInt(coff.input_symbols.items.len) }); + coff.input_symbols.addOneAssumeCapacity().* = .{ + .si = symbol.si, + .name = symbol.name, + }; + } + } +} + +fn failMultipleDefinitions( + coff: *Coff, + path: std.Build.Cache.Path, + member_name: ?[]const u8, + name: String, + index: u32, + existing_si: Symbol.Index, + comdat_reason: union(enum) { + none: void, + duplicate: void, + size: struct { a: u64, b: u64 }, + crc: struct { a: u32, b: u32 }, + }, +) error{ AlreadyReported, OutOfMemory } { + const num_notes: usize = 2 + @as(usize, @intFromBool(comdat_reason != .none)); + var err = try coff.base.comp.link_diags.addErrorWithNotes(num_notes); + try err.addMsg("multiple definitions of '{s}'", .{name.toSlice(coff)}); + + switch (coff.getNode(existing_si.get(coff).ni)) { + .input_section => |isi| { + const other_ioi = isi.input(coff); + err.addNote("first seen in input '{f}{f}'", .{ + other_ioi.path(coff).fmtEscapeString(), + fmtMemberNameString(other_ioi.memberName(coff)), + }); + }, + .nav, .uav => err.addNote("first seen in module '{s}'", .{ + coff.base.comp.zcu.?.root_mod.fully_qualified_name, + }), + else => unreachable, + } + + err.addNote("defined again in input '{f}{f}' (0x{x}))", .{ path, fmtMemberNameString(member_name), index }); + switch (comdat_reason) { + .none => {}, + .duplicate => err.addNote("COMDAT rule requires no duplicates", .{}), + .size => |s| err.addNote( + "COMDAT rule require duplicates to have the same size ({d} vs {d})", + .{ s.a, s.b }, + ), + .crc => |s| err.addNote( + "COMDAT rule require duplicates to have the same CRC (0x{x} vs 0x{x})", + .{ s.a, s.b }, + ), + } + + return error.AlreadyReported; +} + +const ArchiveMemberHeader = struct { + name: []const u8, + size: u34, +}; + +/// Return value lifetime is that of `header` +fn parseArchiveMemberHeader( + diags: *link.Diags, + path: std.Build.Cache.Path, + header: *const std.coff.ArchiveMemberHeader, + opt_longnames: ?[]const u8, +) !ArchiveMemberHeader { + return parseArchiveMemberHeaderInner(header, opt_longnames) catch |err| switch (err) { + error.BadName => return diags.failParse(path, "malformed member name: '{s}'", .{&header.name}), + error.BadSize => return diags.failParse(path, "malformed member size: '{s}'", .{&header.size}), + error.BadEndOfHeader => return diags.failParse(path, "end of header was invalid", .{}), + error.NoLongNames => return diags.failParse(path, "long name used without longnames member", .{}), + }; +} + +fn parseArchiveMemberHeaderInner( + header: *const std.coff.ArchiveMemberHeader, + opt_longnames: ?[]const u8, +) !ArchiveMemberHeader { + const name = try header.parseName(opt_longnames); + const size = header.parseSize() catch return error.BadSize; + + if (!std.mem.eql(u8, &header.end_of_header, std.coff.archive_end_of_header)) + return error.BadEndOfHeader; + + return .{ + .name = name, + .size = size, + }; +} + +fn loadArchive(coff: *Coff, path: std.Build.Cache.Path, fr: *Io.File.Reader) LoadInputError!void { + const comp = coff.base.comp; + const gpa = comp.gpa; + const diags = &comp.link_diags; + const r = &fr.interface; + const target_endian = coff.targetEndian(); + + log.debug("loadArchive({f})", .{path.fmtEscapeString()}); + + const signature = try r.take(std.coff.archive_signature.len); + if (!std.mem.eql(u8, signature, std.coff.archive_signature)) + return diags.failParse(path, "bad signature", .{}); + + var opt_expected_kind: ?std.coff.ArchiveMemberHeader.Kind = .first_linker; + var opt_longnames: ?[]const u8 = null; + defer if (opt_longnames) |l| gpa.free(l); + + var members: std.ArrayList(struct { + offset: u32, + iami: ?InputArchive.Member.Index, + }) = .empty; + var symbol_member_indices: std.ArrayList(u32) = .empty; + + const iai: InputArchive.Index = @enumFromInt(coff.input_archives.items.len); + (try coff.input_archives.addOne(gpa)).* = .{ + .path = path, + }; + + const first_iami = coff.input_archive_members.items.len; + const first_iamsi = coff.input_archive_symbols.items.len; + const first_symbol_indices_index = coff.input_archive_symbol_indices.count(); + + errdefer { + for (coff.input_archive_symbol_indices.values()) |*v| { + if (@intFromEnum(v.last) < first_iamsi) continue; + if (@intFromEnum(v.first) >= first_iamsi) continue; + + var iter = v.first; + v.last = while (iter != v.last) { + const sym = &coff.input_archive_symbols.items[@intFromEnum(iter)]; + if (@intFromEnum(sym.next) >= first_iamsi) { + sym.next = iter; + break iter; + } + + iter = sym.next; + } else unreachable; + } + + // New entries in this map will only have pointed to iamsi we also just added + coff.input_archive_symbol_indices.shrinkRetainingCapacity(first_symbol_indices_index); + coff.input_archive_symbols.shrinkRetainingCapacity(first_iamsi); + coff.input_archive_members.shrinkRetainingCapacity(first_iami); + _ = coff.input_archives.pop(); + } + + var pos = fr.logicalPos(); + const size = try fr.getSize(); + while (pos < size) : (pos = fr.logicalPos()) { + if ((pos & 1) != 0) try r.discardAll(1); + const header = try r.takeStruct(std.coff.ArchiveMemberHeader, target_endian); + const res = try parseArchiveMemberHeader(diags, path, &header, opt_longnames); + + const member_end = fr.logicalPos() + res.size; + if (member_end > size) + return diags.failParse(path, "out-of-bounds length 0x{x} in member '{s}'", .{ res.size, res.name }); + + log.debug("loadArchiveMember({s})", .{res.name}); + + if (opt_expected_kind) |expected_kind| switch (expected_kind) { + .first_linker => { + if (!std.mem.eql(u8, res.name, "/")) + return diags.failParse(path, "expected first linker member, found '{s}'", .{res.name}); + + try fr.seekTo(fr.logicalPos() + res.size); + opt_expected_kind = .second_linker; + continue; + }, + .second_linker => { + if (!std.mem.eql(u8, res.name, "/")) + return diags.failParse(path, "expected second linker member, found '{s}'", .{res.name}); + + const num_members = try r.takeInt(u32, target_endian); + pos = fr.logicalPos(); + if (pos + num_members * @sizeOf(u32) > member_end) + return diags.failParse(path, "invalid member count 0x{x} in second linker member", .{num_members}); + + try members.ensureTotalCapacity(gpa, num_members); + for (0..num_members) |_| + members.addOneAssumeCapacity().* = .{ + .offset = try r.takeInt(u32, target_endian), + .iami = null, + }; + + const num_symbols = try r.takeInt(u32, target_endian); + pos = fr.logicalPos(); + if (pos + num_symbols * @sizeOf(u16) > member_end) + return diags.failParse(path, "invalid symbol count 0x{x} in second linker member", .{num_symbols}); + + try symbol_member_indices.ensureTotalCapacity(gpa, num_symbols); + for (0..num_symbols) |_| + symbol_member_indices.addOneAssumeCapacity().* = (try r.takeInt(u16, target_endian)) - 1; + + pos = fr.logicalPos(); + try coff.ensureManyUnusedStringCapacity(num_symbols, @intCast(member_end - pos)); + try coff.input_archive_members.ensureUnusedCapacity(gpa, num_members); + try coff.input_archive_symbols.ensureUnusedCapacity(gpa, num_symbols); + try coff.input_archive_symbol_indices.ensureUnusedCapacity(gpa, num_symbols); + + var symbol_i: u32 = 0; + while (pos < member_end and symbol_i < num_symbols) : ({ + pos = fr.logicalPos(); + symbol_i += 1; + }) { + const name = if (r.takeDelimiter(0) catch |err| switch (err) { + error.StreamTooLong => null, + else => |e| return e, + }) |n| n else return diags.failParse(path, "unterminated string found in second linker member", .{}); + + const string = coff.getOrPutStringAssumeCapacity(name); + const iamsi: InputArchive.Member.Symbol.Index = @enumFromInt(coff.input_archive_symbols.items.len); + const symbol_gop = coff.input_archive_symbol_indices.getOrPutAssumeCapacity(string); + if (!symbol_gop.found_existing) { + symbol_gop.value_ptr.* = .{ + .first = iamsi, + .last = iamsi, + }; + } else { + coff.input_archive_symbols.items[@intFromEnum(symbol_gop.value_ptr.last)].next = iamsi; + symbol_gop.value_ptr.last = iamsi; + } + + const iami = members.items[symbol_member_indices.items[symbol_i]].iami orelse iami: { + const iami: InputArchive.Member.Index = @enumFromInt(coff.input_archive_members.items.len); + const member_offset = members.items[symbol_member_indices.items[symbol_i]].offset; + coff.input_archive_members.addOneAssumeCapacity().* = .{ + .iai = iai, + .name = undefined, + .content = .{ + .object = .{ + .offset = member_offset, + .size = undefined, + }, + }, + .flags = .{ + .is_loaded = false, + }, + }; + + members.items[symbol_member_indices.items[symbol_i]].iami = iami; + break :iami iami; + }; + + log.debug("loadArchiveMemberSymbol({s}) = ({d}, {d}, {d})", .{ name, iai, iami, iamsi }); + + coff.input_archive_symbols.addOneAssumeCapacity().* = .{ + .iami = iami, + .next = iamsi, + }; + } + + if (symbol_i != num_symbols) + return diags.failParse( + path, + " expected {d} entries in second linker member string table, but found {d}", + .{ num_symbols, symbol_i }, + ); + + try fr.seekTo(member_end); + opt_expected_kind = .longnames; + continue; + }, + .longnames => { + // This member is optional + if (std.mem.eql(u8, res.name, "//")) + opt_longnames = try r.readAlloc(gpa, @intCast(res.size)); + + opt_expected_kind = null; + break; + }, + else => unreachable, + }; + } + + if (opt_expected_kind) |expected_kind| switch (expected_kind) { + .first_linker => return diags.failParse(path, "missing first linker member", .{}), + .second_linker => return diags.failParse(path, "missing second linker member", .{}), + else => {}, + }; + + // Validate / read names and sizes of all the referenced members, enumerate imports + for (coff.input_archive_members.items[first_iami..]) |*member| { + try fr.seekTo(member.content.object.offset); + + const header = try r.takeStruct(std.coff.ArchiveMemberHeader, target_endian); + const res = try parseArchiveMemberHeader(diags, path, &header, opt_longnames); + + try coff.ensureUnusedStringCapacity(res.name.len); + member.name = coff.getOrPutStringAssumeCapacity(res.name); + + const member_sig = try r.peek(4); + const machine: std.coff.IMAGE.FILE.MACHINE = + @enumFromInt(std.mem.readInt(u16, member_sig[0..2], target_endian)); + const sig = std.mem.readInt(u16, member_sig[2..4], target_endian); + + log.debug("verifyArchiveMember({s}) = 0x{x}+{x}", .{ + res.name, + member.content.object.offset, + res.size, + }); + + const expected_machine = comp.root_mod.resolved_target.result.toCoffMachine(); + if (machine == std.coff.IMAGE.FILE.MACHINE.UNKNOWN and sig == 0xffff) { + const import_header = try r.takeStruct(std.coff.ImportHeader, target_endian); + const strings = r.take(import_header.size_of_data) catch |err| switch (err) { + error.EndOfStream => return diags.failParse(path, "invalid data size in import header '{s}'", .{res.name}), + else => |e| return e, + }; + + var split = std.mem.splitScalar(u8, strings, 0); + const symbol_name = split.next() orelse + return diags.failParse(path, "invalid symbol name string in import header '{s}'", .{res.name}); + var lib_name = split.next() orelse + return diags.failParse(path, "invalid dll name string in import header '{s}' ('{s}')", .{ res.name, symbol_name }); + + if (import_header.machine != expected_machine) + return diags.failParse(path, "machine mismatch in import header '{s}' ('{s}'): expected {t}, found {t}", .{ + res.name, + symbol_name, + expected_machine, + machine, + }); + + const ext = ".dll"; + if (!std.mem.endsWith(u8, lib_name, ext)) + return diags.failParse( + path, + "unexpected extension for import '{s} ('{s}'): '{s}'", + .{ res.name, symbol_name, lib_name }, + ); + + lib_name = lib_name[0 .. lib_name.len - ext.len]; + log.debug("verifyArchiveImportHeader({s}, {s}, {s}) = {t} ({t})", .{ + res.name, + symbol_name, + lib_name, + import_header.types.type, + import_header.types.name_type, + }); + + try coff.ensureManyUnusedStringCapacity(2, strings.len - ext.len); + member.content = .{ + .import = .{ + .symbol_name = coff.getOrPutStringAssumeCapacity(symbol_name), + .lib_name = coff.getOrPutStringAssumeCapacity(lib_name), + .import_ordinal_hint = import_header.hint, + .type = import_header.types.type, + .name_type = import_header.types.name_type, + }, + }; + } else { + member.content.object.size = res.size; + // Microsoft's CRT contains members that set .UNKNOWN but do have undef symbols + if (machine != expected_machine and machine != .UNKNOWN) { + return diags.failParse(path, "machine mismatch in member header '{s}': expected {t}, found {t}", .{ + res.name, + expected_machine, + machine, + }); + } + } + } +} + +fn loadRes(coff: *Coff, path: std.Build.Cache.Path, fr: *Io.File.Reader) LoadInputError!void { + const comp = coff.base.comp; + const gpa = comp.gpa; + const diags = &comp.link_diags; + const r = &fr.interface; + + log.debug("loadRes({f})", .{path.fmtEscapeString()}); + + _ = gpa; + _ = diags; + _ = r; +} + +fn loadDll(coff: *Coff, path: std.Build.Cache.Path, fr: *Io.File.Reader) LoadInputError!void { + const comp = coff.base.comp; + const gpa = comp.gpa; + const diags = &comp.link_diags; + const r = &fr.interface; + + log.debug("loadDll({f})", .{path.fmtEscapeString()}); + + _ = gpa; + _ = diags; + _ = r; +} + pub fn prelink(coff: *Coff, prog_node: std.Progress.Node) link.Error!void { - _ = coff; _ = prog_node; + const base = coff.base; + const comp = base.comp; + + log.debug("prelink()", .{}); + + if (coff.pending_default_libs.items.len > 0) { + // Libs provided by /DEFAULTLIB arguments in objects are searched after all other inputs + const gpa = comp.gpa; + const arena = comp.arena; + const target = &comp.root_mod.resolved_target.result; + + defer { + for (coff.pending_default_libs.items) |l| gpa.free(l.path); + coff.pending_default_libs.clearAndFree(gpa); + } + + assert(comp.config.link_libc); + const libc_installation = comp.libc_installation.?; + const all_paths: [3]?[]const u8 = .{ + libc_installation.crt_dir, + libc_installation.msvc_lib_dir, + libc_installation.kernel32_lib_dir, + }; + const search_paths = all_paths[0..if (target.abi == .msvc or target.abi == .itanium) 3 else 1]; + lib: for (coff.pending_default_libs.items) |lib| { + if (!std.mem.eql(u8, std.fs.path.extension(lib.path), ".lib")) + return comp.link_diags.failParse( + lib.ioi.path(coff), + "/DEFAULTLIB library '{s}' had unexpected extension", + .{lib.path}, + ); + + log.debug("loadDefaultLib({s}, {f})", .{ lib.path, lib.ioi.path(coff) }); + for (search_paths) |opt_path| if (opt_path) |search_path| { + const lib_path = try Path.initCwd(search_path).join(arena, lib.path); + const archive = link.openObject(comp.io, lib_path, false, false) catch |err| switch (err) { + error.FileNotFound => { + arena.free(lib_path.sub_path); + continue; + }, + else => |e| return comp.link_diags.failParse( + lib.ioi.path(coff), + "error opening /DEFAULTLIB library '{s}': {t}", + .{ lib.path, e }, + ), + }; + errdefer archive.file.close(comp.io); + + coff.loadInput(.{ .archive = archive }) catch |err| switch (err) { + else => |e| return comp.link_diags.failParse( + lib.ioi.path(coff), + "error loading /DEFAULTLIB library '{s}': {t}", + .{ lib.path, e }, + ), + }; + + break :lib; + }; + + return comp.link_diags.failParse( + lib.ioi.path(coff), + "/DEFAULTLIB library '{s}' was not found", + .{lib.path}, + ); + } + } + + coff.inputs_complete = true; + if (comp.zcu == null) + coff.exports_complete = true; } -pub fn updateNav(coff: *Coff, pt: Zcu.PerThread, nav_index: InternPool.Nav.Index) !void { +pub fn updateNav(coff: *Coff, pt: Zcu.PerThread, nav_index: InternPool.Nav.Index) link.Error!void { coff.updateNavInner(pt, nav_index) catch |err| switch (err) { + error.MappedFileIo => return coff.base.cgFail( + nav_index, + "linker failed to update variable: {t}", + .{coff.mf.io_err.?}, + ), else => |e| return e, - error.MappedFileIo => return coff.base.cgFail(nav_index, "linker failed to update variable: {t}", .{coff.mf.io_err.?}), }; } fn updateNavInner(coff: *Coff, pt: Zcu.PerThread, nav_index: InternPool.Nav.Index) !void { @@ -1546,11 +5462,13 @@ fn updateNavInner(coff: *Coff, pt: Zcu.PerThread, nav_index: InternPool.Nav.Inde const nmi = try coff.navMapIndex(zcu, nav_index); const si = nmi.symbol(coff); + log.debug("updateNav({f}) = {d}", .{ nav.fqn.fmt(ip), si }); const ni = ni: { switch (si.get(coff).ni) { .none => { const sec_si = try coff.navSection(zcu, nav.resolved.?); try coff.nodes.ensureUnusedCapacity(gpa, 1); + if (!isImage(coff)) try coff.symbol_table.symbols.ensureUnusedCapacity(gpa, 1); const ni = try coff.mf.addLastChildNode(gpa, sec_si.node(coff), .{ .alignment = zcu.navAlignment(nav_index).toStdMem(), .moved = true, @@ -1565,6 +5483,9 @@ fn updateNavInner(coff: *Coff, pt: Zcu.PerThread, nav_index: InternPool.Nav.Inde const sym = si.get(coff); assert(sym.loc_relocs == .none); sym.loc_relocs = @enumFromInt(coff.relocs.items.len); + if (!isImage(coff) and sym.target_relocs != .none) + try coff.pendingSymbolTableEntry(si); + break :ni sym.ni; }; @@ -1582,12 +5503,12 @@ fn updateNavInner(coff: *Coff, pt: Zcu.PerThread, nav_index: InternPool.Nav.Inde error.WriteFailed => return nw.err.?, else => |e| return e, }; - si.get(coff).size = @intCast(nw.interface.end); - si.applyLocationRelocs(coff); + si.get(coff).extra.size = @intCast(nw.interface.end); + try si.applyLocationRelocs(coff); } if (nav.resolved.?.@"linksection".unwrap()) |_| { - try ni.resize(&coff.mf, gpa, si.get(coff).size); + try ni.resize(&coff.mf, gpa, si.get(coff).extra.size); var parent_ni = ni; while (true) { parent_ni = parent_ni.parent(&coff.mf); @@ -1610,7 +5531,7 @@ pub fn lowerUav( pt: Zcu.PerThread, uav_val: InternPool.Index, uav_align: InternPool.Alignment, -) !link.File.SymbolId { +) link.Error!link.File.SymbolId { const zcu = pt.zcu; const gpa = zcu.gpa; @@ -1639,7 +5560,7 @@ pub fn updateFunc( pt: Zcu.PerThread, func_index: InternPool.Index, mir: *const codegen.AnyMir, -) !void { +) link.Error!void { coff.updateFuncInner(pt, func_index, mir) catch |err| switch (err) { else => |e| return e, error.MappedFileIo => return coff.base.cgFail( @@ -1669,6 +5590,7 @@ fn updateFuncInner( .none => { const sec_si = try coff.navSection(zcu, nav.resolved.?); try coff.nodes.ensureUnusedCapacity(gpa, 1); + if (!isImage(coff)) try coff.symbol_table.symbols.ensureUnusedCapacity(gpa, 1); const mod = zcu.navFileScope(func.owner_nav).mod.?; const target = &mod.resolved_target.result; const ni = try coff.mf.addLastChildNode(gpa, sec_si.node(coff), .{ @@ -1694,6 +5616,8 @@ fn updateFuncInner( const sym = si.get(coff); assert(sym.loc_relocs == .none); sym.loc_relocs = @enumFromInt(coff.relocs.items.len); + if (!isImage(coff) and sym.target_relocs != .none) + try coff.pendingSymbolTableEntry(si); break :ni sym.ni; }; @@ -1712,21 +5636,245 @@ fn updateFuncInner( error.WriteFailed => return nw.err.?, else => |e| return e, }; - si.get(coff).size = @intCast(nw.interface.end); - si.applyLocationRelocs(coff); + si.get(coff).extra.size = @intCast(nw.interface.end); + try si.applyLocationRelocs(coff); +} + +pub fn updateErrorData(coff: *Coff, pt: Zcu.PerThread) !void { + coff.flushLazy(pt, .{ + .kind = .const_data, + .index = @intCast(coff.lazy.getPtr(.const_data).map.getIndex(.anyerror_type) orelse return), + }) catch |err| switch (err) { + else => |e| return e, + error.MappedFileIo => return coff.base.comp.link_diags.fail( + "updateErrorData failed: {t}", + .{coff.mf.io_err.?}, + ), + }; +} + +fn flushImplib( + coff: *Coff, + implib_file: []const u8, +) !void { + // Emitting implibs is only valid for images + assert(coff.export_table.ni != .none); + + const comp = coff.base.comp; + const gpa = comp.gpa; + const io = comp.io; + + const image_name = std.mem.sliceTo( + coff.export_table.ni.slice(&coff.mf)[@sizeOf(std.coff.ExportDirectoryTable)..], + 0, + ); + const machine_type = coff.targetLoad(&coff.headerPtr().machine); + const members = members: { + const def_arena: std.heap.ArenaAllocator = .init(gpa); + var def: ModuleDefinition = .{ + .name = image_name, + .arena = def_arena, + .type = .mingw, + }; + defer def.deinit(); + + try def.exports.ensureUnusedCapacity( + def.arena.allocator(), + coff.export_table.entries.count(), + ); + + const name_table_slice = coff.export_table.name_table_ni.slice(&coff.mf); + for (coff.export_table.entries.values(), 0..) |entry, ord| { + const name = name_table_slice[entry.name_index..][0..entry.name_len]; + const section_number = entry.si.get(coff).section_number; + const import_type: std.coff.ImportType = switch (section_number.symbol(coff)) { + .data, .rdata => .DATA, + .text => .CODE, + else => return comp.link_diags.fail( + "unsupported section for export '{s}': {s}", + .{ name, &section_number.header(coff).name }, + ), + }; + + def.exports.appendAssumeCapacity(.{ + .name = name, + .mangled_symbol_name = null, + .ext_name = null, + .import_name = null, + .export_as = null, + .no_name = false, + .ordinal = @intCast(ord), + .type = import_type, + .private = false, + }); + } + + def.fixupForImportLibraryGeneration(machine_type); + break :members try implib.getMembers(gpa, def, machine_type); + }; + defer members.deinit(); + + const lib_sub_path = try std.fs.path.join(gpa, &.{ + std.fs.path.dirname(coff.base.emit.sub_path) orelse "", + implib_file, + }); + defer gpa.free(lib_sub_path); + + const lib_final_file = try coff.base.emit.root_dir.handle.createFile(io, lib_sub_path, .{ .truncate = true }); + defer lib_final_file.close(io); + var buffer: [1024]u8 = undefined; + var file_writer = lib_final_file.writer(io, &buffer); + try implib.writeCoffArchive(gpa, &file_writer.interface, members); + try file_writer.interface.flush(); } -pub fn updateErrorData(coff: *Coff, pt: Zcu.PerThread) !void { - coff.flushLazy(pt, .{ - .kind = .const_data, - .index = @intCast(coff.lazy.getPtr(.const_data).map.getIndex(.anyerror_type) orelse return), - }) catch |err| switch (err) { - else => |e| return e, - error.MappedFileIo => return coff.base.comp.link_diags.fail( - "updateErrorData failed: {t}", - .{coff.mf.io_err.?}, - ), - }; +fn reportUndefs(coff: *Coff, tid: Zcu.PerThread.Id) !void { + const comp = coff.base.comp; + const gpa = comp.gpa; + const max_notes = 4; + + var undef_indices: std.ArrayListUnmanaged(u32) = .empty; + for (coff.relocs.items, 0..) |reloc, reloc_i| { + if (reloc.flags.free) continue; + const target_sym = reloc.target.get(coff); + switch (target_sym.ni) { + .none => { + assert(target_sym.gmi != .none); + if (target_sym.section_number == .ABSOLUTE) continue; + (try undef_indices.addOne(gpa)).* = @intCast(reloc_i); + }, + else => continue, + } + } + + if (undef_indices.items.len == 0) return; + + const undefLessThan = struct { + fn lessThan(ctx: *const Coff, lhs: u32, rhs: u32) bool { + const reloc_l = &ctx.relocs.items[lhs]; + const reloc_r = &ctx.relocs.items[rhs]; + if (reloc_l.target == reloc_r.target) + return @intFromEnum(reloc_l.loc) < @intFromEnum(reloc_r.loc) + else + return @intFromEnum(reloc_l.target) < @intFromEnum(reloc_r.target); + } + }.lessThan; + + std.mem.sortUnstable(u32, undef_indices.items, coff, undefLessThan); + + var start_i: usize = 0; + var num_unique_references: usize = 1; + for (0..undef_indices.items.len) |i| { + const target = coff.relocs.items[undef_indices.items[start_i]].target; + if (i == undef_indices.items.len - 1 or target != coff.relocs.items[undef_indices.items[i + 1]].target) { + defer { + start_i = i + 1; + num_unique_references = 1; + } + + const num_full_notes = @min(max_notes, num_unique_references); + var err = try comp.link_diags.addErrorWithNotes( + num_full_notes + @intFromBool(num_unique_references > max_notes), + ); + const target_sym = target.get(coff); + try err.addMsg("undefined symbol: {s}", .{target_sym.gmi.name(coff).toSlice(coff)}); + + // TODO: If lib_name is set, show the user + + var prev_loc_si: Symbol.Index = .null; + for (undef_indices.items[start_i .. i + 1]) |reference_i| { + if (err.note_slot == num_full_notes) break; + + const reloc = &coff.relocs.items[reference_i]; + const loc_si = reloc.loc; + if (loc_si == prev_loc_si) continue; + defer prev_loc_si = loc_si; + + const loc_sym = loc_si.get(coff); + + // TODO: Make this a helper for anything that needs to report "referenced by" notes + switch (coff.getNode(loc_sym.ni)) { + .data_directories => { + const dir: std.coff.IMAGE.DIRECTORY_ENTRY = + @enumFromInt(reloc.offset / @sizeOf(std.coff.ImageDataDirectory)); + err.addNote("referenced by data directory entry: {t}", .{dir}); + }, + .optional_header => err.addNote("referenced by optional header field", .{}), + .input_section => |isi| { + const other_ioi = isi.input(coff); + if (loc_sym.gmi == .none) { + const section = isi.inputSection(coff); + const section_name = coff.getNode(loc_sym.ni.parent(&coff.mf)) + .object_section.name(coff).toSlice(coff); + + if (section.comdat_si != .null) { + const comdat_sym = section.comdat_si.get(coff); + const comdat_name = if (comdat_sym.gmi != .none) + comdat_sym.gmi.name(coff).toSlice(coff) + else + comdat_sym.extra.isli.name(coff).toSlice(coff); + + err.addNote("referenced by input COMDAT section '{s}={s}' '{f}{f}'", .{ + section_name, + comdat_name, + other_ioi.path(coff).fmtEscapeString(), + fmtMemberNameString(other_ioi.memberName(coff)), + }); + } else { + err.addNote("referenced by input section '{s}' '{f}{f}'", .{ + section_name, + other_ioi.path(coff).fmtEscapeString(), + fmtMemberNameString(other_ioi.memberName(coff)), + }); + } + } else { + err.addNote("referenced by input symbol '{s}' from '{f}{f}'", .{ + loc_sym.gmi.name(coff).toSlice(coff), + other_ioi.path(coff).fmtEscapeString(), + fmtMemberNameString(other_ioi.memberName(coff)), + }); + } + }, + .import_thunk => |gmi| err.addNote("referenced by import thunk for '{s}'", .{ + gmi.name(coff).toSlice(coff), + }), + inline .nav, + .uav, + .lazy_code, + .lazy_const_data, + => |val, tag| { + err.addNote("referenced by '{f}'", .{ + format: switch (tag) { + .nav => { + const ip = &comp.zcu.?.intern_pool; + break :format ip.getNav(val.navIndex(coff)).fqn.fmt(ip); + }, + .uav => Value.fromInterned(val.uavValue(coff)).fmtValue(.{ + .zcu = coff.base.comp.zcu.?, + .tid = tid, + }), + inline .lazy_code, .lazy_const_data => Type.fromInterned(val.lazySymbol(coff).ty).fmt(.{ + .zcu = coff.base.comp.zcu.?, + .tid = tid, + }), + else => unreachable, + }, + }); + }, + else => unreachable, + } + } + + if (num_unique_references > max_notes) + err.addNote("referenced {d} more times", .{num_unique_references - max_notes}); + } else if (i != start_i and + coff.relocs.items[undef_indices.items[i - 1]].loc != coff.relocs.items[undef_indices.items[i]].loc) + { + num_unique_references += 1; + } + } + + return error.AlreadyReported; } pub fn flush( @@ -1734,35 +5882,79 @@ pub fn flush( arena: std.mem.Allocator, tid: Zcu.PerThread.Id, prog_node: std.Progress.Node, -) !void { +) link.Error!void { _ = arena; _ = prog_node; + const comp = coff.base.comp; + + // TODO: When https://github.com/ziglang/zig/issues/23617 is in, + // this should be set after updateExports instead + coff.exports_complete = true; + + while (try coff.resolve(tid)) {} while (try coff.idle(tid)) {} - // hack for stage2_x86_64 + coff - const comp = coff.base.comp; - if (comp.compiler_rt_dyn_lib) |crt_file| { - const gpa = comp.gpa; - const io = comp.io; - const compiler_rt_sub_path = try std.fs.path.join(gpa, &.{ - std.fs.path.dirname(coff.base.emit.sub_path) orelse "", - std.fs.path.basename(crt_file.full_object_path.sub_path), - }); - defer gpa.free(compiler_rt_sub_path); - std.Io.Dir.copyFile( - crt_file.full_object_path.root_dir.handle, - crt_file.full_object_path.sub_path, - coff.base.emit.root_dir.handle, - compiler_rt_sub_path, - io, - .{}, - ) catch |err| return comp.link_diags.fail("copy '{s}' failed: {t}", .{ compiler_rt_sub_path, err }); + // This has to occur after all other flushMoved / flushResized have resolved, + // but it will also generate one more set of resizes and moves. + if (coff.symbol_table.pending_shrink) { + coff.symbol_table.pending_shrink = false; + + const number_of_symbols = coff.targetLoad(&coff.headerPtr().number_of_symbols); + coff.symbol_table.ni.shrink( + &coff.mf, + comp.gpa, + number_of_symbols * std.coff.Symbol.sizeOf(), + true, + ) catch |err| return comp.link_diags.fail( + "linker failed to compact symbol table: {t}", + .{err}, + ); } + while (try coff.idle(tid)) {} + + if (coff.isImage()) + try coff.reportUndefs(tid); + + if (comp.emit_implib) |implib_file| + coff.flushImplib(implib_file) catch |err| + return comp.link_diags.fail("flushing implib '{s}' failed: {t}", .{ implib_file, err }); + + coff.mf.flush() catch |err| switch (err) { + error.Canceled => |e| return e, + else => |e| return comp.link_diags.fail("flush write failed: {t}", .{e}), + }; + + if (coff.options.enable_link_snapshots) + coff.dumpStderr(tid) catch |err| + return comp.link_diags.fail("dumping link snapshot failed: {t}", .{err}); } -pub fn idle(coff: *Coff, tid: Zcu.PerThread.Id) !bool { +/// Runs a single "resolution" task. +/// These are tasks that need to modify the node structure in some way. +/// They must run in a defined order with respect to linker tasks. +fn resolve(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(); @@ -1779,22 +5971,52 @@ pub fn idle(coff: *Coff, tid: Zcu.PerThread.Id) !bool { }; break :task; } - if (coff.global_pending_index < coff.globals.count()) { - const pt: Zcu.PerThread = .{ .zcu = comp.zcu.?, .tid = tid }; - const gmi: Node.GlobalMapIndex = @enumFromInt(coff.global_pending_index); - coff.global_pending_index += 1; + if (coff.pending_input) |pending_iami| { + const name_slice = pending_iami.member(coff).name.toSlice(coff); + const sub_prog_node = coff.input_prog_node.start( + name_slice, + 0, + ); + defer sub_prog_node.end(); + coff.pending_input = null; + coff.flushInputMember(pending_iami) catch |err| switch (err) { + error.OutOfMemory => return error.OutOfMemory, + else => |e| return comp.link_diags.fail( + "linker failed to load archive member '{f}{f}': {t}", + .{ + pending_iami.member(coff).iai.path(coff), + fmtMemberNameString(name_slice), + e, + }, + ), + }; + break :task; + } + if (coff.exports_complete and coff.global_pending_index < coff.globals.count()) { + const gmi: Node.GlobalMapIndex = .wrap(coff.global_pending_index); const sub_prog_node = coff.synth_prog_node.start( - gmi.globalName(coff).name.toSlice(coff), + gmi.name(coff).toSlice(coff), 0, ); defer sub_prog_node.end(); - coff.flushGlobal(pt, gmi) catch |err| switch (err) { + if (coff.flushGlobal(gmi) catch |err| switch (err) { else => |e| return e, error.MappedFileIo => return comp.link_diags.fail( "linker failed to lower constant: {t}", .{coff.mf.io_err.?}, ), - }; + }) coff.global_pending_index += 1; + break :task; + } + if (coff.exports_complete and coff.pending_special_symbol != .none) { + coff.pending_special_symbol = coff.flushSpecialSymbol(coff.pending_special_symbol) catch |err| + switch (err) { + error.OutOfMemory => |e| return e, + else => |e| return comp.link_diags.fail( + "linker failed to flush special symbols: {t}", + .{e}, + ), + }; break :task; } var lazy_it = coff.lazy.iterator(); @@ -1824,6 +6046,73 @@ pub fn idle(coff: *Coff, tid: Zcu.PerThread.Id) !bool { }; break :task; }; + if (coff.symbol_table.pending_symbol_index < coff.symbol_table.symbols.count()) { + defer coff.symbol_table.pending_symbol_index += 1; + const si = coff.symbol_table.symbols.keys()[coff.symbol_table.pending_symbol_index]; + const sym = si.get(coff); + const sub_prog_node = coff.idleProgNode( + tid, + coff.symbol_prog_node, + if (sym.ni != .none) + coff.getNode(sym.ni) + else + .{ .import_thunk = sym.gmi }, + ); + defer sub_prog_node.end(); + coff.flushSymbolTableEntry( + coff.symbol_table.pending_symbol_index, + .{ .zcu = comp.zcu.?, .tid = tid }, + ) catch |err| switch (err) { + error.OutOfMemory => return error.OutOfMemory, + else => |e| return comp.link_diags.fail( + "linker failed to flush symbol table entry: {t}", + .{e}, + ), + }; + 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.exports_complete and coff.globals.count() > coff.global_pending_index) return true; + assert(!coff.exports_complete or coff.inputs_complete); + if (coff.exports_complete and coff.pending_special_symbol != .none) return true; + for (&coff.lazy.values) |lazy| if (lazy.map.count() > lazy.pending_index) return true; + if (coff.symbol_table.pending_symbol_index < coff.symbol_table.symbols.count()) return true; + return false; +} + +pub fn idle(coff: *Coff, tid: Zcu.PerThread.Id) !bool { + // Idle tasks should not modify create / modify nodes, otherwise the output is not reproducible. + coff.mf.nodes_lock.lock(); + defer coff.mf.nodes_lock.unlock(); + + const comp = coff.base.comp; + task: { + // TODO: Idle task for flushing obj into lib + if (coff.input_section_pending_index < coff.input_sections.items.len) { + const isi: Node.InputSection.Index = @enumFromInt(coff.input_section_pending_index); + coff.input_section_pending_index += 1; + const sub_prog_node = coff.idleProgNode(tid, coff.input_prog_node, coff.getNode(isi.symbol(coff).node(coff))); + defer sub_prog_node.end(); + coff.flushInputSection(isi) catch |err| switch (err) { + else => |e| { + const ioi = isi.input(coff); + return comp.link_diags.fail( + "linker failed to read input section '{s}' from \"{f}{f}\": {t}", + .{ + isi.symbol(coff).get(coff).section_number.name(coff).toSlice(coff), + ioi.path(coff).fmtEscapeString(), + fmtMemberNameString(ioi.memberName(coff)), + e, + }, + ); + }, + }; + break :task; + } while (coff.mf.updates.pop()) |ni| { const clean_moved = ni.cleanMoved(&coff.mf); const clean_resized = ni.cleanResized(&coff.mf); @@ -1836,11 +6125,33 @@ pub fn idle(coff: *Coff, tid: Zcu.PerThread.Id) !bool { break :task; } else coff.mf.update_prog_node.completeOne(); } + while (coff.pending_members.pop()) |pending_mi| { + const sub_prog_node = coff.idleProgNode( + tid, + coff.symbol_prog_node, + coff.getNode(pending_mi.key.get(coff).content_ni), + ); + defer sub_prog_node.end(); + try coff.flushMember(pending_mi.key); + break :task; + } + if (coff.exports_complete and coff.export_table.pending_sort) { + defer coff.export_table.pending_sort = false; + const sub_prog_node = coff.idleProgNode( + tid, + coff.synth_prog_node, + coff.getNode(coff.export_table.ni), + ); + defer sub_prog_node.end(); + + coff.flushExportsSort(); + break :task; + } } - if (coff.pending_uavs.count() > 0) return true; - if (coff.globals.count() > coff.global_pending_index) return true; - for (&coff.lazy.values) |lazy| if (lazy.map.count() > lazy.pending_index) return true; + if (coff.input_sections.items.len > coff.input_section_pending_index) return true; if (coff.mf.updates.items.len > 0) return true; + if (coff.pending_members.count() > 0) return true; + if (coff.exports_complete and coff.export_table.pending_sort) return true; return false; } @@ -1855,7 +6166,15 @@ fn idleProgNode( else => |tag| @tagName(tag), .image_section => |si| std.mem.sliceTo(&si.get(coff).section_number.header(coff).name, 0), inline .pseudo_section, .object_section => |smi| smi.name(coff).toSlice(coff), - .global => |gmi| gmi.globalName(coff).name.toSlice(coff), + .input_section => |isi| { + const ioi = isi.input(coff); + break :name std.fmt.bufPrint(&name, "{f}{f} {s}", .{ + ioi.path(coff).fmtEscapeString(), + fmtMemberNameString(ioi.memberName(coff)), + coff.getNode(isi.symbol(coff).node(coff).parent(&coff.mf)).object_section.name(coff).toSlice(coff), + }) catch &name; + }, + .import_thunk => |gmi| gmi.name(coff).toSlice(coff), .nav => |nmi| { const ip = &coff.base.comp.zcu.?.intern_pool; break :name ip.getNav(nmi.navIndex(coff)).fqn.toSlice(ip); @@ -1866,6 +6185,7 @@ fn idleProgNode( .tid = tid, }), }) catch &name, + .archive_member => |mi| &mi.get(coff).headerPtr(coff).name, }, 0); } @@ -1886,9 +6206,10 @@ fn flushUav( const sec_si = (try coff.objectSectionMapIndex( .@".rdata", coff.mf.flags.block_size, - .{ .read = true }, + .{ .read = true, .initialized = true }, )).symbol(coff); try coff.nodes.ensureUnusedCapacity(gpa, 1); + if (!isImage(coff)) try coff.symbol_table.symbols.ensureUnusedCapacity(gpa, 1); const sym = si.get(coff); const ni = try coff.mf.addLastChildNode(gpa, sec_si.node(coff), .{ .alignment = uav_align.toStdMem(), @@ -1907,6 +6228,9 @@ fn flushUav( const sym = si.get(coff); assert(sym.loc_relocs == .none); sym.loc_relocs = @enumFromInt(coff.relocs.items.len); + if (!isImage(coff) and sym.target_relocs != .none) + try coff.pendingSymbolTableEntry(si); + break :ni sym.ni; }; @@ -1923,173 +6247,561 @@ fn flushUav( error.WriteFailed => return nw.err.?, else => |e| return e, }; - si.get(coff).size = @intCast(nw.interface.end); - si.applyLocationRelocs(coff); + si.get(coff).extra.size = @intCast(nw.interface.end); + try si.applyLocationRelocs(coff); } -fn flushGlobal(coff: *Coff, pt: Zcu.PerThread, gmi: Node.GlobalMapIndex) !void { - const zcu = pt.zcu; - const comp = zcu.comp; - const gpa = zcu.gpa; - const gn = gmi.globalName(coff); - if (gn.lib_name.toSlice(coff)) |lib_name| { - const name = gn.name.toSlice(coff); - try coff.nodes.ensureUnusedCapacity(gpa, 4); - try coff.symbol_table.ensureUnusedCapacity(gpa, 1); +fn aliasGlobal(coff: *Coff, gmi: Node.GlobalMapIndex, alias_si: Symbol.Index) !void { + const si = gmi.symbol(coff); + const sym = si.get(coff); + const alias_sym = alias_si.get(coff); + assert(sym.section_number == .UNDEFINED); + assert(sym.loc_relocs == .none); + + log.debug("aliasGlobal({s}, {?s}) {d}->{d} ({?s})", .{ + gmi.name(coff).toSlice(coff), + gmi.libName(coff).toSlice(coff), + si, + alias_si, + if (alias_sym.gmi != .none) alias_sym.gmi.name(coff).toSlice(coff) else null, + }); - const target_endian = coff.targetEndian(); - const magic = coff.targetLoad(&coff.optionalHeaderStandardPtr().magic); - const addr_size: u64, const addr_align: std.mem.Alignment = switch (magic) { - _ => unreachable, - .PE32 => .{ 4, .@"4" }, - .@"PE32+" => .{ 8, .@"8" }, - }; + var ri = sym.target_relocs; + while (ri != .none) { + const reloc = ri.get(coff); + assert(reloc.target == si); + reloc.target = alias_si; + if (reloc.next == .none) { + reloc.next = alias_sym.target_relocs; + if (alias_sym.target_relocs != .none) + alias_sym.target_relocs.get(coff).prev = ri; + break; + } + ri = reloc.next; + } - const gop = try coff.import_table.entries.getOrPutAdapted( - gpa, - lib_name, - ImportTable.Adapter{ .coff = coff }, - ); - const import_hint_name_align: std.mem.Alignment = .@"2"; - if (!gop.found_existing) { - errdefer _ = coff.import_table.entries.pop(); - try coff.import_table.ni.resize( - &coff.mf, - gpa, - @sizeOf(std.coff.ImportDirectoryEntry) * (gop.index + 2), + const prev_target_relocs = alias_sym.target_relocs; + if (sym.target_relocs != .none) + alias_sym.target_relocs = sym.target_relocs; + sym.target_relocs = .none; + sym.gmi = alias_sym.gmi; + coff.globals.values()[gmi.unwrap().?].si = alias_si; + // Only apply the new relocs + try alias_si.applyTargetRelocs(coff, prev_target_relocs); +} + +fn flushGlobal(coff: *Coff, gmi: Node.GlobalMapIndex) !bool { + const comp = coff.base.comp; + const gpa = comp.gpa; + const name = gmi.name(coff); + const si = gmi.symbol(coff); + + log.debug( + "flushGlobal({s}, {?s}) = n{d} {d}@{d}", + .{ + name.toSlice(coff), + gmi.libName(coff).toSlice(coff), + si.get(coff).ni, + si, + si.get(coff).section_number, + }, + ); + + if (!coff.isImage()) { + try coff.pendingSymbolTableEntry(si); + if (coff.isArchive() and si.get(coff).ni != .none) + try coff.ensureMemberSymbol( + coff.getNode(Node.known.zcu_member).archive_member, + name, ); - const import_hint_name_table_len = - import_hint_name_align.forward(lib_name.len + ".dll".len + 1); - const idata_section_ni = coff.import_table.ni.parent(&coff.mf); - const import_lookup_table_ni = try coff.mf.addLastChildNode(gpa, idata_section_ni, .{ - .size = addr_size * 2, - .alignment = addr_align, - .moved = true, - }); - const import_address_table_ni = try coff.mf.addLastChildNode(gpa, idata_section_ni, .{ - .size = addr_size * 2, - .alignment = addr_align, - .moved = true, - }); - const import_address_table_si = coff.addSymbolAssumeCapacity(); - { - const import_address_table_sym = import_address_table_si.get(coff); - import_address_table_sym.ni = import_address_table_ni; - assert(import_address_table_sym.loc_relocs == .none); - import_address_table_sym.loc_relocs = @enumFromInt(coff.relocs.items.len); - import_address_table_sym.section_number = - coff.getNode(idata_section_ni).object_section.symbol(coff).get(coff).section_number; + + return true; + } + + if (si.get(coff).ni != .none) + return true; + + const Import = struct { + lib_name: String, + name: String.Optional, + ordinal_hint: u16, + kind: enum { + iat_ptr, + thunk, + }, + }; + + const import: Import = import: { + const sym = si.get(coff); + const name_slice = name.toSlice(coff); + const imp_match = std.mem.startsWith(u8, name_slice, imp_prefix); + + // Globals may have the __imp_ prefix already if they are undef externals from another input. + assert(sym.flags.dll_storage_class != .dllexport); + const search_name, const is_imp = if (imp_match or sym.flags.dll_storage_class != .dllimport) + .{ name, imp_match } + else name: { + try coff.ensureUnusedStringCapacity(imp_prefix.len + name_slice.len); + const imp_name = try std.fmt.allocPrint(gpa, imp_prefix ++ "{s}", .{name_slice}); + defer gpa.free(imp_name); + break :name .{ coff.getOrPutStringAssumeCapacity(imp_name), true }; + }; + + const opt_alt_search_name = coff.alternate_names.get(search_name); + const search_libs = switch (sym.flags.value_tag) { + .weak_alias_si, .weak_alias_name => switch (sym.flags.weak_external_strat) { + .none => unreachable, + .no_library => false, + .library, + .alias, + => true, + .anti_dependency => return comp.link_diags.fail( + // TODO: Figure out what the purpose of this is + "TODO support anti_dependency weak external: {s}", + .{name.toSlice(coff)}, + ), + }, + else => true, + }; + + const opt_indices_lists: []const ?InputArchive.SearchList = if (search_libs) &.{ + coff.input_archive_symbol_indices.get(search_name), + if (opt_alt_search_name) |alt| coff.input_archive_symbol_indices.get(alt) else null, + } else &.{}; + + for (opt_indices_lists) |opt_indices_list| { + const indices_list = opt_indices_list orelse continue; + var iter: InputArchive.Member.Symbol.Index = indices_list.first; + while (true) { + const archive_sym = &coff.input_archive_symbols.items[@intFromEnum(iter)]; + const member = &coff.input_archive_members.items[@intFromEnum(archive_sym.iami)]; + member: switch (member.content) { + .object => if (!member.flags.is_loaded) { + if (gmi.libName(coff).unwrap()) |lib_name| + if (!std.ascii.eqlIgnoreCase( + lib_name.toSlice(coff), + member.iai.path(coff).stem(), + )) break :member; + + // Try loading the input member and then retry. + // This could still be a member containing imports + // that use the older non-IMPORT_HEADER method. + coff.pending_input = archive_sym.iami; + return false; + }, + .import => |import| { + if (gmi.libName(coff).unwrap()) |lib_name| + if (!std.ascii.eqlIgnoreCase( + import.lib_name.toSlice(coff), + lib_name.toSlice(coff), + )) break :member; + + const imp_name: String.Optional = name: switch (import.name_type) { + .NAME, + .NAME_NOPREFIX, + .NAME_UNDECORATE, + => |tag| { + const symbol_name: []const u8 = import.symbol_name.toSlice(coff); + const end_match = std.mem.endsWith(u8, name_slice, symbol_name); + const len_delta = name_slice.len -% symbol_name.len; + if (!end_match or + (!imp_match and len_delta != 0) or + (imp_match and len_delta != imp_prefix.len)) + return comp.link_diags.fail( + "global '{s}' has mismatched symbol name in import header: '{s}'", + .{ + name.toSlice(coff), + import.symbol_name.toSlice(coff), + }, + ); + + const imp_name = if (tag == .NAME) import.symbol_name else undecorated: { + var imp_name = std.mem.trimStart(u8, symbol_name, "?@_"); + if (tag == .NAME_UNDECORATE) + imp_name = std.mem.sliceTo(imp_name, '@'); + + try coff.ensureUnusedStringCapacity(imp_name.len); + break :undecorated coff.getOrPutStringAssumeCapacity(imp_name); + }; + + break :name imp_name.toOptional(); + }, + .ORDINAL => break :name .none, + else => |t| return comp.link_diags.fail("TODO handle name_type {t}", .{t}), + }; + + break :import .{ + .lib_name = import.lib_name, + .name = imp_name, + .ordinal_hint = import.import_ordinal_hint, + .kind = if (import.type == .CODE and !is_imp) .thunk else .iat_ptr, + }; + }, + } + + if (archive_sym.next == iter) break; + iter = archive_sym.next; } - const import_hint_name_table_ni = try coff.mf.addLastChildNode(gpa, idata_section_ni, .{ - .size = import_hint_name_table_len, - .alignment = import_hint_name_align, - .moved = true, - }); - gop.value_ptr.* = .{ - .import_lookup_table_ni = import_lookup_table_ni, - .import_address_table_si = import_address_table_si, - .import_hint_name_table_ni = import_hint_name_table_ni, - .len = 0, - .hint_name_len = @intCast(import_hint_name_table_len), + } + + switch (sym.flags.value_tag) { + .weak_alias_si => { + try coff.aliasGlobal(gmi, sym.value.weak_alias_si); + return true; + }, + .weak_alias_name => { + // Convert an unresolved weak external that itself refers to an undef external + // into a (possibly new) global, so it can be resolved separately. + const alias_gop = try coff.getOrPutGlobalSymbol(.{ + .name = sym.value.weak_alias_name.toSlice(coff), + }); + try coff.aliasGlobal(gmi, alias_gop.value_ptr.si); + return true; + }, + else => {}, + } + + // If there was an object that had the alternate name, we've attempted to load it + if (opt_alt_search_name) |alt_search_name| { + if (coff.globals.get(alt_search_name)) |alias_global| { + try coff.aliasGlobal(gmi, alias_global.si); + return true; + } + } + + // Allow importing symbols with no implib entry, if a lib_name was specified. + // This is necessary for certain ntdll symbols, such as LdrRegisterDllNotification, + // which are not in the implib. + if (sym.flags.type != .unknown) { + if (gmi.libName(coff).unwrap()) |lib_name| break :import .{ + .lib_name = lib_name, + .name = name.toOptional(), + .ordinal_hint = 0, + .kind = if (sym.flags.type == .code) .thunk else .iat_ptr, }; - const import_hint_name_slice = import_hint_name_table_ni.slice(&coff.mf); - @memcpy(import_hint_name_slice[0..lib_name.len], lib_name); - @memcpy(import_hint_name_slice[lib_name.len..][0..".dll".len], ".dll"); - @memset(import_hint_name_slice[lib_name.len + ".dll".len ..], 0); - coff.nodes.appendAssumeCapacity(.{ .import_lookup_table = @enumFromInt(gop.index) }); - coff.nodes.appendAssumeCapacity(.{ .import_address_table = @enumFromInt(gop.index) }); - coff.nodes.appendAssumeCapacity(.{ .import_hint_name_table = @enumFromInt(gop.index) }); - - const import_directory_entries = coff.importDirectoryTableSlice()[gop.index..][0..2]; - import_directory_entries.* = .{ .{ - .import_lookup_table_rva = coff.computeNodeRva(import_lookup_table_ni), - .time_date_stamp = 0, - .forwarder_chain = 0, - .name_rva = coff.computeNodeRva(import_hint_name_table_ni), - .import_address_table_rva = coff.computeNodeRva(import_address_table_ni), - }, .{ - .import_lookup_table_rva = 0, - .time_date_stamp = 0, - .forwarder_chain = 0, - .name_rva = 0, - .import_address_table_rva = 0, - } }; - if (target_endian != native_endian) - std.mem.byteSwapAllFields([2]std.coff.ImportDirectoryEntry, import_directory_entries); } + + return true; + }; + + try coff.nodes.ensureUnusedCapacity(gpa, 4); + try coff.symbols.ensureUnusedCapacity(gpa, 2); + + const target_endian = coff.targetEndian(); + const addr_info = coff.targetAddrInfo(); + const lib_name = import.lib_name.toSlice(coff); + const gop = try coff.import_table.entries.getOrPutAdapted( + gpa, + lib_name, + ImportTable.Adapter{ .coff = coff }, + ); + const import_hint_name_align: std.mem.Alignment = .@"2"; + if (!gop.found_existing) { + errdefer _ = coff.import_table.entries.pop(); + try coff.import_table.ni.resize( + &coff.mf, + gpa, + @sizeOf(std.coff.ImportDirectoryEntry) * (gop.index + 2), + ); + const import_hint_name_table_len = + import_hint_name_align.forward(lib_name.len + ".dll".len + 1); + const idata_section_ni = coff.import_table.ni.parent(&coff.mf); + const import_lookup_table_ni = try coff.mf.addLastChildNode(gpa, idata_section_ni, .{ + .size = addr_info.size * 2, + .alignment = addr_info.alignment, + .moved = true, + }); + const import_address_table_ni = try coff.mf.addLastChildNode(gpa, idata_section_ni, .{ + .size = addr_info.size * 2, + .alignment = addr_info.alignment, + .moved = true, + }); + const import_address_table_si = coff.addSymbolAssumeCapacity(); + { + const import_address_table_sym = import_address_table_si.get(coff); + import_address_table_sym.ni = import_address_table_ni; + assert(import_address_table_sym.loc_relocs == .none); + import_address_table_sym.loc_relocs = @enumFromInt(coff.relocs.items.len); + import_address_table_sym.section_number = + coff.getNode(idata_section_ni).object_section.symbol(coff).get(coff).section_number; + } + const import_hint_name_table_ni = try coff.mf.addLastChildNode(gpa, idata_section_ni, .{ + .size = import_hint_name_table_len, + .alignment = import_hint_name_align, + .moved = true, + }); + gop.value_ptr.* = .{ + .import_lookup_table_ni = import_lookup_table_ni, + .import_address_table_si = import_address_table_si, + .import_hint_name_table_ni = import_hint_name_table_ni, + .import_address_table_symbols = .empty, + .len = 0, + .hint_name_len = @intCast(import_hint_name_table_len), + }; + const import_hint_name_slice = import_hint_name_table_ni.slice(&coff.mf); + @memcpy(import_hint_name_slice[0..lib_name.len], lib_name); + @memcpy(import_hint_name_slice[lib_name.len..][0..".dll".len], ".dll"); + @memset(import_hint_name_slice[lib_name.len + ".dll".len ..], 0); + coff.nodes.appendAssumeCapacity(.{ .import_lookup_table = @enumFromInt(gop.index) }); + coff.nodes.appendAssumeCapacity(.{ .import_address_table = @enumFromInt(gop.index) }); + coff.nodes.appendAssumeCapacity(.{ .import_hint_name_table = @enumFromInt(gop.index) }); + + const import_directory_entries = coff.importDirectoryTableSlice()[gop.index..][0..2]; + import_directory_entries.* = .{ .{ + .import_lookup_table_rva = coff.computeNodeRva(import_lookup_table_ni), + .time_date_stamp = 0, + .forwarder_chain = 0, + .name_rva = coff.computeNodeRva(import_hint_name_table_ni), + .import_address_table_rva = coff.computeNodeRva(import_address_table_ni), + }, .{ + .import_lookup_table_rva = 0, + .time_date_stamp = 0, + .forwarder_chain = 0, + .name_rva = 0, + .import_address_table_rva = 0, + } }; + if (target_endian != native_endian) + std.mem.byteSwapAllFields([2]std.coff.ImportDirectoryEntry, import_directory_entries); + } + + log.debug( + "flushGlobalImport({s}, {?s}, {d}, {s})", + .{ name.toSlice(coff), import.name.toSlice(coff), import.ordinal_hint, lib_name }, + ); + + const iat_symbol_gop = try coff.import_table.iat_symbol_indices.getOrPut(gpa, .{ + .iti = @enumFromInt(gop.index), + .name = import.name, + .ordinal_hint = import.ordinal_hint, + }); + if (!iat_symbol_gop.found_existing) { const import_symbol_index = gop.value_ptr.len; + iat_symbol_gop.value_ptr.* = import_symbol_index; + gop.value_ptr.len = import_symbol_index + 1; - const new_symbol_table_size = addr_size * (import_symbol_index + 2); - const import_hint_name_index = gop.value_ptr.hint_name_len; - gop.value_ptr.hint_name_len = @intCast( - import_hint_name_align.forward(import_hint_name_index + 2 + name.len + 1), - ); + const new_symbol_table_size = addr_info.size * (import_symbol_index + 2); + try gop.value_ptr.import_lookup_table_ni.resize(&coff.mf, gpa, new_symbol_table_size); const import_address_table_ni = gop.value_ptr.import_address_table_si.node(coff); try import_address_table_ni.resize(&coff.mf, gpa, new_symbol_table_size); - try gop.value_ptr.import_hint_name_table_ni.resize(&coff.mf, gpa, gop.value_ptr.hint_name_len); + + const opt_imp_name = import.name.toSlice(coff); + const opt_import_hint_name_index = if (opt_imp_name) |imp_name| blk: { + const import_hint_name_index = gop.value_ptr.hint_name_len; + gop.value_ptr.hint_name_len = @intCast( + import_hint_name_align.forward(import_hint_name_index + 2 + imp_name.len + 1), + ); + try gop.value_ptr.import_hint_name_table_ni.resize(&coff.mf, gpa, gop.value_ptr.hint_name_len); + break :blk import_hint_name_index; + } else null; + + const import_hint_name_rva = if (opt_import_hint_name_index) |import_hint_name_index| blk: { + const import_hint_name_slice = gop.value_ptr.import_hint_name_table_ni.slice(&coff.mf); + const ordinal_hint: *u16 = @ptrCast(@alignCast(import_hint_name_slice[import_hint_name_index..][0..2])); + ordinal_hint.* = std.mem.nativeTo(u16, import.ordinal_hint, target_endian); + @memcpy(import_hint_name_slice[import_hint_name_index + 2 ..][0..opt_imp_name.?.len], opt_imp_name.?); + @memset(import_hint_name_slice[import_hint_name_index + 2 + opt_imp_name.?.len ..], 0); + break :blk coff.computeNodeRva(gop.value_ptr.import_hint_name_table_ni) + import_hint_name_index; + } else 0; + const import_lookup_slice = gop.value_ptr.import_lookup_table_ni.slice(&coff.mf); const import_address_slice = import_address_table_ni.slice(&coff.mf); - const import_hint_name_slice = gop.value_ptr.import_hint_name_table_ni.slice(&coff.mf); - @memset(import_hint_name_slice[import_hint_name_index..][0..2], 0); - @memcpy(import_hint_name_slice[import_hint_name_index + 2 ..][0..name.len], name); - @memset(import_hint_name_slice[import_hint_name_index + 2 + name.len ..], 0); - const import_hint_name_rva = - coff.computeNodeRva(gop.value_ptr.import_hint_name_table_ni) + import_hint_name_index; - switch (magic) { + switch (addr_info.magic) { _ => unreachable, inline .PE32, .@"PE32+" => |ct_magic| { - const Addr = switch (ct_magic) { - _ => comptime unreachable, - .PE32 => u32, - .@"PE32+" => u64, - }; - const import_lookup_table: []Addr = @ptrCast(@alignCast(import_lookup_slice)); - const import_address_table: []Addr = @ptrCast(@alignCast(import_address_slice)); - const import_hint_name_rvas: [2]Addr = .{ - std.mem.nativeTo(Addr, @intCast(import_hint_name_rva), target_endian), - std.mem.nativeTo(Addr, 0, target_endian), + const Entry = std.coff.ImportLookupTableEntry(ct_magic); + const import_lookup_table: []Entry = @ptrCast(@alignCast(import_lookup_slice)); + const import_address_table: []Entry = @ptrCast(@alignCast(import_address_slice)); + var import_hint_name_rvas: [2]Entry = .{ + .{ + .payload = if (import.name == .none) + .{ .ordinal = .{ .ordinal = import.ordinal_hint } } + else + .{ .hint_name_rva = @intCast(import_hint_name_rva) }, + .is_ordinal = import.name == .none, + }, + @bitCast(@as(@typeInfo(Entry).@"struct".backing_integer.?, 0)), }; + if (native_endian != target_endian) + for (&import_hint_name_rvas) |*v| std.mem.byteSwapAllFields(Entry, v); + import_lookup_table[import_symbol_index..][0..2].* = import_hint_name_rvas; import_address_table[import_symbol_index..][0..2].* = import_hint_name_rvas; }, } - const si = gmi.symbol(coff); - const sym = si.get(coff); - sym.section_number = Symbol.Index.text.get(coff).section_number; - assert(sym.loc_relocs == .none); - sym.loc_relocs = @enumFromInt(coff.relocs.items.len); - switch (coff.targetLoad(&coff.headerPtr().machine)) { - else => |tag| @panic(@tagName(tag)), - .AMD64 => { - const init = [_]u8{ 0xff, 0x25, 0x00, 0x00, 0x00, 0x00 }; - const target = &comp.root_mod.resolved_target.result; - const ni = try coff.mf.addLastChildNode(gpa, Symbol.Index.text.node(coff), .{ - .alignment = switch (comp.root_mod.optimize_mode) { - .Debug, - .ReleaseSafe, - .ReleaseFast, - => target_util.defaultFunctionAlignment(target), - .ReleaseSmall => target_util.minFunctionAlignment(target), - }.toStdMem(), - .size = init.len, - }); - @memcpy(ni.slice(&coff.mf)[0..init.len], &init); - sym.ni = ni; - sym.size = init.len; + } + + const sym = si.get(coff); + assert(sym.loc_relocs == .none); + const iat_offset: u32 = @intCast(addr_info.size * iat_symbol_gop.value_ptr.*); + switch (import.kind) { + .iat_ptr => { + const iat_sym = gop.value_ptr.import_address_table_si.get(coff); + sym.section_number = iat_sym.section_number; + sym.ni = iat_sym.ni; + sym.setValue(.{ .node_offset = iat_offset }); + (try gop.value_ptr.import_address_table_symbols.addOne(gpa)).* = si; + }, + .thunk => { + sym.loc_relocs = @enumFromInt(coff.relocs.items.len); + + const target = &comp.root_mod.resolved_target.result; + const alignment = switch (comp.root_mod.optimize_mode) { + .Debug, + .ReleaseSafe, + .ReleaseFast, + => target_util.defaultFunctionAlignment(target), + .ReleaseSmall => target_util.minFunctionAlignment(target), + }.toStdMem(); + const parent_si = (try coff.pseudoSectionMapIndex( + .@".thunks", + alignment, + .{ .execute = true, .read = true }, + )).symbol(coff); + + const parent_sym = parent_si.get(coff); + sym.section_number = parent_sym.section_number; + + switch (coff.targetLoad(&coff.headerPtr().machine)) { + else => |tag| @panic(@tagName(tag)), + .AMD64 => { + const init = [_]u8{ 0xff, 0x25, 0x00, 0x00, 0x00, 0x00 }; + const ni = try coff.mf.addLastChildNode(gpa, parent_sym.ni, .{ + .alignment = alignment, + .size = init.len, + }); + @memcpy(ni.slice(&coff.mf)[0..init.len], &init); + sym.ni = ni; + sym.extra.size = init.len; + try coff.addReloc( + si, + init.len - 4, + gop.value_ptr.import_address_table_si, + .{ .known = iat_offset }, + .{ .AMD64 = .REL32 }, + ); + }, + } + coff.nodes.appendAssumeCapacity(.{ .import_thunk = gmi }); + }, + } + + try si.flushMoved(coff); + return true; +} + +fn flushSpecialSymbol(coff: *Coff, pending: SpecialSymbol) !SpecialSymbol { + const comp = coff.base.comp; + + if (!coff.isImage()) return .none; + const gpa = comp.gpa; + const machine = coff.targetLoad(&coff.headerPtr().machine); + const target = &comp.root_mod.resolved_target.result; + + return next: switch (pending) { + .entry => { + // TODO: Use explicitly specified entry if set, add err if not found + const entries: []const struct { ?[]const u8, []const u8 } = if (coff.isExe()) + if (comp.config.link_libc) switch (coff.optionalHeaderField(.subsystem)) { + .WINDOWS_CUI => &.{ + .{ "main", "mainCRTStartup" }, + .{ "wmain", "wmainCRTStartup" }, + }, + .WINDOWS_GUI => &.{ + .{ "WinMain", "WinMainCRTStartup" }, + .{ "wWinMain", "wWinMainCRTStartup" }, + }, + else => unreachable, + } else &.{ + .{ "wWinMainCRTStartup", "wWinMainCRTStartup" }, + } + else + &.{.{ null, if (target.abi.isGnu()) "DllMainCRTStartup" else "_DllMainCRTStartup" }}; + + const entry_si = for (entries) |entry| { + if (entry[0]) |required_name| + if (coff.getDefinedGlobal(required_name) == .null) continue; + + break try coff.globalSymbol(.{ .name = entry[1], .type = .code }); + } else .null; + + if (entry_si != .null) { + log.debug( + "entry({s}, {d})", + .{ entry_si.get(coff).gmi.name(coff).toSlice(coff), entry_si }, + ); + + try coff.symbols.ensureUnusedCapacity(gpa, 1); + const optional_hdr_si = coff.addSymbolAssumeCapacity(); + const optional_hdr_sym = optional_hdr_si.get(coff); + optional_hdr_sym.ni = Node.known.optional_header; + assert(optional_hdr_sym.loc_relocs == .none); + optional_hdr_sym.loc_relocs = @enumFromInt(coff.relocs.items.len); + + const optional_hdr = coff.optionalHeaderStandardPtr(); + optional_hdr.address_of_entry_point = std.mem.nativeTo( + u32, + entry_si.get(coff).rva, + coff.targetEndian(), + ); + + try coff.addReloc( + optional_hdr_si, + @intFromPtr(&optional_hdr.address_of_entry_point) - @intFromPtr(optional_hdr), + entry_si, + .{ .known = 0 }, + switch (machine) { + else => |tag| @panic(@tagName(tag)), + .AMD64 => .{ .AMD64 = .ADDR32NB }, + .I386 => .{ .I386 = .DIR32NB }, + }, + ); + } + + // Referencing the startup functions may trigger loading the object containing them, + // we need to wait until that is done before looking for further symbols. + break :next .tls; + }, + .tls => { + if (coff.getDefinedGlobal("_tls_used").unwrap()) |tls_used_si| { + log.debug("tlsDir({d})", .{tls_used_si}); + + const tls_directory = coff.dataDirectoryPtr(.TLS); + tls_directory.* = .{ + .virtual_address = tls_used_si.get(coff).rva, + .size = switch (coff.targetLoad(&coff.optionalHeaderStandardPtr().magic)) { + _ => unreachable, + .PE32 => 24, + .@"PE32+" => 40, + }, + }; + if (coff.targetEndian() != native_endian) + std.mem.byteSwapAllFields(std.coff.ImageDataDirectory, tls_directory); + + try coff.symbols.ensureUnusedCapacity(gpa, 1); + const data_dir_si = coff.addSymbolAssumeCapacity(); + const data_dir_sym = data_dir_si.get(coff); + data_dir_sym.ni = Node.known.data_directories; + assert(data_dir_sym.loc_relocs == .none); + data_dir_sym.loc_relocs = @enumFromInt(coff.relocs.items.len); + try coff.addReloc( - si, - init.len - 4, - gop.value_ptr.import_address_table_si, - @intCast(addr_size * import_symbol_index), - .{ .AMD64 = .REL32 }, + data_dir_si, + @intFromPtr(&tls_directory.virtual_address) - @intFromPtr(coff.dataDirectorySlice().ptr), + tls_used_si, + .{ .known = 0 }, + switch (machine) { + else => |tag| @panic(@tagName(tag)), + .AMD64 => .{ .AMD64 = .ADDR32NB }, + .I386 => .{ .I386 = .DIR32NB }, + }, ); - }, - } - coff.nodes.appendAssumeCapacity(.{ .global = gmi }); - sym.rva = coff.computeNodeRva(sym.ni); - si.applyLocationRelocs(coff); - } + } + + break :next .none; + }, + .none => unreachable, + }; } fn flushLazy(coff: *Coff, pt: Zcu.PerThread, lmr: Node.LazyMapRef) !void { @@ -2119,6 +6831,9 @@ fn flushLazy(coff: *Coff, pt: Zcu.PerThread, lmr: Node.LazyMapRef) !void { } assert(sym.loc_relocs == .none); sym.loc_relocs = @enumFromInt(coff.relocs.items.len); + if (!isImage(coff) and sym.target_relocs != .none) + try coff.pendingSymbolTableEntry(si); + break :ni sym.ni; }; @@ -2138,42 +6853,107 @@ fn flushLazy(coff: *Coff, pt: Zcu.PerThread, lmr: Node.LazyMapRef) !void { error.WriteFailed => return nw.err.?, else => |e| return e, }; - si.get(coff).size = @intCast(nw.interface.end); - si.applyLocationRelocs(coff); + si.get(coff).extra.size = @intCast(nw.interface.end); + try si.applyLocationRelocs(coff); } fn flushMoved(coff: *Coff, ni: MappedFile.Node.Index) !void { + log.debug("flushMoved({s}, n{d})", .{ @tagName(coff.getNode(ni)), ni }); switch (coff.getNode(ni)) { .file, .header, .signature, + => unreachable, .coff_header, .optional_header, .data_directories, .section_table, - => unreachable, - .image_section => |si| return coff.targetStore( - &si.get(coff).section_number.header(coff).pointer_to_raw_data, - @intCast(ni.fileLocation(&coff.mf, false).offset), - ), - .import_directory_table => coff.targetStore( - &coff.dataDirectoryPtr(.IMPORT).virtual_address, - coff.computeNodeRva(ni), - ), + .placeholder, + => assert(!coff.isImage()), + .symbol_table, + .string_table, + => |_, tag| { + if (tag == .symbol_table) + coff.targetStore( + &coff.headerPtr().pointer_to_symbol_table, + @intCast(ni.location(&coff.mf).resolve(&coff.mf)[0]), + ); + + if (!coff.symbol_table.pending_shrink) { + const symbol_table_loc, const symbol_table_size = coff.symbol_table.ni.location(&coff.mf).resolve(&coff.mf); + const string_table_offset, _ = coff.symbol_table.strings_ni.location(&coff.mf).resolve(&coff.mf); + coff.symbol_table.pending_shrink = string_table_offset - (symbol_table_loc + symbol_table_size) > 0; + } + }, + .relocation_table => |sn| { + coff.targetStore( + &sn.header(coff).pointer_to_relocations, + @intCast(ni.location(&coff.mf).resolve(&coff.mf)[0]), + ); + }, + .relocation_table_entry => {}, + .archive_member_header => |mi| { + const member = mi.get(coff); + switch (member.kind) { + .first_linker, .second_linker, .longnames => {}, + else => coff.targetStore( + &coff.secondLinkerMemberOffsetsSlice()[@intFromEnum(mi) - Member.Index.known_count], + @intCast(ni.fileLocation(&coff.mf, false).offset), + ), + } + + if (member.kind == .coff) + try coff.pending_members.put(coff.base.comp.gpa, mi, {}); + }, + .archive_member, + => {}, + .image_section => |si| { + const sym = si.get(coff); + const flags = coff.targetLoad(&sym.section_number.header(coff).flags); + if (!flags.CNT_UNINITIALIZED_DATA) { + const file_offset = if (isArchive(coff)) + sym.ni.location(&coff.mf).resolve(&coff.mf)[0] + else + ni.fileLocation(&coff.mf, false).offset; + + return coff.targetStore( + &sym.section_number.header(coff).pointer_to_raw_data, + @intCast(file_offset), + ); + } + }, + .input_section => |isi| { + try isi.symbol(coff).flushMoved(coff); + for (coff.input_symbols.items[@intFromEnum(isi.firstSymbol(coff))..]) |input_symbol| { + if (input_symbol.si.get(coff).ni != ni) break; + try input_symbol.si.flushMoved(coff); + } + }, + .import_directory_table => { + _, const size = ni.location(&coff.mf).resolve(&coff.mf); + if (size > 0) + coff.targetStore( + &coff.dataDirectoryPtr(.IMPORT).virtual_address, + coff.computeNodeRva(ni), + ); + }, .import_lookup_table => |import_index| coff.targetStore( &coff.importDirectoryEntryPtr(import_index).import_lookup_table_rva, coff.computeNodeRva(ni), ), .import_address_table => |import_index| { - const import_address_table_si = import_index.get(coff).import_address_table_si; - import_address_table_si.flushMoved(coff); + const entry = import_index.get(coff); + const import_address_table_si = entry.import_address_table_si; + try import_address_table_si.flushMoved(coff); coff.targetStore( &coff.importDirectoryEntryPtr(import_index).import_address_table_rva, import_address_table_si.get(coff).rva, ); + + for (entry.import_address_table_symbols.items) |iat_ptr_si| + try iat_ptr_si.flushMoved(coff); }, .import_hint_name_table => |import_index| { - const target_endian = coff.targetEndian(); const magic = coff.targetLoad(&coff.optionalHeaderStandardPtr().magic); const import_hint_name_rva = coff.computeNodeRva(ni); coff.targetStore( @@ -2186,78 +6966,173 @@ fn flushMoved(coff: *Coff, ni: MappedFile.Node.Index) !void { import_entry.import_address_table_si.node(coff).slice(&coff.mf); const import_hint_name_slice = ni.slice(&coff.mf); const import_hint_name_align = ni.alignment(&coff.mf); + var import_hint_name_index: u32 = 0; for (0..import_entry.len) |import_symbol_index| { - import_hint_name_index = @intCast(import_hint_name_align.forward( - std.mem.indexOfScalarPos( - u8, - import_hint_name_slice, - import_hint_name_index, - 0, - ).? + 1, - )); switch (magic) { _ => unreachable, inline .PE32, .@"PE32+" => |ct_magic| { - const Addr = switch (ct_magic) { - _ => comptime unreachable, - .PE32 => u32, - .@"PE32+" => u64, - }; - const import_lookup_table: []Addr = @ptrCast(@alignCast(import_lookup_slice)); - const import_address_table: []Addr = @ptrCast(@alignCast(import_address_slice)); - const rva = std.mem.nativeTo( - Addr, - import_hint_name_rva + import_hint_name_index, - target_endian, - ); - import_lookup_table[import_symbol_index] = rva; - import_address_table[import_symbol_index] = rva; + const Entry = std.coff.ImportLookupTableEntry(ct_magic); + const import_lookup_table: []Entry = @ptrCast(@alignCast(import_lookup_slice)); + const import_address_table: []Entry = @ptrCast(@alignCast(import_address_slice)); + + var entry = coff.targetLoad(&import_lookup_table[import_symbol_index]); + if (entry.is_ordinal) + continue; + + import_hint_name_index = @intCast(import_hint_name_align.forward( + std.mem.indexOfScalarPos( + u8, + import_hint_name_slice, + import_hint_name_index, + 0, + ).? + 1, + )); + + entry.payload.hint_name_rva = @intCast(import_hint_name_rva + import_hint_name_index); + import_hint_name_index += 2; + + coff.targetStore(&import_lookup_table[import_symbol_index], entry); + coff.targetStore(&import_address_table[import_symbol_index], entry); }, } - import_hint_name_index += 2; + } + }, + .export_directory_table => { + const rva = coff.computeNodeRva(ni); + coff.targetStore(&coff.dataDirectoryPtr(.EXPORT).virtual_address, rva); + coff.targetStore(&coff.exportDirectoryTable().name_rva, rva + @sizeOf(std.coff.ExportDirectoryTable)); + }, + .export_address_table => { + try coff.export_table.export_address_table_si.flushMoved(coff); + + // These relocs are applied directly here instead of via the above flushMoved call as + // they are non-contiguous, and not tracked under export_address_table_si. + for (coff.export_table.entries.values()) |entry| + try entry.export_address_table_ri.get(coff).apply(coff); + + coff.targetStore( + &coff.exportDirectoryTable().export_address_table_rva, + coff.computeNodeRva(ni), + ); + }, + .export_name_pointer_table => coff.targetStore( + &coff.exportDirectoryTable().name_pointer_table_rva, + coff.computeNodeRva(ni), + ), + .export_ordinal_table => coff.targetStore( + &coff.exportDirectoryTable().ordinal_table_rva, + coff.computeNodeRva(ni), + ), + .export_name_table => { + const name_table_rva = coff.computeNodeRva(coff.export_table.name_table_ni); + for ( + coff.exportNamePointerTableSlice(), + coff.exportOrdinalTableSlice(), + ) |*np, target_ord| { + const ord: ExportTable.Ordinal = @enumFromInt(coff.targetLoad(&target_ord.unbiased_ordinal)); + const entry = ord.get(coff); + coff.targetStore( + &np.name_rva, + @intCast(name_table_rva + entry.name_index), + ); } }, inline .pseudo_section, .object_section, - .global, + .import_thunk, .nav, .uav, .lazy_code, .lazy_const_data, - => |mi| mi.symbol(coff).flushMoved(coff), + => |mi| try mi.symbol(coff).flushMoved(coff), + .builtin => |si| try si.flushMoved(coff), } try ni.childrenMoved(coff.base.comp.gpa, &coff.mf); } fn flushResized(coff: *Coff, ni: MappedFile.Node.Index) !void { - _, const size = ni.location(&coff.mf).resolve(&coff.mf); + const offset, const size = ni.location(&coff.mf).resolve(&coff.mf); + log.debug("flushResized({s}, n{d}, 0x{x})", .{ @tagName(coff.getNode(ni)), ni, size }); + switch (coff.getNode(ni)) { - .file => {}, + .file => { + if (coff.isArchive() and coff.members.items.len > 0) { + const last_member = coff.members.items[coff.members.items.len - 1]; + // See .archive_member branch for reasoning + assert(Node.known.file.reverseChildren(&coff.mf).ni == last_member.content_ni); + try coff.flushResized(last_member.content_ni); + } + }, .header => { - switch (coff.optionalHeaderPtr()) { - inline else => |optional_header| coff.targetStore( - &optional_header.size_of_headers, - @intCast(size), - ), + if (coff.isImage()) { + switch (coff.optionalHeaderPtr()) { + inline else => |optional_header| coff.targetStore( + &optional_header.size_of_headers, + @intCast(size), + ), + } + + if (size > coff.section_table.values()[0].si.get(coff).rva) try coff.virtualSlide( + 0, + std.mem.alignForward( + u32, + @intCast(size * 4), + coff.optionalHeaderField(.section_alignment), + ), + ); } - if (size > coff.image_section_table.items[0].get(coff).rva) try coff.virtualSlide( - 0, - std.mem.alignForward( - u32, - @intCast(size * 4), - coff.optionalHeaderField(.section_alignment), - ), - ); }, - .signature, .coff_header, .optional_header, .data_directories => unreachable, + .signature, + .archive_member_header, + => unreachable, + .archive_member => |mi| { + const content_ni = mi.get(coff).content_ni; + const next_ni = content_ni.next(&coff.mf); + const content_offset, _ = content_ni.location(&coff.mf).resolve(&coff.mf); + const next_offset = switch (next_ni) { + .none => offset: { + assert(content_ni.parent(&coff.mf) == Node.known.file); + // This must take into account the final file size. If there are trailing + // bytes, they will be expected to contain another valid member header + break :offset coff.mf.memory_map.memory.len; + }, + else => offset: { + assert(coff.getNode(next_ni) == .archive_member_header); + break :offset next_ni.location(&coff.mf).resolve(&coff.mf)[0]; + }, + }; + + // Not inserting IMAGE_ARCHIVE_PAD `\n` byte here, because we are expanding to full size + Member.storeHeaderDecimalStr(&mi.get(coff).headerPtr(coff).size, next_offset - content_offset); + }, + .coff_header, + .optional_header, + .data_directories, + => unreachable, .section_table => {}, + .symbol_table => { + assert(!coff.isImage()); + if (!coff.symbol_table.pending_shrink) { + const string_table_offset, _ = coff.symbol_table.strings_ni.location(&coff.mf).resolve(&coff.mf); + coff.symbol_table.pending_shrink = + size > coff.targetLoad(&coff.headerPtr().number_of_symbols) * std.coff.Symbol.sizeOf() or + string_table_offset - (offset + size) > 0; + } + }, + .string_table => { + assert(!coff.isImage()); + coff.targetStore(coff.symbolTableStringLenPtr(), @intCast(size)); + }, + .relocation_table, + .relocation_table_entry, + => assert(!coff.isImage()), .image_section => |si| { const sym = si.get(coff); const section_index = sym.section_number.toIndex(); const section = &coff.sectionTableSlice()[section_index]; coff.targetStore(&section.size_of_raw_data, @intCast(size)); - if (size > coff.targetLoad(&section.virtual_size)) { + if (coff.isImage() and size > coff.targetLoad(&section.virtual_size)) { const virtual_size = std.mem.alignForward( u32, @intCast(size * 4), @@ -2266,29 +7141,221 @@ fn flushResized(coff: *Coff, ni: MappedFile.Node.Index) !void { coff.targetStore(&section.virtual_size, virtual_size); try coff.virtualSlide(section_index + 1, sym.rva + virtual_size); } + + if (!coff.isImage()) { + if (coff.symbolTableSectionAuxEntryPtr(si.sti(coff))) |aux_ptr| + coff.targetStore(&aux_ptr.length, @intCast(size)); + } }, - .import_directory_table => coff.targetStore( - &coff.dataDirectoryPtr(.IMPORT).size, - @intCast(size), - ), - .import_lookup_table, .import_address_table, .import_hint_name_table => {}, + .input_section => {}, + .import_directory_table => { + const prev_size = coff.targetLoad(&coff.dataDirectoryPtr(.IMPORT).size); + coff.targetStore( + &coff.dataDirectoryPtr(.IMPORT).size, + @intCast(size), + ); + if (prev_size == 0) try coff.flushMoved(ni); + }, + .import_lookup_table, + .import_address_table, + .import_hint_name_table, + => {}, + .export_directory_table => unreachable, + .export_address_table, + .export_name_pointer_table, + .export_ordinal_table, + .export_name_table, + => {}, inline .pseudo_section, .object_section, - => |smi| smi.symbol(coff).get(coff).size = @intCast(size), - .global, .nav, .uav, .lazy_code, .lazy_const_data => {}, + => |smi, tag| { + if (tag == .pseudo_section and smi.name(coff) == .@".edata") { + coff.targetStore( + &coff.dataDirectoryPtr(.EXPORT).size, + @intCast(size), + ); + } + + var sym = smi.symbol(coff).get(coff); + while (sym.flags.extra_tag == .next_alias_si) + sym = sym.extra.next_alias_si.get(coff); + + sym.extra.size = @intCast(size); + }, + .import_thunk, + .nav, + .uav, + .lazy_code, + .lazy_const_data, + .builtin, + => {}, + .placeholder, + => unreachable, + } +} + +fn flushMember(coff: *Coff, mi: Member.Index) !void { + const member = mi.get(coff); + switch (member.kind) { + .first_linker, + .longnames, + .import, + => unreachable, + .second_linker => { + const Context = struct { + coff: *Coff, + indices: []u16, + strings: []String, + + pub fn lessThan(ctx: @This(), lhs: usize, rhs: usize) bool { + return std.mem.lessThan( + u8, + ctx.strings[lhs].toSlice(ctx.coff), + ctx.strings[rhs].toSlice(ctx.coff), + ); + } + + pub fn swap(ctx: @This(), lhs: usize, rhs: usize) void { + std.mem.swap(u16, &ctx.indices[lhs], &ctx.indices[rhs]); + std.mem.swap(String, &ctx.strings[lhs], &ctx.strings[rhs]); + } + }; + + // TODO: Does this sort need to also sort by linker input order (if names equal)? + std.sort.pdqContext(0, coff.lib_string_table.items.len, Context{ + .coff = coff, + .indices = coff.secondLinkerMemberIndicesSlice(), + .strings = coff.lib_string_table.items, + }); + + var offset: usize = 0; + var string_table = coff.secondLinkerMemberStringsSlice(); + for (coff.lib_string_table.items) |string| { + const str = string.toSlice(coff); + @memcpy(string_table[offset..][0..str.len], str); + string_table[offset + str.len] = 0; + offset += str.len + 1; + } + }, + .coff => { + const file_offset: u32 = @intCast(member.header_ni.fileLocation(&coff.mf, false).offset); + const first_linker_offsets = coff.firstLinkerMemberOffsetsSlice(); + for (member.first_linker_indices.values()) |mfli| + first_linker_offsets[@intFromEnum(mfli)] = std.mem.nativeTo(u32, file_offset, .big); + }, + } +} + +fn flushExportsSort(coff: *Coff) void { + const Context = struct { + coff: *Coff, + np: []std.coff.ExportNamePointerTableEntry, + ord: []std.coff.ExportOrdinalTableEntry, + entries: []ExportTable.Entry, + nt: []const u8, + + pub fn lessThan(ctx: *const @This(), lhs: usize, rhs: usize) bool { + const lhs_entry = &ctx.entries[ctx.coff.targetLoad(&ctx.ord[lhs].unbiased_ordinal)]; + const rhs_entry = &ctx.entries[ctx.coff.targetLoad(&ctx.ord[rhs].unbiased_ordinal)]; + return std.mem.lessThan( + u8, + ctx.nt[lhs_entry.name_index..][0..lhs_entry.name_len], + ctx.nt[rhs_entry.name_index..][0..rhs_entry.name_len], + ); + } + + pub fn swap(ctx: @This(), lhs: usize, rhs: usize) void { + std.mem.swap(std.coff.ExportNamePointerTableEntry, &ctx.np[lhs], &ctx.np[rhs]); + std.mem.swap(std.coff.ExportOrdinalTableEntry, &ctx.ord[lhs], &ctx.ord[rhs]); + } + }; + + std.sort.pdqContext(0, coff.export_table.entries.count(), &Context{ + .coff = coff, + .np = coff.exportNamePointerTableSlice(), + .ord = coff.exportOrdinalTableSlice(), + .entries = coff.export_table.entries.values(), + .nt = coff.export_table.name_table_ni.slice(&coff.mf), + }); +} + +fn flushSectionMerges(coff: *Coff) !void { + while (coff.section_merge_pending_index < coff.section_merges.count()) : (coff.section_merge_pending_index += 1) + try coff.flushSectionMerge(coff.section_merge_pending_index); +} + +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 ( - coff.image_section_table.items[start_section_index..], + coff.section_table.values()[start_section_index..], coff.sectionTableSlice()[start_section_index..], - ) |section_si, *section| { - const section_sym = section_si.get(coff); + ) |*section, *header| { + const section_sym = section.si.get(coff); section_sym.rva = rva; - coff.targetStore(&section.virtual_address, rva); + coff.targetStore(&header.virtual_address, rva); try section_sym.ni.childrenMoved(coff.base.comp.gpa, &coff.mf); - rva += coff.targetLoad(&section.virtual_size); + rva += coff.targetLoad(&header.virtual_size); } switch (coff.optionalHeaderPtr()) { inline else => |optional_header| coff.targetStore( @@ -2303,19 +7370,27 @@ pub fn updateExports( pt: Zcu.PerThread, exported: Zcu.Exported, export_indices: []const Zcu.Export.Index, +) link.Error!void { + const diags = &coff.base.comp.link_diags; + return coff.updateExportsInner(pt, exported, export_indices) catch |err| switch (err) { + error.MappedFileIo => return diags.fail( + "failed to write output file: {t}", + .{coff.mf.io_err.?}, + ), + else => |e| return e, + }; +} +fn updateExportsInner( + coff: *Coff, + pt: Zcu.PerThread, + exported: Zcu.Exported, + export_indices: []const Zcu.Export.Index, ) !void { const zcu = pt.zcu; const gpa = zcu.gpa; const ip = &zcu.intern_pool; - switch (exported) { - .nav => |nav| log.debug("updateExports({f})", .{ip.getNav(nav).fqn.fmt(ip)}), - .uav => |uav| log.debug("updateExports(@as({f}, {f}))", .{ - Type.fromInterned(ip.typeOf(uav)).fmt(pt), - Value.fromInterned(uav).fmtValue(pt), - }), - } - try coff.symbol_table.ensureUnusedCapacity(gpa, export_indices.len); + try coff.symbols.ensureUnusedCapacity(gpa, export_indices.len); const exported_si: Symbol.Index = switch (exported) { .nav => |nav| try coff.navSymbol(zcu, nav), .uav => |uav| @enumFromInt(@intFromEnum(try coff.lowerUav( @@ -2324,62 +7399,299 @@ pub fn updateExports( Type.fromInterned(ip.typeOf(uav)).abiAlignment(zcu), ))), }; + switch (exported) { + .nav => |nav| log.debug("updateExports({f}) = {d}", .{ ip.getNav(nav).fqn.fmt(ip), exported_si }), + .uav => |uav| log.debug("updateExports(@as({f}, {f})) = {d}", .{ + Type.fromInterned(ip.typeOf(uav)).fmt(pt), + Value.fromInterned(uav).fmtValue(pt), + exported_si, + }), + } + while (try coff.resolve(pt.tid)) {} while (try coff.idle(pt.tid)) {} + + const machine = coff.targetLoad(&coff.headerPtr().machine); const exported_ni = exported_si.node(coff); const exported_sym = exported_si.get(coff); + var prev_alias_si = exported_si; + for (export_indices) |export_index| { const @"export" = export_index.ptr(zcu); - const export_si = try coff.globalSymbol(@"export".opts.name.toSlice(ip), null); + const name = @"export".opts.name.toSlice(ip); + + // TODO: add an errMsg if this conflicts with an existing symbol + const export_si = try coff.globalSymbol(.{ .name = name }); const export_sym = export_si.get(coff); export_sym.ni = exported_ni; export_sym.rva = exported_sym.rva; - export_sym.size = exported_sym.size; export_sym.section_number = exported_sym.section_number; - export_si.applyTargetRelocs(coff); - if (@"export".opts.name.eqlSlice("wWinMainCRTStartup", ip)) { - coff.optionalHeaderStandardPtr().address_of_entry_point = exported_sym.rva; - } else if (@"export".opts.name.eqlSlice("_tls_used", ip)) { - const tls_directory = coff.dataDirectoryPtr(.TLS); - tls_directory.* = .{ .virtual_address = exported_sym.rva, .size = exported_sym.size }; - if (coff.targetEndian() != native_endian) - std.mem.byteSwapAllFields(std.coff.ImageDataDirectory, tls_directory); + if (@"export".opts.linkage == .weak and !coff.isImage()) { + // exported_si needs to be ahead of export_si in the symbol table, + // so that its sti is known when creating the weak external aux entry + try coff.pendingSymbolTableEntry(exported_si); + export_sym.flags.weak_external_strat = .alias; + export_sym.setValue(.{ .weak_alias_si = exported_si }); + } + defer export_si.applyTargetRelocs(coff, .none) catch unreachable; + + // The last symbol in the alias list holds the size + const prev_alias_sym = prev_alias_si.get(coff); + switch (prev_alias_sym.flags.extra_tag) { + .size => export_sym.setExtra(.{ .size = prev_alias_sym.extra.size }), + // This export should have been deleted + .next_alias_si => assert(prev_alias_sym.extra.next_alias_si == export_si), + else => unreachable, + } + + prev_alias_sym.setExtra(.{ .next_alias_si = export_si }); + prev_alias_si = export_si; + + if (!coff.isImage()) continue; + + const entries_ctx = ExportTable.Adapter{ .coff = coff }; + const gop = try coff.export_table.entries.getOrPutAdapted( + gpa, + name, + entries_ctx, + ); + + if (!gop.found_existing) { + errdefer _ = coff.export_table.entries.pop(); + + const export_count = coff.export_table.entries.count(); + if (export_count > std.math.maxInt(@FieldType(std.coff.ExportDirectoryTable, "number_of_entries"))) + return coff.base.comp.link_diags.fail("exceeded maximum number of exports", .{}); + + const name_index: u32 = @intCast(coff.export_table.name_table_ni.location(&coff.mf).resolve(&coff.mf)[1]); + const new_name_table_size = name_index + name.len + 1; + if (new_name_table_size > std.math.maxInt(@FieldType(ExportTable.Entry, "name_index"))) + return coff.base.comp.link_diags.fail("exports name table limit reached", .{}); + + try coff.export_table.name_table_ni.resize(&coff.mf, gpa, new_name_table_size); + + const name_table_slice = coff.export_table.name_table_ni.slice(&coff.mf); + @memcpy(name_table_slice[name_index..][0 .. name.len + 1], name[0 .. name.len + 1]); + + // If the new name sorts after the current tail of the sorted list, we don't need to re-sort + { + const ordinal_table_slice = coff.exportOrdinalTableSlice(); + if (ordinal_table_slice.len > 0 and !coff.export_table.pending_sort) { + const tail_index: ExportTable.Ordinal = + @enumFromInt(ordinal_table_slice[ordinal_table_slice.len - 1].unbiased_ordinal); + const tail_entry = tail_index.get(coff); + const tail_name = name_table_slice[tail_entry.name_index..][0..tail_entry.name_len]; + coff.export_table.pending_sort = std.mem.lessThan(u8, name, tail_name); + } + } + + const edt = coff.exportDirectoryTable(); + coff.targetStore(&edt.number_of_names, @intCast(export_count)); + edt.number_of_entries = edt.number_of_names; + + // TODO: These should all be resized ahead of time to fit all exports + // after https://github.com/ziglang/zig/issues/23616 + try coff.export_table.export_address_table_si.node(coff).resize( + &coff.mf, + gpa, + export_count * @sizeOf(std.coff.ExportAddressTableEntry), + ); + + try coff.export_table.name_pointer_table_ni.resize( + &coff.mf, + gpa, + export_count * @sizeOf(std.coff.ExportNamePointerTableEntry), + ); + + try coff.export_table.ordinal_table_ni.resize( + &coff.mf, + gpa, + export_count * @sizeOf(std.coff.ExportOrdinalTableEntry), + ); + + coff.targetStore( + &coff.exportNamePointerTableSlice()[gop.index].name_rva, + @intCast(coff.computeNodeRva(coff.export_table.name_table_ni) + name_index), + ); + coff.targetStore( + &coff.exportOrdinalTableSlice()[gop.index].unbiased_ordinal, + @intCast(gop.index), + ); + + gop.value_ptr.* = .{ + .si = export_si, + .name_index = @intCast(name_index), + .name_len = @intCast(name.len), + .export_address_table_ri = @enumFromInt(coff.relocs.items.len), + }; + + try coff.addReloc( + coff.export_table.export_address_table_si, + @intCast(@sizeOf(std.coff.ExportAddressTableEntry) * gop.index), + export_si, + .{ .known = 0 }, + switch (machine) { + else => |tag| @panic(@tagName(tag)), + .AMD64 => .{ .AMD64 = .ADDR32NB }, + .I386 => .{ .I386 = .DIR32NB }, + }, + ); + } else { + gop.value_ptr.si = export_si; + const reloc = gop.value_ptr.*.export_address_table_ri.get(coff); + reloc.target = export_si; } } } -pub fn deleteExport(coff: *Coff, exported: Zcu.Exported, name: InternPool.NullTerminatedString) void { - _ = coff; - _ = exported; - _ = name; +pub fn deleteExport( + coff: *Coff, + exported: Zcu.Exported, + name: InternPool.NullTerminatedString, +) void { + const zcu = coff.base.comp.zcu.?; + const ip = &zcu.intern_pool; + + const exported_si: Symbol.Index = switch (exported) { + .nav => |nav| coff.navs.get(nav).?, + .uav => |uav| coff.uavs.get(uav).?, + }; + + const name_slice = name.toSlice(ip); + log.debug("deleteExport({s}, {d})", .{ name_slice, exported_si }); + + // TODO: Delete from first / second linker member table + // TODO: Delete from symbol table inside section } -pub fn dump(coff: *Coff, tid: Zcu.PerThread.Id) Io.Cancelable!void { +fn dumpStderr(coff: *Coff, tid: Zcu.PerThread.Id) !void { const comp = coff.base.comp; const io = comp.io; var buffer: [512]u8 = undefined; const stderr = try io.lockStderr(&buffer, null); defer io.unlockStderr(); const w = &stderr.file_writer.interface; - coff.printNode(tid, w, .root, 0) catch |err| switch (err) { - error.WriteFailed => return stderr.err.?, - }; + _ = try coff.dump(w, tid); } -pub fn printNode( +pub fn dump(coff: *Coff, w: *Io.Writer, tid: Zcu.PerThread.Id) !link.File.DumpResult { + if (coff.options.enable_link_snapshots) { + try coff.printNode(tid, w, .root, 0); + try w.writeAll("Section table:\n"); + for (coff.section_table.keys(), coff.section_table.values()) |name, sec| + try coff.printSection(w, name, sec.si); + try w.writeAll("Symbol table:\n"); + for (1..coff.symbols.items.len) |si| + try coff.printSymbol(w, tid, @enumFromInt(si)); + + return .enabled; + } + return .disabled; +} + +fn printSection(coff: *Coff, w: *Io.Writer, name: String, si: Symbol.Index) !void { + const sym = si.get(coff); + try w.print("{d:0>6}@{d:0>2} {x:08} n{d:0>8} | {s}\n", .{ + si, + sym.section_number, + if (sym.flags.extra_tag == .size) sym.extra.size else 0, + sym.ni, + name.toSlice(coff), + }); +} + +fn printSymbol( coff: *Coff, + w: *Io.Writer, tid: Zcu.PerThread.Id, + si: Symbol.Index, +) !void { + const sym = si.get(coff); + const node = coff.getNode(sym.ni); + try w.print("{d:0>6}@{d:0>2} {x:08} {s} {s} {s} n{d:0>8}+{x:08}:{t: <26} | {x:08} ", .{ + si, + sym.section_number, + if (sym.flags.extra_tag == .size) + @as(u64, sym.extra.size) + else if (sym.ni != .none) + sym.ni.location(&coff.mf).resolve(&coff.mf)[1] + else + 0, + switch (sym.flags.value_tag) { + .none => "xx", + .weak_alias_name => "an", + .weak_alias_si => "as", + .node_offset => "no", + }, + switch (sym.flags.extra_tag) { + .size => "sz", + .isli => "li", + .next_alias_si => "na", + }, + switch (sym.flags.type) { + .unknown => "u", + .code => "c", + .data => "d", + }, + sym.ni, + if (sym.flags.value_tag == .node_offset) sym.value.node_offset else 0, + node, + sym.rva, + }); + + if (sym.gmi != .none) { + try w.print("G {f}\n", .{fmtGlobalName(coff, sym.gmi)}); + } else { + try w.writeAll("| "); + try coff.printNodeName(w, tid, node); + if (sym.flags.extra_tag == .isli) + try w.print(" | {s}", .{sym.extra.isli.name(coff).toSlice(coff)}); + try w.writeByte('\n'); + } +} + +const FmtGlobalName = struct { coff: *Coff, gmi: Node.GlobalMapIndex }; + +fn fmtGlobalName(coff: *Coff, gmi: Node.GlobalMapIndex) std.fmt.Alt(FmtGlobalName, globalNameEscape) { + return .{ .data = .{ .coff = coff, .gmi = gmi } }; +} + +fn globalNameEscape(data: FmtGlobalName, w: *std.Io.Writer) std.Io.Writer.Error!void { + if (data.gmi == .none) return; + try w.writeAll(data.gmi.name(data.coff).toSlice(data.coff)); + if (data.gmi.libName(data.coff).unwrap()) |lib_name| + try w.print("({s})", .{lib_name.toSlice(data.coff)}); +} + +fn printNodeName( + coff: *Coff, w: *std.Io.Writer, - ni: MappedFile.Node.Index, - indent: usize, + tid: Zcu.PerThread.Id, + node: Node, ) !void { - const node = coff.getNode(ni); - try w.splatByteAll(' ', indent); - try w.writeAll(@tagName(node)); switch (node) { else => {}, .image_section => |si| try w.print("({s})", .{ std.mem.sliceTo(&si.get(coff).section_number.header(coff).name, 0), }), + .input_section => |isi| { + const ioi = isi.input(coff); + const is = isi.inputSection(coff); + try w.print("({f}{f}, {s}", .{ + ioi.path(coff).fmtEscapeString(), + fmtMemberNameString(ioi.memberName(coff)), + coff.getNode(is.si.node(coff).parent(&coff.mf)).object_section.name(coff).toSlice(coff), + }); + if (is.comdat_si != .null) { + const comdat_sym = is.comdat_si.get(coff); + const comdat_name = if (comdat_sym.gmi != .none) + comdat_sym.gmi.name(coff).toSlice(coff) + else + coff.input_symbols.items[@intFromEnum(comdat_sym.extra.isli)].name.toSlice(coff); + + try w.print("={s}", .{comdat_name}); + } + try w.writeAll(")"); + }, .import_lookup_table, .import_address_table, .import_hint_name_table, @@ -2389,18 +7701,18 @@ pub fn printNode( inline .pseudo_section, .object_section => |smi| try w.print("({s})", .{ smi.name(coff).toSlice(coff), }), - .global => |gmi| { - const gn = gmi.globalName(coff); + .import_thunk, + => |gmi| { try w.writeByte('('); - if (gn.lib_name.toSlice(coff)) |lib_name| try w.print("{s}.dll, ", .{lib_name}); - try w.print("{s})", .{gn.name.toSlice(coff)}); + if (gmi.libName(coff).toSlice(coff)) |lib_name| try w.print("{s}.dll, ", .{lib_name}); + try w.print("{s})", .{gmi.name(coff).toSlice(coff)}); }, .nav => |nmi| { const zcu = coff.base.comp.zcu.?; const ip = &zcu.intern_pool; const nav = ip.getNav(nmi.navIndex(coff)); try w.print("({f}, {f})", .{ - Type.fromInterned(nav.typeOf(ip)).fmt(.{ .zcu = zcu, .tid = tid }), + Type.fromInterned(ip.typeOf(nav.resolved.?.value)).fmt(.{ .zcu = zcu, .tid = tid }), nav.fqn.fmt(ip), }); }, @@ -2418,7 +7730,28 @@ pub fn printNode( .tid = tid, }), }), + .builtin => |si| { + const sym = si.get(coff); + if (sym.gmi != .none) { + try w.writeByte('('); + if (sym.gmi.libName(coff).toSlice(coff)) |lib_name| try w.print("{s}.dll, ", .{lib_name}); + try w.print("{s})", .{sym.gmi.name(coff).toSlice(coff)}); + } + }, } +} + +pub fn printNode( + coff: *Coff, + tid: Zcu.PerThread.Id, + w: *Io.Writer, + ni: MappedFile.Node.Index, + indent: usize, +) !void { + const node = coff.getNode(ni); + try w.splatByteAll(' ', indent); + try w.writeAll(@tagName(node)); + try coff.printNodeName(w, tid, node); { const mf_node = &coff.mf.nodes.items[@intFromEnum(ni)]; const off, const size = mf_node.location().resolve(&coff.mf); @@ -2446,7 +7779,7 @@ pub fn printNode( const line_len = 0x10; var line_it = std.mem.window( u8, - coff.mf.contents[@intCast(file_loc.offset)..][0..@intCast(file_loc.size)], + coff.mf.memory_map.memory[@intCast(file_loc.offset)..][0..@intCast(file_loc.size)], line_len, line_len, ); diff --git a/src/link/Elf2.zig b/src/link/Elf2.zig @@ -3785,7 +3785,7 @@ fn mapInputSection(elf: *Elf, opts: struct { const new_alignment: std.mem.Alignment = .fromByteUnits( std.math.ceilPowerOfTwoAssert(usize, @intCast(opts.addralign)), ); - try existing_shndx.get(elf).ni.realign(&elf.mf, gpa, new_alignment); + try existing_shndx.get(elf).ni.realign(&elf.mf, gpa, new_alignment, .{ .set_alignment = true }); } // ...and update the shdr as needed. switch (elf.shdrPtr(existing_shndx)) { @@ -3948,7 +3948,7 @@ fn uavMapIndex( } else { const node = uav_gop.value_ptr.lsi.index().ptr(elf).node; if (resolved_align.toStdMem().order(node.alignment(&elf.mf)).compare(.gt)) { - try node.realign(&elf.mf, gpa, resolved_align.toStdMem()); + try node.realign(&elf.mf, gpa, resolved_align.toStdMem(), .{ .set_alignment = true }); } } return umi; @@ -4677,7 +4677,7 @@ fn loadDso(elf: *Elf, path: std.Build.Cache.Path, fr: *Io.File.Reader) (LoadPars // We have a copy relocation for this global, but the amount of space we // reserved for it could be too small or underaligned! try copied_global.node.resize(&elf.mf, gpa, gop.value_ptr.size); - try copied_global.node.realign(&elf.mf, gpa, gop.value_ptr.alignment); + try copied_global.node.realign(&elf.mf, gpa, gop.value_ptr.alignment, .{ .set_alignment = true }); const global_ptr = elf.globalByName(name).?; switch (elf.symPtr(global_ptr.symtab_index)) { inline else => |sym_ptr| elf.targetStore(&sym_ptr.size, @intCast(gop.value_ptr.size)), @@ -6711,16 +6711,12 @@ pub fn deleteExport(elf: *Elf, exported: Zcu.Exported, name: InternPool.NullTerm _ = name; } -pub fn dump(elf: *Elf, tid: Zcu.PerThread.Id) Io.Cancelable!void { - const comp = elf.base.comp; - const io = comp.io; - var buffer: [512]u8 = undefined; - const stderr = try io.lockStderr(&buffer, null); - defer io.lockStderr(); - const w = &stderr.file_writer.interface; - elf.printNode(tid, w, .root, 0) catch |err| switch (err) { - error.WriteFailed => return stderr.err.?, - }; +pub fn dump(elf: *Elf, w: *Io.Writer, tid: Zcu.PerThread.Id) !link.File.DumpResult { + if (elf.options.enable_link_snapshots) { + try elf.printNode(tid, w, .root, 0); + return .enabled; + } + return .disabled; } pub fn printNode( @@ -6764,13 +6760,13 @@ pub fn printNode( elf.getNode(isi.node(elf).parent(&elf.mf)).section.name(elf).slice(elf), }); }, - .copied_global => |name| try w.print("(copy:{s})", .{name}), + .copied_global => |name| try w.print("(copy:{s})", .{name.slice(elf)}), .nav => |nmi| { const zcu = elf.base.comp.zcu.?; const ip = &zcu.intern_pool; const nav = ip.getNav(nmi.navIndex(elf)); try w.print("({f}, {f})", .{ - Type.fromInterned(nav.typeOf(ip)).fmt(.{ .zcu = zcu, .tid = tid }), + Type.fromInterned(ip.typeOf(nav.resolved.?.value)).fmt(.{ .zcu = zcu, .tid = tid }), nav.fqn.fmt(ip), }); }, diff --git a/src/link/MappedFile.zig b/src/link/MappedFile.zig @@ -26,6 +26,9 @@ updates: std.ArrayList(Node.Index), update_prog_node: std.Progress.Node, writers: std.SinglyLinkedList, io_err: ?IoError, +/// If locked, modifying the node layout is not allowed. +/// Modifying node content is always allowed. +nodes_lock: std.debug.SafetyLock = .{}, pub const growth_factor = 4; @@ -188,6 +191,10 @@ pub const Node = extern struct { return ni.get(mf).parent; } + pub fn next(ni: Node.Index, mf: *const MappedFile) Node.Index { + return ni.get(mf).next; + } + pub fn ChildIterator(comptime direction: enum { prev, next }) type { return struct { mf: *const MappedFile, @@ -330,6 +337,7 @@ pub const Node = extern struct { } pub fn resize(ni: Node.Index, mf: *MappedFile, gpa: std.mem.Allocator, size: u64) Error!void { + defer if (std.debug.runtime_safety) mf.verify(); mf.resizeNode(gpa, ni, size) catch |err| switch (err) { error.OutOfMemory, error.Canceled, @@ -346,16 +354,23 @@ pub const Node = extern struct { } } + pub const RealignNodeOptions = struct { + /// Shift the node backwards if possible + try_backwards: bool = true, + /// If `set, persists `new_alignment` as the node's alignment for future operations. + set_alignment: bool = true, + }; + /// Moves and expands a node such that its offset and size are aligned to `new_alignment`. - /// /// Asserts that `ni` is not `Node.Index.root`. pub fn realign( ni: Node.Index, mf: *MappedFile, gpa: std.mem.Allocator, new_alignment: std.mem.Alignment, + opts: RealignNodeOptions, ) Error!void { - mf.realignNode(gpa, ni, new_alignment) catch |err| switch (err) { + mf.realignNode(gpa, ni, new_alignment, opts) catch |err| switch (err) { error.OutOfMemory, error.Canceled, => |e| return e, @@ -371,6 +386,26 @@ pub const Node = extern struct { } } + /// Shrink a node to `size`, exactly. + /// Asserts that the new size can contain all the children. + /// If `shift_next` is set, then the following node is shifted backwards into + /// the free space as much as alignment allows. + /// Asserts that `size` is >= the end of the last child node. + pub fn shrink( + ni: Node.Index, + mf: *MappedFile, + gpa: std.mem.Allocator, + size: u64, + shift_next: bool, + ) Error!void { + try mf.shrinkNode(gpa, ni, size, shift_next); + var writers_it = mf.writers.first; + while (writers_it) |writer_node| : (writers_it = writer_node.next) { + const w: *Node.Writer = @fieldParentPtr("writer_node", writer_node); + w.interface.buffer = w.ni.slice(mf); + } + } + pub fn writer(ni: Node.Index, mf: *MappedFile, gpa: std.mem.Allocator, w: *Writer) void { w.* = .{ .gpa = gpa, @@ -530,7 +565,26 @@ fn addNode(mf: *MappedFile, gpa: std.mem.Allocator, opts: struct { add_node: AddNodeOptions, }) Error!Node.Index { if (opts.add_node.moved or opts.add_node.resized) try mf.updates.ensureUnusedCapacity(gpa, 1); + mf.nodes_lock.assertUnlocked(); const offset = opts.add_node.alignment.forward(@intCast(opts.offset)); + if (opts.parent != .none) { + const new_end = offset + opts.add_node.size; + switch (opts.next) { + .none => { + _, const parent_size = opts.parent.location(mf).resolve(mf); + if (new_end > parent_size) + try opts.parent.resize(mf, gpa, new_end); + }, + else => |next_ni| { + const next_offset, _ = next_ni.location(mf).resolve(mf); + if (new_end > next_offset) + try next_ni.realign(mf, gpa, opts.add_node.alignment, .{ + .try_backwards = false, + .set_alignment = false, + }); + }, + } + } const location_tag: Node.Location.Tag, const location_payload: Node.Location.Payload = location: { if (std.math.cast(u32, offset)) |small_offset| break :location .{ .small, .{ .small = .{ .offset = small_offset, .size = 0 }, @@ -572,14 +626,12 @@ fn addNode(mf: *MappedFile, gpa: std.mem.Allocator, opts: struct { }, .location_payload = location_payload, }; + { - defer { - free_node.flags.moved = false; - free_node.flags.resized = false; - } - _, const parent_size = opts.parent.location(mf).resolve(mf); - if (offset > parent_size) try opts.parent.resize(mf, gpa, offset); try free_ni.resize(mf, gpa, opts.add_node.size); + if (opts.add_node.moved or opts.add_node.resized) try mf.updates.ensureUnusedCapacity(gpa, 1); + free_node.flags.moved = false; + free_node.flags.resized = false; } if (opts.add_node.moved) free_ni.movedAssumeCapacity(mf); if (opts.add_node.resized) free_ni.resizedAssumeCapacity(mf); @@ -666,11 +718,63 @@ pub fn addNodeAfter( }); } -fn resizeNode(mf: *MappedFile, gpa: std.mem.Allocator, ni: Node.Index, requested_size: u64) (Allocator.Error || Io.Cancelable || IoError)!void { +fn shrinkNode( + mf: *MappedFile, + gpa: std.mem.Allocator, + ni: Node.Index, + size: u64, + shift_next: bool, +) !void { + mf.nodes_lock.assertUnlocked(); + const node = ni.get(mf); + const old_offset, _ = node.location().resolve(mf); + + // This would require unmapping first + assert(ni != Node.Index.root); + defer if (std.debug.runtime_safety) mf.verify(); + + if (node.last != .none) { + const last = node.last.get(mf); + const last_offset, const last_size = last.location().resolve(mf); + assert(last_offset + last_size > size); + } + + try mf.large.ensureUnusedCapacity(gpa, 4); + try mf.updates.ensureUnusedCapacity(gpa, 2); + + ni.setLocationAssumeCapacity(mf, old_offset, size); + if (!shift_next or node.next == .none) return; + + const next = node.next.get(mf); + const old_next_offset, const next_size = next.location().resolve(mf); + const padding = old_next_offset - (old_offset + size); + const new_next_offset = next.flags.alignment.forward(@intCast(old_next_offset - padding)); + + if (next.flags.has_content and new_next_offset < old_next_offset) { + const old_file_offset = node.next.fileLocation(mf, false).offset; + const new_file_offset = (old_file_offset - old_next_offset) + new_next_offset; + @memmove( + mf.memory_map.memory[@intCast(new_file_offset)..][0..@intCast(next_size)], + mf.memory_map.memory[@intCast(old_file_offset)..][0..@intCast(next_size)], + ); + @memset(mf.memory_map.memory[@intCast(new_file_offset + next_size)..@intCast(old_file_offset + next_size)], 0); + } + + node.next.setLocationAssumeCapacity(mf, new_next_offset, next_size); +} + +fn resizeNode( + mf: *MappedFile, + gpa: std.mem.Allocator, + ni: Node.Index, + requested_size: u64, +) (Allocator.Error || Io.Cancelable || IoError)!void { + mf.nodes_lock.assertUnlocked(); const io = mf.io; const node = ni.get(mf); const old_offset, const old_size = node.location().resolve(mf); const new_size = node.flags.alignment.forward(@intCast(requested_size)); + // Resize the entire file if (ni == Node.Index.root) { try mf.ensureCapacityForSetLocation(gpa); @@ -703,6 +807,17 @@ fn resizeNode(mf: *MappedFile, gpa: std.mem.Allocator, ni: Node.Index, requested if (is_linux and !mf.flags.fallocate_insert_range_unsupported and node.flags.alignment.order(mf.flags.block_size).compare(.gte)) insert_range: { + const range_file_offset = ni.fileLocation(mf, false).offset + old_size; + const range_size = node.flags.alignment.forward( + @intCast(requested_size +| requested_size / growth_factor), + ) - old_size; + + // If this node is being realigned, its current state might not + // meet the requirements for fallocate + if (!mf.flags.block_size.check(@intCast(range_file_offset)) or + !mf.flags.block_size.check(@intCast(range_size))) + break :insert_range; + mf.memory_map.write(io) catch |err| switch (err) { error.WouldBlock => return error.Unexpected, // file was not opened as non-blocking error.NotOpenForWriting => return error.Unexpected, // we definitely opened the file for writing @@ -712,10 +827,6 @@ fn resizeNode(mf: *MappedFile, gpa: std.mem.Allocator, ni: Node.Index, requested const last_offset, const last_size = parent.last.location(mf).resolve(mf); const last_end = last_offset + last_size; assert(last_end <= old_parent_size); - const range_file_offset = ni.fileLocation(mf, false).offset + old_size; - const range_size = node.flags.alignment.forward( - @intCast(requested_size +| requested_size / growth_factor), - ) - old_size; _, const file_size = Node.Index.root.location(mf).resolve(mf); while (true) switch (linux.errno(switch (std.math.order(range_file_offset, file_size)) { .lt => linux.fallocate( @@ -814,6 +925,7 @@ fn resizeNode(mf: *MappedFile, gpa: std.mem.Allocator, ni: Node.Index, requested var last_fixed_ni = ni; var first_floating_ni = node.next; var shift = new_size - old_size; + var max_shift_align: std.mem.Alignment = .@"1"; var direction: enum { forward, reverse } = .forward; while (true) { assert(last_fixed_ni != .none); @@ -830,10 +942,12 @@ fn resizeNode(mf: *MappedFile, gpa: std.mem.Allocator, ni: Node.Index, requested if (new_last_fixed_offset + last_fixed_size <= old_first_floating_offset) break :make_space; assert(direction == .forward); + max_shift_align = max_shift_align.max(first_floating.flags.alignment.max(last_fixed.flags.alignment)); if (first_floating.flags.fixed) { - shift = first_floating.flags.alignment.forward(@intCast( + shift = max_shift_align.forward(@intCast( @max(shift, first_floating_size), )); + // Not enough space, try the next node last_fixed_ni = first_floating_ni; first_floating_ni = first_floating.next; @@ -842,7 +956,7 @@ fn resizeNode(mf: *MappedFile, gpa: std.mem.Allocator, ni: Node.Index, requested // Move the found floating node to make space for preceding fixed nodes const last = parent.last.get(mf); const last_offset, const last_size = last.location().resolve(mf); - const new_first_floating_offset = first_floating.flags.alignment.forward( + const new_first_floating_offset = max_shift_align.forward( @intCast(@max(new_last_fixed_offset + last_fixed_size, last_offset + last_size)), ); const new_parent_size = new_first_floating_offset + first_floating_size; @@ -903,7 +1017,7 @@ fn resizeNode(mf: *MappedFile, gpa: std.mem.Allocator, ni: Node.Index, requested last_fixed_ni.setLocationAssumeCapacity( mf, old_last_fixed_offset, - last_fixed_size + shift, + new_size, ); return; } @@ -929,8 +1043,10 @@ fn realignNode( gpa: std.mem.Allocator, ni: Node.Index, new_alignment: std.mem.Alignment, + opts: Node.Index.RealignNodeOptions, ) (Allocator.Error || Io.Cancelable || IoError)!void { assert(ni != Node.Index.root); // currently unsupported + mf.nodes_lock.assertUnlocked(); const node = ni.get(mf); const old_offset, const size = node.location().resolve(mf); @@ -939,7 +1055,12 @@ fn realignNode( defer if (std.debug.runtime_safety) mf.verify(); + const prev_alignment = node.flags.alignment; node.flags.alignment = new_alignment; + defer { + // alignment needs to be temporarily set for the resizes below + if (!opts.set_alignment) node.flags.alignment = prev_alignment; + } const new_size = node.flags.alignment.forward(@intCast(size)); if (new_alignment.check(@intCast(old_offset))) { @@ -956,6 +1077,37 @@ fn realignNode( }, }; + if (opts.try_backwards) { + const backward_offset = new_alignment.backward(@intCast(old_offset)); + const prev_end = if (node.prev == .none) 0 else prev: { + const prev_offset, const prev_size = node.prev.location(mf).resolve(mf); + break :prev prev_offset + prev_size; + }; + + if (backward_offset >= prev_end) { + try mf.ensureCapacityForSetLocation(gpa); + + if (node.flags.has_content) { + const old_file_offset = ni.fileLocation(mf, false).offset; + const new_file_offset = (old_file_offset - old_offset) + backward_offset; + @memmove( + mf.memory_map.memory[@intCast(new_file_offset)..][0..@intCast(size)], + mf.memory_map.memory[@intCast(old_file_offset)..][0..@intCast(size)], + ); + @memset(mf.memory_map.memory[@intCast(new_file_offset + size)..@intCast(old_file_offset + size)], 0); + } + + if (backward_offset + new_size <= trailing_end) { + ni.setLocationAssumeCapacity(mf, backward_offset, new_size); + } else { + ni.setLocationAssumeCapacity(mf, backward_offset, size); + try mf.resizeNode(gpa, ni, new_size); + } + + return; + } + } + const forward_offset = new_alignment.forward(@intCast(old_offset)); if (forward_offset + new_size <= trailing_end) { // Shift into the free space if possible @@ -1135,6 +1287,12 @@ fn ensureTotalCapacityPreciseInner(mf: *MappedFile, new_capacity: usize) (Alloca error.OperationUnsupported => {}, else => |e| return e, } + + mf.memory_map.write(io) catch |err| switch (err) { + error.WouldBlock => return error.Unexpected, // file was not opened as non-blocking + error.NotOpenForWriting => return error.Unexpected, // we definitely opened the file for writing + else => |e| return e, + }; unmap(mf); } @@ -1156,11 +1314,12 @@ pub fn unmap(mf: *MappedFile) void { } pub fn flush(mf: *MappedFile) (Io.Cancelable || error{MappedFileIo})!void { - mf.memory_map.write(mf.io) catch |err| switch (err) { + mf.flushInner() catch |err| switch (err) { error.Canceled => |e| return e, error.WouldBlock, // file was not opened as non-blocking error.NotOpenForWriting, // we definitely opened the file for writing + error.ReadOnlyFileSystem, => { mf.io_err = error.Unexpected; return error.MappedFileIo; @@ -1173,6 +1332,11 @@ pub fn flush(mf: *MappedFile) (Io.Cancelable || error{MappedFileIo})!void { }; } +fn flushInner(mf: *MappedFile) (Io.File.WritePositionalError || Io.File.SetTimestampsError)!void { + try mf.memory_map.write(mf.io); + if (is_windows) try mf.memory_map.file.setTimestampsNow(mf.io); +} + fn verify(mf: *MappedFile) void { const root = Node.Index.root.get(mf); assert(root.parent == .none); @@ -1207,3 +1371,184 @@ fn verifyNode(mf: *MappedFile, parent_ni: Node.Index) void { ni = node.next; } } + +const testing = std.testing; +fn testVerifyContent(mf: *@This(), ni: Node.Index, value: u8, init_len: usize) !void { + // Not using std.mem.allEqual, so we can get useful output + const slice = ni.slice(mf); + var buf: [256]u8 = undefined; + @memset(buf[0..init_len], value); + @memset(buf[init_len..], 0); + try testing.expectEqualSlices(u8, buf[0..slice.len], slice); +} + +test { + const gpa = testing.allocator; + + var tmp_dir = testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + var file = try tmp_dir.dir.createFile(testing.io, "test.mf", .{ .read = true }); + defer file.close(testing.io); + + var mf = try init(file, gpa, testing.io); + defer mf.deinit(gpa); + + const a = try mf.addFirstChildNode(gpa, .root, .{ .fixed = true, .alignment = .@"4" }); + const c = try mf.addLastChildNode(gpa, .root, .{ .fixed = true, .alignment = .@"4" }); + const b = try mf.addNodeAfter(gpa, a, .{ .fixed = true, .alignment = .@"16" }); + const d = try mf.addNodeAfter(gpa, b, .{ .alignment = .@"4" }); + + const a_init_size = 8; + const b_init_size = 16; + const c_init_size = 24; + const d_init_size = 28; + + // Resize without content + { + // Verify size is aligned forward + try d.resize(&mf, gpa, d_init_size - 1); + try a.resize(&mf, gpa, a_init_size - 2); + try c.resize(&mf, gpa, c_init_size); + try b.resize(&mf, gpa, b_init_size); + mf.verify(); + + const a_loc, const a_size = a.location(&mf).resolve(&mf); + const b_loc, const b_size = b.location(&mf).resolve(&mf); + const c_loc, const c_size = c.location(&mf).resolve(&mf); + _, const d_size = d.location(&mf).resolve(&mf); + try testing.expect(a_size >= a_init_size); + try testing.expect(b_size >= b_init_size); + try testing.expect(c_size >= c_init_size); + try testing.expect(d_size >= d_init_size); + try testing.expect(b_loc >= a_loc + a_size); + try testing.expect(c_loc >= b_loc + b_size); + } + + const a_exp_size = 24; + const b_exp_size = 28; + const c_exp_size = 48; + const d_exp_size = 32; + + // Resize with content + { + @memset(a.slice(&mf)[0..a_init_size], 0xaa); + @memset(b.slice(&mf)[0..b_init_size], 0xbb); + @memset(c.slice(&mf)[0..c_init_size], 0xcc); + @memset(d.slice(&mf)[0..d_init_size], 0xdd); + + try a.resize(&mf, gpa, a_exp_size); + try b.resize(&mf, gpa, b_exp_size); + try c.resize(&mf, gpa, c_exp_size); + try d.resize(&mf, gpa, d_exp_size); + mf.verify(); + + const a_loc, const a_size = a.location(&mf).resolve(&mf); + const b_loc, const b_size = b.location(&mf).resolve(&mf); + const c_loc, const c_size = c.location(&mf).resolve(&mf); + _, const d_size = d.location(&mf).resolve(&mf); + try testing.expect(a_size >= a_exp_size); + try testing.expect(b_size >= b_exp_size); + try testing.expect(c_size >= c_exp_size); + try testing.expect(d_size >= d_exp_size); + try testing.expect(b_loc >= a_loc + a_size); + try testing.expect(c_loc >= b_loc + b_size); + + try testVerifyContent(&mf, a, 0xaa, a_init_size); + try testVerifyContent(&mf, b, 0xbb, b_init_size); + try testVerifyContent(&mf, c, 0xcc, c_init_size); + try testVerifyContent(&mf, d, 0xdd, d_init_size); + } + + const child_init: []const struct { std.mem.Alignment, usize } = &.{ + .{ .@"16", 16 }, + .{ .@"1", 1 }, + .{ .@"1", 19 }, + .{ .@"1", 3 }, + .{ .@"8", 30 }, + .{ .@"2", 5 }, + .{ .@"1", 60 }, + .{ .@"2", 2 }, + .{ .@"16", 32 }, + }; + + var children: [child_init.len]Node.Index = undefined; + + // Differently-aligned fixed sibling nodes + { + for (children[0 .. children.len - 1], child_init[0 .. children.len - 1], 0..) |*ni, opts, i| { + ni.* = try mf.addLastChildNode(gpa, b, .{ + .alignment = opts.@"0", + .size = opts.@"1", + .fixed = true, + }); + + @memset(ni.slice(&mf)[0..opts.@"1"], @intCast(i + 1)); + } + // Shift differently-aligned nodes by inserting a node + children[children.len - 1] = try mf.addNodeAfter(gpa, children[3], .{ + .alignment = child_init[children.len - 1].@"0", + .size = child_init[children.len - 1].@"1", + .fixed = true, + }); + @memset(children[children.len - 1].slice(&mf), @intCast(children.len)); + + mf.verify(); + for (children, child_init, 0..) |ni, opts, i| { + try testVerifyContent(&mf, ni, @intCast(i + 1), opts.@"1"); + } + } + + // Shifting child nodes forward due via resize of parent.prev + { + try testing.expect(a.location(&mf).resolve(&mf)[1] < 64); + try a.resize(&mf, gpa, 64); + + try testVerifyContent(&mf, a, 0xaa, a_init_size); + try testVerifyContent(&mf, c, 0xcc, c_init_size); + try testVerifyContent(&mf, d, 0xdd, d_init_size); + for (children, child_init, 0..) |ni, opts, i| { + try testVerifyContent(&mf, ni, @intCast(i + 1), opts.@"1"); + } + } + + // Re-align last node into trailing free space within parent + { + try b.resize(&mf, gpa, b.location(&mf).resolve(&mf)[1] + 64); + + const last = children[children.len - 2]; + try last.realign(&mf, gpa, .@"4", true); + mf.verify(); + + for (children, child_init, 0..) |ni, opts, i| + try testVerifyContent(&mf, ni, @intCast(i + 1), opts.@"1"); + try testVerifyContent(&mf, c, 0xcc, c_init_size); + } + + // Re-align, shifting sibling nodes + { + try children[1].realign(&mf, gpa, .@"8", true); + mf.verify(); + + for (children, child_init, 0..) |ni, opts, i| + try testVerifyContent(&mf, ni, @intCast(i + 1), opts.@"1"); + try testVerifyContent(&mf, c, 0xcc, c_init_size); + } + + // Shrink and shift start of trailing node into free space + { + try mf.shrinkNode(gpa, a, 16, true); + mf.verify(); + + const a_loc, const a_size = a.location(&mf).resolve(&mf); + const b_loc, _ = b.location(&mf).resolve(&mf); + try testing.expectEqual(b_loc, a_loc + a_size); + + try testVerifyContent(&mf, a, 0xaa, a_init_size); + try testVerifyContent(&mf, c, 0xcc, c_init_size); + try testVerifyContent(&mf, d, 0xdd, d_init_size); + for (children, child_init, 0..) |ni, opts, i| { + try testVerifyContent(&mf, ni, @intCast(i + 1), opts.@"1"); + } + } +} diff --git a/src/main.zig b/src/main.zig @@ -1440,8 +1440,8 @@ fn buildOutputType( dev.check(.stdio_listen); listen = .stdio; } else if (mem.eql(u8, arg, "--debug-link-snapshot")) { - if (!build_options.enable_link_snapshots) { - warn("Zig was compiled without linker snapshots enabled (-Dlink-snapshot). --debug-link-snapshot has no effect.", .{}); + if (!build_options.enable_debug_extensions) { + warn("Zig was compiled without debug extensions. --debug-link-snapshot has no effect.", .{}); } else { enable_link_snapshots = true; } diff --git a/test/link.zig b/test/link.zig @@ -0,0 +1,341 @@ +pub fn addCases(ctx: *LinkContext) void { + if (ctx.target.result.isMinGW()) + @import("link/mingw.zig").addCases(ctx); + + if (ctx.includeTest("static-lib")) |case| { + const obj1 = case.addObject(.{ + .name = "obj1", + .name_prefix = false, + .name_target = false, + .use_llvm = true, + .use_lld = true, + .c_source_bytes = + \\int foo1 = 1; + \\int foo2 = 2; + \\unsigned int fooBar() { + \\ return foo1 + foo2; + \\} + , + }); + const obj2 = case.addObject(.{ + .name = "this_is_a_long_name", + .name_prefix = false, + .name_target = false, + .zig_source_bytes = + \\fn fooWeak() callconv(.c) usize { + \\ return 0xaabbccddaabbccdd; + \\} + \\export var foo_array: [2]u16 = .{ 0xffff, 0xabcd }; + \\export var foo_strong: usize = 0x1122334411223344; + \\comptime { + \\ @export(&fooWeak, .{ .name = "fooWeak", .linkage = .weak }); + \\ @export(&foo_strong, .{ .name = "foo_strong_alias", .linkage = .strong }); + \\} + , + }); + + const lib = case.addLibrary(.static, .{ + .name = "lib", + .name_prefix = false, + .name_target = false, + }); + lib.root_module.addObject(obj1); + lib.root_module.addObject(obj2); + + case.verifyObjdump(lib.getEmittedBin(), &.{ + "-s", + "--elements=file-type", + "--symbols", + "--only-symbol=foo", + }, .{ .use_llvm = true }); + + const exe = case.addExecutable(.{ + .name = "test", + .zig_source_bytes = + \\extern fn fooBar() c_uint; + \\extern fn fooWeak() usize; + \\extern var foo_array: [2]u16; + \\extern var foo_strong: usize; + \\extern var foo_strong_alias: usize; + \\pub fn main() !u8 { + \\ return @intFromBool(0xcd003365cd00df35 != fooBar() + + \\ fooWeak() + + \\ foo_array[1] + + \\ foo_strong + + \\ foo_strong_alias); + \\} + , + }); + exe.root_module.linkLibrary(lib); + + const run = case.addRunArtifact(exe); + run.addCheck(.{ .expect_term = .{ .exited = 0 } }); + } + + if (ctx.includeTest("tls")) |case| { + const obj = case.addObject(.{ + .name = "obj", + .zig_source_bytes = + \\threadlocal var threadlocal_var: u32 = 1234; + \\threadlocal var threadlocal_arr: [4]u16 = .{ 0x1111, 0x2222, 0x3333, 0x4444, }; + \\export fn threadlocal_read(a: *u32, b: *u16) void { + \\ a.* = threadlocal_var; + \\ b.* = threadlocal_arr[3]; + \\} + \\export fn threadlocal_write(a: u32, b: u16) void { + \\ threadlocal_var = a; + \\ threadlocal_arr[3] = b; + \\} + , + }); + + case.verifyObjdump(obj.getEmittedBin(), &.{ + "-s", + "--symbols", + "--only-symbol=threadlocal", + "--only-symbol=tls", + }, .{ .use_llvm = true }); + + const exe = case.addExecutable(.{ + .name = "test", + .zig_source_bytes = + \\extern fn threadlocal_read(a: *u32, b: *u16) void; + \\extern fn threadlocal_write(a: u32, b: u16) void; + \\threadlocal var threadlocal_foo: u64 = 0xcafecafecafecafe; + \\pub fn main() !u8 { + \\ var a: u32 = undefined; + \\ var b: u16 = undefined; + \\ threadlocal_read(&a, &b); + \\ if (a != 1234 or b != 0x4444) return 1; + \\ if (threadlocal_foo != 0xcafecafecafecafe) return 2; + \\ threadlocal_write(0xabcdabcd, 0x5555); + \\ threadlocal_foo = 1; + \\ threadlocal_read(&a, &b); + \\ if (a != 0xabcdabcd or b != 0x5555) return 3; + \\ if (threadlocal_foo != 1) return 4; + \\ return 0; + \\} + , + }); + exe.root_module.addObject(obj); + + const run = case.addRunArtifact(exe); + run.addCheck(.{ .expect_term = .{ .exited = 0 } }); + } + + if (ctx.includeTest("dynamic-lib-code")) |case| { + const lib = case.addLibrary(.dynamic, .{ + .name = "lib", + .name_target = false, + .zig_source_bytes = + \\export fn foo1() callconv(.c) u64 { + \\ return 0x1122334411223344; + \\} + \\export fn foo2() callconv(.c) u64 { + \\ return 0xaabbccddaabbccdd; + \\} + , + }); + + case.verifyObjdump(lib.getEmittedBin(), &.{ + "-s", + "--exports", + "--only-symbol=foo", + }, .{ .os = true }); + + if (ctx.target.result.os.tag == .windows) { + case.verifyObjdump(lib.getEmittedImplib(), &.{ + "-s", + "--exports=sort", + "--only-symbol=foo", + }, .{ .sub_name = "implib", .os = true, .arch = true }); + } + + const exe = case.addExecutable(.{ + .name = "test", + .zig_source_bytes = + \\extern fn foo1() u64; + \\pub fn main() !u8 { + \\ const foo2 = @extern( + \\ *const fn () callconv(.c) u64, + \\ .{ .name = "foo2", .is_dll_import = true }, + \\ ); + \\ return @intFromBool(0xbbde0021bbde0021 != foo1() + foo2()); + \\} + , + }); + exe.root_module.linkLibrary(lib); + + const run = case.addRunArtifact(exe); + run.addCheck(.{ .expect_term = .{ .exited = 0 } }); + } + + if (ctx.includeTest("dynamic-lib-data")) |case| { + const lib = case.addLibrary(.dynamic, .{ + .name = "lib", + .name_target = false, + .zig_source_bytes = + \\export var foo_array: [2]u16 = .{ 0xffff, 0xabcd }; + \\export var foo_strong: usize = 0x1122334411223344; + \\comptime { + \\ @export(&foo_strong, .{ .name = "foo_strong_alias", .linkage = .strong }); + \\} + , + }); + + case.verifyObjdump(lib.getEmittedBin(), &.{ + "-s", + "--exports", + "--only-symbol=foo", + }, .{}); + + if (ctx.target.result.os.tag == .windows) { + case.verifyObjdump(lib.getEmittedImplib(), &.{ + "-s", + "--exports=sort", + "--only-symbol=foo", + }, .{ .sub_name = "implib", .os = true, .arch = true }); + } + + const exe = case.addExecutable(.{ + .name = "test", + .zig_source_bytes = + \\pub fn main() !u8 { + \\ const foo_array = @extern(*[2]u16, .{ .name = "foo_array", .is_dll_import = true }); + \\ const foo_strong = @extern(*usize, .{ .name = "foo_strong", .is_dll_import = true }); + \\ const foo_strong_alias = @extern(*usize, .{ .name = "foo_strong_alias", .is_dll_import = true }); + \\ return @intFromBool(0x2244668822451255 != + \\ foo_array[1] + + \\ foo_strong.* + + \\ foo_strong_alias.*); + \\} + , + }); + exe.root_module.linkLibrary(lib); + + const run = case.addRunArtifact(exe); + run.addCheck(.{ .expect_term = .{ .exited = 0 } }); + } + + if (ctx.includeTest("abs-symbol")) |case| { + const abs = case.addObject(.{ + .name = "abs", + .use_llvm = true, // TODO: .globl not supported on self-hosted + .use_lld = true, + .asm_source_bytes = + \\.globl foo + \\foo = 0xcafecafe + \\ + , + }); + + const abs_reloc = case.addObject(.{ + .name = "abs_reloc", + .use_llvm = true, // TODO: .globl not supported on self-hosted + .use_lld = true, + .asm_source_bytes = + \\.data + \\.globl foo_copy + \\foo_copy: + \\.long foo + , + }); + + case.verifyObjdump(abs_reloc.getEmittedBin(), &.{ + "-s", + "--relocs", + }, .{ .arch = true }); + + const exe = case.addExecutable(.{ + .name = "test", + .zig_source_bytes = + \\extern var foo_copy: u32; + \\pub fn main() !u8 { + \\ return @intFromBool(foo_copy != 0xcafecafe); + \\} + , + }); + exe.root_module.addObject(abs); + exe.root_module.addObject(abs_reloc); + + const run = case.addRunArtifact(exe); + run.addCheck(.{ .expect_term = .{ .exited = 0 } }); + + if (!ctx.use_llvm) { + const exe_reloc_err = case.addExecutable(.{ + .name = "test-reloc-err", + .zig_source_bytes = + \\extern const foo: u32; + \\pub fn main() !u8 { + \\ return @intFromBool(foo != 0xcafecafe); + \\} + , + }); + exe_reloc_err.root_module.addObject(abs); + case.expectLinkErrors(exe_reloc_err, .{ + .contains = "error: absolute symbol 'foo' targeted by invalid relocation type: /?/", + }); + } + } + + if (ctx.includeTest("explicit-extern-lib-name")) |case| { + // TODO: Lld.zig does not look at explicit inputs to resolve explicit extern lib names + if (ctx.use_llvm) return; + + const lib1 = case.addLibrary(.dynamic, .{ + .name = "lib1", + .name_target = false, + .zig_source_bytes = + \\export fn foo() u8 { + \\ return 43; + \\} + , + }); + + const lib2 = case.addLibrary(.dynamic, .{ + .name = "lib2", + .name_target = false, + .zig_source_bytes = + \\export fn foo() u8 { + \\ return 42; + \\} + , + }); + + const lib3 = case.addLibrary(.static, .{ + .name = "lib3", + .zig_source_bytes = + \\extern fn foo() u8; + \\export fn callFoo() u8 { + \\ return foo(); + \\} + , + }); + + const exe = case.addExecutable(.{ + .name = "test", + .zig_source_bytes = + \\extern "explicit-extern-lib-name-lib2" fn foo() u8; + \\extern fn callFoo() u8; + \\pub fn main() !u8 { + \\ return foo() + callFoo(); + \\} + , + }); + exe.root_module.linkLibrary(lib1); + exe.root_module.linkLibrary(lib2); + // exe.root_module.addLibraryPath(.{ + // .generated = .{ + // .index = lib2.getEmittedBin().generated.index, + // .up = 1, + // }, + // }); + exe.root_module.linkLibrary(lib3); + + const run = case.addRunArtifact(exe); + run.addCheck(.{ .expect_term = .{ .exited = 84 } }); + } +} + +const LinkContext = @import("tests.zig").LinkContext; +const std = @import("std"); diff --git a/test/link/exports.zig b/test/link/exports.zig @@ -0,0 +1,9 @@ +export fn foo_fn() void {} +var foo_var: u32 = 1234; +comptime { + @export(&foo_var, .{ .name = "foo_var", .linkage = .strong }); +} +const foo_const: u64 = 5678; +comptime { + @export(&foo_const, .{ .name = "foo_const", .linkage = .strong }); +} diff --git a/test/link/mingw.zig b/test/link/mingw.zig @@ -0,0 +1,48 @@ +pub fn addCases(ctx: *LinkContext) void { + if (ctx.includeTest("ctor-dtor")) |case| { + if (!ctx.link_libc) return; + + const obj = case.addObject(.{ + .name = "obj", + .use_llvm = true, + .use_lld = true, + .c_source_bytes = + \\#include <stdlib.h> + \\int foo; + \\__attribute__((constructor)) + \\static void init_foo() { + \\ foo = 42; + \\} + \\__attribute__((destructor)) + \\static void deinit_foo() { + \\ exit(42); + \\} + , + }); + + const lib = case.addLibrary(.static, .{ + .name = "lib", + .name_prefix = false, + .name_target = false, + }); + lib.root_module.addObject(obj); + + const exe = case.addExecutable(.{ + .name = "test", + .zig_source_bytes = + \\extern var foo: u32; + \\pub fn main() !u8 { + \\ if (foo != 42) return 1; + \\ return 2; + \\} + , + }); + exe.root_module.addObject(obj); + + const run = case.addRunArtifact(exe); + run.addCheck(.{ .expect_term = .{ .exited = 42 } }); + } +} + +const LinkContext = @import("../tests.zig").LinkContext; +const std = @import("std"); diff --git a/test/link/snapshots/.gitattributes b/test/link/snapshots/.gitattributes @@ -0,0 +1 @@ +*.dmp eol=lf diff --git a/test/link/snapshots/abs-symbol.x86_64.dmp b/test/link/snapshots/abs-symbol.x86_64.dmp @@ -0,0 +1 @@ +xxxxxxxx ADDR32 xxxxxxxx UNDEF | foo diff --git a/test/link/snapshots/dynamic-lib-code.implib-x86_64-windows.dmp b/test/link/snapshots/dynamic-lib-code.implib-x86_64-windows.dmp @@ -0,0 +1,32 @@ + 0 date + 0 user_id + 0 group_id + 0 file_mode +xxxxxxxxxxxxxxxx size + second_linker type + | x symbols + | x members +xxxxxxxx __imp_foo1 +xxxxxxxx __imp_foo2 +xxxxxxxx foo1 +xxxxxxxx foo2 + 0 version + 8664 machine (AMD64) + 0 time_date_stamp +xxxxxxxxxxxxxxxx size_of_data +xxxxxxxxxxxxxxxx hint + CODE import_type + NAME name_type + symbol name | foo1 + import name | foo1 + dll | dynamic-lib-code-lib.dll + 0 version + 8664 machine (AMD64) + 0 time_date_stamp +xxxxxxxxxxxxxxxx size_of_data +xxxxxxxxxxxxxxxx hint + CODE import_type + NAME name_type + symbol name | foo2 + import name | foo2 + dll | dynamic-lib-code-lib.dll diff --git a/test/link/snapshots/dynamic-lib-code.windows.dmp b/test/link/snapshots/dynamic-lib-code.windows.dmp @@ -0,0 +1,13 @@ +Export directory: + 0 flags + 0 time_date_stamp + 0.00 version +xxxxxxxxxxxxxxxx name_rva + 1 ordinal_base +xxxxxxxxxxxxxxxx number_of_entries +xxxxxxxxxxxxxxxx number_of_names +xxxxxxxxxxxxxxxx export_address_table_rva +xxxxxxxxxxxxxxxx name_pointer_table_rva +xxxxxxxxxxxxxxxx ordinal_table_rva +xxxx xxxx xxxxxxxx | foo1 +xxxx xxxx xxxxxxxx | foo2 diff --git a/test/link/snapshots/dynamic-lib-data.dmp b/test/link/snapshots/dynamic-lib-data.dmp @@ -0,0 +1,14 @@ +Export directory: + 0 flags + 0 time_date_stamp + 0.00 version +xxxxxxxxxxxxxxxx name_rva + 1 ordinal_base +xxxxxxxxxxxxxxxx number_of_entries +xxxxxxxxxxxxxxxx number_of_names +xxxxxxxxxxxxxxxx export_address_table_rva +xxxxxxxxxxxxxxxx name_pointer_table_rva +xxxxxxxxxxxxxxxx ordinal_table_rva +xxxx xxxx xxxxxxxx | foo_array +xxxx xxxx xxxxxxxx | foo_strong +xxxx xxxx xxxxxxxx | foo_strong_alias diff --git a/test/link/snapshots/dynamic-lib-data.implib-x86_64-windows.dmp b/test/link/snapshots/dynamic-lib-data.implib-x86_64-windows.dmp @@ -0,0 +1,41 @@ + 0 date + 0 user_id + 0 group_id + 0 file_mode +xxxxxxxxxxxxxxxx size + second_linker type + | x symbols + | x members +xxxxxxxx __imp_foo_array +xxxxxxxx __imp_foo_strong +xxxxxxxx __imp_foo_strong_alias + 0 version + 8664 machine (AMD64) + 0 time_date_stamp +xxxxxxxxxxxxxxxx size_of_data +xxxxxxxxxxxxxxxx hint + DATA import_type + NAME name_type + symbol name | foo_array + import name | foo_array + dll | dynamic-lib-data-lib.dll + 0 version + 8664 machine (AMD64) + 0 time_date_stamp +xxxxxxxxxxxxxxxx size_of_data +xxxxxxxxxxxxxxxx hint + DATA import_type + NAME name_type + symbol name | foo_strong + import name | foo_strong + dll | dynamic-lib-data-lib.dll + 0 version + 8664 machine (AMD64) + 0 time_date_stamp +xxxxxxxxxxxxxxxx size_of_data +xxxxxxxxxxxxxxxx hint + DATA import_type + NAME name_type + symbol name | foo_strong_alias + import name | foo_strong_alias + dll | dynamic-lib-data-lib.dll diff --git a/test/link/snapshots/static-lib.llvm.dmp b/test/link/snapshots/static-lib.llvm.dmp @@ -0,0 +1,15 @@ +lib.lib: COFF archive +lib.lib(obj1.obj): COFF object +xxxx 00000000 1 NULL() EXTERNAL | fooBar +xxxx 00000000 2 NULL EXTERNAL | foo1 +xxxx 00000004 2 NULL EXTERNAL | foo2 +lib.lib(this_is_a_long_name.obj): COFF object +xxxx 00000000 1 NULL() STATIC | this_is_a_long_name.fooWeak +xxxx 00000000 2 NULL STATIC | this_is_a_long_name.foo_strong +xxxx 00000008 2 NULL STATIC | this_is_a_long_name.foo_array +xxxx 00000000 2 NULL EXTERNAL | foo_strong +xxxx 00000000 2 NULL EXTERNAL | foo_strong_alias +xxxx 00000008 2 NULL EXTERNAL | foo_array +xxxx 00000000 UNDEF NULL WEAK_EXTERNAL | fooWeak + | Weak External [falls back to relative ordinal 000000+2 via SEARCH_ALIAS] +xxxx 00000000 1 NULL() EXTERNAL | .weak.fooWeak.default.foo_strong diff --git a/test/link/snapshots/static-lib.no-llvm.dmp b/test/link/snapshots/static-lib.no-llvm.dmp @@ -0,0 +1,12 @@ +lib.lib: COFF archive +lib.lib(obj1.obj): COFF object +xxxx 00000000 1 NULL() EXTERNAL | fooBar +xxxx 00000000 2 NULL EXTERNAL | foo1 +xxxx 00000004 2 NULL EXTERNAL | foo2 +lib.lib(this_is_a_long_name.obj): COFF object +xxxx 00000000 4 NULL() EXTERNAL | this_is_a_long_name.fooWeak +xxxx 00000000 2 NULL EXTERNAL | foo_strong +xxxx 00000000 2 NULL EXTERNAL | foo_strong_alias +xxxx 00000010 2 NULL EXTERNAL | foo_array +xxxx 00000000 UNDEF NULL() WEAK_EXTERNAL | fooWeak + | Weak External [falls back to relative ordinal 000000-4 via SEARCH_ALIAS] diff --git a/test/link/snapshots/tls.llvm.dmp b/test/link/snapshots/tls.llvm.dmp @@ -0,0 +1,9 @@ +xxxx 00000000 6 NULL STATIC | .tls$ + | Section [size xxxxxxxx chksum a194a569 relocs 0000 lines 0000] +xxxx 00000000 1 NULL() STATIC | obj.threadlocal_write +xxxx 00000000 UNDEF NULL EXTERNAL | _tls_index +xxxx 00000000 6 NULL STATIC | obj.threadlocal_var +xxxx 00000004 6 NULL STATIC | obj.threadlocal_arr +xxxx 00000040 1 NULL() STATIC | obj.threadlocal_read +xxxx 00000000 1 NULL() EXTERNAL | threadlocal_write +xxxx 00000040 1 NULL() EXTERNAL | threadlocal_read diff --git a/test/link/snapshots/tls.no-llvm.dmp b/test/link/snapshots/tls.no-llvm.dmp @@ -0,0 +1,7 @@ +xxxx 00000000 5 NULL STATIC | .tls$ + | Section [size xxxxxxxx chksum 00000000 relocs 0000 lines 0000] +xxxx 00000000 5 NULL STATIC | obj.threadlocal_var +xxxx 00000008 5 NULL STATIC | obj.threadlocal_arr +xxxx 00000000 UNDEF NULL EXTERNAL | _tls_index +xxxx 00000000 4 NULL() EXTERNAL | threadlocal_write +xxxx 00000060 4 NULL() EXTERNAL | threadlocal_read diff --git a/test/src/Link.zig b/test/src/Link.zig @@ -0,0 +1,281 @@ +b: *Build, +step: *Step, +optimize: std.builtin.OptimizeMode, +target: std.Build.ResolvedTarget, +target_desc: []const u8, +use_llvm: bool, +use_lld: bool, +link_libc: bool, +test_filters: []const []const u8, +update_step: ?*Step.UpdateSourceFiles, +updated_snapshots: std.StringArrayHashMapUnmanaged(void), +max_rss: usize, + +pub fn includeTest(self: *Link, prefix: []const u8) ?Case { + if (for (self.test_filters) |filter| { + if (std.mem.containsAtLeast(u8, prefix, 1, filter)) break false; + } else self.test_filters.len > 0) return null; + + return .{ + .ctx = self, + .prefix = prefix, + }; +} + +pub fn sourcePath(self: *const Link, sub_path: []const u8) std.Build.LazyPath { + return self.b.path(self.b.pathJoin(&.{ "test/link", sub_path })); +} + +pub const Case = struct { + ctx: *Link, + prefix: []const u8, + + fn resolveName(self: *const Case, overlay: *const OverlayOptions) []const u8 { + if (!overlay.name_prefix and !overlay.name_target) + return overlay.name; + + if (overlay.name_prefix == overlay.name_target) + return self.ctx.b.fmt("{s}-{s}-{s}", .{ self.prefix, overlay.name, self.ctx.target_desc }) + else if (overlay.name_prefix) + return self.ctx.b.fmt("{s}-{s}", .{ self.prefix, overlay.name }) + else + return self.ctx.b.fmt("{s}-{s}", .{ overlay.name, self.ctx.target_desc }); + } + + pub fn addLibrary( + self: *const Case, + linkage: std.builtin.LinkMode, + overlay: OverlayOptions, + ) *Step.Compile { + return self.ctx.b.addLibrary(.{ + .linkage = linkage, + .name = self.resolveName(&overlay), + .root_module = self.ctx.createModule(overlay), + .use_llvm = overlay.use_llvm orelse self.ctx.use_llvm, + .use_lld = overlay.use_lld orelse self.ctx.use_lld, + }); + } + + pub fn addExecutable( + self: *const Case, + overlay: OverlayOptions, + ) *Step.Compile { + return self.ctx.b.addExecutable(.{ + .name = self.resolveName(&overlay), + .root_module = self.ctx.createModule(overlay), + .use_llvm = overlay.use_llvm orelse self.ctx.use_llvm, + .use_lld = overlay.use_lld orelse self.ctx.use_lld, + }); + } + + pub fn addRunArtifact( + self: *const Case, + exe: *Step.Compile, + ) *Step.Run { + const run_step = self.ctx.b.addRunArtifact(exe); + run_step.skip_foreign_checks = true; + self.ctx.step.dependOn(&run_step.step); + return run_step; + } + + pub fn addObject( + self: *const Case, + overlay: OverlayOptions, + ) *Step.Compile { + return self.ctx.b.addObject(.{ + .name = self.resolveName(&overlay), + .root_module = self.ctx.createModule(overlay), + .use_llvm = overlay.use_llvm orelse self.ctx.use_llvm, + .use_lld = overlay.use_lld orelse self.ctx.use_lld, + }); + } + + pub fn expectLinkErrors( + self: *const Case, + comp: *Step.Compile, + expected_errors: Step.Compile.ExpectedCompileErrors, + ) void { + comp.expect_errors = expected_errors; + const bin_file = comp.getEmittedBin(); + bin_file.addStepDependencies(self.ctx.step); + } + + const SnapshotScope = struct { + /// If a test case has multiple verifyObjdump calls, `opt_sub_name` should + /// be used to differentiate them. + sub_name: ?[]const u8 = null, + arch: bool = false, + os: bool = false, + abi: bool = false, + optimize: bool = false, + use_llvm: bool = false, + use_lld: bool = false, + link_libc: bool = false, + }; + + /// Verify the results of a `zig objdump` call against a snapshot, which + /// contains the expected output. Snapshots alias between all build + /// configurations by default, but by specifying fields in `scope`, + /// unique snapshot names are generated for each value of that field. + pub fn verifyObjdump( + self: *const Case, + file: Build.LazyPath, + args: []const []const u8, + scope: SnapshotScope, + ) void { + const ctx = self.ctx; + const snapshot_name = self.snapshotName(scope) catch @panic("OOM"); + const snapshot_sub_path = ctx.b.pathJoin(&.{ "test/link/snapshots/", snapshot_name }); + + // Many tests may read the same snapshot, so only use the first one to update. + // If there are differences in output, they will show up on the next test run. + if (ctx.update_step != null) { + const gop = ctx.updated_snapshots.getOrPut(ctx.b.allocator, snapshot_sub_path) catch @panic("OOM"); + if (gop.found_existing) return; + } + + const run_step = Step.Run.create(ctx.b, ctx.b.fmt( + "objdump {s} {s}", + .{ snapshot_name, ctx.target_desc }, + )); + run_step.addArgs(&.{ ctx.b.graph.zig_exe, "objdump" }); + run_step.addFileArg(file); + run_step.addArgs(args); + run_step.addCheck(.{ .expect_term = .{ .exited = 0 } }); + + if (ctx.update_step) |update_step| { + // Workaround for the build system not realizing objdump itself has changed + run_step.has_side_effects = true; + + const snapshot_update_path = run_step.captureStdOut(.{}); + update_step.addCopyFileToSource(snapshot_update_path, snapshot_sub_path); + } else { + run_step.addCheck(.{ .expect_stdout_snapshot = ctx.b.path(snapshot_sub_path) }); + } + + ctx.step.dependOn(&run_step.step); + } + + fn snapshotName( + self: *const Case, + scope: SnapshotScope, + ) ![]const u8 { + const ctx = self.ctx; + var snapshot_name: std.Io.Writer.Allocating = .init(ctx.b.allocator); + const w = &snapshot_name.writer; + + try w.writeAll(self.prefix); + var sep: u8 = '.'; + + if (try snapshotNameInner(w, scope.sub_name != null, &sep)) + try w.writeAll(scope.sub_name.?); + if (try snapshotNameInner(w, scope.arch, &sep)) + try w.print("{t}", .{ctx.target.result.cpu.arch}); + if (try snapshotNameInner(w, scope.os, &sep)) + try w.print("{t}", .{ctx.target.result.os.tag}); + if (try snapshotNameInner(w, scope.abi, &sep)) + try w.print("{t}", .{ctx.target.result.abi}); + if (try snapshotNameInner(w, scope.optimize, &sep)) + try w.print("{t}", .{ctx.optimize}); + if (try snapshotNameInner(w, scope.use_llvm, &sep)) + try w.writeAll(if (ctx.use_llvm) "llvm" else "no-llvm"); + if (try snapshotNameInner(w, scope.use_lld, &sep)) + try w.writeAll(if (ctx.use_lld) "lld" else "no-lld"); + if (try snapshotNameInner(w, scope.link_libc, &sep)) + try w.writeAll(if (ctx.link_libc) "libc" else "no-libc"); + + if (sep == '-') sep = '.'; + try w.writeByte(sep); + try w.writeAll("dmp"); + + return try snapshot_name.toOwnedSlice(); + } + + fn snapshotNameInner(w: *std.Io.Writer, cond: bool, sep: *u8) !bool { + if (cond) { + try w.writeByte(sep.*); + sep.* = '-'; + } + + return cond; + } +}; + +fn createModule(self: *const Link, overlay: OverlayOptions) *Build.Module { + const write_files = self.b.addWriteFiles(); + + const mod = self.b.createModule(.{ + .target = self.target, + .optimize = self.optimize, + .root_source_file = overlay.zig_source_file orelse rsf: { + const bytes = overlay.zig_source_bytes orelse break :rsf null; + const name = self.b.fmt("{s}.zig", .{overlay.name}); + break :rsf write_files.add(name, bytes); + }, + .link_libc = self.link_libc, // TODO: Should this be in overlay instead? + .pic = overlay.pic, + .strip = overlay.strip, + }); + + if (overlay.objcpp_source_bytes) |bytes| { + mod.addCSourceFile(.{ + .file = write_files.add("a.mm", bytes), + .flags = overlay.objcpp_source_flags, + }); + } + if (overlay.objc_source_bytes) |bytes| { + mod.addCSourceFile(.{ + .file = write_files.add("a.m", bytes), + .flags = overlay.objc_source_flags, + }); + } + if (overlay.cpp_source_bytes) |bytes| { + mod.addCSourceFile(.{ + .file = write_files.add("a.cpp", bytes), + .flags = overlay.cpp_source_flags, + }); + } + if (overlay.c_source_bytes) |bytes| { + mod.addCSourceFile(.{ + .file = write_files.add("a.c", bytes), + .flags = overlay.c_source_flags, + }); + } + if (overlay.asm_source_bytes) |bytes| { + mod.addAssemblyFile(write_files.add("a.s", bytes)); + } + + return mod; +} + +const OverlayOptions = struct { + name: []const u8, + /// Prefix the name with the test case prefix. + /// Unset if names with specific lengths are needed. + name_prefix: bool = true, + /// Prefix the name with `target_desc`. + /// Can be unset when the snapshot needs to contain the name, + /// so that snapshots can alias between targets. + name_target: bool = true, + asm_source_bytes: ?[]const u8 = null, + c_source_bytes: ?[]const u8 = null, + c_source_flags: []const []const u8 = &.{}, + cpp_source_bytes: ?[]const u8 = null, + cpp_source_flags: []const []const u8 = &.{}, + objc_source_bytes: ?[]const u8 = null, + objc_source_flags: []const []const u8 = &.{}, + objcpp_source_bytes: ?[]const u8 = null, + objcpp_source_flags: []const []const u8 = &.{}, + zig_source_bytes: ?[]const u8 = null, + zig_source_file: ?std.Build.LazyPath = null, + pic: ?bool = null, + strip: ?bool = null, + use_llvm: ?bool = null, + use_lld: ?bool = null, +}; + +const std = @import("std"); +const Build = std.Build; +const Step = Build.Step; + +const Link = @This(); diff --git a/test/standalone/shared_library/build.zig b/test/standalone/shared_library/build.zig @@ -7,11 +7,47 @@ pub fn build(b: *std.Build) void { const optimize: std.builtin.OptimizeMode = .Debug; const target = b.standardTargetOptions(.{}); - const exe_names: []const []const u8 = &.{ "test", "test-dync" }; - const lib_names: []const []const u8 = &.{ "mathtest", "mathtest-dync" }; - const lib_link_libc: []const bool = &.{ false, true }; + const exe_names: []const []const u8 = &.{ + "test", + "test-dync", + "test-no-llvm", + "test-no-llvm-dync", + "test-exe-no-llvm", + "test-dync-exe-no-llvm", + "test-no-llvm-exe-no-llvm", + "test-no-llvm-dync-exe-no-llvm", + }; + const lib_names: []const []const u8 = &.{ + "mathtest", + "mathtest-dync", + "mathtest-no-llvm", + "mathtest-no-llvm-dync", + "mathtest-exe-no-llvm", + "mathtest-dync-exe-no-llvm", + "mathtest-no-llvm-exe-no-llvm", + "mathtest-no-llvm-dync-exe-no-llvm", + }; + const lib_link_libc: []const bool = &.{ false, true, false, true, false, true, false, true }; + const lib_use_llvm: []const bool = &.{ true, true, false, false, true, true, false, false }; + const exe_use_llvm: []const bool = &.{ true, true, true, true, false, false, false, false }; + + for ( + exe_names, + lib_names, + lib_link_libc, + lib_use_llvm, + exe_use_llvm, + ) |exe_name, lib_name, dyn_libc, lib_llvm, exe_llvm| { + const no_llvm = !lib_llvm or !exe_llvm; + if (no_llvm and target.result.os.tag == .macos) continue; // TODO + if (no_llvm and target.result.os.tag == .freebsd) continue; // TODO + if (no_llvm and target.result.os.tag == .netbsd) continue; // TODO + if (no_llvm and target.result.os.tag == .openbsd) continue; // TODO + if (no_llvm and target.result.cpu.arch == .aarch64) continue; // TODO + if (no_llvm and target.result.cpu.arch == .loongarch64) continue; // TODO + if (no_llvm and target.result.cpu.arch == .powerpc64le) continue; // TODO + if (no_llvm and target.result.cpu.arch == .s390x) continue; // TODO - for (exe_names, lib_names, lib_link_libc) |exe_name, lib_name, dyn_libc| { const lib = b.addLibrary(.{ .linkage = .dynamic, .name = lib_name, @@ -22,6 +58,7 @@ pub fn build(b: *std.Build) void { .optimize = optimize, .link_libc = dyn_libc, }), + .use_llvm = lib_llvm, }); const exe = b.addExecutable(.{ @@ -32,6 +69,7 @@ pub fn build(b: *std.Build) void { .optimize = optimize, .link_libc = true, }), + .use_llvm = exe_llvm, }); exe.root_module.addCSourceFile(.{ .file = b.path("test.c"), diff --git a/test/standalone/shared_library/mathtest.zig b/test/standalone/shared_library/mathtest.zig @@ -1,3 +1,5 @@ +export var exported_var: i32 = 9999; + export fn add(a: i32, b: i32) i32 { return a + b; } diff --git a/test/standalone/shared_library/test.c b/test/standalone/shared_library/test.c @@ -7,7 +7,16 @@ #include <stdint.h> int32_t add(int32_t a, int32_t b); +#if _WIN32 +#define IMPORT __declspec(dllimport) +#else +#define IMPORT +#endif + +extern IMPORT int32_t exported_var; + int main(int argc, char **argv) { assert(add(42, 1337) == 1379); + assert(exported_var == 9999); return 0; } diff --git a/test/standalone/static_c_lib/build.zig b/test/standalone/static_c_lib/build.zig @@ -5,26 +5,76 @@ pub fn build(b: *std.Build) void { b.default_step = test_step; const optimize: std.builtin.OptimizeMode = .Debug; + const target = b.standardTargetOptions(.{}); - const foo = b.addLibrary(.{ - .linkage = .static, - .name = "foo", - .root_module = b.createModule(.{ - .root_source_file = null, - .optimize = optimize, - .target = b.graph.host, - }), - }); - foo.root_module.addCSourceFile(.{ .file = b.path("foo.c"), .flags = &[_][]const u8{} }); - foo.root_module.addIncludePath(b.path(".")); + const exe_names: []const []const u8 = &.{ + "test", + "test-dync", + "test-no-llvm", + "test-no-llvm-dync", + "test-exe-no-llvm", + "test-dync-exe-no-llvm", + "test-no-llvm-exe-no-llvm", + "test-no-llvm-dync-exe-no-llvm", + }; + const lib_names: []const []const u8 = &.{ + "foo", + "foo-dync", + "foo-no-llvm", + "foo-no-llvm-dync", + "foo-exe-no-llvm", + "foo-dync-exe-no-llvm", + "foo-no-llvm-exe-no-llvm", + "foo-no-llvm-dync-exe-no-llvm", + }; + const lib_link_libc: []const bool = &.{ false, true, false, true, false, true, false, true }; + const lib_use_llvm: []const bool = &.{ true, true, false, false, true, true, false, false }; + const exe_use_llvm: []const bool = &.{ true, true, true, true, false, false, false, false }; - const test_exe = b.addTest(.{ .root_module = b.createModule(.{ - .root_source_file = b.path("foo.zig"), - .target = b.graph.host, - .optimize = optimize, - }) }); - test_exe.root_module.linkLibrary(foo); - test_exe.root_module.addIncludePath(b.path(".")); + for ( + exe_names, + lib_names, + lib_link_libc, + lib_use_llvm, + exe_use_llvm, + ) |exe_name, lib_name, dyn_libc, lib_llvm, exe_llvm| { + const no_llvm = !lib_llvm or !exe_llvm; + if (no_llvm and target.result.os.tag == .macos) continue; // TODO + if (no_llvm and target.result.os.tag == .freebsd) continue; // TODO + if (no_llvm and target.result.os.tag == .netbsd) continue; // TODO + if (no_llvm and target.result.os.tag == .openbsd) continue; // TODO + if (no_llvm and target.result.cpu.arch == .aarch64) continue; // TODO + if (no_llvm and target.result.cpu.arch == .loongarch64) continue; // TODO + if (no_llvm and target.result.cpu.arch == .powerpc64le) continue; // TODO + if (no_llvm and target.result.cpu.arch == .s390x) continue; // TODO - test_step.dependOn(&b.addRunArtifact(test_exe).step); + const foo = b.addLibrary(.{ + .linkage = .static, + .name = lib_name, + .root_module = b.createModule(.{ + .root_source_file = null, + .optimize = optimize, + .target = target, + .link_libc = dyn_libc, + }), + .use_llvm = lib_llvm, + }); + foo.root_module.addCSourceFile(.{ .file = b.path("foo.c"), .flags = &[_][]const u8{} }); + foo.root_module.addIncludePath(b.path(".")); + + const test_exe = b.addTest(.{ + .name = exe_name, + .root_module = b.createModule(.{ + .root_source_file = b.path("foo.zig"), + .target = target, + .optimize = optimize, + .link_libc = dyn_libc, + }), + .use_llvm = exe_llvm, + }); + test_exe.root_module.linkLibrary(foo); + test_exe.root_module.addIncludePath(b.path(".")); + + test_step.dependOn(&b.addRunArtifact(test_exe).step); + } } diff --git a/test/tests.zig b/test/tests.zig @@ -10,6 +10,7 @@ const error_traces = @import("error_traces.zig"); const stack_traces = @import("stack_traces.zig"); const llvm_ir = @import("llvm_ir.zig"); const libc = @import("libc.zig"); +const link = @import("link.zig"); // Implementations pub const ErrorTracesContext = @import("src/ErrorTrace.zig"); @@ -17,6 +18,7 @@ pub const StackTracesContext = @import("src/StackTrace.zig"); pub const DebuggerContext = @import("src/Debugger.zig"); pub const LlvmIrContext = @import("src/LlvmIr.zig"); pub const LibcContext = @import("src/Libc.zig"); +pub const LinkContext = @import("src/Link.zig"); const ModuleTestTarget = struct { linkage: ?std.builtin.LinkMode = null, @@ -2019,42 +2021,132 @@ const c_abi_targets = blk: { }, }, - //.{ - // .target = .{ - // .cpu_arch = .x86_64, - // .os_tag = .windows, - // .abi = .gnu, - // }, - // .use_llvm = false, - // .c_defines = &.{"ZIG_BACKEND_STAGE2_X86_64"}, - //}, - //.{ - // .target = .{ - // .cpu_arch = .x86_64, - // .cpu_model = .{ .explicit = &std.Target.x86.cpu.x86_64_v2 }, - // .os_tag = .windows, - // .abi = .gnu, - // }, - // .use_llvm = false, - // .c_defines = &.{"ZIG_BACKEND_STAGE2_X86_64"}, - //}, - //.{ - // .target = .{ - // .cpu_arch = .x86_64, - // .cpu_model = .{ .explicit = &std.Target.x86.cpu.x86_64_v3 }, - // .os_tag = .windows, - // .abi = .gnu, - // }, - // .use_llvm = false, - // .c_defines = &.{"ZIG_BACKEND_STAGE2_X86_64"}, - //}, .{ .target = .{ .cpu_arch = .x86_64, .os_tag = .windows, .abi = .gnu, }, + .use_llvm = false, + .c_defines = &.{"ZIG_BACKEND_STAGE2_X86_64"}, + }, + .{ + .target = .{ + .cpu_arch = .x86_64, + .cpu_model = .{ .explicit = &std.Target.x86.cpu.x86_64_v2 }, + .os_tag = .windows, + .abi = .gnu, + }, + .use_llvm = false, + .c_defines = &.{"ZIG_BACKEND_STAGE2_X86_64"}, + }, + .{ + .target = .{ + .cpu_arch = .x86_64, + .cpu_model = .{ .explicit = &std.Target.x86.cpu.x86_64_v3 }, + .os_tag = .windows, + .abi = .gnu, + }, + .use_llvm = false, + .c_defines = &.{"ZIG_BACKEND_STAGE2_X86_64"}, + }, + .{ + .target = .{ + .cpu_arch = .x86_64, + .os_tag = .windows, + .abi = .gnu, + }, + .use_llvm = true, + }, + }; +}; + +const LinkTarget = struct { + target: std.Target.Query = .{}, + optimize_mode: std.builtin.OptimizeMode = .Debug, + link_libc: bool = false, + use_llvm: bool = false, + use_lld: bool = false, +}; + +const link_targets = blk: { + @setEvalBranchQuota(30000); + break :blk [_]LinkTarget{ + // Native Targets + + // .{ + // .use_llvm = true, + // }, + + // Windows Targets + + .{ + .target = .{ + .cpu_arch = .x86_64, + .os_tag = .windows, + .abi = .gnu, + }, + }, + .{ + .target = .{ + .cpu_arch = .x86_64, + .os_tag = .windows, + .abi = .gnu, + }, + .link_libc = true, + }, + .{ + .target = .{ + .cpu_arch = .x86_64, + .os_tag = .windows, + .abi = .gnu, + }, + .use_llvm = true, + .use_lld = true, + }, + .{ + .target = .{ + .cpu_arch = .x86_64, + .os_tag = .windows, + .abi = .gnu, + }, + .link_libc = true, + .use_llvm = true, + .use_lld = true, + }, + .{ + .target = .{ + .cpu_arch = .x86_64, + .os_tag = .windows, + .abi = .msvc, + }, + }, + .{ + .target = .{ + .cpu_arch = .x86_64, + .os_tag = .windows, + .abi = .msvc, + }, + .link_libc = true, + }, + .{ + .target = .{ + .cpu_arch = .x86_64, + .os_tag = .windows, + .abi = .msvc, + }, + .use_llvm = true, + .use_lld = true, + }, + .{ + .target = .{ + .cpu_arch = .x86_64, + .os_tag = .windows, + .abi = .msvc, + }, + .link_libc = true, .use_llvm = true, + .use_lld = true, }, }; }; @@ -3083,6 +3175,77 @@ pub fn addCAbiTests(b: *std.Build, options: CAbiTestOptions) *Step { return step; } +const LinkTestOptions = struct { + test_target_filters: []const []const u8, + test_filters: []const []const u8, + optimize_modes: []const OptimizeMode, + skip_non_native: bool, + skip_windows: bool, + skip_llvm: bool, + max_rss: usize, +}; + +pub fn addLinkTests(b: *std.Build, options: LinkTestOptions) *Step { + const step = b.step("test-link", "Run the linker tests"); + const update_snapshots = b.option( + bool, + "link-snapshot-update", + "Update linker test snapshots in-place instead of testing against them", + ) orelse false; + + for (link_targets) |link_target| { + if (options.skip_non_native and !link_target.target.isNative()) continue; + if (options.skip_windows and link_target.target.os_tag == .windows) continue; + + const resolved_target = b.resolveTargetQuery(link_target.target); + const triple_txt = resolved_target.query.zigTriple(b.allocator) catch @panic("OOM"); + const target = &resolved_target.result; + + if (options.test_target_filters.len > 0) { + for (options.test_target_filters) |filter| { + if (std.mem.indexOf(u8, triple_txt, filter) != null) break; + } else continue; + } + + for (options.optimize_modes) |optimize_mode| { + if (link_target.optimize_mode != optimize_mode) continue; + if (link_target.link_libc and target.abi == .msvc and b.graph.host.result.os.tag != .windows) continue; + const would_use_llvm = wouldUseLlvm(link_target.use_llvm, link_target.target, optimize_mode); + if (options.skip_llvm and would_use_llvm) continue; + + const opt_update_step = if (update_snapshots) update: { + const update_step = Step.UpdateSourceFiles.create(b); + step.dependOn(&update_step.step); + break :update update_step; + } else null; + + var context: LinkContext = .{ + .b = b, + .step = step, + .optimize = optimize_mode, + .target = resolved_target, + .target_desc = std.fmt.allocPrint(b.allocator, "{s}-{t}{s}{s}{s}", .{ + target.zigTriple(b.allocator) catch @panic("OOM"), + optimize_mode, + if (link_target.use_llvm) "-llvm" else "", + if (link_target.use_lld) "-lld" else "", + if (link_target.link_libc) "-libc" else "", + }) catch @panic("OOM"), + .use_llvm = link_target.use_llvm, + .use_lld = link_target.use_lld, + .link_libc = link_target.link_libc, + .test_filters = options.test_filters, + .update_step = opt_update_step, + .updated_snapshots = .empty, + .max_rss = options.max_rss, + }; + + link.addCases(&context); + } + } + return step; +} + pub fn addCases( b: *std.Build, parent_step: *Step,