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,
22symbols: []const Symbol,
33strings: []const u8,
44text_vmaddr: u64,
5uuid: ?Uuid,
6adjacent_dsym: ?DsymFile,
57
68/// Key is index into `strings` of the file path.
79ofiles: std.array_hash_map.Auto(u32, Error!OFile),
......@@ -16,6 +18,7 @@ pub const Error = error{
1618};
1719
1820pub fn deinit(mf: *MachOFile, gpa: Allocator) void {
21 if (mf.adjacent_dsym) |*dsym| dsym.deinit(gpa);
1922 for (mf.ofiles.values()) |*maybe_of| {
2023 const of = &(maybe_of.* catch continue);
2124 posix.munmap(of.mapped_memory);
......@@ -36,48 +39,7 @@ pub fn load(gpa: Allocator, io: Io, path: []const u8, arch: std.Target.Cpu.Arch)
3639 const all_mapped_memory = try mapDebugInfoFile(io, path);
3740 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 "universal
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 };
42 const mapped_macho = try selectMachOSlice(all_mapped_memory, arch);
8143
8244 var r: Io.Reader = .fixed(mapped_macho);
8345 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)
8850 if (hdr.magic != macho.MH_MAGIC_64)
8951 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: {
9254 var it: macho.LoadCommandIterator = try .init(&hdr, mapped_macho[@sizeOf(macho.mach_header_64)..]);
9355 var symtab: ?macho.symtab_command = null;
9456 var text_vmaddr: ?u64 = null;
57 var uuid: ?Uuid = null;
9558 while (try it.next()) |cmd| switch (cmd.hdr.cmd) {
9659 .SYMTAB => symtab = cmd.cast(macho.symtab_command) orelse return error.InvalidMachO,
9760 .SEGMENT_64 => if (cmd.cast(macho.segment_command_64)) |seg_cmd| {
9861 if (!mem.eql(u8, seg_cmd.segName(), "__TEXT")) continue;
9962 text_vmaddr = seg_cmd.vmaddr;
10063 },
64 .UUID => if (cmd.cast(macho.uuid_command)) |uuid_cmd| {
65 uuid = uuid_cmd.uuid;
66 },
10167 else => {},
10268 };
10369 break :lcs .{
10470 symtab orelse return error.MissingDebugInfo,
10571 text_vmaddr orelse return error.MissingDebugInfo,
72 uuid,
10673 };
10774 };
10875
......@@ -253,15 +220,27 @@ pub fn load(gpa: Allocator, io: Io, path: []const u8, arch: std.Target.Cpu.Arch)
253220 // This sort is so that we can binary search later.
254221 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
256228 return .{
257229 .mapped_memory = all_mapped_memory,
258230 .symbols = symbols_slice,
259231 .strings = strings,
260232 .ofiles = .empty,
261233 .text_vmaddr = text_vmaddr,
234 .uuid = uuid,
235 .adjacent_dsym = adjacent_dsym,
262236 };
263237}
238
264239pub 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
265244 const symbol = Symbol.find(mf.symbols, vaddr) orelse return error.MissingDebugInfo;
266245
267246 if (symbol.ofile == Symbol.unknown_ofile) return error.MissingDebugInfo;
......@@ -324,6 +303,16 @@ const OFile = struct {
324303 };
325304};
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
327316const Symbol = struct {
328317 strx: u32,
329318 addr: u64,
......@@ -394,6 +383,74 @@ fn appendStabSymbol(
394383 }
395384}
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
397454fn loadOFile(gpa: Allocator, io: Io, o_file_name: []const u8) !OFile {
398455 const all_mapped_memory, const mapped_ofile = map: {
399456 const open_paren = paren: {
......@@ -497,8 +554,24 @@ fn loadOFile(gpa: Allocator, io: Io, o_file_name: []const u8) !OFile {
497554 gop.key_ptr.* = @intCast(sym_index);
498555 }
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 {
500573 var sections: Dwarf.SectionArray = @splat(null);
501 for (seg_cmd.getSections()) |sect_raw| {
574 for (section_headers) |sect_raw| {
502575 var sect = sect_raw;
503576 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 {
511584 if (mem.eql(u8, section_name_trunc, sect.sectName())) break i;
512585 } else continue;
513586
514 if (mapped_ofile.len < sect.offset + sect.size) return error.InvalidMachO;
515 const section_bytes = mapped_ofile[sect.offset..][0..sect.size];
587 if (mapped_macho.len < sect.offset + sect.size) return error.InvalidMachO;
588 const section_bytes = mapped_macho[sect.offset..][0..sect.size];
516589 sections[section_index] = .{
517590 .data = section_bytes,
518591 .owned = false,
......@@ -542,13 +615,56 @@ fn loadOFile(gpa: Allocator, io: Io, o_file_name: []const u8) !OFile {
542615 => |e| return e,
543616 };
544617
545 return .{
546 .mapped_memory = all_mapped_memory,
547 .dwarf = dwarf,
548 .strtab = strtab,
549 .symtab_raw = symtab_raw,
550 .symbols_by_name = symbols_by_name.move(),
618 return dwarf;
619}
620
621fn selectMachOSlice(
622 all_mapped_memory: []align(std.heap.page_size_min) const u8,
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,
551666 };
667 return mapped_macho;
552668}
553669
554670/// Uses `mmap` to map the file at `path` into memory.
......@@ -586,4 +702,5 @@ const testing = std.testing;
586702
587703const builtin = @import("builtin");
588704
705const Uuid = @FieldType(macho.uuid_command, "uuid");
589706const MachOFile = @This();