Commit 2e690f5c74

Jakub Konka <kubkon@jakubkonka.com>
2023-10-30 00:09:32
macho: implement enough of extern handling to pass comptime export tests
1 parent 71dfea1
src/arch/x86_64/CodeGen.zig
@@ -13080,12 +13080,13 @@ fn genExternSymbolRef(
             else => unreachable,
         }
     } else if (self.bin_file.cast(link.File.MachO)) |macho_file| {
+        const global_index = try macho_file.getGlobalSymbol(callee, lib);
         _ = try self.addInst(.{
             .tag = .call,
             .ops = .extern_fn_reloc,
             .data = .{ .reloc = .{
                 .atom_index = atom_index,
-                .sym_index = try macho_file.getGlobalSymbol(callee, lib),
+                .sym_index = link.File.MachO.global_symbol_bit | global_index,
             } },
         });
     } else return self.fail("TODO implement calling extern functions", .{});
src/arch/x86_64/Emit.zig
@@ -52,7 +52,10 @@ pub fn emitMir(emit: *Emit) Error!void {
                     // Add relocation to the decl.
                     const atom_index =
                         macho_file.getAtomIndexForSymbol(.{ .sym_index = symbol.atom_index }).?;
-                    const target = macho_file.getGlobalByIndex(symbol.sym_index);
+                    const target = if (link.File.MachO.global_symbol_bit & symbol.sym_index != 0)
+                        macho_file.getGlobalByIndex(link.File.MachO.global_symbol_mask & symbol.sym_index)
+                    else
+                        link.File.MachO.SymbolWithLoc{ .sym_index = symbol.sym_index };
                     try link.File.MachO.Atom.addRelocation(macho_file, atom_index, .{
                         .type = .branch,
                         .target = target,
@@ -116,6 +119,10 @@ pub fn emitMir(emit: *Emit) Error!void {
                 } else if (emit.lower.bin_file.cast(link.File.MachO)) |macho_file| {
                     const atom_index =
                         macho_file.getAtomIndexForSymbol(.{ .sym_index = symbol.atom_index }).?;
+                    const target = if (link.File.MachO.global_symbol_bit & symbol.sym_index != 0)
+                        macho_file.getGlobalByIndex(link.File.MachO.global_symbol_mask & symbol.sym_index)
+                    else
+                        link.File.MachO.SymbolWithLoc{ .sym_index = symbol.sym_index };
                     try link.File.MachO.Atom.addRelocation(macho_file, atom_index, .{
                         .type = switch (lowered_relocs[0].target) {
                             .linker_got => .got,
@@ -123,7 +130,7 @@ pub fn emitMir(emit: *Emit) Error!void {
                             .linker_tlv => .tlv,
                             else => unreachable,
                         },
-                        .target = .{ .sym_index = symbol.sym_index },
+                        .target = target,
                         .offset = @as(u32, @intCast(end_offset - 4)),
                         .addend = 0,
                         .pcrel = true,
src/link/MachO/Atom.zig
@@ -300,7 +300,7 @@ pub fn resolveRelocations(
     relocs: []*const Relocation,
     code: []u8,
 ) void {
-    log.debug("relocating '{s}'", .{macho_file.getAtom(atom_index).getName(macho_file)});
+    relocs_log.debug("relocating '{s}'", .{macho_file.getAtom(atom_index).getName(macho_file)});
     for (relocs) |reloc| {
         reloc.resolve(macho_file, atom_index, code);
     }
@@ -603,7 +603,7 @@ pub fn resolveRelocs(
     const atom = macho_file.getAtom(atom_index);
     assert(atom.getFile() != null); // synthetic atoms do not have relocs
 
-    log.debug("resolving relocations in ATOM(%{d}, '{s}')", .{
+    relocs_log.debug("resolving relocations in ATOM(%{d}, '{s}')", .{
         atom.sym_index,
         macho_file.getSymbolName(atom.getSymbolWithLoc()),
     });
@@ -683,7 +683,7 @@ fn resolveRelocsArm64(
             .ARM64_RELOC_ADDEND => {
                 assert(addend == null);
 
-                log.debug("  RELA({s}) @ {x} => {x}", .{ @tagName(rel_type), rel.r_address, rel.r_symbolnum });
+                relocs_log.debug("  RELA({s}) @ {x} => {x}", .{ @tagName(rel_type), rel.r_address, rel.r_symbolnum });
 
                 addend = rel.r_symbolnum;
                 continue;
@@ -691,7 +691,7 @@ fn resolveRelocsArm64(
             .ARM64_RELOC_SUBTRACTOR => {
                 assert(subtractor == null);
 
-                log.debug("  RELA({s}) @ {x} => %{d} in object({?d})", .{
+                relocs_log.debug("  RELA({s}) @ {x} => %{d} in object({?d})", .{
                     @tagName(rel_type),
                     rel.r_address,
                     rel.r_symbolnum,
@@ -719,7 +719,7 @@ fn resolveRelocsArm64(
         });
         const rel_offset = @as(u32, @intCast(rel.r_address - context.base_offset));
 
-        log.debug("  RELA({s}) @ {x} => %{d} ('{s}') in object({?})", .{
+        relocs_log.debug("  RELA({s}) @ {x} => %{d} ('{s}') in object({?})", .{
             @tagName(rel_type),
             rel.r_address,
             target.sym_index,
@@ -745,11 +745,11 @@ fn resolveRelocsArm64(
             break :blk getRelocTargetAddress(macho_file, target, is_tlv);
         };
 
-        log.debug("    | source_addr = 0x{x}", .{source_addr});
+        relocs_log.debug("    | source_addr = 0x{x}", .{source_addr});
 
         switch (rel_type) {
             .ARM64_RELOC_BRANCH26 => {
-                log.debug("  source {s} (object({?})), target {s}", .{
+                relocs_log.debug("  source {s} (object({?})), target {s}", .{
                     macho_file.getSymbolName(atom.getSymbolWithLoc()),
                     atom.getFile(),
                     macho_file.getSymbolName(target),
@@ -759,7 +759,7 @@ fn resolveRelocsArm64(
                     source_addr,
                     target_addr,
                 )) |disp| blk: {
-                    log.debug("    | target_addr = 0x{x}", .{target_addr});
+                    relocs_log.debug("    | target_addr = 0x{x}", .{target_addr});
                     break :blk disp;
                 } else |_| blk: {
                     const thunk_index = macho_file.thunk_table.get(atom_index).?;
@@ -769,7 +769,7 @@ fn resolveRelocsArm64(
                     else
                         thunk.getTrampoline(macho_file, .atom, target).?;
                     const thunk_addr = macho_file.getSymbol(thunk_sym_loc).n_value;
-                    log.debug("    | target_addr = 0x{x} (thunk)", .{thunk_addr});
+                    relocs_log.debug("    | target_addr = 0x{x} (thunk)", .{thunk_addr});
                     break :blk try Relocation.calcPcRelativeDisplacementArm64(source_addr, thunk_addr);
                 };
 
@@ -790,7 +790,7 @@ fn resolveRelocsArm64(
             => {
                 const adjusted_target_addr = @as(u64, @intCast(@as(i64, @intCast(target_addr)) + (addend orelse 0)));
 
-                log.debug("    | target_addr = 0x{x}", .{adjusted_target_addr});
+                relocs_log.debug("    | target_addr = 0x{x}", .{adjusted_target_addr});
 
                 const pages = @as(u21, @bitCast(Relocation.calcNumberOfPages(source_addr, adjusted_target_addr)));
                 const code = atom_code[rel_offset..][0..4];
@@ -809,7 +809,7 @@ fn resolveRelocsArm64(
             .ARM64_RELOC_PAGEOFF12 => {
                 const adjusted_target_addr = @as(u64, @intCast(@as(i64, @intCast(target_addr)) + (addend orelse 0)));
 
-                log.debug("    | target_addr = 0x{x}", .{adjusted_target_addr});
+                relocs_log.debug("    | target_addr = 0x{x}", .{adjusted_target_addr});
 
                 const code = atom_code[rel_offset..][0..4];
                 if (Relocation.isArithmeticOp(code)) {
@@ -848,7 +848,7 @@ fn resolveRelocsArm64(
                 const code = atom_code[rel_offset..][0..4];
                 const adjusted_target_addr = @as(u64, @intCast(@as(i64, @intCast(target_addr)) + (addend orelse 0)));
 
-                log.debug("    | target_addr = 0x{x}", .{adjusted_target_addr});
+                relocs_log.debug("    | target_addr = 0x{x}", .{adjusted_target_addr});
 
                 const off = try Relocation.calcPageOffset(adjusted_target_addr, .load_store_64);
                 var inst: aarch64.Instruction = .{
@@ -866,7 +866,7 @@ fn resolveRelocsArm64(
                 const code = atom_code[rel_offset..][0..4];
                 const adjusted_target_addr = @as(u64, @intCast(@as(i64, @intCast(target_addr)) + (addend orelse 0)));
 
-                log.debug("    | target_addr = 0x{x}", .{adjusted_target_addr});
+                relocs_log.debug("    | target_addr = 0x{x}", .{adjusted_target_addr});
 
                 const RegInfo = struct {
                     rd: u5,
@@ -923,7 +923,7 @@ fn resolveRelocsArm64(
             },
 
             .ARM64_RELOC_POINTER_TO_GOT => {
-                log.debug("    | target_addr = 0x{x}", .{target_addr});
+                relocs_log.debug("    | target_addr = 0x{x}", .{target_addr});
                 const result = math.cast(i32, @as(i64, @intCast(target_addr)) - @as(i64, @intCast(source_addr))) orelse
                     return error.Overflow;
                 mem.writeIntLittle(u32, atom_code[rel_offset..][0..4], @as(u32, @bitCast(result)));
@@ -951,7 +951,7 @@ fn resolveRelocsArm64(
                         break :blk @as(i64, @intCast(target_addr)) + ptr_addend;
                     }
                 };
-                log.debug("    | target_addr = 0x{x}", .{result});
+                relocs_log.debug("    | target_addr = 0x{x}", .{result});
 
                 if (rel.r_length == 3) {
                     mem.writeIntLittle(u64, atom_code[rel_offset..][0..8], @as(u64, @bitCast(result)));
@@ -987,7 +987,7 @@ fn resolveRelocsX86(
             .X86_64_RELOC_SUBTRACTOR => {
                 assert(subtractor == null);
 
-                log.debug("  RELA({s}) @ {x} => %{d} in object({?d})", .{
+                relocs_log.debug("  RELA({s}) @ {x} => %{d} in object({?d})", .{
                     @tagName(rel_type),
                     rel.r_address,
                     rel.r_symbolnum,
@@ -1015,7 +1015,7 @@ fn resolveRelocsX86(
         });
         const rel_offset = @as(u32, @intCast(rel.r_address - context.base_offset));
 
-        log.debug("  RELA({s}) @ {x} => %{d} ('{s}') in object({?})", .{
+        relocs_log.debug("  RELA({s}) @ {x} => %{d} ('{s}') in object({?})", .{
             @tagName(rel_type),
             rel.r_address,
             target.sym_index,
@@ -1041,13 +1041,13 @@ fn resolveRelocsX86(
             break :blk getRelocTargetAddress(macho_file, target, is_tlv);
         };
 
-        log.debug("    | source_addr = 0x{x}", .{source_addr});
+        relocs_log.debug("    | source_addr = 0x{x}", .{source_addr});
 
         switch (rel_type) {
             .X86_64_RELOC_BRANCH => {
                 const addend = mem.readIntLittle(i32, atom_code[rel_offset..][0..4]);
                 const adjusted_target_addr = @as(u64, @intCast(@as(i64, @intCast(target_addr)) + addend));
-                log.debug("    | target_addr = 0x{x}", .{adjusted_target_addr});
+                relocs_log.debug("    | target_addr = 0x{x}", .{adjusted_target_addr});
                 const disp = try Relocation.calcPcRelativeDisplacementX86(source_addr, adjusted_target_addr, 0);
                 mem.writeIntLittle(i32, atom_code[rel_offset..][0..4], disp);
             },
@@ -1057,7 +1057,7 @@ fn resolveRelocsX86(
             => {
                 const addend = mem.readIntLittle(i32, atom_code[rel_offset..][0..4]);
                 const adjusted_target_addr = @as(u64, @intCast(@as(i64, @intCast(target_addr)) + addend));
-                log.debug("    | target_addr = 0x{x}", .{adjusted_target_addr});
+                relocs_log.debug("    | target_addr = 0x{x}", .{adjusted_target_addr});
                 const disp = try Relocation.calcPcRelativeDisplacementX86(source_addr, adjusted_target_addr, 0);
                 mem.writeIntLittle(i32, atom_code[rel_offset..][0..4], disp);
             },
@@ -1065,7 +1065,7 @@ fn resolveRelocsX86(
             .X86_64_RELOC_TLV => {
                 const addend = mem.readIntLittle(i32, atom_code[rel_offset..][0..4]);
                 const adjusted_target_addr = @as(u64, @intCast(@as(i64, @intCast(target_addr)) + addend));
-                log.debug("    | target_addr = 0x{x}", .{adjusted_target_addr});
+                relocs_log.debug("    | target_addr = 0x{x}", .{adjusted_target_addr});
                 const disp = try Relocation.calcPcRelativeDisplacementX86(source_addr, adjusted_target_addr, 0);
 
                 if (macho_file.tlv_ptr_table.lookup.get(target) == null) {
@@ -1101,7 +1101,7 @@ fn resolveRelocsX86(
 
                 const adjusted_target_addr = @as(u64, @intCast(@as(i64, @intCast(target_addr)) + addend));
 
-                log.debug("    | target_addr = 0x{x}", .{adjusted_target_addr});
+                relocs_log.debug("    | target_addr = 0x{x}", .{adjusted_target_addr});
 
                 const disp = try Relocation.calcPcRelativeDisplacementX86(source_addr, adjusted_target_addr, correction);
                 mem.writeIntLittle(i32, atom_code[rel_offset..][0..4], disp);
@@ -1129,7 +1129,7 @@ fn resolveRelocsX86(
                         break :blk @as(i64, @intCast(target_addr)) + addend;
                     }
                 };
-                log.debug("    | target_addr = 0x{x}", .{result});
+                relocs_log.debug("    | target_addr = 0x{x}", .{result});
 
                 if (rel.r_length == 3) {
                     mem.writeIntLittle(u64, atom_code[rel_offset..][0..8], @as(u64, @bitCast(result)));
@@ -1247,6 +1247,7 @@ const build_options = @import("build_options");
 const aarch64 = @import("../../arch/aarch64/bits.zig");
 const assert = std.debug.assert;
 const log = std.log.scoped(.link);
+const relocs_log = std.log.scoped(.link_relocs);
 const macho = std.macho;
 const math = std.math;
 const mem = std.mem;
src/link/MachO/Relocation.zig
@@ -99,7 +99,7 @@ pub fn resolve(self: Relocation, macho_file: *MachO, atom_index: Atom.Index, cod
         else => @as(i64, @intCast(target_base_addr)) + self.addend,
     };
 
-    log.debug("  ({x}: [() => 0x{x} ({s})) ({s})", .{
+    relocs_log.debug("  ({x}: [() => 0x{x} ({s})) ({s})", .{
         source_addr,
         target_addr,
         macho_file.getSymbolName(self.target),
@@ -256,7 +256,7 @@ const Relocation = @This();
 const std = @import("std");
 const aarch64 = @import("../../arch/aarch64/bits.zig");
 const assert = std.debug.assert;
-const log = std.log.scoped(.link);
+const relocs_log = std.log.scoped(.link_relocs);
 const macho = std.macho;
 const math = std.math;
 const mem = std.mem;
src/link/MachO/zld.zig
@@ -390,9 +390,7 @@ pub fn linkWithZld(
 
         try macho_file.parseDependentLibs(&dependent_libs);
 
-        var actions = std.ArrayList(MachO.ResolveAction).init(gpa);
-        defer actions.deinit();
-        try macho_file.resolveSymbols(&actions);
+        try macho_file.resolveSymbols();
         if (macho_file.unresolved.count() > 0) {
             try macho_file.reportUndefined();
             return error.FlushFailure;
src/link/MachO.zig
@@ -50,7 +50,7 @@ tlv_ptr_section_index: ?u8 = null,
 locals: std.ArrayListUnmanaged(macho.nlist_64) = .{},
 globals: std.ArrayListUnmanaged(SymbolWithLoc) = .{},
 resolver: std.StringHashMapUnmanaged(u32) = .{},
-unresolved: std.AutoArrayHashMapUnmanaged(u32, ResolveAction.Kind) = .{},
+unresolved: std.AutoArrayHashMapUnmanaged(u32, void) = .{},
 
 locals_free_list: std.ArrayListUnmanaged(u32) = .{},
 globals_free_list: std.ArrayListUnmanaged(u32) = .{},
@@ -115,6 +115,10 @@ anon_decls: AnonDeclTable = .{},
 /// Note that once we refactor `Atom`'s lifetime and ownership rules,
 /// this will be a table indexed by index into the list of Atoms.
 relocs: RelocationTable = .{},
+/// TODO I do not have time to make this right but this will go once
+/// MachO linker is rewritten more-or-less to feature the same resolution
+/// mechanism as the ELF linker.
+actions: ActionTable = .{},
 
 /// A table of rebases indexed by the owning them `Atom`.
 /// Note that once we refactor `Atom`'s lifetime and ownership rules,
@@ -417,9 +421,7 @@ pub fn flushModule(self: *MachO, comp: *Compilation, prog_node: *std.Progress.No
         try self.parseDependentLibs(&dependent_libs);
     }
 
-    var actions = std.ArrayList(ResolveAction).init(self.base.allocator);
-    defer actions.deinit();
-    try self.resolveSymbols(&actions);
+    try self.resolveSymbols();
 
     if (self.getEntryPoint() == null) {
         self.error_flags.no_entry_point_found = true;
@@ -429,11 +431,16 @@ pub fn flushModule(self: *MachO, comp: *Compilation, prog_node: *std.Progress.No
         return error.FlushFailure;
     }
 
-    for (actions.items) |action| switch (action.kind) {
-        .none => {},
-        .add_got => try self.addGotEntry(action.target),
-        .add_stub => try self.addStubEntry(action.target),
-    };
+    {
+        var it = self.actions.iterator();
+        while (it.next()) |entry| {
+            const global_index = entry.key_ptr.*;
+            const global = self.globals.items[global_index];
+            const flags = entry.value_ptr.*;
+            if (flags.add_got) try self.addGotEntry(global);
+            if (flags.add_stub) try self.addStubEntry(global);
+        }
+    }
 
     try self.createDyldPrivateAtom();
     try self.writeStubHelperPreamble();
@@ -1589,18 +1596,18 @@ pub fn createDsoHandleSymbol(self: *MachO) !void {
     _ = self.unresolved.swapRemove(self.getGlobalIndex("___dso_handle").?);
 }
 
-pub fn resolveSymbols(self: *MachO, actions: *std.ArrayList(ResolveAction)) !void {
+pub fn resolveSymbols(self: *MachO) !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.base.options.output_mode == .Exe) {
         const entry_name = self.base.options.entry orelse load_commands.default_entry_point;
-        _ = try self.addUndefined(entry_name, .none);
+        _ = try self.addUndefined(entry_name, .{});
     }
 
     // Force resolution of any symbols requested by the user.
     for (self.base.options.force_undefined_symbols.keys()) |sym_name| {
-        _ = try self.addUndefined(sym_name, .none);
+        _ = try self.addUndefined(sym_name, .{});
     }
 
     for (self.objects.items, 0..) |_, object_id| {
@@ -1612,13 +1619,13 @@ pub fn resolveSymbols(self: *MachO, actions: *std.ArrayList(ResolveAction)) !voi
     // Finally, force resolution of dyld_stub_binder if there are imports
     // requested.
     if (self.unresolved.count() > 0 and self.dyld_stub_binder_index == null) {
-        self.dyld_stub_binder_index = try self.addUndefined("dyld_stub_binder", .add_got);
+        self.dyld_stub_binder_index = try self.addUndefined("dyld_stub_binder", .{ .add_got = true });
     }
     if (!self.base.options.single_threaded and self.mode == .incremental) {
-        _ = try self.addUndefined("__tlv_bootstrap", .none);
+        _ = try self.addUndefined("__tlv_bootstrap", .{});
     }
 
-    try self.resolveSymbolsInDylibs(actions);
+    try self.resolveSymbolsInDylibs();
 
     try self.createMhExecuteHeaderSymbol();
     try self.createDsoHandleSymbol();
@@ -1634,7 +1641,7 @@ fn resolveGlobalSymbol(self: *MachO, current: SymbolWithLoc) !void {
     if (!gop.found_existing) {
         gop.value_ptr.* = current;
         if (sym.undf() and !sym.tentative()) {
-            try self.unresolved.putNoClobber(gpa, self.getGlobalIndex(sym_name).?, .none);
+            try self.unresolved.putNoClobber(gpa, self.getGlobalIndex(sym_name).?, {});
         }
         return;
     }
@@ -1766,7 +1773,7 @@ fn resolveSymbolsInArchives(self: *MachO) !void {
     }
 }
 
-fn resolveSymbolsInDylibs(self: *MachO, actions: *std.ArrayList(ResolveAction)) !void {
+fn resolveSymbolsInDylibs(self: *MachO) !void {
     if (self.dylibs.items.len == 0) return;
 
     const gpa = self.base.allocator;
@@ -1793,11 +1800,7 @@ fn resolveSymbolsInDylibs(self: *MachO, actions: *std.ArrayList(ResolveAction))
                 sym.n_desc |= macho.N_WEAK_REF;
             }
 
-            if (self.unresolved.fetchSwapRemove(global_index)) |entry| blk: {
-                if (!sym.undf()) break :blk;
-                if (self.mode == .zld) break :blk;
-                try actions.append(.{ .kind = entry.value, .target = global });
-            }
+            _ = self.unresolved.swapRemove(global_index);
 
             continue :loop;
         }
@@ -1927,6 +1930,7 @@ pub fn deinit(self: *MachO) void {
         relocs.deinit(gpa);
     }
     self.relocs.deinit(gpa);
+    self.actions.deinit(gpa);
 
     for (self.rebases.values()) |*rebases| {
         rebases.deinit(gpa);
@@ -2266,6 +2270,7 @@ fn lowerConst(
     log.debug("  (required alignment 0x{x})", .{required_alignment});
 
     try self.writeAtom(atom_index, code);
+    self.markRelocsDirtyByTarget(atom.getSymbolWithLoc());
 
     return .{ .ok = atom_index };
 }
@@ -2281,12 +2286,16 @@ pub fn updateDecl(self: *MachO, mod: *Module, decl_index: Module.Decl.Index) !vo
     const decl = mod.declPtr(decl_index);
 
     if (decl.val.getExternFunc(mod)) |_| {
-        return; // TODO Should we do more when front-end analyzed extern decl?
+        return;
     }
-    if (decl.val.getVariable(mod)) |variable| {
-        if (variable.is_extern) {
-            return; // TODO Should we do more when front-end analyzed extern decl?
-        }
+
+    if (decl.isExtern(mod)) {
+        // TODO make this part of getGlobalSymbol
+        const name = mod.intern_pool.stringToSlice(decl.name);
+        const sym_name = try std.fmt.allocPrint(self.base.allocator, "_{s}", .{name});
+        defer self.base.allocator.free(sym_name);
+        _ = try self.addUndefined(sym_name, .{ .add_got = true });
+        return;
     }
 
     const is_threadlocal = if (decl.val.getVariable(mod)) |variable|
@@ -2753,7 +2762,17 @@ pub fn updateExports(
         }
 
         const global_sym_index = metadata.getExport(self, exp_name) orelse blk: {
-            const global_sym_index = try self.allocateSymbol();
+            const global_sym_index = if (self.getGlobalIndex(exp_name)) |global_index| ind: {
+                const global = self.globals.items[global_index];
+                // TODO this is just plain wrong as it all should happen in a single `resolveSymbols`
+                // pass. This will go away once we abstact away Zig's incremental compilation into
+                // its own module.
+                if (global.getFile() == null and self.getSymbol(global).undf()) {
+                    _ = self.unresolved.swapRemove(global_index);
+                    break :ind global.sym_index;
+                }
+                break :ind try self.allocateSymbol();
+            } else try self.allocateSymbol();
             try metadata.exports.append(gpa, global_sym_index);
             break :blk global_sym_index;
         };
@@ -3422,7 +3441,7 @@ pub fn getGlobalSymbol(self: *MachO, name: []const u8, lib_name: ?[]const u8) !u
     const gpa = self.base.allocator;
     const sym_name = try std.fmt.allocPrint(gpa, "_{s}", .{name});
     defer gpa.free(sym_name);
-    return self.addUndefined(sym_name, .add_stub);
+    return self.addUndefined(sym_name, .{ .add_stub = true });
 }
 
 pub fn writeSegmentHeaders(self: *MachO, writer: anytype) !void {
@@ -4706,13 +4725,16 @@ pub fn ptraceDetach(self: *MachO, pid: std.os.pid_t) !void {
     self.hot_state.mach_task = null;
 }
 
-fn addUndefined(self: *MachO, name: []const u8, action: ResolveAction.Kind) !u32 {
+pub fn addUndefined(self: *MachO, name: []const u8, flags: RelocFlags) !u32 {
     const gpa = self.base.allocator;
 
     const gop = try self.getOrPutGlobalPtr(name);
     const global_index = self.getGlobalIndex(name).?;
 
-    if (gop.found_existing) return global_index;
+    if (gop.found_existing) {
+        try self.updateRelocActions(global_index, flags);
+        return global_index;
+    }
 
     const sym_index = try self.allocateSymbol();
     const sym_loc = SymbolWithLoc{ .sym_index = sym_index };
@@ -4720,13 +4742,23 @@ fn addUndefined(self: *MachO, name: []const u8, action: ResolveAction.Kind) !u32
 
     const sym = self.getSymbolPtr(sym_loc);
     sym.n_strx = try self.strtab.insert(gpa, name);
-    sym.n_type = macho.N_UNDF;
+    sym.n_type = macho.N_EXT | macho.N_UNDF;
 
-    try self.unresolved.putNoClobber(gpa, global_index, action);
+    try self.unresolved.putNoClobber(gpa, global_index, {});
+    try self.updateRelocActions(global_index, flags);
 
     return global_index;
 }
 
+fn updateRelocActions(self: *MachO, global_index: u32, flags: RelocFlags) !void {
+    const act_gop = try self.actions.getOrPut(self.base.allocator, global_index);
+    if (!act_gop.found_existing) {
+        act_gop.value_ptr.* = .{};
+    }
+    act_gop.value_ptr.add_got = act_gop.value_ptr.add_got or flags.add_got;
+    act_gop.value_ptr.add_stub = act_gop.value_ptr.add_stub or flags.add_stub;
+}
+
 pub fn makeStaticString(bytes: []const u8) [16]u8 {
     var buf = [_]u8{0} ** 16;
     @memcpy(buf[0..bytes.len], bytes);
@@ -4838,6 +4870,11 @@ const GetOrPutGlobalPtrResult = struct {
     value_ptr: *SymbolWithLoc,
 };
 
+/// Used only for disambiguating local from global at relocation level.
+/// TODO this must go away.
+pub const global_symbol_bit: u32 = 0x80000000;
+pub const global_symbol_mask: u32 = 0x7fffffff;
+
 /// Return pointer to the global entry for `name` if one exists.
 /// Puts a new global entry for `name` if one doesn't exist, and
 /// returns a pointer to it.
@@ -5510,16 +5547,11 @@ const BindingTable = std.AutoArrayHashMapUnmanaged(Atom.Index, std.ArrayListUnma
 const UnnamedConstTable = std.AutoArrayHashMapUnmanaged(Module.Decl.Index, std.ArrayListUnmanaged(Atom.Index));
 const RebaseTable = std.AutoArrayHashMapUnmanaged(Atom.Index, std.ArrayListUnmanaged(u32));
 const RelocationTable = std.AutoArrayHashMapUnmanaged(Atom.Index, std.ArrayListUnmanaged(Relocation));
+const ActionTable = std.AutoHashMapUnmanaged(u32, RelocFlags);
 
-pub const ResolveAction = struct {
-    kind: Kind,
-    target: SymbolWithLoc,
-
-    const Kind = enum {
-        none,
-        add_got,
-        add_stub,
-    };
+pub const RelocFlags = packed struct {
+    add_got: bool = false,
+    add_stub: bool = false,
 };
 
 pub const SymbolWithLoc = extern struct {
src/codegen.zig
@@ -721,6 +721,7 @@ fn lowerAnonDeclRef(
     const ptr_width_bytes = @divExact(target.ptrBitWidth(), 8);
     const decl_val = anon_decl.val;
     const decl_ty = mod.intern_pool.typeOf(decl_val).toType();
+    log.debug("lowerAnonDecl: ty = {}", .{decl_ty.fmt(mod)});
     const is_fn_body = decl_ty.zigTypeTag(mod) == .Fn;
     if (!is_fn_body and !decl_ty.hasRuntimeBits(mod)) {
         try code.appendNTimes(0xaa, ptr_width_bytes);
@@ -911,6 +912,14 @@ fn genDeclRef(
         _ = try sym.getOrCreateZigGotEntry(sym_index, elf_file);
         return GenResult.mcv(.{ .load_symbol = sym.esym_index });
     } else if (bin_file.cast(link.File.MachO)) |macho_file| {
+        if (is_extern) {
+            // TODO make this part of getGlobalSymbol
+            const name = mod.intern_pool.stringToSlice(decl.name);
+            const sym_name = try std.fmt.allocPrint(bin_file.allocator, "_{s}", .{name});
+            defer bin_file.allocator.free(sym_name);
+            const global_index = try macho_file.addUndefined(sym_name, .{ .add_got = true });
+            return GenResult.mcv(.{ .load_got = link.File.MachO.global_symbol_bit | global_index });
+        }
         const atom_index = try macho_file.getOrCreateAtomForDecl(decl_index);
         const sym_index = macho_file.getAtom(atom_index).getSymbolIndex().?;
         if (is_threadlocal) {