Commit 1120546f72
Changed files (13)
lib
test
standalone
coff_dwarf
lib/std/debug/Dwarf/expression.zig
@@ -10,7 +10,7 @@ const assert = std.debug.assert;
const testing = std.testing;
const Writer = std.Io.Writer;
-const regNative = std.debug.SelfInfo.DwarfUnwindContext.regNative;
+const regNative = std.debug.Dwarf.SelfUnwinder.regNative;
const ip_reg_num = std.debug.Dwarf.ipRegNum(native_arch).?;
const fp_reg_num = std.debug.Dwarf.fpRegNum(native_arch);
lib/std/debug/Dwarf/SelfUnwinder.zig
@@ -0,0 +1,334 @@
+//! Implements stack unwinding based on `Dwarf.Unwind`. The caller is responsible for providing the
+//! initialized `Dwarf.Unwind` from the `.debug_frame` (or equivalent) section; this type handles
+//! computing and applying the CFI register rules to evolve a `std.debug.cpu_context.Native` through
+//! stack frames, hence performing the virtual unwind.
+//!
+//! Notably, this type is a valid implementation of `std.debug.SelfInfo.UnwindContext`.
+
+/// The state of the CPU in the current stack frame.
+cpu_state: std.debug.cpu_context.Native,
+/// The value of the Program Counter in this frame. This is almost the same as the value of the IP
+/// register in `cpu_state`, but may be off by one because the IP is typically a *return* address.
+pc: usize,
+
+cfi_vm: Dwarf.Unwind.VirtualMachine,
+expr_vm: Dwarf.expression.StackMachine(.{ .call_frame_context = true }),
+
+pub const CacheEntry = struct {
+ const max_regs = 32;
+
+ pc: usize,
+ cie: *const Dwarf.Unwind.CommonInformationEntry,
+ cfa_rule: Dwarf.Unwind.VirtualMachine.CfaRule,
+ num_rules: u8,
+ rules_regs: [max_regs]u16,
+ rules: [max_regs]Dwarf.Unwind.VirtualMachine.RegisterRule,
+
+ pub fn find(entries: []const CacheEntry, pc: usize) ?*const CacheEntry {
+ assert(pc != 0);
+ const idx = std.hash.int(pc) % entries.len;
+ const entry = &entries[idx];
+ return if (entry.pc == pc) entry else null;
+ }
+
+ pub fn populate(entry: *const CacheEntry, entries: []CacheEntry) void {
+ const idx = std.hash.int(entry.pc) % entries.len;
+ entries[idx] = entry.*;
+ }
+
+ pub const empty: CacheEntry = .{
+ .pc = 0,
+ .cie = undefined,
+ .cfa_rule = undefined,
+ .num_rules = undefined,
+ .rules_regs = undefined,
+ .rules = undefined,
+ };
+};
+
+pub fn init(cpu_context: *const std.debug.cpu_context.Native) SelfUnwinder {
+ // `@constCast` is safe because we aren't going to store to the resulting pointer.
+ const raw_pc_ptr = regNative(@constCast(cpu_context), ip_reg_num) catch |err| switch (err) {
+ error.InvalidRegister => unreachable, // `ip_reg_num` is definitely valid
+ error.UnsupportedRegister => unreachable, // the implementation needs to support ip
+ error.IncompatibleRegisterSize => unreachable, // ip is definitely `usize`-sized
+ };
+ const pc = stripInstructionPtrAuthCode(raw_pc_ptr.*);
+ return .{
+ .cpu_state = cpu_context.*,
+ .pc = pc,
+ .cfi_vm = .{},
+ .expr_vm = .{},
+ };
+}
+
+pub fn deinit(unwinder: *SelfUnwinder, gpa: Allocator) void {
+ unwinder.cfi_vm.deinit(gpa);
+ unwinder.expr_vm.deinit(gpa);
+ unwinder.* = undefined;
+}
+
+pub fn getFp(unwinder: *const SelfUnwinder) usize {
+ // `@constCast` is safe because we aren't going to store to the resulting pointer.
+ const ptr = regNative(@constCast(&unwinder.cpu_state), fp_reg_num) catch |err| switch (err) {
+ error.InvalidRegister => unreachable, // `fp_reg_num` is definitely valid
+ error.UnsupportedRegister => unreachable, // the implementation needs to support fp
+ error.IncompatibleRegisterSize => unreachable, // fp is a pointer so is `usize`-sized
+ };
+ return ptr.*;
+}
+
+/// Compute the rule set for the address `unwinder.pc` from the information in `unwind`. The caller
+/// may store the returned rule set in a simple fixed-size cache keyed on the `pc` field to avoid
+/// frequently recomputing register rules when unwinding many times.
+///
+/// To actually apply the computed rules, see `next`.
+pub fn computeRules(
+ unwinder: *SelfUnwinder,
+ gpa: Allocator,
+ unwind: *const Dwarf.Unwind,
+ load_offset: usize,
+ explicit_fde_offset: ?usize,
+) !CacheEntry {
+ assert(unwinder.pc != 0);
+
+ const pc_vaddr = unwinder.pc - load_offset;
+
+ const fde_offset = explicit_fde_offset orelse try unwind.lookupPc(
+ pc_vaddr,
+ @sizeOf(usize),
+ native_endian,
+ ) orelse return error.MissingDebugInfo;
+ const cie, const fde = try unwind.getFde(fde_offset, native_endian);
+
+ // `lookupPc` can return false positives, so check if the FDE *actually* includes the pc
+ if (pc_vaddr < fde.pc_begin or pc_vaddr >= fde.pc_begin + fde.pc_range) {
+ return error.MissingDebugInfo;
+ }
+
+ unwinder.cfi_vm.reset();
+ const row = try unwinder.cfi_vm.runTo(gpa, pc_vaddr, cie, &fde, @sizeOf(usize), native_endian);
+ const cols = unwinder.cfi_vm.rowColumns(&row);
+
+ if (cols.len > CacheEntry.max_regs) return error.UnsupportedDebugInfo;
+
+ var entry: CacheEntry = .{
+ .pc = unwinder.pc,
+ .cie = cie,
+ .cfa_rule = row.cfa,
+ .num_rules = @intCast(cols.len),
+ .rules_regs = undefined,
+ .rules = undefined,
+ };
+ for (cols, 0..) |col, i| {
+ entry.rules_regs[i] = col.register;
+ entry.rules[i] = col.rule;
+ }
+ return entry;
+}
+
+/// Applies the register rules given in `cache_entry` to the current state of `unwinder`. The caller
+/// is responsible for ensuring that `cache_entry` contains the correct rule set for `unwinder.pc`.
+///
+/// `unwinder.cpu_state` and `unwinder.pc` are updated to refer to the next frame, and this frame's
+/// return address is returned as a `usize`.
+pub fn next(unwinder: *SelfUnwinder, gpa: Allocator, cache_entry: *const CacheEntry) std.debug.SelfInfoError!usize {
+ return unwinder.nextInner(gpa, cache_entry) catch |err| switch (err) {
+ error.OutOfMemory,
+ error.InvalidDebugInfo,
+ => |e| return e,
+
+ error.UnsupportedRegister,
+ error.UnimplementedExpressionCall,
+ error.UnimplementedOpcode,
+ error.UnimplementedUserOpcode,
+ error.UnimplementedTypedComparison,
+ error.UnimplementedTypeConversion,
+ error.UnknownExpressionOpcode,
+ => return error.UnsupportedDebugInfo,
+
+ error.ReadFailed,
+ error.EndOfStream,
+ error.Overflow,
+ error.IncompatibleRegisterSize,
+ error.InvalidRegister,
+ error.IncompleteExpressionContext,
+ error.InvalidCFAOpcode,
+ error.InvalidExpression,
+ error.InvalidFrameBase,
+ error.InvalidIntegralTypeSize,
+ error.InvalidSubExpression,
+ error.InvalidTypeLength,
+ error.TruncatedIntegralType,
+ error.DivisionByZero,
+ => return error.InvalidDebugInfo,
+ };
+}
+
+fn nextInner(unwinder: *SelfUnwinder, gpa: Allocator, cache_entry: *const CacheEntry) !usize {
+ const format = cache_entry.cie.format;
+ const return_address_register = cache_entry.cie.return_address_register;
+
+ const cfa = switch (cache_entry.cfa_rule) {
+ .none => return error.InvalidDebugInfo,
+ .reg_off => |ro| cfa: {
+ const ptr = try regNative(&unwinder.cpu_state, ro.register);
+ break :cfa try applyOffset(ptr.*, ro.offset);
+ },
+ .expression => |expr| cfa: {
+ // On all implemented architectures, the CFA is defined to be the previous frame's SP
+ const prev_cfa_val = (try regNative(&unwinder.cpu_state, sp_reg_num)).*;
+ unwinder.expr_vm.reset();
+ const value = try unwinder.expr_vm.run(expr, gpa, .{
+ .format = format,
+ .cpu_context = &unwinder.cpu_state,
+ }, prev_cfa_val) orelse return error.InvalidDebugInfo;
+ switch (value) {
+ .generic => |g| break :cfa g,
+ else => return error.InvalidDebugInfo,
+ }
+ },
+ };
+
+ // If unspecified, we'll use the default rule for the return address register, which is
+ // typically equivalent to `.undefined` (meaning there is no return address), but may be
+ // overriden by ABIs.
+ var has_return_address: bool = builtin.cpu.arch.isAARCH64() and
+ return_address_register >= 19 and
+ return_address_register <= 28;
+
+ // Create a copy of the CPU state, to which we will apply the new rules.
+ var new_cpu_state = unwinder.cpu_state;
+
+ // On all implemented architectures, the CFA is defined to be the previous frame's SP
+ (try regNative(&new_cpu_state, sp_reg_num)).* = cfa;
+
+ const rules_len = cache_entry.num_rules;
+ for (cache_entry.rules_regs[0..rules_len], cache_entry.rules[0..rules_len]) |register, rule| {
+ const new_val: union(enum) {
+ same,
+ undefined,
+ val: usize,
+ bytes: []const u8,
+ } = switch (rule) {
+ .default => val: {
+ // The default rule is typically equivalent to `.undefined`, but ABIs may override it.
+ if (builtin.cpu.arch.isAARCH64() and register >= 19 and register <= 28) {
+ break :val .same;
+ }
+ break :val .undefined;
+ },
+ .undefined => .undefined,
+ .same_value => .same,
+ .offset => |offset| val: {
+ const ptr: *const usize = @ptrFromInt(try applyOffset(cfa, offset));
+ break :val .{ .val = ptr.* };
+ },
+ .val_offset => |offset| .{ .val = try applyOffset(cfa, offset) },
+ .register => |r| .{ .bytes = try unwinder.cpu_state.dwarfRegisterBytes(r) },
+ .expression => |expr| val: {
+ unwinder.expr_vm.reset();
+ const value = try unwinder.expr_vm.run(expr, gpa, .{
+ .format = format,
+ .cpu_context = &unwinder.cpu_state,
+ }, cfa) orelse return error.InvalidDebugInfo;
+ const ptr: *const usize = switch (value) {
+ .generic => |addr| @ptrFromInt(addr),
+ else => return error.InvalidDebugInfo,
+ };
+ break :val .{ .val = ptr.* };
+ },
+ .val_expression => |expr| val: {
+ unwinder.expr_vm.reset();
+ const value = try unwinder.expr_vm.run(expr, gpa, .{
+ .format = format,
+ .cpu_context = &unwinder.cpu_state,
+ }, cfa) orelse return error.InvalidDebugInfo;
+ switch (value) {
+ .generic => |val| break :val .{ .val = val },
+ else => return error.InvalidDebugInfo,
+ }
+ },
+ };
+ switch (new_val) {
+ .same => {},
+ .undefined => {
+ const dest = try new_cpu_state.dwarfRegisterBytes(@intCast(register));
+ @memset(dest, undefined);
+ },
+ .val => |val| {
+ const dest = try new_cpu_state.dwarfRegisterBytes(@intCast(register));
+ if (dest.len != @sizeOf(usize)) return error.InvalidDebugInfo;
+ const dest_ptr: *align(1) usize = @ptrCast(dest);
+ dest_ptr.* = val;
+ },
+ .bytes => |src| {
+ const dest = try new_cpu_state.dwarfRegisterBytes(@intCast(register));
+ if (dest.len != src.len) return error.InvalidDebugInfo;
+ @memcpy(dest, src);
+ },
+ }
+ if (register == return_address_register) {
+ has_return_address = new_val != .undefined;
+ }
+ }
+
+ const return_address: usize = if (has_return_address) pc: {
+ const raw_ptr = try regNative(&new_cpu_state, return_address_register);
+ break :pc stripInstructionPtrAuthCode(raw_ptr.*);
+ } else 0;
+
+ (try regNative(&new_cpu_state, ip_reg_num)).* = return_address;
+
+ // The new CPU state is complete; flush changes.
+ unwinder.cpu_state = new_cpu_state;
+
+ // The caller will subtract 1 from the return address to get an address corresponding to the
+ // function call. However, if this is a signal frame, that's actually incorrect, because the
+ // "return address" we have is the instruction which triggered the signal (if the signal
+ // handler returned, the instruction would be re-run). Compensate for this by incrementing
+ // the address in that case.
+ const adjusted_ret_addr = if (cache_entry.cie.is_signal_frame) return_address +| 1 else return_address;
+
+ // We also want to do that same subtraction here to get the PC for the next frame's FDE.
+ // This is because if the callee was noreturn, then the function call might be the caller's
+ // last instruction, so `return_address` might actually point outside of it!
+ unwinder.pc = adjusted_ret_addr -| 1;
+
+ return adjusted_ret_addr;
+}
+
+pub fn regNative(ctx: *std.debug.cpu_context.Native, num: u16) error{
+ InvalidRegister,
+ UnsupportedRegister,
+ IncompatibleRegisterSize,
+}!*align(1) usize {
+ const bytes = try ctx.dwarfRegisterBytes(num);
+ if (bytes.len != @sizeOf(usize)) return error.IncompatibleRegisterSize;
+ return @ptrCast(bytes);
+}
+
+/// Since register rules are applied (usually) during a panic,
+/// checked addition / subtraction is used so that we can return
+/// an error and fall back to FP-based unwinding.
+fn applyOffset(base: usize, offset: i64) !usize {
+ return if (offset >= 0)
+ try std.math.add(usize, base, @as(usize, @intCast(offset)))
+ else
+ try std.math.sub(usize, base, @as(usize, @intCast(-offset)));
+}
+
+const ip_reg_num = Dwarf.ipRegNum(builtin.target.cpu.arch).?;
+const fp_reg_num = Dwarf.fpRegNum(builtin.target.cpu.arch);
+const sp_reg_num = Dwarf.spRegNum(builtin.target.cpu.arch);
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const Dwarf = std.debug.Dwarf;
+const assert = std.debug.assert;
+const stripInstructionPtrAuthCode = std.debug.stripInstructionPtrAuthCode;
+
+const builtin = @import("builtin");
+const native_endian = builtin.target.cpu.arch.endian();
+
+const SelfUnwinder = @This();
lib/std/debug/Dwarf/Unwind.zig
@@ -530,16 +530,18 @@ pub fn prepare(
};
if (saw_terminator != expect_terminator) return bad();
- std.mem.sortUnstable(SortedFdeEntry, fde_list.items, {}, struct {
- fn lessThan(ctx: void, a: SortedFdeEntry, b: SortedFdeEntry) bool {
- ctx;
- return a.pc_begin < b.pc_begin;
- }
- }.lessThan);
+ if (need_lookup) {
+ std.mem.sortUnstable(SortedFdeEntry, fde_list.items, {}, struct {
+ fn lessThan(ctx: void, a: SortedFdeEntry, b: SortedFdeEntry) bool {
+ ctx;
+ return a.pc_begin < b.pc_begin;
+ }
+ }.lessThan);
- // This temporary is necessary to avoid an RLS footgun where `lookup` ends up non-null `undefined` on OOM.
- const final_fdes = try fde_list.toOwnedSlice(gpa);
- unwind.lookup = .{ .sorted_fdes = final_fdes };
+ // This temporary is necessary to avoid an RLS footgun where `lookup` ends up non-null `undefined` on OOM.
+ const final_fdes = try fde_list.toOwnedSlice(gpa);
+ unwind.lookup = .{ .sorted_fdes = final_fdes };
+ }
}
fn findCie(unwind: *const Unwind, offset: u64) ?*const CommonInformationEntry {
lib/std/debug/SelfInfo/Darwin.zig
@@ -0,0 +1,993 @@
+mutex: std.Thread.Mutex,
+/// Accessed through `Module.Adapter`.
+modules: std.ArrayHashMapUnmanaged(Module, void, Module.Context, false),
+ofiles: std.StringArrayHashMapUnmanaged(?OFile),
+
+pub const init: SelfInfo = .{
+ .mutex = .{},
+ .modules = .empty,
+ .ofiles = .empty,
+};
+pub fn deinit(si: *SelfInfo, gpa: Allocator) void {
+ for (si.modules.keys()) |*module| {
+ unwind: {
+ const u = &(module.unwind orelse break :unwind catch break :unwind);
+ if (u.dwarf) |*dwarf| dwarf.deinit(gpa);
+ }
+ loaded: {
+ const l = &(module.loaded_macho orelse break :loaded catch break :loaded);
+ gpa.free(l.symbols);
+ posix.munmap(l.mapped_memory);
+ }
+ }
+ for (si.ofiles.values()) |*opt_ofile| {
+ const ofile = &(opt_ofile.* orelse continue);
+ ofile.dwarf.deinit(gpa);
+ ofile.symbols_by_name.deinit(gpa);
+ posix.munmap(ofile.mapped_memory);
+ }
+ si.modules.deinit(gpa);
+ si.ofiles.deinit(gpa);
+}
+
+pub fn getSymbol(si: *SelfInfo, gpa: Allocator, address: usize) Error!std.debug.Symbol {
+ const module = try si.findModule(gpa, address);
+ defer si.mutex.unlock();
+
+ const loaded_macho = try module.getLoadedMachO(gpa);
+
+ const vaddr = address - loaded_macho.vaddr_offset;
+ const symbol = MachoSymbol.find(loaded_macho.symbols, vaddr) orelse return .unknown;
+
+ // offset of `address` from start of `symbol`
+ const address_symbol_offset = vaddr - symbol.addr;
+
+ // Take the symbol name from the N_FUN STAB entry, we're going to
+ // use it if we fail to find the DWARF infos
+ const stab_symbol = mem.sliceTo(loaded_macho.strings[symbol.strx..], 0);
+
+ // If any information is missing, we can at least return this from now on.
+ const sym_only_result: std.debug.Symbol = .{
+ .name = stab_symbol,
+ .compile_unit_name = null,
+ .source_location = null,
+ };
+
+ if (symbol.ofile == MachoSymbol.unknown_ofile) {
+ // We don't have STAB info, so can't track down the object file; all we can do is the symbol name.
+ return sym_only_result;
+ }
+
+ const o_file: *OFile = of: {
+ const path = mem.sliceTo(loaded_macho.strings[symbol.ofile..], 0);
+ const gop = try si.ofiles.getOrPut(gpa, path);
+ if (!gop.found_existing) {
+ gop.value_ptr.* = loadOFile(gpa, path) catch null;
+ }
+ if (gop.value_ptr.*) |*o_file| {
+ break :of o_file;
+ } else {
+ return sym_only_result;
+ }
+ };
+
+ const symbol_index = o_file.symbols_by_name.getKeyAdapted(
+ @as([]const u8, stab_symbol),
+ @as(OFile.SymbolAdapter, .{ .strtab = o_file.strtab, .symtab = o_file.symtab }),
+ ) orelse return sym_only_result;
+ const symbol_ofile_vaddr = o_file.symtab[symbol_index].n_value;
+
+ const compile_unit = o_file.dwarf.findCompileUnit(native_endian, symbol_ofile_vaddr) catch return sym_only_result;
+
+ return .{
+ .name = o_file.dwarf.getSymbolName(symbol_ofile_vaddr + address_symbol_offset) orelse stab_symbol,
+ .compile_unit_name = compile_unit.die.getAttrString(
+ &o_file.dwarf,
+ native_endian,
+ std.dwarf.AT.name,
+ o_file.dwarf.section(.debug_str),
+ compile_unit,
+ ) catch |err| switch (err) {
+ error.MissingDebugInfo, error.InvalidDebugInfo => null,
+ },
+ .source_location = o_file.dwarf.getLineNumberInfo(
+ gpa,
+ native_endian,
+ compile_unit,
+ symbol_ofile_vaddr + address_symbol_offset,
+ ) catch null,
+ };
+}
+pub fn getModuleName(si: *SelfInfo, gpa: Allocator, address: usize) Error![]const u8 {
+ const module = try si.findModule(gpa, address);
+ defer si.mutex.unlock();
+ return module.name;
+}
+
+pub const can_unwind: bool = true;
+pub const UnwindContext = std.debug.Dwarf.SelfUnwinder;
+/// Unwind a frame using MachO compact unwind info (from `__unwind_info`).
+/// If the compact encoding can't encode a way to unwind a frame, it will
+/// defer unwinding to DWARF, in which case `__eh_frame` will be used if available.
+pub fn unwindFrame(si: *SelfInfo, gpa: Allocator, context: *UnwindContext) Error!usize {
+ return unwindFrameInner(si, gpa, context) catch |err| switch (err) {
+ error.InvalidDebugInfo,
+ error.MissingDebugInfo,
+ error.UnsupportedDebugInfo,
+ error.ReadFailed,
+ error.OutOfMemory,
+ error.Unexpected,
+ => |e| return e,
+ error.UnsupportedRegister,
+ error.UnsupportedAddrSize,
+ error.UnimplementedUserOpcode,
+ => return error.UnsupportedDebugInfo,
+ error.Overflow,
+ error.EndOfStream,
+ error.StreamTooLong,
+ error.InvalidOpcode,
+ error.InvalidOperation,
+ error.InvalidOperand,
+ error.InvalidRegister,
+ error.IncompatibleRegisterSize,
+ => return error.InvalidDebugInfo,
+ };
+}
+fn unwindFrameInner(si: *SelfInfo, gpa: Allocator, context: *UnwindContext) !usize {
+ const module = try si.findModule(gpa, context.pc);
+ defer si.mutex.unlock();
+
+ const unwind: *Module.Unwind = try module.getUnwindInfo(gpa);
+
+ const ip_reg_num = comptime Dwarf.ipRegNum(builtin.target.cpu.arch).?;
+ const fp_reg_num = comptime Dwarf.fpRegNum(builtin.target.cpu.arch);
+ const sp_reg_num = comptime Dwarf.spRegNum(builtin.target.cpu.arch);
+
+ const unwind_info = unwind.unwind_info orelse return error.MissingDebugInfo;
+ if (unwind_info.len < @sizeOf(macho.unwind_info_section_header)) return error.InvalidDebugInfo;
+ const header: *align(1) const macho.unwind_info_section_header = @ptrCast(unwind_info);
+
+ const index_byte_count = header.indexCount * @sizeOf(macho.unwind_info_section_header_index_entry);
+ if (unwind_info.len < header.indexSectionOffset + index_byte_count) return error.InvalidDebugInfo;
+ const indices: []align(1) const macho.unwind_info_section_header_index_entry = @ptrCast(unwind_info[header.indexSectionOffset..][0..index_byte_count]);
+ if (indices.len == 0) return error.MissingDebugInfo;
+
+ // offset of the PC into the `__TEXT` segment
+ const pc_text_offset = context.pc - module.text_base;
+
+ const start_offset: u32, const first_level_offset: u32 = index: {
+ var left: usize = 0;
+ var len: usize = indices.len;
+ while (len > 1) {
+ const mid = left + len / 2;
+ if (pc_text_offset < indices[mid].functionOffset) {
+ len /= 2;
+ } else {
+ left = mid;
+ len -= len / 2;
+ }
+ }
+ break :index .{ indices[left].secondLevelPagesSectionOffset, indices[left].functionOffset };
+ };
+ // An offset of 0 is a sentinel indicating a range does not have unwind info.
+ if (start_offset == 0) return error.MissingDebugInfo;
+
+ const common_encodings_byte_count = header.commonEncodingsArrayCount * @sizeOf(macho.compact_unwind_encoding_t);
+ if (unwind_info.len < header.commonEncodingsArraySectionOffset + common_encodings_byte_count) return error.InvalidDebugInfo;
+ const common_encodings: []align(1) const macho.compact_unwind_encoding_t = @ptrCast(
+ unwind_info[header.commonEncodingsArraySectionOffset..][0..common_encodings_byte_count],
+ );
+
+ if (unwind_info.len < start_offset + @sizeOf(macho.UNWIND_SECOND_LEVEL)) return error.InvalidDebugInfo;
+ const kind: *align(1) const macho.UNWIND_SECOND_LEVEL = @ptrCast(unwind_info[start_offset..]);
+
+ const entry: struct {
+ function_offset: usize,
+ raw_encoding: u32,
+ } = switch (kind.*) {
+ .REGULAR => entry: {
+ if (unwind_info.len < start_offset + @sizeOf(macho.unwind_info_regular_second_level_page_header)) return error.InvalidDebugInfo;
+ const page_header: *align(1) const macho.unwind_info_regular_second_level_page_header = @ptrCast(unwind_info[start_offset..]);
+
+ const entries_byte_count = page_header.entryCount * @sizeOf(macho.unwind_info_regular_second_level_entry);
+ if (unwind_info.len < start_offset + entries_byte_count) return error.InvalidDebugInfo;
+ const entries: []align(1) const macho.unwind_info_regular_second_level_entry = @ptrCast(
+ unwind_info[start_offset + page_header.entryPageOffset ..][0..entries_byte_count],
+ );
+ if (entries.len == 0) return error.InvalidDebugInfo;
+
+ var left: usize = 0;
+ var len: usize = entries.len;
+ while (len > 1) {
+ const mid = left + len / 2;
+ if (pc_text_offset < entries[mid].functionOffset) {
+ len /= 2;
+ } else {
+ left = mid;
+ len -= len / 2;
+ }
+ }
+ break :entry .{
+ .function_offset = entries[left].functionOffset,
+ .raw_encoding = entries[left].encoding,
+ };
+ },
+ .COMPRESSED => entry: {
+ if (unwind_info.len < start_offset + @sizeOf(macho.unwind_info_compressed_second_level_page_header)) return error.InvalidDebugInfo;
+ const page_header: *align(1) const macho.unwind_info_compressed_second_level_page_header = @ptrCast(unwind_info[start_offset..]);
+
+ const entries_byte_count = page_header.entryCount * @sizeOf(macho.UnwindInfoCompressedEntry);
+ if (unwind_info.len < start_offset + entries_byte_count) return error.InvalidDebugInfo;
+ const entries: []align(1) const macho.UnwindInfoCompressedEntry = @ptrCast(
+ unwind_info[start_offset + page_header.entryPageOffset ..][0..entries_byte_count],
+ );
+ if (entries.len == 0) return error.InvalidDebugInfo;
+
+ var left: usize = 0;
+ var len: usize = entries.len;
+ while (len > 1) {
+ const mid = left + len / 2;
+ if (pc_text_offset < first_level_offset + entries[mid].funcOffset) {
+ len /= 2;
+ } else {
+ left = mid;
+ len -= len / 2;
+ }
+ }
+ const entry = entries[left];
+
+ const function_offset = first_level_offset + entry.funcOffset;
+ if (entry.encodingIndex < common_encodings.len) {
+ break :entry .{
+ .function_offset = function_offset,
+ .raw_encoding = common_encodings[entry.encodingIndex],
+ };
+ }
+
+ const local_index = entry.encodingIndex - common_encodings.len;
+ const local_encodings_byte_count = page_header.encodingsCount * @sizeOf(macho.compact_unwind_encoding_t);
+ if (unwind_info.len < start_offset + page_header.encodingsPageOffset + local_encodings_byte_count) return error.InvalidDebugInfo;
+ const local_encodings: []align(1) const macho.compact_unwind_encoding_t = @ptrCast(
+ unwind_info[start_offset + page_header.encodingsPageOffset ..][0..local_encodings_byte_count],
+ );
+ if (local_index >= local_encodings.len) return error.InvalidDebugInfo;
+ break :entry .{
+ .function_offset = function_offset,
+ .raw_encoding = local_encodings[local_index],
+ };
+ },
+ else => return error.InvalidDebugInfo,
+ };
+
+ if (entry.raw_encoding == 0) return error.MissingDebugInfo;
+
+ const encoding: macho.CompactUnwindEncoding = @bitCast(entry.raw_encoding);
+ const new_ip = switch (builtin.cpu.arch) {
+ .x86_64 => switch (encoding.mode.x86_64) {
+ .OLD => return error.UnsupportedDebugInfo,
+ .RBP_FRAME => ip: {
+ const frame = encoding.value.x86_64.frame;
+
+ const fp = (try dwarfRegNative(&context.cpu_state, fp_reg_num)).*;
+ const new_sp = fp + 2 * @sizeOf(usize);
+
+ const ip_ptr = fp + @sizeOf(usize);
+ const new_ip = @as(*const usize, @ptrFromInt(ip_ptr)).*;
+ const new_fp = @as(*const usize, @ptrFromInt(fp)).*;
+
+ (try dwarfRegNative(&context.cpu_state, fp_reg_num)).* = new_fp;
+ (try dwarfRegNative(&context.cpu_state, sp_reg_num)).* = new_sp;
+ (try dwarfRegNative(&context.cpu_state, ip_reg_num)).* = new_ip;
+
+ const regs: [5]u3 = .{
+ frame.reg0,
+ frame.reg1,
+ frame.reg2,
+ frame.reg3,
+ frame.reg4,
+ };
+ for (regs, 0..) |reg, i| {
+ if (reg == 0) continue;
+ const addr = fp - frame.frame_offset * @sizeOf(usize) + i * @sizeOf(usize);
+ const reg_number = try Dwarf.compactUnwindToDwarfRegNumber(reg);
+ (try dwarfRegNative(&context.cpu_state, reg_number)).* = @as(*const usize, @ptrFromInt(addr)).*;
+ }
+
+ break :ip new_ip;
+ },
+ .STACK_IMMD,
+ .STACK_IND,
+ => ip: {
+ const frameless = encoding.value.x86_64.frameless;
+
+ const sp = (try dwarfRegNative(&context.cpu_state, sp_reg_num)).*;
+ const stack_size: usize = stack_size: {
+ if (encoding.mode.x86_64 == .STACK_IMMD) {
+ break :stack_size @as(usize, frameless.stack.direct.stack_size) * @sizeOf(usize);
+ }
+ // In .STACK_IND, the stack size is inferred from the subq instruction at the beginning of the function.
+ const sub_offset_addr =
+ module.text_base +
+ entry.function_offset +
+ frameless.stack.indirect.sub_offset;
+ // `sub_offset_addr` points to the offset of the literal within the instruction
+ const sub_operand = @as(*align(1) const u32, @ptrFromInt(sub_offset_addr)).*;
+ break :stack_size sub_operand + @sizeOf(usize) * @as(usize, frameless.stack.indirect.stack_adjust);
+ };
+
+ // Decode the Lehmer-coded sequence of registers.
+ // For a description of the encoding see lib/libc/include/any-macos.13-any/mach-o/compact_unwind_encoding.h
+
+ // Decode the variable-based permutation number into its digits. Each digit represents
+ // an index into the list of register numbers that weren't yet used in the sequence at
+ // the time the digit was added.
+ const reg_count = frameless.stack_reg_count;
+ const ip_ptr = ip_ptr: {
+ var digits: [6]u3 = undefined;
+ var accumulator: usize = frameless.stack_reg_permutation;
+ var base: usize = 2;
+ for (0..reg_count) |i| {
+ const div = accumulator / base;
+ digits[digits.len - 1 - i] = @intCast(accumulator - base * div);
+ accumulator = div;
+ base += 1;
+ }
+
+ var registers: [6]u3 = undefined;
+ var used_indices: [6]bool = @splat(false);
+ for (digits[digits.len - reg_count ..], 0..) |target_unused_index, i| {
+ var unused_count: u8 = 0;
+ const unused_index = for (used_indices, 0..) |used, index| {
+ if (!used) {
+ if (target_unused_index == unused_count) break index;
+ unused_count += 1;
+ }
+ } else unreachable;
+ registers[i] = @intCast(unused_index + 1);
+ used_indices[unused_index] = true;
+ }
+
+ var reg_addr = sp + stack_size - @sizeOf(usize) * @as(usize, reg_count + 1);
+ for (0..reg_count) |i| {
+ const reg_number = try Dwarf.compactUnwindToDwarfRegNumber(registers[i]);
+ (try dwarfRegNative(&context.cpu_state, reg_number)).* = @as(*const usize, @ptrFromInt(reg_addr)).*;
+ reg_addr += @sizeOf(usize);
+ }
+
+ break :ip_ptr reg_addr;
+ };
+
+ const new_ip = @as(*const usize, @ptrFromInt(ip_ptr)).*;
+ const new_sp = ip_ptr + @sizeOf(usize);
+
+ (try dwarfRegNative(&context.cpu_state, sp_reg_num)).* = new_sp;
+ (try dwarfRegNative(&context.cpu_state, ip_reg_num)).* = new_ip;
+
+ break :ip new_ip;
+ },
+ .DWARF => {
+ const dwarf = &(unwind.dwarf orelse return error.MissingDebugInfo);
+ const rules = try context.computeRules(gpa, dwarf, unwind.vmaddr_slide, encoding.value.x86_64.dwarf);
+ return context.next(gpa, &rules);
+ },
+ },
+ .aarch64, .aarch64_be => switch (encoding.mode.arm64) {
+ .OLD => return error.UnsupportedDebugInfo,
+ .FRAMELESS => ip: {
+ const sp = (try dwarfRegNative(&context.cpu_state, sp_reg_num)).*;
+ const new_sp = sp + encoding.value.arm64.frameless.stack_size * 16;
+ const new_ip = (try dwarfRegNative(&context.cpu_state, 30)).*;
+ (try dwarfRegNative(&context.cpu_state, sp_reg_num)).* = new_sp;
+ break :ip new_ip;
+ },
+ .DWARF => {
+ const dwarf = &(unwind.dwarf orelse return error.MissingDebugInfo);
+ const rules = try context.computeRules(gpa, dwarf, unwind.vmaddr_slide, encoding.value.arm64.dwarf);
+ return context.next(gpa, &rules);
+ },
+ .FRAME => ip: {
+ const frame = encoding.value.arm64.frame;
+
+ const fp = (try dwarfRegNative(&context.cpu_state, fp_reg_num)).*;
+ const ip_ptr = fp + @sizeOf(usize);
+
+ var reg_addr = fp - @sizeOf(usize);
+ inline for (@typeInfo(@TypeOf(frame.x_reg_pairs)).@"struct".fields, 0..) |field, i| {
+ if (@field(frame.x_reg_pairs, field.name) != 0) {
+ (try dwarfRegNative(&context.cpu_state, 19 + i)).* = @as(*const usize, @ptrFromInt(reg_addr)).*;
+ reg_addr += @sizeOf(usize);
+ (try dwarfRegNative(&context.cpu_state, 20 + i)).* = @as(*const usize, @ptrFromInt(reg_addr)).*;
+ reg_addr += @sizeOf(usize);
+ }
+ }
+
+ inline for (@typeInfo(@TypeOf(frame.d_reg_pairs)).@"struct".fields, 0..) |field, i| {
+ if (@field(frame.d_reg_pairs, field.name) != 0) {
+ // Only the lower half of the 128-bit V registers are restored during unwinding
+ {
+ const dest: *align(1) usize = @ptrCast(try context.cpu_state.dwarfRegisterBytes(64 + 8 + i));
+ dest.* = @as(*const usize, @ptrFromInt(reg_addr)).*;
+ }
+ reg_addr += @sizeOf(usize);
+ {
+ const dest: *align(1) usize = @ptrCast(try context.cpu_state.dwarfRegisterBytes(64 + 9 + i));
+ dest.* = @as(*const usize, @ptrFromInt(reg_addr)).*;
+ }
+ reg_addr += @sizeOf(usize);
+ }
+ }
+
+ const new_ip = @as(*const usize, @ptrFromInt(ip_ptr)).*;
+ const new_fp = @as(*const usize, @ptrFromInt(fp)).*;
+
+ (try dwarfRegNative(&context.cpu_state, fp_reg_num)).* = new_fp;
+ (try dwarfRegNative(&context.cpu_state, ip_reg_num)).* = new_ip;
+
+ break :ip new_ip;
+ },
+ },
+ else => comptime unreachable, // unimplemented
+ };
+
+ const ret_addr = std.debug.stripInstructionPtrAuthCode(new_ip);
+
+ // Like `Dwarf.SelfUnwinder.next`, adjust our next lookup pc in case the `call` was this
+ // function's last instruction making `ret_addr` one byte past its end.
+ context.pc = ret_addr -| 1;
+
+ return ret_addr;
+}
+
+/// Acquires the mutex on success.
+fn findModule(si: *SelfInfo, gpa: Allocator, address: usize) Error!*Module {
+ var info: std.c.dl_info = undefined;
+ if (std.c.dladdr(@ptrFromInt(address), &info) == 0) {
+ return error.MissingDebugInfo;
+ }
+ si.mutex.lock();
+ errdefer si.mutex.unlock();
+ const gop = try si.modules.getOrPutAdapted(gpa, @intFromPtr(info.fbase), Module.Adapter{});
+ errdefer comptime unreachable;
+ if (!gop.found_existing) {
+ gop.key_ptr.* = .{
+ .text_base = @intFromPtr(info.fbase),
+ .name = std.mem.span(info.fname),
+ .unwind = null,
+ .loaded_macho = null,
+ };
+ }
+ return gop.key_ptr;
+}
+
+const Module = struct {
+ text_base: usize,
+ name: []const u8,
+ unwind: ?(Error!Unwind),
+ loaded_macho: ?(Error!LoadedMachO),
+
+ const Adapter = struct {
+ pub fn hash(_: Adapter, text_base: usize) u32 {
+ return @truncate(std.hash.int(text_base));
+ }
+ pub fn eql(_: Adapter, a_text_base: usize, b_module: Module, b_index: usize) bool {
+ _ = b_index;
+ return a_text_base == b_module.text_base;
+ }
+ };
+ const Context = struct {
+ pub fn hash(_: Context, module: Module) u32 {
+ return @truncate(std.hash.int(module.text_base));
+ }
+ pub fn eql(_: Context, a_module: Module, b_module: Module, b_index: usize) bool {
+ _ = b_index;
+ return a_module.text_base == b_module.text_base;
+ }
+ };
+
+ const Unwind = struct {
+ /// The slide applied to the `__unwind_info` and `__eh_frame` sections.
+ /// So, `unwind_info.ptr` is this many bytes higher than the section's vmaddr.
+ vmaddr_slide: u64,
+ /// Backed by the in-memory section mapped by the loader.
+ unwind_info: ?[]const u8,
+ /// Backed by the in-memory `__eh_frame` section mapped by the loader.
+ dwarf: ?Dwarf.Unwind,
+ };
+
+ const LoadedMachO = struct {
+ mapped_memory: []align(std.heap.page_size_min) const u8,
+ symbols: []const MachoSymbol,
+ strings: []const u8,
+ /// This is not necessarily the same as the vmaddr_slide that dyld would report. This is
+ /// because the segments in the file on disk might differ from the ones in memory. Normally
+ /// we wouldn't necessarily expect that to work, but /usr/lib/dyld is incredibly annoying:
+ /// it exists on disk (necessarily, because the kernel needs to load it!), but is also in
+ /// the dyld cache (dyld actually restart itself from cache after loading it), and the two
+ /// versions have (very) different segment base addresses. It's sort of like a large slide
+ /// has been applied to all addresses in memory. For an optimal experience, we consider the
+ /// on-disk vmaddr instead of the in-memory one.
+ vaddr_offset: usize,
+ };
+
+ fn getUnwindInfo(module: *Module, gpa: Allocator) Error!*Unwind {
+ if (module.unwind == null) module.unwind = loadUnwindInfo(module, gpa);
+ return if (module.unwind.?) |*unwind| unwind else |err| err;
+ }
+ fn loadUnwindInfo(module: *const Module, gpa: Allocator) Error!Unwind {
+ const header: *std.macho.mach_header = @ptrFromInt(module.text_base);
+
+ var it: macho.LoadCommandIterator = .{
+ .ncmds = header.ncmds,
+ .buffer = @as([*]u8, @ptrCast(header))[@sizeOf(macho.mach_header_64)..][0..header.sizeofcmds],
+ };
+ const sections, const text_vmaddr = while (it.next()) |load_cmd| {
+ if (load_cmd.cmd() != .SEGMENT_64) continue;
+ const segment_cmd = load_cmd.cast(macho.segment_command_64).?;
+ if (!mem.eql(u8, segment_cmd.segName(), "__TEXT")) continue;
+ break .{ load_cmd.getSections(), segment_cmd.vmaddr };
+ } else unreachable;
+
+ const vmaddr_slide = module.text_base - text_vmaddr;
+
+ var opt_unwind_info: ?[]const u8 = null;
+ var opt_eh_frame: ?[]const u8 = null;
+ for (sections) |sect| {
+ if (mem.eql(u8, sect.sectName(), "__unwind_info")) {
+ const sect_ptr: [*]u8 = @ptrFromInt(@as(usize, @intCast(vmaddr_slide + sect.addr)));
+ opt_unwind_info = sect_ptr[0..@intCast(sect.size)];
+ } else if (mem.eql(u8, sect.sectName(), "__eh_frame")) {
+ const sect_ptr: [*]u8 = @ptrFromInt(@as(usize, @intCast(vmaddr_slide + sect.addr)));
+ opt_eh_frame = sect_ptr[0..@intCast(sect.size)];
+ }
+ }
+ const eh_frame = opt_eh_frame orelse return .{
+ .vmaddr_slide = vmaddr_slide,
+ .unwind_info = opt_unwind_info,
+ .dwarf = null,
+ };
+ var dwarf: Dwarf.Unwind = .initSection(.eh_frame, @intFromPtr(eh_frame.ptr) - vmaddr_slide, eh_frame);
+ errdefer dwarf.deinit(gpa);
+ // We don't need lookups, so this call is just for scanning CIEs.
+ dwarf.prepare(gpa, @sizeOf(usize), native_endian, false, true) catch |err| switch (err) {
+ error.ReadFailed => unreachable, // it's all fixed buffers
+ error.InvalidDebugInfo,
+ error.MissingDebugInfo,
+ error.OutOfMemory,
+ => |e| return e,
+ error.EndOfStream,
+ error.Overflow,
+ error.StreamTooLong,
+ error.InvalidOperand,
+ error.InvalidOpcode,
+ error.InvalidOperation,
+ => return error.InvalidDebugInfo,
+ error.UnsupportedAddrSize,
+ error.UnsupportedDwarfVersion,
+ error.UnimplementedUserOpcode,
+ => return error.UnsupportedDebugInfo,
+ };
+
+ return .{
+ .vmaddr_slide = vmaddr_slide,
+ .unwind_info = opt_unwind_info,
+ .dwarf = dwarf,
+ };
+ }
+
+ fn getLoadedMachO(module: *Module, gpa: Allocator) Error!*LoadedMachO {
+ if (module.loaded_macho == null) module.loaded_macho = loadMachO(module, gpa) catch |err| switch (err) {
+ error.InvalidDebugInfo, error.MissingDebugInfo, error.OutOfMemory, error.Unexpected => |e| e,
+ else => error.ReadFailed,
+ };
+ return if (module.loaded_macho.?) |*lm| lm else |err| err;
+ }
+ fn loadMachO(module: *const Module, gpa: Allocator) Error!LoadedMachO {
+ const all_mapped_memory = try mapDebugInfoFile(module.name);
+ errdefer posix.munmap(all_mapped_memory);
+
+ // In most cases, the file we just mapped is a Mach-O binary. However, it could be a "universal
+ // binary": a simple file format which contains Mach-O binaries for multiple targets. For
+ // instance, `/usr/lib/dyld` is currently distributed as a universal binary containing images
+ // for both ARM64 macOS and x86_64 macOS.
+ if (all_mapped_memory.len < 4) return error.InvalidDebugInfo;
+ const magic = @as(*const u32, @ptrCast(all_mapped_memory.ptr)).*;
+ // The contents of a Mach-O file, which may or may not be the whole of `all_mapped_memory`.
+ const mapped_macho = switch (magic) {
+ macho.MH_MAGIC_64 => all_mapped_memory,
+
+ macho.FAT_CIGAM => mapped_macho: {
+ // This is the universal binary format (aka a "fat binary"). Annoyingly, the whole thing
+ // is big-endian, so we'll be swapping some bytes.
+ if (all_mapped_memory.len < @sizeOf(macho.fat_header)) return error.InvalidDebugInfo;
+ const hdr: *const macho.fat_header = @ptrCast(all_mapped_memory.ptr);
+ const archs_ptr: [*]const macho.fat_arch = @ptrCast(all_mapped_memory.ptr + @sizeOf(macho.fat_header));
+ const archs: []const macho.fat_arch = archs_ptr[0..@byteSwap(hdr.nfat_arch)];
+ const native_cpu_type = switch (builtin.cpu.arch) {
+ .x86_64 => macho.CPU_TYPE_X86_64,
+ .aarch64 => macho.CPU_TYPE_ARM64,
+ else => comptime unreachable,
+ };
+ for (archs) |*arch| {
+ if (@byteSwap(arch.cputype) != native_cpu_type) continue;
+ const offset = @byteSwap(arch.offset);
+ const size = @byteSwap(arch.size);
+ break :mapped_macho all_mapped_memory[offset..][0..size];
+ }
+ // Our native architecture was not present in the fat binary.
+ return error.MissingDebugInfo;
+ },
+
+ // Even on modern 64-bit targets, this format doesn't seem to be too extensively used. It
+ // will be fairly easy to add support here if necessary; it's very similar to above.
+ macho.FAT_CIGAM_64 => return error.UnsupportedDebugInfo,
+
+ else => return error.InvalidDebugInfo,
+ };
+
+ const hdr: *const macho.mach_header_64 = @ptrCast(@alignCast(mapped_macho.ptr));
+ if (hdr.magic != macho.MH_MAGIC_64)
+ return error.InvalidDebugInfo;
+
+ const symtab: macho.symtab_command, const text_vmaddr: u64 = lc_iter: {
+ var it: macho.LoadCommandIterator = .{
+ .ncmds = hdr.ncmds,
+ .buffer = mapped_macho[@sizeOf(macho.mach_header_64)..][0..hdr.sizeofcmds],
+ };
+ var symtab: ?macho.symtab_command = null;
+ var text_vmaddr: ?u64 = null;
+ while (it.next()) |cmd| switch (cmd.cmd()) {
+ .SYMTAB => symtab = cmd.cast(macho.symtab_command) orelse return error.InvalidDebugInfo,
+ .SEGMENT_64 => if (cmd.cast(macho.segment_command_64)) |seg_cmd| {
+ if (!mem.eql(u8, seg_cmd.segName(), "__TEXT")) continue;
+ text_vmaddr = seg_cmd.vmaddr;
+ },
+ else => {},
+ };
+ break :lc_iter .{
+ symtab orelse return error.MissingDebugInfo,
+ text_vmaddr orelse return error.MissingDebugInfo,
+ };
+ };
+
+ const syms_ptr: [*]align(1) const macho.nlist_64 = @ptrCast(mapped_macho[symtab.symoff..]);
+ const syms = syms_ptr[0..symtab.nsyms];
+ const strings = mapped_macho[symtab.stroff..][0 .. symtab.strsize - 1];
+
+ var symbols: std.ArrayList(MachoSymbol) = try .initCapacity(gpa, syms.len);
+ defer symbols.deinit(gpa);
+
+ // This map is temporary; it is used only to detect duplicates here. This is
+ // necessary because we prefer to use STAB ("symbolic debugging table") symbols,
+ // but they might not be present, so we track normal symbols too.
+ // Indices match 1-1 with those of `symbols`.
+ var symbol_names: std.StringArrayHashMapUnmanaged(void) = .empty;
+ defer symbol_names.deinit(gpa);
+ try symbol_names.ensureUnusedCapacity(gpa, syms.len);
+
+ var ofile: u32 = undefined;
+ var last_sym: MachoSymbol = undefined;
+ var state: enum {
+ init,
+ oso_open,
+ oso_close,
+ bnsym,
+ fun_strx,
+ fun_size,
+ ensym,
+ } = .init;
+
+ for (syms) |*sym| {
+ if (sym.n_type.bits.is_stab == 0) {
+ if (sym.n_strx == 0) continue;
+ switch (sym.n_type.bits.type) {
+ .undf, .pbud, .indr, .abs, _ => continue,
+ .sect => {
+ const name = std.mem.sliceTo(strings[sym.n_strx..], 0);
+ const gop = symbol_names.getOrPutAssumeCapacity(name);
+ if (!gop.found_existing) {
+ assert(gop.index == symbols.items.len);
+ symbols.appendAssumeCapacity(.{
+ .strx = sym.n_strx,
+ .addr = sym.n_value,
+ .ofile = MachoSymbol.unknown_ofile,
+ });
+ }
+ },
+ }
+ continue;
+ }
+
+ // TODO handle globals N_GSYM, and statics N_STSYM
+ switch (sym.n_type.stab) {
+ .oso => switch (state) {
+ .init, .oso_close => {
+ state = .oso_open;
+ ofile = sym.n_strx;
+ },
+ else => return error.InvalidDebugInfo,
+ },
+ .bnsym => switch (state) {
+ .oso_open, .ensym => {
+ state = .bnsym;
+ last_sym = .{
+ .strx = 0,
+ .addr = sym.n_value,
+ .ofile = ofile,
+ };
+ },
+ else => return error.InvalidDebugInfo,
+ },
+ .fun => switch (state) {
+ .bnsym => {
+ state = .fun_strx;
+ last_sym.strx = sym.n_strx;
+ },
+ .fun_strx => {
+ state = .fun_size;
+ },
+ else => return error.InvalidDebugInfo,
+ },
+ .ensym => switch (state) {
+ .fun_size => {
+ state = .ensym;
+ if (last_sym.strx != 0) {
+ const name = std.mem.sliceTo(strings[last_sym.strx..], 0);
+ const gop = symbol_names.getOrPutAssumeCapacity(name);
+ if (!gop.found_existing) {
+ assert(gop.index == symbols.items.len);
+ symbols.appendAssumeCapacity(last_sym);
+ } else {
+ symbols.items[gop.index] = last_sym;
+ }
+ }
+ },
+ else => return error.InvalidDebugInfo,
+ },
+ .so => switch (state) {
+ .init, .oso_close => {},
+ .oso_open, .ensym => {
+ state = .oso_close;
+ },
+ else => return error.InvalidDebugInfo,
+ },
+ else => {},
+ }
+ }
+
+ switch (state) {
+ .init => {
+ // Missing STAB symtab entries is still okay, unless there were also no normal symbols.
+ if (symbols.items.len == 0) return error.MissingDebugInfo;
+ },
+ .oso_close => {},
+ else => return error.InvalidDebugInfo, // corrupted STAB entries in symtab
+ }
+
+ const symbols_slice = try symbols.toOwnedSlice(gpa);
+ errdefer gpa.free(symbols_slice);
+
+ // Even though lld emits symbols in ascending order, this debug code
+ // should work for programs linked in any valid way.
+ // This sort is so that we can binary search later.
+ mem.sort(MachoSymbol, symbols_slice, {}, MachoSymbol.addressLessThan);
+
+ return .{
+ .mapped_memory = all_mapped_memory,
+ .symbols = symbols_slice,
+ .strings = strings,
+ .vaddr_offset = module.text_base - text_vmaddr,
+ };
+ }
+};
+
+const OFile = struct {
+ mapped_memory: []align(std.heap.page_size_min) const u8,
+ dwarf: Dwarf,
+ strtab: []const u8,
+ symtab: []align(1) const macho.nlist_64,
+ /// All named symbols in `symtab`. Stored `u32` key is the index into `symtab`. Accessed
+ /// through `SymbolAdapter`, so that the symbol name is used as the logical key.
+ symbols_by_name: std.ArrayHashMapUnmanaged(u32, void, void, true),
+
+ const SymbolAdapter = struct {
+ strtab: []const u8,
+ symtab: []align(1) const macho.nlist_64,
+ pub fn hash(ctx: SymbolAdapter, sym_name: []const u8) u32 {
+ _ = ctx;
+ return @truncate(std.hash.Wyhash.hash(0, sym_name));
+ }
+ pub fn eql(ctx: SymbolAdapter, a_sym_name: []const u8, b_sym_index: u32, b_index: usize) bool {
+ _ = b_index;
+ const b_sym = ctx.symtab[b_sym_index];
+ const b_sym_name = std.mem.sliceTo(ctx.strtab[b_sym.n_strx..], 0);
+ return mem.eql(u8, a_sym_name, b_sym_name);
+ }
+ };
+};
+
+const MachoSymbol = struct {
+ strx: u32,
+ addr: u64,
+ /// Value may be `unknown_ofile`.
+ ofile: u32,
+ const unknown_ofile = std.math.maxInt(u32);
+ fn addressLessThan(context: void, lhs: MachoSymbol, rhs: MachoSymbol) bool {
+ _ = context;
+ return lhs.addr < rhs.addr;
+ }
+ /// Assumes that `symbols` is sorted in order of ascending `addr`.
+ fn find(symbols: []const MachoSymbol, address: usize) ?*const MachoSymbol {
+ if (symbols.len == 0) return null; // no potential match
+ if (address < symbols[0].addr) return null; // address is before the lowest-address symbol
+ var left: usize = 0;
+ var len: usize = symbols.len;
+ while (len > 1) {
+ const mid = left + len / 2;
+ if (address < symbols[mid].addr) {
+ len /= 2;
+ } else {
+ left = mid;
+ len -= len / 2;
+ }
+ }
+ return &symbols[left];
+ }
+
+ test find {
+ const symbols: []const MachoSymbol = &.{
+ .{ .addr = 100, .strx = undefined, .ofile = undefined },
+ .{ .addr = 200, .strx = undefined, .ofile = undefined },
+ .{ .addr = 300, .strx = undefined, .ofile = undefined },
+ };
+
+ try testing.expectEqual(null, find(symbols, 0));
+ try testing.expectEqual(null, find(symbols, 99));
+ try testing.expectEqual(&symbols[0], find(symbols, 100).?);
+ try testing.expectEqual(&symbols[0], find(symbols, 150).?);
+ try testing.expectEqual(&symbols[0], find(symbols, 199).?);
+
+ try testing.expectEqual(&symbols[1], find(symbols, 200).?);
+ try testing.expectEqual(&symbols[1], find(symbols, 250).?);
+ try testing.expectEqual(&symbols[1], find(symbols, 299).?);
+
+ try testing.expectEqual(&symbols[2], find(symbols, 300).?);
+ try testing.expectEqual(&symbols[2], find(symbols, 301).?);
+ try testing.expectEqual(&symbols[2], find(symbols, 5000).?);
+ }
+};
+test {
+ _ = MachoSymbol;
+}
+
+/// Uses `mmap` to map the file at `path` into memory.
+fn mapDebugInfoFile(path: []const u8) ![]align(std.heap.page_size_min) const u8 {
+ const file = std.fs.cwd().openFile(path, .{}) catch |err| switch (err) {
+ error.FileNotFound => return error.MissingDebugInfo,
+ else => return error.ReadFailed,
+ };
+ defer file.close();
+
+ const file_end_pos = file.getEndPos() catch |err| switch (err) {
+ error.Unexpected => |e| return e,
+ else => return error.ReadFailed,
+ };
+ const file_len = std.math.cast(usize, file_end_pos) orelse return error.InvalidDebugInfo;
+
+ return posix.mmap(
+ null,
+ file_len,
+ posix.PROT.READ,
+ .{ .TYPE = .SHARED },
+ file.handle,
+ 0,
+ ) catch |err| switch (err) {
+ error.Unexpected => |e| return e,
+ else => return error.ReadFailed,
+ };
+}
+
+fn loadOFile(gpa: Allocator, o_file_path: []const u8) !OFile {
+ const mapped_mem = try mapDebugInfoFile(o_file_path);
+ errdefer posix.munmap(mapped_mem);
+
+ if (mapped_mem.len < @sizeOf(macho.mach_header_64)) return error.InvalidDebugInfo;
+ const hdr: *const macho.mach_header_64 = @ptrCast(@alignCast(mapped_mem.ptr));
+ if (hdr.magic != std.macho.MH_MAGIC_64) return error.InvalidDebugInfo;
+
+ const seg_cmd: macho.LoadCommandIterator.LoadCommand, const symtab_cmd: macho.symtab_command = cmds: {
+ var seg_cmd: ?macho.LoadCommandIterator.LoadCommand = null;
+ var symtab_cmd: ?macho.symtab_command = null;
+ var it: macho.LoadCommandIterator = .{
+ .ncmds = hdr.ncmds,
+ .buffer = mapped_mem[@sizeOf(macho.mach_header_64)..][0..hdr.sizeofcmds],
+ };
+ while (it.next()) |cmd| switch (cmd.cmd()) {
+ .SEGMENT_64 => seg_cmd = cmd,
+ .SYMTAB => symtab_cmd = cmd.cast(macho.symtab_command) orelse return error.InvalidDebugInfo,
+ else => {},
+ };
+ break :cmds .{
+ seg_cmd orelse return error.MissingDebugInfo,
+ symtab_cmd orelse return error.MissingDebugInfo,
+ };
+ };
+
+ if (mapped_mem.len < symtab_cmd.stroff + symtab_cmd.strsize) return error.InvalidDebugInfo;
+ if (mapped_mem[symtab_cmd.stroff + symtab_cmd.strsize - 1] != 0) return error.InvalidDebugInfo;
+ const strtab = mapped_mem[symtab_cmd.stroff..][0 .. symtab_cmd.strsize - 1];
+
+ const n_sym_bytes = symtab_cmd.nsyms * @sizeOf(macho.nlist_64);
+ if (mapped_mem.len < symtab_cmd.symoff + n_sym_bytes) return error.InvalidDebugInfo;
+ const symtab: []align(1) const macho.nlist_64 = @ptrCast(mapped_mem[symtab_cmd.symoff..][0..n_sym_bytes]);
+
+ // TODO handle tentative (common) symbols
+ var symbols_by_name: std.ArrayHashMapUnmanaged(u32, void, void, true) = .empty;
+ defer symbols_by_name.deinit(gpa);
+ try symbols_by_name.ensureUnusedCapacity(gpa, @intCast(symtab.len));
+ for (symtab, 0..) |sym, sym_index| {
+ if (sym.n_strx == 0) continue;
+ switch (sym.n_type.bits.type) {
+ .undf => continue, // includes tentative symbols
+ .abs => continue,
+ else => {},
+ }
+ const sym_name = mem.sliceTo(strtab[sym.n_strx..], 0);
+ const gop = symbols_by_name.getOrPutAssumeCapacityAdapted(
+ @as([]const u8, sym_name),
+ @as(OFile.SymbolAdapter, .{ .strtab = strtab, .symtab = symtab }),
+ );
+ if (gop.found_existing) return error.InvalidDebugInfo;
+ gop.key_ptr.* = @intCast(sym_index);
+ }
+
+ var sections: Dwarf.SectionArray = @splat(null);
+ for (seg_cmd.getSections()) |sect| {
+ if (!std.mem.eql(u8, "__DWARF", sect.segName())) continue;
+
+ const section_index: usize = inline for (@typeInfo(Dwarf.Section.Id).@"enum".fields, 0..) |section, i| {
+ if (mem.eql(u8, "__" ++ section.name, sect.sectName())) break i;
+ } else continue;
+
+ if (mapped_mem.len < sect.offset + sect.size) return error.InvalidDebugInfo;
+ const section_bytes = mapped_mem[sect.offset..][0..sect.size];
+ sections[section_index] = .{
+ .data = section_bytes,
+ .owned = false,
+ };
+ }
+
+ const missing_debug_info =
+ sections[@intFromEnum(Dwarf.Section.Id.debug_info)] == null or
+ sections[@intFromEnum(Dwarf.Section.Id.debug_abbrev)] == null or
+ sections[@intFromEnum(Dwarf.Section.Id.debug_str)] == null or
+ sections[@intFromEnum(Dwarf.Section.Id.debug_line)] == null;
+ if (missing_debug_info) return error.MissingDebugInfo;
+
+ var dwarf: Dwarf = .{ .sections = sections };
+ errdefer dwarf.deinit(gpa);
+ try dwarf.open(gpa, native_endian);
+
+ return .{
+ .mapped_memory = mapped_mem,
+ .dwarf = dwarf,
+ .strtab = strtab,
+ .symtab = symtab,
+ .symbols_by_name = symbols_by_name.move(),
+ };
+}
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const Dwarf = std.debug.Dwarf;
+const Error = std.debug.SelfInfoError;
+const assert = std.debug.assert;
+const posix = std.posix;
+const macho = std.macho;
+const mem = std.mem;
+const testing = std.testing;
+const dwarfRegNative = std.debug.Dwarf.SelfUnwinder.regNative;
+
+const builtin = @import("builtin");
+const native_endian = builtin.target.cpu.arch.endian();
+
+const SelfInfo = @This();
lib/std/debug/SelfInfo/DarwinModule.zig
@@ -1,954 +0,0 @@
-/// The runtime address where __TEXT is loaded.
-text_base: usize,
-name: []const u8,
-
-pub fn key(m: *const DarwinModule) usize {
- return m.text_base;
-}
-
-/// No cache needed, because `_dyld_get_image_header` etc are already fast.
-pub const LookupCache = void;
-pub fn lookup(cache: *LookupCache, gpa: Allocator, address: usize) Error!DarwinModule {
- _ = cache;
- _ = gpa;
- var info: std.c.dl_info = undefined;
- switch (std.c.dladdr(@ptrFromInt(address), &info)) {
- 0 => return error.MissingDebugInfo,
- else => return .{
- .name = std.mem.span(info.fname),
- .text_base = @intFromPtr(info.fbase),
- },
- }
-}
-fn loadUnwindInfo(module: *const DarwinModule, gpa: Allocator, out: *DebugInfo) !void {
- const header: *std.macho.mach_header = @ptrFromInt(module.text_base);
-
- var it: macho.LoadCommandIterator = .{
- .ncmds = header.ncmds,
- .buffer = @as([*]u8, @ptrCast(header))[@sizeOf(macho.mach_header_64)..][0..header.sizeofcmds],
- };
- const sections, const text_vmaddr = while (it.next()) |load_cmd| {
- if (load_cmd.cmd() != .SEGMENT_64) continue;
- const segment_cmd = load_cmd.cast(macho.segment_command_64).?;
- if (!mem.eql(u8, segment_cmd.segName(), "__TEXT")) continue;
- break .{ load_cmd.getSections(), segment_cmd.vmaddr };
- } else unreachable;
-
- const vmaddr_slide = module.text_base - text_vmaddr;
-
- var opt_unwind_info: ?[]const u8 = null;
- var opt_eh_frame: ?[]const u8 = null;
- for (sections) |sect| {
- if (mem.eql(u8, sect.sectName(), "__unwind_info")) {
- const sect_ptr: [*]u8 = @ptrFromInt(@as(usize, @intCast(vmaddr_slide + sect.addr)));
- opt_unwind_info = sect_ptr[0..@intCast(sect.size)];
- } else if (mem.eql(u8, sect.sectName(), "__eh_frame")) {
- const sect_ptr: [*]u8 = @ptrFromInt(@as(usize, @intCast(vmaddr_slide + sect.addr)));
- opt_eh_frame = sect_ptr[0..@intCast(sect.size)];
- }
- }
- const eh_frame = opt_eh_frame orelse {
- out.unwind = .{
- .vmaddr_slide = vmaddr_slide,
- .unwind_info = opt_unwind_info,
- .dwarf = null,
- .dwarf_cache = undefined,
- };
- return;
- };
- var dwarf: Dwarf.Unwind = .initSection(.eh_frame, @intFromPtr(eh_frame.ptr) - vmaddr_slide, eh_frame);
- errdefer dwarf.deinit(gpa);
- // We don't need lookups, so this call is just for scanning CIEs.
- dwarf.prepare(gpa, @sizeOf(usize), native_endian, false, true) catch |err| switch (err) {
- error.ReadFailed => unreachable, // it's all fixed buffers
- error.InvalidDebugInfo,
- error.MissingDebugInfo,
- error.OutOfMemory,
- => |e| return e,
- error.EndOfStream,
- error.Overflow,
- error.StreamTooLong,
- error.InvalidOperand,
- error.InvalidOpcode,
- error.InvalidOperation,
- => return error.InvalidDebugInfo,
- error.UnsupportedAddrSize,
- error.UnsupportedDwarfVersion,
- error.UnimplementedUserOpcode,
- => return error.UnsupportedDebugInfo,
- };
-
- const dwarf_cache = try gpa.create(UnwindContext.Cache);
- errdefer gpa.destroy(dwarf_cache);
- dwarf_cache.init();
-
- out.unwind = .{
- .vmaddr_slide = vmaddr_slide,
- .unwind_info = opt_unwind_info,
- .dwarf = dwarf,
- .dwarf_cache = dwarf_cache,
- };
-}
-fn loadMachO(module: *const DarwinModule, gpa: Allocator) !DebugInfo.LoadedMachO {
- const all_mapped_memory = try mapDebugInfoFile(module.name);
- errdefer posix.munmap(all_mapped_memory);
-
- // In most cases, the file we just mapped is a Mach-O binary. However, it could be a "universal
- // binary": a simple file format which contains Mach-O binaries for multiple targets. For
- // instance, `/usr/lib/dyld` is currently distributed as a universal binary containing images
- // for both ARM64 Macs and x86_64 Macs.
- if (all_mapped_memory.len < 4) return error.InvalidDebugInfo;
- const magic = @as(*const u32, @ptrCast(all_mapped_memory.ptr)).*;
- // The contents of a Mach-O file, which may or may not be the whole of `all_mapped_memory`.
- const mapped_macho = switch (magic) {
- macho.MH_MAGIC_64 => all_mapped_memory,
-
- macho.FAT_CIGAM => mapped_macho: {
- // This is the universal binary format (aka a "fat binary"). Annoyingly, the whole thing
- // is big-endian, so we'll be swapping some bytes.
- if (all_mapped_memory.len < @sizeOf(macho.fat_header)) return error.InvalidDebugInfo;
- const hdr: *const macho.fat_header = @ptrCast(all_mapped_memory.ptr);
- const archs_ptr: [*]const macho.fat_arch = @ptrCast(all_mapped_memory.ptr + @sizeOf(macho.fat_header));
- const archs: []const macho.fat_arch = archs_ptr[0..@byteSwap(hdr.nfat_arch)];
- const native_cpu_type = switch (builtin.cpu.arch) {
- .x86_64 => macho.CPU_TYPE_X86_64,
- .aarch64 => macho.CPU_TYPE_ARM64,
- else => comptime unreachable,
- };
- for (archs) |*arch| {
- if (@byteSwap(arch.cputype) != native_cpu_type) continue;
- const offset = @byteSwap(arch.offset);
- const size = @byteSwap(arch.size);
- break :mapped_macho all_mapped_memory[offset..][0..size];
- }
- // Our native architecture was not present in the fat binary.
- return error.MissingDebugInfo;
- },
-
- // Even on modern 64-bit targets, this format doesn't seem to be too extensively used. It
- // will be fairly easy to add support here if necessary; it's very similar to above.
- macho.FAT_CIGAM_64 => return error.UnsupportedDebugInfo,
-
- else => return error.InvalidDebugInfo,
- };
-
- const hdr: *const macho.mach_header_64 = @ptrCast(@alignCast(mapped_macho.ptr));
- if (hdr.magic != macho.MH_MAGIC_64)
- return error.InvalidDebugInfo;
-
- const symtab: macho.symtab_command, const text_vmaddr: u64 = lc_iter: {
- var it: macho.LoadCommandIterator = .{
- .ncmds = hdr.ncmds,
- .buffer = mapped_macho[@sizeOf(macho.mach_header_64)..][0..hdr.sizeofcmds],
- };
- var symtab: ?macho.symtab_command = null;
- var text_vmaddr: ?u64 = null;
- while (it.next()) |cmd| switch (cmd.cmd()) {
- .SYMTAB => symtab = cmd.cast(macho.symtab_command) orelse return error.InvalidDebugInfo,
- .SEGMENT_64 => if (cmd.cast(macho.segment_command_64)) |seg_cmd| {
- if (!mem.eql(u8, seg_cmd.segName(), "__TEXT")) continue;
- text_vmaddr = seg_cmd.vmaddr;
- },
- else => {},
- };
- break :lc_iter .{
- symtab orelse return error.MissingDebugInfo,
- text_vmaddr orelse return error.MissingDebugInfo,
- };
- };
-
- const syms_ptr: [*]align(1) const macho.nlist_64 = @ptrCast(mapped_macho[symtab.symoff..]);
- const syms = syms_ptr[0..symtab.nsyms];
- const strings = mapped_macho[symtab.stroff..][0 .. symtab.strsize - 1];
-
- var symbols: std.ArrayList(MachoSymbol) = try .initCapacity(gpa, syms.len);
- defer symbols.deinit(gpa);
-
- // This map is temporary; it is used only to detect duplicates here. This is
- // necessary because we prefer to use STAB ("symbolic debugging table") symbols,
- // but they might not be present, so we track normal symbols too.
- // Indices match 1-1 with those of `symbols`.
- var symbol_names: std.StringArrayHashMapUnmanaged(void) = .empty;
- defer symbol_names.deinit(gpa);
- try symbol_names.ensureUnusedCapacity(gpa, syms.len);
-
- var ofile: u32 = undefined;
- var last_sym: MachoSymbol = undefined;
- var state: enum {
- init,
- oso_open,
- oso_close,
- bnsym,
- fun_strx,
- fun_size,
- ensym,
- } = .init;
-
- for (syms) |*sym| {
- if (sym.n_type.bits.is_stab == 0) {
- if (sym.n_strx == 0) continue;
- switch (sym.n_type.bits.type) {
- .undf, .pbud, .indr, .abs, _ => continue,
- .sect => {
- const name = std.mem.sliceTo(strings[sym.n_strx..], 0);
- const gop = symbol_names.getOrPutAssumeCapacity(name);
- if (!gop.found_existing) {
- assert(gop.index == symbols.items.len);
- symbols.appendAssumeCapacity(.{
- .strx = sym.n_strx,
- .addr = sym.n_value,
- .ofile = MachoSymbol.unknown_ofile,
- });
- }
- },
- }
- continue;
- }
-
- // TODO handle globals N_GSYM, and statics N_STSYM
- switch (sym.n_type.stab) {
- .oso => switch (state) {
- .init, .oso_close => {
- state = .oso_open;
- ofile = sym.n_strx;
- },
- else => return error.InvalidDebugInfo,
- },
- .bnsym => switch (state) {
- .oso_open, .ensym => {
- state = .bnsym;
- last_sym = .{
- .strx = 0,
- .addr = sym.n_value,
- .ofile = ofile,
- };
- },
- else => return error.InvalidDebugInfo,
- },
- .fun => switch (state) {
- .bnsym => {
- state = .fun_strx;
- last_sym.strx = sym.n_strx;
- },
- .fun_strx => {
- state = .fun_size;
- },
- else => return error.InvalidDebugInfo,
- },
- .ensym => switch (state) {
- .fun_size => {
- state = .ensym;
- if (last_sym.strx != 0) {
- const name = std.mem.sliceTo(strings[last_sym.strx..], 0);
- const gop = symbol_names.getOrPutAssumeCapacity(name);
- if (!gop.found_existing) {
- assert(gop.index == symbols.items.len);
- symbols.appendAssumeCapacity(last_sym);
- } else {
- symbols.items[gop.index] = last_sym;
- }
- }
- },
- else => return error.InvalidDebugInfo,
- },
- .so => switch (state) {
- .init, .oso_close => {},
- .oso_open, .ensym => {
- state = .oso_close;
- },
- else => return error.InvalidDebugInfo,
- },
- else => {},
- }
- }
-
- switch (state) {
- .init => {
- // Missing STAB symtab entries is still okay, unless there were also no normal symbols.
- if (symbols.items.len == 0) return error.MissingDebugInfo;
- },
- .oso_close => {},
- else => return error.InvalidDebugInfo, // corrupted STAB entries in symtab
- }
-
- const symbols_slice = try symbols.toOwnedSlice(gpa);
- errdefer gpa.free(symbols_slice);
-
- // Even though lld emits symbols in ascending order, this debug code
- // should work for programs linked in any valid way.
- // This sort is so that we can binary search later.
- mem.sort(MachoSymbol, symbols_slice, {}, MachoSymbol.addressLessThan);
-
- return .{
- .mapped_memory = all_mapped_memory,
- .symbols = symbols_slice,
- .strings = strings,
- .ofiles = .empty,
- .vaddr_offset = module.text_base - text_vmaddr,
- };
-}
-pub fn getSymbolAtAddress(module: *const DarwinModule, gpa: Allocator, di: *DebugInfo, address: usize) Error!std.debug.Symbol {
- // We need the lock for a few things:
- // * loading the Mach-O module
- // * loading the referenced object file
- // * scanning the DWARF of that object file
- // * building the line number table of that object file
- // That's enough that it doesn't really seem worth scoping the lock more tightly than the whole function..
- di.mutex.lock();
- defer di.mutex.unlock();
-
- if (di.loaded_macho == null) di.loaded_macho = module.loadMachO(gpa) catch |err| switch (err) {
- error.InvalidDebugInfo, error.MissingDebugInfo, error.OutOfMemory, error.Unexpected => |e| return e,
- else => return error.ReadFailed,
- };
- const loaded_macho = &di.loaded_macho.?;
-
- const vaddr = address - loaded_macho.vaddr_offset;
- const symbol = MachoSymbol.find(loaded_macho.symbols, vaddr) orelse return .unknown;
-
- // offset of `address` from start of `symbol`
- const address_symbol_offset = vaddr - symbol.addr;
-
- // Take the symbol name from the N_FUN STAB entry, we're going to
- // use it if we fail to find the DWARF infos
- const stab_symbol = mem.sliceTo(loaded_macho.strings[symbol.strx..], 0);
-
- // If any information is missing, we can at least return this from now on.
- const sym_only_result: std.debug.Symbol = .{
- .name = stab_symbol,
- .compile_unit_name = null,
- .source_location = null,
- };
-
- if (symbol.ofile == MachoSymbol.unknown_ofile) {
- // We don't have STAB info, so can't track down the object file; all we can do is the symbol name.
- return sym_only_result;
- }
-
- const o_file: *DebugInfo.OFile = of: {
- const gop = try loaded_macho.ofiles.getOrPut(gpa, symbol.ofile);
- if (!gop.found_existing) {
- const o_file_path = mem.sliceTo(loaded_macho.strings[symbol.ofile..], 0);
- gop.value_ptr.* = DebugInfo.loadOFile(gpa, o_file_path) catch {
- _ = loaded_macho.ofiles.pop().?;
- return sym_only_result;
- };
- }
- break :of gop.value_ptr;
- };
-
- const symbol_index = o_file.symbols_by_name.getKeyAdapted(
- @as([]const u8, stab_symbol),
- @as(DebugInfo.OFile.SymbolAdapter, .{ .strtab = o_file.strtab, .symtab = o_file.symtab }),
- ) orelse return sym_only_result;
- const symbol_ofile_vaddr = o_file.symtab[symbol_index].n_value;
-
- const compile_unit = o_file.dwarf.findCompileUnit(native_endian, symbol_ofile_vaddr) catch return sym_only_result;
-
- return .{
- .name = o_file.dwarf.getSymbolName(symbol_ofile_vaddr + address_symbol_offset) orelse stab_symbol,
- .compile_unit_name = compile_unit.die.getAttrString(
- &o_file.dwarf,
- native_endian,
- std.dwarf.AT.name,
- o_file.dwarf.section(.debug_str),
- compile_unit,
- ) catch |err| switch (err) {
- error.MissingDebugInfo, error.InvalidDebugInfo => null,
- },
- .source_location = o_file.dwarf.getLineNumberInfo(
- gpa,
- native_endian,
- compile_unit,
- symbol_ofile_vaddr + address_symbol_offset,
- ) catch null,
- };
-}
-pub const supports_unwinding: bool = true;
-pub const UnwindContext = std.debug.SelfInfo.DwarfUnwindContext;
-/// Unwind a frame using MachO compact unwind info (from __unwind_info).
-/// If the compact encoding can't encode a way to unwind a frame, it will
-/// defer unwinding to DWARF, in which case `.eh_frame` will be used if available.
-pub fn unwindFrame(module: *const DarwinModule, gpa: Allocator, di: *DebugInfo, context: *UnwindContext) Error!usize {
- return unwindFrameInner(module, gpa, di, context) catch |err| switch (err) {
- error.InvalidDebugInfo,
- error.MissingDebugInfo,
- error.UnsupportedDebugInfo,
- error.ReadFailed,
- error.OutOfMemory,
- error.Unexpected,
- => |e| return e,
- error.UnsupportedRegister,
- => return error.UnsupportedDebugInfo,
- error.InvalidRegister,
- error.IncompatibleRegisterSize,
- => return error.InvalidDebugInfo,
- };
-}
-fn unwindFrameInner(module: *const DarwinModule, gpa: Allocator, di: *DebugInfo, context: *UnwindContext) !usize {
- const unwind: *DebugInfo.Unwind = u: {
- di.mutex.lock();
- defer di.mutex.unlock();
- if (di.unwind == null) try module.loadUnwindInfo(gpa, di);
- break :u &di.unwind.?;
- };
-
- const unwind_info = unwind.unwind_info orelse return error.MissingDebugInfo;
- if (unwind_info.len < @sizeOf(macho.unwind_info_section_header)) return error.InvalidDebugInfo;
- const header: *align(1) const macho.unwind_info_section_header = @ptrCast(unwind_info);
-
- const index_byte_count = header.indexCount * @sizeOf(macho.unwind_info_section_header_index_entry);
- if (unwind_info.len < header.indexSectionOffset + index_byte_count) return error.InvalidDebugInfo;
- const indices: []align(1) const macho.unwind_info_section_header_index_entry = @ptrCast(unwind_info[header.indexSectionOffset..][0..index_byte_count]);
- if (indices.len == 0) return error.MissingDebugInfo;
-
- // offset of the PC into the `__TEXT` segment
- const pc_text_offset = context.pc - module.text_base;
-
- const start_offset: u32, const first_level_offset: u32 = index: {
- var left: usize = 0;
- var len: usize = indices.len;
- while (len > 1) {
- const mid = left + len / 2;
- if (pc_text_offset < indices[mid].functionOffset) {
- len /= 2;
- } else {
- left = mid;
- len -= len / 2;
- }
- }
- break :index .{ indices[left].secondLevelPagesSectionOffset, indices[left].functionOffset };
- };
- // An offset of 0 is a sentinel indicating a range does not have unwind info.
- if (start_offset == 0) return error.MissingDebugInfo;
-
- const common_encodings_byte_count = header.commonEncodingsArrayCount * @sizeOf(macho.compact_unwind_encoding_t);
- if (unwind_info.len < header.commonEncodingsArraySectionOffset + common_encodings_byte_count) return error.InvalidDebugInfo;
- const common_encodings: []align(1) const macho.compact_unwind_encoding_t = @ptrCast(
- unwind_info[header.commonEncodingsArraySectionOffset..][0..common_encodings_byte_count],
- );
-
- if (unwind_info.len < start_offset + @sizeOf(macho.UNWIND_SECOND_LEVEL)) return error.InvalidDebugInfo;
- const kind: *align(1) const macho.UNWIND_SECOND_LEVEL = @ptrCast(unwind_info[start_offset..]);
-
- const entry: struct {
- function_offset: usize,
- raw_encoding: u32,
- } = switch (kind.*) {
- .REGULAR => entry: {
- if (unwind_info.len < start_offset + @sizeOf(macho.unwind_info_regular_second_level_page_header)) return error.InvalidDebugInfo;
- const page_header: *align(1) const macho.unwind_info_regular_second_level_page_header = @ptrCast(unwind_info[start_offset..]);
-
- const entries_byte_count = page_header.entryCount * @sizeOf(macho.unwind_info_regular_second_level_entry);
- if (unwind_info.len < start_offset + entries_byte_count) return error.InvalidDebugInfo;
- const entries: []align(1) const macho.unwind_info_regular_second_level_entry = @ptrCast(
- unwind_info[start_offset + page_header.entryPageOffset ..][0..entries_byte_count],
- );
- if (entries.len == 0) return error.InvalidDebugInfo;
-
- var left: usize = 0;
- var len: usize = entries.len;
- while (len > 1) {
- const mid = left + len / 2;
- if (pc_text_offset < entries[mid].functionOffset) {
- len /= 2;
- } else {
- left = mid;
- len -= len / 2;
- }
- }
- break :entry .{
- .function_offset = entries[left].functionOffset,
- .raw_encoding = entries[left].encoding,
- };
- },
- .COMPRESSED => entry: {
- if (unwind_info.len < start_offset + @sizeOf(macho.unwind_info_compressed_second_level_page_header)) return error.InvalidDebugInfo;
- const page_header: *align(1) const macho.unwind_info_compressed_second_level_page_header = @ptrCast(unwind_info[start_offset..]);
-
- const entries_byte_count = page_header.entryCount * @sizeOf(macho.UnwindInfoCompressedEntry);
- if (unwind_info.len < start_offset + entries_byte_count) return error.InvalidDebugInfo;
- const entries: []align(1) const macho.UnwindInfoCompressedEntry = @ptrCast(
- unwind_info[start_offset + page_header.entryPageOffset ..][0..entries_byte_count],
- );
- if (entries.len == 0) return error.InvalidDebugInfo;
-
- var left: usize = 0;
- var len: usize = entries.len;
- while (len > 1) {
- const mid = left + len / 2;
- if (pc_text_offset < first_level_offset + entries[mid].funcOffset) {
- len /= 2;
- } else {
- left = mid;
- len -= len / 2;
- }
- }
- const entry = entries[left];
-
- const function_offset = first_level_offset + entry.funcOffset;
- if (entry.encodingIndex < common_encodings.len) {
- break :entry .{
- .function_offset = function_offset,
- .raw_encoding = common_encodings[entry.encodingIndex],
- };
- }
-
- const local_index = entry.encodingIndex - common_encodings.len;
- const local_encodings_byte_count = page_header.encodingsCount * @sizeOf(macho.compact_unwind_encoding_t);
- if (unwind_info.len < start_offset + page_header.encodingsPageOffset + local_encodings_byte_count) return error.InvalidDebugInfo;
- const local_encodings: []align(1) const macho.compact_unwind_encoding_t = @ptrCast(
- unwind_info[start_offset + page_header.encodingsPageOffset ..][0..local_encodings_byte_count],
- );
- if (local_index >= local_encodings.len) return error.InvalidDebugInfo;
- break :entry .{
- .function_offset = function_offset,
- .raw_encoding = local_encodings[local_index],
- };
- },
- else => return error.InvalidDebugInfo,
- };
-
- if (entry.raw_encoding == 0) return error.MissingDebugInfo;
-
- const encoding: macho.CompactUnwindEncoding = @bitCast(entry.raw_encoding);
- const new_ip = switch (builtin.cpu.arch) {
- .x86_64 => switch (encoding.mode.x86_64) {
- .OLD => return error.UnsupportedDebugInfo,
- .RBP_FRAME => ip: {
- const frame = encoding.value.x86_64.frame;
-
- const fp = (try dwarfRegNative(&context.cpu_context, fp_reg_num)).*;
- const new_sp = fp + 2 * @sizeOf(usize);
-
- const ip_ptr = fp + @sizeOf(usize);
- const new_ip = @as(*const usize, @ptrFromInt(ip_ptr)).*;
- const new_fp = @as(*const usize, @ptrFromInt(fp)).*;
-
- (try dwarfRegNative(&context.cpu_context, fp_reg_num)).* = new_fp;
- (try dwarfRegNative(&context.cpu_context, sp_reg_num)).* = new_sp;
- (try dwarfRegNative(&context.cpu_context, ip_reg_num)).* = new_ip;
-
- const regs: [5]u3 = .{
- frame.reg0,
- frame.reg1,
- frame.reg2,
- frame.reg3,
- frame.reg4,
- };
- for (regs, 0..) |reg, i| {
- if (reg == 0) continue;
- const addr = fp - frame.frame_offset * @sizeOf(usize) + i * @sizeOf(usize);
- const reg_number = try Dwarf.compactUnwindToDwarfRegNumber(reg);
- (try dwarfRegNative(&context.cpu_context, reg_number)).* = @as(*const usize, @ptrFromInt(addr)).*;
- }
-
- break :ip new_ip;
- },
- .STACK_IMMD,
- .STACK_IND,
- => ip: {
- const frameless = encoding.value.x86_64.frameless;
-
- const sp = (try dwarfRegNative(&context.cpu_context, sp_reg_num)).*;
- const stack_size: usize = stack_size: {
- if (encoding.mode.x86_64 == .STACK_IMMD) {
- break :stack_size @as(usize, frameless.stack.direct.stack_size) * @sizeOf(usize);
- }
- // In .STACK_IND, the stack size is inferred from the subq instruction at the beginning of the function.
- const sub_offset_addr =
- module.text_base +
- entry.function_offset +
- frameless.stack.indirect.sub_offset;
- // `sub_offset_addr` points to the offset of the literal within the instruction
- const sub_operand = @as(*align(1) const u32, @ptrFromInt(sub_offset_addr)).*;
- break :stack_size sub_operand + @sizeOf(usize) * @as(usize, frameless.stack.indirect.stack_adjust);
- };
-
- // Decode the Lehmer-coded sequence of registers.
- // For a description of the encoding see lib/libc/include/any-macos.13-any/mach-o/compact_unwind_encoding.h
-
- // Decode the variable-based permutation number into its digits. Each digit represents
- // an index into the list of register numbers that weren't yet used in the sequence at
- // the time the digit was added.
- const reg_count = frameless.stack_reg_count;
- const ip_ptr = ip_ptr: {
- var digits: [6]u3 = undefined;
- var accumulator: usize = frameless.stack_reg_permutation;
- var base: usize = 2;
- for (0..reg_count) |i| {
- const div = accumulator / base;
- digits[digits.len - 1 - i] = @intCast(accumulator - base * div);
- accumulator = div;
- base += 1;
- }
-
- var registers: [6]u3 = undefined;
- var used_indices: [6]bool = @splat(false);
- for (digits[digits.len - reg_count ..], 0..) |target_unused_index, i| {
- var unused_count: u8 = 0;
- const unused_index = for (used_indices, 0..) |used, index| {
- if (!used) {
- if (target_unused_index == unused_count) break index;
- unused_count += 1;
- }
- } else unreachable;
- registers[i] = @intCast(unused_index + 1);
- used_indices[unused_index] = true;
- }
-
- var reg_addr = sp + stack_size - @sizeOf(usize) * @as(usize, reg_count + 1);
- for (0..reg_count) |i| {
- const reg_number = try Dwarf.compactUnwindToDwarfRegNumber(registers[i]);
- (try dwarfRegNative(&context.cpu_context, reg_number)).* = @as(*const usize, @ptrFromInt(reg_addr)).*;
- reg_addr += @sizeOf(usize);
- }
-
- break :ip_ptr reg_addr;
- };
-
- const new_ip = @as(*const usize, @ptrFromInt(ip_ptr)).*;
- const new_sp = ip_ptr + @sizeOf(usize);
-
- (try dwarfRegNative(&context.cpu_context, sp_reg_num)).* = new_sp;
- (try dwarfRegNative(&context.cpu_context, ip_reg_num)).* = new_ip;
-
- break :ip new_ip;
- },
- .DWARF => {
- const dwarf = &(unwind.dwarf orelse return error.MissingDebugInfo);
- return context.unwindFrame(unwind.dwarf_cache, gpa, dwarf, unwind.vmaddr_slide, encoding.value.x86_64.dwarf);
- },
- },
- .aarch64, .aarch64_be => switch (encoding.mode.arm64) {
- .OLD => return error.UnsupportedDebugInfo,
- .FRAMELESS => ip: {
- const sp = (try dwarfRegNative(&context.cpu_context, sp_reg_num)).*;
- const new_sp = sp + encoding.value.arm64.frameless.stack_size * 16;
- const new_ip = (try dwarfRegNative(&context.cpu_context, 30)).*;
- (try dwarfRegNative(&context.cpu_context, sp_reg_num)).* = new_sp;
- break :ip new_ip;
- },
- .DWARF => {
- const dwarf = &(unwind.dwarf orelse return error.MissingDebugInfo);
- return context.unwindFrame(unwind.dwarf_cache, gpa, dwarf, unwind.vmaddr_slide, encoding.value.arm64.dwarf);
- },
- .FRAME => ip: {
- const frame = encoding.value.arm64.frame;
-
- const fp = (try dwarfRegNative(&context.cpu_context, fp_reg_num)).*;
- const ip_ptr = fp + @sizeOf(usize);
-
- var reg_addr = fp - @sizeOf(usize);
- inline for (@typeInfo(@TypeOf(frame.x_reg_pairs)).@"struct".fields, 0..) |field, i| {
- if (@field(frame.x_reg_pairs, field.name) != 0) {
- (try dwarfRegNative(&context.cpu_context, 19 + i)).* = @as(*const usize, @ptrFromInt(reg_addr)).*;
- reg_addr += @sizeOf(usize);
- (try dwarfRegNative(&context.cpu_context, 20 + i)).* = @as(*const usize, @ptrFromInt(reg_addr)).*;
- reg_addr += @sizeOf(usize);
- }
- }
-
- inline for (@typeInfo(@TypeOf(frame.d_reg_pairs)).@"struct".fields, 0..) |field, i| {
- if (@field(frame.d_reg_pairs, field.name) != 0) {
- // Only the lower half of the 128-bit V registers are restored during unwinding
- {
- const dest: *align(1) usize = @ptrCast(try context.cpu_context.dwarfRegisterBytes(64 + 8 + i));
- dest.* = @as(*const usize, @ptrFromInt(reg_addr)).*;
- }
- reg_addr += @sizeOf(usize);
- {
- const dest: *align(1) usize = @ptrCast(try context.cpu_context.dwarfRegisterBytes(64 + 9 + i));
- dest.* = @as(*const usize, @ptrFromInt(reg_addr)).*;
- }
- reg_addr += @sizeOf(usize);
- }
- }
-
- const new_ip = @as(*const usize, @ptrFromInt(ip_ptr)).*;
- const new_fp = @as(*const usize, @ptrFromInt(fp)).*;
-
- (try dwarfRegNative(&context.cpu_context, fp_reg_num)).* = new_fp;
- (try dwarfRegNative(&context.cpu_context, ip_reg_num)).* = new_ip;
-
- break :ip new_ip;
- },
- },
- else => comptime unreachable, // unimplemented
- };
-
- const ret_addr = std.debug.stripInstructionPtrAuthCode(new_ip);
-
- // Like `DwarfUnwindContext.unwindFrame`, adjust our next lookup pc in case the `call` was this
- // function's last instruction making `ret_addr` one byte past its end.
- context.pc = ret_addr -| 1;
-
- return ret_addr;
-}
-pub const DebugInfo = struct {
- /// Held while checking and/or populating `unwind` or `loaded_macho`.
- /// Once a field is populated and the pointer `&di.loaded_macho.?` or `&di.unwind.?` has been
- /// gotten, the lock is released; i.e. it is not held while *using* the loaded info.
- mutex: std.Thread.Mutex,
-
- unwind: ?Unwind,
- loaded_macho: ?LoadedMachO,
-
- pub const init: DebugInfo = .{
- .mutex = .{},
-
- .unwind = null,
- .loaded_macho = null,
- };
-
- pub fn deinit(di: *DebugInfo, gpa: Allocator) void {
- if (di.loaded_macho) |*loaded_macho| {
- for (loaded_macho.ofiles.values()) |*ofile| {
- ofile.dwarf.deinit(gpa);
- ofile.symbols_by_name.deinit(gpa);
- posix.munmap(ofile.mapped_memory);
- }
- loaded_macho.ofiles.deinit(gpa);
- gpa.free(loaded_macho.symbols);
- posix.munmap(loaded_macho.mapped_memory);
- }
- }
-
- const Unwind = struct {
- /// The slide applied to the `__unwind_info` and `__eh_frame` sections.
- /// So, `unwind_info.ptr` is this many bytes higher than the section's vmaddr.
- vmaddr_slide: u64,
- /// Backed by the in-memory section mapped by the loader.
- unwind_info: ?[]const u8,
- /// Backed by the in-memory `__eh_frame` section mapped by the loader.
- dwarf: ?Dwarf.Unwind,
- /// This is `undefined` if `dwarf == null`.
- dwarf_cache: *UnwindContext.Cache,
- };
-
- const LoadedMachO = struct {
- mapped_memory: []align(std.heap.page_size_min) const u8,
- symbols: []const MachoSymbol,
- strings: []const u8,
- /// Key is index into `strings` of the file path.
- ofiles: std.AutoArrayHashMapUnmanaged(u32, OFile),
- /// This is not necessarily the same as the vmaddr_slide that dyld would report. This is
- /// because the segments in the file on disk might differ from the ones in memory. Normally
- /// we wouldn't necessarily expect that to work, but /usr/lib/dyld is incredibly annoying:
- /// it exists on disk (necessarily, because the kernel needs to load it!), but is also in
- /// the dyld cache (dyld actually restart itself from cache after loading it), and the two
- /// versions have (very) different segment base addresses. It's sort of like a large slide
- /// has been applied to all addresses in memory. For an optimal experience, we consider the
- /// on-disk vmaddr instead of the in-memory one.
- vaddr_offset: usize,
- };
-
- const OFile = struct {
- mapped_memory: []align(std.heap.page_size_min) const u8,
- dwarf: Dwarf,
- strtab: []const u8,
- symtab: []align(1) const macho.nlist_64,
- /// All named symbols in `symtab`. Stored `u32` key is the index into `symtab`. Accessed
- /// through `SymbolAdapter`, so that the symbol name is used as the logical key.
- symbols_by_name: std.ArrayHashMapUnmanaged(u32, void, void, true),
-
- const SymbolAdapter = struct {
- strtab: []const u8,
- symtab: []align(1) const macho.nlist_64,
- pub fn hash(ctx: SymbolAdapter, sym_name: []const u8) u32 {
- _ = ctx;
- return @truncate(std.hash.Wyhash.hash(0, sym_name));
- }
- pub fn eql(ctx: SymbolAdapter, a_sym_name: []const u8, b_sym_index: u32, b_index: usize) bool {
- _ = b_index;
- const b_sym = ctx.symtab[b_sym_index];
- const b_sym_name = std.mem.sliceTo(ctx.strtab[b_sym.n_strx..], 0);
- return mem.eql(u8, a_sym_name, b_sym_name);
- }
- };
- };
-
- fn loadOFile(gpa: Allocator, o_file_path: []const u8) !OFile {
- const mapped_mem = try mapDebugInfoFile(o_file_path);
- errdefer posix.munmap(mapped_mem);
-
- if (mapped_mem.len < @sizeOf(macho.mach_header_64)) return error.InvalidDebugInfo;
- const hdr: *const macho.mach_header_64 = @ptrCast(@alignCast(mapped_mem.ptr));
- if (hdr.magic != std.macho.MH_MAGIC_64) return error.InvalidDebugInfo;
-
- const seg_cmd: macho.LoadCommandIterator.LoadCommand, const symtab_cmd: macho.symtab_command = cmds: {
- var seg_cmd: ?macho.LoadCommandIterator.LoadCommand = null;
- var symtab_cmd: ?macho.symtab_command = null;
- var it: macho.LoadCommandIterator = .{
- .ncmds = hdr.ncmds,
- .buffer = mapped_mem[@sizeOf(macho.mach_header_64)..][0..hdr.sizeofcmds],
- };
- while (it.next()) |cmd| switch (cmd.cmd()) {
- .SEGMENT_64 => seg_cmd = cmd,
- .SYMTAB => symtab_cmd = cmd.cast(macho.symtab_command) orelse return error.InvalidDebugInfo,
- else => {},
- };
- break :cmds .{
- seg_cmd orelse return error.MissingDebugInfo,
- symtab_cmd orelse return error.MissingDebugInfo,
- };
- };
-
- if (mapped_mem.len < symtab_cmd.stroff + symtab_cmd.strsize) return error.InvalidDebugInfo;
- if (mapped_mem[symtab_cmd.stroff + symtab_cmd.strsize - 1] != 0) return error.InvalidDebugInfo;
- const strtab = mapped_mem[symtab_cmd.stroff..][0 .. symtab_cmd.strsize - 1];
-
- const n_sym_bytes = symtab_cmd.nsyms * @sizeOf(macho.nlist_64);
- if (mapped_mem.len < symtab_cmd.symoff + n_sym_bytes) return error.InvalidDebugInfo;
- const symtab: []align(1) const macho.nlist_64 = @ptrCast(mapped_mem[symtab_cmd.symoff..][0..n_sym_bytes]);
-
- // TODO handle tentative (common) symbols
- var symbols_by_name: std.ArrayHashMapUnmanaged(u32, void, void, true) = .empty;
- defer symbols_by_name.deinit(gpa);
- try symbols_by_name.ensureUnusedCapacity(gpa, @intCast(symtab.len));
- for (symtab, 0..) |sym, sym_index| {
- if (sym.n_strx == 0) continue;
- switch (sym.n_type.bits.type) {
- .undf => continue, // includes tentative symbols
- .abs => continue,
- else => {},
- }
- const sym_name = mem.sliceTo(strtab[sym.n_strx..], 0);
- const gop = symbols_by_name.getOrPutAssumeCapacityAdapted(
- @as([]const u8, sym_name),
- @as(DebugInfo.OFile.SymbolAdapter, .{ .strtab = strtab, .symtab = symtab }),
- );
- if (gop.found_existing) return error.InvalidDebugInfo;
- gop.key_ptr.* = @intCast(sym_index);
- }
-
- var sections: Dwarf.SectionArray = @splat(null);
- for (seg_cmd.getSections()) |sect| {
- if (!std.mem.eql(u8, "__DWARF", sect.segName())) continue;
-
- const section_index: usize = inline for (@typeInfo(Dwarf.Section.Id).@"enum".fields, 0..) |section, i| {
- if (mem.eql(u8, "__" ++ section.name, sect.sectName())) break i;
- } else continue;
-
- if (mapped_mem.len < sect.offset + sect.size) return error.InvalidDebugInfo;
- const section_bytes = mapped_mem[sect.offset..][0..sect.size];
- sections[section_index] = .{
- .data = section_bytes,
- .owned = false,
- };
- }
-
- const missing_debug_info =
- sections[@intFromEnum(Dwarf.Section.Id.debug_info)] == null or
- sections[@intFromEnum(Dwarf.Section.Id.debug_abbrev)] == null or
- sections[@intFromEnum(Dwarf.Section.Id.debug_str)] == null or
- sections[@intFromEnum(Dwarf.Section.Id.debug_line)] == null;
- if (missing_debug_info) return error.MissingDebugInfo;
-
- var dwarf: Dwarf = .{ .sections = sections };
- errdefer dwarf.deinit(gpa);
- try dwarf.open(gpa, native_endian);
-
- return .{
- .mapped_memory = mapped_mem,
- .dwarf = dwarf,
- .strtab = strtab,
- .symtab = symtab,
- .symbols_by_name = symbols_by_name.move(),
- };
- }
-};
-
-const MachoSymbol = struct {
- strx: u32,
- addr: u64,
- /// Value may be `unknown_ofile`.
- ofile: u32,
- const unknown_ofile = std.math.maxInt(u32);
- fn addressLessThan(context: void, lhs: MachoSymbol, rhs: MachoSymbol) bool {
- _ = context;
- return lhs.addr < rhs.addr;
- }
- /// Assumes that `symbols` is sorted in order of ascending `addr`.
- fn find(symbols: []const MachoSymbol, address: usize) ?*const MachoSymbol {
- if (symbols.len == 0) return null; // no potential match
- if (address < symbols[0].addr) return null; // address is before the lowest-address symbol
- var left: usize = 0;
- var len: usize = symbols.len;
- while (len > 1) {
- const mid = left + len / 2;
- if (address < symbols[mid].addr) {
- len /= 2;
- } else {
- left = mid;
- len -= len / 2;
- }
- }
- return &symbols[left];
- }
-
- test find {
- const symbols: []const MachoSymbol = &.{
- .{ .addr = 100, .strx = undefined, .ofile = undefined },
- .{ .addr = 200, .strx = undefined, .ofile = undefined },
- .{ .addr = 300, .strx = undefined, .ofile = undefined },
- };
-
- try testing.expectEqual(null, find(symbols, 0));
- try testing.expectEqual(null, find(symbols, 99));
- try testing.expectEqual(&symbols[0], find(symbols, 100).?);
- try testing.expectEqual(&symbols[0], find(symbols, 150).?);
- try testing.expectEqual(&symbols[0], find(symbols, 199).?);
-
- try testing.expectEqual(&symbols[1], find(symbols, 200).?);
- try testing.expectEqual(&symbols[1], find(symbols, 250).?);
- try testing.expectEqual(&symbols[1], find(symbols, 299).?);
-
- try testing.expectEqual(&symbols[2], find(symbols, 300).?);
- try testing.expectEqual(&symbols[2], find(symbols, 301).?);
- try testing.expectEqual(&symbols[2], find(symbols, 5000).?);
- }
-};
-test {
- _ = MachoSymbol;
-}
-
-const ip_reg_num = Dwarf.ipRegNum(builtin.target.cpu.arch).?;
-const fp_reg_num = Dwarf.fpRegNum(builtin.target.cpu.arch);
-const sp_reg_num = Dwarf.spRegNum(builtin.target.cpu.arch);
-
-/// Uses `mmap` to map the file at `path` into memory.
-fn mapDebugInfoFile(path: []const u8) ![]align(std.heap.page_size_min) const u8 {
- const file = std.fs.cwd().openFile(path, .{}) catch |err| switch (err) {
- error.FileNotFound => return error.MissingDebugInfo,
- else => return error.ReadFailed,
- };
- defer file.close();
-
- const file_len = std.math.cast(usize, try file.getEndPos()) orelse return error.InvalidDebugInfo;
-
- return posix.mmap(
- null,
- file_len,
- posix.PROT.READ,
- .{ .TYPE = .SHARED },
- file.handle,
- 0,
- );
-}
-
-const DarwinModule = @This();
-
-const std = @import("../../std.zig");
-const Allocator = std.mem.Allocator;
-const Dwarf = std.debug.Dwarf;
-const assert = std.debug.assert;
-const macho = std.macho;
-const mem = std.mem;
-const posix = std.posix;
-const testing = std.testing;
-const Error = std.debug.SelfInfo.Error;
-const dwarfRegNative = std.debug.SelfInfo.DwarfUnwindContext.regNative;
-
-const builtin = @import("builtin");
-const native_endian = builtin.target.cpu.arch.endian();
lib/std/debug/SelfInfo/Elf.zig
@@ -0,0 +1,427 @@
+rwlock: std.Thread.RwLock,
+
+modules: std.ArrayList(Module),
+ranges: std.ArrayList(Module.Range),
+
+unwind_cache: if (can_unwind) ?[]Dwarf.SelfUnwinder.CacheEntry else ?noreturn,
+
+pub const init: SelfInfo = .{
+ .rwlock = .{},
+ .modules = .empty,
+ .ranges = .empty,
+ .unwind_cache = null,
+};
+pub fn deinit(si: *SelfInfo, gpa: Allocator) void {
+ for (si.modules.items) |*mod| {
+ unwind: {
+ const u = &(mod.unwind orelse break :unwind catch break :unwind);
+ for (u.buf[0..u.len]) |*unwind| unwind.deinit(gpa);
+ }
+ loaded: {
+ const l = &(mod.loaded_elf orelse break :loaded catch break :loaded);
+ l.file.deinit(gpa);
+ }
+ }
+
+ si.modules.deinit(gpa);
+ si.ranges.deinit(gpa);
+ if (si.unwind_cache) |cache| gpa.free(cache);
+}
+
+pub fn getSymbol(si: *SelfInfo, gpa: Allocator, address: usize) Error!std.debug.Symbol {
+ const module = try si.findModule(gpa, address, .exclusive);
+ defer si.rwlock.unlock();
+
+ const vaddr = address - module.load_offset;
+
+ const loaded_elf = try module.getLoadedElf(gpa);
+ if (loaded_elf.file.dwarf) |*dwarf| {
+ if (!loaded_elf.scanned_dwarf) {
+ dwarf.open(gpa, native_endian) catch |err| switch (err) {
+ error.InvalidDebugInfo,
+ error.MissingDebugInfo,
+ error.OutOfMemory,
+ => |e| return e,
+ error.EndOfStream,
+ error.Overflow,
+ error.ReadFailed,
+ error.StreamTooLong,
+ => return error.InvalidDebugInfo,
+ };
+ loaded_elf.scanned_dwarf = true;
+ }
+ if (dwarf.getSymbol(gpa, native_endian, vaddr)) |sym| {
+ return sym;
+ } else |err| switch (err) {
+ error.MissingDebugInfo => {},
+
+ error.InvalidDebugInfo,
+ error.OutOfMemory,
+ => |e| return e,
+
+ error.ReadFailed,
+ error.EndOfStream,
+ error.Overflow,
+ error.StreamTooLong,
+ => return error.InvalidDebugInfo,
+ }
+ }
+ // When DWARF is unavailable, fall back to searching the symtab.
+ return loaded_elf.file.searchSymtab(gpa, vaddr) catch |err| switch (err) {
+ error.NoSymtab, error.NoStrtab => return error.MissingDebugInfo,
+ error.BadSymtab => return error.InvalidDebugInfo,
+ error.OutOfMemory => |e| return e,
+ };
+}
+pub fn getModuleName(si: *SelfInfo, gpa: Allocator, address: usize) Error![]const u8 {
+ const module = try si.findModule(gpa, address, .shared);
+ defer si.rwlock.unlockShared();
+ if (module.name.len == 0) return error.MissingDebugInfo;
+ return module.name;
+}
+
+pub const can_unwind: bool = s: {
+ // Notably, we are yet to support unwinding on ARM. There, unwinding is not done through
+ // `.eh_frame`, but instead with the `.ARM.exidx` section, which has a different format.
+ const archs: []const std.Target.Cpu.Arch = switch (builtin.target.os.tag) {
+ .linux => &.{ .x86, .x86_64, .aarch64, .aarch64_be },
+ .netbsd => &.{ .x86, .x86_64, .aarch64, .aarch64_be },
+ .freebsd => &.{ .x86_64, .aarch64, .aarch64_be },
+ .openbsd => &.{.x86_64},
+ .solaris => &.{ .x86, .x86_64 },
+ .illumos => &.{ .x86, .x86_64 },
+ else => unreachable,
+ };
+ for (archs) |a| {
+ if (builtin.target.cpu.arch == a) break :s true;
+ }
+ break :s false;
+};
+comptime {
+ if (can_unwind) {
+ std.debug.assert(Dwarf.supportsUnwinding(&builtin.target));
+ }
+}
+pub const UnwindContext = Dwarf.SelfUnwinder;
+pub fn unwindFrame(si: *SelfInfo, gpa: Allocator, context: *UnwindContext) Error!usize {
+ comptime assert(can_unwind);
+
+ {
+ si.rwlock.lockShared();
+ defer si.rwlock.unlockShared();
+ if (si.unwind_cache) |cache| {
+ if (Dwarf.SelfUnwinder.CacheEntry.find(cache, context.pc)) |entry| {
+ return context.next(gpa, entry);
+ }
+ }
+ }
+
+ const module = try si.findModule(gpa, context.pc, .exclusive);
+ defer si.rwlock.unlock();
+
+ if (si.unwind_cache == null) {
+ si.unwind_cache = try gpa.alloc(Dwarf.SelfUnwinder.CacheEntry, 2048);
+ @memset(si.unwind_cache.?, .empty);
+ }
+
+ const unwind_sections = try module.getUnwindSections(gpa);
+ for (unwind_sections) |*unwind| {
+ if (context.computeRules(gpa, unwind, module.load_offset, null)) |entry| {
+ entry.populate(si.unwind_cache.?);
+ return context.next(gpa, &entry);
+ } else |err| switch (err) {
+ error.MissingDebugInfo => continue,
+
+ error.InvalidDebugInfo,
+ error.UnsupportedDebugInfo,
+ error.OutOfMemory,
+ => |e| return e,
+
+ error.EndOfStream,
+ error.StreamTooLong,
+ error.ReadFailed,
+ error.Overflow,
+ error.InvalidOpcode,
+ error.InvalidOperation,
+ error.InvalidOperand,
+ => return error.InvalidDebugInfo,
+
+ error.UnimplementedUserOpcode,
+ error.UnsupportedAddrSize,
+ => return error.UnsupportedDebugInfo,
+ }
+ }
+ return error.MissingDebugInfo;
+}
+
+const Module = struct {
+ load_offset: usize,
+ name: []const u8,
+ build_id: ?[]const u8,
+ gnu_eh_frame: ?[]const u8,
+
+ /// `null` means unwind information has not yet been loaded.
+ unwind: ?(Error!UnwindSections),
+
+ /// `null` means the ELF file has not yet been loaded.
+ loaded_elf: ?(Error!LoadedElf),
+
+ const LoadedElf = struct {
+ file: std.debug.ElfFile,
+ scanned_dwarf: bool,
+ };
+
+ const UnwindSections = struct {
+ buf: [2]Dwarf.Unwind,
+ len: usize,
+ };
+
+ const Range = struct {
+ start: usize,
+ len: usize,
+ /// Index into `modules`
+ module_index: usize,
+ };
+
+ /// Assumes we already hold an exclusive lock.
+ fn getUnwindSections(mod: *Module, gpa: Allocator) Error![]Dwarf.Unwind {
+ if (mod.unwind == null) mod.unwind = loadUnwindSections(mod, gpa);
+ const us = &(mod.unwind.? catch |err| return err);
+ return us.buf[0..us.len];
+ }
+ fn loadUnwindSections(mod: *Module, gpa: Allocator) Error!UnwindSections {
+ var us: UnwindSections = .{
+ .buf = undefined,
+ .len = 0,
+ };
+ if (mod.gnu_eh_frame) |section_bytes| {
+ const section_vaddr: u64 = @intFromPtr(section_bytes.ptr) - mod.load_offset;
+ const header = Dwarf.Unwind.EhFrameHeader.parse(section_vaddr, section_bytes, @sizeOf(usize), native_endian) catch |err| switch (err) {
+ error.ReadFailed => unreachable, // it's all fixed buffers
+ error.InvalidDebugInfo => |e| return e,
+ error.EndOfStream, error.Overflow => return error.InvalidDebugInfo,
+ error.UnsupportedAddrSize => return error.UnsupportedDebugInfo,
+ };
+ us.buf[us.len] = .initEhFrameHdr(header, section_vaddr, @ptrFromInt(@as(usize, @intCast(mod.load_offset + header.eh_frame_vaddr))));
+ us.len += 1;
+ } else {
+ // There is no `.eh_frame_hdr` section. There may still be an `.eh_frame` or `.debug_frame`
+ // section, but we'll have to load the binary to get at it.
+ const loaded = try mod.getLoadedElf(gpa);
+ // If both are present, we can't just pick one -- the info could be split between them.
+ // `.debug_frame` is likely to be the more complete section, so we'll prioritize that one.
+ if (loaded.file.debug_frame) |*debug_frame| {
+ us.buf[us.len] = .initSection(.debug_frame, debug_frame.vaddr, debug_frame.bytes);
+ us.len += 1;
+ }
+ if (loaded.file.eh_frame) |*eh_frame| {
+ us.buf[us.len] = .initSection(.eh_frame, eh_frame.vaddr, eh_frame.bytes);
+ us.len += 1;
+ }
+ }
+ errdefer for (us.buf[0..us.len]) |*u| u.deinit(gpa);
+ for (us.buf[0..us.len]) |*u| u.prepare(gpa, @sizeOf(usize), native_endian, true, false) catch |err| switch (err) {
+ error.ReadFailed => unreachable, // it's all fixed buffers
+ error.InvalidDebugInfo,
+ error.MissingDebugInfo,
+ error.OutOfMemory,
+ => |e| return e,
+ error.EndOfStream,
+ error.Overflow,
+ error.StreamTooLong,
+ error.InvalidOperand,
+ error.InvalidOpcode,
+ error.InvalidOperation,
+ => return error.InvalidDebugInfo,
+ error.UnsupportedAddrSize,
+ error.UnsupportedDwarfVersion,
+ error.UnimplementedUserOpcode,
+ => return error.UnsupportedDebugInfo,
+ };
+ return us;
+ }
+
+ /// Assumes we already hold an exclusive lock.
+ fn getLoadedElf(mod: *Module, gpa: Allocator) Error!*LoadedElf {
+ if (mod.loaded_elf == null) mod.loaded_elf = loadElf(mod, gpa);
+ return if (mod.loaded_elf.?) |*elf| elf else |err| err;
+ }
+ fn loadElf(mod: *Module, gpa: Allocator) Error!LoadedElf {
+ const load_result = if (mod.name.len > 0) res: {
+ var file = std.fs.cwd().openFile(mod.name, .{}) catch return error.MissingDebugInfo;
+ defer file.close();
+ break :res std.debug.ElfFile.load(gpa, file, mod.build_id, &.native(mod.name));
+ } else res: {
+ const path = std.fs.selfExePathAlloc(gpa) catch |err| switch (err) {
+ error.OutOfMemory => |e| return e,
+ else => return error.ReadFailed,
+ };
+ defer gpa.free(path);
+ var file = std.fs.cwd().openFile(path, .{}) catch return error.MissingDebugInfo;
+ defer file.close();
+ break :res std.debug.ElfFile.load(gpa, file, mod.build_id, &.native(path));
+ };
+
+ var elf_file = load_result catch |err| switch (err) {
+ error.OutOfMemory,
+ error.Unexpected,
+ => |e| return e,
+
+ error.Overflow,
+ error.TruncatedElfFile,
+ error.InvalidCompressedSection,
+ error.InvalidElfMagic,
+ error.InvalidElfVersion,
+ error.InvalidElfClass,
+ error.InvalidElfEndian,
+ => return error.InvalidDebugInfo,
+
+ error.SystemResources,
+ error.MemoryMappingNotSupported,
+ error.AccessDenied,
+ error.LockedMemoryLimitExceeded,
+ error.ProcessFdQuotaExceeded,
+ error.SystemFdQuotaExceeded,
+ => return error.ReadFailed,
+ };
+ errdefer elf_file.deinit(gpa);
+
+ if (elf_file.endian != native_endian) return error.InvalidDebugInfo;
+ if (elf_file.is_64 != (@sizeOf(usize) == 8)) return error.InvalidDebugInfo;
+
+ return .{
+ .file = elf_file,
+ .scanned_dwarf = false,
+ };
+ }
+};
+
+fn findModule(si: *SelfInfo, gpa: Allocator, address: usize, lock: enum { shared, exclusive }) Error!*Module {
+ // With the requested lock, scan the module ranges looking for `address`.
+ switch (lock) {
+ .shared => si.rwlock.lockShared(),
+ .exclusive => si.rwlock.lock(),
+ }
+ for (si.ranges.items) |*range| {
+ if (address >= range.start and address < range.start + range.len) {
+ return &si.modules.items[range.module_index];
+ }
+ }
+ // The address wasn't in a known range. We will rebuild the module/range lists, since it's possible
+ // a new module was loaded. Upgrade to an exclusive lock if necessary.
+ switch (lock) {
+ .shared => {
+ si.rwlock.unlockShared();
+ si.rwlock.lock();
+ },
+ .exclusive => {},
+ }
+ // Rebuild module list with the exclusive lock.
+ {
+ errdefer si.rwlock.unlock();
+ for (si.modules.items) |*mod| {
+ unwind: {
+ const u = &(mod.unwind orelse break :unwind catch break :unwind);
+ for (u.buf[0..u.len]) |*unwind| unwind.deinit(gpa);
+ }
+ loaded: {
+ const l = &(mod.loaded_elf orelse break :loaded catch break :loaded);
+ l.file.deinit(gpa);
+ }
+ }
+ si.modules.clearRetainingCapacity();
+ si.ranges.clearRetainingCapacity();
+ var ctx: DlIterContext = .{ .si = si, .gpa = gpa };
+ try std.posix.dl_iterate_phdr(&ctx, error{OutOfMemory}, DlIterContext.callback);
+ }
+ // Downgrade the lock back to shared if necessary.
+ switch (lock) {
+ .shared => {
+ si.rwlock.unlock();
+ si.rwlock.lockShared();
+ },
+ .exclusive => {},
+ }
+ // Scan the newly rebuilt module ranges.
+ for (si.ranges.items) |*range| {
+ if (address >= range.start and address < range.start + range.len) {
+ return &si.modules.items[range.module_index];
+ }
+ }
+ // Still nothing; unlock and error.
+ switch (lock) {
+ .shared => si.rwlock.unlockShared(),
+ .exclusive => si.rwlock.unlock(),
+ }
+ return error.MissingDebugInfo;
+}
+const DlIterContext = struct {
+ si: *SelfInfo,
+ gpa: Allocator,
+
+ fn callback(info: *std.posix.dl_phdr_info, size: usize, context: *@This()) !void {
+ _ = size;
+
+ var build_id: ?[]const u8 = null;
+ var gnu_eh_frame: ?[]const u8 = null;
+
+ // Populate `build_id` and `gnu_eh_frame`
+ for (info.phdr[0..info.phnum]) |phdr| {
+ switch (phdr.p_type) {
+ std.elf.PT_NOTE => {
+ // Look for .note.gnu.build-id
+ const segment_ptr: [*]const u8 = @ptrFromInt(info.addr + phdr.p_vaddr);
+ var r: std.Io.Reader = .fixed(segment_ptr[0..phdr.p_memsz]);
+ const name_size = r.takeInt(u32, native_endian) catch continue;
+ const desc_size = r.takeInt(u32, native_endian) catch continue;
+ const note_type = r.takeInt(u32, native_endian) catch continue;
+ const name = r.take(name_size) catch continue;
+ if (note_type != std.elf.NT_GNU_BUILD_ID) continue;
+ if (!std.mem.eql(u8, name, "GNU\x00")) continue;
+ const desc = r.take(desc_size) catch continue;
+ build_id = desc;
+ },
+ std.elf.PT_GNU_EH_FRAME => {
+ const segment_ptr: [*]const u8 = @ptrFromInt(info.addr + phdr.p_vaddr);
+ gnu_eh_frame = segment_ptr[0..phdr.p_memsz];
+ },
+ else => {},
+ }
+ }
+
+ const gpa = context.gpa;
+ const si = context.si;
+
+ const module_index = si.modules.items.len;
+ try si.modules.append(gpa, .{
+ .load_offset = info.addr,
+ // Android libc uses NULL instead of "" to mark the main program
+ .name = std.mem.sliceTo(info.name, 0) orelse "",
+ .build_id = build_id,
+ .gnu_eh_frame = gnu_eh_frame,
+ .unwind = null,
+ .loaded_elf = null,
+ });
+
+ for (info.phdr[0..info.phnum]) |phdr| {
+ if (phdr.p_type != std.elf.PT_LOAD) continue;
+ try context.si.ranges.append(gpa, .{
+ // Overflowing addition handles VSDOs having p_vaddr = 0xffffffffff700000
+ .start = info.addr +% phdr.p_vaddr,
+ .len = phdr.p_memsz,
+ .module_index = module_index,
+ });
+ }
+ }
+};
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const Dwarf = std.debug.Dwarf;
+const Error = std.debug.SelfInfoError;
+const assert = std.debug.assert;
+
+const builtin = @import("builtin");
+const native_endian = builtin.target.cpu.arch.endian();
+
+const SelfInfo = @This();
lib/std/debug/SelfInfo/ElfModule.zig
@@ -1,349 +0,0 @@
-load_offset: usize,
-name: []const u8,
-build_id: ?[]const u8,
-gnu_eh_frame: ?[]const u8,
-
-pub const LookupCache = struct {
- rwlock: std.Thread.RwLock,
- ranges: std.ArrayList(Range),
- const Range = struct {
- start: usize,
- len: usize,
- mod: ElfModule,
- };
- pub const init: LookupCache = .{
- .rwlock = .{},
- .ranges = .empty,
- };
- pub fn deinit(lc: *LookupCache, gpa: Allocator) void {
- lc.ranges.deinit(gpa);
- }
-};
-
-pub const DebugInfo = struct {
- /// Held while checking and/or populating `loaded_elf`/`scanned_dwarf`/`unwind`.
- /// Once data is populated and a pointer to the field has been gotten, the lock
- /// is released; i.e. it is not held while *using* the loaded debug info.
- mutex: std.Thread.Mutex,
-
- loaded_elf: ?ElfFile,
- scanned_dwarf: bool,
- unwind: if (supports_unwinding) [2]?Dwarf.Unwind else void,
- unwind_cache: if (supports_unwinding) *UnwindContext.Cache else void,
-
- pub const init: DebugInfo = .{
- .mutex = .{},
- .loaded_elf = null,
- .scanned_dwarf = false,
- .unwind = if (supports_unwinding) @splat(null),
- .unwind_cache = undefined,
- };
- pub fn deinit(di: *DebugInfo, gpa: Allocator) void {
- if (di.loaded_elf) |*loaded_elf| loaded_elf.deinit(gpa);
- if (supports_unwinding) {
- if (di.unwind[0] != null) gpa.destroy(di.unwind_cache);
- for (&di.unwind) |*opt_unwind| {
- const unwind = &(opt_unwind.* orelse continue);
- unwind.deinit(gpa);
- }
- }
- }
-};
-
-pub fn key(m: ElfModule) usize {
- return m.load_offset;
-}
-pub fn lookup(cache: *LookupCache, gpa: Allocator, address: usize) Error!ElfModule {
- if (lookupInCache(cache, address)) |m| return m;
-
- {
- // Check a new module hasn't been loaded
- cache.rwlock.lock();
- defer cache.rwlock.unlock();
- const DlIterContext = struct {
- ranges: *std.ArrayList(LookupCache.Range),
- gpa: Allocator,
-
- fn callback(info: *std.posix.dl_phdr_info, size: usize, context: *@This()) !void {
- _ = size;
-
- var mod: ElfModule = .{
- .load_offset = info.addr,
- // Android libc uses NULL instead of "" to mark the main program
- .name = mem.sliceTo(info.name, 0) orelse "",
- .build_id = null,
- .gnu_eh_frame = null,
- };
-
- // Populate `build_id` and `gnu_eh_frame`
- for (info.phdr[0..info.phnum]) |phdr| {
- switch (phdr.p_type) {
- elf.PT_NOTE => {
- // Look for .note.gnu.build-id
- const segment_ptr: [*]const u8 = @ptrFromInt(info.addr + phdr.p_vaddr);
- var r: std.Io.Reader = .fixed(segment_ptr[0..phdr.p_memsz]);
- const name_size = r.takeInt(u32, native_endian) catch continue;
- const desc_size = r.takeInt(u32, native_endian) catch continue;
- const note_type = r.takeInt(u32, native_endian) catch continue;
- const name = r.take(name_size) catch continue;
- if (note_type != elf.NT_GNU_BUILD_ID) continue;
- if (!mem.eql(u8, name, "GNU\x00")) continue;
- const desc = r.take(desc_size) catch continue;
- mod.build_id = desc;
- },
- elf.PT_GNU_EH_FRAME => {
- const segment_ptr: [*]const u8 = @ptrFromInt(info.addr + phdr.p_vaddr);
- mod.gnu_eh_frame = segment_ptr[0..phdr.p_memsz];
- },
- else => {},
- }
- }
-
- // Now that `mod` is populated, create the ranges
- for (info.phdr[0..info.phnum]) |phdr| {
- if (phdr.p_type != elf.PT_LOAD) continue;
- try context.ranges.append(context.gpa, .{
- // Overflowing addition handles VSDOs having p_vaddr = 0xffffffffff700000
- .start = info.addr +% phdr.p_vaddr,
- .len = phdr.p_memsz,
- .mod = mod,
- });
- }
- }
- };
- cache.ranges.clearRetainingCapacity();
- var ctx: DlIterContext = .{
- .ranges = &cache.ranges,
- .gpa = gpa,
- };
- try std.posix.dl_iterate_phdr(&ctx, error{OutOfMemory}, DlIterContext.callback);
- }
-
- if (lookupInCache(cache, address)) |m| return m;
- return error.MissingDebugInfo;
-}
-fn lookupInCache(cache: *LookupCache, address: usize) ?ElfModule {
- cache.rwlock.lockShared();
- defer cache.rwlock.unlockShared();
- for (cache.ranges.items) |*range| {
- if (address >= range.start and address < range.start + range.len) {
- return range.mod;
- }
- }
- return null;
-}
-fn loadElf(module: *const ElfModule, gpa: Allocator, di: *DebugInfo) Error!void {
- std.debug.assert(di.loaded_elf == null);
- std.debug.assert(!di.scanned_dwarf);
-
- const load_result = if (module.name.len > 0) res: {
- var file = std.fs.cwd().openFile(module.name, .{}) catch return error.MissingDebugInfo;
- defer file.close();
- break :res ElfFile.load(gpa, file, module.build_id, &.native(module.name));
- } else res: {
- const path = std.fs.selfExePathAlloc(gpa) catch |err| switch (err) {
- error.OutOfMemory => |e| return e,
- else => return error.ReadFailed,
- };
- defer gpa.free(path);
- var file = std.fs.cwd().openFile(path, .{}) catch return error.MissingDebugInfo;
- defer file.close();
- break :res ElfFile.load(gpa, file, module.build_id, &.native(path));
- };
- di.loaded_elf = load_result catch |err| switch (err) {
- error.OutOfMemory,
- error.Unexpected,
- => |e| return e,
-
- error.Overflow,
- error.TruncatedElfFile,
- error.InvalidCompressedSection,
- error.InvalidElfMagic,
- error.InvalidElfVersion,
- error.InvalidElfClass,
- error.InvalidElfEndian,
- => return error.InvalidDebugInfo,
-
- error.SystemResources,
- error.MemoryMappingNotSupported,
- error.AccessDenied,
- error.LockedMemoryLimitExceeded,
- error.ProcessFdQuotaExceeded,
- error.SystemFdQuotaExceeded,
- => return error.ReadFailed,
- };
-
- const matches_native =
- di.loaded_elf.?.endian == native_endian and
- di.loaded_elf.?.is_64 == (@sizeOf(usize) == 8);
-
- if (!matches_native) {
- di.loaded_elf.?.deinit(gpa);
- di.loaded_elf = null;
- return error.InvalidDebugInfo;
- }
-}
-pub fn getSymbolAtAddress(module: *const ElfModule, gpa: Allocator, di: *DebugInfo, address: usize) Error!std.debug.Symbol {
- const vaddr = address - module.load_offset;
- {
- di.mutex.lock();
- defer di.mutex.unlock();
- if (di.loaded_elf == null) try module.loadElf(gpa, di);
- const loaded_elf = &di.loaded_elf.?;
- // We need the lock if using DWARF, as we might scan the DWARF or build a line number table.
- if (loaded_elf.dwarf) |*dwarf| {
- if (!di.scanned_dwarf) {
- dwarf.open(gpa, native_endian) catch |err| switch (err) {
- error.InvalidDebugInfo,
- error.MissingDebugInfo,
- error.OutOfMemory,
- => |e| return e,
- error.EndOfStream,
- error.Overflow,
- error.ReadFailed,
- error.StreamTooLong,
- => return error.InvalidDebugInfo,
- };
- di.scanned_dwarf = true;
- }
- return dwarf.getSymbol(gpa, native_endian, vaddr) catch |err| switch (err) {
- error.InvalidDebugInfo,
- error.MissingDebugInfo,
- error.OutOfMemory,
- => |e| return e,
- error.ReadFailed,
- error.EndOfStream,
- error.Overflow,
- error.StreamTooLong,
- => return error.InvalidDebugInfo,
- };
- }
- // Otherwise, we're just going to scan the symtab, which we don't need the lock for; fall out of this block.
- }
- // When there's no DWARF available, fall back to searching the symtab.
- return di.loaded_elf.?.searchSymtab(gpa, vaddr) catch |err| switch (err) {
- error.NoSymtab, error.NoStrtab => return error.MissingDebugInfo,
- error.BadSymtab => return error.InvalidDebugInfo,
- error.OutOfMemory => |e| return e,
- };
-}
-fn prepareUnwindLookup(unwind: *Dwarf.Unwind, gpa: Allocator) Error!void {
- unwind.prepare(gpa, @sizeOf(usize), native_endian, true, false) catch |err| switch (err) {
- error.ReadFailed => unreachable, // it's all fixed buffers
- error.InvalidDebugInfo,
- error.MissingDebugInfo,
- error.OutOfMemory,
- => |e| return e,
- error.EndOfStream,
- error.Overflow,
- error.StreamTooLong,
- error.InvalidOperand,
- error.InvalidOpcode,
- error.InvalidOperation,
- => return error.InvalidDebugInfo,
- error.UnsupportedAddrSize,
- error.UnsupportedDwarfVersion,
- error.UnimplementedUserOpcode,
- => return error.UnsupportedDebugInfo,
- };
-}
-fn loadUnwindInfo(module: *const ElfModule, gpa: Allocator, di: *DebugInfo) Error!void {
- var buf: [2]Dwarf.Unwind = undefined;
- const unwinds: []Dwarf.Unwind = if (module.gnu_eh_frame) |section_bytes| unwinds: {
- const section_vaddr: u64 = @intFromPtr(section_bytes.ptr) - module.load_offset;
- const header = Dwarf.Unwind.EhFrameHeader.parse(section_vaddr, section_bytes, @sizeOf(usize), native_endian) catch |err| switch (err) {
- error.ReadFailed => unreachable, // it's all fixed buffers
- error.InvalidDebugInfo => |e| return e,
- error.EndOfStream, error.Overflow => return error.InvalidDebugInfo,
- error.UnsupportedAddrSize => return error.UnsupportedDebugInfo,
- };
- buf[0] = .initEhFrameHdr(header, section_vaddr, @ptrFromInt(@as(usize, @intCast(module.load_offset + header.eh_frame_vaddr))));
- break :unwinds buf[0..1];
- } else unwinds: {
- // There is no `.eh_frame_hdr` section. There may still be an `.eh_frame` or `.debug_frame`
- // section, but we'll have to load the binary to get at it.
- if (di.loaded_elf == null) try module.loadElf(gpa, di);
- const opt_debug_frame = &di.loaded_elf.?.debug_frame;
- const opt_eh_frame = &di.loaded_elf.?.eh_frame;
- var i: usize = 0;
- // If both are present, we can't just pick one -- the info could be split between them.
- // `.debug_frame` is likely to be the more complete section, so we'll prioritize that one.
- if (opt_debug_frame.*) |*debug_frame| {
- buf[i] = .initSection(.debug_frame, debug_frame.vaddr, debug_frame.bytes);
- i += 1;
- }
- if (opt_eh_frame.*) |*eh_frame| {
- buf[i] = .initSection(.eh_frame, eh_frame.vaddr, eh_frame.bytes);
- i += 1;
- }
- if (i == 0) return error.MissingDebugInfo;
- break :unwinds buf[0..i];
- };
- errdefer for (unwinds) |*u| u.deinit(gpa);
- for (unwinds) |*u| try prepareUnwindLookup(u, gpa);
-
- const unwind_cache = try gpa.create(UnwindContext.Cache);
- errdefer gpa.destroy(unwind_cache);
- unwind_cache.init();
-
- switch (unwinds.len) {
- 0 => unreachable,
- 1 => di.unwind = .{ unwinds[0], null },
- 2 => di.unwind = .{ unwinds[0], unwinds[1] },
- else => unreachable,
- }
- di.unwind_cache = unwind_cache;
-}
-pub fn unwindFrame(module: *const ElfModule, gpa: Allocator, di: *DebugInfo, context: *UnwindContext) Error!usize {
- const unwinds: *const [2]?Dwarf.Unwind = u: {
- di.mutex.lock();
- defer di.mutex.unlock();
- if (di.unwind[0] == null) try module.loadUnwindInfo(gpa, di);
- std.debug.assert(di.unwind[0] != null);
- break :u &di.unwind;
- };
- for (unwinds) |*opt_unwind| {
- const unwind = &(opt_unwind.* orelse break);
- return context.unwindFrame(di.unwind_cache, gpa, unwind, module.load_offset, null) catch |err| switch (err) {
- error.MissingDebugInfo => continue, // try the next one
- else => |e| return e,
- };
- }
- return error.MissingDebugInfo;
-}
-pub const UnwindContext = std.debug.SelfInfo.DwarfUnwindContext;
-pub const supports_unwinding: bool = s: {
- // Notably, we are yet to support unwinding on ARM. There, unwinding is not done through
- // `.eh_frame`, but instead with the `.ARM.exidx` section, which has a different format.
- const archs: []const std.Target.Cpu.Arch = switch (builtin.target.os.tag) {
- .linux => &.{ .x86, .x86_64, .aarch64, .aarch64_be },
- .netbsd => &.{ .x86, .x86_64, .aarch64, .aarch64_be },
- .freebsd => &.{ .x86_64, .aarch64, .aarch64_be },
- .openbsd => &.{.x86_64},
- .solaris => &.{ .x86, .x86_64 },
- .illumos => &.{ .x86, .x86_64 },
- else => unreachable,
- };
- for (archs) |a| {
- if (builtin.target.cpu.arch == a) break :s true;
- }
- break :s false;
-};
-comptime {
- if (supports_unwinding) {
- std.debug.assert(Dwarf.supportsUnwinding(&builtin.target));
- }
-}
-
-const ElfModule = @This();
-
-const std = @import("../../std.zig");
-const Allocator = std.mem.Allocator;
-const Dwarf = std.debug.Dwarf;
-const ElfFile = std.debug.ElfFile;
-const elf = std.elf;
-const mem = std.mem;
-const Error = std.debug.SelfInfo.Error;
-
-const builtin = @import("builtin");
-const native_endian = builtin.target.cpu.arch.endian();
lib/std/debug/SelfInfo/Windows.zig
@@ -0,0 +1,559 @@
+mutex: std.Thread.Mutex,
+modules: std.ArrayListUnmanaged(Module),
+module_name_arena: std.heap.ArenaAllocator.State,
+
+pub const init: SelfInfo = .{
+ .mutex = .{},
+ .modules = .empty,
+ .module_name_arena = .{},
+};
+pub fn deinit(si: *SelfInfo, gpa: Allocator) void {
+ for (si.modules.items) |*module| {
+ di: {
+ const di = &(module.di orelse break :di catch break :di);
+ di.deinit(gpa);
+ }
+ }
+ si.modules.deinit(gpa);
+
+ var module_name_arena = si.module_name_arena.promote(gpa);
+ module_name_arena.deinit();
+}
+
+pub fn getSymbol(si: *SelfInfo, gpa: Allocator, address: usize) Error!std.debug.Symbol {
+ si.mutex.lock();
+ defer si.mutex.unlock();
+ const module = try si.findModule(gpa, address);
+ const di = try module.getDebugInfo(gpa);
+ return di.getSymbol(gpa, address - module.base_address);
+}
+pub fn getModuleName(si: *SelfInfo, gpa: Allocator, address: usize) Error![]const u8 {
+ si.mutex.lock();
+ defer si.mutex.unlock();
+ const module = try si.findModule(gpa, address);
+ return module.name;
+}
+
+pub const can_unwind: bool = switch (builtin.cpu.arch) {
+ else => true,
+ // On x86, `RtlVirtualUnwind` does not exist. We could in theory use `RtlCaptureStackBackTrace`
+ // instead, but on x86, it turns out that function is just... doing FP unwinding with esp! It's
+ // hard to find implementation details to confirm that, but the most authoritative source I have
+ // is an entry in the LLVM mailing list from 2020/08/16 which contains this quote:
+ //
+ // > x86 doesn't have what most architectures would consider an "unwinder" in the sense of
+ // > restoring registers; there is simply a linked list of frames that participate in SEH and
+ // > that desire to be called for a dynamic unwind operation, so RtlCaptureStackBackTrace
+ // > assumes that EBP-based frames are in use and walks an EBP-based frame chain on x86 - not
+ // > all x86 code is written with EBP-based frames so while even though we generally build the
+ // > OS that way, you might always run the risk of encountering external code that uses EBP as a
+ // > general purpose register for which such an unwind attempt for a stack trace would fail.
+ //
+ // Regardless, it's easy to effectively confirm this hypothesis just by compiling some code with
+ // `-fomit-frame-pointer -OReleaseFast` and observing that `RtlCaptureStackBackTrace` returns an
+ // empty trace when it's called in such an application. Note that without `-OReleaseFast` or
+ // similar, LLVM seems reluctant to ever clobber ebp, so you'll get a trace returned which just
+ // contains all of the kernel32/ntdll frames but none of your own. Don't be deceived---this is
+ // just coincidental!
+ //
+ // Anyway, the point is, the only stack walking primitive on x86-windows is FP unwinding. We
+ // *could* ask Microsoft to do that for us with `RtlCaptureStackBackTrace`... but better to just
+ // use our existing FP unwinder in `std.debug`!
+ .x86 => false,
+};
+pub const UnwindContext = struct {
+ pc: usize,
+ cur: windows.CONTEXT,
+ history_table: windows.UNWIND_HISTORY_TABLE,
+ pub fn init(ctx: *const std.debug.cpu_context.Native) UnwindContext {
+ return .{
+ .pc = @returnAddress(),
+ .cur = switch (builtin.cpu.arch) {
+ .x86_64 => std.mem.zeroInit(windows.CONTEXT, .{
+ .Rax = ctx.gprs.get(.rax),
+ .Rcx = ctx.gprs.get(.rcx),
+ .Rdx = ctx.gprs.get(.rdx),
+ .Rbx = ctx.gprs.get(.rbx),
+ .Rsp = ctx.gprs.get(.rsp),
+ .Rbp = ctx.gprs.get(.rbp),
+ .Rsi = ctx.gprs.get(.rsi),
+ .Rdi = ctx.gprs.get(.rdi),
+ .R8 = ctx.gprs.get(.r8),
+ .R9 = ctx.gprs.get(.r9),
+ .R10 = ctx.gprs.get(.r10),
+ .R11 = ctx.gprs.get(.r11),
+ .R12 = ctx.gprs.get(.r12),
+ .R13 = ctx.gprs.get(.r13),
+ .R14 = ctx.gprs.get(.r14),
+ .R15 = ctx.gprs.get(.r15),
+ .Rip = ctx.gprs.get(.rip),
+ }),
+ .aarch64, .aarch64_be => .{
+ .ContextFlags = 0,
+ .Cpsr = 0,
+ .DUMMYUNIONNAME = .{ .X = ctx.x },
+ .Sp = ctx.sp,
+ .Pc = ctx.pc,
+ .V = @splat(.{ .B = @splat(0) }),
+ .Fpcr = 0,
+ .Fpsr = 0,
+ .Bcr = @splat(0),
+ .Bvr = @splat(0),
+ .Wcr = @splat(0),
+ .Wvr = @splat(0),
+ },
+ .thumb => .{
+ .ContextFlags = 0,
+ .R0 = ctx.r[0],
+ .R1 = ctx.r[1],
+ .R2 = ctx.r[2],
+ .R3 = ctx.r[3],
+ .R4 = ctx.r[4],
+ .R5 = ctx.r[5],
+ .R6 = ctx.r[6],
+ .R7 = ctx.r[7],
+ .R8 = ctx.r[8],
+ .R9 = ctx.r[9],
+ .R10 = ctx.r[10],
+ .R11 = ctx.r[11],
+ .R12 = ctx.r[12],
+ .Sp = ctx.r[13],
+ .Lr = ctx.r[14],
+ .Pc = ctx.r[15],
+ .Cpsr = 0,
+ .Fpcsr = 0,
+ .Padding = 0,
+ .DUMMYUNIONNAME = .{ .S = @splat(0) },
+ .Bvr = @splat(0),
+ .Bcr = @splat(0),
+ .Wvr = @splat(0),
+ .Wcr = @splat(0),
+ .Padding2 = @splat(0),
+ },
+ else => comptime unreachable,
+ },
+ .history_table = std.mem.zeroes(windows.UNWIND_HISTORY_TABLE),
+ };
+ }
+ pub fn deinit(ctx: *UnwindContext, gpa: Allocator) void {
+ _ = ctx;
+ _ = gpa;
+ }
+ pub fn getFp(ctx: *UnwindContext) usize {
+ return ctx.cur.getRegs().bp;
+ }
+};
+pub fn unwindFrame(si: *SelfInfo, gpa: Allocator, context: *UnwindContext) Error!usize {
+ _ = si;
+ _ = gpa;
+
+ const current_regs = context.cur.getRegs();
+ var image_base: windows.DWORD64 = undefined;
+ if (windows.ntdll.RtlLookupFunctionEntry(current_regs.ip, &image_base, &context.history_table)) |runtime_function| {
+ var handler_data: ?*anyopaque = null;
+ var establisher_frame: u64 = undefined;
+ _ = windows.ntdll.RtlVirtualUnwind(
+ windows.UNW_FLAG_NHANDLER,
+ image_base,
+ current_regs.ip,
+ runtime_function,
+ &context.cur,
+ &handler_data,
+ &establisher_frame,
+ null,
+ );
+ } else {
+ // leaf function
+ context.cur.setIp(@as(*const usize, @ptrFromInt(current_regs.sp)).*);
+ context.cur.setSp(current_regs.sp + @sizeOf(usize));
+ }
+
+ const next_regs = context.cur.getRegs();
+ const tib = &windows.teb().NtTib;
+ if (next_regs.sp < @intFromPtr(tib.StackLimit) or next_regs.sp > @intFromPtr(tib.StackBase)) {
+ context.pc = 0;
+ return 0;
+ }
+ // Like `DwarfUnwindContext.unwindFrame`, adjust our next lookup pc in case the `call` was this
+ // function's last instruction making `next_regs.ip` one byte past its end.
+ context.pc = next_regs.ip -| 1;
+ return next_regs.ip;
+}
+
+const Module = struct {
+ base_address: usize,
+ size: u32,
+ name: []const u8,
+ handle: windows.HMODULE,
+
+ di: ?(Error!DebugInfo),
+
+ const DebugInfo = struct {
+ arena: std.heap.ArenaAllocator.State,
+ coff_image_base: u64,
+ mapped_file: ?MappedFile,
+ dwarf: ?Dwarf,
+ pdb: ?Pdb,
+ coff_section_headers: []coff.SectionHeader,
+
+ const MappedFile = struct {
+ file: fs.File,
+ section_handle: windows.HANDLE,
+ section_view: []const u8,
+ fn deinit(mf: *const MappedFile) void {
+ const process_handle = windows.GetCurrentProcess();
+ assert(windows.ntdll.NtUnmapViewOfSection(process_handle, @constCast(mf.section_view.ptr)) == .SUCCESS);
+ windows.CloseHandle(mf.section_handle);
+ mf.file.close();
+ }
+ };
+
+ fn deinit(di: *DebugInfo, gpa: Allocator) void {
+ if (di.dwarf) |*dwarf| dwarf.deinit(gpa);
+ if (di.pdb) |*pdb| {
+ pdb.file_reader.file.close();
+ pdb.deinit();
+ }
+ if (di.mapped_file) |*mf| mf.deinit();
+
+ var arena = di.arena.promote(gpa);
+ arena.deinit();
+ }
+
+ fn getSymbol(di: *DebugInfo, gpa: Allocator, vaddr: usize) Error!std.debug.Symbol {
+ pdb: {
+ const pdb = &(di.pdb orelse break :pdb);
+ var coff_section: *align(1) const coff.SectionHeader = undefined;
+ const mod_index = for (pdb.sect_contribs) |sect_contrib| {
+ if (sect_contrib.section > di.coff_section_headers.len) continue;
+ // Remember that SectionContribEntry.Section is 1-based.
+ coff_section = &di.coff_section_headers[sect_contrib.section - 1];
+
+ const vaddr_start = coff_section.virtual_address + sect_contrib.offset;
+ const vaddr_end = vaddr_start + sect_contrib.size;
+ if (vaddr >= vaddr_start and vaddr < vaddr_end) {
+ break sect_contrib.module_index;
+ }
+ } else {
+ // we have no information to add to the address
+ break :pdb;
+ };
+ const module = pdb.getModule(mod_index) catch |err| switch (err) {
+ error.InvalidDebugInfo,
+ error.MissingDebugInfo,
+ error.OutOfMemory,
+ => |e| return e,
+
+ error.ReadFailed,
+ error.EndOfStream,
+ => return error.InvalidDebugInfo,
+ } orelse {
+ return error.InvalidDebugInfo; // bad module index
+ };
+ return .{
+ .name = pdb.getSymbolName(module, vaddr - coff_section.virtual_address),
+ .compile_unit_name = fs.path.basename(module.obj_file_name),
+ .source_location = pdb.getLineNumberInfo(module, vaddr - coff_section.virtual_address) catch null,
+ };
+ }
+ dwarf: {
+ const dwarf = &(di.dwarf orelse break :dwarf);
+ const dwarf_address = vaddr + di.coff_image_base;
+ return dwarf.getSymbol(gpa, native_endian, dwarf_address) catch |err| switch (err) {
+ error.MissingDebugInfo => break :dwarf,
+
+ error.InvalidDebugInfo,
+ error.OutOfMemory,
+ => |e| return e,
+
+ error.ReadFailed,
+ error.EndOfStream,
+ error.Overflow,
+ error.StreamTooLong,
+ => return error.InvalidDebugInfo,
+ };
+ }
+ return error.MissingDebugInfo;
+ }
+ };
+
+ fn getDebugInfo(module: *Module, gpa: Allocator) Error!*DebugInfo {
+ if (module.di == null) module.di = loadDebugInfo(module, gpa);
+ return if (module.di.?) |*di| di else |err| err;
+ }
+ fn loadDebugInfo(module: *const Module, gpa: Allocator) Error!DebugInfo {
+ const mapped_ptr: [*]const u8 = @ptrFromInt(module.base_address);
+ const mapped = mapped_ptr[0..module.size];
+ var coff_obj = coff.Coff.init(mapped, true) catch return error.InvalidDebugInfo;
+
+ var arena_instance: std.heap.ArenaAllocator = .init(gpa);
+ errdefer arena_instance.deinit();
+ const arena = arena_instance.allocator();
+
+ // The string table is not mapped into memory by the loader, so if a section name is in the
+ // string table then we have to map the full image file from disk. This can happen when
+ // a binary is produced with -gdwarf, since the section names are longer than 8 bytes.
+ const mapped_file: ?DebugInfo.MappedFile = mapped: {
+ if (!coff_obj.strtabRequired()) break :mapped null;
+ var name_buffer: [windows.PATH_MAX_WIDE + 4:0]u16 = undefined;
+ name_buffer[0..4].* = .{ '\\', '?', '?', '\\' }; // openFileAbsoluteW requires the prefix to be present
+ const process_handle = windows.GetCurrentProcess();
+ const len = windows.kernel32.GetModuleFileNameExW(
+ process_handle,
+ module.handle,
+ name_buffer[4..],
+ windows.PATH_MAX_WIDE,
+ );
+ if (len == 0) return error.MissingDebugInfo;
+ const coff_file = fs.openFileAbsoluteW(name_buffer[0 .. len + 4 :0], .{}) catch |err| switch (err) {
+ error.Unexpected => |e| return e,
+ error.FileNotFound => return error.MissingDebugInfo,
+
+ error.FileTooBig,
+ error.IsDir,
+ error.NotDir,
+ error.SymLinkLoop,
+ error.NameTooLong,
+ error.InvalidUtf8,
+ error.InvalidWtf8,
+ error.BadPathName,
+ => return error.InvalidDebugInfo,
+
+ error.SystemResources,
+ error.WouldBlock,
+ error.AccessDenied,
+ error.ProcessNotFound,
+ error.PermissionDenied,
+ error.NoSpaceLeft,
+ error.DeviceBusy,
+ error.NoDevice,
+ error.SharingViolation,
+ error.PathAlreadyExists,
+ error.PipeBusy,
+ error.NetworkNotFound,
+ error.AntivirusInterference,
+ error.ProcessFdQuotaExceeded,
+ error.SystemFdQuotaExceeded,
+ error.FileLocksNotSupported,
+ error.FileBusy,
+ => return error.ReadFailed,
+ };
+ errdefer coff_file.close();
+ var section_handle: windows.HANDLE = undefined;
+ const create_section_rc = windows.ntdll.NtCreateSection(
+ §ion_handle,
+ windows.STANDARD_RIGHTS_REQUIRED | windows.SECTION_QUERY | windows.SECTION_MAP_READ,
+ null,
+ null,
+ windows.PAGE_READONLY,
+ // The documentation states that if no AllocationAttribute is specified, then SEC_COMMIT is the default.
+ // In practice, this isn't the case and specifying 0 will result in INVALID_PARAMETER_6.
+ windows.SEC_COMMIT,
+ coff_file.handle,
+ );
+ if (create_section_rc != .SUCCESS) return error.MissingDebugInfo;
+ errdefer windows.CloseHandle(section_handle);
+ var coff_len: usize = 0;
+ var section_view_ptr: ?[*]const u8 = null;
+ const map_section_rc = windows.ntdll.NtMapViewOfSection(
+ section_handle,
+ process_handle,
+ @ptrCast(§ion_view_ptr),
+ null,
+ 0,
+ null,
+ &coff_len,
+ .ViewUnmap,
+ 0,
+ windows.PAGE_READONLY,
+ );
+ if (map_section_rc != .SUCCESS) return error.MissingDebugInfo;
+ errdefer assert(windows.ntdll.NtUnmapViewOfSection(process_handle, @constCast(section_view_ptr.?)) == .SUCCESS);
+ const section_view = section_view_ptr.?[0..coff_len];
+ coff_obj = coff.Coff.init(section_view, false) catch return error.InvalidDebugInfo;
+ break :mapped .{
+ .file = coff_file,
+ .section_handle = section_handle,
+ .section_view = section_view,
+ };
+ };
+ errdefer if (mapped_file) |*mf| mf.deinit();
+
+ const coff_image_base = coff_obj.getImageBase();
+
+ var opt_dwarf: ?Dwarf = dwarf: {
+ if (coff_obj.getSectionByName(".debug_info") == null) break :dwarf null;
+
+ var sections: Dwarf.SectionArray = undefined;
+ inline for (@typeInfo(Dwarf.Section.Id).@"enum".fields, 0..) |section, i| {
+ sections[i] = if (coff_obj.getSectionByName("." ++ section.name)) |section_header| .{
+ .data = try coff_obj.getSectionDataAlloc(section_header, arena),
+ .owned = false,
+ } else null;
+ }
+ break :dwarf .{ .sections = sections };
+ };
+ errdefer if (opt_dwarf) |*dwarf| dwarf.deinit(gpa);
+
+ if (opt_dwarf) |*dwarf| {
+ dwarf.open(gpa, native_endian) catch |err| switch (err) {
+ error.Overflow,
+ error.EndOfStream,
+ error.StreamTooLong,
+ error.ReadFailed,
+ => return error.InvalidDebugInfo,
+
+ error.InvalidDebugInfo,
+ error.MissingDebugInfo,
+ error.OutOfMemory,
+ => |e| return e,
+ };
+ }
+
+ var opt_pdb: ?Pdb = pdb: {
+ const path = coff_obj.getPdbPath() catch {
+ return error.InvalidDebugInfo;
+ } orelse {
+ break :pdb null;
+ };
+ const pdb_file_open_result = if (fs.path.isAbsolute(path)) res: {
+ break :res std.fs.cwd().openFile(path, .{});
+ } else res: {
+ const self_dir = fs.selfExeDirPathAlloc(gpa) catch |err| switch (err) {
+ error.OutOfMemory, error.Unexpected => |e| return e,
+ else => return error.ReadFailed,
+ };
+ defer gpa.free(self_dir);
+ const abs_path = try fs.path.join(gpa, &.{ self_dir, path });
+ defer gpa.free(abs_path);
+ break :res std.fs.cwd().openFile(abs_path, .{});
+ };
+ const pdb_file = pdb_file_open_result catch |err| switch (err) {
+ error.FileNotFound, error.IsDir => break :pdb null,
+ else => return error.ReadFailed,
+ };
+ errdefer pdb_file.close();
+
+ const pdb_reader = try arena.create(std.fs.File.Reader);
+ pdb_reader.* = pdb_file.reader(try arena.alloc(u8, 4096));
+
+ var pdb = Pdb.init(gpa, pdb_reader) catch |err| switch (err) {
+ error.OutOfMemory, error.ReadFailed, error.Unexpected => |e| return e,
+ else => return error.InvalidDebugInfo,
+ };
+ errdefer pdb.deinit();
+ pdb.parseInfoStream() catch |err| switch (err) {
+ error.UnknownPDBVersion => return error.UnsupportedDebugInfo,
+ error.EndOfStream => return error.InvalidDebugInfo,
+
+ error.InvalidDebugInfo,
+ error.MissingDebugInfo,
+ error.OutOfMemory,
+ error.ReadFailed,
+ => |e| return e,
+ };
+ pdb.parseDbiStream() catch |err| switch (err) {
+ error.UnknownPDBVersion => return error.UnsupportedDebugInfo,
+
+ error.EndOfStream,
+ error.EOF,
+ error.StreamTooLong,
+ error.WriteFailed,
+ => return error.InvalidDebugInfo,
+
+ error.InvalidDebugInfo,
+ error.OutOfMemory,
+ error.ReadFailed,
+ => |e| return e,
+ };
+
+ if (!std.mem.eql(u8, &coff_obj.guid, &pdb.guid) or coff_obj.age != pdb.age)
+ return error.InvalidDebugInfo;
+
+ break :pdb pdb;
+ };
+ errdefer if (opt_pdb) |*pdb| {
+ pdb.file_reader.file.close();
+ pdb.deinit();
+ };
+
+ const coff_section_headers: []coff.SectionHeader = if (opt_pdb != null) csh: {
+ break :csh try coff_obj.getSectionHeadersAlloc(arena);
+ } else &.{};
+
+ return .{
+ .arena = arena_instance.state,
+ .coff_image_base = coff_image_base,
+ .mapped_file = mapped_file,
+ .dwarf = opt_dwarf,
+ .pdb = opt_pdb,
+ .coff_section_headers = coff_section_headers,
+ };
+ }
+};
+
+/// Assumes we already hold `si.mutex`.
+fn findModule(si: *SelfInfo, gpa: Allocator, address: usize) error{ MissingDebugInfo, OutOfMemory, Unexpected }!*Module {
+ for (si.modules.items) |*mod| {
+ if (address >= mod.base_address and address < mod.base_address + mod.size) {
+ return mod;
+ }
+ }
+
+ // A new module might have been loaded; rebuild the list.
+ {
+ for (si.modules.items) |*mod| {
+ const di = &(mod.di orelse continue catch continue);
+ di.deinit(gpa);
+ }
+ si.modules.clearRetainingCapacity();
+
+ var module_name_arena = si.module_name_arena.promote(gpa);
+ defer si.module_name_arena = module_name_arena.state;
+ _ = module_name_arena.reset(.retain_capacity);
+
+ const handle = windows.kernel32.CreateToolhelp32Snapshot(windows.TH32CS_SNAPMODULE | windows.TH32CS_SNAPMODULE32, 0);
+ if (handle == windows.INVALID_HANDLE_VALUE) {
+ return windows.unexpectedError(windows.GetLastError());
+ }
+ defer windows.CloseHandle(handle);
+ var entry: windows.MODULEENTRY32 = undefined;
+ entry.dwSize = @sizeOf(windows.MODULEENTRY32);
+ var result = windows.kernel32.Module32First(handle, &entry);
+ while (result != 0) : (result = windows.kernel32.Module32Next(handle, &entry)) {
+ try si.modules.append(gpa, .{
+ .base_address = @intFromPtr(entry.modBaseAddr),
+ .size = entry.modBaseSize,
+ .name = try module_name_arena.allocator().dupe(
+ u8,
+ std.mem.sliceTo(&entry.szModule, 0),
+ ),
+ .handle = entry.hModule,
+ .di = null,
+ });
+ }
+ }
+
+ for (si.modules.items) |*mod| {
+ if (address >= mod.base_address and address < mod.base_address + mod.size) {
+ return mod;
+ }
+ }
+
+ return error.MissingDebugInfo;
+}
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const Dwarf = std.debug.Dwarf;
+const Pdb = std.debug.Pdb;
+const Error = std.debug.SelfInfoError;
+const assert = std.debug.assert;
+const coff = std.coff;
+const fs = std.fs;
+const windows = std.os.windows;
+
+const builtin = @import("builtin");
+const native_endian = builtin.target.cpu.arch.endian();
+
+const SelfInfo = @This();
lib/std/debug/SelfInfo/WindowsModule.zig
@@ -1,442 +0,0 @@
-base_address: usize,
-size: usize,
-name: []const u8,
-handle: windows.HMODULE,
-pub fn key(m: WindowsModule) usize {
- return m.base_address;
-}
-pub fn lookup(cache: *LookupCache, gpa: Allocator, address: usize) std.debug.SelfInfo.Error!WindowsModule {
- if (lookupInCache(cache, address)) |m| return m;
- {
- // Check a new module hasn't been loaded
- cache.rwlock.lock();
- defer cache.rwlock.unlock();
- cache.modules.clearRetainingCapacity();
- const handle = windows.kernel32.CreateToolhelp32Snapshot(windows.TH32CS_SNAPMODULE | windows.TH32CS_SNAPMODULE32, 0);
- if (handle == windows.INVALID_HANDLE_VALUE) {
- return windows.unexpectedError(windows.GetLastError());
- }
- defer windows.CloseHandle(handle);
- var entry: windows.MODULEENTRY32 = undefined;
- entry.dwSize = @sizeOf(windows.MODULEENTRY32);
- if (windows.kernel32.Module32First(handle, &entry) != 0) {
- try cache.modules.append(gpa, entry);
- while (windows.kernel32.Module32Next(handle, &entry) != 0) {
- try cache.modules.append(gpa, entry);
- }
- }
- }
- if (lookupInCache(cache, address)) |m| return m;
- return error.MissingDebugInfo;
-}
-pub fn getSymbolAtAddress(module: *const WindowsModule, gpa: Allocator, di: *DebugInfo, address: usize) std.debug.SelfInfo.Error!std.debug.Symbol {
- // The `Pdb` API doesn't really allow us *any* thread-safe access, and the `Dwarf` API isn't
- // great for it either; just lock the whole thing.
- di.mutex.lock();
- defer di.mutex.unlock();
-
- if (!di.loaded) module.loadDebugInfo(gpa, di) catch |err| switch (err) {
- error.OutOfMemory, error.InvalidDebugInfo, error.MissingDebugInfo, error.Unexpected => |e| return e,
- error.FileNotFound => return error.MissingDebugInfo,
- error.UnknownPDBVersion => return error.UnsupportedDebugInfo,
- else => return error.ReadFailed,
- };
-
- // Translate the runtime address into a virtual address into the module
- const vaddr = address - module.base_address;
-
- if (di.pdb != null) {
- if (di.getSymbolFromPdb(vaddr) catch return error.InvalidDebugInfo) |symbol| return symbol;
- }
-
- if (di.dwarf) |*dwarf| {
- const dwarf_address = vaddr + di.coff_image_base;
- return dwarf.getSymbol(gpa, native_endian, dwarf_address) catch return error.InvalidDebugInfo;
- }
-
- return error.MissingDebugInfo;
-}
-fn lookupInCache(cache: *LookupCache, address: usize) ?WindowsModule {
- cache.rwlock.lockShared();
- defer cache.rwlock.unlockShared();
- for (cache.modules.items) |*entry| {
- const base_address = @intFromPtr(entry.modBaseAddr);
- if (address >= base_address and address < base_address + entry.modBaseSize) {
- return .{
- .base_address = base_address,
- .size = entry.modBaseSize,
- .name = std.mem.sliceTo(&entry.szModule, 0),
- .handle = entry.hModule,
- };
- }
- }
- return null;
-}
-fn loadDebugInfo(module: *const WindowsModule, gpa: Allocator, di: *DebugInfo) !void {
- const mapped_ptr: [*]const u8 = @ptrFromInt(module.base_address);
- const mapped = mapped_ptr[0..module.size];
- var coff_obj = coff.Coff.init(mapped, true) catch return error.InvalidDebugInfo;
- // The string table is not mapped into memory by the loader, so if a section name is in the
- // string table then we have to map the full image file from disk. This can happen when
- // a binary is produced with -gdwarf, since the section names are longer than 8 bytes.
- if (coff_obj.strtabRequired()) {
- var name_buffer: [windows.PATH_MAX_WIDE + 4:0]u16 = undefined;
- name_buffer[0..4].* = .{ '\\', '?', '?', '\\' }; // openFileAbsoluteW requires the prefix to be present
- const process_handle = windows.GetCurrentProcess();
- const len = windows.kernel32.GetModuleFileNameExW(
- process_handle,
- module.handle,
- name_buffer[4..],
- windows.PATH_MAX_WIDE,
- );
- if (len == 0) return error.MissingDebugInfo;
- const coff_file = fs.openFileAbsoluteW(name_buffer[0 .. len + 4 :0], .{}) catch |err| switch (err) {
- error.FileNotFound => return error.MissingDebugInfo,
- else => |e| return e,
- };
- errdefer coff_file.close();
- var section_handle: windows.HANDLE = undefined;
- const create_section_rc = windows.ntdll.NtCreateSection(
- §ion_handle,
- windows.STANDARD_RIGHTS_REQUIRED | windows.SECTION_QUERY | windows.SECTION_MAP_READ,
- null,
- null,
- windows.PAGE_READONLY,
- // The documentation states that if no AllocationAttribute is specified, then SEC_COMMIT is the default.
- // In practice, this isn't the case and specifying 0 will result in INVALID_PARAMETER_6.
- windows.SEC_COMMIT,
- coff_file.handle,
- );
- if (create_section_rc != .SUCCESS) return error.MissingDebugInfo;
- errdefer windows.CloseHandle(section_handle);
- var coff_len: usize = 0;
- var section_view_ptr: ?[*]const u8 = null;
- const map_section_rc = windows.ntdll.NtMapViewOfSection(
- section_handle,
- process_handle,
- @ptrCast(§ion_view_ptr),
- null,
- 0,
- null,
- &coff_len,
- .ViewUnmap,
- 0,
- windows.PAGE_READONLY,
- );
- if (map_section_rc != .SUCCESS) return error.MissingDebugInfo;
- errdefer assert(windows.ntdll.NtUnmapViewOfSection(process_handle, @constCast(section_view_ptr.?)) == .SUCCESS);
- const section_view = section_view_ptr.?[0..coff_len];
- coff_obj = coff.Coff.init(section_view, false) catch return error.InvalidDebugInfo;
- di.mapped_file = .{
- .file = coff_file,
- .section_handle = section_handle,
- .section_view = section_view,
- };
- }
- di.coff_image_base = coff_obj.getImageBase();
-
- if (coff_obj.getSectionByName(".debug_info")) |_| {
- di.dwarf = .{};
-
- inline for (@typeInfo(Dwarf.Section.Id).@"enum".fields, 0..) |section, i| {
- di.dwarf.?.sections[i] = if (coff_obj.getSectionByName("." ++ section.name)) |section_header| blk: {
- break :blk .{
- .data = try coff_obj.getSectionDataAlloc(section_header, gpa),
- .owned = true,
- };
- } else null;
- }
-
- try di.dwarf.?.open(gpa, native_endian);
- }
-
- if (coff_obj.getPdbPath() catch return error.InvalidDebugInfo) |raw_path| pdb: {
- const path = blk: {
- if (fs.path.isAbsolute(raw_path)) {
- break :blk raw_path;
- } else {
- const self_dir = try fs.selfExeDirPathAlloc(gpa);
- defer gpa.free(self_dir);
- break :blk try fs.path.join(gpa, &.{ self_dir, raw_path });
- }
- };
- defer if (path.ptr != raw_path.ptr) gpa.free(path);
-
- const pdb_file = std.fs.cwd().openFile(path, .{}) catch |err| switch (err) {
- error.FileNotFound, error.IsDir => break :pdb,
- else => |e| return e,
- };
- errdefer pdb_file.close();
-
- const pdb_reader = try gpa.create(std.fs.File.Reader);
- errdefer gpa.destroy(pdb_reader);
-
- pdb_reader.* = pdb_file.reader(try gpa.alloc(u8, 4096));
- errdefer gpa.free(pdb_reader.interface.buffer);
-
- var pdb: Pdb = try .init(gpa, pdb_reader);
- errdefer pdb.deinit();
- try pdb.parseInfoStream();
- try pdb.parseDbiStream();
-
- if (!mem.eql(u8, &coff_obj.guid, &pdb.guid) or coff_obj.age != pdb.age)
- return error.InvalidDebugInfo;
-
- di.coff_section_headers = try coff_obj.getSectionHeadersAlloc(gpa);
-
- di.pdb = pdb;
- }
-
- di.loaded = true;
-}
-pub const LookupCache = struct {
- rwlock: std.Thread.RwLock,
- modules: std.ArrayListUnmanaged(windows.MODULEENTRY32),
- pub const init: LookupCache = .{
- .rwlock = .{},
- .modules = .empty,
- };
- pub fn deinit(lc: *LookupCache, gpa: Allocator) void {
- lc.modules.deinit(gpa);
- }
-};
-pub const DebugInfo = struct {
- mutex: std.Thread.Mutex,
-
- loaded: bool,
-
- coff_image_base: u64,
- mapped_file: ?struct {
- file: fs.File,
- section_handle: windows.HANDLE,
- section_view: []const u8,
- },
-
- dwarf: ?Dwarf,
-
- pdb: ?Pdb,
- /// Populated iff `pdb != null`; otherwise `&.{}`.
- coff_section_headers: []coff.SectionHeader,
-
- pub const init: DebugInfo = .{
- .mutex = .{},
- .loaded = false,
- .coff_image_base = undefined,
- .mapped_file = null,
- .dwarf = null,
- .pdb = null,
- .coff_section_headers = &.{},
- };
-
- pub fn deinit(di: *DebugInfo, gpa: Allocator) void {
- if (!di.loaded) return;
- if (di.dwarf) |*dwarf| dwarf.deinit(gpa);
- if (di.pdb) |*pdb| {
- pdb.file_reader.file.close();
- gpa.free(pdb.file_reader.interface.buffer);
- gpa.destroy(pdb.file_reader);
- pdb.deinit();
- }
- gpa.free(di.coff_section_headers);
- if (di.mapped_file) |mapped| {
- const process_handle = windows.GetCurrentProcess();
- assert(windows.ntdll.NtUnmapViewOfSection(process_handle, @constCast(mapped.section_view.ptr)) == .SUCCESS);
- windows.CloseHandle(mapped.section_handle);
- mapped.file.close();
- }
- }
-
- fn getSymbolFromPdb(di: *DebugInfo, relocated_address: usize) !?std.debug.Symbol {
- var coff_section: *align(1) const coff.SectionHeader = undefined;
- const mod_index = for (di.pdb.?.sect_contribs) |sect_contrib| {
- if (sect_contrib.section > di.coff_section_headers.len) continue;
- // Remember that SectionContribEntry.Section is 1-based.
- coff_section = &di.coff_section_headers[sect_contrib.section - 1];
-
- const vaddr_start = coff_section.virtual_address + sect_contrib.offset;
- const vaddr_end = vaddr_start + sect_contrib.size;
- if (relocated_address >= vaddr_start and relocated_address < vaddr_end) {
- break sect_contrib.module_index;
- }
- } else {
- // we have no information to add to the address
- return null;
- };
-
- const module = try di.pdb.?.getModule(mod_index) orelse return error.InvalidDebugInfo;
-
- return .{
- .name = di.pdb.?.getSymbolName(
- module,
- relocated_address - coff_section.virtual_address,
- ),
- .compile_unit_name = fs.path.basename(module.obj_file_name),
- .source_location = try di.pdb.?.getLineNumberInfo(
- module,
- relocated_address - coff_section.virtual_address,
- ),
- };
- }
-};
-
-pub const supports_unwinding: bool = switch (builtin.cpu.arch) {
- else => true,
- // On x86, `RtlVirtualUnwind` does not exist. We could in theory use `RtlCaptureStackBackTrace`
- // instead, but on x86, it turns out that function is just... doing FP unwinding with esp! It's
- // hard to find implementation details to confirm that, but the most authoritative source I have
- // is an entry in the LLVM mailing list from 2020/08/16 which contains this quote:
- //
- // > x86 doesn't have what most architectures would consider an "unwinder" in the sense of
- // > restoring registers; there is simply a linked list of frames that participate in SEH and
- // > that desire to be called for a dynamic unwind operation, so RtlCaptureStackBackTrace
- // > assumes that EBP-based frames are in use and walks an EBP-based frame chain on x86 - not
- // > all x86 code is written with EBP-based frames so while even though we generally build the
- // > OS that way, you might always run the risk of encountering external code that uses EBP as a
- // > general purpose register for which such an unwind attempt for a stack trace would fail.
- //
- // Regardless, it's easy to effectively confirm this hypothesis just by compiling some code with
- // `-fomit-frame-pointer -OReleaseFast` and observing that `RtlCaptureStackBackTrace` returns an
- // empty trace when it's called in such an application. Note that without `-OReleaseFast` or
- // similar, LLVM seems reluctant to ever clobber ebp, so you'll get a trace returned which just
- // contains all of the kernel32/ntdll frames but none of your own. Don't be deceived---this is
- // just coincidental!
- //
- // Anyway, the point is, the only stack walking primitive on x86-windows is FP unwinding. We
- // *could* ask Microsoft to do that for us with `RtlCaptureStackBackTrace`... but better to just
- // use our existing FP unwinder in `std.debug`!
- .x86 => false,
-};
-pub const UnwindContext = struct {
- pc: usize,
- cur: windows.CONTEXT,
- history_table: windows.UNWIND_HISTORY_TABLE,
- pub fn init(ctx: *const std.debug.cpu_context.Native) UnwindContext {
- return .{
- .pc = @returnAddress(),
- .cur = switch (builtin.cpu.arch) {
- .x86_64 => std.mem.zeroInit(windows.CONTEXT, .{
- .Rax = ctx.gprs.get(.rax),
- .Rcx = ctx.gprs.get(.rcx),
- .Rdx = ctx.gprs.get(.rdx),
- .Rbx = ctx.gprs.get(.rbx),
- .Rsp = ctx.gprs.get(.rsp),
- .Rbp = ctx.gprs.get(.rbp),
- .Rsi = ctx.gprs.get(.rsi),
- .Rdi = ctx.gprs.get(.rdi),
- .R8 = ctx.gprs.get(.r8),
- .R9 = ctx.gprs.get(.r9),
- .R10 = ctx.gprs.get(.r10),
- .R11 = ctx.gprs.get(.r11),
- .R12 = ctx.gprs.get(.r12),
- .R13 = ctx.gprs.get(.r13),
- .R14 = ctx.gprs.get(.r14),
- .R15 = ctx.gprs.get(.r15),
- .Rip = ctx.gprs.get(.rip),
- }),
- .aarch64, .aarch64_be => .{
- .ContextFlags = 0,
- .Cpsr = 0,
- .DUMMYUNIONNAME = .{ .X = ctx.x },
- .Sp = ctx.sp,
- .Pc = ctx.pc,
- .V = @splat(.{ .B = @splat(0) }),
- .Fpcr = 0,
- .Fpsr = 0,
- .Bcr = @splat(0),
- .Bvr = @splat(0),
- .Wcr = @splat(0),
- .Wvr = @splat(0),
- },
- .thumb => .{
- .ContextFlags = 0,
- .R0 = ctx.r[0],
- .R1 = ctx.r[1],
- .R2 = ctx.r[2],
- .R3 = ctx.r[3],
- .R4 = ctx.r[4],
- .R5 = ctx.r[5],
- .R6 = ctx.r[6],
- .R7 = ctx.r[7],
- .R8 = ctx.r[8],
- .R9 = ctx.r[9],
- .R10 = ctx.r[10],
- .R11 = ctx.r[11],
- .R12 = ctx.r[12],
- .Sp = ctx.r[13],
- .Lr = ctx.r[14],
- .Pc = ctx.r[15],
- .Cpsr = 0,
- .Fpcsr = 0,
- .Padding = 0,
- .DUMMYUNIONNAME = .{ .S = @splat(0) },
- .Bvr = @splat(0),
- .Bcr = @splat(0),
- .Wvr = @splat(0),
- .Wcr = @splat(0),
- .Padding2 = @splat(0),
- },
- else => comptime unreachable,
- },
- .history_table = std.mem.zeroes(windows.UNWIND_HISTORY_TABLE),
- };
- }
- pub fn deinit(ctx: *UnwindContext, gpa: Allocator) void {
- _ = ctx;
- _ = gpa;
- }
- pub fn getFp(ctx: *UnwindContext) usize {
- return ctx.cur.getRegs().bp;
- }
-};
-pub fn unwindFrame(module: *const WindowsModule, gpa: Allocator, di: *DebugInfo, context: *UnwindContext) !usize {
- _ = module;
- _ = gpa;
- _ = di;
-
- const current_regs = context.cur.getRegs();
- var image_base: windows.DWORD64 = undefined;
- if (windows.ntdll.RtlLookupFunctionEntry(current_regs.ip, &image_base, &context.history_table)) |runtime_function| {
- var handler_data: ?*anyopaque = null;
- var establisher_frame: u64 = undefined;
- _ = windows.ntdll.RtlVirtualUnwind(
- windows.UNW_FLAG_NHANDLER,
- image_base,
- current_regs.ip,
- runtime_function,
- &context.cur,
- &handler_data,
- &establisher_frame,
- null,
- );
- } else {
- // leaf function
- context.cur.setIp(@as(*const usize, @ptrFromInt(current_regs.sp)).*);
- context.cur.setSp(current_regs.sp + @sizeOf(usize));
- }
-
- const next_regs = context.cur.getRegs();
- const tib = &windows.teb().NtTib;
- if (next_regs.sp < @intFromPtr(tib.StackLimit) or next_regs.sp > @intFromPtr(tib.StackBase)) {
- context.pc = 0;
- return 0;
- }
- // Like `DwarfUnwindContext.unwindFrame`, adjust our next lookup pc in case the `call` was this
- // function's last instruction making `next_regs.ip` one byte past its end.
- context.pc = next_regs.ip -| 1;
- return next_regs.ip;
-}
-
-const WindowsModule = @This();
-
-const std = @import("../../std.zig");
-const Allocator = std.mem.Allocator;
-const Dwarf = std.debug.Dwarf;
-const Pdb = std.debug.Pdb;
-const assert = std.debug.assert;
-const coff = std.coff;
-const fs = std.fs;
-const mem = std.mem;
-const windows = std.os.windows;
-
-const builtin = @import("builtin");
-const native_endian = builtin.target.cpu.arch.endian();
lib/std/debug/Dwarf.zig
@@ -28,6 +28,7 @@ const Dwarf = @This();
pub const expression = @import("Dwarf/expression.zig");
pub const Unwind = @import("Dwarf/Unwind.zig");
+pub const SelfUnwinder = @import("Dwarf/SelfUnwinder.zig");
/// Useful to temporarily enable while working on this file.
const debug_debug_mode = false;
@@ -1458,8 +1459,8 @@ pub fn spRegNum(arch: std.Target.Cpu.Arch) u16 {
/// Tells whether unwinding for this target is supported by the Dwarf standard.
///
-/// See also `std.debug.SelfInfo.supports_unwinding` which tells whether the Zig
-/// standard library has a working implementation of unwinding for this target.
+/// See also `std.debug.SelfInfo.can_unwind` which tells whether the Zig standard
+/// library has a working implementation of unwinding for the current target.
pub fn supportsUnwinding(target: *const std.Target) bool {
return switch (target.cpu.arch) {
.amdgcn,
lib/std/debug/SelfInfo.zig
@@ -1,551 +0,0 @@
-//! Cross-platform abstraction for this binary's own debug information, with a
-//! goal of minimal code bloat and compilation speed penalty.
-
-const builtin = @import("builtin");
-const native_endian = native_arch.endian();
-const native_arch = builtin.cpu.arch;
-
-const std = @import("../std.zig");
-const mem = std.mem;
-const Allocator = std.mem.Allocator;
-const assert = std.debug.assert;
-const Dwarf = std.debug.Dwarf;
-const CpuContext = std.debug.cpu_context.Native;
-
-const stripInstructionPtrAuthCode = std.debug.stripInstructionPtrAuthCode;
-
-const root = @import("root");
-
-const SelfInfo = @This();
-
-/// Locks access to `modules`. However, does *not* lock the `Module.DebugInfo`, nor `lookup_cache`
-/// the implementation is responsible for locking as needed in its exposed methods.
-///
-/// TODO: to allow `SelfInfo` to work on freestanding, we currently just don't use this mutex there.
-/// That's a bad solution, but a better one depends on the standard library's general support for
-/// "bring your own OS" being improved.
-modules_mutex: switch (builtin.os.tag) {
- else => std.Thread.Mutex,
- .freestanding, .other => struct {
- fn lock(_: @This()) void {}
- fn unlock(_: @This()) void {}
- },
-},
-/// Value is allocated into gpa to give it a stable pointer.
-modules: if (target_supported) std.AutoArrayHashMapUnmanaged(usize, *Module.DebugInfo) else void,
-lookup_cache: if (target_supported) Module.LookupCache else void,
-
-pub const Error = error{
- /// The required debug info is invalid or corrupted.
- InvalidDebugInfo,
- /// The required debug info could not be found.
- MissingDebugInfo,
- /// The required debug info was found, and may be valid, but is not supported by this implementation.
- UnsupportedDebugInfo,
- /// The required debug info could not be read from disk due to some IO error.
- ReadFailed,
- OutOfMemory,
- Unexpected,
-};
-
-/// Indicates whether the `SelfInfo` implementation has support for this target.
-pub const target_supported: bool = Module != void;
-
-/// Indicates whether the `SelfInfo` implementation has support for unwinding on this target.
-pub const supports_unwinding: bool = target_supported and Module.supports_unwinding;
-
-pub const UnwindContext = if (supports_unwinding) Module.UnwindContext;
-
-pub const init: SelfInfo = .{
- .modules_mutex = .{},
- .modules = .empty,
- .lookup_cache = if (Module.LookupCache != void) .init,
-};
-
-pub fn deinit(self: *SelfInfo, gpa: Allocator) void {
- for (self.modules.values()) |di| {
- di.deinit(gpa);
- gpa.destroy(di);
- }
- self.modules.deinit(gpa);
- if (Module.LookupCache != void) self.lookup_cache.deinit(gpa);
-}
-
-pub fn unwindFrame(self: *SelfInfo, gpa: Allocator, context: *UnwindContext) Error!usize {
- comptime assert(supports_unwinding);
- const module: Module = try .lookup(&self.lookup_cache, gpa, context.pc);
- const di: *Module.DebugInfo = di: {
- self.modules_mutex.lock();
- defer self.modules_mutex.unlock();
- const gop = try self.modules.getOrPut(gpa, module.key());
- if (gop.found_existing) break :di gop.value_ptr.*;
- errdefer _ = self.modules.pop().?;
- const di = try gpa.create(Module.DebugInfo);
- di.* = .init;
- gop.value_ptr.* = di;
- break :di di;
- };
- return module.unwindFrame(gpa, di, context);
-}
-
-pub fn getSymbolAtAddress(self: *SelfInfo, gpa: Allocator, address: usize) Error!std.debug.Symbol {
- comptime assert(target_supported);
- const module: Module = try .lookup(&self.lookup_cache, gpa, address);
- const di: *Module.DebugInfo = di: {
- self.modules_mutex.lock();
- defer self.modules_mutex.unlock();
- const gop = try self.modules.getOrPut(gpa, module.key());
- if (gop.found_existing) break :di gop.value_ptr.*;
- errdefer _ = self.modules.pop().?;
- const di = try gpa.create(Module.DebugInfo);
- di.* = .init;
- gop.value_ptr.* = di;
- break :di di;
- };
- return module.getSymbolAtAddress(gpa, di, address);
-}
-
-pub fn getModuleNameForAddress(self: *SelfInfo, gpa: Allocator, address: usize) Error![]const u8 {
- comptime assert(target_supported);
- const module: Module = try .lookup(&self.lookup_cache, gpa, address);
- if (module.name.len == 0) return error.MissingDebugInfo;
- return module.name;
-}
-
-/// `void` indicates that `SelfInfo` is not supported for this target.
-///
-/// This type contains the target-specific implementation. Logically, a `Module` represents a subset
-/// of the executable with its own debug information. This typically corresponds to what ELF calls a
-/// module, i.e. a shared library or executable image, but could be anything. For instance, it would
-/// be valid to consider the entire application one module, or on the other hand to consider each
-/// object file a module.
-///
-/// Because different threads can collect stack traces concurrently, the implementation must be able
-/// to tolerate concurrent calls to any method it implements.
-///
-/// This type must must expose the following declarations:
-///
-/// ```
-/// /// Holds state cached by the implementation between calls to `lookup`.
-/// /// This may be `void`, in which case the inner declarations can be omitted.
-/// pub const LookupCache = struct {
-/// pub const init: LookupCache;
-/// pub fn deinit(lc: *LookupCache, gpa: Allocator) void;
-/// };
-/// /// Holds debug information associated with a particular `Module`.
-/// pub const DebugInfo = struct {
-/// pub const init: DebugInfo;
-/// };
-/// /// Finds the `Module` corresponding to `address`.
-/// pub fn lookup(lc: *LookupCache, gpa: Allocator, address: usize) SelfInfo.Error!Module;
-/// /// Returns a unique identifier for this `Module`, such as a load address.
-/// pub fn key(mod: *const Module) usize;
-/// /// Locates and loads location information for the symbol corresponding to `address`.
-/// pub fn getSymbolAtAddress(
-/// mod: *const Module,
-/// gpa: Allocator,
-/// di: *DebugInfo,
-/// address: usize,
-/// ) SelfInfo.Error!std.debug.Symbol;
-/// /// Whether a reliable stack unwinding strategy, such as DWARF unwinding, is available.
-/// pub const supports_unwinding: bool;
-/// /// Only required if `supports_unwinding == true`.
-/// pub const UnwindContext = struct {
-/// /// A PC value representing the location in the last frame.
-/// pc: usize,
-/// pub fn init(ctx: *std.debug.cpu_context.Native, gpa: Allocator) Allocator.Error!UnwindContext;
-/// pub fn deinit(uc: *UnwindContext, gpa: Allocator) void;
-/// /// Returns the frame pointer associated with the last unwound stack frame. If the frame
-/// /// pointer is unknown, 0 may be returned instead.
-/// pub fn getFp(uc: *UnwindContext) usize;
-/// };
-/// /// Only required if `supports_unwinding == true`. Unwinds a single stack frame, and returns
-/// /// the frame's return address.
-/// pub fn unwindFrame(
-/// mod: *const Module,
-/// gpa: Allocator,
-/// di: *DebugInfo,
-/// ctx: *UnwindContext,
-/// ) SelfInfo.Error!usize;
-/// ```
-const Module: type = Module: {
- // Allow overriding the target-specific `SelfInfo` implementation by exposing `root.debug.Module`.
- if (@hasDecl(root, "debug") and @hasDecl(root.debug, "Module")) {
- break :Module root.debug.Module;
- }
- break :Module switch (builtin.os.tag) {
- .linux,
- .netbsd,
- .freebsd,
- .dragonfly,
- .openbsd,
- .solaris,
- .illumos,
- => @import("SelfInfo/ElfModule.zig"),
-
- .macos,
- .ios,
- .watchos,
- .tvos,
- .visionos,
- => @import("SelfInfo/DarwinModule.zig"),
-
- .uefi,
- .windows,
- => @import("SelfInfo/WindowsModule.zig"),
-
- else => void,
- };
-};
-
-/// An implementation of `UnwindContext` useful for DWARF-based unwinders. The `Module.unwindFrame`
-/// implementation should wrap `DwarfUnwindContext.unwindFrame`.
-pub const DwarfUnwindContext = struct {
- cfa: ?usize,
- pc: usize,
- cpu_context: CpuContext,
- vm: Dwarf.Unwind.VirtualMachine,
- stack_machine: Dwarf.expression.StackMachine(.{ .call_frame_context = true }),
-
- pub const Cache = struct {
- /// TODO: to allow `DwarfUnwindContext` to work on freestanding, we currently just don't use
- /// this mutex there. That's a bad solution, but a better one depends on the standard
- /// library's general support for "bring your own OS" being improved.
- mutex: switch (builtin.os.tag) {
- else => std.Thread.Mutex,
- .freestanding, .other => struct {
- fn lock(_: @This()) void {}
- fn unlock(_: @This()) void {}
- },
- },
- buf: [num_slots]Slot,
- const num_slots = 2048;
- const Slot = struct {
- const max_regs = 32;
- pc: usize,
- cie: *const Dwarf.Unwind.CommonInformationEntry,
- cfa_rule: Dwarf.Unwind.VirtualMachine.CfaRule,
- rules_regs: [max_regs]u16,
- rules: [max_regs]Dwarf.Unwind.VirtualMachine.RegisterRule,
- num_rules: u8,
- };
- /// This is a function rather than a declaration to avoid lowering a very large struct value
- /// into the binary when most of it is `undefined`.
- pub fn init(c: *Cache) void {
- c.mutex = .{};
- for (&c.buf) |*slot| slot.pc = 0;
- }
- };
-
- pub fn init(cpu_context: *const CpuContext) DwarfUnwindContext {
- comptime assert(supports_unwinding);
-
- // `@constCast` is safe because we aren't going to store to the resulting pointer.
- const raw_pc_ptr = regNative(@constCast(cpu_context), ip_reg_num) catch |err| switch (err) {
- error.InvalidRegister => unreachable, // `ip_reg_num` is definitely valid
- error.UnsupportedRegister => unreachable, // the implementation needs to support ip
- error.IncompatibleRegisterSize => unreachable, // ip is definitely `usize`-sized
- };
- const pc = stripInstructionPtrAuthCode(raw_pc_ptr.*);
-
- return .{
- .cfa = null,
- .pc = pc,
- .cpu_context = cpu_context.*,
- .vm = .{},
- .stack_machine = .{},
- };
- }
-
- pub fn deinit(self: *DwarfUnwindContext, gpa: Allocator) void {
- self.vm.deinit(gpa);
- self.stack_machine.deinit(gpa);
- self.* = undefined;
- }
-
- pub fn getFp(self: *const DwarfUnwindContext) usize {
- // `@constCast` is safe because we aren't going to store to the resulting pointer.
- const ptr = regNative(@constCast(&self.cpu_context), fp_reg_num) catch |err| switch (err) {
- error.InvalidRegister => unreachable, // `fp_reg_num` is definitely valid
- error.UnsupportedRegister => unreachable, // the implementation needs to support fp
- error.IncompatibleRegisterSize => unreachable, // fp is a pointer so is `usize`-sized
- };
- return ptr.*;
- }
-
- /// Unwind a stack frame using DWARF unwinding info, updating the register context.
- ///
- /// If `.eh_frame_hdr` is available and complete, it will be used to binary search for the FDE.
- /// Otherwise, a linear scan of `.eh_frame` and `.debug_frame` is done to find the FDE. The latter
- /// may require lazily loading the data in those sections.
- ///
- /// `explicit_fde_offset` is for cases where the FDE offset is known, such as when using macOS'
- /// `__unwind_info` section.
- pub fn unwindFrame(
- context: *DwarfUnwindContext,
- cache: *Cache,
- gpa: Allocator,
- unwind: *const Dwarf.Unwind,
- load_offset: usize,
- explicit_fde_offset: ?usize,
- ) Error!usize {
- return unwindFrameInner(context, cache, gpa, unwind, load_offset, explicit_fde_offset) catch |err| switch (err) {
- error.InvalidDebugInfo,
- error.MissingDebugInfo,
- error.UnsupportedDebugInfo,
- error.OutOfMemory,
- => |e| return e,
-
- error.UnsupportedAddrSize,
- error.UnimplementedUserOpcode,
- error.UnimplementedExpressionCall,
- error.UnimplementedOpcode,
- error.UnimplementedTypedComparison,
- error.UnimplementedTypeConversion,
- error.UnknownExpressionOpcode,
- error.UnsupportedRegister,
- => return error.UnsupportedDebugInfo,
-
- error.InvalidRegister,
- error.ReadFailed,
- error.EndOfStream,
- error.IncompatibleRegisterSize,
- error.Overflow,
- error.StreamTooLong,
- error.InvalidOperand,
- error.InvalidOpcode,
- error.InvalidOperation,
- error.InvalidCFARule,
- error.IncompleteExpressionContext,
- error.InvalidCFAOpcode,
- error.InvalidExpression,
- error.InvalidFrameBase,
- error.InvalidIntegralTypeSize,
- error.InvalidSubExpression,
- error.InvalidTypeLength,
- error.TruncatedIntegralType,
- error.DivisionByZero,
- error.InvalidExpressionValue,
- error.NoExpressionValue,
- error.RegisterSizeMismatch,
- => return error.InvalidDebugInfo,
- };
- }
- fn unwindFrameInner(
- context: *DwarfUnwindContext,
- cache: *Cache,
- gpa: Allocator,
- unwind: *const Dwarf.Unwind,
- load_offset: usize,
- explicit_fde_offset: ?usize,
- ) !usize {
- comptime assert(supports_unwinding);
-
- if (context.pc == 0) return 0;
-
- const pc_vaddr = context.pc - load_offset;
-
- const cache_slot: Cache.Slot = slot: {
- const slot_idx = std.hash.int(pc_vaddr) % Cache.num_slots;
-
- {
- cache.mutex.lock();
- defer cache.mutex.unlock();
- if (cache.buf[slot_idx].pc == pc_vaddr) break :slot cache.buf[slot_idx];
- }
-
- const fde_offset = explicit_fde_offset orelse try unwind.lookupPc(
- pc_vaddr,
- @sizeOf(usize),
- native_endian,
- ) orelse return error.MissingDebugInfo;
- const cie, const fde = try unwind.getFde(fde_offset, native_endian);
-
- // Check if the FDE *actually* includes the pc (`lookupPc` can return false positives).
- if (pc_vaddr < fde.pc_begin or pc_vaddr >= fde.pc_begin + fde.pc_range) {
- return error.MissingDebugInfo;
- }
-
- context.vm.reset();
-
- const row = try context.vm.runTo(gpa, pc_vaddr, cie, &fde, @sizeOf(usize), native_endian);
-
- if (row.columns.len > Cache.Slot.max_regs) return error.UnsupportedDebugInfo;
-
- var slot: Cache.Slot = .{
- .pc = pc_vaddr,
- .cie = cie,
- .cfa_rule = row.cfa,
- .rules_regs = undefined,
- .rules = undefined,
- .num_rules = 0,
- };
- for (context.vm.rowColumns(&row)) |col| {
- const i = slot.num_rules;
- slot.rules_regs[i] = col.register;
- slot.rules[i] = col.rule;
- slot.num_rules += 1;
- }
-
- {
- cache.mutex.lock();
- defer cache.mutex.unlock();
- cache.buf[slot_idx] = slot;
- }
-
- break :slot slot;
- };
-
- const format = cache_slot.cie.format;
- const return_address_register = cache_slot.cie.return_address_register;
-
- context.cfa = switch (cache_slot.cfa_rule) {
- .none => return error.InvalidCFARule,
- .reg_off => |ro| cfa: {
- const ptr = try regNative(&context.cpu_context, ro.register);
- break :cfa try applyOffset(ptr.*, ro.offset);
- },
- .expression => |expr| cfa: {
- context.stack_machine.reset();
- const value = try context.stack_machine.run(expr, gpa, .{
- .format = format,
- .cpu_context = &context.cpu_context,
- }, context.cfa) orelse return error.NoExpressionValue;
- switch (value) {
- .generic => |g| break :cfa g,
- else => return error.InvalidExpressionValue,
- }
- },
- };
-
- // If unspecified, we'll use the default rule for the return address register, which is
- // typically equivalent to `.undefined` (meaning there is no return address), but may be
- // overriden by ABIs.
- var has_return_address: bool = builtin.cpu.arch.isAARCH64() and
- return_address_register >= 19 and
- return_address_register <= 28;
-
- // Create a copy of the CPU context, to which we will apply the new rules.
- var new_cpu_context = context.cpu_context;
-
- // On all implemented architectures, the CFA is defined as being the previous frame's SP
- (try regNative(&new_cpu_context, sp_reg_num)).* = context.cfa.?;
-
- const rules_len = cache_slot.num_rules;
- for (cache_slot.rules_regs[0..rules_len], cache_slot.rules[0..rules_len]) |register, rule| {
- const new_val: union(enum) {
- same,
- undefined,
- val: usize,
- bytes: []const u8,
- } = switch (rule) {
- .default => val: {
- // The default rule is typically equivalent to `.undefined`, but ABIs may override it.
- if (builtin.cpu.arch.isAARCH64() and register >= 19 and register <= 28) {
- break :val .same;
- }
- break :val .undefined;
- },
- .undefined => .undefined,
- .same_value => .same,
- .offset => |offset| val: {
- const ptr: *const usize = @ptrFromInt(try applyOffset(context.cfa.?, offset));
- break :val .{ .val = ptr.* };
- },
- .val_offset => |offset| .{ .val = try applyOffset(context.cfa.?, offset) },
- .register => |r| .{ .bytes = try context.cpu_context.dwarfRegisterBytes(r) },
- .expression => |expr| val: {
- context.stack_machine.reset();
- const value = try context.stack_machine.run(expr, gpa, .{
- .format = format,
- .cpu_context = &context.cpu_context,
- }, context.cfa.?) orelse return error.NoExpressionValue;
- const ptr: *const usize = switch (value) {
- .generic => |addr| @ptrFromInt(addr),
- else => return error.InvalidExpressionValue,
- };
- break :val .{ .val = ptr.* };
- },
- .val_expression => |expr| val: {
- context.stack_machine.reset();
- const value = try context.stack_machine.run(expr, gpa, .{
- .format = format,
- .cpu_context = &context.cpu_context,
- }, context.cfa.?) orelse return error.NoExpressionValue;
- switch (value) {
- .generic => |val| break :val .{ .val = val },
- else => return error.InvalidExpressionValue,
- }
- },
- };
- switch (new_val) {
- .same => {},
- .undefined => {
- const dest = try new_cpu_context.dwarfRegisterBytes(@intCast(register));
- @memset(dest, undefined);
- },
- .val => |val| {
- const dest = try new_cpu_context.dwarfRegisterBytes(@intCast(register));
- if (dest.len != @sizeOf(usize)) return error.RegisterSizeMismatch;
- const dest_ptr: *align(1) usize = @ptrCast(dest);
- dest_ptr.* = val;
- },
- .bytes => |src| {
- const dest = try new_cpu_context.dwarfRegisterBytes(@intCast(register));
- if (dest.len != src.len) return error.RegisterSizeMismatch;
- @memcpy(dest, src);
- },
- }
- if (register == return_address_register) {
- has_return_address = new_val != .undefined;
- }
- }
-
- const return_address: usize = if (has_return_address) pc: {
- const raw_ptr = try regNative(&new_cpu_context, return_address_register);
- break :pc stripInstructionPtrAuthCode(raw_ptr.*);
- } else 0;
-
- (try regNative(&new_cpu_context, ip_reg_num)).* = return_address;
-
- // The new CPU context is complete; flush changes.
- context.cpu_context = new_cpu_context;
-
- // The caller will subtract 1 from the return address to get an address corresponding to the
- // function call. However, if this is a signal frame, that's actually incorrect, because the
- // "return address" we have is the instruction which triggered the signal (if the signal
- // handler returned, the instruction would be re-run). Compensate for this by incrementing
- // the address in that case.
- const adjusted_ret_addr = if (cache_slot.cie.is_signal_frame) return_address +| 1 else return_address;
-
- // We also want to do that same subtraction here to get the PC for the next frame's FDE.
- // This is because if the callee was noreturn, then the function call might be the caller's
- // last instruction, so `return_address` might actually point outside of it!
- context.pc = adjusted_ret_addr -| 1;
-
- return adjusted_ret_addr;
- }
- /// Since register rules are applied (usually) during a panic,
- /// checked addition / subtraction is used so that we can return
- /// an error and fall back to FP-based unwinding.
- fn applyOffset(base: usize, offset: i64) !usize {
- return if (offset >= 0)
- try std.math.add(usize, base, @as(usize, @intCast(offset)))
- else
- try std.math.sub(usize, base, @as(usize, @intCast(-offset)));
- }
-
- pub fn regNative(ctx: *CpuContext, num: u16) error{
- InvalidRegister,
- UnsupportedRegister,
- IncompatibleRegisterSize,
- }!*align(1) usize {
- const bytes = try ctx.dwarfRegisterBytes(num);
- if (bytes.len != @sizeOf(usize)) return error.IncompatibleRegisterSize;
- return @ptrCast(bytes);
- }
-
- const ip_reg_num = Dwarf.ipRegNum(native_arch).?;
- const fp_reg_num = Dwarf.fpRegNum(native_arch);
- const sp_reg_num = Dwarf.spRegNum(native_arch);
-};
lib/std/debug.zig
@@ -19,11 +19,85 @@ const root = @import("root");
pub const Dwarf = @import("debug/Dwarf.zig");
pub const Pdb = @import("debug/Pdb.zig");
pub const ElfFile = @import("debug/ElfFile.zig");
-pub const SelfInfo = @import("debug/SelfInfo.zig");
pub const Info = @import("debug/Info.zig");
pub const Coverage = @import("debug/Coverage.zig");
pub const cpu_context = @import("debug/cpu_context.zig");
+/// This type abstracts the target-specific implementation of accessing this process' own debug
+/// information behind a generic interface which supports looking up source locations associated
+/// with addresses, as well as unwinding the stack where a safe mechanism to do so exists.
+///
+/// The Zig Standard Library provides default implementations of `SelfInfo` for common targets, but
+/// the implementation can be overriden by exposing `root.debug.SelfInfo`. Setting `SelfInfo` to
+/// `void` indicates that the `SelfInfo` API is not supported.
+///
+/// This type must expose the following declarations:
+///
+/// ```
+/// pub const init: SelfInfo;
+/// pub fn deinit(si: *SelfInfo, gpa: Allocator) void;
+///
+/// /// Returns the symbol and source location of the instruction at `address`.
+/// pub fn getSymbol(si: *SelfInfo, gpa: Allocator, address: usize) SelfInfoError!Symbol;
+/// /// Returns a name for the "module" (e.g. shared library or executable image) containing `address`.
+/// pub fn getModuleName(si: *SelfInfo, gpa: Allocator, address: usize) SelfInfoError![]const u8;
+///
+/// /// Whether a reliable stack unwinding strategy, such as DWARF unwinding, is available.
+/// pub const can_unwind: bool;
+/// /// Only required if `can_unwind == true`.
+/// pub const UnwindContext = struct {
+/// /// An address representing the instruction pointer in the last frame.
+/// pc: usize,
+///
+/// pub fn init(ctx: *cpu_context.Native, gpa: Allocator) Allocator.Error!UnwindContext;
+/// pub fn deinit(ctx: *UnwindContext, gpa: Allocator) void;
+/// /// Returns the frame pointer associated with the last unwound stack frame.
+/// /// If the frame pointer is unknown, 0 may be returned instead.
+/// pub fn getFp(uc: *UnwindContext) usize;
+/// };
+/// /// Only required if `can_unwind == true`. Unwinds a single stack frame, returning the frame's
+/// /// return address, or 0 if the end of the stack has been reached.
+/// pub fn unwindFrame(si: *SelfInfo, gpa: Allocator, context: *UnwindContext) SelfInfoError!usize;
+/// ```
+pub const SelfInfo = if (@hasDecl(root, "debug") and @hasDecl(root.debug, "SelfInfo"))
+ root.debug.SelfInfo
+else switch (native_os) {
+ .linux,
+ .netbsd,
+ .freebsd,
+ .dragonfly,
+ .openbsd,
+ .solaris,
+ .illumos,
+ => @import("debug/SelfInfo/Elf.zig"),
+
+ .macos,
+ .ios,
+ .watchos,
+ .tvos,
+ .visionos,
+ => @import("debug/SelfInfo/Darwin.zig"),
+
+ .uefi,
+ .windows,
+ => @import("debug/SelfInfo/Windows.zig"),
+
+ else => void,
+};
+
+pub const SelfInfoError = error{
+ /// The required debug info is invalid or corrupted.
+ InvalidDebugInfo,
+ /// The required debug info could not be found.
+ MissingDebugInfo,
+ /// The required debug info was found, and may be valid, but is not supported by this implementation.
+ UnsupportedDebugInfo,
+ /// The required debug info could not be read from disk due to some IO error.
+ ReadFailed,
+ OutOfMemory,
+ Unexpected,
+};
+
pub const simple_panic = @import("debug/simple_panic.zig");
pub const no_panic = @import("debug/no_panic.zig");
@@ -240,7 +314,7 @@ pub fn print(comptime fmt: []const u8, args: anytype) void {
/// Marked `inline` to propagate a comptime-known error to callers.
pub inline fn getSelfDebugInfo() !*SelfInfo {
- if (!SelfInfo.target_supported) return error.UnsupportedTarget;
+ if (SelfInfo == void) return error.UnsupportedTarget;
const S = struct {
var self_info: SelfInfo = .init;
};
@@ -640,7 +714,7 @@ pub fn writeCurrentStackTrace(options: StackUnwindOptions, writer: *Writer, tty_
while (true) switch (it.next()) {
.switch_to_fp => |unwind_error| {
if (StackIterator.fp_unwind_is_safe) continue; // no need to even warn
- const module_name = di.getModuleNameForAddress(di_gpa, unwind_error.address) catch "???";
+ const module_name = di.getModuleName(di_gpa, unwind_error.address) catch "???";
const caption: []const u8 = switch (unwind_error.err) {
error.MissingDebugInfo => "unwind info unavailable",
error.InvalidDebugInfo => "unwind info invalid",
@@ -753,9 +827,9 @@ pub fn dumpStackTrace(st: *const std.builtin.StackTrace) void {
const StackIterator = union(enum) {
/// Unwinding using debug info (e.g. DWARF CFI).
- di: if (SelfInfo.supports_unwinding) SelfInfo.UnwindContext else noreturn,
+ di: if (SelfInfo != void and SelfInfo.can_unwind) SelfInfo.UnwindContext else noreturn,
/// We will first report the *current* PC of this `UnwindContext`, then we will switch to `di`.
- di_first: if (SelfInfo.supports_unwinding) SelfInfo.UnwindContext else noreturn,
+ di_first: if (SelfInfo != void and SelfInfo.can_unwind) SelfInfo.UnwindContext else noreturn,
/// Naive frame-pointer-based unwinding. Very simple, but typically unreliable.
fp: usize,
@@ -772,7 +846,7 @@ const StackIterator = union(enum) {
}
}
if (opt_context_ptr) |context_ptr| {
- if (!SelfInfo.supports_unwinding) return error.CannotUnwindFromContext;
+ if (SelfInfo == void or !SelfInfo.can_unwind) return error.CannotUnwindFromContext;
// Use `di_first` here so we report the PC in the context before unwinding any further.
return .{ .di_first = .init(context_ptr) };
}
@@ -780,7 +854,8 @@ const StackIterator = union(enum) {
// call to `current`. This effectively constrains stack trace collection and dumping to FP
// unwinding when building with CBE for MSVC.
if (!(builtin.zig_backend == .stage2_c and builtin.target.abi == .msvc) and
- SelfInfo.supports_unwinding and
+ SelfInfo != void and
+ SelfInfo.can_unwind and
cpu_context.Native != noreturn)
{
// We don't need `di_first` here, because our PC is in `std.debug`; we're only interested
@@ -820,7 +895,7 @@ const StackIterator = union(enum) {
/// We were using `SelfInfo.UnwindInfo`, but are now switching to FP unwinding due to this error.
switch_to_fp: struct {
address: usize,
- err: SelfInfo.Error,
+ err: SelfInfoError,
},
};
@@ -929,7 +1004,7 @@ pub inline fn stripInstructionPtrAuthCode(ptr: usize) usize {
}
fn printSourceAtAddress(gpa: Allocator, debug_info: *SelfInfo, writer: *Writer, address: usize, tty_config: tty.Config) Writer.Error!void {
- const symbol: Symbol = debug_info.getSymbolAtAddress(gpa, address) catch |err| switch (err) {
+ const symbol: Symbol = debug_info.getSymbol(gpa, address) catch |err| switch (err) {
error.MissingDebugInfo,
error.UnsupportedDebugInfo,
error.InvalidDebugInfo,
@@ -953,7 +1028,7 @@ fn printSourceAtAddress(gpa: Allocator, debug_info: *SelfInfo, writer: *Writer,
symbol.source_location,
address,
symbol.name orelse "???",
- symbol.compile_unit_name orelse debug_info.getModuleNameForAddress(gpa, address) catch "???",
+ symbol.compile_unit_name orelse debug_info.getModuleName(gpa, address) catch "???",
tty_config,
);
}
@@ -1386,7 +1461,7 @@ pub fn dumpStackPointerAddr(prefix: []const u8) void {
}
test "manage resources correctly" {
- if (!SelfInfo.target_supported) return error.SkipZigTest;
+ if (SelfInfo == void) return error.SkipZigTest;
const S = struct {
noinline fn showMyTrace() usize {
return @returnAddress();
test/standalone/coff_dwarf/main.zig
@@ -14,7 +14,7 @@ pub fn main() void {
var add_addr: usize = undefined;
_ = add(1, 2, &add_addr);
- const symbol = di.getSymbolAtAddress(gpa, add_addr) catch |err| fatal("failed to get symbol: {t}", .{err});
+ const symbol = di.getSymbol(gpa, add_addr) catch |err| fatal("failed to get symbol: {t}", .{err});
defer if (symbol.source_location) |sl| gpa.free(sl.file_name);
if (symbol.name == null) fatal("failed to resolve symbol name", .{});