From fafbc70ac58e056373eed8f2f21ff348f1e3a78a Mon Sep 17 00:00:00 2001 From: Corentin Kerisit Date: Fri, 7 Aug 2026 16:14:05 +0200 Subject: [PATCH] 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 --- lib/std/debug/MachOFile.zig | 221 +++++++++++++++++++++++++++--------- 1 file changed, 169 insertions(+), 52 deletions(-) diff --git a/lib/std/debug/MachOFile.zig b/lib/std/debug/MachOFile.zig index 8b35d7541d05caae74e065d6146ebbffd4e8d3e4..220876105ec527ba88a3e3b8fb02353741ff3055 100644 --- a/lib/std/debug/MachOFile.zig +++ b/lib/std/debug/MachOFile.zig @@ -2,6 +2,8 @@ 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), @@ -16,6 +18,7 @@ pub const Error = error{ }; 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); @@ -36,48 +39,7 @@ pub fn load(gpa: Allocator, io: Io, path: []const u8, arch: std.Target.Cpu.Arch) const all_mapped_memory = try mapDebugInfoFile(io, path); errdefer posix.munmap(all_mapped_memory); - // 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, - }; + 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) { @@ -88,21 +50,26 @@ pub fn load(gpa: Allocator, io: Io, path: []const u8, arch: std.Target.Cpu.Arch) if (hdr.magic != macho.MH_MAGIC_64) return error.InvalidMachO; - const symtab: macho.symtab_command, const text_vmaddr: u64 = lcs: { + 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, }; }; @@ -253,15 +220,27 @@ pub fn load(gpa: Allocator, io: Io, path: []const u8, arch: std.Target.Cpu.Arch) // 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; @@ -324,6 +303,16 @@ const OFile = struct { }; }; +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, @@ -394,6 +383,74 @@ fn appendStabSymbol( } } +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: { @@ -497,8 +554,24 @@ fn loadOFile(gpa: Allocator, io: Io, o_file_name: []const u8) !OFile { 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 (seg_cmd.getSections()) |sect_raw| { + for (section_headers) |sect_raw| { var sect = sect_raw; if (builtin.cpu.arch.endian() != .little) std.mem.byteSwapAllFields(macho.section_64, §); @@ -511,8 +584,8 @@ fn loadOFile(gpa: Allocator, io: Io, o_file_name: []const u8) !OFile { if (mem.eql(u8, section_name_trunc, sect.sectName())) break i; } else continue; - if (mapped_ofile.len < sect.offset + sect.size) return error.InvalidMachO; - const section_bytes = mapped_ofile[sect.offset..][0..sect.size]; + 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, @@ -542,13 +615,56 @@ fn loadOFile(gpa: Allocator, io: Io, o_file_name: []const u8) !OFile { => |e| return e, }; - return .{ - .mapped_memory = all_mapped_memory, - .dwarf = dwarf, - .strtab = strtab, - .symtab_raw = symtab_raw, - .symbols_by_name = symbols_by_name.move(), + 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. @@ -586,4 +702,5 @@ const testing = std.testing; const builtin = @import("builtin"); +const Uuid = @FieldType(macho.uuid_command, "uuid"); const MachOFile = @This(); -- 2.54.0