Files
zig/lib/std/debug/MachOFile.zig
Corentin Kerisit fafbc70ac5 std.debug.MachOFile: support loading DWARF from adjacent dSYM (#35582)
Fixes #31797

I finally had time to work on this.

I've implemented it so that, when an adjacent dSYM exist and its uuid match that of the current binary, we load its DWARF and use that for all getDwarfForAddress requests.
All of this is done eagerly at `MachOFile.load` for simplicity rather than later and lazier. Happy to change that to whatever you think is best.

This implementation is quite minimal and makes dSYM support "best-effort" as in, it treats dSYM parsing error as non-fatal and fallbacks using the current stabs/ofiles mechanism.

You can test it that way on macOS:

```
zig build-exe panic.zig
dsymutil panic -o panic.dSYM
strip -S panic
./panic
```

with panic.zig:
```
pub fn main() void {
    @panic("panic");
}
```

Before:
```
thread 57569166 panic: panic
???:?:?: 0x103071c2b in _panic.main (/private/tmp/panic)
???:?:?: 0x103071bb3 in _main (/private/tmp/panic)
???:?:?: 0x180d5ab97 in start (/usr/lib/dyld)
fish: Job 1, './panic' terminated by signal SIGABRT (Abort)
```

After:
```
thread 57569653 panic: panic
/private/tmp/panic.zig:2:5: 0x1029d771b in main (panic)
    @panic("panic");
    ^
/Users/cerisier/code/codeberg.org/ziglang/zig/lib/std/start.zig:698:59: 0x1029d76a3 in callMain (panic)
    if (fn_info.params.len == 0) return wrapMain(root.main());
                                                          ^
???:?:?: 0x180d5ab97 in start (/usr/lib/dyld)
fish: Job 1, './panic' terminated by signal SIGABRT (Abort)
```

Reviewed-on: https://codeberg.org/ziglang/zig/pulls/35582
Reviewed-by: mlugg <mlugg@noreply.codeberg.org>
2026-08-07 16:14:05 +02:00

707 lines
27 KiB
Zig

mapped_memory: []align(std.heap.page_size_min) const u8,
symbols: []const Symbol,
strings: []const u8,
text_vmaddr: u64,
uuid: ?Uuid,
adjacent_dsym: ?DsymFile,
/// Key is index into `strings` of the file path.
ofiles: std.array_hash_map.Auto(u32, Error!OFile),
pub const Error = error{
InvalidMachO,
InvalidDwarf,
MissingDebugInfo,
UnsupportedDebugInfo,
ReadFailed,
OutOfMemory,
};
pub fn deinit(mf: *MachOFile, gpa: Allocator) void {
if (mf.adjacent_dsym) |*dsym| dsym.deinit(gpa);
for (mf.ofiles.values()) |*maybe_of| {
const of = &(maybe_of.* catch continue);
posix.munmap(of.mapped_memory);
of.dwarf.deinit(gpa);
of.symbols_by_name.deinit(gpa);
}
mf.ofiles.deinit(gpa);
gpa.free(mf.symbols);
posix.munmap(mf.mapped_memory);
}
pub fn load(gpa: Allocator, io: Io, path: []const u8, arch: std.Target.Cpu.Arch) Error!MachOFile {
switch (arch) {
.x86_64, .aarch64 => {},
else => unreachable,
}
const all_mapped_memory = try mapDebugInfoFile(io, path);
errdefer posix.munmap(all_mapped_memory);
const mapped_macho = try selectMachOSlice(all_mapped_memory, arch);
var r: Io.Reader = .fixed(mapped_macho);
const hdr = r.takeStruct(macho.mach_header_64, .little) catch |err| switch (err) {
error.ReadFailed => unreachable,
error.EndOfStream => return error.InvalidMachO,
};
if (hdr.magic != macho.MH_MAGIC_64)
return error.InvalidMachO;
const symtab: macho.symtab_command, const text_vmaddr: u64, const uuid: ?Uuid = lcs: {
var it: macho.LoadCommandIterator = try .init(&hdr, mapped_macho[@sizeOf(macho.mach_header_64)..]);
var symtab: ?macho.symtab_command = null;
var text_vmaddr: ?u64 = null;
var uuid: ?Uuid = null;
while (try it.next()) |cmd| switch (cmd.hdr.cmd) {
.SYMTAB => symtab = cmd.cast(macho.symtab_command) orelse return error.InvalidMachO,
.SEGMENT_64 => if (cmd.cast(macho.segment_command_64)) |seg_cmd| {
if (!mem.eql(u8, seg_cmd.segName(), "__TEXT")) continue;
text_vmaddr = seg_cmd.vmaddr;
},
.UUID => if (cmd.cast(macho.uuid_command)) |uuid_cmd| {
uuid = uuid_cmd.uuid;
},
else => {},
};
break :lcs .{
symtab orelse return error.MissingDebugInfo,
text_vmaddr orelse return error.MissingDebugInfo,
uuid,
};
};
const strings = mapped_macho[symtab.stroff..][0 .. symtab.strsize - 1];
var symbols: std.ArrayList(Symbol) = try .initCapacity(gpa, symtab.nsyms);
defer symbols.deinit(gpa);
// This map is temporary; it is used only to detect duplicates here. This is
// necessary because we prefer to use STAB ("symbolic debugging table") symbols,
// but they might not be present, so we track normal symbols too.
// Indices match 1-1 with those of `symbols`.
var symbol_names: std.array_hash_map.String(void) = .empty;
defer symbol_names.deinit(gpa);
try symbol_names.ensureUnusedCapacity(gpa, symtab.nsyms);
var ofile: u32 = undefined;
var last_sym: Symbol = undefined;
var state: enum {
init,
oso_open,
oso_close,
bnsym,
fun_strx,
fun_size,
ensym,
} = .init;
var sym_r: Io.Reader = .fixed(mapped_macho[symtab.symoff..]);
for (0..symtab.nsyms) |_| {
const sym = sym_r.takeStruct(macho.nlist_64, .little) catch |err| switch (err) {
error.ReadFailed => unreachable,
error.EndOfStream => return error.InvalidMachO,
};
if (sym.n_type.bits.is_stab == 0) {
if (sym.n_strx == 0) continue;
switch (sym.n_type.bits.type) {
.undf, .pbud, .indr, .abs, _ => continue,
.sect => {
const name = std.mem.sliceTo(strings[sym.n_strx..], 0);
const gop = symbol_names.getOrPutAssumeCapacity(name);
if (!gop.found_existing) {
assert(gop.index == symbols.items.len);
symbols.appendAssumeCapacity(.{
.strx = sym.n_strx,
.addr = sym.n_value,
.ofile = Symbol.unknown_ofile,
});
}
},
}
continue;
}
// TODO handle globals N_GSYM, and statics N_STSYM
//
// NOTE: ld64.lld and Apple's ld differ in STABS layout.
// Apple's ld emit N_BNSYM and N_ENSYM to mark the start and end of
// functions, while ld64.lld doesn't.
switch (sym.n_type.stab) {
.oso => switch (state) {
.init, .oso_close => {
state = .oso_open;
ofile = sym.n_strx;
},
else => return error.InvalidMachO,
},
.bnsym => switch (state) {
.oso_open, .ensym => {
state = .bnsym;
last_sym = .{
.strx = 0,
.addr = sym.n_value,
.ofile = ofile,
};
},
else => return error.InvalidMachO,
},
.fun => switch (state) {
.oso_open => {
state = .fun_strx;
last_sym = .{
.strx = sym.n_strx,
.addr = sym.n_value,
.ofile = ofile,
};
},
.bnsym => {
state = .fun_strx;
last_sym.strx = sym.n_strx;
},
.fun_strx => {
state = .fun_size;
},
.fun_size => {
if (last_sym.strx != 0) {
appendStabSymbol(&symbols, &symbol_names, strings, last_sym);
}
last_sym = .{
.strx = sym.n_strx,
.addr = sym.n_value,
.ofile = ofile,
};
state = .fun_strx;
},
else => return error.InvalidMachO,
},
.ensym => switch (state) {
.fun_size => {
state = .ensym;
if (last_sym.strx != 0) {
appendStabSymbol(&symbols, &symbol_names, strings, last_sym);
}
},
else => return error.InvalidMachO,
},
.so => switch (state) {
.init, .oso_close => {},
.oso_open, .ensym => {
state = .oso_close;
},
.fun_size => {
state = .oso_close;
if (last_sym.strx != 0) {
appendStabSymbol(&symbols, &symbol_names, strings, last_sym);
}
},
else => return error.InvalidMachO,
},
else => {},
}
}
switch (state) {
.init => {
// Missing STAB symtab entries is still okay, unless there were also no normal symbols.
if (symbols.items.len == 0) return error.MissingDebugInfo;
},
.oso_close => {},
else => return error.InvalidMachO, // corrupted STAB entries in symtab
}
const symbols_slice = try symbols.toOwnedSlice(gpa);
errdefer gpa.free(symbols_slice);
// Even though lld emits symbols in ascending order, this debug code
// should work for programs linked in any valid way.
// This sort is so that we can binary search later.
mem.sort(Symbol, symbols_slice, {}, Symbol.addressLessThan);
const adjacent_dsym = if (uuid) |expected_uuid|
try loadAdjacentDsym(gpa, io, path, arch, expected_uuid)
else
null;
return .{
.mapped_memory = all_mapped_memory,
.symbols = symbols_slice,
.strings = strings,
.ofiles = .empty,
.text_vmaddr = text_vmaddr,
.uuid = uuid,
.adjacent_dsym = adjacent_dsym,
};
}
pub fn getDwarfForAddress(mf: *MachOFile, gpa: Allocator, io: Io, vaddr: u64) !struct { *Dwarf, u64 } {
if (mf.adjacent_dsym) |*dsym| {
return .{ &dsym.dwarf, vaddr };
}
const symbol = Symbol.find(mf.symbols, vaddr) orelse return error.MissingDebugInfo;
if (symbol.ofile == Symbol.unknown_ofile) return error.MissingDebugInfo;
// offset of `address` from start of `symbol`
const address_symbol_offset = vaddr - symbol.addr;
// Take the symbol name from the N_FUN STAB entry, we're going to
// use it if we fail to find the DWARF infos
const stab_symbol = mem.sliceTo(mf.strings[symbol.strx..], 0);
const gop = try mf.ofiles.getOrPut(gpa, symbol.ofile);
if (!gop.found_existing) {
const name = mem.sliceTo(mf.strings[symbol.ofile..], 0);
gop.value_ptr.* = loadOFile(gpa, io, name);
}
const of = &(gop.value_ptr.* catch |err| return err);
const symbol_index = of.symbols_by_name.getKeyAdapted(
@as([]const u8, stab_symbol),
@as(OFile.SymbolAdapter, .{ .strtab = of.strtab, .symtab_raw = of.symtab_raw }),
) orelse return error.MissingDebugInfo;
const symbol_ofile_vaddr = vaddr: {
var sym = of.symtab_raw[symbol_index];
if (builtin.cpu.arch.endian() != .little) std.mem.byteSwapAllFields(macho.nlist_64, &sym);
break :vaddr sym.n_value;
};
return .{ &of.dwarf, symbol_ofile_vaddr + address_symbol_offset };
}
pub fn lookupSymbolName(mf: *MachOFile, vaddr: u64) error{MissingDebugInfo}![]const u8 {
const symbol = Symbol.find(mf.symbols, vaddr) orelse return error.MissingDebugInfo;
return mem.sliceTo(mf.strings[symbol.strx..], 0);
}
const OFile = struct {
mapped_memory: []align(std.heap.page_size_min) const u8,
dwarf: Dwarf,
strtab: []const u8,
symtab_raw: []align(1) const macho.nlist_64,
/// All named symbols in `symtab_raw`. Stored `u32` key is the index into `symtab_raw`. Accessed
/// through `SymbolAdapter`, so that the symbol name is used as the logical key.
symbols_by_name: std.array_hash_map.Custom(u32, void, void, true),
const SymbolAdapter = struct {
strtab: []const u8,
symtab_raw: []align(1) const macho.nlist_64,
pub fn hash(ctx: SymbolAdapter, sym_name: []const u8) u32 {
_ = ctx;
return @truncate(std.hash.Wyhash.hash(0, sym_name));
}
pub fn eql(ctx: SymbolAdapter, a_sym_name: []const u8, b_sym_index: u32, b_index: usize) bool {
_ = b_index;
var b_sym = ctx.symtab_raw[b_sym_index];
if (builtin.cpu.arch.endian() != .little) std.mem.byteSwapAllFields(macho.nlist_64, &b_sym);
const b_sym_name = std.mem.sliceTo(ctx.strtab[b_sym.n_strx..], 0);
return mem.eql(u8, a_sym_name, b_sym_name);
}
};
};
const DsymFile = struct {
mapped_memory: []align(std.heap.page_size_min) const u8,
dwarf: Dwarf,
fn deinit(df: *DsymFile, gpa: Allocator) void {
df.dwarf.deinit(gpa);
posix.munmap(df.mapped_memory);
}
};
const Symbol = struct {
strx: u32,
addr: u64,
/// Value may be `unknown_ofile`.
ofile: u32,
const unknown_ofile = std.math.maxInt(u32);
fn addressLessThan(context: void, lhs: Symbol, rhs: Symbol) bool {
_ = context;
return lhs.addr < rhs.addr;
}
/// Assumes that `symbols` is sorted in order of ascending `addr`.
fn find(symbols: []const Symbol, address: usize) ?*const Symbol {
if (symbols.len == 0) return null; // no potential match
if (address < symbols[0].addr) return null; // address is before the lowest-address symbol
var left: usize = 0;
var len: usize = symbols.len;
while (len > 1) {
const mid = left + len / 2;
if (address < symbols[mid].addr) {
len /= 2;
} else {
left = mid;
len -= len / 2;
}
}
return &symbols[left];
}
test find {
const symbols: []const Symbol = &.{
.{ .addr = 100, .strx = undefined, .ofile = undefined },
.{ .addr = 200, .strx = undefined, .ofile = undefined },
.{ .addr = 300, .strx = undefined, .ofile = undefined },
};
try testing.expectEqual(null, find(symbols, 0));
try testing.expectEqual(null, find(symbols, 99));
try testing.expectEqual(&symbols[0], find(symbols, 100).?);
try testing.expectEqual(&symbols[0], find(symbols, 150).?);
try testing.expectEqual(&symbols[0], find(symbols, 199).?);
try testing.expectEqual(&symbols[1], find(symbols, 200).?);
try testing.expectEqual(&symbols[1], find(symbols, 250).?);
try testing.expectEqual(&symbols[1], find(symbols, 299).?);
try testing.expectEqual(&symbols[2], find(symbols, 300).?);
try testing.expectEqual(&symbols[2], find(symbols, 301).?);
try testing.expectEqual(&symbols[2], find(symbols, 5000).?);
}
};
test {
_ = Symbol;
}
fn appendStabSymbol(
symbols: *std.ArrayList(Symbol),
symbol_names: *std.array_hash_map.String(void),
strings: []const u8,
last_sym: Symbol,
) void {
const name = std.mem.sliceTo(strings[last_sym.strx..], 0);
const gop = symbol_names.getOrPutAssumeCapacity(name);
if (!gop.found_existing) {
assert(gop.index == symbols.items.len);
symbols.appendAssumeCapacity(last_sym);
} else {
symbols.items[gop.index] = last_sym;
}
}
fn loadAdjacentDsym(
gpa: Allocator,
io: Io,
binary_path: []const u8,
arch: std.Target.Cpu.Arch,
uuid: Uuid,
) Error!?DsymFile {
const s = std.fs.path.sep_str;
const dsym_path = try std.fmt.allocPrint(
gpa,
"{s}.dSYM" ++ s ++ "Contents" ++ s ++ "Resources" ++ s ++ "DWARF" ++ s ++ "{s}",
.{ binary_path, std.fs.path.basename(binary_path) },
);
defer gpa.free(dsym_path);
return loadDsymFile(gpa, io, dsym_path, arch, uuid) catch |err| switch (err) {
error.MissingDebugInfo,
error.InvalidMachO,
error.InvalidDwarf,
error.UnsupportedDebugInfo,
error.ReadFailed,
=> null,
error.OutOfMemory => |e| return e,
};
}
fn loadDsymFile(
gpa: Allocator,
io: Io,
path: []const u8,
arch: std.Target.Cpu.Arch,
expected_uuid: Uuid,
) Error!DsymFile {
const all_mapped_memory = try mapDebugInfoFile(io, path);
errdefer posix.munmap(all_mapped_memory);
const mapped_macho = try selectMachOSlice(all_mapped_memory, arch);
var r: Io.Reader = .fixed(mapped_macho);
const hdr = r.takeStruct(macho.mach_header_64, .little) catch |err| switch (err) {
error.ReadFailed => unreachable,
error.EndOfStream => return error.InvalidMachO,
};
if (hdr.magic != macho.MH_MAGIC_64) return error.InvalidMachO;
if (hdr.filetype != macho.MH_DSYM) return error.MissingDebugInfo;
var uuid: ?Uuid = null;
var dwarf_sections: ?[]align(1) const macho.section_64 = null;
var it: macho.LoadCommandIterator = try .init(&hdr, mapped_macho[@sizeOf(macho.mach_header_64)..]);
while (try it.next()) |lc| switch (lc.hdr.cmd) {
.SEGMENT_64 => if (lc.cast(macho.segment_command_64)) |seg_cmd| {
if (!mem.eql(u8, "__DWARF", seg_cmd.segName())) continue;
dwarf_sections = lc.getSections();
},
.UUID => if (lc.cast(macho.uuid_command)) |uuid_cmd| {
uuid = uuid_cmd.uuid;
},
else => {},
};
const actual_uuid = uuid orelse return error.MissingDebugInfo;
if (!mem.eql(u8, &actual_uuid, &expected_uuid)) return error.MissingDebugInfo;
return .{
.mapped_memory = all_mapped_memory,
.dwarf = try loadDwarfFromSections(gpa, mapped_macho, dwarf_sections orelse return error.MissingDebugInfo),
};
}
fn loadOFile(gpa: Allocator, io: Io, o_file_name: []const u8) !OFile {
const all_mapped_memory, const mapped_ofile = map: {
const open_paren = paren: {
if (std.mem.endsWith(u8, o_file_name, ")")) {
if (std.mem.findScalarLast(u8, o_file_name, '(')) |i| {
break :paren i;
}
}
// Not an archive, just a normal path to a .o file
const m = try mapDebugInfoFile(io, o_file_name);
break :map .{ m, m };
};
// We have the form 'path/to/archive.a(entry.o)'. Map the archive and find the object file in question.
const archive_path = o_file_name[0..open_paren];
const target_name_in_archive = o_file_name[open_paren + 1 .. o_file_name.len - 1];
const mapped_archive = try mapDebugInfoFile(io, archive_path);
errdefer posix.munmap(mapped_archive);
var ar_reader: Io.Reader = .fixed(mapped_archive);
const ar_magic = ar_reader.take(8) catch return error.InvalidMachO;
if (!std.mem.eql(u8, ar_magic, "!<arch>\n")) return error.InvalidMachO;
while (true) {
if (ar_reader.seek == ar_reader.buffer.len) return error.MissingDebugInfo;
const raw_name = ar_reader.takeArray(16) catch return error.InvalidMachO;
ar_reader.discardAll(12 + 6 + 6 + 8) catch return error.InvalidMachO;
const raw_size = ar_reader.takeArray(10) catch return error.InvalidMachO;
const file_magic = ar_reader.takeArray(2) catch return error.InvalidMachO;
if (!std.mem.eql(u8, file_magic, "`\n")) return error.InvalidMachO;
const size = std.fmt.parseInt(u32, mem.sliceTo(raw_size, ' '), 10) catch return error.InvalidMachO;
const raw_data = ar_reader.take(size) catch return error.InvalidMachO;
const entry_name: []const u8, const entry_contents: []const u8 = entry: {
if (!std.mem.startsWith(u8, raw_name, "#1/")) {
break :entry .{ mem.sliceTo(raw_name, '/'), raw_data };
}
const len = std.fmt.parseInt(u32, mem.sliceTo(raw_name[3..], ' '), 10) catch return error.InvalidMachO;
if (len > size) return error.InvalidMachO;
break :entry .{ mem.sliceTo(raw_data[0..len], 0), raw_data[len..] };
};
if (std.mem.eql(u8, entry_name, target_name_in_archive)) {
break :map .{ mapped_archive, entry_contents };
}
}
};
errdefer posix.munmap(all_mapped_memory);
var r: Io.Reader = .fixed(mapped_ofile);
const hdr = r.takeStruct(macho.mach_header_64, .little) catch |err| switch (err) {
error.ReadFailed => unreachable,
error.EndOfStream => return error.InvalidMachO,
};
if (hdr.magic != std.macho.MH_MAGIC_64) return error.InvalidMachO;
const seg_cmd: macho.LoadCommandIterator.LoadCommand, const symtab_cmd: macho.symtab_command = cmds: {
var seg_cmd: ?macho.LoadCommandIterator.LoadCommand = null;
var symtab_cmd: ?macho.symtab_command = null;
var it: macho.LoadCommandIterator = try .init(&hdr, mapped_ofile[@sizeOf(macho.mach_header_64)..]);
while (try it.next()) |lc| switch (lc.hdr.cmd) {
.SEGMENT_64 => seg_cmd = lc,
.SYMTAB => symtab_cmd = lc.cast(macho.symtab_command) orelse return error.InvalidMachO,
else => {},
};
break :cmds .{
seg_cmd orelse return error.MissingDebugInfo,
symtab_cmd orelse return error.MissingDebugInfo,
};
};
if (mapped_ofile.len < symtab_cmd.stroff + symtab_cmd.strsize) return error.InvalidMachO;
if (mapped_ofile[symtab_cmd.stroff + symtab_cmd.strsize - 1] != 0) return error.InvalidMachO;
const strtab = mapped_ofile[symtab_cmd.stroff..][0 .. symtab_cmd.strsize - 1];
const n_sym_bytes = symtab_cmd.nsyms * @sizeOf(macho.nlist_64);
if (mapped_ofile.len < symtab_cmd.symoff + n_sym_bytes) return error.InvalidMachO;
const symtab_raw: []align(1) const macho.nlist_64 = @ptrCast(mapped_ofile[symtab_cmd.symoff..][0..n_sym_bytes]);
// TODO handle tentative (common) symbols
var symbols_by_name: std.array_hash_map.Custom(u32, void, void, true) = .empty;
defer symbols_by_name.deinit(gpa);
try symbols_by_name.ensureUnusedCapacity(gpa, @intCast(symtab_raw.len));
for (symtab_raw, 0..) |sym_raw, sym_index| {
var sym = sym_raw;
if (builtin.cpu.arch.endian() != .little) std.mem.byteSwapAllFields(macho.nlist_64, &sym);
if (sym.n_strx == 0) continue;
switch (sym.n_type.bits.type) {
.undf => continue, // includes tentative symbols
.abs => continue,
else => {},
}
const sym_name = mem.sliceTo(strtab[sym.n_strx..], 0);
const gop = symbols_by_name.getOrPutAssumeCapacityAdapted(
@as([]const u8, sym_name),
@as(OFile.SymbolAdapter, .{ .strtab = strtab, .symtab_raw = symtab_raw }),
);
if (gop.found_existing) return error.InvalidMachO;
gop.key_ptr.* = @intCast(sym_index);
}
const dwarf = try loadDwarfFromSections(gpa, mapped_ofile, seg_cmd.getSections());
return .{
.mapped_memory = all_mapped_memory,
.dwarf = dwarf,
.strtab = strtab,
.symtab_raw = symtab_raw,
.symbols_by_name = symbols_by_name.move(),
};
}
fn loadDwarfFromSections(
gpa: Allocator,
mapped_macho: []const u8,
section_headers: []align(1) const macho.section_64,
) !Dwarf {
var sections: Dwarf.SectionArray = @splat(null);
for (section_headers) |sect_raw| {
var sect = sect_raw;
if (builtin.cpu.arch.endian() != .little) std.mem.byteSwapAllFields(macho.section_64, &sect);
if (!std.mem.eql(u8, "__DWARF", sect.segName())) continue;
const section_index: usize = inline for (@typeInfo(Dwarf.Section.Id).@"enum".field_names, 0..) |field_name, i| {
const section_name_long = "__" ++ field_name;
// Some dwarf section names don't fit in the `sectname` buffer, so they are truncated.
const section_name_trunc = section_name_long[0..@min(section_name_long.len, sect.sectname.len)];
if (mem.eql(u8, section_name_trunc, sect.sectName())) break i;
} else continue;
if (mapped_macho.len < sect.offset + sect.size) return error.InvalidMachO;
const section_bytes = mapped_macho[sect.offset..][0..sect.size];
sections[section_index] = .{
.data = section_bytes,
.owned = false,
};
}
if (sections[@backingInt(Dwarf.Section.Id.debug_info)] == null or
sections[@backingInt(Dwarf.Section.Id.debug_abbrev)] == null or
sections[@backingInt(Dwarf.Section.Id.debug_str)] == null or
sections[@backingInt(Dwarf.Section.Id.debug_line)] == null)
{
return error.MissingDebugInfo;
}
var dwarf: Dwarf = .{ .sections = sections };
errdefer dwarf.deinit(gpa);
dwarf.open(gpa, .little) catch |err| switch (err) {
error.InvalidDebugInfo,
error.EndOfStream,
error.Overflow,
error.StreamTooLong,
=> return error.InvalidDwarf,
error.MissingDebugInfo,
error.ReadFailed,
error.OutOfMemory,
=> |e| return e,
};
return dwarf;
}
fn selectMachOSlice(
all_mapped_memory: []align(std.heap.page_size_min) const u8,
arch: std.Target.Cpu.Arch,
) Error![]const u8 {
// In most cases, the file we just mapped is a Mach-O binary. However, it could be a "universal
// binary": a simple file format which contains Mach-O binaries for multiple targets. For
// instance, `/usr/lib/dyld` is currently distributed as a universal binary containing images
// for both ARM64 macOS and x86_64 macOS.
if (all_mapped_memory.len < 4) return error.InvalidMachO;
const magic = std.mem.readInt(u32, all_mapped_memory.ptr[0..4], .little);
// The contents of a Mach-O file, which may or may not be the whole of `all_mapped_memory`.
const mapped_macho = switch (magic) {
macho.MH_MAGIC_64 => all_mapped_memory,
macho.FAT_CIGAM => mapped_macho: {
// This is the universal binary format (aka a "fat binary").
var fat_r: Io.Reader = .fixed(all_mapped_memory);
const hdr = fat_r.takeStruct(macho.fat_header, .big) catch |err| switch (err) {
error.ReadFailed => unreachable,
error.EndOfStream => return error.InvalidMachO,
};
const want_cpu_type = switch (arch) {
.x86_64 => macho.CPU_TYPE_X86_64,
.aarch64 => macho.CPU_TYPE_ARM64,
else => unreachable,
};
for (0..hdr.nfat_arch) |_| {
const fat_arch = fat_r.takeStruct(macho.fat_arch, .big) catch |err| switch (err) {
error.ReadFailed => unreachable,
error.EndOfStream => return error.InvalidMachO,
};
if (fat_arch.cputype != want_cpu_type) continue;
if (fat_arch.offset + fat_arch.size > all_mapped_memory.len) return error.InvalidMachO;
break :mapped_macho all_mapped_memory[fat_arch.offset..][0..fat_arch.size];
}
// `arch` was not present in the fat binary.
return error.MissingDebugInfo;
},
// Even on modern 64-bit targets, this format doesn't seem to be too extensively used. It
// will be fairly easy to add support here if necessary; it's very similar to above.
macho.FAT_CIGAM_64 => return error.UnsupportedDebugInfo,
else => return error.InvalidMachO,
};
return mapped_macho;
}
/// Uses `mmap` to map the file at `path` into memory.
fn mapDebugInfoFile(io: Io, path: []const u8) ![]align(std.heap.page_size_min) const u8 {
const file = Io.Dir.cwd().openFile(io, path, .{}) catch |err| switch (err) {
error.FileNotFound => return error.MissingDebugInfo,
else => return error.ReadFailed,
};
defer file.close(io);
const file_len = std.math.cast(
usize,
file.length(io) catch return error.ReadFailed,
) orelse return error.ReadFailed;
return posix.mmap(
null,
file_len,
.{ .READ = true },
.{ .TYPE = .SHARED },
file.handle,
0,
) catch return error.ReadFailed;
}
const std = @import("std");
const Allocator = std.mem.Allocator;
const Dwarf = std.debug.Dwarf;
const Io = std.Io;
const assert = std.debug.assert;
const posix = std.posix;
const macho = std.macho;
const mem = std.mem;
const testing = std.testing;
const builtin = @import("builtin");
const Uuid = @FieldType(macho.uuid_command, "uuid");
const MachOFile = @This();