Commit bc78b02c04

Jakub Konka <kubkon@jakubkonka.com>
2021-06-20 12:45:51
zld: introduce Stub.zig which represents parsed stub file
Instead of trying to fit a stub file into the frame of a Dylib struct, I think it makes more sense to keep them as separate entities with possibly shared interface (which would be added in the future). This cleaned up a lot of logic in Dylib as well as Stub. Also, while here I've made creating actual *Symbols lazy in the sense Dylib and Stub only store hash maps of symbol names that they expose but we defer create and referencing given dylib/stub until link time when a symbol is actually referenced. This should reduce memory usage and speed things up a bit.
1 parent 09b4619
src/link/MachO/Archive.zig
@@ -234,8 +234,11 @@ pub fn parseObject(self: Archive, offset: u32) !*Object {
     return object;
 }
 
-pub fn isArchive(file: fs.File) bool {
-    const magic = file.reader().readBytesNoEof(Archive.SARMAG) catch return false;
-    file.seekTo(0) catch return false;
+pub fn isArchive(file: fs.File) !bool {
+    const magic = file.reader().readBytesNoEof(Archive.SARMAG) catch |err| switch (err) {
+        error.EndOfStream => return false,
+        else => |e| return e,
+    };
+    try file.seekTo(0);
     return mem.eql(u8, &magic, Archive.ARMAG);
 }
src/link/MachO/Dylib.zig
@@ -9,7 +9,6 @@ const mem = std.mem;
 
 const Allocator = mem.Allocator;
 const Symbol = @import("Symbol.zig");
-const LibStub = @import("../tapi.zig").LibStub;
 
 usingnamespace @import("commands.zig");
 
@@ -29,7 +28,10 @@ id_cmd_index: ?u16 = null,
 
 id: ?Id = null,
 
-symbols: std.StringArrayHashMapUnmanaged(*Symbol) = .{},
+/// Parsed symbol table represented as hash map of symbols'
+/// names. We can and should defer creating *Symbols until
+/// a symbol is referenced by an object file.
+symbols: std.StringArrayHashMapUnmanaged(void) = .{},
 
 pub const Id = struct {
     name: []const u8,
@@ -52,9 +54,8 @@ pub fn deinit(self: *Dylib) void {
     }
     self.load_commands.deinit(self.allocator);
 
-    for (self.symbols.values()) |value| {
-        value.deinit(self.allocator);
-        self.allocator.destroy(value);
+    for (self.symbols.keys()) |key| {
+        self.allocator.free(key);
     }
     self.symbols.deinit(self.allocator);
 
@@ -171,103 +172,33 @@ pub fn parseSymbols(self: *Dylib) !void {
         if (!(Symbol.isSect(sym) and Symbol.isExt(sym))) continue;
 
         const name = try self.allocator.dupe(u8, sym_name);
-        const proxy = try self.allocator.create(Symbol.Proxy);
-        errdefer self.allocator.destroy(proxy);
-
-        proxy.* = .{
-            .base = .{
-                .@"type" = .proxy,
-                .name = name,
-            },
-            .dylib = self,
-        };
-
-        try self.symbols.putNoClobber(self.allocator, name, &proxy.base);
+        try self.symbols.putNoClobber(self.allocator, name, {});
     }
 }
 
-pub fn isDylib(file: fs.File) bool {
-    const header = file.reader().readStruct(macho.mach_header_64) catch return false;
-    file.seekTo(0) catch return false;
+pub fn isDylib(file: fs.File) !bool {
+    const header = file.reader().readStruct(macho.mach_header_64) catch |err| switch (err) {
+        error.EndOfStream => return false,
+        else => |e| return e,
+    };
+    try file.seekTo(0);
     return header.filetype == macho.MH_DYLIB;
 }
 
-pub fn parseFromStub(self: *Dylib, lib_stub: LibStub) !void {
-    assert(lib_stub.inner.len > 0);
+pub fn createProxy(self: *Dylib, sym_name: []const u8) !?*Symbol {
+    if (!self.symbols.contains(sym_name)) return null;
 
-    log.debug("parsing shared library from stub '{s}'", .{self.name.?});
+    const name = try self.allocator.dupe(u8, sym_name);
+    const proxy = try self.allocator.create(Symbol.Proxy);
+    errdefer self.allocator.destroy(proxy);
 
-    const umbrella_lib = lib_stub.inner[0];
-    self.id = .{
-        .name = try self.allocator.dupe(u8, umbrella_lib.install_name),
-        // TODO parse from the stub
-        .timestamp = 2,
-        .current_version = 0,
-        .compatibility_version = 0,
-    };
-
-    const target_string: []const u8 = switch (self.arch.?) {
-        .aarch64 => "arm64-macos",
-        .x86_64 => "x86_64-macos",
-        else => unreachable,
+    proxy.* = .{
+        .base = .{
+            .@"type" = .proxy,
+            .name = name,
+        },
+        .file = .{ .dylib = self },
     };
 
-    for (lib_stub.inner) |stub| {
-        if (!hasTarget(stub.targets, target_string)) continue;
-
-        if (stub.exports) |exports| {
-            for (exports) |exp| {
-                if (!hasTarget(exp.targets, target_string)) continue;
-
-                for (exp.symbols) |sym_name| {
-                    if (self.symbols.contains(sym_name)) continue;
-
-                    const name = try self.allocator.dupe(u8, sym_name);
-                    const proxy = try self.allocator.create(Symbol.Proxy);
-                    errdefer self.allocator.destroy(proxy);
-
-                    proxy.* = .{
-                        .base = .{
-                            .@"type" = .proxy,
-                            .name = name,
-                        },
-                        .dylib = self,
-                    };
-
-                    try self.symbols.putNoClobber(self.allocator, name, &proxy.base);
-                }
-            }
-        }
-
-        if (stub.reexports) |reexports| {
-            for (reexports) |reexp| {
-                if (!hasTarget(reexp.targets, target_string)) continue;
-
-                for (reexp.symbols) |sym_name| {
-                    if (self.symbols.contains(sym_name)) continue;
-
-                    const name = try self.allocator.dupe(u8, sym_name);
-                    const proxy = try self.allocator.create(Symbol.Proxy);
-                    errdefer self.allocator.destroy(proxy);
-
-                    proxy.* = .{
-                        .base = .{
-                            .@"type" = .proxy,
-                            .name = name,
-                        },
-                        .dylib = self,
-                    };
-
-                    try self.symbols.putNoClobber(self.allocator, name, &proxy.base);
-                }
-            }
-        }
-    }
-}
-
-fn hasTarget(targets: []const []const u8, target: []const u8) bool {
-    for (targets) |t| {
-        if (mem.eql(u8, t, target)) return true;
-    }
-    return false;
+    return &proxy.base;
 }
src/link/MachO/Object.zig
@@ -534,8 +534,11 @@ pub fn parseDataInCode(self: *Object) !void {
     }
 }
 
-pub fn isObject(file: fs.File) bool {
-    const header = file.reader().readStruct(macho.mach_header_64) catch return false;
-    file.seekTo(0) catch return false;
+pub fn isObject(file: fs.File) !bool {
+    const header = file.reader().readStruct(macho.mach_header_64) catch |err| switch (err) {
+        error.EndOfStream => return false,
+        else => |e| return e,
+    };
+    try file.seekTo(0);
     return header.filetype == macho.MH_OBJECT;
 }
src/link/MachO/Stub.zig
@@ -0,0 +1,130 @@
+const Stub = @This();
+
+const std = @import("std");
+const assert = std.debug.assert;
+const fs = std.fs;
+const log = std.log.scoped(.stub);
+const macho = std.macho;
+const mem = std.mem;
+
+const Allocator = mem.Allocator;
+const Symbol = @import("Symbol.zig");
+pub const LibStub = @import("../tapi.zig").LibStub;
+
+allocator: *Allocator,
+arch: ?std.Target.Cpu.Arch = null,
+lib_stub: ?LibStub = null,
+name: ?[]const u8 = null,
+
+ordinal: ?u16 = null,
+
+id: ?Id = null,
+
+/// Parsed symbol table represented as hash map of symbols'
+/// names. We can and should defer creating *Symbols until
+/// a symbol is referenced by an object file.
+symbols: std.StringArrayHashMapUnmanaged(void) = .{},
+
+pub const Id = struct {
+    name: []const u8,
+    timestamp: u32,
+    current_version: u32,
+    compatibility_version: u32,
+
+    pub fn deinit(id: *Id, allocator: *Allocator) void {
+        allocator.free(id.name);
+    }
+};
+
+pub fn init(allocator: *Allocator) Stub {
+    return .{ .allocator = allocator };
+}
+
+pub fn deinit(self: *Stub) void {
+    self.symbols.deinit(self.allocator);
+
+    if (self.lib_stub) |*lib_stub| {
+        lib_stub.deinit();
+    }
+
+    if (self.name) |name| {
+        self.allocator.free(name);
+    }
+
+    if (self.id) |*id| {
+        id.deinit(self.allocator);
+    }
+}
+
+pub fn parse(self: *Stub) !void {
+    const lib_stub = self.lib_stub orelse return error.EmptyStubFile;
+    if (lib_stub.inner.len == 0) return error.EmptyStubFile;
+
+    log.debug("parsing shared library from stub '{s}'", .{self.name.?});
+
+    const umbrella_lib = lib_stub.inner[0];
+    self.id = .{
+        .name = try self.allocator.dupe(u8, umbrella_lib.install_name),
+        // TODO parse from the stub
+        .timestamp = 2,
+        .current_version = 0,
+        .compatibility_version = 0,
+    };
+
+    const target_string: []const u8 = switch (self.arch.?) {
+        .aarch64 => "arm64-macos",
+        .x86_64 => "x86_64-macos",
+        else => unreachable,
+    };
+
+    for (lib_stub.inner) |stub| {
+        if (!hasTarget(stub.targets, target_string)) continue;
+
+        if (stub.exports) |exports| {
+            for (exports) |exp| {
+                if (!hasTarget(exp.targets, target_string)) continue;
+
+                for (exp.symbols) |sym_name| {
+                    if (self.symbols.contains(sym_name)) continue;
+                    try self.symbols.putNoClobber(self.allocator, sym_name, {});
+                }
+            }
+        }
+
+        if (stub.reexports) |reexports| {
+            for (reexports) |reexp| {
+                if (!hasTarget(reexp.targets, target_string)) continue;
+
+                for (reexp.symbols) |sym_name| {
+                    if (self.symbols.contains(sym_name)) continue;
+                    try self.symbols.putNoClobber(self.allocator, sym_name, {});
+                }
+            }
+        }
+    }
+}
+
+fn hasTarget(targets: []const []const u8, target: []const u8) bool {
+    for (targets) |t| {
+        if (mem.eql(u8, t, target)) return true;
+    }
+    return false;
+}
+
+pub fn createProxy(self: *Stub, sym_name: []const u8) !?*Symbol {
+    if (!self.symbols.contains(sym_name)) return null;
+
+    const name = try self.allocator.dupe(u8, sym_name);
+    const proxy = try self.allocator.create(Symbol.Proxy);
+    errdefer self.allocator.destroy(proxy);
+
+    proxy.* = .{
+        .base = .{
+            .@"type" = .proxy,
+            .name = name,
+        },
+        .file = .{ .stub = self },
+    };
+
+    return &proxy.base;
+}
src/link/MachO/Symbol.zig
@@ -7,6 +7,7 @@ const mem = std.mem;
 const Allocator = mem.Allocator;
 const Dylib = @import("Dylib.zig");
 const Object = @import("Object.zig");
+const Stub = @import("Stub.zig");
 
 pub const Type = enum {
     regular,
@@ -84,11 +85,22 @@ pub const Regular = struct {
 pub const Proxy = struct {
     base: Symbol,
 
-    /// Dylib where to locate this symbol.
+    /// Dylib or stub where to locate this symbol.
     /// null means self-reference.
-    dylib: ?*Dylib = null,
+    file: ?union(enum) {
+        dylib: *Dylib,
+        stub: *Stub,
+    } = null,
 
     pub const base_type: Symbol.Type = .proxy;
+
+    pub fn dylibOrdinal(proxy: *Proxy) u16 {
+        const file = proxy.file orelse return 0;
+        return switch (file) {
+            .dylib => |dylib| dylib.ordinal.?,
+            .stub => |stub| stub.ordinal.?,
+        };
+    }
 };
 
 pub const Unresolved = struct {
src/link/MachO/Zld.zig
@@ -16,8 +16,8 @@ const Allocator = mem.Allocator;
 const Archive = @import("Archive.zig");
 const CodeSignature = @import("CodeSignature.zig");
 const Dylib = @import("Dylib.zig");
-const LibStub = @import("../tapi.zig").LibStub;
 const Object = @import("Object.zig");
+const Stub = @import("Stub.zig");
 const Symbol = @import("Symbol.zig");
 const Trie = @import("Trie.zig");
 
@@ -38,6 +38,10 @@ stack_size: u64 = 0,
 objects: std.ArrayListUnmanaged(*Object) = .{},
 archives: std.ArrayListUnmanaged(*Archive) = .{},
 dylibs: std.ArrayListUnmanaged(*Dylib) = .{},
+lib_stubs: std.ArrayListUnmanaged(*Stub) = .{},
+
+libsystem_stub_index: ?u16 = null,
+next_dylib_ordinal: u16 = 1,
 
 load_commands: std.ArrayListUnmanaged(LoadCommand) = .{},
 
@@ -153,9 +157,20 @@ pub fn deinit(self: *Zld) void {
     }
     self.dylibs.deinit(self.allocator);
 
+    for (self.lib_stubs.items) |stub| {
+        stub.deinit();
+        self.allocator.destroy(stub);
+    }
+    self.lib_stubs.deinit(self.allocator);
+
+    for (self.imports.values()) |proxy| {
+        proxy.deinit(self.allocator);
+        self.allocator.destroy(proxy);
+    }
+    self.imports.deinit(self.allocator);
+
     self.tentatives.deinit(self.allocator);
     self.globals.deinit(self.allocator);
-    self.imports.deinit(self.allocator);
     self.unresolved.deinit(self.allocator);
     self.strtab.deinit(self.allocator);
 
@@ -245,9 +260,11 @@ fn parseInputFiles(self: *Zld, files: []const []const u8) !void {
             dylib,
             stub,
         },
-        file: fs.File,
+        origin: union {
+            file: fs.File,
+            stub: Stub.LibStub,
+        },
         name: []const u8,
-        stub: ?LibStub = null,
     };
     var classified = std.ArrayList(Input).init(self.allocator);
     defer classified.deinit();
@@ -262,45 +279,45 @@ fn parseInputFiles(self: *Zld, files: []const []const u8) !void {
         };
 
         try_object: {
-            if (!Object.isObject(file)) break :try_object;
+            if (!(try Object.isObject(file))) break :try_object;
             try classified.append(.{
                 .kind = .object,
-                .file = file,
+                .origin = .{ .file = file },
                 .name = full_path,
             });
             continue;
         }
 
         try_archive: {
-            if (!Archive.isArchive(file)) break :try_archive;
+            if (!(try Archive.isArchive(file))) break :try_archive;
             try classified.append(.{
                 .kind = .archive,
-                .file = file,
+                .origin = .{ .file = file },
                 .name = full_path,
             });
             continue;
         }
 
         try_dylib: {
-            if (!Dylib.isDylib(file)) break :try_dylib;
+            if (!(try Dylib.isDylib(file))) break :try_dylib;
             try classified.append(.{
                 .kind = .dylib,
-                .file = file,
+                .origin = .{ .file = file },
                 .name = full_path,
             });
             continue;
         }
 
         try_stub: {
-            var lib_stub = LibStub.loadFromFile(self.allocator, file) catch {
+            var lib_stub = Stub.LibStub.loadFromFile(self.allocator, file) catch {
                 break :try_stub;
             };
             try classified.append(.{
                 .kind = .stub,
-                .file = file,
+                .origin = .{ .stub = lib_stub },
                 .name = full_path,
-                .stub = lib_stub,
             });
+            file.close();
             continue;
         }
 
@@ -318,7 +335,8 @@ fn parseInputFiles(self: *Zld, files: []const []const u8) !void {
                 object.* = Object.init(self.allocator);
                 object.arch = self.arch.?;
                 object.name = input.name;
-                object.file = input.file;
+                object.file = input.origin.file;
+
                 try object.parse();
                 try self.objects.append(self.allocator, object);
             },
@@ -329,40 +347,34 @@ fn parseInputFiles(self: *Zld, files: []const []const u8) !void {
                 archive.* = Archive.init(self.allocator);
                 archive.arch = self.arch.?;
                 archive.name = input.name;
-                archive.file = input.file;
+                archive.file = input.origin.file;
+
                 try archive.parse();
                 try self.archives.append(self.allocator, archive);
             },
-            .dylib, .stub => {
+            .dylib => {
                 const dylib = try self.allocator.create(Dylib);
                 errdefer self.allocator.destroy(dylib);
 
                 dylib.* = Dylib.init(self.allocator);
                 dylib.arch = self.arch.?;
                 dylib.name = input.name;
-                dylib.file = input.file;
-                dylib.ordinal = @intCast(u16, self.dylibs.items.len) + 1;
+                dylib.file = input.origin.file;
 
-                // TODO Defer parsing of the dylibs until they are actually needed
-                if (input.stub) |stub| {
-                    try dylib.parseFromStub(stub);
-                } else {
-                    try dylib.parse();
-                }
+                try dylib.parse();
                 try self.dylibs.append(self.allocator, dylib);
+            },
+            .stub => {
+                const stub = try self.allocator.create(Stub);
+                errdefer self.allocator.destroy(stub);
 
-                // Add LC_LOAD_DYLIB command
-                const dylib_id = dylib.id orelse unreachable;
-                var dylib_cmd = try createLoadDylibCommand(
-                    self.allocator,
-                    dylib_id.name,
-                    dylib_id.timestamp,
-                    dylib_id.current_version,
-                    dylib_id.compatibility_version,
-                );
-                errdefer dylib_cmd.deinit(self.allocator);
-
-                try self.load_commands.append(self.allocator, .{ .Dylib = dylib_cmd });
+                stub.* = Stub.init(self.allocator);
+                stub.arch = self.arch.?;
+                stub.name = input.name;
+                stub.lib_stub = input.origin.stub;
+
+                try stub.parse();
+                try self.lib_stubs.append(self.allocator, stub);
             },
         }
     }
@@ -372,7 +384,7 @@ fn parseLibs(self: *Zld, libs: []const []const u8) !void {
     for (libs) |lib| {
         const file = try fs.cwd().openFile(lib, .{});
 
-        if (Dylib.isDylib(file)) {
+        if (try Dylib.isDylib(file)) {
             const dylib = try self.allocator.create(Dylib);
             errdefer self.allocator.destroy(dylib);
 
@@ -380,57 +392,27 @@ fn parseLibs(self: *Zld, libs: []const []const u8) !void {
             dylib.arch = self.arch.?;
             dylib.name = try self.allocator.dupe(u8, lib);
             dylib.file = file;
-            dylib.ordinal = @intCast(u16, self.dylibs.items.len) + 1;
 
-            // TODO Defer parsing of the dylibs until they are actually needed
             try dylib.parse();
             try self.dylibs.append(self.allocator, dylib);
-
-            // Add LC_LOAD_DYLIB command
-            const dylib_id = dylib.id orelse unreachable;
-            var dylib_cmd = try createLoadDylibCommand(
-                self.allocator,
-                dylib_id.name,
-                dylib_id.timestamp,
-                dylib_id.current_version,
-                dylib_id.compatibility_version,
-            );
-            errdefer dylib_cmd.deinit(self.allocator);
-
-            try self.load_commands.append(self.allocator, .{ .Dylib = dylib_cmd });
         } else {
             // Try tbd stub file next.
-            if (LibStub.loadFromFile(self.allocator, file)) |*lib_stub| {
-                defer lib_stub.deinit();
-
-                const dylib = try self.allocator.create(Dylib);
-                errdefer self.allocator.destroy(dylib);
-
-                dylib.* = Dylib.init(self.allocator);
-                dylib.arch = self.arch.?;
-                dylib.name = try self.allocator.dupe(u8, lib);
-                dylib.file = file;
-                dylib.ordinal = @intCast(u16, self.dylibs.items.len) + 1;
+            if (Stub.LibStub.loadFromFile(self.allocator, file)) |lib_stub| {
+                const stub = try self.allocator.create(Stub);
+                errdefer self.allocator.destroy(stub);
 
-                try dylib.parseFromStub(lib_stub.*);
-                try self.dylibs.append(self.allocator, dylib);
+                stub.* = Stub.init(self.allocator);
+                stub.arch = self.arch.?;
+                stub.name = try self.allocator.dupe(u8, lib);
+                stub.lib_stub = lib_stub;
 
-                // Add LC_LOAD_DYLIB command
-                const dylib_id = dylib.id orelse unreachable;
-                var dylib_cmd = try createLoadDylibCommand(
-                    self.allocator,
-                    dylib_id.name,
-                    dylib_id.timestamp,
-                    dylib_id.current_version,
-                    dylib_id.compatibility_version,
-                );
-                errdefer dylib_cmd.deinit(self.allocator);
-
-                try self.load_commands.append(self.allocator, .{ .Dylib = dylib_cmd });
+                try stub.parse();
+                try self.lib_stubs.append(self.allocator, stub);
             } else |_| {
                 // TODO this entire logic has to be cleaned up.
                 try file.seekTo(0);
-                if (Archive.isArchive(file)) {
+
+                if (try Archive.isArchive(file)) {
                     const archive = try self.allocator.create(Archive);
                     errdefer self.allocator.destroy(archive);
 
@@ -438,6 +420,7 @@ fn parseLibs(self: *Zld, libs: []const []const u8) !void {
                     archive.arch = self.arch.?;
                     archive.name = try self.allocator.dupe(u8, lib);
                     archive.file = file;
+
                     try archive.parse();
                     try self.archives.append(self.allocator, archive);
                 } else {
@@ -449,26 +432,28 @@ fn parseLibs(self: *Zld, libs: []const []const u8) !void {
     }
 }
 
-fn parseLibSystem(self: *Zld, lib_system_path: []const u8) !void {
-    const file = try fs.cwd().openFile(lib_system_path, .{});
+fn parseLibSystem(self: *Zld, libc_stub_path: []const u8) !void {
+    const file = try fs.cwd().openFile(libc_stub_path, .{});
+    defer file.close();
+
+    var lib_stub = try Stub.LibStub.loadFromFile(self.allocator, file);
 
-    var lib_stub = try LibStub.loadFromFile(self.allocator, file);
-    defer lib_stub.deinit();
+    const stub = try self.allocator.create(Stub);
+    errdefer self.allocator.destroy(stub);
 
-    const dylib = try self.allocator.create(Dylib);
-    errdefer self.allocator.destroy(dylib);
+    stub.* = Stub.init(self.allocator);
+    stub.arch = self.arch.?;
+    stub.name = try self.allocator.dupe(u8, libc_stub_path);
+    stub.lib_stub = lib_stub;
 
-    dylib.* = Dylib.init(self.allocator);
-    dylib.arch = self.arch.?;
-    dylib.name = try self.allocator.dupe(u8, lib_system_path);
-    dylib.file = file;
-    dylib.ordinal = @intCast(u16, self.dylibs.items.len) + 1;
+    try stub.parse();
 
-    try dylib.parseFromStub(lib_stub);
-    try self.dylibs.append(self.allocator, dylib);
+    self.libsystem_stub_index = @intCast(u16, self.lib_stubs.items.len);
+    try self.lib_stubs.append(self.allocator, stub);
 
-    // Add LC_LOAD_DYLIB command
-    const dylib_id = dylib.id orelse unreachable;
+    // Add LC_LOAD_DYLIB load command.
+    stub.ordinal = self.next_dylib_ordinal;
+    const dylib_id = stub.id orelse unreachable;
     var dylib_cmd = try createLoadDylibCommand(
         self.allocator,
         dylib_id.name,
@@ -477,8 +462,8 @@ fn parseLibSystem(self: *Zld, lib_system_path: []const u8) !void {
         dylib_id.compatibility_version,
     );
     errdefer dylib_cmd.deinit(self.allocator);
-
     try self.load_commands.append(self.allocator, .{ .Dylib = dylib_cmd });
+    self.next_dylib_ordinal += 1;
 }
 
 fn mapAndUpdateSections(
@@ -1906,21 +1891,34 @@ fn resolveSymbols(self: *Zld) !void {
     for (self.unresolved.values()) |value| {
         unresolved.appendAssumeCapacity(value);
     }
-    self.unresolved.clearAndFree(self.allocator);
-
-    var has_undefined = false;
-    while (unresolved.popOrNull()) |undef| {
-        var found = false;
-        for (self.dylibs.items) |dylib| {
-            const proxy = dylib.symbols.get(undef.name) orelse continue;
-            try self.imports.putNoClobber(self.allocator, proxy.name, proxy);
-            undef.alias = proxy;
-            found = true;
-        }
-
-        if (!found) {
-            if (mem.eql(u8, undef.name, "___dso_handle")) {
-                const proxy = self.imports.get(undef.name) orelse blk: {
+    self.unresolved.clearRetainingCapacity();
+
+    var referenced = std.AutoHashMap(union(enum) {
+        dylib: *Dylib,
+        stub: *Stub,
+    }, void).init(self.allocator);
+    defer referenced.deinit();
+
+    loop: while (unresolved.popOrNull()) |undef| {
+        const proxy = self.imports.get(undef.name) orelse outer: {
+            const proxy = inner: {
+                for (self.dylibs.items) |dylib| {
+                    const proxy = (try dylib.createProxy(undef.name)) orelse continue;
+                    try referenced.put(.{ .dylib = dylib }, {});
+                    break :inner proxy;
+                }
+                for (self.lib_stubs.items) |stub, i| {
+                    const proxy = (try stub.createProxy(undef.name)) orelse continue;
+                    if (self.libsystem_stub_index.? != @intCast(u16, i)) {
+                        // LibSystem gets its load command separately.
+                        try referenced.put(.{ .stub = stub }, {});
+                    }
+                    break :inner proxy;
+                }
+                if (mem.eql(u8, undef.name, "___dso_handle")) {
+                    // TODO this is just a temp patch until I work out what to actually
+                    // do with ___dso_handle and __mh_execute_header symbols which are
+                    // synthetically created by the linker on macOS.
                     const name = try self.allocator.dupe(u8, undef.name);
                     const proxy = try self.allocator.create(Symbol.Proxy);
                     errdefer self.allocator.destroy(proxy);
@@ -1929,26 +1927,69 @@ fn resolveSymbols(self: *Zld) !void {
                             .@"type" = .proxy,
                             .name = name,
                         },
+                        .file = null,
                     };
-                    try self.imports.putNoClobber(self.allocator, name, &proxy.base);
-                    break :blk &proxy.base;
-                };
-                undef.alias = proxy;
-                continue;
+                    break :inner &proxy.base;
+                }
+
+                self.unresolved.putAssumeCapacityNoClobber(undef.name, undef);
+                continue :loop;
+            };
+
+            try self.imports.putNoClobber(self.allocator, proxy.name, proxy);
+            break :outer proxy;
+        };
+        undef.alias = proxy;
+    }
+
+    // Add LC_LOAD_DYLIB load command for each referenced dylib/stub.
+    var it = referenced.iterator();
+    while (it.next()) |key| {
+        var dylib_cmd = blk: {
+            switch (key.key_ptr.*) {
+                .dylib => |dylib| {
+                    dylib.ordinal = self.next_dylib_ordinal;
+                    const dylib_id = dylib.id orelse unreachable;
+                    break :blk try createLoadDylibCommand(
+                        self.allocator,
+                        dylib_id.name,
+                        dylib_id.timestamp,
+                        dylib_id.current_version,
+                        dylib_id.compatibility_version,
+                    );
+                },
+                .stub => |stub| {
+                    stub.ordinal = self.next_dylib_ordinal;
+                    const dylib_id = stub.id orelse unreachable;
+                    break :blk try createLoadDylibCommand(
+                        self.allocator,
+                        dylib_id.name,
+                        dylib_id.timestamp,
+                        dylib_id.current_version,
+                        dylib_id.compatibility_version,
+                    );
+                },
             }
+        };
+        errdefer dylib_cmd.deinit(self.allocator);
+        try self.load_commands.append(self.allocator, .{ .Dylib = dylib_cmd });
+        self.next_dylib_ordinal += 1;
+    }
 
+    if (self.unresolved.count() > 0) {
+        for (self.unresolved.values()) |undef| {
             log.err("undefined reference to symbol '{s}'", .{undef.name});
             log.err("    | referenced in {s}", .{
                 undef.cast(Symbol.Unresolved).?.file.name.?,
             });
-            has_undefined = true;
         }
-    }
 
-    if (has_undefined) return error.UndefinedSymbolReference;
+        return error.UndefinedSymbolReference;
+    }
 
     // Finally put dyld_stub_binder as an Import
-    const proxy = self.dylibs.items[self.dylibs.items.len - 1].symbols.get("dyld_stub_binder") orelse {
+    const libsystem_stub = self.lib_stubs.items[self.libsystem_stub_index.?];
+    const proxy = (try libsystem_stub.createProxy("dyld_stub_binder")) orelse {
         log.err("undefined reference to symbol 'dyld_stub_binder'", .{});
         return error.UndefinedSymbolReference;
     };
@@ -2814,7 +2855,7 @@ fn writeBindInfoTable(self: *Zld) !void {
                 try pointers.append(.{
                     .offset = base_offset + proxy.base.got_index.? * @sizeOf(u64),
                     .segment_id = segment_id,
-                    .dylib_ordinal = if (proxy.dylib) |dylib| dylib.ordinal.? else 0,
+                    .dylib_ordinal = proxy.dylibOrdinal(),
                     .name = proxy.base.name,
                 });
             }
@@ -2833,7 +2874,7 @@ fn writeBindInfoTable(self: *Zld) !void {
         try pointers.append(.{
             .offset = base_offset,
             .segment_id = segment_id,
-            .dylib_ordinal = if (proxy.dylib) |dylib| dylib.ordinal.? else 0,
+            .dylib_ordinal = proxy.dylibOrdinal(),
             .name = proxy.base.name,
         });
     }
@@ -2873,7 +2914,7 @@ fn writeLazyBindInfoTable(self: *Zld) !void {
             pointers.appendAssumeCapacity(.{
                 .offset = base_offset + sym.stubs_index.? * @sizeOf(u64),
                 .segment_id = segment_id,
-                .dylib_ordinal = if (proxy.dylib) |dylib| dylib.ordinal.? else 0,
+                .dylib_ordinal = proxy.dylibOrdinal(),
                 .name = sym.name,
             });
         }
@@ -3181,12 +3222,11 @@ fn writeSymbolTable(self: *Zld) !void {
 
     for (self.imports.values()) |sym| {
         const proxy = sym.cast(Symbol.Proxy) orelse unreachable;
-        const dylib_ordinal = if (proxy.dylib) |dylib| dylib.ordinal.? else 0;
         try undefs.append(.{
             .n_strx = try self.makeString(sym.name),
             .n_type = macho.N_UNDF | macho.N_EXT,
             .n_sect = 0,
-            .n_desc = (dylib_ordinal * macho.N_SYMBOL_RESOLVER) | macho.REFERENCE_FLAG_UNDEFINED_NON_LAZY,
+            .n_desc = (proxy.dylibOrdinal() * macho.N_SYMBOL_RESOLVER) | macho.REFERENCE_FLAG_UNDEFINED_NON_LAZY,
             .n_value = 0,
         });
     }
CMakeLists.txt
@@ -579,6 +579,7 @@ set(ZIG_STAGE2_SOURCES
     "${CMAKE_SOURCE_DIR}/src/link/MachO/DebugSymbols.zig"
     "${CMAKE_SOURCE_DIR}/src/link/MachO/Dylib.zig"
     "${CMAKE_SOURCE_DIR}/src/link/MachO/Object.zig"
+    "${CMAKE_SOURCE_DIR}/src/link/MachO/Stub.zig"
     "${CMAKE_SOURCE_DIR}/src/link/MachO/Symbol.zig"
     "${CMAKE_SOURCE_DIR}/src/link/MachO/Trie.zig"
     "${CMAKE_SOURCE_DIR}/src/link/MachO/Zld.zig"