| ... | @@ -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, |
| 2 | symbols: []const Symbol, | 2 | symbols: []const Symbol, |
| 3 | strings: []const u8, | 3 | strings: []const u8, |
| 4 | text_vmaddr: u64, | 4 | text_vmaddr: u64, |
| | 5 | uuid: ?Uuid, |
| | 6 | adjacent_dsym: ?DsymFile, |
| 5 | | 7 | |
| 6 | /// Key is index into `strings` of the file path. | 8 | /// Key is index into `strings` of the file path. |
| 7 | ofiles: std.array_hash_map.Auto(u32, Error!OFile), | 9 | ofiles: 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 | }; |
| 17 | | 19 | |
| 18 | pub fn deinit(mf: *MachOFile, gpa: Allocator) void { | 20 | pub 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); |
| 38 | | 41 | |
| 39 | // In most cases, the file we just mapped is a Mach-O binary. However, it could be a "universal | 42 | 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 | }; | | |
| 81 | | 43 | |
| 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; |
| 90 | | 52 | |
| 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 | }; |
| 108 | | 75 | |
| ... | @@ -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); |
| 255 | | 222 | |
| | 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 | |
| 264 | pub fn getDwarfForAddress(mf: *MachOFile, gpa: Allocator, io: Io, vaddr: u64) !struct { *Dwarf, u64 } { | 239 | pub 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; |
| 266 | | 245 | |
| 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 | }; |
| 326 | | 305 | |
| | 306 | const 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 | |
| 327 | const Symbol = struct { | 316 | const 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 | } |
| 396 | | 385 | |
| | 386 | fn 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 | |
| | 411 | fn 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 | |
| 397 | fn loadOFile(gpa: Allocator, io: Io, o_file_name: []const u8) !OFile { | 454 | fn 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 | } |
| 499 | | 556 | |
| | 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 | |
| | 568 | fn 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); |
| 504 | | 577 | |
| ... | @@ -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; |
| 513 | | 586 | |
| 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 | }; |
| 544 | | 617 | |
| 545 | return .{ | 618 | return dwarf; |
| 546 | .mapped_memory = all_mapped_memory, | 619 | } |
| 547 | .dwarf = dwarf, | 620 | |
| 548 | .strtab = strtab, | 621 | fn 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 | } |
| 553 | | 669 | |
| 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; |
| 586 | | 702 | |
| 587 | const builtin = @import("builtin"); | 703 | const builtin = @import("builtin"); |
| 588 | | 704 | |
| | 705 | const Uuid = @FieldType(macho.uuid_command, "uuid"); |
| 589 | const MachOFile = @This(); | 706 | const MachOFile = @This(); |