authorgravatar for alex@alexrp.comAlex Rønne Petersen <alex@alexrp.com> 2025-10-01 15:34:32+02:00
committergravatar for alex@alexrp.comAlex Rønne Petersen <alex@alexrp.com> 2025-10-01 23:47:47+02:00
log771410cbf231e1a2b20e25f9d70c13d4ffeace9c
treed9c08d535aca1250739c91c5f82f2ce9e411570e
parente1fb662f600a7e134661d3c46d12e7ead83dd799
signaturebadge-check Signed by SSH key SHA256:7B/LJ7bpR1eX8aCXSr4mtd5M45VMPKcx9zY8e95b5QM

std.debug.SelfInfo: rename Darwin to MachO


3 files changed, 994 insertions(+), 994 deletions(-)

lib/std/debug.zig+1-1
...@@ -64,7 +64,7 @@ pub const SelfInfo = if (@hasDecl(root, "debug") and @hasDecl(root.debug, "SelfI...@@ -64,7 +64,7 @@ pub const SelfInfo = if (@hasDecl(root, "debug") and @hasDecl(root.debug, "SelfI
64else switch (std.Target.ObjectFormat.default(native_os, native_arch)) {64else switch (std.Target.ObjectFormat.default(native_os, native_arch)) {
65 .coff => if (native_os == .windows) @import("debug/SelfInfo/Windows.zig") else void,65 .coff => if (native_os == .windows) @import("debug/SelfInfo/Windows.zig") else void,
66 .elf => @import("debug/SelfInfo/Elf.zig"),66 .elf => @import("debug/SelfInfo/Elf.zig"),
67 .macho => @import("debug/SelfInfo/Darwin.zig"),67 .macho => @import("debug/SelfInfo/MachO.zig"),
68 .goff, .plan9, .spirv, .wasm, .xcoff => void,68 .goff, .plan9, .spirv, .wasm, .xcoff => void,
69 .c, .hex, .raw => unreachable,69 .c, .hex, .raw => unreachable,
70};70};
lib/std/debug/SelfInfo/Darwin.zig deleted-993
...@@ -1,993 +0,0 @@
1mutex: std.Thread.Mutex,
2/// Accessed through `Module.Adapter`.
3modules: std.ArrayHashMapUnmanaged(Module, void, Module.Context, false),
4ofiles: std.StringArrayHashMapUnmanaged(?OFile),
5
6pub const init: SelfInfo = .{
7 .mutex = .{},
8 .modules = .empty,
9 .ofiles = .empty,
10};
11pub fn deinit(si: *SelfInfo, gpa: Allocator) void {
12 for (si.modules.keys()) |*module| {
13 unwind: {
14 const u = &(module.unwind orelse break :unwind catch break :unwind);
15 if (u.dwarf) |*dwarf| dwarf.deinit(gpa);
16 }
17 loaded: {
18 const l = &(module.loaded_macho orelse break :loaded catch break :loaded);
19 gpa.free(l.symbols);
20 posix.munmap(l.mapped_memory);
21 }
22 }
23 for (si.ofiles.values()) |*opt_ofile| {
24 const ofile = &(opt_ofile.* orelse continue);
25 ofile.dwarf.deinit(gpa);
26 ofile.symbols_by_name.deinit(gpa);
27 posix.munmap(ofile.mapped_memory);
28 }
29 si.modules.deinit(gpa);
30 si.ofiles.deinit(gpa);
31}
32
33pub fn getSymbol(si: *SelfInfo, gpa: Allocator, address: usize) Error!std.debug.Symbol {
34 const module = try si.findModule(gpa, address);
35 defer si.mutex.unlock();
36
37 const loaded_macho = try module.getLoadedMachO(gpa);
38
39 const vaddr = address - loaded_macho.vaddr_offset;
40 const symbol = MachoSymbol.find(loaded_macho.symbols, vaddr) orelse return .unknown;
41
42 // offset of `address` from start of `symbol`
43 const address_symbol_offset = vaddr - symbol.addr;
44
45 // Take the symbol name from the N_FUN STAB entry, we're going to
46 // use it if we fail to find the DWARF infos
47 const stab_symbol = mem.sliceTo(loaded_macho.strings[symbol.strx..], 0);
48
49 // If any information is missing, we can at least return this from now on.
50 const sym_only_result: std.debug.Symbol = .{
51 .name = stab_symbol,
52 .compile_unit_name = null,
53 .source_location = null,
54 };
55
56 if (symbol.ofile == MachoSymbol.unknown_ofile) {
57 // We don't have STAB info, so can't track down the object file; all we can do is the symbol name.
58 return sym_only_result;
59 }
60
61 const o_file: *OFile = of: {
62 const path = mem.sliceTo(loaded_macho.strings[symbol.ofile..], 0);
63 const gop = try si.ofiles.getOrPut(gpa, path);
64 if (!gop.found_existing) {
65 gop.value_ptr.* = loadOFile(gpa, path) catch null;
66 }
67 if (gop.value_ptr.*) |*o_file| {
68 break :of o_file;
69 } else {
70 return sym_only_result;
71 }
72 };
73
74 const symbol_index = o_file.symbols_by_name.getKeyAdapted(
75 @as([]const u8, stab_symbol),
76 @as(OFile.SymbolAdapter, .{ .strtab = o_file.strtab, .symtab = o_file.symtab }),
77 ) orelse return sym_only_result;
78 const symbol_ofile_vaddr = o_file.symtab[symbol_index].n_value;
79
80 const compile_unit = o_file.dwarf.findCompileUnit(native_endian, symbol_ofile_vaddr) catch return sym_only_result;
81
82 return .{
83 .name = o_file.dwarf.getSymbolName(symbol_ofile_vaddr + address_symbol_offset) orelse stab_symbol,
84 .compile_unit_name = compile_unit.die.getAttrString(
85 &o_file.dwarf,
86 native_endian,
87 std.dwarf.AT.name,
88 o_file.dwarf.section(.debug_str),
89 compile_unit,
90 ) catch |err| switch (err) {
91 error.MissingDebugInfo, error.InvalidDebugInfo => null,
92 },
93 .source_location = o_file.dwarf.getLineNumberInfo(
94 gpa,
95 native_endian,
96 compile_unit,
97 symbol_ofile_vaddr + address_symbol_offset,
98 ) catch null,
99 };
100}
101pub fn getModuleName(si: *SelfInfo, gpa: Allocator, address: usize) Error![]const u8 {
102 const module = try si.findModule(gpa, address);
103 defer si.mutex.unlock();
104 return module.name;
105}
106
107pub const can_unwind: bool = true;
108pub const UnwindContext = std.debug.Dwarf.SelfUnwinder;
109/// Unwind a frame using MachO compact unwind info (from `__unwind_info`).
110/// If the compact encoding can't encode a way to unwind a frame, it will
111/// defer unwinding to DWARF, in which case `__eh_frame` will be used if available.
112pub fn unwindFrame(si: *SelfInfo, gpa: Allocator, context: *UnwindContext) Error!usize {
113 return unwindFrameInner(si, gpa, context) catch |err| switch (err) {
114 error.InvalidDebugInfo,
115 error.MissingDebugInfo,
116 error.UnsupportedDebugInfo,
117 error.ReadFailed,
118 error.OutOfMemory,
119 error.Unexpected,
120 => |e| return e,
121 error.UnsupportedRegister,
122 error.UnsupportedAddrSize,
123 error.UnimplementedUserOpcode,
124 => return error.UnsupportedDebugInfo,
125 error.Overflow,
126 error.EndOfStream,
127 error.StreamTooLong,
128 error.InvalidOpcode,
129 error.InvalidOperation,
130 error.InvalidOperand,
131 error.InvalidRegister,
132 error.IncompatibleRegisterSize,
133 => return error.InvalidDebugInfo,
134 };
135}
136fn unwindFrameInner(si: *SelfInfo, gpa: Allocator, context: *UnwindContext) !usize {
137 const module = try si.findModule(gpa, context.pc);
138 defer si.mutex.unlock();
139
140 const unwind: *Module.Unwind = try module.getUnwindInfo(gpa);
141
142 const ip_reg_num = comptime Dwarf.ipRegNum(builtin.target.cpu.arch).?;
143 const fp_reg_num = comptime Dwarf.fpRegNum(builtin.target.cpu.arch);
144 const sp_reg_num = comptime Dwarf.spRegNum(builtin.target.cpu.arch);
145
146 const unwind_info = unwind.unwind_info orelse return error.MissingDebugInfo;
147 if (unwind_info.len < @sizeOf(macho.unwind_info_section_header)) return error.InvalidDebugInfo;
148 const header: *align(1) const macho.unwind_info_section_header = @ptrCast(unwind_info);
149
150 const index_byte_count = header.indexCount * @sizeOf(macho.unwind_info_section_header_index_entry);
151 if (unwind_info.len < header.indexSectionOffset + index_byte_count) return error.InvalidDebugInfo;
152 const indices: []align(1) const macho.unwind_info_section_header_index_entry = @ptrCast(unwind_info[header.indexSectionOffset..][0..index_byte_count]);
153 if (indices.len == 0) return error.MissingDebugInfo;
154
155 // offset of the PC into the `__TEXT` segment
156 const pc_text_offset = context.pc - module.text_base;
157
158 const start_offset: u32, const first_level_offset: u32 = index: {
159 var left: usize = 0;
160 var len: usize = indices.len;
161 while (len > 1) {
162 const mid = left + len / 2;
163 if (pc_text_offset < indices[mid].functionOffset) {
164 len /= 2;
165 } else {
166 left = mid;
167 len -= len / 2;
168 }
169 }
170 break :index .{ indices[left].secondLevelPagesSectionOffset, indices[left].functionOffset };
171 };
172 // An offset of 0 is a sentinel indicating a range does not have unwind info.
173 if (start_offset == 0) return error.MissingDebugInfo;
174
175 const common_encodings_byte_count = header.commonEncodingsArrayCount * @sizeOf(macho.compact_unwind_encoding_t);
176 if (unwind_info.len < header.commonEncodingsArraySectionOffset + common_encodings_byte_count) return error.InvalidDebugInfo;
177 const common_encodings: []align(1) const macho.compact_unwind_encoding_t = @ptrCast(
178 unwind_info[header.commonEncodingsArraySectionOffset..][0..common_encodings_byte_count],
179 );
180
181 if (unwind_info.len < start_offset + @sizeOf(macho.UNWIND_SECOND_LEVEL)) return error.InvalidDebugInfo;
182 const kind: *align(1) const macho.UNWIND_SECOND_LEVEL = @ptrCast(unwind_info[start_offset..]);
183
184 const entry: struct {
185 function_offset: usize,
186 raw_encoding: u32,
187 } = switch (kind.*) {
188 .REGULAR => entry: {
189 if (unwind_info.len < start_offset + @sizeOf(macho.unwind_info_regular_second_level_page_header)) return error.InvalidDebugInfo;
190 const page_header: *align(1) const macho.unwind_info_regular_second_level_page_header = @ptrCast(unwind_info[start_offset..]);
191
192 const entries_byte_count = page_header.entryCount * @sizeOf(macho.unwind_info_regular_second_level_entry);
193 if (unwind_info.len < start_offset + entries_byte_count) return error.InvalidDebugInfo;
194 const entries: []align(1) const macho.unwind_info_regular_second_level_entry = @ptrCast(
195 unwind_info[start_offset + page_header.entryPageOffset ..][0..entries_byte_count],
196 );
197 if (entries.len == 0) return error.InvalidDebugInfo;
198
199 var left: usize = 0;
200 var len: usize = entries.len;
201 while (len > 1) {
202 const mid = left + len / 2;
203 if (pc_text_offset < entries[mid].functionOffset) {
204 len /= 2;
205 } else {
206 left = mid;
207 len -= len / 2;
208 }
209 }
210 break :entry .{
211 .function_offset = entries[left].functionOffset,
212 .raw_encoding = entries[left].encoding,
213 };
214 },
215 .COMPRESSED => entry: {
216 if (unwind_info.len < start_offset + @sizeOf(macho.unwind_info_compressed_second_level_page_header)) return error.InvalidDebugInfo;
217 const page_header: *align(1) const macho.unwind_info_compressed_second_level_page_header = @ptrCast(unwind_info[start_offset..]);
218
219 const entries_byte_count = page_header.entryCount * @sizeOf(macho.UnwindInfoCompressedEntry);
220 if (unwind_info.len < start_offset + entries_byte_count) return error.InvalidDebugInfo;
221 const entries: []align(1) const macho.UnwindInfoCompressedEntry = @ptrCast(
222 unwind_info[start_offset + page_header.entryPageOffset ..][0..entries_byte_count],
223 );
224 if (entries.len == 0) return error.InvalidDebugInfo;
225
226 var left: usize = 0;
227 var len: usize = entries.len;
228 while (len > 1) {
229 const mid = left + len / 2;
230 if (pc_text_offset < first_level_offset + entries[mid].funcOffset) {
231 len /= 2;
232 } else {
233 left = mid;
234 len -= len / 2;
235 }
236 }
237 const entry = entries[left];
238
239 const function_offset = first_level_offset + entry.funcOffset;
240 if (entry.encodingIndex < common_encodings.len) {
241 break :entry .{
242 .function_offset = function_offset,
243 .raw_encoding = common_encodings[entry.encodingIndex],
244 };
245 }
246
247 const local_index = entry.encodingIndex - common_encodings.len;
248 const local_encodings_byte_count = page_header.encodingsCount * @sizeOf(macho.compact_unwind_encoding_t);
249 if (unwind_info.len < start_offset + page_header.encodingsPageOffset + local_encodings_byte_count) return error.InvalidDebugInfo;
250 const local_encodings: []align(1) const macho.compact_unwind_encoding_t = @ptrCast(
251 unwind_info[start_offset + page_header.encodingsPageOffset ..][0..local_encodings_byte_count],
252 );
253 if (local_index >= local_encodings.len) return error.InvalidDebugInfo;
254 break :entry .{
255 .function_offset = function_offset,
256 .raw_encoding = local_encodings[local_index],
257 };
258 },
259 else => return error.InvalidDebugInfo,
260 };
261
262 if (entry.raw_encoding == 0) return error.MissingDebugInfo;
263
264 const encoding: macho.CompactUnwindEncoding = @bitCast(entry.raw_encoding);
265 const new_ip = switch (builtin.cpu.arch) {
266 .x86_64 => switch (encoding.mode.x86_64) {
267 .OLD => return error.UnsupportedDebugInfo,
268 .RBP_FRAME => ip: {
269 const frame = encoding.value.x86_64.frame;
270
271 const fp = (try dwarfRegNative(&context.cpu_state, fp_reg_num)).*;
272 const new_sp = fp + 2 * @sizeOf(usize);
273
274 const ip_ptr = fp + @sizeOf(usize);
275 const new_ip = @as(*const usize, @ptrFromInt(ip_ptr)).*;
276 const new_fp = @as(*const usize, @ptrFromInt(fp)).*;
277
278 (try dwarfRegNative(&context.cpu_state, fp_reg_num)).* = new_fp;
279 (try dwarfRegNative(&context.cpu_state, sp_reg_num)).* = new_sp;
280 (try dwarfRegNative(&context.cpu_state, ip_reg_num)).* = new_ip;
281
282 const regs: [5]u3 = .{
283 frame.reg0,
284 frame.reg1,
285 frame.reg2,
286 frame.reg3,
287 frame.reg4,
288 };
289 for (regs, 0..) |reg, i| {
290 if (reg == 0) continue;
291 const addr = fp - frame.frame_offset * @sizeOf(usize) + i * @sizeOf(usize);
292 const reg_number = try Dwarf.compactUnwindToDwarfRegNumber(reg);
293 (try dwarfRegNative(&context.cpu_state, reg_number)).* = @as(*const usize, @ptrFromInt(addr)).*;
294 }
295
296 break :ip new_ip;
297 },
298 .STACK_IMMD,
299 .STACK_IND,
300 => ip: {
301 const frameless = encoding.value.x86_64.frameless;
302
303 const sp = (try dwarfRegNative(&context.cpu_state, sp_reg_num)).*;
304 const stack_size: usize = stack_size: {
305 if (encoding.mode.x86_64 == .STACK_IMMD) {
306 break :stack_size @as(usize, frameless.stack.direct.stack_size) * @sizeOf(usize);
307 }
308 // In .STACK_IND, the stack size is inferred from the subq instruction at the beginning of the function.
309 const sub_offset_addr =
310 module.text_base +
311 entry.function_offset +
312 frameless.stack.indirect.sub_offset;
313 // `sub_offset_addr` points to the offset of the literal within the instruction
314 const sub_operand = @as(*align(1) const u32, @ptrFromInt(sub_offset_addr)).*;
315 break :stack_size sub_operand + @sizeOf(usize) * @as(usize, frameless.stack.indirect.stack_adjust);
316 };
317
318 // Decode the Lehmer-coded sequence of registers.
319 // For a description of the encoding see lib/libc/include/any-macos.13-any/mach-o/compact_unwind_encoding.h
320
321 // Decode the variable-based permutation number into its digits. Each digit represents
322 // an index into the list of register numbers that weren't yet used in the sequence at
323 // the time the digit was added.
324 const reg_count = frameless.stack_reg_count;
325 const ip_ptr = ip_ptr: {
326 var digits: [6]u3 = undefined;
327 var accumulator: usize = frameless.stack_reg_permutation;
328 var base: usize = 2;
329 for (0..reg_count) |i| {
330 const div = accumulator / base;
331 digits[digits.len - 1 - i] = @intCast(accumulator - base * div);
332 accumulator = div;
333 base += 1;
334 }
335
336 var registers: [6]u3 = undefined;
337 var used_indices: [6]bool = @splat(false);
338 for (digits[digits.len - reg_count ..], 0..) |target_unused_index, i| {
339 var unused_count: u8 = 0;
340 const unused_index = for (used_indices, 0..) |used, index| {
341 if (!used) {
342 if (target_unused_index == unused_count) break index;
343 unused_count += 1;
344 }
345 } else unreachable;
346 registers[i] = @intCast(unused_index + 1);
347 used_indices[unused_index] = true;
348 }
349
350 var reg_addr = sp + stack_size - @sizeOf(usize) * @as(usize, reg_count + 1);
351 for (0..reg_count) |i| {
352 const reg_number = try Dwarf.compactUnwindToDwarfRegNumber(registers[i]);
353 (try dwarfRegNative(&context.cpu_state, reg_number)).* = @as(*const usize, @ptrFromInt(reg_addr)).*;
354 reg_addr += @sizeOf(usize);
355 }
356
357 break :ip_ptr reg_addr;
358 };
359
360 const new_ip = @as(*const usize, @ptrFromInt(ip_ptr)).*;
361 const new_sp = ip_ptr + @sizeOf(usize);
362
363 (try dwarfRegNative(&context.cpu_state, sp_reg_num)).* = new_sp;
364 (try dwarfRegNative(&context.cpu_state, ip_reg_num)).* = new_ip;
365
366 break :ip new_ip;
367 },
368 .DWARF => {
369 const dwarf = &(unwind.dwarf orelse return error.MissingDebugInfo);
370 const rules = try context.computeRules(gpa, dwarf, unwind.vmaddr_slide, encoding.value.x86_64.dwarf);
371 return context.next(gpa, &rules);
372 },
373 },
374 .aarch64, .aarch64_be => switch (encoding.mode.arm64) {
375 .OLD => return error.UnsupportedDebugInfo,
376 .FRAMELESS => ip: {
377 const sp = (try dwarfRegNative(&context.cpu_state, sp_reg_num)).*;
378 const new_sp = sp + encoding.value.arm64.frameless.stack_size * 16;
379 const new_ip = (try dwarfRegNative(&context.cpu_state, 30)).*;
380 (try dwarfRegNative(&context.cpu_state, sp_reg_num)).* = new_sp;
381 break :ip new_ip;
382 },
383 .DWARF => {
384 const dwarf = &(unwind.dwarf orelse return error.MissingDebugInfo);
385 const rules = try context.computeRules(gpa, dwarf, unwind.vmaddr_slide, encoding.value.arm64.dwarf);
386 return context.next(gpa, &rules);
387 },
388 .FRAME => ip: {
389 const frame = encoding.value.arm64.frame;
390
391 const fp = (try dwarfRegNative(&context.cpu_state, fp_reg_num)).*;
392 const ip_ptr = fp + @sizeOf(usize);
393
394 var reg_addr = fp - @sizeOf(usize);
395 inline for (@typeInfo(@TypeOf(frame.x_reg_pairs)).@"struct".fields, 0..) |field, i| {
396 if (@field(frame.x_reg_pairs, field.name) != 0) {
397 (try dwarfRegNative(&context.cpu_state, 19 + i)).* = @as(*const usize, @ptrFromInt(reg_addr)).*;
398 reg_addr += @sizeOf(usize);
399 (try dwarfRegNative(&context.cpu_state, 20 + i)).* = @as(*const usize, @ptrFromInt(reg_addr)).*;
400 reg_addr += @sizeOf(usize);
401 }
402 }
403
404 inline for (@typeInfo(@TypeOf(frame.d_reg_pairs)).@"struct".fields, 0..) |field, i| {
405 if (@field(frame.d_reg_pairs, field.name) != 0) {
406 // Only the lower half of the 128-bit V registers are restored during unwinding
407 {
408 const dest: *align(1) usize = @ptrCast(try context.cpu_state.dwarfRegisterBytes(64 + 8 + i));
409 dest.* = @as(*const usize, @ptrFromInt(reg_addr)).*;
410 }
411 reg_addr += @sizeOf(usize);
412 {
413 const dest: *align(1) usize = @ptrCast(try context.cpu_state.dwarfRegisterBytes(64 + 9 + i));
414 dest.* = @as(*const usize, @ptrFromInt(reg_addr)).*;
415 }
416 reg_addr += @sizeOf(usize);
417 }
418 }
419
420 const new_ip = @as(*const usize, @ptrFromInt(ip_ptr)).*;
421 const new_fp = @as(*const usize, @ptrFromInt(fp)).*;
422
423 (try dwarfRegNative(&context.cpu_state, fp_reg_num)).* = new_fp;
424 (try dwarfRegNative(&context.cpu_state, ip_reg_num)).* = new_ip;
425
426 break :ip new_ip;
427 },
428 },
429 else => comptime unreachable, // unimplemented
430 };
431
432 const ret_addr = std.debug.stripInstructionPtrAuthCode(new_ip);
433
434 // Like `Dwarf.SelfUnwinder.next`, adjust our next lookup pc in case the `call` was this
435 // function's last instruction making `ret_addr` one byte past its end.
436 context.pc = ret_addr -| 1;
437
438 return ret_addr;
439}
440
441/// Acquires the mutex on success.
442fn findModule(si: *SelfInfo, gpa: Allocator, address: usize) Error!*Module {
443 var info: std.c.dl_info = undefined;
444 if (std.c.dladdr(@ptrFromInt(address), &info) == 0) {
445 return error.MissingDebugInfo;
446 }
447 si.mutex.lock();
448 errdefer si.mutex.unlock();
449 const gop = try si.modules.getOrPutAdapted(gpa, @intFromPtr(info.fbase), Module.Adapter{});
450 errdefer comptime unreachable;
451 if (!gop.found_existing) {
452 gop.key_ptr.* = .{
453 .text_base = @intFromPtr(info.fbase),
454 .name = std.mem.span(info.fname),
455 .unwind = null,
456 .loaded_macho = null,
457 };
458 }
459 return gop.key_ptr;
460}
461
462const Module = struct {
463 text_base: usize,
464 name: []const u8,
465 unwind: ?(Error!Unwind),
466 loaded_macho: ?(Error!LoadedMachO),
467
468 const Adapter = struct {
469 pub fn hash(_: Adapter, text_base: usize) u32 {
470 return @truncate(std.hash.int(text_base));
471 }
472 pub fn eql(_: Adapter, a_text_base: usize, b_module: Module, b_index: usize) bool {
473 _ = b_index;
474 return a_text_base == b_module.text_base;
475 }
476 };
477 const Context = struct {
478 pub fn hash(_: Context, module: Module) u32 {
479 return @truncate(std.hash.int(module.text_base));
480 }
481 pub fn eql(_: Context, a_module: Module, b_module: Module, b_index: usize) bool {
482 _ = b_index;
483 return a_module.text_base == b_module.text_base;
484 }
485 };
486
487 const Unwind = struct {
488 /// The slide applied to the `__unwind_info` and `__eh_frame` sections.
489 /// So, `unwind_info.ptr` is this many bytes higher than the section's vmaddr.
490 vmaddr_slide: u64,
491 /// Backed by the in-memory section mapped by the loader.
492 unwind_info: ?[]const u8,
493 /// Backed by the in-memory `__eh_frame` section mapped by the loader.
494 dwarf: ?Dwarf.Unwind,
495 };
496
497 const LoadedMachO = struct {
498 mapped_memory: []align(std.heap.page_size_min) const u8,
499 symbols: []const MachoSymbol,
500 strings: []const u8,
501 /// This is not necessarily the same as the vmaddr_slide that dyld would report. This is
502 /// because the segments in the file on disk might differ from the ones in memory. Normally
503 /// we wouldn't necessarily expect that to work, but /usr/lib/dyld is incredibly annoying:
504 /// it exists on disk (necessarily, because the kernel needs to load it!), but is also in
505 /// the dyld cache (dyld actually restart itself from cache after loading it), and the two
506 /// versions have (very) different segment base addresses. It's sort of like a large slide
507 /// has been applied to all addresses in memory. For an optimal experience, we consider the
508 /// on-disk vmaddr instead of the in-memory one.
509 vaddr_offset: usize,
510 };
511
512 fn getUnwindInfo(module: *Module, gpa: Allocator) Error!*Unwind {
513 if (module.unwind == null) module.unwind = loadUnwindInfo(module, gpa);
514 return if (module.unwind.?) |*unwind| unwind else |err| err;
515 }
516 fn loadUnwindInfo(module: *const Module, gpa: Allocator) Error!Unwind {
517 const header: *std.macho.mach_header = @ptrFromInt(module.text_base);
518
519 var it: macho.LoadCommandIterator = .{
520 .ncmds = header.ncmds,
521 .buffer = @as([*]u8, @ptrCast(header))[@sizeOf(macho.mach_header_64)..][0..header.sizeofcmds],
522 };
523 const sections, const text_vmaddr = while (it.next()) |load_cmd| {
524 if (load_cmd.cmd() != .SEGMENT_64) continue;
525 const segment_cmd = load_cmd.cast(macho.segment_command_64).?;
526 if (!mem.eql(u8, segment_cmd.segName(), "__TEXT")) continue;
527 break .{ load_cmd.getSections(), segment_cmd.vmaddr };
528 } else unreachable;
529
530 const vmaddr_slide = module.text_base - text_vmaddr;
531
532 var opt_unwind_info: ?[]const u8 = null;
533 var opt_eh_frame: ?[]const u8 = null;
534 for (sections) |sect| {
535 if (mem.eql(u8, sect.sectName(), "__unwind_info")) {
536 const sect_ptr: [*]u8 = @ptrFromInt(@as(usize, @intCast(vmaddr_slide + sect.addr)));
537 opt_unwind_info = sect_ptr[0..@intCast(sect.size)];
538 } else if (mem.eql(u8, sect.sectName(), "__eh_frame")) {
539 const sect_ptr: [*]u8 = @ptrFromInt(@as(usize, @intCast(vmaddr_slide + sect.addr)));
540 opt_eh_frame = sect_ptr[0..@intCast(sect.size)];
541 }
542 }
543 const eh_frame = opt_eh_frame orelse return .{
544 .vmaddr_slide = vmaddr_slide,
545 .unwind_info = opt_unwind_info,
546 .dwarf = null,
547 };
548 var dwarf: Dwarf.Unwind = .initSection(.eh_frame, @intFromPtr(eh_frame.ptr) - vmaddr_slide, eh_frame);
549 errdefer dwarf.deinit(gpa);
550 // We don't need lookups, so this call is just for scanning CIEs.
551 dwarf.prepare(gpa, @sizeOf(usize), native_endian, false, true) catch |err| switch (err) {
552 error.ReadFailed => unreachable, // it's all fixed buffers
553 error.InvalidDebugInfo,
554 error.MissingDebugInfo,
555 error.OutOfMemory,
556 => |e| return e,
557 error.EndOfStream,
558 error.Overflow,
559 error.StreamTooLong,
560 error.InvalidOperand,
561 error.InvalidOpcode,
562 error.InvalidOperation,
563 => return error.InvalidDebugInfo,
564 error.UnsupportedAddrSize,
565 error.UnsupportedDwarfVersion,
566 error.UnimplementedUserOpcode,
567 => return error.UnsupportedDebugInfo,
568 };
569
570 return .{
571 .vmaddr_slide = vmaddr_slide,
572 .unwind_info = opt_unwind_info,
573 .dwarf = dwarf,
574 };
575 }
576
577 fn getLoadedMachO(module: *Module, gpa: Allocator) Error!*LoadedMachO {
578 if (module.loaded_macho == null) module.loaded_macho = loadMachO(module, gpa) catch |err| switch (err) {
579 error.InvalidDebugInfo, error.MissingDebugInfo, error.OutOfMemory, error.Unexpected => |e| e,
580 else => error.ReadFailed,
581 };
582 return if (module.loaded_macho.?) |*lm| lm else |err| err;
583 }
584 fn loadMachO(module: *const Module, gpa: Allocator) Error!LoadedMachO {
585 const all_mapped_memory = try mapDebugInfoFile(module.name);
586 errdefer posix.munmap(all_mapped_memory);
587
588 // In most cases, the file we just mapped is a Mach-O binary. However, it could be a "universal
589 // binary": a simple file format which contains Mach-O binaries for multiple targets. For
590 // instance, `/usr/lib/dyld` is currently distributed as a universal binary containing images
591 // for both ARM64 macOS and x86_64 macOS.
592 if (all_mapped_memory.len < 4) return error.InvalidDebugInfo;
593 const magic = @as(*const u32, @ptrCast(all_mapped_memory.ptr)).*;
594 // The contents of a Mach-O file, which may or may not be the whole of `all_mapped_memory`.
595 const mapped_macho = switch (magic) {
596 macho.MH_MAGIC_64 => all_mapped_memory,
597
598 macho.FAT_CIGAM => mapped_macho: {
599 // This is the universal binary format (aka a "fat binary"). Annoyingly, the whole thing
600 // is big-endian, so we'll be swapping some bytes.
601 if (all_mapped_memory.len < @sizeOf(macho.fat_header)) return error.InvalidDebugInfo;
602 const hdr: *const macho.fat_header = @ptrCast(all_mapped_memory.ptr);
603 const archs_ptr: [*]const macho.fat_arch = @ptrCast(all_mapped_memory.ptr + @sizeOf(macho.fat_header));
604 const archs: []const macho.fat_arch = archs_ptr[0..@byteSwap(hdr.nfat_arch)];
605 const native_cpu_type = switch (builtin.cpu.arch) {
606 .x86_64 => macho.CPU_TYPE_X86_64,
607 .aarch64 => macho.CPU_TYPE_ARM64,
608 else => comptime unreachable,
609 };
610 for (archs) |*arch| {
611 if (@byteSwap(arch.cputype) != native_cpu_type) continue;
612 const offset = @byteSwap(arch.offset);
613 const size = @byteSwap(arch.size);
614 break :mapped_macho all_mapped_memory[offset..][0..size];
615 }
616 // Our native architecture was not present in the fat binary.
617 return error.MissingDebugInfo;
618 },
619
620 // Even on modern 64-bit targets, this format doesn't seem to be too extensively used. It
621 // will be fairly easy to add support here if necessary; it's very similar to above.
622 macho.FAT_CIGAM_64 => return error.UnsupportedDebugInfo,
623
624 else => return error.InvalidDebugInfo,
625 };
626
627 const hdr: *const macho.mach_header_64 = @ptrCast(@alignCast(mapped_macho.ptr));
628 if (hdr.magic != macho.MH_MAGIC_64)
629 return error.InvalidDebugInfo;
630
631 const symtab: macho.symtab_command, const text_vmaddr: u64 = lc_iter: {
632 var it: macho.LoadCommandIterator = .{
633 .ncmds = hdr.ncmds,
634 .buffer = mapped_macho[@sizeOf(macho.mach_header_64)..][0..hdr.sizeofcmds],
635 };
636 var symtab: ?macho.symtab_command = null;
637 var text_vmaddr: ?u64 = null;
638 while (it.next()) |cmd| switch (cmd.cmd()) {
639 .SYMTAB => symtab = cmd.cast(macho.symtab_command) orelse return error.InvalidDebugInfo,
640 .SEGMENT_64 => if (cmd.cast(macho.segment_command_64)) |seg_cmd| {
641 if (!mem.eql(u8, seg_cmd.segName(), "__TEXT")) continue;
642 text_vmaddr = seg_cmd.vmaddr;
643 },
644 else => {},
645 };
646 break :lc_iter .{
647 symtab orelse return error.MissingDebugInfo,
648 text_vmaddr orelse return error.MissingDebugInfo,
649 };
650 };
651
652 const syms_ptr: [*]align(1) const macho.nlist_64 = @ptrCast(mapped_macho[symtab.symoff..]);
653 const syms = syms_ptr[0..symtab.nsyms];
654 const strings = mapped_macho[symtab.stroff..][0 .. symtab.strsize - 1];
655
656 var symbols: std.ArrayList(MachoSymbol) = try .initCapacity(gpa, syms.len);
657 defer symbols.deinit(gpa);
658
659 // This map is temporary; it is used only to detect duplicates here. This is
660 // necessary because we prefer to use STAB ("symbolic debugging table") symbols,
661 // but they might not be present, so we track normal symbols too.
662 // Indices match 1-1 with those of `symbols`.
663 var symbol_names: std.StringArrayHashMapUnmanaged(void) = .empty;
664 defer symbol_names.deinit(gpa);
665 try symbol_names.ensureUnusedCapacity(gpa, syms.len);
666
667 var ofile: u32 = undefined;
668 var last_sym: MachoSymbol = undefined;
669 var state: enum {
670 init,
671 oso_open,
672 oso_close,
673 bnsym,
674 fun_strx,
675 fun_size,
676 ensym,
677 } = .init;
678
679 for (syms) |*sym| {
680 if (sym.n_type.bits.is_stab == 0) {
681 if (sym.n_strx == 0) continue;
682 switch (sym.n_type.bits.type) {
683 .undf, .pbud, .indr, .abs, _ => continue,
684 .sect => {
685 const name = std.mem.sliceTo(strings[sym.n_strx..], 0);
686 const gop = symbol_names.getOrPutAssumeCapacity(name);
687 if (!gop.found_existing) {
688 assert(gop.index == symbols.items.len);
689 symbols.appendAssumeCapacity(.{
690 .strx = sym.n_strx,
691 .addr = sym.n_value,
692 .ofile = MachoSymbol.unknown_ofile,
693 });
694 }
695 },
696 }
697 continue;
698 }
699
700 // TODO handle globals N_GSYM, and statics N_STSYM
701 switch (sym.n_type.stab) {
702 .oso => switch (state) {
703 .init, .oso_close => {
704 state = .oso_open;
705 ofile = sym.n_strx;
706 },
707 else => return error.InvalidDebugInfo,
708 },
709 .bnsym => switch (state) {
710 .oso_open, .ensym => {
711 state = .bnsym;
712 last_sym = .{
713 .strx = 0,
714 .addr = sym.n_value,
715 .ofile = ofile,
716 };
717 },
718 else => return error.InvalidDebugInfo,
719 },
720 .fun => switch (state) {
721 .bnsym => {
722 state = .fun_strx;
723 last_sym.strx = sym.n_strx;
724 },
725 .fun_strx => {
726 state = .fun_size;
727 },
728 else => return error.InvalidDebugInfo,
729 },
730 .ensym => switch (state) {
731 .fun_size => {
732 state = .ensym;
733 if (last_sym.strx != 0) {
734 const name = std.mem.sliceTo(strings[last_sym.strx..], 0);
735 const gop = symbol_names.getOrPutAssumeCapacity(name);
736 if (!gop.found_existing) {
737 assert(gop.index == symbols.items.len);
738 symbols.appendAssumeCapacity(last_sym);
739 } else {
740 symbols.items[gop.index] = last_sym;
741 }
742 }
743 },
744 else => return error.InvalidDebugInfo,
745 },
746 .so => switch (state) {
747 .init, .oso_close => {},
748 .oso_open, .ensym => {
749 state = .oso_close;
750 },
751 else => return error.InvalidDebugInfo,
752 },
753 else => {},
754 }
755 }
756
757 switch (state) {
758 .init => {
759 // Missing STAB symtab entries is still okay, unless there were also no normal symbols.
760 if (symbols.items.len == 0) return error.MissingDebugInfo;
761 },
762 .oso_close => {},
763 else => return error.InvalidDebugInfo, // corrupted STAB entries in symtab
764 }
765
766 const symbols_slice = try symbols.toOwnedSlice(gpa);
767 errdefer gpa.free(symbols_slice);
768
769 // Even though lld emits symbols in ascending order, this debug code
770 // should work for programs linked in any valid way.
771 // This sort is so that we can binary search later.
772 mem.sort(MachoSymbol, symbols_slice, {}, MachoSymbol.addressLessThan);
773
774 return .{
775 .mapped_memory = all_mapped_memory,
776 .symbols = symbols_slice,
777 .strings = strings,
778 .vaddr_offset = module.text_base - text_vmaddr,
779 };
780 }
781};
782
783const OFile = struct {
784 mapped_memory: []align(std.heap.page_size_min) const u8,
785 dwarf: Dwarf,
786 strtab: []const u8,
787 symtab: []align(1) const macho.nlist_64,
788 /// All named symbols in `symtab`. Stored `u32` key is the index into `symtab`. Accessed
789 /// through `SymbolAdapter`, so that the symbol name is used as the logical key.
790 symbols_by_name: std.ArrayHashMapUnmanaged(u32, void, void, true),
791
792 const SymbolAdapter = struct {
793 strtab: []const u8,
794 symtab: []align(1) const macho.nlist_64,
795 pub fn hash(ctx: SymbolAdapter, sym_name: []const u8) u32 {
796 _ = ctx;
797 return @truncate(std.hash.Wyhash.hash(0, sym_name));
798 }
799 pub fn eql(ctx: SymbolAdapter, a_sym_name: []const u8, b_sym_index: u32, b_index: usize) bool {
800 _ = b_index;
801 const b_sym = ctx.symtab[b_sym_index];
802 const b_sym_name = std.mem.sliceTo(ctx.strtab[b_sym.n_strx..], 0);
803 return mem.eql(u8, a_sym_name, b_sym_name);
804 }
805 };
806};
807
808const MachoSymbol = struct {
809 strx: u32,
810 addr: u64,
811 /// Value may be `unknown_ofile`.
812 ofile: u32,
813 const unknown_ofile = std.math.maxInt(u32);
814 fn addressLessThan(context: void, lhs: MachoSymbol, rhs: MachoSymbol) bool {
815 _ = context;
816 return lhs.addr < rhs.addr;
817 }
818 /// Assumes that `symbols` is sorted in order of ascending `addr`.
819 fn find(symbols: []const MachoSymbol, address: usize) ?*const MachoSymbol {
820 if (symbols.len == 0) return null; // no potential match
821 if (address < symbols[0].addr) return null; // address is before the lowest-address symbol
822 var left: usize = 0;
823 var len: usize = symbols.len;
824 while (len > 1) {
825 const mid = left + len / 2;
826 if (address < symbols[mid].addr) {
827 len /= 2;
828 } else {
829 left = mid;
830 len -= len / 2;
831 }
832 }
833 return &symbols[left];
834 }
835
836 test find {
837 const symbols: []const MachoSymbol = &.{
838 .{ .addr = 100, .strx = undefined, .ofile = undefined },
839 .{ .addr = 200, .strx = undefined, .ofile = undefined },
840 .{ .addr = 300, .strx = undefined, .ofile = undefined },
841 };
842
843 try testing.expectEqual(null, find(symbols, 0));
844 try testing.expectEqual(null, find(symbols, 99));
845 try testing.expectEqual(&symbols[0], find(symbols, 100).?);
846 try testing.expectEqual(&symbols[0], find(symbols, 150).?);
847 try testing.expectEqual(&symbols[0], find(symbols, 199).?);
848
849 try testing.expectEqual(&symbols[1], find(symbols, 200).?);
850 try testing.expectEqual(&symbols[1], find(symbols, 250).?);
851 try testing.expectEqual(&symbols[1], find(symbols, 299).?);
852
853 try testing.expectEqual(&symbols[2], find(symbols, 300).?);
854 try testing.expectEqual(&symbols[2], find(symbols, 301).?);
855 try testing.expectEqual(&symbols[2], find(symbols, 5000).?);
856 }
857};
858test {
859 _ = MachoSymbol;
860}
861
862/// Uses `mmap` to map the file at `path` into memory.
863fn mapDebugInfoFile(path: []const u8) ![]align(std.heap.page_size_min) const u8 {
864 const file = std.fs.cwd().openFile(path, .{}) catch |err| switch (err) {
865 error.FileNotFound => return error.MissingDebugInfo,
866 else => return error.ReadFailed,
867 };
868 defer file.close();
869
870 const file_end_pos = file.getEndPos() catch |err| switch (err) {
871 error.Unexpected => |e| return e,
872 else => return error.ReadFailed,
873 };
874 const file_len = std.math.cast(usize, file_end_pos) orelse return error.InvalidDebugInfo;
875
876 return posix.mmap(
877 null,
878 file_len,
879 posix.PROT.READ,
880 .{ .TYPE = .SHARED },
881 file.handle,
882 0,
883 ) catch |err| switch (err) {
884 error.Unexpected => |e| return e,
885 else => return error.ReadFailed,
886 };
887}
888
889fn loadOFile(gpa: Allocator, o_file_path: []const u8) !OFile {
890 const mapped_mem = try mapDebugInfoFile(o_file_path);
891 errdefer posix.munmap(mapped_mem);
892
893 if (mapped_mem.len < @sizeOf(macho.mach_header_64)) return error.InvalidDebugInfo;
894 const hdr: *const macho.mach_header_64 = @ptrCast(@alignCast(mapped_mem.ptr));
895 if (hdr.magic != std.macho.MH_MAGIC_64) return error.InvalidDebugInfo;
896
897 const seg_cmd: macho.LoadCommandIterator.LoadCommand, const symtab_cmd: macho.symtab_command = cmds: {
898 var seg_cmd: ?macho.LoadCommandIterator.LoadCommand = null;
899 var symtab_cmd: ?macho.symtab_command = null;
900 var it: macho.LoadCommandIterator = .{
901 .ncmds = hdr.ncmds,
902 .buffer = mapped_mem[@sizeOf(macho.mach_header_64)..][0..hdr.sizeofcmds],
903 };
904 while (it.next()) |cmd| switch (cmd.cmd()) {
905 .SEGMENT_64 => seg_cmd = cmd,
906 .SYMTAB => symtab_cmd = cmd.cast(macho.symtab_command) orelse return error.InvalidDebugInfo,
907 else => {},
908 };
909 break :cmds .{
910 seg_cmd orelse return error.MissingDebugInfo,
911 symtab_cmd orelse return error.MissingDebugInfo,
912 };
913 };
914
915 if (mapped_mem.len < symtab_cmd.stroff + symtab_cmd.strsize) return error.InvalidDebugInfo;
916 if (mapped_mem[symtab_cmd.stroff + symtab_cmd.strsize - 1] != 0) return error.InvalidDebugInfo;
917 const strtab = mapped_mem[symtab_cmd.stroff..][0 .. symtab_cmd.strsize - 1];
918
919 const n_sym_bytes = symtab_cmd.nsyms * @sizeOf(macho.nlist_64);
920 if (mapped_mem.len < symtab_cmd.symoff + n_sym_bytes) return error.InvalidDebugInfo;
921 const symtab: []align(1) const macho.nlist_64 = @ptrCast(mapped_mem[symtab_cmd.symoff..][0..n_sym_bytes]);
922
923 // TODO handle tentative (common) symbols
924 var symbols_by_name: std.ArrayHashMapUnmanaged(u32, void, void, true) = .empty;
925 defer symbols_by_name.deinit(gpa);
926 try symbols_by_name.ensureUnusedCapacity(gpa, @intCast(symtab.len));
927 for (symtab, 0..) |sym, sym_index| {
928 if (sym.n_strx == 0) continue;
929 switch (sym.n_type.bits.type) {
930 .undf => continue, // includes tentative symbols
931 .abs => continue,
932 else => {},
933 }
934 const sym_name = mem.sliceTo(strtab[sym.n_strx..], 0);
935 const gop = symbols_by_name.getOrPutAssumeCapacityAdapted(
936 @as([]const u8, sym_name),
937 @as(OFile.SymbolAdapter, .{ .strtab = strtab, .symtab = symtab }),
938 );
939 if (gop.found_existing) return error.InvalidDebugInfo;
940 gop.key_ptr.* = @intCast(sym_index);
941 }
942
943 var sections: Dwarf.SectionArray = @splat(null);
944 for (seg_cmd.getSections()) |sect| {
945 if (!std.mem.eql(u8, "__DWARF", sect.segName())) continue;
946
947 const section_index: usize = inline for (@typeInfo(Dwarf.Section.Id).@"enum".fields, 0..) |section, i| {
948 if (mem.eql(u8, "__" ++ section.name, sect.sectName())) break i;
949 } else continue;
950
951 if (mapped_mem.len < sect.offset + sect.size) return error.InvalidDebugInfo;
952 const section_bytes = mapped_mem[sect.offset..][0..sect.size];
953 sections[section_index] = .{
954 .data = section_bytes,
955 .owned = false,
956 };
957 }
958
959 const missing_debug_info =
960 sections[@intFromEnum(Dwarf.Section.Id.debug_info)] == null or
961 sections[@intFromEnum(Dwarf.Section.Id.debug_abbrev)] == null or
962 sections[@intFromEnum(Dwarf.Section.Id.debug_str)] == null or
963 sections[@intFromEnum(Dwarf.Section.Id.debug_line)] == null;
964 if (missing_debug_info) return error.MissingDebugInfo;
965
966 var dwarf: Dwarf = .{ .sections = sections };
967 errdefer dwarf.deinit(gpa);
968 try dwarf.open(gpa, native_endian);
969
970 return .{
971 .mapped_memory = mapped_mem,
972 .dwarf = dwarf,
973 .strtab = strtab,
974 .symtab = symtab,
975 .symbols_by_name = symbols_by_name.move(),
976 };
977}
978
979const std = @import("std");
980const Allocator = std.mem.Allocator;
981const Dwarf = std.debug.Dwarf;
982const Error = std.debug.SelfInfoError;
983const assert = std.debug.assert;
984const posix = std.posix;
985const macho = std.macho;
986const mem = std.mem;
987const testing = std.testing;
988const dwarfRegNative = std.debug.Dwarf.SelfUnwinder.regNative;
989
990const builtin = @import("builtin");
991const native_endian = builtin.target.cpu.arch.endian();
992
993const SelfInfo = @This();
lib/std/debug/SelfInfo/MachO.zig created+993
...@@ -0,0 +1,993 @@
1mutex: std.Thread.Mutex,
2/// Accessed through `Module.Adapter`.
3modules: std.ArrayHashMapUnmanaged(Module, void, Module.Context, false),
4ofiles: std.StringArrayHashMapUnmanaged(?OFile),
5
6pub const init: SelfInfo = .{
7 .mutex = .{},
8 .modules = .empty,
9 .ofiles = .empty,
10};
11pub fn deinit(si: *SelfInfo, gpa: Allocator) void {
12 for (si.modules.keys()) |*module| {
13 unwind: {
14 const u = &(module.unwind orelse break :unwind catch break :unwind);
15 if (u.dwarf) |*dwarf| dwarf.deinit(gpa);
16 }
17 loaded: {
18 const l = &(module.loaded_macho orelse break :loaded catch break :loaded);
19 gpa.free(l.symbols);
20 posix.munmap(l.mapped_memory);
21 }
22 }
23 for (si.ofiles.values()) |*opt_ofile| {
24 const ofile = &(opt_ofile.* orelse continue);
25 ofile.dwarf.deinit(gpa);
26 ofile.symbols_by_name.deinit(gpa);
27 posix.munmap(ofile.mapped_memory);
28 }
29 si.modules.deinit(gpa);
30 si.ofiles.deinit(gpa);
31}
32
33pub fn getSymbol(si: *SelfInfo, gpa: Allocator, address: usize) Error!std.debug.Symbol {
34 const module = try si.findModule(gpa, address);
35 defer si.mutex.unlock();
36
37 const loaded_macho = try module.getLoadedMachO(gpa);
38
39 const vaddr = address - loaded_macho.vaddr_offset;
40 const symbol = MachoSymbol.find(loaded_macho.symbols, vaddr) orelse return .unknown;
41
42 // offset of `address` from start of `symbol`
43 const address_symbol_offset = vaddr - symbol.addr;
44
45 // Take the symbol name from the N_FUN STAB entry, we're going to
46 // use it if we fail to find the DWARF infos
47 const stab_symbol = mem.sliceTo(loaded_macho.strings[symbol.strx..], 0);
48
49 // If any information is missing, we can at least return this from now on.
50 const sym_only_result: std.debug.Symbol = .{
51 .name = stab_symbol,
52 .compile_unit_name = null,
53 .source_location = null,
54 };
55
56 if (symbol.ofile == MachoSymbol.unknown_ofile) {
57 // We don't have STAB info, so can't track down the object file; all we can do is the symbol name.
58 return sym_only_result;
59 }
60
61 const o_file: *OFile = of: {
62 const path = mem.sliceTo(loaded_macho.strings[symbol.ofile..], 0);
63 const gop = try si.ofiles.getOrPut(gpa, path);
64 if (!gop.found_existing) {
65 gop.value_ptr.* = loadOFile(gpa, path) catch null;
66 }
67 if (gop.value_ptr.*) |*o_file| {
68 break :of o_file;
69 } else {
70 return sym_only_result;
71 }
72 };
73
74 const symbol_index = o_file.symbols_by_name.getKeyAdapted(
75 @as([]const u8, stab_symbol),
76 @as(OFile.SymbolAdapter, .{ .strtab = o_file.strtab, .symtab = o_file.symtab }),
77 ) orelse return sym_only_result;
78 const symbol_ofile_vaddr = o_file.symtab[symbol_index].n_value;
79
80 const compile_unit = o_file.dwarf.findCompileUnit(native_endian, symbol_ofile_vaddr) catch return sym_only_result;
81
82 return .{
83 .name = o_file.dwarf.getSymbolName(symbol_ofile_vaddr + address_symbol_offset) orelse stab_symbol,
84 .compile_unit_name = compile_unit.die.getAttrString(
85 &o_file.dwarf,
86 native_endian,
87 std.dwarf.AT.name,
88 o_file.dwarf.section(.debug_str),
89 compile_unit,
90 ) catch |err| switch (err) {
91 error.MissingDebugInfo, error.InvalidDebugInfo => null,
92 },
93 .source_location = o_file.dwarf.getLineNumberInfo(
94 gpa,
95 native_endian,
96 compile_unit,
97 symbol_ofile_vaddr + address_symbol_offset,
98 ) catch null,
99 };
100}
101pub fn getModuleName(si: *SelfInfo, gpa: Allocator, address: usize) Error![]const u8 {
102 const module = try si.findModule(gpa, address);
103 defer si.mutex.unlock();
104 return module.name;
105}
106
107pub const can_unwind: bool = true;
108pub const UnwindContext = std.debug.Dwarf.SelfUnwinder;
109/// Unwind a frame using MachO compact unwind info (from `__unwind_info`).
110/// If the compact encoding can't encode a way to unwind a frame, it will
111/// defer unwinding to DWARF, in which case `__eh_frame` will be used if available.
112pub fn unwindFrame(si: *SelfInfo, gpa: Allocator, context: *UnwindContext) Error!usize {
113 return unwindFrameInner(si, gpa, context) catch |err| switch (err) {
114 error.InvalidDebugInfo,
115 error.MissingDebugInfo,
116 error.UnsupportedDebugInfo,
117 error.ReadFailed,
118 error.OutOfMemory,
119 error.Unexpected,
120 => |e| return e,
121 error.UnsupportedRegister,
122 error.UnsupportedAddrSize,
123 error.UnimplementedUserOpcode,
124 => return error.UnsupportedDebugInfo,
125 error.Overflow,
126 error.EndOfStream,
127 error.StreamTooLong,
128 error.InvalidOpcode,
129 error.InvalidOperation,
130 error.InvalidOperand,
131 error.InvalidRegister,
132 error.IncompatibleRegisterSize,
133 => return error.InvalidDebugInfo,
134 };
135}
136fn unwindFrameInner(si: *SelfInfo, gpa: Allocator, context: *UnwindContext) !usize {
137 const module = try si.findModule(gpa, context.pc);
138 defer si.mutex.unlock();
139
140 const unwind: *Module.Unwind = try module.getUnwindInfo(gpa);
141
142 const ip_reg_num = comptime Dwarf.ipRegNum(builtin.target.cpu.arch).?;
143 const fp_reg_num = comptime Dwarf.fpRegNum(builtin.target.cpu.arch);
144 const sp_reg_num = comptime Dwarf.spRegNum(builtin.target.cpu.arch);
145
146 const unwind_info = unwind.unwind_info orelse return error.MissingDebugInfo;
147 if (unwind_info.len < @sizeOf(macho.unwind_info_section_header)) return error.InvalidDebugInfo;
148 const header: *align(1) const macho.unwind_info_section_header = @ptrCast(unwind_info);
149
150 const index_byte_count = header.indexCount * @sizeOf(macho.unwind_info_section_header_index_entry);
151 if (unwind_info.len < header.indexSectionOffset + index_byte_count) return error.InvalidDebugInfo;
152 const indices: []align(1) const macho.unwind_info_section_header_index_entry = @ptrCast(unwind_info[header.indexSectionOffset..][0..index_byte_count]);
153 if (indices.len == 0) return error.MissingDebugInfo;
154
155 // offset of the PC into the `__TEXT` segment
156 const pc_text_offset = context.pc - module.text_base;
157
158 const start_offset: u32, const first_level_offset: u32 = index: {
159 var left: usize = 0;
160 var len: usize = indices.len;
161 while (len > 1) {
162 const mid = left + len / 2;
163 if (pc_text_offset < indices[mid].functionOffset) {
164 len /= 2;
165 } else {
166 left = mid;
167 len -= len / 2;
168 }
169 }
170 break :index .{ indices[left].secondLevelPagesSectionOffset, indices[left].functionOffset };
171 };
172 // An offset of 0 is a sentinel indicating a range does not have unwind info.
173 if (start_offset == 0) return error.MissingDebugInfo;
174
175 const common_encodings_byte_count = header.commonEncodingsArrayCount * @sizeOf(macho.compact_unwind_encoding_t);
176 if (unwind_info.len < header.commonEncodingsArraySectionOffset + common_encodings_byte_count) return error.InvalidDebugInfo;
177 const common_encodings: []align(1) const macho.compact_unwind_encoding_t = @ptrCast(
178 unwind_info[header.commonEncodingsArraySectionOffset..][0..common_encodings_byte_count],
179 );
180
181 if (unwind_info.len < start_offset + @sizeOf(macho.UNWIND_SECOND_LEVEL)) return error.InvalidDebugInfo;
182 const kind: *align(1) const macho.UNWIND_SECOND_LEVEL = @ptrCast(unwind_info[start_offset..]);
183
184 const entry: struct {
185 function_offset: usize,
186 raw_encoding: u32,
187 } = switch (kind.*) {
188 .REGULAR => entry: {
189 if (unwind_info.len < start_offset + @sizeOf(macho.unwind_info_regular_second_level_page_header)) return error.InvalidDebugInfo;
190 const page_header: *align(1) const macho.unwind_info_regular_second_level_page_header = @ptrCast(unwind_info[start_offset..]);
191
192 const entries_byte_count = page_header.entryCount * @sizeOf(macho.unwind_info_regular_second_level_entry);
193 if (unwind_info.len < start_offset + entries_byte_count) return error.InvalidDebugInfo;
194 const entries: []align(1) const macho.unwind_info_regular_second_level_entry = @ptrCast(
195 unwind_info[start_offset + page_header.entryPageOffset ..][0..entries_byte_count],
196 );
197 if (entries.len == 0) return error.InvalidDebugInfo;
198
199 var left: usize = 0;
200 var len: usize = entries.len;
201 while (len > 1) {
202 const mid = left + len / 2;
203 if (pc_text_offset < entries[mid].functionOffset) {
204 len /= 2;
205 } else {
206 left = mid;
207 len -= len / 2;
208 }
209 }
210 break :entry .{
211 .function_offset = entries[left].functionOffset,
212 .raw_encoding = entries[left].encoding,
213 };
214 },
215 .COMPRESSED => entry: {
216 if (unwind_info.len < start_offset + @sizeOf(macho.unwind_info_compressed_second_level_page_header)) return error.InvalidDebugInfo;
217 const page_header: *align(1) const macho.unwind_info_compressed_second_level_page_header = @ptrCast(unwind_info[start_offset..]);
218
219 const entries_byte_count = page_header.entryCount * @sizeOf(macho.UnwindInfoCompressedEntry);
220 if (unwind_info.len < start_offset + entries_byte_count) return error.InvalidDebugInfo;
221 const entries: []align(1) const macho.UnwindInfoCompressedEntry = @ptrCast(
222 unwind_info[start_offset + page_header.entryPageOffset ..][0..entries_byte_count],
223 );
224 if (entries.len == 0) return error.InvalidDebugInfo;
225
226 var left: usize = 0;
227 var len: usize = entries.len;
228 while (len > 1) {
229 const mid = left + len / 2;
230 if (pc_text_offset < first_level_offset + entries[mid].funcOffset) {
231 len /= 2;
232 } else {
233 left = mid;
234 len -= len / 2;
235 }
236 }
237 const entry = entries[left];
238
239 const function_offset = first_level_offset + entry.funcOffset;
240 if (entry.encodingIndex < common_encodings.len) {
241 break :entry .{
242 .function_offset = function_offset,
243 .raw_encoding = common_encodings[entry.encodingIndex],
244 };
245 }
246
247 const local_index = entry.encodingIndex - common_encodings.len;
248 const local_encodings_byte_count = page_header.encodingsCount * @sizeOf(macho.compact_unwind_encoding_t);
249 if (unwind_info.len < start_offset + page_header.encodingsPageOffset + local_encodings_byte_count) return error.InvalidDebugInfo;
250 const local_encodings: []align(1) const macho.compact_unwind_encoding_t = @ptrCast(
251 unwind_info[start_offset + page_header.encodingsPageOffset ..][0..local_encodings_byte_count],
252 );
253 if (local_index >= local_encodings.len) return error.InvalidDebugInfo;
254 break :entry .{
255 .function_offset = function_offset,
256 .raw_encoding = local_encodings[local_index],
257 };
258 },
259 else => return error.InvalidDebugInfo,
260 };
261
262 if (entry.raw_encoding == 0) return error.MissingDebugInfo;
263
264 const encoding: macho.CompactUnwindEncoding = @bitCast(entry.raw_encoding);
265 const new_ip = switch (builtin.cpu.arch) {
266 .x86_64 => switch (encoding.mode.x86_64) {
267 .OLD => return error.UnsupportedDebugInfo,
268 .RBP_FRAME => ip: {
269 const frame = encoding.value.x86_64.frame;
270
271 const fp = (try dwarfRegNative(&context.cpu_state, fp_reg_num)).*;
272 const new_sp = fp + 2 * @sizeOf(usize);
273
274 const ip_ptr = fp + @sizeOf(usize);
275 const new_ip = @as(*const usize, @ptrFromInt(ip_ptr)).*;
276 const new_fp = @as(*const usize, @ptrFromInt(fp)).*;
277
278 (try dwarfRegNative(&context.cpu_state, fp_reg_num)).* = new_fp;
279 (try dwarfRegNative(&context.cpu_state, sp_reg_num)).* = new_sp;
280 (try dwarfRegNative(&context.cpu_state, ip_reg_num)).* = new_ip;
281
282 const regs: [5]u3 = .{
283 frame.reg0,
284 frame.reg1,
285 frame.reg2,
286 frame.reg3,
287 frame.reg4,
288 };
289 for (regs, 0..) |reg, i| {
290 if (reg == 0) continue;
291 const addr = fp - frame.frame_offset * @sizeOf(usize) + i * @sizeOf(usize);
292 const reg_number = try Dwarf.compactUnwindToDwarfRegNumber(reg);
293 (try dwarfRegNative(&context.cpu_state, reg_number)).* = @as(*const usize, @ptrFromInt(addr)).*;
294 }
295
296 break :ip new_ip;
297 },
298 .STACK_IMMD,
299 .STACK_IND,
300 => ip: {
301 const frameless = encoding.value.x86_64.frameless;
302
303 const sp = (try dwarfRegNative(&context.cpu_state, sp_reg_num)).*;
304 const stack_size: usize = stack_size: {
305 if (encoding.mode.x86_64 == .STACK_IMMD) {
306 break :stack_size @as(usize, frameless.stack.direct.stack_size) * @sizeOf(usize);
307 }
308 // In .STACK_IND, the stack size is inferred from the subq instruction at the beginning of the function.
309 const sub_offset_addr =
310 module.text_base +
311 entry.function_offset +
312 frameless.stack.indirect.sub_offset;
313 // `sub_offset_addr` points to the offset of the literal within the instruction
314 const sub_operand = @as(*align(1) const u32, @ptrFromInt(sub_offset_addr)).*;
315 break :stack_size sub_operand + @sizeOf(usize) * @as(usize, frameless.stack.indirect.stack_adjust);
316 };
317
318 // Decode the Lehmer-coded sequence of registers.
319 // For a description of the encoding see lib/libc/include/any-macos.13-any/mach-o/compact_unwind_encoding.h
320
321 // Decode the variable-based permutation number into its digits. Each digit represents
322 // an index into the list of register numbers that weren't yet used in the sequence at
323 // the time the digit was added.
324 const reg_count = frameless.stack_reg_count;
325 const ip_ptr = ip_ptr: {
326 var digits: [6]u3 = undefined;
327 var accumulator: usize = frameless.stack_reg_permutation;
328 var base: usize = 2;
329 for (0..reg_count) |i| {
330 const div = accumulator / base;
331 digits[digits.len - 1 - i] = @intCast(accumulator - base * div);
332 accumulator = div;
333 base += 1;
334 }
335
336 var registers: [6]u3 = undefined;
337 var used_indices: [6]bool = @splat(false);
338 for (digits[digits.len - reg_count ..], 0..) |target_unused_index, i| {
339 var unused_count: u8 = 0;
340 const unused_index = for (used_indices, 0..) |used, index| {
341 if (!used) {
342 if (target_unused_index == unused_count) break index;
343 unused_count += 1;
344 }
345 } else unreachable;
346 registers[i] = @intCast(unused_index + 1);
347 used_indices[unused_index] = true;
348 }
349
350 var reg_addr = sp + stack_size - @sizeOf(usize) * @as(usize, reg_count + 1);
351 for (0..reg_count) |i| {
352 const reg_number = try Dwarf.compactUnwindToDwarfRegNumber(registers[i]);
353 (try dwarfRegNative(&context.cpu_state, reg_number)).* = @as(*const usize, @ptrFromInt(reg_addr)).*;
354 reg_addr += @sizeOf(usize);
355 }
356
357 break :ip_ptr reg_addr;
358 };
359
360 const new_ip = @as(*const usize, @ptrFromInt(ip_ptr)).*;
361 const new_sp = ip_ptr + @sizeOf(usize);
362
363 (try dwarfRegNative(&context.cpu_state, sp_reg_num)).* = new_sp;
364 (try dwarfRegNative(&context.cpu_state, ip_reg_num)).* = new_ip;
365
366 break :ip new_ip;
367 },
368 .DWARF => {
369 const dwarf = &(unwind.dwarf orelse return error.MissingDebugInfo);
370 const rules = try context.computeRules(gpa, dwarf, unwind.vmaddr_slide, encoding.value.x86_64.dwarf);
371 return context.next(gpa, &rules);
372 },
373 },
374 .aarch64, .aarch64_be => switch (encoding.mode.arm64) {
375 .OLD => return error.UnsupportedDebugInfo,
376 .FRAMELESS => ip: {
377 const sp = (try dwarfRegNative(&context.cpu_state, sp_reg_num)).*;
378 const new_sp = sp + encoding.value.arm64.frameless.stack_size * 16;
379 const new_ip = (try dwarfRegNative(&context.cpu_state, 30)).*;
380 (try dwarfRegNative(&context.cpu_state, sp_reg_num)).* = new_sp;
381 break :ip new_ip;
382 },
383 .DWARF => {
384 const dwarf = &(unwind.dwarf orelse return error.MissingDebugInfo);
385 const rules = try context.computeRules(gpa, dwarf, unwind.vmaddr_slide, encoding.value.arm64.dwarf);
386 return context.next(gpa, &rules);
387 },
388 .FRAME => ip: {
389 const frame = encoding.value.arm64.frame;
390
391 const fp = (try dwarfRegNative(&context.cpu_state, fp_reg_num)).*;
392 const ip_ptr = fp + @sizeOf(usize);
393
394 var reg_addr = fp - @sizeOf(usize);
395 inline for (@typeInfo(@TypeOf(frame.x_reg_pairs)).@"struct".fields, 0..) |field, i| {
396 if (@field(frame.x_reg_pairs, field.name) != 0) {
397 (try dwarfRegNative(&context.cpu_state, 19 + i)).* = @as(*const usize, @ptrFromInt(reg_addr)).*;
398 reg_addr += @sizeOf(usize);
399 (try dwarfRegNative(&context.cpu_state, 20 + i)).* = @as(*const usize, @ptrFromInt(reg_addr)).*;
400 reg_addr += @sizeOf(usize);
401 }
402 }
403
404 inline for (@typeInfo(@TypeOf(frame.d_reg_pairs)).@"struct".fields, 0..) |field, i| {
405 if (@field(frame.d_reg_pairs, field.name) != 0) {
406 // Only the lower half of the 128-bit V registers are restored during unwinding
407 {
408 const dest: *align(1) usize = @ptrCast(try context.cpu_state.dwarfRegisterBytes(64 + 8 + i));
409 dest.* = @as(*const usize, @ptrFromInt(reg_addr)).*;
410 }
411 reg_addr += @sizeOf(usize);
412 {
413 const dest: *align(1) usize = @ptrCast(try context.cpu_state.dwarfRegisterBytes(64 + 9 + i));
414 dest.* = @as(*const usize, @ptrFromInt(reg_addr)).*;
415 }
416 reg_addr += @sizeOf(usize);
417 }
418 }
419
420 const new_ip = @as(*const usize, @ptrFromInt(ip_ptr)).*;
421 const new_fp = @as(*const usize, @ptrFromInt(fp)).*;
422
423 (try dwarfRegNative(&context.cpu_state, fp_reg_num)).* = new_fp;
424 (try dwarfRegNative(&context.cpu_state, ip_reg_num)).* = new_ip;
425
426 break :ip new_ip;
427 },
428 },
429 else => comptime unreachable, // unimplemented
430 };
431
432 const ret_addr = std.debug.stripInstructionPtrAuthCode(new_ip);
433
434 // Like `Dwarf.SelfUnwinder.next`, adjust our next lookup pc in case the `call` was this
435 // function's last instruction making `ret_addr` one byte past its end.
436 context.pc = ret_addr -| 1;
437
438 return ret_addr;
439}
440
441/// Acquires the mutex on success.
442fn findModule(si: *SelfInfo, gpa: Allocator, address: usize) Error!*Module {
443 var info: std.c.dl_info = undefined;
444 if (std.c.dladdr(@ptrFromInt(address), &info) == 0) {
445 return error.MissingDebugInfo;
446 }
447 si.mutex.lock();
448 errdefer si.mutex.unlock();
449 const gop = try si.modules.getOrPutAdapted(gpa, @intFromPtr(info.fbase), Module.Adapter{});
450 errdefer comptime unreachable;
451 if (!gop.found_existing) {
452 gop.key_ptr.* = .{
453 .text_base = @intFromPtr(info.fbase),
454 .name = std.mem.span(info.fname),
455 .unwind = null,
456 .loaded_macho = null,
457 };
458 }
459 return gop.key_ptr;
460}
461
462const Module = struct {
463 text_base: usize,
464 name: []const u8,
465 unwind: ?(Error!Unwind),
466 loaded_macho: ?(Error!LoadedMachO),
467
468 const Adapter = struct {
469 pub fn hash(_: Adapter, text_base: usize) u32 {
470 return @truncate(std.hash.int(text_base));
471 }
472 pub fn eql(_: Adapter, a_text_base: usize, b_module: Module, b_index: usize) bool {
473 _ = b_index;
474 return a_text_base == b_module.text_base;
475 }
476 };
477 const Context = struct {
478 pub fn hash(_: Context, module: Module) u32 {
479 return @truncate(std.hash.int(module.text_base));
480 }
481 pub fn eql(_: Context, a_module: Module, b_module: Module, b_index: usize) bool {
482 _ = b_index;
483 return a_module.text_base == b_module.text_base;
484 }
485 };
486
487 const Unwind = struct {
488 /// The slide applied to the `__unwind_info` and `__eh_frame` sections.
489 /// So, `unwind_info.ptr` is this many bytes higher than the section's vmaddr.
490 vmaddr_slide: u64,
491 /// Backed by the in-memory section mapped by the loader.
492 unwind_info: ?[]const u8,
493 /// Backed by the in-memory `__eh_frame` section mapped by the loader.
494 dwarf: ?Dwarf.Unwind,
495 };
496
497 const LoadedMachO = struct {
498 mapped_memory: []align(std.heap.page_size_min) const u8,
499 symbols: []const MachoSymbol,
500 strings: []const u8,
501 /// This is not necessarily the same as the vmaddr_slide that dyld would report. This is
502 /// because the segments in the file on disk might differ from the ones in memory. Normally
503 /// we wouldn't necessarily expect that to work, but /usr/lib/dyld is incredibly annoying:
504 /// it exists on disk (necessarily, because the kernel needs to load it!), but is also in
505 /// the dyld cache (dyld actually restart itself from cache after loading it), and the two
506 /// versions have (very) different segment base addresses. It's sort of like a large slide
507 /// has been applied to all addresses in memory. For an optimal experience, we consider the
508 /// on-disk vmaddr instead of the in-memory one.
509 vaddr_offset: usize,
510 };
511
512 fn getUnwindInfo(module: *Module, gpa: Allocator) Error!*Unwind {
513 if (module.unwind == null) module.unwind = loadUnwindInfo(module, gpa);
514 return if (module.unwind.?) |*unwind| unwind else |err| err;
515 }
516 fn loadUnwindInfo(module: *const Module, gpa: Allocator) Error!Unwind {
517 const header: *std.macho.mach_header = @ptrFromInt(module.text_base);
518
519 var it: macho.LoadCommandIterator = .{
520 .ncmds = header.ncmds,
521 .buffer = @as([*]u8, @ptrCast(header))[@sizeOf(macho.mach_header_64)..][0..header.sizeofcmds],
522 };
523 const sections, const text_vmaddr = while (it.next()) |load_cmd| {
524 if (load_cmd.cmd() != .SEGMENT_64) continue;
525 const segment_cmd = load_cmd.cast(macho.segment_command_64).?;
526 if (!mem.eql(u8, segment_cmd.segName(), "__TEXT")) continue;
527 break .{ load_cmd.getSections(), segment_cmd.vmaddr };
528 } else unreachable;
529
530 const vmaddr_slide = module.text_base - text_vmaddr;
531
532 var opt_unwind_info: ?[]const u8 = null;
533 var opt_eh_frame: ?[]const u8 = null;
534 for (sections) |sect| {
535 if (mem.eql(u8, sect.sectName(), "__unwind_info")) {
536 const sect_ptr: [*]u8 = @ptrFromInt(@as(usize, @intCast(vmaddr_slide + sect.addr)));
537 opt_unwind_info = sect_ptr[0..@intCast(sect.size)];
538 } else if (mem.eql(u8, sect.sectName(), "__eh_frame")) {
539 const sect_ptr: [*]u8 = @ptrFromInt(@as(usize, @intCast(vmaddr_slide + sect.addr)));
540 opt_eh_frame = sect_ptr[0..@intCast(sect.size)];
541 }
542 }
543 const eh_frame = opt_eh_frame orelse return .{
544 .vmaddr_slide = vmaddr_slide,
545 .unwind_info = opt_unwind_info,
546 .dwarf = null,
547 };
548 var dwarf: Dwarf.Unwind = .initSection(.eh_frame, @intFromPtr(eh_frame.ptr) - vmaddr_slide, eh_frame);
549 errdefer dwarf.deinit(gpa);
550 // We don't need lookups, so this call is just for scanning CIEs.
551 dwarf.prepare(gpa, @sizeOf(usize), native_endian, false, true) catch |err| switch (err) {
552 error.ReadFailed => unreachable, // it's all fixed buffers
553 error.InvalidDebugInfo,
554 error.MissingDebugInfo,
555 error.OutOfMemory,
556 => |e| return e,
557 error.EndOfStream,
558 error.Overflow,
559 error.StreamTooLong,
560 error.InvalidOperand,
561 error.InvalidOpcode,
562 error.InvalidOperation,
563 => return error.InvalidDebugInfo,
564 error.UnsupportedAddrSize,
565 error.UnsupportedDwarfVersion,
566 error.UnimplementedUserOpcode,
567 => return error.UnsupportedDebugInfo,
568 };
569
570 return .{
571 .vmaddr_slide = vmaddr_slide,
572 .unwind_info = opt_unwind_info,
573 .dwarf = dwarf,
574 };
575 }
576
577 fn getLoadedMachO(module: *Module, gpa: Allocator) Error!*LoadedMachO {
578 if (module.loaded_macho == null) module.loaded_macho = loadMachO(module, gpa) catch |err| switch (err) {
579 error.InvalidDebugInfo, error.MissingDebugInfo, error.OutOfMemory, error.Unexpected => |e| e,
580 else => error.ReadFailed,
581 };
582 return if (module.loaded_macho.?) |*lm| lm else |err| err;
583 }
584 fn loadMachO(module: *const Module, gpa: Allocator) Error!LoadedMachO {
585 const all_mapped_memory = try mapDebugInfoFile(module.name);
586 errdefer posix.munmap(all_mapped_memory);
587
588 // In most cases, the file we just mapped is a Mach-O binary. However, it could be a "universal
589 // binary": a simple file format which contains Mach-O binaries for multiple targets. For
590 // instance, `/usr/lib/dyld` is currently distributed as a universal binary containing images
591 // for both ARM64 macOS and x86_64 macOS.
592 if (all_mapped_memory.len < 4) return error.InvalidDebugInfo;
593 const magic = @as(*const u32, @ptrCast(all_mapped_memory.ptr)).*;
594 // The contents of a Mach-O file, which may or may not be the whole of `all_mapped_memory`.
595 const mapped_macho = switch (magic) {
596 macho.MH_MAGIC_64 => all_mapped_memory,
597
598 macho.FAT_CIGAM => mapped_macho: {
599 // This is the universal binary format (aka a "fat binary"). Annoyingly, the whole thing
600 // is big-endian, so we'll be swapping some bytes.
601 if (all_mapped_memory.len < @sizeOf(macho.fat_header)) return error.InvalidDebugInfo;
602 const hdr: *const macho.fat_header = @ptrCast(all_mapped_memory.ptr);
603 const archs_ptr: [*]const macho.fat_arch = @ptrCast(all_mapped_memory.ptr + @sizeOf(macho.fat_header));
604 const archs: []const macho.fat_arch = archs_ptr[0..@byteSwap(hdr.nfat_arch)];
605 const native_cpu_type = switch (builtin.cpu.arch) {
606 .x86_64 => macho.CPU_TYPE_X86_64,
607 .aarch64 => macho.CPU_TYPE_ARM64,
608 else => comptime unreachable,
609 };
610 for (archs) |*arch| {
611 if (@byteSwap(arch.cputype) != native_cpu_type) continue;
612 const offset = @byteSwap(arch.offset);
613 const size = @byteSwap(arch.size);
614 break :mapped_macho all_mapped_memory[offset..][0..size];
615 }
616 // Our native architecture was not present in the fat binary.
617 return error.MissingDebugInfo;
618 },
619
620 // Even on modern 64-bit targets, this format doesn't seem to be too extensively used. It
621 // will be fairly easy to add support here if necessary; it's very similar to above.
622 macho.FAT_CIGAM_64 => return error.UnsupportedDebugInfo,
623
624 else => return error.InvalidDebugInfo,
625 };
626
627 const hdr: *const macho.mach_header_64 = @ptrCast(@alignCast(mapped_macho.ptr));
628 if (hdr.magic != macho.MH_MAGIC_64)
629 return error.InvalidDebugInfo;
630
631 const symtab: macho.symtab_command, const text_vmaddr: u64 = lc_iter: {
632 var it: macho.LoadCommandIterator = .{
633 .ncmds = hdr.ncmds,
634 .buffer = mapped_macho[@sizeOf(macho.mach_header_64)..][0..hdr.sizeofcmds],
635 };
636 var symtab: ?macho.symtab_command = null;
637 var text_vmaddr: ?u64 = null;
638 while (it.next()) |cmd| switch (cmd.cmd()) {
639 .SYMTAB => symtab = cmd.cast(macho.symtab_command) orelse return error.InvalidDebugInfo,
640 .SEGMENT_64 => if (cmd.cast(macho.segment_command_64)) |seg_cmd| {
641 if (!mem.eql(u8, seg_cmd.segName(), "__TEXT")) continue;
642 text_vmaddr = seg_cmd.vmaddr;
643 },
644 else => {},
645 };
646 break :lc_iter .{
647 symtab orelse return error.MissingDebugInfo,
648 text_vmaddr orelse return error.MissingDebugInfo,
649 };
650 };
651
652 const syms_ptr: [*]align(1) const macho.nlist_64 = @ptrCast(mapped_macho[symtab.symoff..]);
653 const syms = syms_ptr[0..symtab.nsyms];
654 const strings = mapped_macho[symtab.stroff..][0 .. symtab.strsize - 1];
655
656 var symbols: std.ArrayList(MachoSymbol) = try .initCapacity(gpa, syms.len);
657 defer symbols.deinit(gpa);
658
659 // This map is temporary; it is used only to detect duplicates here. This is
660 // necessary because we prefer to use STAB ("symbolic debugging table") symbols,
661 // but they might not be present, so we track normal symbols too.
662 // Indices match 1-1 with those of `symbols`.
663 var symbol_names: std.StringArrayHashMapUnmanaged(void) = .empty;
664 defer symbol_names.deinit(gpa);
665 try symbol_names.ensureUnusedCapacity(gpa, syms.len);
666
667 var ofile: u32 = undefined;
668 var last_sym: MachoSymbol = undefined;
669 var state: enum {
670 init,
671 oso_open,
672 oso_close,
673 bnsym,
674 fun_strx,
675 fun_size,
676 ensym,
677 } = .init;
678
679 for (syms) |*sym| {
680 if (sym.n_type.bits.is_stab == 0) {
681 if (sym.n_strx == 0) continue;
682 switch (sym.n_type.bits.type) {
683 .undf, .pbud, .indr, .abs, _ => continue,
684 .sect => {
685 const name = std.mem.sliceTo(strings[sym.n_strx..], 0);
686 const gop = symbol_names.getOrPutAssumeCapacity(name);
687 if (!gop.found_existing) {
688 assert(gop.index == symbols.items.len);
689 symbols.appendAssumeCapacity(.{
690 .strx = sym.n_strx,
691 .addr = sym.n_value,
692 .ofile = MachoSymbol.unknown_ofile,
693 });
694 }
695 },
696 }
697 continue;
698 }
699
700 // TODO handle globals N_GSYM, and statics N_STSYM
701 switch (sym.n_type.stab) {
702 .oso => switch (state) {
703 .init, .oso_close => {
704 state = .oso_open;
705 ofile = sym.n_strx;
706 },
707 else => return error.InvalidDebugInfo,
708 },
709 .bnsym => switch (state) {
710 .oso_open, .ensym => {
711 state = .bnsym;
712 last_sym = .{
713 .strx = 0,
714 .addr = sym.n_value,
715 .ofile = ofile,
716 };
717 },
718 else => return error.InvalidDebugInfo,
719 },
720 .fun => switch (state) {
721 .bnsym => {
722 state = .fun_strx;
723 last_sym.strx = sym.n_strx;
724 },
725 .fun_strx => {
726 state = .fun_size;
727 },
728 else => return error.InvalidDebugInfo,
729 },
730 .ensym => switch (state) {
731 .fun_size => {
732 state = .ensym;
733 if (last_sym.strx != 0) {
734 const name = std.mem.sliceTo(strings[last_sym.strx..], 0);
735 const gop = symbol_names.getOrPutAssumeCapacity(name);
736 if (!gop.found_existing) {
737 assert(gop.index == symbols.items.len);
738 symbols.appendAssumeCapacity(last_sym);
739 } else {
740 symbols.items[gop.index] = last_sym;
741 }
742 }
743 },
744 else => return error.InvalidDebugInfo,
745 },
746 .so => switch (state) {
747 .init, .oso_close => {},
748 .oso_open, .ensym => {
749 state = .oso_close;
750 },
751 else => return error.InvalidDebugInfo,
752 },
753 else => {},
754 }
755 }
756
757 switch (state) {
758 .init => {
759 // Missing STAB symtab entries is still okay, unless there were also no normal symbols.
760 if (symbols.items.len == 0) return error.MissingDebugInfo;
761 },
762 .oso_close => {},
763 else => return error.InvalidDebugInfo, // corrupted STAB entries in symtab
764 }
765
766 const symbols_slice = try symbols.toOwnedSlice(gpa);
767 errdefer gpa.free(symbols_slice);
768
769 // Even though lld emits symbols in ascending order, this debug code
770 // should work for programs linked in any valid way.
771 // This sort is so that we can binary search later.
772 mem.sort(MachoSymbol, symbols_slice, {}, MachoSymbol.addressLessThan);
773
774 return .{
775 .mapped_memory = all_mapped_memory,
776 .symbols = symbols_slice,
777 .strings = strings,
778 .vaddr_offset = module.text_base - text_vmaddr,
779 };
780 }
781};
782
783const OFile = struct {
784 mapped_memory: []align(std.heap.page_size_min) const u8,
785 dwarf: Dwarf,
786 strtab: []const u8,
787 symtab: []align(1) const macho.nlist_64,
788 /// All named symbols in `symtab`. Stored `u32` key is the index into `symtab`. Accessed
789 /// through `SymbolAdapter`, so that the symbol name is used as the logical key.
790 symbols_by_name: std.ArrayHashMapUnmanaged(u32, void, void, true),
791
792 const SymbolAdapter = struct {
793 strtab: []const u8,
794 symtab: []align(1) const macho.nlist_64,
795 pub fn hash(ctx: SymbolAdapter, sym_name: []const u8) u32 {
796 _ = ctx;
797 return @truncate(std.hash.Wyhash.hash(0, sym_name));
798 }
799 pub fn eql(ctx: SymbolAdapter, a_sym_name: []const u8, b_sym_index: u32, b_index: usize) bool {
800 _ = b_index;
801 const b_sym = ctx.symtab[b_sym_index];
802 const b_sym_name = std.mem.sliceTo(ctx.strtab[b_sym.n_strx..], 0);
803 return mem.eql(u8, a_sym_name, b_sym_name);
804 }
805 };
806};
807
808const MachoSymbol = struct {
809 strx: u32,
810 addr: u64,
811 /// Value may be `unknown_ofile`.
812 ofile: u32,
813 const unknown_ofile = std.math.maxInt(u32);
814 fn addressLessThan(context: void, lhs: MachoSymbol, rhs: MachoSymbol) bool {
815 _ = context;
816 return lhs.addr < rhs.addr;
817 }
818 /// Assumes that `symbols` is sorted in order of ascending `addr`.
819 fn find(symbols: []const MachoSymbol, address: usize) ?*const MachoSymbol {
820 if (symbols.len == 0) return null; // no potential match
821 if (address < symbols[0].addr) return null; // address is before the lowest-address symbol
822 var left: usize = 0;
823 var len: usize = symbols.len;
824 while (len > 1) {
825 const mid = left + len / 2;
826 if (address < symbols[mid].addr) {
827 len /= 2;
828 } else {
829 left = mid;
830 len -= len / 2;
831 }
832 }
833 return &symbols[left];
834 }
835
836 test find {
837 const symbols: []const MachoSymbol = &.{
838 .{ .addr = 100, .strx = undefined, .ofile = undefined },
839 .{ .addr = 200, .strx = undefined, .ofile = undefined },
840 .{ .addr = 300, .strx = undefined, .ofile = undefined },
841 };
842
843 try testing.expectEqual(null, find(symbols, 0));
844 try testing.expectEqual(null, find(symbols, 99));
845 try testing.expectEqual(&symbols[0], find(symbols, 100).?);
846 try testing.expectEqual(&symbols[0], find(symbols, 150).?);
847 try testing.expectEqual(&symbols[0], find(symbols, 199).?);
848
849 try testing.expectEqual(&symbols[1], find(symbols, 200).?);
850 try testing.expectEqual(&symbols[1], find(symbols, 250).?);
851 try testing.expectEqual(&symbols[1], find(symbols, 299).?);
852
853 try testing.expectEqual(&symbols[2], find(symbols, 300).?);
854 try testing.expectEqual(&symbols[2], find(symbols, 301).?);
855 try testing.expectEqual(&symbols[2], find(symbols, 5000).?);
856 }
857};
858test {
859 _ = MachoSymbol;
860}
861
862/// Uses `mmap` to map the file at `path` into memory.
863fn mapDebugInfoFile(path: []const u8) ![]align(std.heap.page_size_min) const u8 {
864 const file = std.fs.cwd().openFile(path, .{}) catch |err| switch (err) {
865 error.FileNotFound => return error.MissingDebugInfo,
866 else => return error.ReadFailed,
867 };
868 defer file.close();
869
870 const file_end_pos = file.getEndPos() catch |err| switch (err) {
871 error.Unexpected => |e| return e,
872 else => return error.ReadFailed,
873 };
874 const file_len = std.math.cast(usize, file_end_pos) orelse return error.InvalidDebugInfo;
875
876 return posix.mmap(
877 null,
878 file_len,
879 posix.PROT.READ,
880 .{ .TYPE = .SHARED },
881 file.handle,
882 0,
883 ) catch |err| switch (err) {
884 error.Unexpected => |e| return e,
885 else => return error.ReadFailed,
886 };
887}
888
889fn loadOFile(gpa: Allocator, o_file_path: []const u8) !OFile {
890 const mapped_mem = try mapDebugInfoFile(o_file_path);
891 errdefer posix.munmap(mapped_mem);
892
893 if (mapped_mem.len < @sizeOf(macho.mach_header_64)) return error.InvalidDebugInfo;
894 const hdr: *const macho.mach_header_64 = @ptrCast(@alignCast(mapped_mem.ptr));
895 if (hdr.magic != std.macho.MH_MAGIC_64) return error.InvalidDebugInfo;
896
897 const seg_cmd: macho.LoadCommandIterator.LoadCommand, const symtab_cmd: macho.symtab_command = cmds: {
898 var seg_cmd: ?macho.LoadCommandIterator.LoadCommand = null;
899 var symtab_cmd: ?macho.symtab_command = null;
900 var it: macho.LoadCommandIterator = .{
901 .ncmds = hdr.ncmds,
902 .buffer = mapped_mem[@sizeOf(macho.mach_header_64)..][0..hdr.sizeofcmds],
903 };
904 while (it.next()) |cmd| switch (cmd.cmd()) {
905 .SEGMENT_64 => seg_cmd = cmd,
906 .SYMTAB => symtab_cmd = cmd.cast(macho.symtab_command) orelse return error.InvalidDebugInfo,
907 else => {},
908 };
909 break :cmds .{
910 seg_cmd orelse return error.MissingDebugInfo,
911 symtab_cmd orelse return error.MissingDebugInfo,
912 };
913 };
914
915 if (mapped_mem.len < symtab_cmd.stroff + symtab_cmd.strsize) return error.InvalidDebugInfo;
916 if (mapped_mem[symtab_cmd.stroff + symtab_cmd.strsize - 1] != 0) return error.InvalidDebugInfo;
917 const strtab = mapped_mem[symtab_cmd.stroff..][0 .. symtab_cmd.strsize - 1];
918
919 const n_sym_bytes = symtab_cmd.nsyms * @sizeOf(macho.nlist_64);
920 if (mapped_mem.len < symtab_cmd.symoff + n_sym_bytes) return error.InvalidDebugInfo;
921 const symtab: []align(1) const macho.nlist_64 = @ptrCast(mapped_mem[symtab_cmd.symoff..][0..n_sym_bytes]);
922
923 // TODO handle tentative (common) symbols
924 var symbols_by_name: std.ArrayHashMapUnmanaged(u32, void, void, true) = .empty;
925 defer symbols_by_name.deinit(gpa);
926 try symbols_by_name.ensureUnusedCapacity(gpa, @intCast(symtab.len));
927 for (symtab, 0..) |sym, sym_index| {
928 if (sym.n_strx == 0) continue;
929 switch (sym.n_type.bits.type) {
930 .undf => continue, // includes tentative symbols
931 .abs => continue,
932 else => {},
933 }
934 const sym_name = mem.sliceTo(strtab[sym.n_strx..], 0);
935 const gop = symbols_by_name.getOrPutAssumeCapacityAdapted(
936 @as([]const u8, sym_name),
937 @as(OFile.SymbolAdapter, .{ .strtab = strtab, .symtab = symtab }),
938 );
939 if (gop.found_existing) return error.InvalidDebugInfo;
940 gop.key_ptr.* = @intCast(sym_index);
941 }
942
943 var sections: Dwarf.SectionArray = @splat(null);
944 for (seg_cmd.getSections()) |sect| {
945 if (!std.mem.eql(u8, "__DWARF", sect.segName())) continue;
946
947 const section_index: usize = inline for (@typeInfo(Dwarf.Section.Id).@"enum".fields, 0..) |section, i| {
948 if (mem.eql(u8, "__" ++ section.name, sect.sectName())) break i;
949 } else continue;
950
951 if (mapped_mem.len < sect.offset + sect.size) return error.InvalidDebugInfo;
952 const section_bytes = mapped_mem[sect.offset..][0..sect.size];
953 sections[section_index] = .{
954 .data = section_bytes,
955 .owned = false,
956 };
957 }
958
959 const missing_debug_info =
960 sections[@intFromEnum(Dwarf.Section.Id.debug_info)] == null or
961 sections[@intFromEnum(Dwarf.Section.Id.debug_abbrev)] == null or
962 sections[@intFromEnum(Dwarf.Section.Id.debug_str)] == null or
963 sections[@intFromEnum(Dwarf.Section.Id.debug_line)] == null;
964 if (missing_debug_info) return error.MissingDebugInfo;
965
966 var dwarf: Dwarf = .{ .sections = sections };
967 errdefer dwarf.deinit(gpa);
968 try dwarf.open(gpa, native_endian);
969
970 return .{
971 .mapped_memory = mapped_mem,
972 .dwarf = dwarf,
973 .strtab = strtab,
974 .symtab = symtab,
975 .symbols_by_name = symbols_by_name.move(),
976 };
977}
978
979const std = @import("std");
980const Allocator = std.mem.Allocator;
981const Dwarf = std.debug.Dwarf;
982const Error = std.debug.SelfInfoError;
983const assert = std.debug.assert;
984const posix = std.posix;
985const macho = std.macho;
986const mem = std.mem;
987const testing = std.testing;
988const dwarfRegNative = std.debug.Dwarf.SelfUnwinder.regNative;
989
990const builtin = @import("builtin");
991const native_endian = builtin.target.cpu.arch.endian();
992
993const SelfInfo = @This();