mirror of
https://codeberg.org/ziglang/zig.git
synced 2025-12-06 13:54:21 +00:00
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:
parent
12ceb896fa
commit
1120546f72
13 changed files with 2415 additions and 2320 deletions
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
334
lib/std/debug/Dwarf/SelfUnwinder.zig
Normal file
334
lib/std/debug/Dwarf/SelfUnwinder.zig
Normal 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();
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
};
|
|
||||||
993
lib/std/debug/SelfInfo/Darwin.zig
Normal file
993
lib/std/debug/SelfInfo/Darwin.zig
Normal 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();
|
||||||
|
|
@ -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();
|
|
||||||
427
lib/std/debug/SelfInfo/Elf.zig
Normal file
427
lib/std/debug/SelfInfo/Elf.zig
Normal 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();
|
||||||
|
|
@ -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();
|
|
||||||
559
lib/std/debug/SelfInfo/Windows.zig
Normal file
559
lib/std/debug/SelfInfo/Windows.zig
Normal 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(
|
||||||
|
§ion_handle,
|
||||||
|
windows.STANDARD_RIGHTS_REQUIRED | windows.SECTION_QUERY | windows.SECTION_MAP_READ,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
windows.PAGE_READONLY,
|
||||||
|
// The documentation states that if no AllocationAttribute is specified, then SEC_COMMIT is the default.
|
||||||
|
// In practice, this isn't the case and specifying 0 will result in INVALID_PARAMETER_6.
|
||||||
|
windows.SEC_COMMIT,
|
||||||
|
coff_file.handle,
|
||||||
|
);
|
||||||
|
if (create_section_rc != .SUCCESS) return error.MissingDebugInfo;
|
||||||
|
errdefer windows.CloseHandle(section_handle);
|
||||||
|
var coff_len: usize = 0;
|
||||||
|
var section_view_ptr: ?[*]const u8 = null;
|
||||||
|
const map_section_rc = windows.ntdll.NtMapViewOfSection(
|
||||||
|
section_handle,
|
||||||
|
process_handle,
|
||||||
|
@ptrCast(§ion_view_ptr),
|
||||||
|
null,
|
||||||
|
0,
|
||||||
|
null,
|
||||||
|
&coff_len,
|
||||||
|
.ViewUnmap,
|
||||||
|
0,
|
||||||
|
windows.PAGE_READONLY,
|
||||||
|
);
|
||||||
|
if (map_section_rc != .SUCCESS) return error.MissingDebugInfo;
|
||||||
|
errdefer assert(windows.ntdll.NtUnmapViewOfSection(process_handle, @constCast(section_view_ptr.?)) == .SUCCESS);
|
||||||
|
const section_view = section_view_ptr.?[0..coff_len];
|
||||||
|
coff_obj = coff.Coff.init(section_view, false) catch return error.InvalidDebugInfo;
|
||||||
|
break :mapped .{
|
||||||
|
.file = coff_file,
|
||||||
|
.section_handle = section_handle,
|
||||||
|
.section_view = section_view,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
errdefer if (mapped_file) |*mf| mf.deinit();
|
||||||
|
|
||||||
|
const coff_image_base = coff_obj.getImageBase();
|
||||||
|
|
||||||
|
var opt_dwarf: ?Dwarf = dwarf: {
|
||||||
|
if (coff_obj.getSectionByName(".debug_info") == null) break :dwarf null;
|
||||||
|
|
||||||
|
var sections: Dwarf.SectionArray = undefined;
|
||||||
|
inline for (@typeInfo(Dwarf.Section.Id).@"enum".fields, 0..) |section, i| {
|
||||||
|
sections[i] = if (coff_obj.getSectionByName("." ++ section.name)) |section_header| .{
|
||||||
|
.data = try coff_obj.getSectionDataAlloc(section_header, arena),
|
||||||
|
.owned = false,
|
||||||
|
} else null;
|
||||||
|
}
|
||||||
|
break :dwarf .{ .sections = sections };
|
||||||
|
};
|
||||||
|
errdefer if (opt_dwarf) |*dwarf| dwarf.deinit(gpa);
|
||||||
|
|
||||||
|
if (opt_dwarf) |*dwarf| {
|
||||||
|
dwarf.open(gpa, native_endian) catch |err| switch (err) {
|
||||||
|
error.Overflow,
|
||||||
|
error.EndOfStream,
|
||||||
|
error.StreamTooLong,
|
||||||
|
error.ReadFailed,
|
||||||
|
=> return error.InvalidDebugInfo,
|
||||||
|
|
||||||
|
error.InvalidDebugInfo,
|
||||||
|
error.MissingDebugInfo,
|
||||||
|
error.OutOfMemory,
|
||||||
|
=> |e| return e,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var opt_pdb: ?Pdb = pdb: {
|
||||||
|
const path = coff_obj.getPdbPath() catch {
|
||||||
|
return error.InvalidDebugInfo;
|
||||||
|
} orelse {
|
||||||
|
break :pdb null;
|
||||||
|
};
|
||||||
|
const pdb_file_open_result = if (fs.path.isAbsolute(path)) res: {
|
||||||
|
break :res std.fs.cwd().openFile(path, .{});
|
||||||
|
} else res: {
|
||||||
|
const self_dir = fs.selfExeDirPathAlloc(gpa) catch |err| switch (err) {
|
||||||
|
error.OutOfMemory, error.Unexpected => |e| return e,
|
||||||
|
else => return error.ReadFailed,
|
||||||
|
};
|
||||||
|
defer gpa.free(self_dir);
|
||||||
|
const abs_path = try fs.path.join(gpa, &.{ self_dir, path });
|
||||||
|
defer gpa.free(abs_path);
|
||||||
|
break :res std.fs.cwd().openFile(abs_path, .{});
|
||||||
|
};
|
||||||
|
const pdb_file = pdb_file_open_result catch |err| switch (err) {
|
||||||
|
error.FileNotFound, error.IsDir => break :pdb null,
|
||||||
|
else => return error.ReadFailed,
|
||||||
|
};
|
||||||
|
errdefer pdb_file.close();
|
||||||
|
|
||||||
|
const pdb_reader = try arena.create(std.fs.File.Reader);
|
||||||
|
pdb_reader.* = pdb_file.reader(try arena.alloc(u8, 4096));
|
||||||
|
|
||||||
|
var pdb = Pdb.init(gpa, pdb_reader) catch |err| switch (err) {
|
||||||
|
error.OutOfMemory, error.ReadFailed, error.Unexpected => |e| return e,
|
||||||
|
else => return error.InvalidDebugInfo,
|
||||||
|
};
|
||||||
|
errdefer pdb.deinit();
|
||||||
|
pdb.parseInfoStream() catch |err| switch (err) {
|
||||||
|
error.UnknownPDBVersion => return error.UnsupportedDebugInfo,
|
||||||
|
error.EndOfStream => return error.InvalidDebugInfo,
|
||||||
|
|
||||||
|
error.InvalidDebugInfo,
|
||||||
|
error.MissingDebugInfo,
|
||||||
|
error.OutOfMemory,
|
||||||
|
error.ReadFailed,
|
||||||
|
=> |e| return e,
|
||||||
|
};
|
||||||
|
pdb.parseDbiStream() catch |err| switch (err) {
|
||||||
|
error.UnknownPDBVersion => return error.UnsupportedDebugInfo,
|
||||||
|
|
||||||
|
error.EndOfStream,
|
||||||
|
error.EOF,
|
||||||
|
error.StreamTooLong,
|
||||||
|
error.WriteFailed,
|
||||||
|
=> return error.InvalidDebugInfo,
|
||||||
|
|
||||||
|
error.InvalidDebugInfo,
|
||||||
|
error.OutOfMemory,
|
||||||
|
error.ReadFailed,
|
||||||
|
=> |e| return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!std.mem.eql(u8, &coff_obj.guid, &pdb.guid) or coff_obj.age != pdb.age)
|
||||||
|
return error.InvalidDebugInfo;
|
||||||
|
|
||||||
|
break :pdb pdb;
|
||||||
|
};
|
||||||
|
errdefer if (opt_pdb) |*pdb| {
|
||||||
|
pdb.file_reader.file.close();
|
||||||
|
pdb.deinit();
|
||||||
|
};
|
||||||
|
|
||||||
|
const coff_section_headers: []coff.SectionHeader = if (opt_pdb != null) csh: {
|
||||||
|
break :csh try coff_obj.getSectionHeadersAlloc(arena);
|
||||||
|
} else &.{};
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.arena = arena_instance.state,
|
||||||
|
.coff_image_base = coff_image_base,
|
||||||
|
.mapped_file = mapped_file,
|
||||||
|
.dwarf = opt_dwarf,
|
||||||
|
.pdb = opt_pdb,
|
||||||
|
.coff_section_headers = coff_section_headers,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Assumes we already hold `si.mutex`.
|
||||||
|
fn findModule(si: *SelfInfo, gpa: Allocator, address: usize) error{ MissingDebugInfo, OutOfMemory, Unexpected }!*Module {
|
||||||
|
for (si.modules.items) |*mod| {
|
||||||
|
if (address >= mod.base_address and address < mod.base_address + mod.size) {
|
||||||
|
return mod;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A new module might have been loaded; rebuild the list.
|
||||||
|
{
|
||||||
|
for (si.modules.items) |*mod| {
|
||||||
|
const di = &(mod.di orelse continue catch continue);
|
||||||
|
di.deinit(gpa);
|
||||||
|
}
|
||||||
|
si.modules.clearRetainingCapacity();
|
||||||
|
|
||||||
|
var module_name_arena = si.module_name_arena.promote(gpa);
|
||||||
|
defer si.module_name_arena = module_name_arena.state;
|
||||||
|
_ = module_name_arena.reset(.retain_capacity);
|
||||||
|
|
||||||
|
const handle = windows.kernel32.CreateToolhelp32Snapshot(windows.TH32CS_SNAPMODULE | windows.TH32CS_SNAPMODULE32, 0);
|
||||||
|
if (handle == windows.INVALID_HANDLE_VALUE) {
|
||||||
|
return windows.unexpectedError(windows.GetLastError());
|
||||||
|
}
|
||||||
|
defer windows.CloseHandle(handle);
|
||||||
|
var entry: windows.MODULEENTRY32 = undefined;
|
||||||
|
entry.dwSize = @sizeOf(windows.MODULEENTRY32);
|
||||||
|
var result = windows.kernel32.Module32First(handle, &entry);
|
||||||
|
while (result != 0) : (result = windows.kernel32.Module32Next(handle, &entry)) {
|
||||||
|
try si.modules.append(gpa, .{
|
||||||
|
.base_address = @intFromPtr(entry.modBaseAddr),
|
||||||
|
.size = entry.modBaseSize,
|
||||||
|
.name = try module_name_arena.allocator().dupe(
|
||||||
|
u8,
|
||||||
|
std.mem.sliceTo(&entry.szModule, 0),
|
||||||
|
),
|
||||||
|
.handle = entry.hModule,
|
||||||
|
.di = null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (si.modules.items) |*mod| {
|
||||||
|
if (address >= mod.base_address and address < mod.base_address + mod.size) {
|
||||||
|
return mod;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return error.MissingDebugInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const Dwarf = std.debug.Dwarf;
|
||||||
|
const Pdb = std.debug.Pdb;
|
||||||
|
const Error = std.debug.SelfInfoError;
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const coff = std.coff;
|
||||||
|
const fs = std.fs;
|
||||||
|
const windows = std.os.windows;
|
||||||
|
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
const native_endian = builtin.target.cpu.arch.endian();
|
||||||
|
|
||||||
|
const SelfInfo = @This();
|
||||||
|
|
@ -1,442 +0,0 @@
|
||||||
base_address: usize,
|
|
||||||
size: usize,
|
|
||||||
name: []const u8,
|
|
||||||
handle: windows.HMODULE,
|
|
||||||
pub fn key(m: WindowsModule) usize {
|
|
||||||
return m.base_address;
|
|
||||||
}
|
|
||||||
pub fn lookup(cache: *LookupCache, gpa: Allocator, address: usize) std.debug.SelfInfo.Error!WindowsModule {
|
|
||||||
if (lookupInCache(cache, address)) |m| return m;
|
|
||||||
{
|
|
||||||
// Check a new module hasn't been loaded
|
|
||||||
cache.rwlock.lock();
|
|
||||||
defer cache.rwlock.unlock();
|
|
||||||
cache.modules.clearRetainingCapacity();
|
|
||||||
const handle = windows.kernel32.CreateToolhelp32Snapshot(windows.TH32CS_SNAPMODULE | windows.TH32CS_SNAPMODULE32, 0);
|
|
||||||
if (handle == windows.INVALID_HANDLE_VALUE) {
|
|
||||||
return windows.unexpectedError(windows.GetLastError());
|
|
||||||
}
|
|
||||||
defer windows.CloseHandle(handle);
|
|
||||||
var entry: windows.MODULEENTRY32 = undefined;
|
|
||||||
entry.dwSize = @sizeOf(windows.MODULEENTRY32);
|
|
||||||
if (windows.kernel32.Module32First(handle, &entry) != 0) {
|
|
||||||
try cache.modules.append(gpa, entry);
|
|
||||||
while (windows.kernel32.Module32Next(handle, &entry) != 0) {
|
|
||||||
try cache.modules.append(gpa, entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (lookupInCache(cache, address)) |m| return m;
|
|
||||||
return error.MissingDebugInfo;
|
|
||||||
}
|
|
||||||
pub fn getSymbolAtAddress(module: *const WindowsModule, gpa: Allocator, di: *DebugInfo, address: usize) std.debug.SelfInfo.Error!std.debug.Symbol {
|
|
||||||
// The `Pdb` API doesn't really allow us *any* thread-safe access, and the `Dwarf` API isn't
|
|
||||||
// great for it either; just lock the whole thing.
|
|
||||||
di.mutex.lock();
|
|
||||||
defer di.mutex.unlock();
|
|
||||||
|
|
||||||
if (!di.loaded) module.loadDebugInfo(gpa, di) catch |err| switch (err) {
|
|
||||||
error.OutOfMemory, error.InvalidDebugInfo, error.MissingDebugInfo, error.Unexpected => |e| return e,
|
|
||||||
error.FileNotFound => return error.MissingDebugInfo,
|
|
||||||
error.UnknownPDBVersion => return error.UnsupportedDebugInfo,
|
|
||||||
else => return error.ReadFailed,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Translate the runtime address into a virtual address into the module
|
|
||||||
const vaddr = address - module.base_address;
|
|
||||||
|
|
||||||
if (di.pdb != null) {
|
|
||||||
if (di.getSymbolFromPdb(vaddr) catch return error.InvalidDebugInfo) |symbol| return symbol;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (di.dwarf) |*dwarf| {
|
|
||||||
const dwarf_address = vaddr + di.coff_image_base;
|
|
||||||
return dwarf.getSymbol(gpa, native_endian, dwarf_address) catch return error.InvalidDebugInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
return error.MissingDebugInfo;
|
|
||||||
}
|
|
||||||
fn lookupInCache(cache: *LookupCache, address: usize) ?WindowsModule {
|
|
||||||
cache.rwlock.lockShared();
|
|
||||||
defer cache.rwlock.unlockShared();
|
|
||||||
for (cache.modules.items) |*entry| {
|
|
||||||
const base_address = @intFromPtr(entry.modBaseAddr);
|
|
||||||
if (address >= base_address and address < base_address + entry.modBaseSize) {
|
|
||||||
return .{
|
|
||||||
.base_address = base_address,
|
|
||||||
.size = entry.modBaseSize,
|
|
||||||
.name = std.mem.sliceTo(&entry.szModule, 0),
|
|
||||||
.handle = entry.hModule,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
fn loadDebugInfo(module: *const WindowsModule, gpa: Allocator, di: *DebugInfo) !void {
|
|
||||||
const mapped_ptr: [*]const u8 = @ptrFromInt(module.base_address);
|
|
||||||
const mapped = mapped_ptr[0..module.size];
|
|
||||||
var coff_obj = coff.Coff.init(mapped, true) catch return error.InvalidDebugInfo;
|
|
||||||
// The string table is not mapped into memory by the loader, so if a section name is in the
|
|
||||||
// string table then we have to map the full image file from disk. This can happen when
|
|
||||||
// a binary is produced with -gdwarf, since the section names are longer than 8 bytes.
|
|
||||||
if (coff_obj.strtabRequired()) {
|
|
||||||
var name_buffer: [windows.PATH_MAX_WIDE + 4:0]u16 = undefined;
|
|
||||||
name_buffer[0..4].* = .{ '\\', '?', '?', '\\' }; // openFileAbsoluteW requires the prefix to be present
|
|
||||||
const process_handle = windows.GetCurrentProcess();
|
|
||||||
const len = windows.kernel32.GetModuleFileNameExW(
|
|
||||||
process_handle,
|
|
||||||
module.handle,
|
|
||||||
name_buffer[4..],
|
|
||||||
windows.PATH_MAX_WIDE,
|
|
||||||
);
|
|
||||||
if (len == 0) return error.MissingDebugInfo;
|
|
||||||
const coff_file = fs.openFileAbsoluteW(name_buffer[0 .. len + 4 :0], .{}) catch |err| switch (err) {
|
|
||||||
error.FileNotFound => return error.MissingDebugInfo,
|
|
||||||
else => |e| return e,
|
|
||||||
};
|
|
||||||
errdefer coff_file.close();
|
|
||||||
var section_handle: windows.HANDLE = undefined;
|
|
||||||
const create_section_rc = windows.ntdll.NtCreateSection(
|
|
||||||
§ion_handle,
|
|
||||||
windows.STANDARD_RIGHTS_REQUIRED | windows.SECTION_QUERY | windows.SECTION_MAP_READ,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
windows.PAGE_READONLY,
|
|
||||||
// The documentation states that if no AllocationAttribute is specified, then SEC_COMMIT is the default.
|
|
||||||
// In practice, this isn't the case and specifying 0 will result in INVALID_PARAMETER_6.
|
|
||||||
windows.SEC_COMMIT,
|
|
||||||
coff_file.handle,
|
|
||||||
);
|
|
||||||
if (create_section_rc != .SUCCESS) return error.MissingDebugInfo;
|
|
||||||
errdefer windows.CloseHandle(section_handle);
|
|
||||||
var coff_len: usize = 0;
|
|
||||||
var section_view_ptr: ?[*]const u8 = null;
|
|
||||||
const map_section_rc = windows.ntdll.NtMapViewOfSection(
|
|
||||||
section_handle,
|
|
||||||
process_handle,
|
|
||||||
@ptrCast(§ion_view_ptr),
|
|
||||||
null,
|
|
||||||
0,
|
|
||||||
null,
|
|
||||||
&coff_len,
|
|
||||||
.ViewUnmap,
|
|
||||||
0,
|
|
||||||
windows.PAGE_READONLY,
|
|
||||||
);
|
|
||||||
if (map_section_rc != .SUCCESS) return error.MissingDebugInfo;
|
|
||||||
errdefer assert(windows.ntdll.NtUnmapViewOfSection(process_handle, @constCast(section_view_ptr.?)) == .SUCCESS);
|
|
||||||
const section_view = section_view_ptr.?[0..coff_len];
|
|
||||||
coff_obj = coff.Coff.init(section_view, false) catch return error.InvalidDebugInfo;
|
|
||||||
di.mapped_file = .{
|
|
||||||
.file = coff_file,
|
|
||||||
.section_handle = section_handle,
|
|
||||||
.section_view = section_view,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
di.coff_image_base = coff_obj.getImageBase();
|
|
||||||
|
|
||||||
if (coff_obj.getSectionByName(".debug_info")) |_| {
|
|
||||||
di.dwarf = .{};
|
|
||||||
|
|
||||||
inline for (@typeInfo(Dwarf.Section.Id).@"enum".fields, 0..) |section, i| {
|
|
||||||
di.dwarf.?.sections[i] = if (coff_obj.getSectionByName("." ++ section.name)) |section_header| blk: {
|
|
||||||
break :blk .{
|
|
||||||
.data = try coff_obj.getSectionDataAlloc(section_header, gpa),
|
|
||||||
.owned = true,
|
|
||||||
};
|
|
||||||
} else null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try di.dwarf.?.open(gpa, native_endian);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (coff_obj.getPdbPath() catch return error.InvalidDebugInfo) |raw_path| pdb: {
|
|
||||||
const path = blk: {
|
|
||||||
if (fs.path.isAbsolute(raw_path)) {
|
|
||||||
break :blk raw_path;
|
|
||||||
} else {
|
|
||||||
const self_dir = try fs.selfExeDirPathAlloc(gpa);
|
|
||||||
defer gpa.free(self_dir);
|
|
||||||
break :blk try fs.path.join(gpa, &.{ self_dir, raw_path });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
defer if (path.ptr != raw_path.ptr) gpa.free(path);
|
|
||||||
|
|
||||||
const pdb_file = std.fs.cwd().openFile(path, .{}) catch |err| switch (err) {
|
|
||||||
error.FileNotFound, error.IsDir => break :pdb,
|
|
||||||
else => |e| return e,
|
|
||||||
};
|
|
||||||
errdefer pdb_file.close();
|
|
||||||
|
|
||||||
const pdb_reader = try gpa.create(std.fs.File.Reader);
|
|
||||||
errdefer gpa.destroy(pdb_reader);
|
|
||||||
|
|
||||||
pdb_reader.* = pdb_file.reader(try gpa.alloc(u8, 4096));
|
|
||||||
errdefer gpa.free(pdb_reader.interface.buffer);
|
|
||||||
|
|
||||||
var pdb: Pdb = try .init(gpa, pdb_reader);
|
|
||||||
errdefer pdb.deinit();
|
|
||||||
try pdb.parseInfoStream();
|
|
||||||
try pdb.parseDbiStream();
|
|
||||||
|
|
||||||
if (!mem.eql(u8, &coff_obj.guid, &pdb.guid) or coff_obj.age != pdb.age)
|
|
||||||
return error.InvalidDebugInfo;
|
|
||||||
|
|
||||||
di.coff_section_headers = try coff_obj.getSectionHeadersAlloc(gpa);
|
|
||||||
|
|
||||||
di.pdb = pdb;
|
|
||||||
}
|
|
||||||
|
|
||||||
di.loaded = true;
|
|
||||||
}
|
|
||||||
pub const LookupCache = struct {
|
|
||||||
rwlock: std.Thread.RwLock,
|
|
||||||
modules: std.ArrayListUnmanaged(windows.MODULEENTRY32),
|
|
||||||
pub const init: LookupCache = .{
|
|
||||||
.rwlock = .{},
|
|
||||||
.modules = .empty,
|
|
||||||
};
|
|
||||||
pub fn deinit(lc: *LookupCache, gpa: Allocator) void {
|
|
||||||
lc.modules.deinit(gpa);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
pub const DebugInfo = struct {
|
|
||||||
mutex: std.Thread.Mutex,
|
|
||||||
|
|
||||||
loaded: bool,
|
|
||||||
|
|
||||||
coff_image_base: u64,
|
|
||||||
mapped_file: ?struct {
|
|
||||||
file: fs.File,
|
|
||||||
section_handle: windows.HANDLE,
|
|
||||||
section_view: []const u8,
|
|
||||||
},
|
|
||||||
|
|
||||||
dwarf: ?Dwarf,
|
|
||||||
|
|
||||||
pdb: ?Pdb,
|
|
||||||
/// Populated iff `pdb != null`; otherwise `&.{}`.
|
|
||||||
coff_section_headers: []coff.SectionHeader,
|
|
||||||
|
|
||||||
pub const init: DebugInfo = .{
|
|
||||||
.mutex = .{},
|
|
||||||
.loaded = false,
|
|
||||||
.coff_image_base = undefined,
|
|
||||||
.mapped_file = null,
|
|
||||||
.dwarf = null,
|
|
||||||
.pdb = null,
|
|
||||||
.coff_section_headers = &.{},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn deinit(di: *DebugInfo, gpa: Allocator) void {
|
|
||||||
if (!di.loaded) return;
|
|
||||||
if (di.dwarf) |*dwarf| dwarf.deinit(gpa);
|
|
||||||
if (di.pdb) |*pdb| {
|
|
||||||
pdb.file_reader.file.close();
|
|
||||||
gpa.free(pdb.file_reader.interface.buffer);
|
|
||||||
gpa.destroy(pdb.file_reader);
|
|
||||||
pdb.deinit();
|
|
||||||
}
|
|
||||||
gpa.free(di.coff_section_headers);
|
|
||||||
if (di.mapped_file) |mapped| {
|
|
||||||
const process_handle = windows.GetCurrentProcess();
|
|
||||||
assert(windows.ntdll.NtUnmapViewOfSection(process_handle, @constCast(mapped.section_view.ptr)) == .SUCCESS);
|
|
||||||
windows.CloseHandle(mapped.section_handle);
|
|
||||||
mapped.file.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn getSymbolFromPdb(di: *DebugInfo, relocated_address: usize) !?std.debug.Symbol {
|
|
||||||
var coff_section: *align(1) const coff.SectionHeader = undefined;
|
|
||||||
const mod_index = for (di.pdb.?.sect_contribs) |sect_contrib| {
|
|
||||||
if (sect_contrib.section > di.coff_section_headers.len) continue;
|
|
||||||
// Remember that SectionContribEntry.Section is 1-based.
|
|
||||||
coff_section = &di.coff_section_headers[sect_contrib.section - 1];
|
|
||||||
|
|
||||||
const vaddr_start = coff_section.virtual_address + sect_contrib.offset;
|
|
||||||
const vaddr_end = vaddr_start + sect_contrib.size;
|
|
||||||
if (relocated_address >= vaddr_start and relocated_address < vaddr_end) {
|
|
||||||
break sect_contrib.module_index;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// we have no information to add to the address
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const module = try di.pdb.?.getModule(mod_index) orelse return error.InvalidDebugInfo;
|
|
||||||
|
|
||||||
return .{
|
|
||||||
.name = di.pdb.?.getSymbolName(
|
|
||||||
module,
|
|
||||||
relocated_address - coff_section.virtual_address,
|
|
||||||
),
|
|
||||||
.compile_unit_name = fs.path.basename(module.obj_file_name),
|
|
||||||
.source_location = try di.pdb.?.getLineNumberInfo(
|
|
||||||
module,
|
|
||||||
relocated_address - coff_section.virtual_address,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const supports_unwinding: bool = switch (builtin.cpu.arch) {
|
|
||||||
else => true,
|
|
||||||
// On x86, `RtlVirtualUnwind` does not exist. We could in theory use `RtlCaptureStackBackTrace`
|
|
||||||
// instead, but on x86, it turns out that function is just... doing FP unwinding with esp! It's
|
|
||||||
// hard to find implementation details to confirm that, but the most authoritative source I have
|
|
||||||
// is an entry in the LLVM mailing list from 2020/08/16 which contains this quote:
|
|
||||||
//
|
|
||||||
// > x86 doesn't have what most architectures would consider an "unwinder" in the sense of
|
|
||||||
// > restoring registers; there is simply a linked list of frames that participate in SEH and
|
|
||||||
// > that desire to be called for a dynamic unwind operation, so RtlCaptureStackBackTrace
|
|
||||||
// > assumes that EBP-based frames are in use and walks an EBP-based frame chain on x86 - not
|
|
||||||
// > all x86 code is written with EBP-based frames so while even though we generally build the
|
|
||||||
// > OS that way, you might always run the risk of encountering external code that uses EBP as a
|
|
||||||
// > general purpose register for which such an unwind attempt for a stack trace would fail.
|
|
||||||
//
|
|
||||||
// Regardless, it's easy to effectively confirm this hypothesis just by compiling some code with
|
|
||||||
// `-fomit-frame-pointer -OReleaseFast` and observing that `RtlCaptureStackBackTrace` returns an
|
|
||||||
// empty trace when it's called in such an application. Note that without `-OReleaseFast` or
|
|
||||||
// similar, LLVM seems reluctant to ever clobber ebp, so you'll get a trace returned which just
|
|
||||||
// contains all of the kernel32/ntdll frames but none of your own. Don't be deceived---this is
|
|
||||||
// just coincidental!
|
|
||||||
//
|
|
||||||
// Anyway, the point is, the only stack walking primitive on x86-windows is FP unwinding. We
|
|
||||||
// *could* ask Microsoft to do that for us with `RtlCaptureStackBackTrace`... but better to just
|
|
||||||
// use our existing FP unwinder in `std.debug`!
|
|
||||||
.x86 => false,
|
|
||||||
};
|
|
||||||
pub const UnwindContext = struct {
|
|
||||||
pc: usize,
|
|
||||||
cur: windows.CONTEXT,
|
|
||||||
history_table: windows.UNWIND_HISTORY_TABLE,
|
|
||||||
pub fn init(ctx: *const std.debug.cpu_context.Native) UnwindContext {
|
|
||||||
return .{
|
|
||||||
.pc = @returnAddress(),
|
|
||||||
.cur = switch (builtin.cpu.arch) {
|
|
||||||
.x86_64 => std.mem.zeroInit(windows.CONTEXT, .{
|
|
||||||
.Rax = ctx.gprs.get(.rax),
|
|
||||||
.Rcx = ctx.gprs.get(.rcx),
|
|
||||||
.Rdx = ctx.gprs.get(.rdx),
|
|
||||||
.Rbx = ctx.gprs.get(.rbx),
|
|
||||||
.Rsp = ctx.gprs.get(.rsp),
|
|
||||||
.Rbp = ctx.gprs.get(.rbp),
|
|
||||||
.Rsi = ctx.gprs.get(.rsi),
|
|
||||||
.Rdi = ctx.gprs.get(.rdi),
|
|
||||||
.R8 = ctx.gprs.get(.r8),
|
|
||||||
.R9 = ctx.gprs.get(.r9),
|
|
||||||
.R10 = ctx.gprs.get(.r10),
|
|
||||||
.R11 = ctx.gprs.get(.r11),
|
|
||||||
.R12 = ctx.gprs.get(.r12),
|
|
||||||
.R13 = ctx.gprs.get(.r13),
|
|
||||||
.R14 = ctx.gprs.get(.r14),
|
|
||||||
.R15 = ctx.gprs.get(.r15),
|
|
||||||
.Rip = ctx.gprs.get(.rip),
|
|
||||||
}),
|
|
||||||
.aarch64, .aarch64_be => .{
|
|
||||||
.ContextFlags = 0,
|
|
||||||
.Cpsr = 0,
|
|
||||||
.DUMMYUNIONNAME = .{ .X = ctx.x },
|
|
||||||
.Sp = ctx.sp,
|
|
||||||
.Pc = ctx.pc,
|
|
||||||
.V = @splat(.{ .B = @splat(0) }),
|
|
||||||
.Fpcr = 0,
|
|
||||||
.Fpsr = 0,
|
|
||||||
.Bcr = @splat(0),
|
|
||||||
.Bvr = @splat(0),
|
|
||||||
.Wcr = @splat(0),
|
|
||||||
.Wvr = @splat(0),
|
|
||||||
},
|
|
||||||
.thumb => .{
|
|
||||||
.ContextFlags = 0,
|
|
||||||
.R0 = ctx.r[0],
|
|
||||||
.R1 = ctx.r[1],
|
|
||||||
.R2 = ctx.r[2],
|
|
||||||
.R3 = ctx.r[3],
|
|
||||||
.R4 = ctx.r[4],
|
|
||||||
.R5 = ctx.r[5],
|
|
||||||
.R6 = ctx.r[6],
|
|
||||||
.R7 = ctx.r[7],
|
|
||||||
.R8 = ctx.r[8],
|
|
||||||
.R9 = ctx.r[9],
|
|
||||||
.R10 = ctx.r[10],
|
|
||||||
.R11 = ctx.r[11],
|
|
||||||
.R12 = ctx.r[12],
|
|
||||||
.Sp = ctx.r[13],
|
|
||||||
.Lr = ctx.r[14],
|
|
||||||
.Pc = ctx.r[15],
|
|
||||||
.Cpsr = 0,
|
|
||||||
.Fpcsr = 0,
|
|
||||||
.Padding = 0,
|
|
||||||
.DUMMYUNIONNAME = .{ .S = @splat(0) },
|
|
||||||
.Bvr = @splat(0),
|
|
||||||
.Bcr = @splat(0),
|
|
||||||
.Wvr = @splat(0),
|
|
||||||
.Wcr = @splat(0),
|
|
||||||
.Padding2 = @splat(0),
|
|
||||||
},
|
|
||||||
else => comptime unreachable,
|
|
||||||
},
|
|
||||||
.history_table = std.mem.zeroes(windows.UNWIND_HISTORY_TABLE),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
pub fn deinit(ctx: *UnwindContext, gpa: Allocator) void {
|
|
||||||
_ = ctx;
|
|
||||||
_ = gpa;
|
|
||||||
}
|
|
||||||
pub fn getFp(ctx: *UnwindContext) usize {
|
|
||||||
return ctx.cur.getRegs().bp;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
pub fn unwindFrame(module: *const WindowsModule, gpa: Allocator, di: *DebugInfo, context: *UnwindContext) !usize {
|
|
||||||
_ = module;
|
|
||||||
_ = gpa;
|
|
||||||
_ = di;
|
|
||||||
|
|
||||||
const current_regs = context.cur.getRegs();
|
|
||||||
var image_base: windows.DWORD64 = undefined;
|
|
||||||
if (windows.ntdll.RtlLookupFunctionEntry(current_regs.ip, &image_base, &context.history_table)) |runtime_function| {
|
|
||||||
var handler_data: ?*anyopaque = null;
|
|
||||||
var establisher_frame: u64 = undefined;
|
|
||||||
_ = windows.ntdll.RtlVirtualUnwind(
|
|
||||||
windows.UNW_FLAG_NHANDLER,
|
|
||||||
image_base,
|
|
||||||
current_regs.ip,
|
|
||||||
runtime_function,
|
|
||||||
&context.cur,
|
|
||||||
&handler_data,
|
|
||||||
&establisher_frame,
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// leaf function
|
|
||||||
context.cur.setIp(@as(*const usize, @ptrFromInt(current_regs.sp)).*);
|
|
||||||
context.cur.setSp(current_regs.sp + @sizeOf(usize));
|
|
||||||
}
|
|
||||||
|
|
||||||
const next_regs = context.cur.getRegs();
|
|
||||||
const tib = &windows.teb().NtTib;
|
|
||||||
if (next_regs.sp < @intFromPtr(tib.StackLimit) or next_regs.sp > @intFromPtr(tib.StackBase)) {
|
|
||||||
context.pc = 0;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
// Like `DwarfUnwindContext.unwindFrame`, adjust our next lookup pc in case the `call` was this
|
|
||||||
// function's last instruction making `next_regs.ip` one byte past its end.
|
|
||||||
context.pc = next_regs.ip -| 1;
|
|
||||||
return next_regs.ip;
|
|
||||||
}
|
|
||||||
|
|
||||||
const WindowsModule = @This();
|
|
||||||
|
|
||||||
const std = @import("../../std.zig");
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
const Dwarf = std.debug.Dwarf;
|
|
||||||
const Pdb = std.debug.Pdb;
|
|
||||||
const assert = std.debug.assert;
|
|
||||||
const coff = std.coff;
|
|
||||||
const fs = std.fs;
|
|
||||||
const mem = std.mem;
|
|
||||||
const windows = std.os.windows;
|
|
||||||
|
|
||||||
const builtin = @import("builtin");
|
|
||||||
const native_endian = builtin.target.cpu.arch.endian();
|
|
||||||
|
|
@ -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", .{});
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue