| ... | ... | @@ -2,6 +2,8 @@ mapped_memory: []align(std.heap.page_size_min) const u8, |
| 2 | 2 | symbols: []const Symbol, |
| 3 | 3 | strings: []const u8, |
| 4 | 4 | text_vmaddr: u64, |
| 5 | uuid: ?Uuid, |
| 6 | adjacent_dsym: ?DsymFile, |
| 5 | 7 | |
| 6 | 8 | /// Key is index into `strings` of the file path. |
| 7 | 9 | ofiles: std.array_hash_map.Auto(u32, Error!OFile), |
| ... | ... | @@ -16,6 +18,7 @@ pub const Error = error{ |
| 16 | 18 | }; |
| 17 | 19 | |
| 18 | 20 | pub fn deinit(mf: *MachOFile, gpa: Allocator) void { |
| 21 | if (mf.adjacent_dsym) |*dsym| dsym.deinit(gpa); |
| 19 | 22 | for (mf.ofiles.values()) |*maybe_of| { |
| 20 | 23 | const of = &(maybe_of.* catch continue); |
| 21 | 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 | 39 | const all_mapped_memory = try mapDebugInfoFile(io, path); |
| 37 | 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 |
| 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); |
| 81 | 43 | |
| 82 | 44 | var r: Io.Reader = .fixed(mapped_macho); |
| 83 | 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 | 50 | if (hdr.magic != macho.MH_MAGIC_64) |
| 89 | 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 | 54 | var it: macho.LoadCommandIterator = try .init(&hdr, mapped_macho[@sizeOf(macho.mach_header_64)..]); |
| 93 | 55 | var symtab: ?macho.symtab_command = null; |
| 94 | 56 | var text_vmaddr: ?u64 = null; |
| 57 | var uuid: ?Uuid = null; |
| 95 | 58 | while (try it.next()) |cmd| switch (cmd.hdr.cmd) { |
| 96 | 59 | .SYMTAB => symtab = cmd.cast(macho.symtab_command) orelse return error.InvalidMachO, |
| 97 | 60 | .SEGMENT_64 => if (cmd.cast(macho.segment_command_64)) |seg_cmd| { |
| 98 | 61 | if (!mem.eql(u8, seg_cmd.segName(), "__TEXT")) continue; |
| 99 | 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 | 67 | else => {}, |
| 102 | 68 | }; |
| 103 | 69 | break :lcs .{ |
| 104 | 70 | symtab orelse return error.MissingDebugInfo, |
| 105 | 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 | 220 | // This sort is so that we can binary search later. |
| 254 | 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 | 228 | return .{ |
| 257 | 229 | .mapped_memory = all_mapped_memory, |
| 258 | 230 | .symbols = symbols_slice, |
| 259 | 231 | .strings = strings, |
| 260 | 232 | .ofiles = .empty, |
| 261 | 233 | .text_vmaddr = text_vmaddr, |
| 234 | .uuid = uuid, |
| 235 | .adjacent_dsym = adjacent_dsym, |
| 262 | 236 | }; |
| 263 | 237 | } |
| 238 | |
| 264 | 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 | 244 | const symbol = Symbol.find(mf.symbols, vaddr) orelse return error.MissingDebugInfo; |
| 266 | 245 | |
| 267 | 246 | if (symbol.ofile == Symbol.unknown_ofile) return error.MissingDebugInfo; |
| ... | ... | @@ -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 | 316 | const Symbol = struct { |
| 328 | 317 | strx: u32, |
| 329 | 318 | addr: u64, |
| ... | ... | @@ -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 | 454 | fn loadOFile(gpa: Allocator, io: Io, o_file_name: []const u8) !OFile { |
| 398 | 455 | const all_mapped_memory, const mapped_ofile = map: { |
| 399 | 456 | const open_paren = paren: { |
| ... | ... | @@ -497,8 +554,24 @@ fn loadOFile(gpa: Allocator, io: Io, o_file_name: []const u8) !OFile { |
| 497 | 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 | 573 | var sections: Dwarf.SectionArray = @splat(null); |
| 501 | | for (seg_cmd.getSections()) |sect_raw| { |
| 574 | for (section_headers) |sect_raw| { |
| 502 | 575 | var sect = sect_raw; |
| 503 | 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 | 584 | if (mem.eql(u8, section_name_trunc, sect.sectName())) break i; |
| 512 | 585 | } else continue; |
| 513 | 586 | |
| 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]; |
| 516 | 589 | sections[section_index] = .{ |
| 517 | 590 | .data = section_bytes, |
| 518 | 591 | .owned = false, |
| ... | ... | @@ -542,13 +615,56 @@ fn loadOFile(gpa: Allocator, io: Io, o_file_name: []const u8) !OFile { |
| 542 | 615 | => |e| return e, |
| 543 | 616 | }; |
| 544 | 617 | |
| 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 | |
| 621 | fn 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, |
| 551 | 666 | }; |
| 667 | return mapped_macho; |
| 552 | 668 | } |
| 553 | 669 | |
| 554 | 670 | /// Uses `mmap` to map the file at `path` into memory. |
| ... | ... | @@ -586,4 +702,5 @@ const testing = std.testing; |
| 586 | 702 | |
| 587 | 703 | const builtin = @import("builtin"); |
| 588 | 704 | |
| 705 | const Uuid = @FieldType(macho.uuid_command, "uuid"); |
| 589 | 706 | const MachOFile = @This(); |