Commit 6a866f1a96

Jakub Konka <kubkon@jakubkonka.com>
2021-04-02 19:53:16
zld: preprocess relocs on arm64
1 parent 4e676ec
Changed files (3)
src/link/MachO/Object.zig
@@ -7,8 +7,10 @@ const io = std.io;
 const log = std.log.scoped(.object);
 const macho = std.macho;
 const mem = std.mem;
+const reloc = @import("reloc.zig");
 
 const Allocator = mem.Allocator;
+const Relocation = reloc.Relocation;
 const Symbol = @import("Symbol.zig");
 const parseName = @import("Zld.zig").parseName;
 
@@ -22,6 +24,7 @@ file_offset: ?u32 = null,
 name: ?[]u8 = null,
 
 load_commands: std.ArrayListUnmanaged(LoadCommand) = .{},
+sections: std.ArrayListUnmanaged(Section) = .{},
 
 segment_cmd_index: ?u16 = null,
 symtab_cmd_index: ?u16 = null,
@@ -42,6 +45,24 @@ strtab: std.ArrayListUnmanaged(u8) = .{},
 
 data_in_code_entries: std.ArrayListUnmanaged(macho.data_in_code_entry) = .{},
 
+const Section = struct {
+    inner: macho.section_64,
+    code: []u8,
+    relocs: ?[]*Relocation,
+    // TODO store object-to-exe-section mapping here
+
+    pub fn deinit(self: *Section, allocator: *Allocator) void {
+        allocator.free(self.code);
+
+        if (self.relocs) |relocs| {
+            for (relocs) |rel| {
+                allocator.destroy(rel);
+            }
+            allocator.free(relocs);
+        }
+    }
+};
+
 pub fn init(allocator: *Allocator) Object {
     return .{
         .allocator = allocator,
@@ -53,6 +74,12 @@ pub fn deinit(self: *Object) void {
         lc.deinit(self.allocator);
     }
     self.load_commands.deinit(self.allocator);
+
+    for (self.sections.items) |*sect| {
+        sect.deinit(self.allocator);
+    }
+    self.sections.deinit(self.allocator);
+
     self.symtab.deinit(self.allocator);
     self.strtab.deinit(self.allocator);
     self.data_in_code_entries.deinit(self.allocator);
@@ -95,15 +122,9 @@ pub fn parse(self: *Object) !void {
     }
 
     try self.readLoadCommands(reader);
+    try self.parseSections();
     if (self.symtab_cmd_index != null) try self.parseSymtab();
     if (self.data_in_code_cmd_index != null) try self.readDataInCode();
-
-    {
-        const seg = self.load_commands.items[self.segment_cmd_index.?].Segment;
-        for (seg.sections.items) |_, sect_id| {
-            try self.parseRelocs(@intCast(u16, sect_id));
-        }
-    }
 }
 
 pub fn readLoadCommands(self: *Object, reader: anytype) !void {
@@ -170,53 +191,37 @@ pub fn readLoadCommands(self: *Object, reader: anytype) !void {
     }
 }
 
-pub fn parseRelocs(self: *Object, sect_id: u16) !void {
+pub fn parseSections(self: *Object) !void {
     const seg = self.load_commands.items[self.segment_cmd_index.?].Segment;
-    const sect = seg.sections.items[sect_id];
-
-    if (sect.nreloc == 0) return;
-
-    var raw_relocs = try self.allocator.alloc(u8, @sizeOf(macho.relocation_info) * sect.nreloc);
-    defer self.allocator.free(raw_relocs);
-    _ = try self.file.?.preadAll(raw_relocs, sect.reloff);
-    const relocs = mem.bytesAsSlice(macho.relocation_info, raw_relocs);
-
-    for (relocs) |reloc| {
-        const is_addend = is_addend: {
-            switch (self.arch.?) {
-                .x86_64 => {
-                    const rel_type = @intToEnum(macho.reloc_type_x86_64, reloc.r_type);
-                    log.warn("{s}", .{rel_type});
-
-                    break :is_addend false;
-                },
-                .aarch64 => {
-                    const rel_type = @intToEnum(macho.reloc_type_arm64, reloc.r_type);
-                    log.warn("{s}", .{rel_type});
-
-                    break :is_addend rel_type == .ARM64_RELOC_ADDEND;
-                },
-                else => unreachable,
-            }
+
+    try self.sections.ensureCapacity(self.allocator, seg.sections.items.len);
+
+    for (seg.sections.items) |sect| {
+        // Read sections' code
+        var code = try self.allocator.alloc(u8, sect.size);
+        _ = try self.file.?.preadAll(code, sect.offset);
+
+        var section = Section{
+            .inner = sect,
+            .code = code,
+            .relocs = undefined,
         };
 
-        if (!is_addend) {
-            if (reloc.r_extern == 1) {
-                const sym = self.symtab.items[reloc.r_symbolnum];
-                const sym_name = self.getString(sym.inner.n_strx);
-                log.warn("    | symbol = {s}", .{sym_name});
-            } else {
-                const target_sect = seg.sections.items[reloc.r_symbolnum - 1];
-                log.warn("    | section = {s},{s}", .{
-                    parseName(&target_sect.segname),
-                    parseName(&target_sect.sectname),
-                });
-            }
-        }
+        // Parse relocations
+        var relocs: ?[]*Relocation = if (sect.nreloc > 0) relocs: {
+            var raw_relocs = try self.allocator.alloc(u8, @sizeOf(macho.relocation_info) * sect.nreloc);
+            defer self.allocator.free(raw_relocs);
+
+            _ = try self.file.?.preadAll(raw_relocs, sect.reloff);
+
+            break :relocs try reloc.parse(
+                self.allocator,
+                &section.code,
+                mem.bytesAsSlice(macho.relocation_info, raw_relocs),
+            );
+        } else null;
 
-        log.warn("    | offset = 0x{x}", .{reloc.r_address});
-        log.warn("    | PC = {}", .{reloc.r_pcrel == 1});
-        log.warn("    | length = {}", .{reloc.r_length});
+        self.sections.appendAssumeCapacity(section);
     }
 }
 
src/link/MachO/reloc.zig
@@ -0,0 +1,708 @@
+const std = @import("std");
+const aarch64 = @import("../../codegen/aarch64.zig");
+const assert = std.debug.assert;
+const log = std.log.scoped(.reloc);
+const macho = std.macho;
+const mem = std.mem;
+const meta = std.meta;
+
+const Allocator = mem.Allocator;
+
+pub const Relocation = struct {
+    @"type": Type,
+    code: *[]u8,
+    offset: u32,
+    target: Target,
+
+    pub fn cast(base: *Relocation, comptime T: type) ?*T {
+        if (base.@"type" != T.base_type)
+            return null;
+
+        return @fieldParentPtr(T, "base", base);
+    }
+
+    pub const Type = enum {
+        branch,
+        unsigned,
+        page,
+        page_off,
+        got_page,
+        got_page_off,
+        tlvp_page,
+        tlvp_page_off,
+    };
+
+    pub const Target = union(enum) {
+        symbol: u32,
+        section: u16,
+
+        pub fn from_reloc(reloc: macho.relocation_info) Target {
+            return if (reloc.r_extern == 1) .{
+                .symbol = reloc.r_symbolnum,
+            } else .{
+                .section = @intCast(u16, reloc.r_symbolnum - 1),
+            };
+        }
+    };
+
+    pub const Branch = struct {
+        base: Relocation,
+        /// Always .UnconditionalBranchImmediate
+        inst: aarch64.Instruction,
+
+        pub const base_type: Relocation.Type = .branch;
+
+        pub fn resolve(branch: Branch, source_addr: u64, target_addr: u64) !void {
+            const displacement = try math.cast(i28, @intCast(i64, target_addr) - @intCast(i64, source_addr));
+            var inst = branch.inst;
+            inst.AddSubtractImmediate.imm26 = @truncate(u26, @bitCast(u28, displacement) >> 2);
+            mem.writeIntLittle(u32, branch.base.code.*[branch.base.offset..], inst.toU32());
+        }
+    };
+
+    pub const Unsigned = struct {
+        base: Relocation,
+        subtractor: ?Target = null,
+        /// Addend embedded directly in the relocation slot
+        addend: i64,
+        /// Extracted from r_length:
+        /// => 3 implies true
+        /// => 2 implies false
+        /// => * is unreachable
+        is_64bit: bool,
+
+        pub const base_type: Relocation.Type = .unsigned;
+
+        pub fn resolve(unsigned: Unsigned, target_addr: u64, subtractor: i64) !void {
+            const result = @intCast(i64, target_addr) - subtractor + unsigned.addend;
+
+            log.debug("    | calculated addend 0x{x}", .{unsigned.addend});
+            log.debug("    | calculated unsigned value 0x{x}", .{result});
+
+            if (unsigned.is_64bit) {
+                mem.writeIntLittle(
+                    u64,
+                    unsigned.base.code.*[unsigned.base.offset..],
+                    @bitCast(u64, result),
+                );
+            } else {
+                mem.writeIntLittle(
+                    u32,
+                    unsigned.base.code.*[unsigned.base.offset..],
+                    @truncate(u32, @bitCast(u64, result)),
+                );
+            }
+        }
+    };
+
+    pub const Page = struct {
+        base: Relocation,
+        addend: ?u32 = null,
+        /// Always .PCRelativeAddress
+        inst: aarch64.Instruction,
+
+        pub const base_type: Relocation.Type = .page;
+
+        pub fn resolve(page: Page, source_addr: u64, target_addr: u64) !void {
+            const ta = if (page.addend) |a| target_addr + a else target_addr;
+            const source_page = @intCast(i32, source_addr >> 12);
+            const target_page = @intCast(i32, ta >> 12);
+            const pages = @bitCast(u21, @intCast(i21, target_page - source_page));
+
+            log.debug("    | moving by {} pages", .{pages});
+
+            var inst = page.inst;
+            inst.PCRelativeAddress.immhi = @truncate(u19, pages >> 2);
+            inst.PCRelativeAddress.immlo = @truncate(u2, pages);
+
+            mem.writeIntLittle(u32, page.base.code.*[page.base.offset..], inst.toU32());
+        }
+    };
+
+    pub const PageOff = struct {
+        base: Relocation,
+        addend: ?u32 = null,
+        op_kind: OpKind,
+        inst: aarch64.Instruction,
+
+        pub const base_type: Relocation.Type = .page_off;
+
+        pub const OpKind = enum {
+            arithmetic,
+            load_store,
+        };
+
+        pub fn resolve(page_off: PageOff, target_addr: u64) !void {
+            const ta = if (page_off.addend) |a| target_addr + a else target_addr;
+            const narrowed = @truncate(u12, ta);
+
+            log.debug("    | narrowed address within the page 0x{x}", .{narrowed});
+            log.debug("    | {s} opcode", .{page_off.op_kind});
+
+            var inst = page_off.inst;
+            if (page_off.op_kind == .arithmetic) {
+                inst.AddSubtractImmediate.imm12 = narrowed;
+            } else {
+                const offset: u12 = blk: {
+                    if (inst.LoadStoreRegister.size == 0) {
+                        if (inst.LoadStoreRegister.v == 1) {
+                            // 128-bit SIMD is scaled by 16.
+                            break :blk try math.divExact(u12, narrowed, 16);
+                        }
+                        // Otherwise, 8-bit SIMD or ldrb.
+                        break :blk narrowed;
+                    } else {
+                        const denom: u4 = try math.powi(u4, 2, inst.LoadStoreRegister.size);
+                        break :blk try math.divExact(u12, narrowed, denom);
+                    }
+                };
+                inst.LoadStoreRegister.offset = offset;
+            }
+
+            mem.writeIntLittle(u32, page_off.base.code.*[page_off.base.offset..], inst.toU32());
+        }
+    };
+
+    pub const GotPage = struct {
+        base: Relocation,
+        /// Always .PCRelativeAddress
+        inst: aarch64.Instruction,
+
+        pub const base_type: Relocation.Type = .got_page;
+
+        pub fn resolve(page: GotPage, source_addr: u64, target_addr: u64) !void {
+            const source_page = @intCast(i32, source_addr >> 12);
+            const target_page = @intCast(i32, target_addr >> 12);
+            const pages = @bitCast(u21, @intCast(i21, target_page - source_page));
+
+            log.debug("    | moving by {} pages", .{pages});
+
+            var inst = page.inst;
+            inst.PCRelativeAddress.immhi = @truncate(u19, pages >> 2);
+            inst.PCRelativeAddress.immlo = @truncate(u2, pages);
+
+            mem.writeIntLittle(u32, page.base.code.*[page.base.offset..], inst.toU32());
+        }
+    };
+
+    pub const GotPageOff = struct {
+        base: Relocation,
+        /// Always .LoadStoreRegister with size = 3 for GOT indirection
+        inst: aarch64.Instruction,
+
+        pub const base_type: Relocation.Type = .got_page_off;
+
+        pub fn resolve(page_off: GotPageOff, target_addr: u64) !void {
+            const narrowed = @truncate(u12, target_addr);
+
+            log.debug("    | narrowed address within the page 0x{x}", .{narrowed});
+
+            var inst = page_off.inst;
+            const offset = try math.divExact(u12, narrowed, 8);
+            inst.LoadStoreRegister.offset = offset;
+
+            mem.writeIntLittle(u32, page_off.base.code.*[page_off.base.offset..], inst.toU32());
+        }
+    };
+
+    pub const TlvpPage = struct {
+        base: Relocation,
+        /// Always .PCRelativeAddress
+        inst: aarch64.Instruction,
+
+        pub const base_type: Relocation.Type = .tlvp_page;
+
+        pub fn resolve(page: TlvpPage, source_addr: u64, target_addr: u64) !void {
+            const source_page = @intCast(i32, source_addr >> 12);
+            const target_page = @intCast(i32, target_addr >> 12);
+            const pages = @bitCast(u21, @intCast(i21, target_page - source_page));
+
+            log.debug("    | moving by {} pages", .{pages});
+
+            var inst = page.inst;
+            inst.PCRelativeAddress.immhi = @truncate(u19, pages >> 2);
+            inst.PCRelativeAddress.immlo = @truncate(u2, pages);
+
+            mem.writeIntLittle(u32, page.base.code.*[page.base.offset..], inst.toU32());
+        }
+    };
+
+    pub const TlvpPageOff = struct {
+        base: Relocation,
+        /// Always .AddSubtractImmediate regardless of the source instruction.
+        /// This means, we always rewrite the instruction to add even if the
+        /// source instruction was an ldr.
+        inst: aarch64.Instruction,
+
+        pub const base_type: Relocation.Type = .tlvp_page_off;
+
+        pub fn resolve(page_off: TlvpPageOff, target_addr: u64) !void {
+            const narrowed = @truncate(u12, target_addr);
+
+            log.debug("    | narrowed address within the page 0x{x}", .{narrowed});
+
+            var inst = page_off.inst;
+            inst.AddSubtractImmediate.imm12 = narrowed;
+
+            mem.writeIntLittle(u32, page_off.base.code.*[page_off.base.offset..], inst.toU32());
+        }
+    };
+};
+
+pub fn parse(allocator: *Allocator, code: *[]u8, relocs: []const macho.relocation_info) ![]*Relocation {
+    var it = RelocIterator{
+        .buffer = relocs,
+    };
+
+    var parser = Parser{
+        .allocator = allocator,
+        .it = &it,
+        .code = code,
+        .parsed = std.ArrayList(*Relocation).init(allocator),
+    };
+    defer parser.deinit();
+
+    return parser.parse();
+}
+
+const RelocIterator = struct {
+    buffer: []const macho.relocation_info,
+    index: i64 = -1,
+
+    pub fn next(self: *RelocIterator) ?macho.relocation_info {
+        self.index += 1;
+        if (self.index < self.buffer.len) {
+            const reloc = self.buffer[@intCast(u64, self.index)];
+            log.warn("{s}", .{@intToEnum(macho.reloc_type_arm64, reloc.r_type)});
+            log.warn("    | offset = {}", .{reloc.r_address});
+            log.warn("    | PC = {}", .{reloc.r_pcrel == 1});
+            log.warn("    | length = {}", .{reloc.r_length});
+            log.warn("    | symbolnum = {}", .{reloc.r_symbolnum});
+            log.warn("    | extern = {}", .{reloc.r_extern == 1});
+            return reloc;
+        }
+        return null;
+    }
+
+    pub fn peek(self: *RelocIterator) ?macho.reloc_type_arm64 {
+        if (self.index + 1 < self.buffer.len) {
+            const reloc = self.buffer[@intCast(u64, self.index + 1)];
+            const tt = @intToEnum(macho.reloc_type_arm64, reloc.r_type);
+            return tt;
+        }
+        return null;
+    }
+};
+
+const Parser = struct {
+    allocator: *Allocator,
+    it: *RelocIterator,
+    code: *[]u8,
+    parsed: std.ArrayList(*Relocation),
+    addend: ?u32 = null,
+    subtractor: ?Relocation.Target = null,
+
+    fn deinit(parser: *Parser) void {
+        parser.parsed.deinit();
+    }
+
+    fn parse(parser: *Parser) ![]*Relocation {
+        while (parser.it.next()) |reloc| {
+            switch (@intToEnum(macho.reloc_type_arm64, reloc.r_type)) {
+                .ARM64_RELOC_BRANCH26 => {
+                    try parser.parseBranch(reloc);
+                },
+                .ARM64_RELOC_SUBTRACTOR => {
+                    try parser.parseSubtractor(reloc);
+                },
+                .ARM64_RELOC_UNSIGNED => {
+                    try parser.parseUnsigned(reloc);
+                },
+                .ARM64_RELOC_ADDEND => {
+                    try parser.parseAddend(reloc);
+                },
+                .ARM64_RELOC_PAGE21,
+                .ARM64_RELOC_GOT_LOAD_PAGE21,
+                .ARM64_RELOC_TLVP_LOAD_PAGE21,
+                => {
+                    try parser.parsePage(reloc);
+                },
+                .ARM64_RELOC_PAGEOFF12 => {
+                    try parser.parsePageOff(reloc);
+                },
+                .ARM64_RELOC_GOT_LOAD_PAGEOFF12 => {
+                    try parser.parseGotLoadPageOff(reloc);
+                },
+                .ARM64_RELOC_TLVP_LOAD_PAGEOFF12 => {
+                    try parser.parseTlvpLoadPageOff(reloc);
+                },
+                .ARM64_RELOC_POINTER_TO_GOT => {
+                    return error.ToDoRelocPointerToGot;
+                },
+            }
+        }
+
+        return parser.parsed.toOwnedSlice();
+    }
+
+    fn parseAddend(parser: *Parser, reloc: macho.relocation_info) !void {
+        const reloc_type = @intToEnum(macho.reloc_type_arm64, reloc.r_type);
+        assert(reloc_type == .ARM64_RELOC_ADDEND);
+        assert(reloc.r_pcrel == 0);
+        assert(reloc.r_extern == 0);
+        assert(parser.addend == null);
+
+        parser.addend = reloc.r_symbolnum;
+
+        // Verify ADDEND is followed by a load.
+        if (parser.it.peek()) |tt| {
+            switch (tt) {
+                .ARM64_RELOC_PAGE21, .ARM64_RELOC_PAGEOFF12 => {},
+                else => |other| {
+                    log.err("unexpected relocation type: expected PAGE21 or PAGEOFF12, found {s}", .{other});
+                    return error.UnexpectedRelocationType;
+                },
+            }
+        } else {
+            log.err("unexpected end of stream", .{});
+            return error.UnexpectedEndOfStream;
+        }
+    }
+
+    fn parseBranch(parser: *Parser, reloc: macho.relocation_info) !void {
+        const reloc_type = @intToEnum(macho.reloc_type_arm64, reloc.r_type);
+        assert(reloc_type == .ARM64_RELOC_BRANCH26);
+        assert(reloc.r_pcrel == 1);
+        assert(reloc.r_length == 2);
+
+        const offset = @intCast(u32, reloc.r_address);
+        const inst = parser.code.*[offset..][0..4];
+        const parsed_inst = aarch64.Instruction{ .UnconditionalBranchImmediate = mem.bytesToValue(
+            meta.TagPayload(
+                aarch64.Instruction,
+                aarch64.Instruction.UnconditionalBranchImmediate,
+            ),
+            inst,
+        ) };
+
+        var branch = try parser.allocator.create(Relocation.Branch);
+        errdefer parser.allocator.destroy(branch);
+
+        const target = Relocation.Target.from_reloc(reloc);
+
+        branch.* = .{
+            .base = .{
+                .@"type" = .branch,
+                .code = parser.code,
+                .offset = @intCast(u32, reloc.r_address),
+                .target = target,
+            },
+            .inst = parsed_inst,
+        };
+
+        log.warn("    | emitting {}", .{branch});
+        try parser.parsed.append(&branch.base);
+    }
+
+    fn parsePage(parser: *Parser, reloc: macho.relocation_info) !void {
+        assert(reloc.r_pcrel == 1);
+        assert(reloc.r_length == 2);
+
+        const reloc_type = @intToEnum(macho.reloc_type_arm64, reloc.r_type);
+        const target = Relocation.Target.from_reloc(reloc);
+
+        const offset = @intCast(u32, reloc.r_address);
+        const inst = parser.code.*[offset..][0..4];
+        const parsed_inst = aarch64.Instruction{ .PCRelativeAddress = mem.bytesToValue(meta.TagPayload(
+            aarch64.Instruction,
+            aarch64.Instruction.PCRelativeAddress,
+        ), inst) };
+
+        const ptr: *Relocation = ptr: {
+            switch (reloc_type) {
+                .ARM64_RELOC_PAGE21 => {
+                    defer {
+                        // Reset parser's addend state
+                        parser.addend = null;
+                    }
+                    var page = try parser.allocator.create(Relocation.Page);
+                    errdefer parser.allocator.destroy(page);
+
+                    page.* = .{
+                        .base = .{
+                            .@"type" = .page,
+                            .code = parser.code,
+                            .offset = offset,
+                            .target = target,
+                        },
+                        .addend = parser.addend,
+                        .inst = parsed_inst,
+                    };
+
+                    log.warn("    | emitting {}", .{page});
+
+                    break :ptr &page.base;
+                },
+                .ARM64_RELOC_GOT_LOAD_PAGE21 => {
+                    var page = try parser.allocator.create(Relocation.GotPage);
+                    errdefer parser.allocator.destroy(page);
+
+                    page.* = .{
+                        .base = .{
+                            .@"type" = .got_page,
+                            .code = parser.code,
+                            .offset = offset,
+                            .target = target,
+                        },
+                        .inst = parsed_inst,
+                    };
+
+                    log.warn("    | emitting {}", .{page});
+
+                    break :ptr &page.base;
+                },
+                .ARM64_RELOC_TLVP_LOAD_PAGE21 => {
+                    var page = try parser.allocator.create(Relocation.TlvpPage);
+                    errdefer parser.allocator.destroy(page);
+
+                    page.* = .{
+                        .base = .{
+                            .@"type" = .tlvp_page,
+                            .code = parser.code,
+                            .offset = offset,
+                            .target = target,
+                        },
+                        .inst = parsed_inst,
+                    };
+
+                    log.warn("    | emitting {}", .{page});
+
+                    break :ptr &page.base;
+                },
+                else => unreachable,
+            }
+        };
+
+        try parser.parsed.append(ptr);
+    }
+
+    fn parsePageOff(parser: *Parser, reloc: macho.relocation_info) !void {
+        defer {
+            // Reset parser's addend state
+            parser.addend = null;
+        }
+
+        const reloc_type = @intToEnum(macho.reloc_type_arm64, reloc.r_type);
+        assert(reloc_type == .ARM64_RELOC_PAGEOFF12);
+        assert(reloc.r_pcrel == 0);
+        assert(reloc.r_length == 2);
+
+        const offset = @intCast(u32, reloc.r_address);
+        const inst = parser.code.*[offset..][0..4];
+
+        var op_kind: Relocation.PageOff.OpKind = undefined;
+        var parsed_inst: aarch64.Instruction = undefined;
+        if (isArithmeticOp(inst)) {
+            op_kind = .arithmetic;
+            parsed_inst = .{ .AddSubtractImmediate = mem.bytesToValue(meta.TagPayload(
+                aarch64.Instruction,
+                aarch64.Instruction.AddSubtractImmediate,
+            ), inst) };
+        } else {
+            op_kind = .load_store;
+            parsed_inst = .{ .LoadStoreRegister = mem.bytesToValue(meta.TagPayload(
+                aarch64.Instruction,
+                aarch64.Instruction.LoadStoreRegister,
+            ), inst) };
+        }
+        const target = Relocation.Target.from_reloc(reloc);
+
+        var page_off = try parser.allocator.create(Relocation.PageOff);
+        errdefer parser.allocator.destroy(page_off);
+
+        page_off.* = .{
+            .base = .{
+                .@"type" = .page_off,
+                .code = parser.code,
+                .offset = offset,
+                .target = target,
+            },
+            .op_kind = op_kind,
+            .inst = parsed_inst,
+            .addend = parser.addend,
+        };
+
+        log.warn("    | emitting {}", .{page_off});
+        try parser.parsed.append(&page_off.base);
+    }
+
+    fn parseGotLoadPageOff(parser: *Parser, reloc: macho.relocation_info) !void {
+        const reloc_type = @intToEnum(macho.reloc_type_arm64, reloc.r_type);
+        assert(reloc_type == .ARM64_RELOC_GOT_LOAD_PAGEOFF12);
+        assert(reloc.r_pcrel == 0);
+        assert(reloc.r_length == 2);
+
+        const offset = @intCast(u32, reloc.r_address);
+        const inst = parser.code.*[offset..][0..4];
+        assert(!isArithmeticOp(inst));
+
+        const parsed_inst = mem.bytesToValue(meta.TagPayload(
+            aarch64.Instruction,
+            aarch64.Instruction.LoadStoreRegister,
+        ), inst);
+        assert(parsed_inst.size == 3);
+
+        const target = Relocation.Target.from_reloc(reloc);
+
+        var page_off = try parser.allocator.create(Relocation.GotPageOff);
+        errdefer parser.allocator.destroy(page_off);
+
+        page_off.* = .{
+            .base = .{
+                .@"type" = .got_page_off,
+                .code = parser.code,
+                .offset = offset,
+                .target = target,
+            },
+            .inst = .{
+                .LoadStoreRegister = parsed_inst,
+            },
+        };
+
+        log.warn("    | emitting {}", .{page_off});
+        try parser.parsed.append(&page_off.base);
+    }
+
+    fn parseTlvpLoadPageOff(parser: *Parser, reloc: macho.relocation_info) !void {
+        const reloc_type = @intToEnum(macho.reloc_type_arm64, reloc.r_type);
+        assert(reloc_type == .ARM64_RELOC_TLVP_LOAD_PAGEOFF12);
+        assert(reloc.r_pcrel == 0);
+        assert(reloc.r_length == 2);
+
+        const RegInfo = struct {
+            rd: u5,
+            rn: u5,
+            size: u1,
+        };
+
+        const offset = @intCast(u32, reloc.r_address);
+        const inst = parser.code.*[offset..][0..4];
+        const parsed: RegInfo = parsed: {
+            if (isArithmeticOp(inst)) {
+                const parsed_inst = mem.bytesAsValue(meta.TagPayload(
+                    aarch64.Instruction,
+                    aarch64.Instruction.AddSubtractImmediate,
+                ), inst);
+                break :parsed .{
+                    .rd = parsed_inst.rd,
+                    .rn = parsed_inst.rn,
+                    .size = parsed_inst.sf,
+                };
+            } else {
+                const parsed_inst = mem.bytesAsValue(meta.TagPayload(
+                    aarch64.Instruction,
+                    aarch64.Instruction.LoadStoreRegister,
+                ), inst);
+                break :parsed .{
+                    .rd = parsed_inst.rt,
+                    .rn = parsed_inst.rn,
+                    .size = @truncate(u1, parsed_inst.size),
+                };
+            }
+        };
+
+        const target = Relocation.Target.from_reloc(reloc);
+
+        var page_off = try parser.allocator.create(Relocation.TlvpPageOff);
+        errdefer parser.allocator.destroy(page_off);
+
+        page_off.* = .{
+            .base = .{
+                .@"type" = .tlvp_page_off,
+                .code = parser.code,
+                .offset = @intCast(u32, reloc.r_address),
+                .target = target,
+            },
+            .inst = .{
+                .AddSubtractImmediate = .{
+                    .rd = parsed.rd,
+                    .rn = parsed.rn,
+                    .imm12 = 0, // This will be filled when target addresses are known.
+                    .sh = 0,
+                    .s = 0,
+                    .op = 0,
+                    .sf = parsed.size,
+                },
+            },
+        };
+
+        log.warn("    | emitting {}", .{page_off});
+        try parser.parsed.append(&page_off.base);
+    }
+
+    fn parseSubtractor(parser: *Parser, reloc: macho.relocation_info) !void {
+        const reloc_type = @intToEnum(macho.reloc_type_arm64, reloc.r_type);
+        assert(reloc_type == .ARM64_RELOC_SUBTRACTOR);
+        assert(reloc.r_pcrel == 0);
+        assert(parser.subtractor == null);
+
+        parser.subtractor = Relocation.Target.from_reloc(reloc);
+
+        // Verify SUBTRACTOR is followed by UNSIGNED.
+        if (parser.it.peek()) |tt| {
+            if (tt != .ARM64_RELOC_UNSIGNED) {
+                log.err("unexpected relocation type: expected UNSIGNED, found {s}", .{tt});
+                return error.UnexpectedRelocationType;
+            }
+        } else {
+            log.err("unexpected end of stream", .{});
+            return error.UnexpectedEndOfStream;
+        }
+    }
+
+    fn parseUnsigned(parser: *Parser, reloc: macho.relocation_info) !void {
+        defer {
+            // Reset parser's subtractor state
+            parser.subtractor = null;
+        }
+
+        const reloc_type = @intToEnum(macho.reloc_type_arm64, reloc.r_type);
+        assert(reloc_type == .ARM64_RELOC_UNSIGNED);
+        assert(reloc.r_pcrel == 0);
+
+        var unsigned = try parser.allocator.create(Relocation.Unsigned);
+        errdefer parser.allocator.destroy(unsigned);
+
+        const target = Relocation.Target.from_reloc(reloc);
+        const is_64bit: bool = switch (reloc.r_length) {
+            3 => true,
+            2 => false,
+            else => unreachable,
+        };
+        const offset = @intCast(u32, reloc.r_address);
+        const addend: i64 = if (is_64bit)
+            mem.readIntLittle(i64, parser.code.*[offset..][0..8])
+        else
+            mem.readIntLittle(i32, parser.code.*[offset..][0..4]);
+
+        unsigned.* = .{
+            .base = .{
+                .@"type" = .unsigned,
+                .code = parser.code,
+                .offset = offset,
+                .target = target,
+            },
+            .subtractor = parser.subtractor,
+            .is_64bit = is_64bit,
+            .addend = addend,
+        };
+
+        log.warn("    | emitting {}", .{unsigned});
+        try parser.parsed.append(&unsigned.base);
+    }
+};
+
+fn isArithmeticOp(inst: *const [4]u8) callconv(.Inline) bool {
+    const group_decode = @truncate(u5, inst[3]);
+    return ((group_decode >> 2) == 4);
+}
CMakeLists.txt
@@ -574,6 +574,7 @@ set(ZIG_STAGE2_SOURCES
     "${CMAKE_SOURCE_DIR}/src/link/MachO/Zld.zig"
     "${CMAKE_SOURCE_DIR}/src/link/MachO/bind.zig"
     "${CMAKE_SOURCE_DIR}/src/link/MachO/commands.zig"
+    "${CMAKE_SOURCE_DIR}/src/link/MachO/reloc.zig"
     "${CMAKE_SOURCE_DIR}/src/link/Wasm.zig"
     "${CMAKE_SOURCE_DIR}/src/link/C/zig.h"
     "${CMAKE_SOURCE_DIR}/src/link/msdos-stub.bin"