authorgravatar for corentin.kerisit@gmail.comCorentin Kerisit <corentin.kerisit@gmail.com> 2026-08-07 16:14:05+02:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2026-08-07 16:14:05+02:00
logfafbc70ac58e056373eed8f2f21ff348f1e3a78a
treea09d86ed094be3fcdecb027fb3f7ea333b85dda7
parent1ebc455715958d131cc00caa9aa6c58fa8e7b726

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>

1 files changed, 169 insertions(+), 52 deletions(-)

lib/std/debug/MachOFile.zig+169-52
...@@ -2,6 +2,8 @@ mapped_memory: []align(std.heap.page_size_min) const u8,...@@ -2,6 +2,8 @@ mapped_memory: []align(std.heap.page_size_min) const u8,
2symbols: []const Symbol,2symbols: []const Symbol,
3strings: []const u8,3strings: []const u8,
4text_vmaddr: u64,4text_vmaddr: u64,
5uuid: ?Uuid,
6adjacent_dsym: ?DsymFile,
57
6/// Key is index into `strings` of the file path.8/// Key is index into `strings` of the file path.
7ofiles: std.array_hash_map.Auto(u32, Error!OFile),9ofiles: std.array_hash_map.Auto(u32, Error!OFile),
...@@ -16,6 +18,7 @@ pub const Error = error{...@@ -16,6 +18,7 @@ pub const Error = error{
16};18};
1719
18pub fn deinit(mf: *MachOFile, gpa: Allocator) void {20pub fn deinit(mf: *MachOFile, gpa: Allocator) void {
21 if (mf.adjacent_dsym) |*dsym| dsym.deinit(gpa);
19 for (mf.ofiles.values()) |*maybe_of| {22 for (mf.ofiles.values()) |*maybe_of| {
20 const of = &(maybe_of.* catch continue);23 const of = &(maybe_of.* catch continue);
21 posix.munmap(of.mapped_memory);24 posix.munmap(of.mapped_memory);
...@@ -36,48 +39,7 @@ pub fn load(gpa: Allocator, io: Io, path: []const u8, arch: std.Target.Cpu.Arch)...@@ -36,48 +39,7 @@ pub fn load(gpa: Allocator, io: Io, path: []const u8, arch: std.Target.Cpu.Arch)
36 const all_mapped_memory = try mapDebugInfoFile(io, path);39 const all_mapped_memory = try mapDebugInfoFile(io, path);
37 errdefer posix.munmap(all_mapped_memory);40 errdefer posix.munmap(all_mapped_memory);
3841
39 // In most cases, the file we just mapped is a Mach-O binary. However, it could be a "universal42 const mapped_macho = try selectMachOSlice(all_mapped_memory, arch);
40 // binary": a simple file format which contains Mach-O binaries for multiple targets. For
41 // instance, `/usr/lib/dyld` is currently distributed as a universal binary containing images
42 // for both ARM64 macOS and x86_64 macOS.
43 if (all_mapped_memory.len < 4) return error.InvalidMachO;
44 const magic = std.mem.readInt(u32, all_mapped_memory.ptr[0..4], .little);
45
46 // The contents of a Mach-O file, which may or may not be the whole of `all_mapped_memory`.
47 const mapped_macho = switch (magic) {
48 macho.MH_MAGIC_64 => all_mapped_memory,
49
50 macho.FAT_CIGAM => mapped_macho: {
51 // This is the universal binary format (aka a "fat binary").
52 var fat_r: Io.Reader = .fixed(all_mapped_memory);
53 const hdr = fat_r.takeStruct(macho.fat_header, .big) catch |err| switch (err) {
54 error.ReadFailed => unreachable,
55 error.EndOfStream => return error.InvalidMachO,
56 };
57 const want_cpu_type = switch (arch) {
58 .x86_64 => macho.CPU_TYPE_X86_64,
59 .aarch64 => macho.CPU_TYPE_ARM64,
60 else => unreachable,
61 };
62 for (0..hdr.nfat_arch) |_| {
63 const fat_arch = fat_r.takeStruct(macho.fat_arch, .big) catch |err| switch (err) {
64 error.ReadFailed => unreachable,
65 error.EndOfStream => return error.InvalidMachO,
66 };
67 if (fat_arch.cputype != want_cpu_type) continue;
68 if (fat_arch.offset + fat_arch.size > all_mapped_memory.len) return error.InvalidMachO;
69 break :mapped_macho all_mapped_memory[fat_arch.offset..][0..fat_arch.size];
70 }
71 // `arch` was not present in the fat binary.
72 return error.MissingDebugInfo;
73 },
74
75 // Even on modern 64-bit targets, this format doesn't seem to be too extensively used. It
76 // will be fairly easy to add support here if necessary; it's very similar to above.
77 macho.FAT_CIGAM_64 => return error.UnsupportedDebugInfo,
78
79 else => return error.InvalidMachO,
80 };
8143
82 var r: Io.Reader = .fixed(mapped_macho);44 var r: Io.Reader = .fixed(mapped_macho);
83 const hdr = r.takeStruct(macho.mach_header_64, .little) catch |err| switch (err) {45 const hdr = r.takeStruct(macho.mach_header_64, .little) catch |err| switch (err) {
...@@ -88,21 +50,26 @@ pub fn load(gpa: Allocator, io: Io, path: []const u8, arch: std.Target.Cpu.Arch)...@@ -88,21 +50,26 @@ pub fn load(gpa: Allocator, io: Io, path: []const u8, arch: std.Target.Cpu.Arch)
88 if (hdr.magic != macho.MH_MAGIC_64)50 if (hdr.magic != macho.MH_MAGIC_64)
89 return error.InvalidMachO;51 return error.InvalidMachO;
9052
91 const symtab: macho.symtab_command, const text_vmaddr: u64 = lcs: {53 const symtab: macho.symtab_command, const text_vmaddr: u64, const uuid: ?Uuid = lcs: {
92 var it: macho.LoadCommandIterator = try .init(&hdr, mapped_macho[@sizeOf(macho.mach_header_64)..]);54 var it: macho.LoadCommandIterator = try .init(&hdr, mapped_macho[@sizeOf(macho.mach_header_64)..]);
93 var symtab: ?macho.symtab_command = null;55 var symtab: ?macho.symtab_command = null;
94 var text_vmaddr: ?u64 = null;56 var text_vmaddr: ?u64 = null;
57 var uuid: ?Uuid = null;
95 while (try it.next()) |cmd| switch (cmd.hdr.cmd) {58 while (try it.next()) |cmd| switch (cmd.hdr.cmd) {
96 .SYMTAB => symtab = cmd.cast(macho.symtab_command) orelse return error.InvalidMachO,59 .SYMTAB => symtab = cmd.cast(macho.symtab_command) orelse return error.InvalidMachO,
97 .SEGMENT_64 => if (cmd.cast(macho.segment_command_64)) |seg_cmd| {60 .SEGMENT_64 => if (cmd.cast(macho.segment_command_64)) |seg_cmd| {
98 if (!mem.eql(u8, seg_cmd.segName(), "__TEXT")) continue;61 if (!mem.eql(u8, seg_cmd.segName(), "__TEXT")) continue;
99 text_vmaddr = seg_cmd.vmaddr;62 text_vmaddr = seg_cmd.vmaddr;
100 },63 },
64 .UUID => if (cmd.cast(macho.uuid_command)) |uuid_cmd| {
65 uuid = uuid_cmd.uuid;
66 },
101 else => {},67 else => {},
102 };68 };
103 break :lcs .{69 break :lcs .{
104 symtab orelse return error.MissingDebugInfo,70 symtab orelse return error.MissingDebugInfo,
105 text_vmaddr orelse return error.MissingDebugInfo,71 text_vmaddr orelse return error.MissingDebugInfo,
72 uuid,
106 };73 };
107 };74 };
10875
...@@ -253,15 +220,27 @@ pub fn load(gpa: Allocator, io: Io, path: []const u8, arch: std.Target.Cpu.Arch)...@@ -253,15 +220,27 @@ pub fn load(gpa: Allocator, io: Io, path: []const u8, arch: std.Target.Cpu.Arch)
253 // This sort is so that we can binary search later.220 // This sort is so that we can binary search later.
254 mem.sort(Symbol, symbols_slice, {}, Symbol.addressLessThan);221 mem.sort(Symbol, symbols_slice, {}, Symbol.addressLessThan);
255222
223 const adjacent_dsym = if (uuid) |expected_uuid|
224 try loadAdjacentDsym(gpa, io, path, arch, expected_uuid)
225 else
226 null;
227
256 return .{228 return .{
257 .mapped_memory = all_mapped_memory,229 .mapped_memory = all_mapped_memory,
258 .symbols = symbols_slice,230 .symbols = symbols_slice,
259 .strings = strings,231 .strings = strings,
260 .ofiles = .empty,232 .ofiles = .empty,
261 .text_vmaddr = text_vmaddr,233 .text_vmaddr = text_vmaddr,
234 .uuid = uuid,
235 .adjacent_dsym = adjacent_dsym,
262 };236 };
263}237}
238
264pub fn getDwarfForAddress(mf: *MachOFile, gpa: Allocator, io: Io, vaddr: u64) !struct { *Dwarf, u64 } {239pub fn getDwarfForAddress(mf: *MachOFile, gpa: Allocator, io: Io, vaddr: u64) !struct { *Dwarf, u64 } {
240 if (mf.adjacent_dsym) |*dsym| {
241 return .{ &dsym.dwarf, vaddr };
242 }
243
265 const symbol = Symbol.find(mf.symbols, vaddr) orelse return error.MissingDebugInfo;244 const symbol = Symbol.find(mf.symbols, vaddr) orelse return error.MissingDebugInfo;
266245
267 if (symbol.ofile == Symbol.unknown_ofile) return error.MissingDebugInfo;246 if (symbol.ofile == Symbol.unknown_ofile) return error.MissingDebugInfo;
...@@ -324,6 +303,16 @@ const OFile = struct {...@@ -324,6 +303,16 @@ const OFile = struct {
324 };303 };
325};304};
326305
306const DsymFile = struct {
307 mapped_memory: []align(std.heap.page_size_min) const u8,
308 dwarf: Dwarf,
309
310 fn deinit(df: *DsymFile, gpa: Allocator) void {
311 df.dwarf.deinit(gpa);
312 posix.munmap(df.mapped_memory);
313 }
314};
315
327const Symbol = struct {316const Symbol = struct {
328 strx: u32,317 strx: u32,
329 addr: u64,318 addr: u64,
...@@ -394,6 +383,74 @@ fn appendStabSymbol(...@@ -394,6 +383,74 @@ fn appendStabSymbol(
394 }383 }
395}384}
396385
386fn loadAdjacentDsym(
387 gpa: Allocator,
388 io: Io,
389 binary_path: []const u8,
390 arch: std.Target.Cpu.Arch,
391 uuid: Uuid,
392) Error!?DsymFile {
393 const s = std.fs.path.sep_str;
394 const dsym_path = try std.fmt.allocPrint(
395 gpa,
396 "{s}.dSYM" ++ s ++ "Contents" ++ s ++ "Resources" ++ s ++ "DWARF" ++ s ++ "{s}",
397 .{ binary_path, std.fs.path.basename(binary_path) },
398 );
399 defer gpa.free(dsym_path);
400 return loadDsymFile(gpa, io, dsym_path, arch, uuid) catch |err| switch (err) {
401 error.MissingDebugInfo,
402 error.InvalidMachO,
403 error.InvalidDwarf,
404 error.UnsupportedDebugInfo,
405 error.ReadFailed,
406 => null,
407 error.OutOfMemory => |e| return e,
408 };
409}
410
411fn loadDsymFile(
412 gpa: Allocator,
413 io: Io,
414 path: []const u8,
415 arch: std.Target.Cpu.Arch,
416 expected_uuid: Uuid,
417) Error!DsymFile {
418 const all_mapped_memory = try mapDebugInfoFile(io, path);
419 errdefer posix.munmap(all_mapped_memory);
420 const mapped_macho = try selectMachOSlice(all_mapped_memory, arch);
421
422 var r: Io.Reader = .fixed(mapped_macho);
423 const hdr = r.takeStruct(macho.mach_header_64, .little) catch |err| switch (err) {
424 error.ReadFailed => unreachable,
425 error.EndOfStream => return error.InvalidMachO,
426 };
427 if (hdr.magic != macho.MH_MAGIC_64) return error.InvalidMachO;
428 if (hdr.filetype != macho.MH_DSYM) return error.MissingDebugInfo;
429
430 var uuid: ?Uuid = null;
431 var dwarf_sections: ?[]align(1) const macho.section_64 = null;
432
433 var it: macho.LoadCommandIterator = try .init(&hdr, mapped_macho[@sizeOf(macho.mach_header_64)..]);
434 while (try it.next()) |lc| switch (lc.hdr.cmd) {
435 .SEGMENT_64 => if (lc.cast(macho.segment_command_64)) |seg_cmd| {
436 if (!mem.eql(u8, "__DWARF", seg_cmd.segName())) continue;
437 dwarf_sections = lc.getSections();
438 },
439 .UUID => if (lc.cast(macho.uuid_command)) |uuid_cmd| {
440 uuid = uuid_cmd.uuid;
441 },
442 else => {},
443 };
444
445 const actual_uuid = uuid orelse return error.MissingDebugInfo;
446 if (!mem.eql(u8, &actual_uuid, &expected_uuid)) return error.MissingDebugInfo;
447
448 return .{
449 .mapped_memory = all_mapped_memory,
450 .dwarf = try loadDwarfFromSections(gpa, mapped_macho, dwarf_sections orelse return error.MissingDebugInfo),
451 };
452}
453
397fn loadOFile(gpa: Allocator, io: Io, o_file_name: []const u8) !OFile {454fn loadOFile(gpa: Allocator, io: Io, o_file_name: []const u8) !OFile {
398 const all_mapped_memory, const mapped_ofile = map: {455 const all_mapped_memory, const mapped_ofile = map: {
399 const open_paren = paren: {456 const open_paren = paren: {
...@@ -497,8 +554,24 @@ fn loadOFile(gpa: Allocator, io: Io, o_file_name: []const u8) !OFile {...@@ -497,8 +554,24 @@ fn loadOFile(gpa: Allocator, io: Io, o_file_name: []const u8) !OFile {
497 gop.key_ptr.* = @intCast(sym_index);554 gop.key_ptr.* = @intCast(sym_index);
498 }555 }
499556
557 const dwarf = try loadDwarfFromSections(gpa, mapped_ofile, seg_cmd.getSections());
558
559 return .{
560 .mapped_memory = all_mapped_memory,
561 .dwarf = dwarf,
562 .strtab = strtab,
563 .symtab_raw = symtab_raw,
564 .symbols_by_name = symbols_by_name.move(),
565 };
566}
567
568fn loadDwarfFromSections(
569 gpa: Allocator,
570 mapped_macho: []const u8,
571 section_headers: []align(1) const macho.section_64,
572) !Dwarf {
500 var sections: Dwarf.SectionArray = @splat(null);573 var sections: Dwarf.SectionArray = @splat(null);
501 for (seg_cmd.getSections()) |sect_raw| {574 for (section_headers) |sect_raw| {
502 var sect = sect_raw;575 var sect = sect_raw;
503 if (builtin.cpu.arch.endian() != .little) std.mem.byteSwapAllFields(macho.section_64, &sect);576 if (builtin.cpu.arch.endian() != .little) std.mem.byteSwapAllFields(macho.section_64, &sect);
504577
...@@ -511,8 +584,8 @@ fn loadOFile(gpa: Allocator, io: Io, o_file_name: []const u8) !OFile {...@@ -511,8 +584,8 @@ fn loadOFile(gpa: Allocator, io: Io, o_file_name: []const u8) !OFile {
511 if (mem.eql(u8, section_name_trunc, sect.sectName())) break i;584 if (mem.eql(u8, section_name_trunc, sect.sectName())) break i;
512 } else continue;585 } else continue;
513586
514 if (mapped_ofile.len < sect.offset + sect.size) return error.InvalidMachO;587 if (mapped_macho.len < sect.offset + sect.size) return error.InvalidMachO;
515 const section_bytes = mapped_ofile[sect.offset..][0..sect.size];588 const section_bytes = mapped_macho[sect.offset..][0..sect.size];
516 sections[section_index] = .{589 sections[section_index] = .{
517 .data = section_bytes,590 .data = section_bytes,
518 .owned = false,591 .owned = false,
...@@ -542,13 +615,56 @@ fn loadOFile(gpa: Allocator, io: Io, o_file_name: []const u8) !OFile {...@@ -542,13 +615,56 @@ fn loadOFile(gpa: Allocator, io: Io, o_file_name: []const u8) !OFile {
542 => |e| return e,615 => |e| return e,
543 };616 };
544617
545 return .{618 return dwarf;
546 .mapped_memory = all_mapped_memory,619}
547 .dwarf = dwarf,620
548 .strtab = strtab,621fn selectMachOSlice(
549 .symtab_raw = symtab_raw,622 all_mapped_memory: []align(std.heap.page_size_min) const u8,
550 .symbols_by_name = symbols_by_name.move(),623 arch: std.Target.Cpu.Arch,
624) Error![]const u8 {
625 // In most cases, the file we just mapped is a Mach-O binary. However, it could be a "universal
626 // binary": a simple file format which contains Mach-O binaries for multiple targets. For
627 // instance, `/usr/lib/dyld` is currently distributed as a universal binary containing images
628 // for both ARM64 macOS and x86_64 macOS.
629 if (all_mapped_memory.len < 4) return error.InvalidMachO;
630 const magic = std.mem.readInt(u32, all_mapped_memory.ptr[0..4], .little);
631
632 // The contents of a Mach-O file, which may or may not be the whole of `all_mapped_memory`.
633 const mapped_macho = switch (magic) {
634 macho.MH_MAGIC_64 => all_mapped_memory,
635
636 macho.FAT_CIGAM => mapped_macho: {
637 // This is the universal binary format (aka a "fat binary").
638 var fat_r: Io.Reader = .fixed(all_mapped_memory);
639 const hdr = fat_r.takeStruct(macho.fat_header, .big) catch |err| switch (err) {
640 error.ReadFailed => unreachable,
641 error.EndOfStream => return error.InvalidMachO,
642 };
643 const want_cpu_type = switch (arch) {
644 .x86_64 => macho.CPU_TYPE_X86_64,
645 .aarch64 => macho.CPU_TYPE_ARM64,
646 else => unreachable,
647 };
648 for (0..hdr.nfat_arch) |_| {
649 const fat_arch = fat_r.takeStruct(macho.fat_arch, .big) catch |err| switch (err) {
650 error.ReadFailed => unreachable,
651 error.EndOfStream => return error.InvalidMachO,
652 };
653 if (fat_arch.cputype != want_cpu_type) continue;
654 if (fat_arch.offset + fat_arch.size > all_mapped_memory.len) return error.InvalidMachO;
655 break :mapped_macho all_mapped_memory[fat_arch.offset..][0..fat_arch.size];
656 }
657 // `arch` was not present in the fat binary.
658 return error.MissingDebugInfo;
659 },
660
661 // Even on modern 64-bit targets, this format doesn't seem to be too extensively used. It
662 // will be fairly easy to add support here if necessary; it's very similar to above.
663 macho.FAT_CIGAM_64 => return error.UnsupportedDebugInfo,
664
665 else => return error.InvalidMachO,
551 };666 };
667 return mapped_macho;
552}668}
553669
554/// Uses `mmap` to map the file at `path` into memory.670/// Uses `mmap` to map the file at `path` into memory.
...@@ -586,4 +702,5 @@ const testing = std.testing;...@@ -586,4 +702,5 @@ const testing = std.testing;
586702
587const builtin = @import("builtin");703const builtin = @import("builtin");
588704
705const Uuid = @FieldType(macho.uuid_command, "uuid");
589const MachOFile = @This();706const MachOFile = @This();