Commit a88c0b4d08

Jakub Konka <kubkon@jakubkonka.com>
2023-04-01 11:51:05
link: handle -u flag in all linkers
Also clean up parsing of linker args - reuse `ArgsIterator`. In MachO, ensure we add every symbol marked with `-u` as undefined before proceeding with symbol resolution. Additionally, ensure those symbols are never garbage collected. MachO entry_in_dylib test: pass `-u _my_main` when linking executable so that it is not incorrectly garbage collected by the linker.
1 parent 2dd1784
lib/std/Build/CompileStep.zig
@@ -202,6 +202,11 @@ subsystem: ?std.Target.SubSystem = null,
 
 entry_symbol_name: ?[]const u8 = null,
 
+/// List of symbols forced as undefined in the symbol table
+/// thus forcing their resolution by the linker.
+/// Corresponds to `-u <symbol>` for ELF/MachO and `/include:<symbol>` for COFF/PE.
+force_undefined_symbols: std.StringHashMap(void),
+
 /// Overrides the default stack size
 stack_size: ?u64 = null,
 
@@ -386,6 +391,7 @@ pub fn create(owner: *std.Build, options: Options) *CompileStep {
         .override_dest_dir = null,
         .installed_path = null,
         .install_step = null,
+        .force_undefined_symbols = StringHashMap(void).init(owner.allocator),
 
         .output_path_source = GeneratedFile{ .step = &self.step },
         .output_lib_path_source = GeneratedFile{ .step = &self.step },
@@ -568,6 +574,11 @@ pub fn setLinkerScriptPath(self: *CompileStep, source: FileSource) void {
     source.addStepDependencies(&self.step);
 }
 
+pub fn forceUndefinedSymbol(self: *CompileStep, symbol_name: []const u8) void {
+    const b = self.step.owner;
+    self.force_undefined_symbols.put(b.dupe(symbol_name), {}) catch @panic("OOM");
+}
+
 pub fn linkFramework(self: *CompileStep, framework_name: []const u8) void {
     const b = self.step.owner;
     self.frameworks.put(b.dupe(framework_name), .{}) catch @panic("OOM");
@@ -1266,6 +1277,14 @@ fn make(step: *Step, prog_node: *std.Progress.Node) !void {
         try zig_args.append(entry);
     }
 
+    {
+        var it = self.force_undefined_symbols.keyIterator();
+        while (it.next()) |symbol_name| {
+            try zig_args.append("--force_undefined");
+            try zig_args.append(symbol_name.*);
+        }
+    }
+
     if (self.stack_size) |stack_size| {
         try zig_args.append("--stack");
         try zig_args.append(try std.fmt.allocPrint(b.allocator, "{}", .{stack_size}));
src/link/Coff/lld.zig
@@ -63,7 +63,7 @@ pub fn linkWithLLD(self: *Coff, comp: *Compilation, prog_node: *std.Progress.Nod
         man = comp.cache_parent.obtain();
         self.base.releaseLock();
 
-        comptime assert(Compilation.link_hash_implementation_version == 7);
+        comptime assert(Compilation.link_hash_implementation_version == 8);
 
         for (self.base.options.objects) |obj| {
             _ = try man.addFile(obj.path, null);
src/link/MachO/dead_strip.zig
@@ -12,6 +12,7 @@ const Allocator = mem.Allocator;
 const AtomIndex = @import("zld.zig").AtomIndex;
 const Atom = @import("ZldAtom.zig");
 const SymbolWithLoc = @import("zld.zig").SymbolWithLoc;
+const SymbolResolver = @import("zld.zig").SymbolResolver;
 const UnwindInfo = @import("UnwindInfo.zig");
 const Zld = @import("zld.zig").Zld;
 
@@ -19,7 +20,7 @@ const N_DEAD = @import("zld.zig").N_DEAD;
 
 const AtomTable = std.AutoHashMap(AtomIndex, void);
 
-pub fn gcAtoms(zld: *Zld) !void {
+pub fn gcAtoms(zld: *Zld, resolver: *const SymbolResolver) !void {
     const gpa = zld.gpa;
 
     var arena = std.heap.ArenaAllocator.init(gpa);
@@ -31,12 +32,25 @@ pub fn gcAtoms(zld: *Zld) !void {
     var alive = AtomTable.init(arena.allocator());
     try alive.ensureTotalCapacity(@intCast(u32, zld.atoms.items.len));
 
-    try collectRoots(zld, &roots);
+    try collectRoots(zld, &roots, resolver);
     try mark(zld, roots, &alive);
     prune(zld, alive);
 }
 
-fn collectRoots(zld: *Zld, roots: *AtomTable) !void {
+fn addRoot(zld: *Zld, roots: *AtomTable, file: u32, sym_loc: SymbolWithLoc) !void {
+    const sym = zld.getSymbol(sym_loc);
+    assert(!sym.undf());
+    const object = &zld.objects.items[file];
+    const atom_index = object.getAtomIndexForSymbol(sym_loc.sym_index).?; // panic here means fatal error
+    log.debug("root(ATOM({d}, %{d}, {d}))", .{
+        atom_index,
+        zld.getAtom(atom_index).sym_index,
+        file,
+    });
+    _ = try roots.getOrPut(atom_index);
+}
+
+fn collectRoots(zld: *Zld, roots: *AtomTable, resolver: *const SymbolResolver) !void {
     log.debug("collecting roots", .{});
 
     switch (zld.options.output_mode) {
@@ -44,15 +58,7 @@ fn collectRoots(zld: *Zld, roots: *AtomTable) !void {
             // Add entrypoint as GC root
             const global: SymbolWithLoc = zld.getEntryPoint();
             if (global.getFile()) |file| {
-                const object = zld.objects.items[file];
-                const atom_index = object.getAtomIndexForSymbol(global.sym_index).?; // panic here means fatal error
-                _ = try roots.getOrPut(atom_index);
-
-                log.debug("root(ATOM({d}, %{d}, {?d}))", .{
-                    atom_index,
-                    zld.getAtom(atom_index).sym_index,
-                    zld.getAtom(atom_index).getFile(),
-                });
+                try addRoot(zld, roots, file, global);
             } else {
                 assert(zld.getSymbol(global).undf()); // Stub as our entrypoint is in a dylib.
             }
@@ -64,20 +70,22 @@ fn collectRoots(zld: *Zld, roots: *AtomTable) !void {
                 const sym = zld.getSymbol(global);
                 if (sym.undf()) continue;
 
-                const file = global.getFile() orelse continue; // synthetic globals are atomless
-                const object = zld.objects.items[file];
-                const atom_index = object.getAtomIndexForSymbol(global.sym_index).?; // panic here means fatal error
-                _ = try roots.getOrPut(atom_index);
-
-                log.debug("root(ATOM({d}, %{d}, {?d}))", .{
-                    atom_index,
-                    zld.getAtom(atom_index).sym_index,
-                    zld.getAtom(atom_index).getFile(),
-                });
+                if (global.getFile()) |file| {
+                    try addRoot(zld, roots, file, global);
+                }
             }
         },
     }
 
+    // Add all symbols force-defined by the user.
+    for (zld.options.force_undefined_symbols.keys()) |sym_name| {
+        const global_index = resolver.table.get(sym_name).?;
+        const global = zld.globals.items[global_index];
+        const sym = zld.getSymbol(global);
+        assert(!sym.undf());
+        try addRoot(zld, roots, global.getFile().?, global);
+    }
+
     for (zld.objects.items) |object| {
         const has_subsections = object.header.flags & macho.MH_SUBSECTIONS_VIA_SYMBOLS != 0;
 
src/link/MachO/zld.zig
@@ -932,20 +932,29 @@ pub const Zld = struct {
         }
     }
 
+    fn forceSymbolDefined(self: *Zld, name: []const u8, resolver: *SymbolResolver) !void {
+        const sym_index = try self.allocateSymbol();
+        const sym_loc = SymbolWithLoc{ .sym_index = sym_index };
+        const sym = self.getSymbolPtr(sym_loc);
+        sym.n_strx = try self.strtab.insert(self.gpa, name);
+        sym.n_type = macho.N_UNDF | macho.N_EXT;
+        const global_index = try self.addGlobal(sym_loc);
+        try resolver.table.putNoClobber(name, global_index);
+        try resolver.unresolved.putNoClobber(global_index, {});
+    }
+
     fn resolveSymbols(self: *Zld, resolver: *SymbolResolver) !void {
         // We add the specified entrypoint as the first unresolved symbols so that
         // we search for it in libraries should there be no object files specified
         // on the linker line.
         if (self.options.output_mode == .Exe) {
             const entry_name = self.options.entry orelse load_commands.default_entry_point;
-            const sym_index = try self.allocateSymbol();
-            const sym_loc = SymbolWithLoc{ .sym_index = sym_index };
-            const sym = self.getSymbolPtr(sym_loc);
-            sym.n_strx = try self.strtab.insert(self.gpa, entry_name);
-            sym.n_type = macho.N_UNDF | macho.N_EXT;
-            const global_index = try self.addGlobal(sym_loc);
-            try resolver.table.putNoClobber(entry_name, global_index);
-            try resolver.unresolved.putNoClobber(global_index, {});
+            try self.forceSymbolDefined(entry_name, resolver);
+        }
+
+        // Force resolution of any symbols requested by the user.
+        for (self.options.force_undefined_symbols.keys()) |sym_name| {
+            try self.forceSymbolDefined(sym_name, resolver);
         }
 
         for (self.objects.items, 0..) |_, object_id| {
@@ -3539,7 +3548,7 @@ pub const SymbolWithLoc = extern struct {
     }
 };
 
-const SymbolResolver = struct {
+pub const SymbolResolver = struct {
     arena: Allocator,
     table: std.StringHashMap(u32),
     unresolved: std.AutoArrayHashMap(u32, void),
@@ -3600,7 +3609,7 @@ pub fn linkWithZld(macho_file: *MachO, comp: *Compilation, prog_node: *std.Progr
         // We are about to obtain this lock, so here we give other processes a chance first.
         macho_file.base.releaseLock();
 
-        comptime assert(Compilation.link_hash_implementation_version == 7);
+        comptime assert(Compilation.link_hash_implementation_version == 8);
 
         for (options.objects) |obj| {
             _ = try man.addFile(obj.path, null);
@@ -3630,6 +3639,7 @@ pub fn linkWithZld(macho_file: *MachO, comp: *Compilation, prog_node: *std.Progr
         }
         link.hashAddSystemLibs(&man.hash, options.system_libs);
         man.hash.addOptionalBytes(options.sysroot);
+        man.hash.addListOfBytes(options.force_undefined_symbols.keys());
         try man.addOptionalFile(options.entitlements);
 
         // We don't actually care whether it's a cache hit or miss; we just
@@ -4035,7 +4045,7 @@ pub fn linkWithZld(macho_file: *MachO, comp: *Compilation, prog_node: *std.Progr
         }
 
         if (gc_sections) {
-            try dead_strip.gcAtoms(&zld);
+            try dead_strip.gcAtoms(&zld, &resolver);
         }
 
         try zld.createDyldPrivateAtom();
src/link/Elf.zig
@@ -1305,7 +1305,7 @@ fn linkWithLLD(self: *Elf, comp: *Compilation, prog_node: *std.Progress.Node) !v
         // We are about to obtain this lock, so here we give other processes a chance first.
         self.base.releaseLock();
 
-        comptime assert(Compilation.link_hash_implementation_version == 7);
+        comptime assert(Compilation.link_hash_implementation_version == 8);
 
         try man.addOptionalFile(self.base.options.linker_script);
         try man.addOptionalFile(self.base.options.version_script);
src/link/Wasm.zig
@@ -3059,7 +3059,7 @@ fn linkWithZld(wasm: *Wasm, comp: *Compilation, prog_node: *std.Progress.Node) l
         // We are about to obtain this lock, so here we give other processes a chance first.
         wasm.base.releaseLock();
 
-        comptime assert(Compilation.link_hash_implementation_version == 7);
+        comptime assert(Compilation.link_hash_implementation_version == 8);
 
         for (options.objects) |obj| {
             _ = try man.addFile(obj.path, null);
@@ -4086,7 +4086,7 @@ fn linkWithLLD(wasm: *Wasm, comp: *Compilation, prog_node: *std.Progress.Node) !
         // We are about to obtain this lock, so here we give other processes a chance first.
         wasm.base.releaseLock();
 
-        comptime assert(Compilation.link_hash_implementation_version == 7);
+        comptime assert(Compilation.link_hash_implementation_version == 8);
 
         for (wasm.base.options.objects) |obj| {
             _ = try man.addFile(obj.path, null);
src/clang_options_data.zig
@@ -1448,7 +1448,7 @@ flagpsl("MT"),
 .{
     .name = "u",
     .syntax = .flag,
-    .zig_equivalent = .other,
+    .zig_equivalent = .force_undefined_symbol,
     .pd1 = true,
     .pd2 = false,
     .psl = true,
@@ -7170,7 +7170,14 @@ joinpd1("d"),
     .pd2 = false,
     .psl = true,
 },
-jspd1("u"),
+.{
+    .name = "u",
+    .syntax = .joined_or_separate,
+    .zig_equivalent = .force_undefined_symbol,
+    .pd1 = true,
+    .pd2 = false,
+    .psl = false,
+},
 .{
     .name = "x",
     .syntax = .joined_or_separate,
src/Compilation.zig
@@ -602,6 +602,7 @@ pub const InitOptions = struct {
     parent_compilation_link_libc: bool = false,
     hash_style: link.HashStyle = .both,
     entry: ?[]const u8 = null,
+    force_undefined_symbols: std.StringArrayHashMapUnmanaged(void) = .{},
     stack_size_override: ?u64 = null,
     image_base_override: ?u64 = null,
     self_exe_path: ?[]const u8 = null,
@@ -1523,7 +1524,7 @@ pub fn create(gpa: Allocator, options: InitOptions) !*Compilation {
             .headerpad_size = options.headerpad_size,
             .headerpad_max_install_names = options.headerpad_max_install_names,
             .dead_strip_dylibs = options.dead_strip_dylibs,
-            .force_undefined_symbols = .{},
+            .force_undefined_symbols = options.force_undefined_symbols,
             .pdb_source_path = options.pdb_source_path,
             .pdb_out_path = options.pdb_out_path,
         });
@@ -2186,7 +2187,7 @@ fn prepareWholeEmitSubPath(arena: Allocator, opt_emit: ?EmitLoc) error{OutOfMemo
 /// to remind the programmer to update multiple related pieces of code that
 /// are in different locations. Bump this number when adding or deleting
 /// anything from the link cache manifest.
-pub const link_hash_implementation_version = 7;
+pub const link_hash_implementation_version = 8;
 
 fn addNonIncrementalStuffToCacheManifest(comp: *Compilation, man: *Cache.Manifest) !void {
     const gpa = comp.gpa;
@@ -2196,7 +2197,7 @@ fn addNonIncrementalStuffToCacheManifest(comp: *Compilation, man: *Cache.Manifes
     defer arena_allocator.deinit();
     const arena = arena_allocator.allocator();
 
-    comptime assert(link_hash_implementation_version == 7);
+    comptime assert(link_hash_implementation_version == 8);
 
     if (comp.bin_file.options.module) |mod| {
         const main_zig_file = try mod.main_pkg.root_src_directory.join(arena, &[_][]const u8{
src/link.zig
@@ -186,8 +186,7 @@ pub const Options = struct {
 
     /// List of symbols forced as undefined in the symbol table
     /// thus forcing their resolution by the linker.
-    /// Corresponds to `-u <symbol>` for ELF and `/include:<symbol>` for COFF/PE.
-    /// TODO add handling for MachO.
+    /// Corresponds to `-u <symbol>` for ELF/MachO and `/include:<symbol>` for COFF/PE.
     force_undefined_symbols: std.StringArrayHashMapUnmanaged(void),
 
     version: ?std.builtin.Version,
src/main.zig
@@ -478,6 +478,7 @@ const usage_build_generic =
     \\  --sysroot [path]               Set the system root directory (usually /)
     \\  --version [ver]                Dynamic library semver
     \\  --entry [name]                 Set the entrypoint symbol name
+    \\  --force_undefined [name]       Specify the symbol must be defined for the link to succeed
     \\  -fsoname[=name]                Override the default SONAME value
     \\  -fno-soname                    Disable emitting a SONAME
     \\  -fLLD                          Force using LLD as the linker
@@ -680,6 +681,28 @@ const Listen = union(enum) {
     stdio,
 };
 
+const ArgsIterator = struct {
+    resp_file: ?ArgIteratorResponseFile = null,
+    args: []const []const u8,
+    i: usize = 0,
+    fn next(it: *@This()) ?[]const u8 {
+        if (it.i >= it.args.len) {
+            if (it.resp_file) |*resp| return resp.next();
+            return null;
+        }
+        defer it.i += 1;
+        return it.args[it.i];
+    }
+    fn nextOrFatal(it: *@This()) []const u8 {
+        if (it.i >= it.args.len) {
+            if (it.resp_file) |*resp| if (resp.next()) |ret| return ret;
+            fatal("expected parameter after {s}", .{it.args[it.i - 1]});
+        }
+        defer it.i += 1;
+        return it.args[it.i];
+    }
+};
+
 fn buildOutputType(
     gpa: Allocator,
     arena: Allocator,
@@ -784,6 +807,7 @@ fn buildOutputType(
     var test_evented_io = false;
     var test_no_exec = false;
     var entry: ?[]const u8 = null;
+    var force_undefined_symbols: std.StringArrayHashMapUnmanaged(void) = .{};
     var stack_size_override: ?u64 = null;
     var image_base_override: ?u64 = null;
     var use_llvm: ?bool = null;
@@ -917,28 +941,7 @@ fn buildOutputType(
 
             soname = .yes_default_value;
 
-            const Iterator = struct {
-                resp_file: ?ArgIteratorResponseFile = null,
-                args: []const []const u8,
-                i: usize = 0,
-                fn next(it: *@This()) ?[]const u8 {
-                    if (it.i >= it.args.len) {
-                        if (it.resp_file) |*resp| return resp.next();
-                        return null;
-                    }
-                    defer it.i += 1;
-                    return it.args[it.i];
-                }
-                fn nextOrFatal(it: *@This()) []const u8 {
-                    if (it.i >= it.args.len) {
-                        if (it.resp_file) |*resp| if (resp.next()) |ret| return ret;
-                        fatal("expected parameter after {s}", .{it.args[it.i - 1]});
-                    }
-                    defer it.i += 1;
-                    return it.args[it.i];
-                }
-            };
-            var args_iter = Iterator{
+            var args_iter = ArgsIterator{
                 .args = all_args[2..],
             };
 
@@ -1029,6 +1032,8 @@ fn buildOutputType(
                         optimize_mode_string = args_iter.nextOrFatal();
                     } else if (mem.eql(u8, arg, "--entry")) {
                         entry = args_iter.nextOrFatal();
+                    } else if (mem.eql(u8, arg, "--force_undefined")) {
+                        try force_undefined_symbols.put(gpa, args_iter.nextOrFatal(), {});
                     } else if (mem.eql(u8, arg, "--stack")) {
                         const next_arg = args_iter.nextOrFatal();
                         stack_size_override = std.fmt.parseUnsigned(u64, next_arg, 0) catch |err| {
@@ -1816,6 +1821,9 @@ fn buildOutputType(
                     .entry => {
                         entry = it.only_arg;
                     },
+                    .force_undefined_symbol => {
+                        try force_undefined_symbols.put(gpa, it.only_arg, {});
+                    },
                     .weak_library => try system_libs.put(it.only_arg, .{ .weak = true }),
                     .weak_framework => try frameworks.put(gpa, it.only_arg, .{ .weak = true }),
                     .headerpad_max_install_names => headerpad_max_install_names = true,
@@ -1843,17 +1851,14 @@ fn buildOutputType(
                 }
             }
             // Parse linker args.
-            var i: usize = 0;
-            while (i < linker_args.items.len) : (i += 1) {
-                const arg = linker_args.items[i];
+            var linker_args_it = ArgsIterator{
+                .args = linker_args.items,
+            };
+            while (linker_args_it.next()) |arg| {
                 if (mem.eql(u8, arg, "-soname") or
                     mem.eql(u8, arg, "--soname"))
                 {
-                    i += 1;
-                    if (i >= linker_args.items.len) {
-                        fatal("expected linker arg after '{s}'", .{arg});
-                    }
-                    const name = linker_args.items[i];
+                    const name = linker_args_it.nextOrFatal();
                     soname = .{ .yes = name };
                     // Use it as --name.
                     // Example: libsoundio.so.2
@@ -1881,64 +1886,37 @@ fn buildOutputType(
                     }
                     provided_name = name[prefix..end];
                 } else if (mem.eql(u8, arg, "-rpath")) {
-                    i += 1;
-                    if (i >= linker_args.items.len) {
-                        fatal("expected linker arg after '{s}'", .{arg});
-                    }
-                    try rpath_list.append(linker_args.items[i]);
+                    try rpath_list.append(linker_args_it.nextOrFatal());
                 } else if (mem.eql(u8, arg, "--subsystem")) {
-                    i += 1;
-                    if (i >= linker_args.items.len) {
-                        fatal("expected linker arg after '{s}'", .{arg});
-                    }
-                    subsystem = try parseSubSystem(linker_args.items[i]);
+                    subsystem = try parseSubSystem(linker_args_it.nextOrFatal());
                 } else if (mem.eql(u8, arg, "-I") or
                     mem.eql(u8, arg, "--dynamic-linker") or
                     mem.eql(u8, arg, "-dynamic-linker"))
                 {
-                    i += 1;
-                    if (i >= linker_args.items.len) {
-                        fatal("expected linker arg after '{s}'", .{arg});
-                    }
-                    target_dynamic_linker = linker_args.items[i];
+                    target_dynamic_linker = linker_args_it.nextOrFatal();
                 } else if (mem.eql(u8, arg, "-E") or
                     mem.eql(u8, arg, "--export-dynamic") or
                     mem.eql(u8, arg, "-export-dynamic"))
                 {
                     rdynamic = true;
                 } else if (mem.eql(u8, arg, "--version-script")) {
-                    i += 1;
-                    if (i >= linker_args.items.len) {
-                        fatal("expected linker arg after '{s}'", .{arg});
-                    }
-                    version_script = linker_args.items[i];
+                    version_script = linker_args_it.nextOrFatal();
                 } else if (mem.eql(u8, arg, "-O")) {
-                    i += 1;
-                    if (i >= linker_args.items.len) {
-                        fatal("expected linker arg after '{s}'", .{arg});
-                    }
-                    linker_optimization = std.fmt.parseUnsigned(u8, linker_args.items[i], 10) catch |err| {
-                        fatal("unable to parse optimization level '{s}': {s}", .{ linker_args.items[i], @errorName(err) });
+                    const opt = linker_args_it.nextOrFatal();
+                    linker_optimization = std.fmt.parseUnsigned(u8, opt, 10) catch |err| {
+                        fatal("unable to parse optimization level '{s}': {s}", .{ opt, @errorName(err) });
                     };
                 } else if (mem.startsWith(u8, arg, "-O")) {
                     linker_optimization = std.fmt.parseUnsigned(u8, arg["-O".len..], 10) catch |err| {
                         fatal("unable to parse optimization level '{s}': {s}", .{ arg, @errorName(err) });
                     };
                 } else if (mem.eql(u8, arg, "-pagezero_size")) {
-                    i += 1;
-                    if (i >= linker_args.items.len) {
-                        fatal("expected linker arg after '{s}'", .{arg});
-                    }
-                    const next_arg = linker_args.items[i];
+                    const next_arg = linker_args_it.nextOrFatal();
                     pagezero_size = std.fmt.parseUnsigned(u64, eatIntPrefix(next_arg, 16), 16) catch |err| {
                         fatal("unable to parse pagezero size '{s}': {s}", .{ next_arg, @errorName(err) });
                     };
                 } else if (mem.eql(u8, arg, "-headerpad")) {
-                    i += 1;
-                    if (i >= linker_args.items.len) {
-                        fatal("expected linker arg after '{s}'", .{arg});
-                    }
-                    const next_arg = linker_args.items[i];
+                    const next_arg = linker_args_it.nextOrFatal();
                     headerpad_size = std.fmt.parseUnsigned(u32, eatIntPrefix(next_arg, 16), 16) catch |err| {
                         fatal("unable to parse  headerpad size '{s}': {s}", .{ next_arg, @errorName(err) });
                     };
@@ -1961,11 +1939,7 @@ fn buildOutputType(
                 } else if (mem.eql(u8, arg, "--print-map")) {
                     linker_print_map = true;
                 } else if (mem.eql(u8, arg, "--sort-section")) {
-                    i += 1;
-                    if (i >= linker_args.items.len) {
-                        fatal("expected linker arg after '{s}'", .{arg});
-                    }
-                    const arg1 = linker_args.items[i];
+                    const arg1 = linker_args_it.nextOrFatal();
                     linker_sort_section = std.meta.stringToEnum(link.SortSection, arg1) orelse {
                         fatal("expected [name|alignment] after --sort-section, found '{s}'", .{arg1});
                     };
@@ -1998,28 +1972,16 @@ fn buildOutputType(
                 } else if (mem.startsWith(u8, arg, "--export=")) {
                     try linker_export_symbol_names.append(arg["--export=".len..]);
                 } else if (mem.eql(u8, arg, "--export")) {
-                    i += 1;
-                    if (i >= linker_args.items.len) {
-                        fatal("expected linker arg after '{s}'", .{arg});
-                    }
-                    try linker_export_symbol_names.append(linker_args.items[i]);
+                    try linker_export_symbol_names.append(linker_args_it.nextOrFatal());
                 } else if (mem.eql(u8, arg, "--compress-debug-sections")) {
-                    i += 1;
-                    if (i >= linker_args.items.len) {
-                        fatal("expected linker arg after '{s}'", .{arg});
-                    }
-                    const arg1 = linker_args.items[i];
+                    const arg1 = linker_args_it.nextOrFatal();
                     linker_compress_debug_sections = std.meta.stringToEnum(link.CompressDebugSections, arg1) orelse {
                         fatal("expected [none|zlib] after --compress-debug-sections, found '{s}'", .{arg1});
                     };
                 } else if (mem.startsWith(u8, arg, "-z")) {
                     var z_arg = arg[2..];
                     if (z_arg.len == 0) {
-                        i += 1;
-                        if (i >= linker_args.items.len) {
-                            fatal("expected linker extension flag after '{s}'", .{arg});
-                        }
-                        z_arg = linker_args.items[i];
+                        z_arg = linker_args_it.nextOrFatal();
                     }
                     if (mem.eql(u8, z_arg, "nodelete")) {
                         linker_z_nodelete = true;
@@ -2056,51 +2018,33 @@ fn buildOutputType(
                         fatal("unsupported linker extension flag: -z {s}", .{z_arg});
                     }
                 } else if (mem.eql(u8, arg, "--major-image-version")) {
-                    i += 1;
-                    if (i >= linker_args.items.len) {
-                        fatal("expected linker arg after '{s}'", .{arg});
-                    }
-                    version.major = std.fmt.parseUnsigned(u32, linker_args.items[i], 10) catch |err| {
-                        fatal("unable to parse major image version '{s}': {s}", .{ linker_args.items[i], @errorName(err) });
+                    const major = linker_args_it.nextOrFatal();
+                    version.major = std.fmt.parseUnsigned(u32, major, 10) catch |err| {
+                        fatal("unable to parse major image version '{s}': {s}", .{ major, @errorName(err) });
                     };
                     have_version = true;
                 } else if (mem.eql(u8, arg, "--minor-image-version")) {
-                    i += 1;
-                    if (i >= linker_args.items.len) {
-                        fatal("expected linker arg after '{s}'", .{arg});
-                    }
-                    version.minor = std.fmt.parseUnsigned(u32, linker_args.items[i], 10) catch |err| {
-                        fatal("unable to parse minor image version '{s}': {s}", .{ linker_args.items[i], @errorName(err) });
+                    const minor = linker_args_it.nextOrFatal();
+                    version.minor = std.fmt.parseUnsigned(u32, minor, 10) catch |err| {
+                        fatal("unable to parse minor image version '{s}': {s}", .{ minor, @errorName(err) });
                     };
                     have_version = true;
                 } else if (mem.eql(u8, arg, "-e") or mem.eql(u8, arg, "--entry")) {
-                    i += 1;
-                    if (i >= linker_args.items.len) {
-                        fatal("expected linker arg after '{s}'", .{arg});
-                    }
-                    entry = linker_args.items[i];
+                    entry = linker_args_it.nextOrFatal();
+                } else if (mem.eql(u8, arg, "-u")) {
+                    try force_undefined_symbols.put(gpa, linker_args_it.nextOrFatal(), {});
                 } else if (mem.eql(u8, arg, "--stack") or mem.eql(u8, arg, "-stack_size")) {
-                    i += 1;
-                    if (i >= linker_args.items.len) {
-                        fatal("expected linker arg after '{s}'", .{arg});
-                    }
-                    stack_size_override = std.fmt.parseUnsigned(u64, linker_args.items[i], 0) catch |err| {
-                        fatal("unable to parse stack size override '{s}': {s}", .{ linker_args.items[i], @errorName(err) });
+                    const stack_size = linker_args_it.nextOrFatal();
+                    stack_size_override = std.fmt.parseUnsigned(u64, stack_size, 0) catch |err| {
+                        fatal("unable to parse stack size override '{s}': {s}", .{ stack_size, @errorName(err) });
                     };
                 } else if (mem.eql(u8, arg, "--image-base")) {
-                    i += 1;
-                    if (i >= linker_args.items.len) {
-                        fatal("expected linker arg after '{s}'", .{arg});
-                    }
-                    image_base_override = std.fmt.parseUnsigned(u64, linker_args.items[i], 0) catch |err| {
-                        fatal("unable to parse image base override '{s}': {s}", .{ linker_args.items[i], @errorName(err) });
+                    const image_base = linker_args_it.nextOrFatal();
+                    image_base_override = std.fmt.parseUnsigned(u64, image_base, 0) catch |err| {
+                        fatal("unable to parse image base override '{s}': {s}", .{ image_base, @errorName(err) });
                     };
                 } else if (mem.eql(u8, arg, "-T") or mem.eql(u8, arg, "--script")) {
-                    i += 1;
-                    if (i >= linker_args.items.len) {
-                        fatal("expected linker arg after '{s}'", .{arg});
-                    }
-                    linker_script = linker_args.items[i];
+                    linker_script = linker_args_it.nextOrFatal();
                 } else if (mem.eql(u8, arg, "--eh-frame-hdr")) {
                     link_eh_frame_hdr = true;
                 } else if (mem.eql(u8, arg, "--no-eh-frame-hdr")) {
@@ -2138,130 +2082,74 @@ fn buildOutputType(
                 } else if (mem.eql(u8, arg, "--major-os-version") or
                     mem.eql(u8, arg, "--minor-os-version"))
                 {
-                    i += 1;
-                    if (i >= linker_args.items.len) {
-                        fatal("expected linker arg after '{s}'", .{arg});
-                    }
                     // This option does not do anything.
+                    _ = linker_args_it.nextOrFatal();
                 } else if (mem.eql(u8, arg, "--major-subsystem-version")) {
-                    i += 1;
-                    if (i >= linker_args.items.len) {
-                        fatal("expected linker arg after '{s}'", .{arg});
-                    }
-
+                    const major = linker_args_it.nextOrFatal();
                     major_subsystem_version = std.fmt.parseUnsigned(
                         u32,
-                        linker_args.items[i],
+                        major,
                         10,
                     ) catch |err| {
-                        fatal("unable to parse major subsystem version '{s}': {s}", .{ linker_args.items[i], @errorName(err) });
+                        fatal("unable to parse major subsystem version '{s}': {s}", .{ major, @errorName(err) });
                     };
                 } else if (mem.eql(u8, arg, "--minor-subsystem-version")) {
-                    i += 1;
-                    if (i >= linker_args.items.len) {
-                        fatal("expected linker arg after '{s}'", .{arg});
-                    }
-
+                    const minor = linker_args_it.nextOrFatal();
                     minor_subsystem_version = std.fmt.parseUnsigned(
                         u32,
-                        linker_args.items[i],
+                        minor,
                         10,
                     ) catch |err| {
-                        fatal("unable to parse minor subsystem version '{s}': {s}", .{ linker_args.items[i], @errorName(err) });
+                        fatal("unable to parse minor subsystem version '{s}': {s}", .{ minor, @errorName(err) });
                     };
                 } else if (mem.eql(u8, arg, "-framework")) {
-                    i += 1;
-                    if (i >= linker_args.items.len) {
-                        fatal("expected linker arg after '{s}'", .{arg});
-                    }
-                    try frameworks.put(gpa, linker_args.items[i], .{});
+                    try frameworks.put(gpa, linker_args_it.nextOrFatal(), .{});
                 } else if (mem.eql(u8, arg, "-weak_framework")) {
-                    i += 1;
-                    if (i >= linker_args.items.len) {
-                        fatal("expected linker arg after '{s}'", .{arg});
-                    }
-                    try frameworks.put(gpa, linker_args.items[i], .{ .weak = true });
+                    try frameworks.put(gpa, linker_args_it.nextOrFatal(), .{ .weak = true });
                 } else if (mem.eql(u8, arg, "-needed_framework")) {
-                    i += 1;
-                    if (i >= linker_args.items.len) {
-                        fatal("expected linker arg after '{s}'", .{arg});
-                    }
-                    try frameworks.put(gpa, linker_args.items[i], .{ .needed = true });
+                    try frameworks.put(gpa, linker_args_it.nextOrFatal(), .{ .needed = true });
                 } else if (mem.eql(u8, arg, "-needed_library")) {
-                    i += 1;
-                    if (i >= linker_args.items.len) {
-                        fatal("expected linker arg after '{s}'", .{arg});
-                    }
-                    try system_libs.put(linker_args.items[i], .{ .needed = true });
+                    try system_libs.put(linker_args_it.nextOrFatal(), .{ .needed = true });
                 } else if (mem.startsWith(u8, arg, "-weak-l")) {
                     try system_libs.put(arg["-weak-l".len..], .{ .weak = true });
                 } else if (mem.eql(u8, arg, "-weak_library")) {
-                    i += 1;
-                    if (i >= linker_args.items.len) {
-                        fatal("expected linker arg after '{s}'", .{arg});
-                    }
-                    try system_libs.put(linker_args.items[i], .{ .weak = true });
+                    try system_libs.put(linker_args_it.nextOrFatal(), .{ .weak = true });
                 } else if (mem.eql(u8, arg, "-compatibility_version")) {
-                    i += 1;
-                    if (i >= linker_args.items.len) {
-                        fatal("expected linker arg after '{s}'", .{arg});
-                    }
-                    compatibility_version = std.builtin.Version.parse(linker_args.items[i]) catch |err| {
-                        fatal("unable to parse -compatibility_version '{s}': {s}", .{ linker_args.items[i], @errorName(err) });
+                    const compat_version = linker_args_it.nextOrFatal();
+                    compatibility_version = std.builtin.Version.parse(compat_version) catch |err| {
+                        fatal("unable to parse -compatibility_version '{s}': {s}", .{ compat_version, @errorName(err) });
                     };
                 } else if (mem.eql(u8, arg, "-current_version")) {
-                    i += 1;
-                    if (i >= linker_args.items.len) {
-                        fatal("expected linker arg after '{s}'", .{arg});
-                    }
-                    version = std.builtin.Version.parse(linker_args.items[i]) catch |err| {
-                        fatal("unable to parse -current_version '{s}': {s}", .{ linker_args.items[i], @errorName(err) });
+                    const curr_version = linker_args_it.nextOrFatal();
+                    version = std.builtin.Version.parse(curr_version) catch |err| {
+                        fatal("unable to parse -current_version '{s}': {s}", .{ curr_version, @errorName(err) });
                     };
                     have_version = true;
                 } else if (mem.eql(u8, arg, "--out-implib") or
                     mem.eql(u8, arg, "-implib"))
                 {
-                    i += 1;
-                    if (i >= linker_args.items.len) {
-                        fatal("expected linker arg after '{s}'", .{arg});
-                    }
-                    emit_implib = .{ .yes = linker_args.items[i] };
+                    emit_implib = .{ .yes = linker_args_it.nextOrFatal() };
                     emit_implib_arg_provided = true;
                 } else if (mem.eql(u8, arg, "-undefined")) {
-                    i += 1;
-                    if (i >= linker_args.items.len) {
-                        fatal("expected linker arg after '{s}'", .{arg});
-                    }
-                    if (mem.eql(u8, "dynamic_lookup", linker_args.items[i])) {
+                    const lookup_type = linker_args_it.nextOrFatal();
+                    if (mem.eql(u8, "dynamic_lookup", lookup_type)) {
                         linker_allow_shlib_undefined = true;
-                    } else if (mem.eql(u8, "error", linker_args.items[i])) {
+                    } else if (mem.eql(u8, "error", lookup_type)) {
                         linker_allow_shlib_undefined = false;
                     } else {
-                        fatal("unsupported -undefined option '{s}'", .{linker_args.items[i]});
+                        fatal("unsupported -undefined option '{s}'", .{lookup_type});
                     }
                 } else if (mem.eql(u8, arg, "-install_name")) {
-                    i += 1;
-                    if (i >= linker_args.items.len) {
-                        fatal("expected linker arg after '{s}'", .{arg});
-                    }
-                    install_name = linker_args.items[i];
+                    install_name = linker_args_it.nextOrFatal();
                 } else if (mem.eql(u8, arg, "-force_load")) {
-                    i += 1;
-                    if (i >= linker_args.items.len) {
-                        fatal("expected linker arg after '{s}'", .{arg});
-                    }
                     try link_objects.append(.{
-                        .path = linker_args.items[i],
+                        .path = linker_args_it.nextOrFatal(),
                         .must_link = true,
                     });
                 } else if (mem.eql(u8, arg, "-hash-style") or
                     mem.eql(u8, arg, "--hash-style"))
                 {
-                    i += 1;
-                    if (i >= linker_args.items.len) {
-                        fatal("expected linker arg after '{s}'", .{arg});
-                    }
-                    const next_arg = linker_args.items[i];
+                    const next_arg = linker_args_it.nextOrFatal();
                     hash_style = std.meta.stringToEnum(link.HashStyle, next_arg) orelse {
                         fatal("expected [sysv|gnu|both] after --hash-style, found '{s}'", .{
                             next_arg,
@@ -3219,6 +3107,7 @@ fn buildOutputType(
         .link_eh_frame_hdr = link_eh_frame_hdr,
         .link_emit_relocs = link_emit_relocs,
         .entry = entry,
+        .force_undefined_symbols = force_undefined_symbols,
         .stack_size_override = stack_size_override,
         .image_base_override = image_base_override,
         .strip = strip,
@@ -5295,6 +5184,7 @@ pub const ClangArgIterator = struct {
         emit_llvm,
         sysroot,
         entry,
+        force_undefined_symbol,
         weak_library,
         weak_framework,
         headerpad_max_install_names,
test/link/macho/entry_in_dylib/build.zig
@@ -31,6 +31,7 @@ fn add(b: *std.Build, test_step: *std.Build.Step, optimize: std.builtin.Optimize
     exe.linkLibrary(lib);
     exe.linkLibC();
     exe.entry_symbol_name = "_bootstrap";
+    exe.forceUndefinedSymbol("_my_main");
 
     const check_exe = exe.checkObject();
     check_exe.checkStart("segname __TEXT");
tools/update_clang_options.zig
@@ -468,6 +468,10 @@ const known_options = [_]KnownOpt{
         .name = "e",
         .ident = "entry",
     },
+    .{
+        .name = "u",
+        .ident = "force_undefined_symbol",
+    },
     .{
         .name = "weak-l",
         .ident = "weak_library",