std.debug.SelfInfo: remove shared logic

There were only a few dozen lines of common logic, and they frankly
introduced more complexity than they eliminated. Instead, let's accept
that the implementations of `SelfInfo` are all pretty different and want
to track different state. This probably fixes some synchronization and
memory bugs by simplifying a bunch of stuff. It also improves the DWARF
unwind cache, making it around twice as fast in a debug build with the
self-hosted x86_64 backend, because we no longer have to redundantly go
through the hashmap lookup logic to find the module. Unwinding on
Windows will also see a slight performance boost from this change,
because `RtlVirtualUnwind` does not need to know the module whatsoever,
so the old `SelfInfo` implementation was doing redundant work. Lastly,
this makes it even easier to implement `SelfInfo` on freestanding
targets; there is no longer a need to emulate a real module system,
since the user controls the whole implementation!

There are various other small refactors here in the `SelfInfo`
implementations as well as in the DWARF unwinding logic. This change
turned out to make a lot of stuff simpler!
This commit is contained in:
mlugg 2025-09-30 11:06:21 +01:00
parent 12ceb896fa
commit 1120546f72
No known key found for this signature in database
GPG key ID: 3F5B7DCCBF4AF02E
13 changed files with 2415 additions and 2320 deletions

View file

@ -19,11 +19,85 @@ const root = @import("root");
pub const Dwarf = @import("debug/Dwarf.zig"); pub const Dwarf = @import("debug/Dwarf.zig");
pub const Pdb = @import("debug/Pdb.zig"); pub const Pdb = @import("debug/Pdb.zig");
pub const ElfFile = @import("debug/ElfFile.zig"); pub const ElfFile = @import("debug/ElfFile.zig");
pub const SelfInfo = @import("debug/SelfInfo.zig");
pub const Info = @import("debug/Info.zig"); pub const Info = @import("debug/Info.zig");
pub const Coverage = @import("debug/Coverage.zig"); pub const Coverage = @import("debug/Coverage.zig");
pub const cpu_context = @import("debug/cpu_context.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 simple_panic = @import("debug/simple_panic.zig");
pub const no_panic = @import("debug/no_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. /// Marked `inline` to propagate a comptime-known error to callers.
pub inline fn getSelfDebugInfo() !*SelfInfo { pub inline fn getSelfDebugInfo() !*SelfInfo {
if (!SelfInfo.target_supported) return error.UnsupportedTarget; if (SelfInfo == void) return error.UnsupportedTarget;
const S = struct { const S = struct {
var self_info: SelfInfo = .init; var self_info: SelfInfo = .init;
}; };
@ -640,7 +714,7 @@ pub fn writeCurrentStackTrace(options: StackUnwindOptions, writer: *Writer, tty_
while (true) switch (it.next()) { while (true) switch (it.next()) {
.switch_to_fp => |unwind_error| { .switch_to_fp => |unwind_error| {
if (StackIterator.fp_unwind_is_safe) continue; // no need to even warn 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) { const caption: []const u8 = switch (unwind_error.err) {
error.MissingDebugInfo => "unwind info unavailable", error.MissingDebugInfo => "unwind info unavailable",
error.InvalidDebugInfo => "unwind info invalid", error.InvalidDebugInfo => "unwind info invalid",
@ -753,9 +827,9 @@ pub fn dumpStackTrace(st: *const std.builtin.StackTrace) void {
const StackIterator = union(enum) { const StackIterator = union(enum) {
/// Unwinding using debug info (e.g. DWARF CFI). /// 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`. /// 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. /// Naive frame-pointer-based unwinding. Very simple, but typically unreliable.
fp: usize, fp: usize,
@ -772,7 +846,7 @@ const StackIterator = union(enum) {
} }
} }
if (opt_context_ptr) |context_ptr| { 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. // Use `di_first` here so we report the PC in the context before unwinding any further.
return .{ .di_first = .init(context_ptr) }; 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 // call to `current`. This effectively constrains stack trace collection and dumping to FP
// unwinding when building with CBE for MSVC. // unwinding when building with CBE for MSVC.
if (!(builtin.zig_backend == .stage2_c and builtin.target.abi == .msvc) and 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) cpu_context.Native != noreturn)
{ {
// We don't need `di_first` here, because our PC is in `std.debug`; we're only interested // 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. /// We were using `SelfInfo.UnwindInfo`, but are now switching to FP unwinding due to this error.
switch_to_fp: struct { switch_to_fp: struct {
address: usize, 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 { 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.MissingDebugInfo,
error.UnsupportedDebugInfo, error.UnsupportedDebugInfo,
error.InvalidDebugInfo, error.InvalidDebugInfo,
@ -953,7 +1028,7 @@ fn printSourceAtAddress(gpa: Allocator, debug_info: *SelfInfo, writer: *Writer,
symbol.source_location, symbol.source_location,
address, address,
symbol.name orelse "???", 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, tty_config,
); );
} }
@ -1386,7 +1461,7 @@ pub fn dumpStackPointerAddr(prefix: []const u8) void {
} }
test "manage resources correctly" { test "manage resources correctly" {
if (!SelfInfo.target_supported) return error.SkipZigTest; if (SelfInfo == void) return error.SkipZigTest;
const S = struct { const S = struct {
noinline fn showMyTrace() usize { noinline fn showMyTrace() usize {
return @returnAddress(); return @returnAddress();

View file

@ -28,6 +28,7 @@ const Dwarf = @This();
pub const expression = @import("Dwarf/expression.zig"); pub const expression = @import("Dwarf/expression.zig");
pub const Unwind = @import("Dwarf/Unwind.zig"); pub const Unwind = @import("Dwarf/Unwind.zig");
pub const SelfUnwinder = @import("Dwarf/SelfUnwinder.zig");
/// Useful to temporarily enable while working on this file. /// Useful to temporarily enable while working on this file.
const debug_debug_mode = false; 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. /// Tells whether unwinding for this target is supported by the Dwarf standard.
/// ///
/// See also `std.debug.SelfInfo.supports_unwinding` which tells whether the Zig /// See also `std.debug.SelfInfo.can_unwind` which tells whether the Zig standard
/// standard library has a working implementation of unwinding for this target. /// library has a working implementation of unwinding for the current target.
pub fn supportsUnwinding(target: *const std.Target) bool { pub fn supportsUnwinding(target: *const std.Target) bool {
return switch (target.cpu.arch) { return switch (target.cpu.arch) {
.amdgcn, .amdgcn,

View file

@ -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();

View file

@ -530,16 +530,18 @@ pub fn prepare(
}; };
if (saw_terminator != expect_terminator) return bad(); if (saw_terminator != expect_terminator) return bad();
std.mem.sortUnstable(SortedFdeEntry, fde_list.items, {}, struct { if (need_lookup) {
fn lessThan(ctx: void, a: SortedFdeEntry, b: SortedFdeEntry) bool { std.mem.sortUnstable(SortedFdeEntry, fde_list.items, {}, struct {
ctx; fn lessThan(ctx: void, a: SortedFdeEntry, b: SortedFdeEntry) bool {
return a.pc_begin < b.pc_begin; ctx;
} return a.pc_begin < b.pc_begin;
}.lessThan); }
}.lessThan);
// This temporary is necessary to avoid an RLS footgun where `lookup` ends up non-null `undefined` on OOM. // 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); const final_fdes = try fde_list.toOwnedSlice(gpa);
unwind.lookup = .{ .sorted_fdes = final_fdes }; unwind.lookup = .{ .sorted_fdes = final_fdes };
}
} }
fn findCie(unwind: *const Unwind, offset: u64) ?*const CommonInformationEntry { fn findCie(unwind: *const Unwind, offset: u64) ?*const CommonInformationEntry {

View file

@ -10,7 +10,7 @@ const assert = std.debug.assert;
const testing = std.testing; const testing = std.testing;
const Writer = std.Io.Writer; 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 ip_reg_num = std.debug.Dwarf.ipRegNum(native_arch).?;
const fp_reg_num = std.debug.Dwarf.fpRegNum(native_arch); const fp_reg_num = std.debug.Dwarf.fpRegNum(native_arch);

View file

@ -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);
};

View file

@ -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();

View file

@ -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();

View file

@ -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();

View file

@ -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();

View file

@ -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(
&section_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(&section_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();

View file

@ -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(
&section_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(&section_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();

View file

@ -14,7 +14,7 @@ pub fn main() void {
var add_addr: usize = undefined; var add_addr: usize = undefined;
_ = add(1, 2, &add_addr); _ = 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); defer if (symbol.source_location) |sl| gpa.free(sl.file_name);
if (symbol.name == null) fatal("failed to resolve symbol name", .{}); if (symbol.name == null) fatal("failed to resolve symbol name", .{});