| 1 | mapped_memory: []align(std.heap.page_size_min) const u8, |
| 2 | symbols: []const Symbol, |
| 3 | strings: []const u8, |
| 4 | text_vmaddr: u64, |
| 5 | uuid: ?Uuid, |
| 6 | adjacent_dsym: ?DsymFile, |
| 7 | |
| 8 | /// Key is index into `strings` of the file path. |
| 9 | ofiles: std.array_hash_map.Auto(u32, Error!OFile), |
| 10 | |
| 11 | pub const Error = error{ |
| 12 | InvalidMachO, |
| 13 | InvalidDwarf, |
| 14 | MissingDebugInfo, |
| 15 | UnsupportedDebugInfo, |
| 16 | ReadFailed, |
| 17 | OutOfMemory, |
| 18 | }; |
| 19 | |
| 20 | pub fn deinit(mf: *MachOFile, gpa: Allocator) void { |
| 21 | if (mf.adjacent_dsym) |*dsym| dsym.deinit(gpa); |
| 22 | for (mf.ofiles.values()) |*maybe_of| { |
| 23 | const of = &(maybe_of.* catch continue); |
| 24 | posix.munmap(of.mapped_memory); |
| 25 | of.dwarf.deinit(gpa); |
| 26 | of.symbols_by_name.deinit(gpa); |
| 27 | } |
| 28 | mf.ofiles.deinit(gpa); |
| 29 | gpa.free(mf.symbols); |
| 30 | posix.munmap(mf.mapped_memory); |
| 31 | } |
| 32 | |
| 33 | pub fn load(gpa: Allocator, io: Io, path: []const u8, arch: std.Target.Cpu.Arch) Error!MachOFile { |
| 34 | switch (arch) { |
| 35 | .x86_64, .aarch64 => {}, |
| 36 | else => unreachable, |
| 37 | } |
| 38 | |
| 39 | const all_mapped_memory = try mapDebugInfoFile(io, path); |
| 40 | errdefer posix.munmap(all_mapped_memory); |
| 41 | |
| 42 | const mapped_macho = try selectMachOSlice(all_mapped_memory, arch); |
| 43 | |
| 44 | var r: Io.Reader = .fixed(mapped_macho); |
| 45 | const hdr = r.takeStruct(macho.mach_header_64, .little) catch |err| switch (err) { |
| 46 | error.ReadFailed => unreachable, |
| 47 | error.EndOfStream => return error.InvalidMachO, |
| 48 | }; |
| 49 | |
| 50 | if (hdr.magic != macho.MH_MAGIC_64) |
| 51 | return error.InvalidMachO; |
| 52 | |
| 53 | const symtab: macho.symtab_command, const text_vmaddr: u64, const uuid: ?Uuid = lcs: { |
| 54 | var it: macho.LoadCommandIterator = try .init(&hdr, mapped_macho[@sizeOf(macho.mach_header_64)..]); |
| 55 | var symtab: ?macho.symtab_command = null; |
| 56 | var text_vmaddr: ?u64 = null; |
| 57 | var uuid: ?Uuid = null; |
| 58 | while (try it.next()) |cmd| switch (cmd.hdr.cmd) { |
| 59 | .SYMTAB => symtab = cmd.cast(macho.symtab_command) orelse return error.InvalidMachO, |
| 60 | .SEGMENT_64 => if (cmd.cast(macho.segment_command_64)) |seg_cmd| { |
| 61 | if (!mem.eql(u8, seg_cmd.segName(), "__TEXT")) continue; |
| 62 | text_vmaddr = seg_cmd.vmaddr; |
| 63 | }, |
| 64 | .UUID => if (cmd.cast(macho.uuid_command)) |uuid_cmd| { |
| 65 | uuid = uuid_cmd.uuid; |
| 66 | }, |
| 67 | else => {}, |
| 68 | }; |
| 69 | break :lcs .{ |
| 70 | symtab orelse return error.MissingDebugInfo, |
| 71 | text_vmaddr orelse return error.MissingDebugInfo, |
| 72 | uuid, |
| 73 | }; |
| 74 | }; |
| 75 | |
| 76 | const strings = mapped_macho[symtab.stroff..][0 .. symtab.strsize - 1]; |
| 77 | |
| 78 | var symbols: std.ArrayList(Symbol) = try .initCapacity(gpa, symtab.nsyms); |
| 79 | defer symbols.deinit(gpa); |
| 80 | |
| 81 | // This map is temporary; it is used only to detect duplicates here. This is |
| 82 | // necessary because we prefer to use STAB ("symbolic debugging table") symbols, |
| 83 | // but they might not be present, so we track normal symbols too. |
| 84 | // Indices match 1-1 with those of `symbols`. |
| 85 | var symbol_names: std.array_hash_map.String(void) = .empty; |
| 86 | defer symbol_names.deinit(gpa); |
| 87 | try symbol_names.ensureUnusedCapacity(gpa, symtab.nsyms); |
| 88 | |
| 89 | var ofile: u32 = undefined; |
| 90 | var last_sym: Symbol = undefined; |
| 91 | var state: enum { |
| 92 | init, |
| 93 | oso_open, |
| 94 | oso_close, |
| 95 | bnsym, |
| 96 | fun_strx, |
| 97 | fun_size, |
| 98 | ensym, |
| 99 | } = .init; |
| 100 | |
| 101 | var sym_r: Io.Reader = .fixed(mapped_macho[symtab.symoff..]); |
| 102 | for (0..symtab.nsyms) |_| { |
| 103 | const sym = sym_r.takeStruct(macho.nlist_64, .little) catch |err| switch (err) { |
| 104 | error.ReadFailed => unreachable, |
| 105 | error.EndOfStream => return error.InvalidMachO, |
| 106 | }; |
| 107 | if (sym.n_type.bits.is_stab == 0) { |
| 108 | if (sym.n_strx == 0) continue; |
| 109 | switch (sym.n_type.bits.type) { |
| 110 | .undf, .pbud, .indr, .abs, _ => continue, |
| 111 | .sect => { |
| 112 | const name = std.mem.sliceTo(strings[sym.n_strx..], 0); |
| 113 | const gop = symbol_names.getOrPutAssumeCapacity(name); |
| 114 | if (!gop.found_existing) { |
| 115 | assert(gop.index == symbols.items.len); |
| 116 | symbols.appendAssumeCapacity(.{ |
| 117 | .strx = sym.n_strx, |
| 118 | .addr = sym.n_value, |
| 119 | .ofile = Symbol.unknown_ofile, |
| 120 | }); |
| 121 | } |
| 122 | }, |
| 123 | } |
| 124 | continue; |
| 125 | } |
| 126 | |
| 127 | // TODO handle globals N_GSYM, and statics N_STSYM |
| 128 | // |
| 129 | // NOTE: ld64.lld and Apple's ld differ in STABS layout. |
| 130 | // Apple's ld emit N_BNSYM and N_ENSYM to mark the start and end of |
| 131 | // functions, while ld64.lld doesn't. |
| 132 | switch (sym.n_type.stab) { |
| 133 | .oso => switch (state) { |
| 134 | .init, .oso_close => { |
| 135 | state = .oso_open; |
| 136 | ofile = sym.n_strx; |
| 137 | }, |
| 138 | else => return error.InvalidMachO, |
| 139 | }, |
| 140 | .bnsym => switch (state) { |
| 141 | .oso_open, .ensym => { |
| 142 | state = .bnsym; |
| 143 | last_sym = .{ |
| 144 | .strx = 0, |
| 145 | .addr = sym.n_value, |
| 146 | .ofile = ofile, |
| 147 | }; |
| 148 | }, |
| 149 | else => return error.InvalidMachO, |
| 150 | }, |
| 151 | .fun => switch (state) { |
| 152 | .oso_open => { |
| 153 | state = .fun_strx; |
| 154 | last_sym = .{ |
| 155 | .strx = sym.n_strx, |
| 156 | .addr = sym.n_value, |
| 157 | .ofile = ofile, |
| 158 | }; |
| 159 | }, |
| 160 | .bnsym => { |
| 161 | state = .fun_strx; |
| 162 | last_sym.strx = sym.n_strx; |
| 163 | }, |
| 164 | .fun_strx => { |
| 165 | state = .fun_size; |
| 166 | }, |
| 167 | .fun_size => { |
| 168 | if (last_sym.strx != 0) { |
| 169 | appendStabSymbol(&symbols, &symbol_names, strings, last_sym); |
| 170 | } |
| 171 | last_sym = .{ |
| 172 | .strx = sym.n_strx, |
| 173 | .addr = sym.n_value, |
| 174 | .ofile = ofile, |
| 175 | }; |
| 176 | state = .fun_strx; |
| 177 | }, |
| 178 | else => return error.InvalidMachO, |
| 179 | }, |
| 180 | .ensym => switch (state) { |
| 181 | .fun_size => { |
| 182 | state = .ensym; |
| 183 | if (last_sym.strx != 0) { |
| 184 | appendStabSymbol(&symbols, &symbol_names, strings, last_sym); |
| 185 | } |
| 186 | }, |
| 187 | else => return error.InvalidMachO, |
| 188 | }, |
| 189 | .so => switch (state) { |
| 190 | .init, .oso_close => {}, |
| 191 | .oso_open, .ensym => { |
| 192 | state = .oso_close; |
| 193 | }, |
| 194 | .fun_size => { |
| 195 | state = .oso_close; |
| 196 | if (last_sym.strx != 0) { |
| 197 | appendStabSymbol(&symbols, &symbol_names, strings, last_sym); |
| 198 | } |
| 199 | }, |
| 200 | else => return error.InvalidMachO, |
| 201 | }, |
| 202 | else => {}, |
| 203 | } |
| 204 | } |
| 205 | |
| 206 | switch (state) { |
| 207 | .init => { |
| 208 | // Missing STAB symtab entries is still okay, unless there were also no normal symbols. |
| 209 | if (symbols.items.len == 0) return error.MissingDebugInfo; |
| 210 | }, |
| 211 | .oso_close => {}, |
| 212 | else => return error.InvalidMachO, // corrupted STAB entries in symtab |
| 213 | } |
| 214 | |
| 215 | const symbols_slice = try symbols.toOwnedSlice(gpa); |
| 216 | errdefer gpa.free(symbols_slice); |
| 217 | |
| 218 | // Even though lld emits symbols in ascending order, this debug code |
| 219 | // should work for programs linked in any valid way. |
| 220 | // This sort is so that we can binary search later. |
| 221 | mem.sort(Symbol, symbols_slice, {}, Symbol.addressLessThan); |
| 222 | |
| 223 | const adjacent_dsym = if (uuid) |expected_uuid| |
| 224 | try loadAdjacentDsym(gpa, io, path, arch, expected_uuid) |
| 225 | else |
| 226 | null; |
| 227 | |
| 228 | return .{ |
| 229 | .mapped_memory = all_mapped_memory, |
| 230 | .symbols = symbols_slice, |
| 231 | .strings = strings, |
| 232 | .ofiles = .empty, |
| 233 | .text_vmaddr = text_vmaddr, |
| 234 | .uuid = uuid, |
| 235 | .adjacent_dsym = adjacent_dsym, |
| 236 | }; |
| 237 | } |
| 238 | |
| 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 | |
| 244 | const symbol = Symbol.find(mf.symbols, vaddr) orelse return error.MissingDebugInfo; |
| 245 | |
| 246 | if (symbol.ofile == Symbol.unknown_ofile) return error.MissingDebugInfo; |
| 247 | |
| 248 | // offset of `address` from start of `symbol` |
| 249 | const address_symbol_offset = vaddr - symbol.addr; |
| 250 | |
| 251 | // Take the symbol name from the N_FUN STAB entry, we're going to |
| 252 | // use it if we fail to find the DWARF infos |
| 253 | const stab_symbol = mem.sliceTo(mf.strings[symbol.strx..], 0); |
| 254 | |
| 255 | const gop = try mf.ofiles.getOrPut(gpa, symbol.ofile); |
| 256 | if (!gop.found_existing) { |
| 257 | const name = mem.sliceTo(mf.strings[symbol.ofile..], 0); |
| 258 | gop.value_ptr.* = loadOFile(gpa, io, name); |
| 259 | } |
| 260 | const of = &(gop.value_ptr.* catch |err| return err); |
| 261 | |
| 262 | const symbol_index = of.symbols_by_name.getKeyAdapted( |
| 263 | @as([]const u8, stab_symbol), |
| 264 | @as(OFile.SymbolAdapter, .{ .strtab = of.strtab, .symtab_raw = of.symtab_raw }), |
| 265 | ) orelse return error.MissingDebugInfo; |
| 266 | |
| 267 | const symbol_ofile_vaddr = vaddr: { |
| 268 | var sym = of.symtab_raw[symbol_index]; |
| 269 | if (builtin.cpu.arch.endian() != .little) std.mem.byteSwapAllFields(macho.nlist_64, &sym); |
| 270 | break :vaddr sym.n_value; |
| 271 | }; |
| 272 | |
| 273 | return .{ &of.dwarf, symbol_ofile_vaddr + address_symbol_offset }; |
| 274 | } |
| 275 | pub fn lookupSymbolName(mf: *MachOFile, vaddr: u64) error{MissingDebugInfo}![]const u8 { |
| 276 | const symbol = Symbol.find(mf.symbols, vaddr) orelse return error.MissingDebugInfo; |
| 277 | return mem.sliceTo(mf.strings[symbol.strx..], 0); |
| 278 | } |
| 279 | |
| 280 | const OFile = struct { |
| 281 | mapped_memory: []align(std.heap.page_size_min) const u8, |
| 282 | dwarf: Dwarf, |
| 283 | strtab: []const u8, |
| 284 | symtab_raw: []align(1) const macho.nlist_64, |
| 285 | /// All named symbols in `symtab_raw`. Stored `u32` key is the index into `symtab_raw`. Accessed |
| 286 | /// through `SymbolAdapter`, so that the symbol name is used as the logical key. |
| 287 | symbols_by_name: std.array_hash_map.Custom(u32, void, void, true), |
| 288 | |
| 289 | const SymbolAdapter = struct { |
| 290 | strtab: []const u8, |
| 291 | symtab_raw: []align(1) const macho.nlist_64, |
| 292 | pub fn hash(ctx: SymbolAdapter, sym_name: []const u8) u32 { |
| 293 | _ = ctx; |
| 294 | return @truncate(std.hash.Wyhash.hash(0, sym_name)); |
| 295 | } |
| 296 | pub fn eql(ctx: SymbolAdapter, a_sym_name: []const u8, b_sym_index: u32, b_index: usize) bool { |
| 297 | _ = b_index; |
| 298 | var b_sym = ctx.symtab_raw[b_sym_index]; |
| 299 | if (builtin.cpu.arch.endian() != .little) std.mem.byteSwapAllFields(macho.nlist_64, &b_sym); |
| 300 | const b_sym_name = std.mem.sliceTo(ctx.strtab[b_sym.n_strx..], 0); |
| 301 | return mem.eql(u8, a_sym_name, b_sym_name); |
| 302 | } |
| 303 | }; |
| 304 | }; |
| 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 | |
| 316 | const Symbol = struct { |
| 317 | strx: u32, |
| 318 | addr: u64, |
| 319 | /// Value may be `unknown_ofile`. |
| 320 | ofile: u32, |
| 321 | const unknown_ofile = std.math.maxInt(u32); |
| 322 | fn addressLessThan(context: void, lhs: Symbol, rhs: Symbol) bool { |
| 323 | _ = context; |
| 324 | return lhs.addr < rhs.addr; |
| 325 | } |
| 326 | /// Assumes that `symbols` is sorted in order of ascending `addr`. |
| 327 | fn find(symbols: []const Symbol, address: usize) ?*const Symbol { |
| 328 | if (symbols.len == 0) return null; // no potential match |
| 329 | if (address < symbols[0].addr) return null; // address is before the lowest-address symbol |
| 330 | var left: usize = 0; |
| 331 | var len: usize = symbols.len; |
| 332 | while (len > 1) { |
| 333 | const mid = left + len / 2; |
| 334 | if (address < symbols[mid].addr) { |
| 335 | len /= 2; |
| 336 | } else { |
| 337 | left = mid; |
| 338 | len -= len / 2; |
| 339 | } |
| 340 | } |
| 341 | return &symbols[left]; |
| 342 | } |
| 343 | |
| 344 | test find { |
| 345 | const symbols: []const Symbol = &.{ |
| 346 | .{ .addr = 100, .strx = undefined, .ofile = undefined }, |
| 347 | .{ .addr = 200, .strx = undefined, .ofile = undefined }, |
| 348 | .{ .addr = 300, .strx = undefined, .ofile = undefined }, |
| 349 | }; |
| 350 | |
| 351 | try testing.expectEqual(null, find(symbols, 0)); |
| 352 | try testing.expectEqual(null, find(symbols, 99)); |
| 353 | try testing.expectEqual(&symbols[0], find(symbols, 100).?); |
| 354 | try testing.expectEqual(&symbols[0], find(symbols, 150).?); |
| 355 | try testing.expectEqual(&symbols[0], find(symbols, 199).?); |
| 356 | |
| 357 | try testing.expectEqual(&symbols[1], find(symbols, 200).?); |
| 358 | try testing.expectEqual(&symbols[1], find(symbols, 250).?); |
| 359 | try testing.expectEqual(&symbols[1], find(symbols, 299).?); |
| 360 | |
| 361 | try testing.expectEqual(&symbols[2], find(symbols, 300).?); |
| 362 | try testing.expectEqual(&symbols[2], find(symbols, 301).?); |
| 363 | try testing.expectEqual(&symbols[2], find(symbols, 5000).?); |
| 364 | } |
| 365 | }; |
| 366 | test { |
| 367 | _ = Symbol; |
| 368 | } |
| 369 | |
| 370 | fn appendStabSymbol( |
| 371 | symbols: *std.ArrayList(Symbol), |
| 372 | symbol_names: *std.array_hash_map.String(void), |
| 373 | strings: []const u8, |
| 374 | last_sym: Symbol, |
| 375 | ) void { |
| 376 | const name = std.mem.sliceTo(strings[last_sym.strx..], 0); |
| 377 | const gop = symbol_names.getOrPutAssumeCapacity(name); |
| 378 | if (!gop.found_existing) { |
| 379 | assert(gop.index == symbols.items.len); |
| 380 | symbols.appendAssumeCapacity(last_sym); |
| 381 | } else { |
| 382 | symbols.items[gop.index] = last_sym; |
| 383 | } |
| 384 | } |
| 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 | |
| 454 | fn loadOFile(gpa: Allocator, io: Io, o_file_name: []const u8) !OFile { |
| 455 | const all_mapped_memory, const mapped_ofile = map: { |
| 456 | const open_paren = paren: { |
| 457 | if (std.mem.endsWith(u8, o_file_name, ")")) { |
| 458 | if (std.mem.findScalarLast(u8, o_file_name, '(')) |i| { |
| 459 | break :paren i; |
| 460 | } |
| 461 | } |
| 462 | // Not an archive, just a normal path to a .o file |
| 463 | const m = try mapDebugInfoFile(io, o_file_name); |
| 464 | break :map .{ m, m }; |
| 465 | }; |
| 466 | |
| 467 | // We have the form 'path/to/archive.a(entry.o)'. Map the archive and find the object file in question. |
| 468 | |
| 469 | const archive_path = o_file_name[0..open_paren]; |
| 470 | const target_name_in_archive = o_file_name[open_paren + 1 .. o_file_name.len - 1]; |
| 471 | const mapped_archive = try mapDebugInfoFile(io, archive_path); |
| 472 | errdefer posix.munmap(mapped_archive); |
| 473 | |
| 474 | var ar_reader: Io.Reader = .fixed(mapped_archive); |
| 475 | const ar_magic = ar_reader.take(8) catch return error.InvalidMachO; |
| 476 | if (!std.mem.eql(u8, ar_magic, "!<arch>\n")) return error.InvalidMachO; |
| 477 | while (true) { |
| 478 | if (ar_reader.seek == ar_reader.buffer.len) return error.MissingDebugInfo; |
| 479 | |
| 480 | const raw_name = ar_reader.takeArray(16) catch return error.InvalidMachO; |
| 481 | ar_reader.discardAll(12 + 6 + 6 + 8) catch return error.InvalidMachO; |
| 482 | const raw_size = ar_reader.takeArray(10) catch return error.InvalidMachO; |
| 483 | const file_magic = ar_reader.takeArray(2) catch return error.InvalidMachO; |
| 484 | if (!std.mem.eql(u8, file_magic, "`\n")) return error.InvalidMachO; |
| 485 | |
| 486 | const size = std.fmt.parseInt(u32, mem.sliceTo(raw_size, ' '), 10) catch return error.InvalidMachO; |
| 487 | const raw_data = ar_reader.take(size) catch return error.InvalidMachO; |
| 488 | |
| 489 | const entry_name: []const u8, const entry_contents: []const u8 = entry: { |
| 490 | if (!std.mem.startsWith(u8, raw_name, "#1/")) { |
| 491 | break :entry .{ mem.sliceTo(raw_name, '/'), raw_data }; |
| 492 | } |
| 493 | const len = std.fmt.parseInt(u32, mem.sliceTo(raw_name[3..], ' '), 10) catch return error.InvalidMachO; |
| 494 | if (len > size) return error.InvalidMachO; |
| 495 | break :entry .{ mem.sliceTo(raw_data[0..len], 0), raw_data[len..] }; |
| 496 | }; |
| 497 | |
| 498 | if (std.mem.eql(u8, entry_name, target_name_in_archive)) { |
| 499 | break :map .{ mapped_archive, entry_contents }; |
| 500 | } |
| 501 | } |
| 502 | }; |
| 503 | errdefer posix.munmap(all_mapped_memory); |
| 504 | |
| 505 | var r: Io.Reader = .fixed(mapped_ofile); |
| 506 | const hdr = r.takeStruct(macho.mach_header_64, .little) catch |err| switch (err) { |
| 507 | error.ReadFailed => unreachable, |
| 508 | error.EndOfStream => return error.InvalidMachO, |
| 509 | }; |
| 510 | if (hdr.magic != std.macho.MH_MAGIC_64) return error.InvalidMachO; |
| 511 | |
| 512 | const seg_cmd: macho.LoadCommandIterator.LoadCommand, const symtab_cmd: macho.symtab_command = cmds: { |
| 513 | var seg_cmd: ?macho.LoadCommandIterator.LoadCommand = null; |
| 514 | var symtab_cmd: ?macho.symtab_command = null; |
| 515 | var it: macho.LoadCommandIterator = try .init(&hdr, mapped_ofile[@sizeOf(macho.mach_header_64)..]); |
| 516 | while (try it.next()) |lc| switch (lc.hdr.cmd) { |
| 517 | .SEGMENT_64 => seg_cmd = lc, |
| 518 | .SYMTAB => symtab_cmd = lc.cast(macho.symtab_command) orelse return error.InvalidMachO, |
| 519 | else => {}, |
| 520 | }; |
| 521 | break :cmds .{ |
| 522 | seg_cmd orelse return error.MissingDebugInfo, |
| 523 | symtab_cmd orelse return error.MissingDebugInfo, |
| 524 | }; |
| 525 | }; |
| 526 | |
| 527 | if (mapped_ofile.len < symtab_cmd.stroff + symtab_cmd.strsize) return error.InvalidMachO; |
| 528 | if (mapped_ofile[symtab_cmd.stroff + symtab_cmd.strsize - 1] != 0) return error.InvalidMachO; |
| 529 | const strtab = mapped_ofile[symtab_cmd.stroff..][0 .. symtab_cmd.strsize - 1]; |
| 530 | |
| 531 | const n_sym_bytes = symtab_cmd.nsyms * @sizeOf(macho.nlist_64); |
| 532 | if (mapped_ofile.len < symtab_cmd.symoff + n_sym_bytes) return error.InvalidMachO; |
| 533 | const symtab_raw: []align(1) const macho.nlist_64 = @ptrCast(mapped_ofile[symtab_cmd.symoff..][0..n_sym_bytes]); |
| 534 | |
| 535 | // TODO handle tentative (common) symbols |
| 536 | var symbols_by_name: std.array_hash_map.Custom(u32, void, void, true) = .empty; |
| 537 | defer symbols_by_name.deinit(gpa); |
| 538 | try symbols_by_name.ensureUnusedCapacity(gpa, @intCast(symtab_raw.len)); |
| 539 | for (symtab_raw, 0..) |sym_raw, sym_index| { |
| 540 | var sym = sym_raw; |
| 541 | if (builtin.cpu.arch.endian() != .little) std.mem.byteSwapAllFields(macho.nlist_64, &sym); |
| 542 | if (sym.n_strx == 0) continue; |
| 543 | switch (sym.n_type.bits.type) { |
| 544 | .undf => continue, // includes tentative symbols |
| 545 | .abs => continue, |
| 546 | else => {}, |
| 547 | } |
| 548 | const sym_name = mem.sliceTo(strtab[sym.n_strx..], 0); |
| 549 | const gop = symbols_by_name.getOrPutAssumeCapacityAdapted( |
| 550 | @as([]const u8, sym_name), |
| 551 | @as(OFile.SymbolAdapter, .{ .strtab = strtab, .symtab_raw = symtab_raw }), |
| 552 | ); |
| 553 | if (gop.found_existing) return error.InvalidMachO; |
| 554 | gop.key_ptr.* = @intCast(sym_index); |
| 555 | } |
| 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 { |
| 573 | var sections: Dwarf.SectionArray = @splat(null); |
| 574 | for (section_headers) |sect_raw| { |
| 575 | var sect = sect_raw; |
| 576 | if (builtin.cpu.arch.endian() != .little) std.mem.byteSwapAllFields(macho.section_64, &sect); |
| 577 | |
| 578 | if (!std.mem.eql(u8, "__DWARF", sect.segName())) continue; |
| 579 | |
| 580 | const section_index: usize = inline for (@typeInfo(Dwarf.Section.Id).@"enum".field_names, 0..) |field_name, i| { |
| 581 | const section_name_long = "__" ++ field_name; |
| 582 | // Some dwarf section names don't fit in the `sectname` buffer, so they are truncated. |
| 583 | const section_name_trunc = section_name_long[0..@min(section_name_long.len, sect.sectname.len)]; |
| 584 | if (mem.eql(u8, section_name_trunc, sect.sectName())) break i; |
| 585 | } else continue; |
| 586 | |
| 587 | if (mapped_macho.len < sect.offset + sect.size) return error.InvalidMachO; |
| 588 | const section_bytes = mapped_macho[sect.offset..][0..sect.size]; |
| 589 | sections[section_index] = .{ |
| 590 | .data = section_bytes, |
| 591 | .owned = false, |
| 592 | }; |
| 593 | } |
| 594 | |
| 595 | if (sections[@backingInt(Dwarf.Section.Id.debug_info)] == null or |
| 596 | sections[@backingInt(Dwarf.Section.Id.debug_abbrev)] == null or |
| 597 | sections[@backingInt(Dwarf.Section.Id.debug_str)] == null or |
| 598 | sections[@backingInt(Dwarf.Section.Id.debug_line)] == null) |
| 599 | { |
| 600 | return error.MissingDebugInfo; |
| 601 | } |
| 602 | |
| 603 | var dwarf: Dwarf = .{ .sections = sections }; |
| 604 | errdefer dwarf.deinit(gpa); |
| 605 | dwarf.open(gpa, .little) catch |err| switch (err) { |
| 606 | error.InvalidDebugInfo, |
| 607 | error.EndOfStream, |
| 608 | error.Overflow, |
| 609 | error.StreamTooLong, |
| 610 | => return error.InvalidDwarf, |
| 611 | |
| 612 | error.MissingDebugInfo, |
| 613 | error.ReadFailed, |
| 614 | error.OutOfMemory, |
| 615 | => |e| return e, |
| 616 | }; |
| 617 | |
| 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, |
| 666 | }; |
| 667 | return mapped_macho; |
| 668 | } |
| 669 | |
| 670 | /// Uses `mmap` to map the file at `path` into memory. |
| 671 | fn mapDebugInfoFile(io: Io, path: []const u8) ![]align(std.heap.page_size_min) const u8 { |
| 672 | const file = Io.Dir.cwd().openFile(io, path, .{}) catch |err| switch (err) { |
| 673 | error.FileNotFound => return error.MissingDebugInfo, |
| 674 | else => return error.ReadFailed, |
| 675 | }; |
| 676 | defer file.close(io); |
| 677 | |
| 678 | const file_len = std.math.cast( |
| 679 | usize, |
| 680 | file.length(io) catch return error.ReadFailed, |
| 681 | ) orelse return error.ReadFailed; |
| 682 | |
| 683 | return posix.mmap( |
| 684 | null, |
| 685 | file_len, |
| 686 | .{ .READ = true }, |
| 687 | .{ .TYPE = .SHARED }, |
| 688 | file.handle, |
| 689 | 0, |
| 690 | ) catch return error.ReadFailed; |
| 691 | } |
| 692 | |
| 693 | const std = @import("std"); |
| 694 | const Allocator = std.mem.Allocator; |
| 695 | const Dwarf = std.debug.Dwarf; |
| 696 | const Io = std.Io; |
| 697 | const assert = std.debug.assert; |
| 698 | const posix = std.posix; |
| 699 | const macho = std.macho; |
| 700 | const mem = std.mem; |
| 701 | const testing = std.testing; |
| 702 | |
| 703 | const builtin = @import("builtin"); |
| 704 | |
| 705 | const Uuid = @FieldType(macho.uuid_command, "uuid"); |
| 706 | const MachOFile = @This(); |