mirror of
https://github.com/ziglang/zig.git
synced 2025-12-06 06:13:07 +00:00
436 lines
18 KiB
Zig
436 lines
18 KiB
Zig
//! Cross-platform abstraction for this binary's own debug information, with a
|
|
//! goal of minimal code bloat and compilation speed penalty.
|
|
|
|
// MLUGG TODO: audit use of errors in this file. ideally, introduce some concrete error sets
|
|
|
|
const builtin = @import("builtin");
|
|
const native_os = builtin.os.tag;
|
|
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 regBytes = Dwarf.abi.regBytes;
|
|
const regValueNative = Dwarf.abi.regValueNative;
|
|
|
|
const SelfInfo = @This();
|
|
|
|
modules: std.AutoArrayHashMapUnmanaged(usize, Module.DebugInfo),
|
|
lookup_cache: Module.LookupCache,
|
|
|
|
/// Indicates whether the `SelfInfo` implementation has support for this target.
|
|
pub const target_supported: bool = switch (native_os) {
|
|
.linux,
|
|
.freebsd,
|
|
.netbsd,
|
|
.dragonfly,
|
|
.openbsd,
|
|
.macos,
|
|
.solaris,
|
|
.illumos,
|
|
.windows,
|
|
=> true,
|
|
else => false,
|
|
};
|
|
|
|
/// Indicates whether unwinding for the host is *implemented* here in the Zig
|
|
/// standard library.
|
|
///
|
|
/// See also `Dwarf.abi.supportsUnwinding` which tells whether Dwarf supports
|
|
/// unwinding on a target *in theory*.
|
|
pub const supports_unwinding: bool = switch (builtin.target.cpu.arch) {
|
|
.x86 => switch (builtin.target.os.tag) {
|
|
.linux, .netbsd, .solaris, .illumos => true,
|
|
else => false,
|
|
},
|
|
.x86_64 => switch (builtin.target.os.tag) {
|
|
.linux, .netbsd, .freebsd, .openbsd, .macos, .ios, .solaris, .illumos => true,
|
|
else => false,
|
|
},
|
|
.arm, .armeb, .thumb, .thumbeb => switch (builtin.target.os.tag) {
|
|
.linux => true,
|
|
else => false,
|
|
},
|
|
.aarch64, .aarch64_be => switch (builtin.target.os.tag) {
|
|
.linux, .netbsd, .freebsd, .macos, .ios => true,
|
|
else => false,
|
|
},
|
|
// Unwinding is possible on other targets but this implementation does
|
|
// not support them...yet!
|
|
else => false,
|
|
};
|
|
comptime {
|
|
if (supports_unwinding) assert(Dwarf.abi.supportsUnwinding(&builtin.target));
|
|
}
|
|
|
|
pub const init: SelfInfo = .{
|
|
.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);
|
|
self.modules.deinit(gpa);
|
|
if (Module.LookupCache != void) self.lookup_cache.deinit(gpa);
|
|
}
|
|
comptime {
|
|
// `std.debug` does not currently utilize `deinit`, as it keeps hold of debug info for the
|
|
// whole lifetime of the program. Let's try to avoid it bitrotting.
|
|
_ = &deinit;
|
|
}
|
|
|
|
pub fn unwindFrame(self: *SelfInfo, gpa: Allocator, context: *UnwindContext) !usize {
|
|
comptime assert(supports_unwinding);
|
|
const module: Module = try .lookup(&self.lookup_cache, gpa, context.pc);
|
|
const gop = try self.modules.getOrPut(gpa, module.load_offset);
|
|
self.modules.lockPointers();
|
|
defer self.modules.unlockPointers();
|
|
if (!gop.found_existing) gop.value_ptr.* = .init;
|
|
return module.unwindFrame(gpa, gop.value_ptr, context);
|
|
}
|
|
|
|
pub fn getSymbolAtAddress(self: *SelfInfo, gpa: Allocator, address: usize) !std.debug.Symbol {
|
|
comptime assert(target_supported);
|
|
const module: Module = try .lookup(&self.lookup_cache, gpa, address);
|
|
const gop = try self.modules.getOrPut(gpa, module.key());
|
|
self.modules.lockPointers();
|
|
defer self.modules.unlockPointers();
|
|
if (!gop.found_existing) gop.value_ptr.* = .init;
|
|
return module.getSymbolAtAddress(gpa, gop.value_ptr, address);
|
|
}
|
|
|
|
pub fn getModuleNameForAddress(self: *SelfInfo, gpa: Allocator, address: usize) error{ Unexpected, OutOfMemory, MissingDebugInfo }![]const u8 {
|
|
comptime assert(target_supported);
|
|
const module: Module = try .lookup(&self.lookup_cache, gpa, address);
|
|
return module.name;
|
|
}
|
|
|
|
/// This type contains the target-specific implementation. It must expose the following declarations:
|
|
///
|
|
/// * `LookupCache: type`, with the following declarations unless `LookupCache == void`:
|
|
/// * `init: LookupCache`
|
|
/// * `deinit: fn (*LookupCache, Allocator) void`
|
|
/// * `lookup: fn (*LookupCache, Allocator, address: usize) !Module`
|
|
/// * `key: fn (*const Module) usize`
|
|
/// * `DebugInfo: type`, with the following declarations:
|
|
/// * `DebugInfo.init: DebugInfo`
|
|
/// * `getSymbolAtAddress: fn (*const Module, Allocator, *DebugInfo, address: usize) !std.debug.Symbol`
|
|
///
|
|
/// If unwinding is supported on this target, it must additionally expose the following declarations:
|
|
///
|
|
/// * `unwindFrame: fn (*const Module, Allocator, *DebugInfo, *UnwindContext) !usize`
|
|
const Module = switch (native_os) {
|
|
else => {}, // Dwarf, // TODO MLUGG: it's this on master but that's definitely broken atm...
|
|
.linux, .netbsd, .freebsd, .dragonfly, .openbsd, .haiku, .solaris, .illumos => @import("SelfInfo/ElfModule.zig"),
|
|
.macos, .ios, .watchos, .tvos, .visionos => @import("SelfInfo/DarwinModule.zig"),
|
|
.uefi, .windows => @import("SelfInfo/WindowsModule.zig"),
|
|
.wasi, .emscripten => struct {
|
|
const LookupCache = struct {
|
|
const init: LookupCache = .{};
|
|
};
|
|
fn lookup(cache: *LookupCache, gpa: Allocator, address: usize) !Module {
|
|
_ = cache;
|
|
_ = gpa;
|
|
_ = address;
|
|
@panic("TODO implement lookup module for Wasm");
|
|
}
|
|
const DebugInfo = struct {
|
|
const init: DebugInfo = .{};
|
|
};
|
|
fn getSymbolAtAddress(module: *const Module, gpa: Allocator, di: *DebugInfo, address: usize) !std.debug.Symbol {
|
|
_ = module;
|
|
_ = gpa;
|
|
_ = di;
|
|
_ = address;
|
|
unreachable;
|
|
}
|
|
},
|
|
};
|
|
test {
|
|
_ = Module;
|
|
}
|
|
|
|
pub const UnwindContext = struct {
|
|
gpa: Allocator,
|
|
cfa: ?usize,
|
|
pc: usize,
|
|
thread_context: *std.debug.ThreadContext,
|
|
reg_context: Dwarf.abi.RegisterContext,
|
|
vm: Dwarf.Unwind.VirtualMachine,
|
|
stack_machine: Dwarf.expression.StackMachine(.{ .call_frame_context = true }),
|
|
|
|
pub fn init(gpa: Allocator, thread_context: *std.debug.ThreadContext) !UnwindContext {
|
|
comptime assert(supports_unwinding);
|
|
|
|
const ip_reg_num = Dwarf.abi.ipRegNum(native_arch).?;
|
|
const pc = stripInstructionPtrAuthCode(
|
|
(try regValueNative(thread_context, ip_reg_num, null)).*,
|
|
);
|
|
|
|
const context_copy = try gpa.create(std.debug.ThreadContext);
|
|
std.debug.copyContext(thread_context, context_copy);
|
|
|
|
return .{
|
|
.gpa = gpa,
|
|
.cfa = null,
|
|
.pc = pc,
|
|
.thread_context = context_copy,
|
|
.reg_context = undefined,
|
|
.vm = .{},
|
|
.stack_machine = .{},
|
|
};
|
|
}
|
|
|
|
pub fn deinit(self: *UnwindContext) void {
|
|
self.vm.deinit(self.gpa);
|
|
self.stack_machine.deinit(self.gpa);
|
|
self.gpa.destroy(self.thread_context);
|
|
self.* = undefined;
|
|
}
|
|
|
|
pub fn getFp(self: *const UnwindContext) !usize {
|
|
return (try regValueNative(self.thread_context, Dwarf.abi.fpRegNum(native_arch, self.reg_context), self.reg_context)).*;
|
|
}
|
|
|
|
/// Resolves the register rule and places the result into `out` (see regBytes)
|
|
pub fn resolveRegisterRule(
|
|
context: *UnwindContext,
|
|
col: Dwarf.Unwind.VirtualMachine.Column,
|
|
expression_context: std.debug.Dwarf.expression.Context,
|
|
out: []u8,
|
|
) !void {
|
|
switch (col.rule) {
|
|
.default => {
|
|
const register = col.register orelse return error.InvalidRegister;
|
|
// The default type is usually undefined, but can be overriden by ABI authors.
|
|
// See the doc comment on `Dwarf.Unwind.VirtualMachine.RegisterRule.default`.
|
|
if (builtin.cpu.arch.isAARCH64() and register >= 19 and register <= 18) {
|
|
// Callee-saved registers are initialized as if they had the .same_value rule
|
|
const src = try regBytes(context.thread_context, register, context.reg_context);
|
|
if (src.len != out.len) return error.RegisterSizeMismatch;
|
|
@memcpy(out, src);
|
|
return;
|
|
}
|
|
@memset(out, undefined);
|
|
},
|
|
.undefined => {
|
|
@memset(out, undefined);
|
|
},
|
|
.same_value => {
|
|
// TODO: This copy could be eliminated if callers always copy the state then call this function to update it
|
|
const register = col.register orelse return error.InvalidRegister;
|
|
const src = try regBytes(context.thread_context, register, context.reg_context);
|
|
if (src.len != out.len) return error.RegisterSizeMismatch;
|
|
@memcpy(out, src);
|
|
},
|
|
.offset => |offset| {
|
|
if (context.cfa) |cfa| {
|
|
const addr = try applyOffset(cfa, offset);
|
|
const ptr: *const usize = @ptrFromInt(addr);
|
|
mem.writeInt(usize, out[0..@sizeOf(usize)], ptr.*, native_endian);
|
|
} else return error.InvalidCFA;
|
|
},
|
|
.val_offset => |offset| {
|
|
if (context.cfa) |cfa| {
|
|
mem.writeInt(usize, out[0..@sizeOf(usize)], try applyOffset(cfa, offset), native_endian);
|
|
} else return error.InvalidCFA;
|
|
},
|
|
.register => |register| {
|
|
const src = try regBytes(context.thread_context, register, context.reg_context);
|
|
if (src.len != out.len) return error.RegisterSizeMismatch;
|
|
@memcpy(out, src);
|
|
},
|
|
.expression => |expression| {
|
|
context.stack_machine.reset();
|
|
const value = try context.stack_machine.run(expression, context.gpa, expression_context, context.cfa.?);
|
|
const addr = if (value) |v| blk: {
|
|
if (v != .generic) return error.InvalidExpressionValue;
|
|
break :blk v.generic;
|
|
} else return error.NoExpressionValue;
|
|
|
|
const ptr: *usize = @ptrFromInt(addr);
|
|
mem.writeInt(usize, out[0..@sizeOf(usize)], ptr.*, native_endian);
|
|
},
|
|
.val_expression => |expression| {
|
|
context.stack_machine.reset();
|
|
const value = try context.stack_machine.run(expression, context.gpa, expression_context, context.cfa.?);
|
|
if (value) |v| {
|
|
if (v != .generic) return error.InvalidExpressionValue;
|
|
mem.writeInt(usize, out[0..@sizeOf(usize)], v.generic, native_endian);
|
|
} else return error.NoExpressionValue;
|
|
},
|
|
.architectural => return error.UnimplementedRegisterRule,
|
|
}
|
|
}
|
|
|
|
/// 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 __unwind_info
|
|
/// defers unwinding to DWARF. This is an offset into the `.eh_frame` section.
|
|
pub fn unwindFrameDwarf(
|
|
context: *UnwindContext,
|
|
unwind: *const Dwarf.Unwind,
|
|
load_offset: usize,
|
|
explicit_fde_offset: ?usize,
|
|
) !usize {
|
|
if (!supports_unwinding) return error.UnsupportedCpuArchitecture;
|
|
if (context.pc == 0) return 0;
|
|
|
|
const pc_vaddr = context.pc - load_offset;
|
|
|
|
const fde_offset = explicit_fde_offset orelse try unwind.lookupPc(
|
|
pc_vaddr,
|
|
@sizeOf(usize),
|
|
native_endian,
|
|
) orelse return error.MissingDebugInfo;
|
|
const format, const cie, const fde = try unwind.getFde(fde_offset, @sizeOf(usize), native_endian);
|
|
|
|
// Check if this FDE *actually* includes the address.
|
|
if (pc_vaddr < fde.pc_begin or pc_vaddr >= fde.pc_begin + fde.pc_range) return error.MissingDebugInfo;
|
|
|
|
// Do not set `compile_unit` because the spec states that CFIs
|
|
// may not reference other debug sections anyway.
|
|
var expression_context: Dwarf.expression.Context = .{
|
|
.format = format,
|
|
.thread_context = context.thread_context,
|
|
.reg_context = context.reg_context,
|
|
.cfa = context.cfa,
|
|
};
|
|
|
|
context.vm.reset();
|
|
context.reg_context.eh_frame = cie.version != 4;
|
|
context.reg_context.is_macho = native_os.isDarwin();
|
|
|
|
const row = try context.vm.runTo(context.gpa, context.pc - load_offset, cie, fde, @sizeOf(usize), native_endian);
|
|
context.cfa = switch (row.cfa.rule) {
|
|
.val_offset => |offset| blk: {
|
|
const register = row.cfa.register orelse return error.InvalidCFARule;
|
|
const value = (try regValueNative(context.thread_context, register, context.reg_context)).*;
|
|
break :blk try applyOffset(value, offset);
|
|
},
|
|
.expression => |expr| blk: {
|
|
context.stack_machine.reset();
|
|
const value = try context.stack_machine.run(
|
|
expr,
|
|
context.gpa,
|
|
expression_context,
|
|
context.cfa,
|
|
);
|
|
|
|
if (value) |v| {
|
|
if (v != .generic) return error.InvalidExpressionValue;
|
|
break :blk v.generic;
|
|
} else return error.NoExpressionValue;
|
|
},
|
|
else => return error.InvalidCFARule,
|
|
};
|
|
|
|
expression_context.cfa = context.cfa;
|
|
|
|
// Buffering the modifications is done because copying the thread context is not portable,
|
|
// some implementations (ie. darwin) use internal pointers to the mcontext.
|
|
var arena: std.heap.ArenaAllocator = .init(context.gpa);
|
|
defer arena.deinit();
|
|
const update_arena = arena.allocator();
|
|
|
|
const RegisterUpdate = struct {
|
|
// Backed by thread_context
|
|
dest: []u8,
|
|
// Backed by arena
|
|
src: []const u8,
|
|
prev: ?*@This(),
|
|
};
|
|
|
|
var update_tail: ?*RegisterUpdate = null;
|
|
var has_return_address = true;
|
|
for (context.vm.rowColumns(row)) |column| {
|
|
if (column.register) |register| {
|
|
if (register == cie.return_address_register) {
|
|
has_return_address = column.rule != .undefined;
|
|
}
|
|
|
|
const dest = try regBytes(context.thread_context, register, context.reg_context);
|
|
const src = try update_arena.alloc(u8, dest.len);
|
|
try context.resolveRegisterRule(column, expression_context, src);
|
|
|
|
const new_update = try update_arena.create(RegisterUpdate);
|
|
new_update.* = .{
|
|
.dest = dest,
|
|
.src = src,
|
|
.prev = update_tail,
|
|
};
|
|
update_tail = new_update;
|
|
}
|
|
}
|
|
|
|
// On all implemented architectures, the CFA is defined as being the previous frame's SP
|
|
(try regValueNative(context.thread_context, Dwarf.abi.spRegNum(native_arch, context.reg_context), context.reg_context)).* = context.cfa.?;
|
|
|
|
while (update_tail) |tail| {
|
|
@memcpy(tail.dest, tail.src);
|
|
update_tail = tail.prev;
|
|
}
|
|
|
|
if (has_return_address) {
|
|
context.pc = stripInstructionPtrAuthCode((try regValueNative(
|
|
context.thread_context,
|
|
cie.return_address_register,
|
|
context.reg_context,
|
|
)).*);
|
|
} else {
|
|
context.pc = 0;
|
|
}
|
|
|
|
const ip_reg_num = Dwarf.abi.ipRegNum(native_arch).?;
|
|
(try regValueNative(context.thread_context, ip_reg_num, context.reg_context)).* = context.pc;
|
|
|
|
// The call instruction will have pushed the address of the instruction that follows the call as the return address.
|
|
// This next instruction may be past the end of the function if the caller was `noreturn` (ie. the last instruction in
|
|
// the function was the call). If we were to look up an FDE entry using the return address directly, it could end up
|
|
// either not finding an FDE at all, or using the next FDE in the program, producing incorrect results. To prevent this,
|
|
// we subtract one so that the next lookup is guaranteed to land inside the
|
|
//
|
|
// The exception to this rule is signal frames, where we return execution would be returned to the instruction
|
|
// that triggered the handler.
|
|
const return_address = context.pc;
|
|
if (context.pc > 0 and !cie.is_signal_frame) context.pc -= 1;
|
|
|
|
return return_address;
|
|
}
|
|
/// 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)));
|
|
}
|
|
/// Some platforms use pointer authentication - the upper bits of instruction pointers contain a signature.
|
|
/// This function clears these signature bits to make the pointer usable.
|
|
pub inline fn stripInstructionPtrAuthCode(ptr: usize) usize {
|
|
if (native_arch.isAARCH64()) {
|
|
// `hint 0x07` maps to `xpaclri` (or `nop` if the hardware doesn't support it)
|
|
// The save / restore is because `xpaclri` operates on x30 (LR)
|
|
return asm (
|
|
\\mov x16, x30
|
|
\\mov x30, x15
|
|
\\hint 0x07
|
|
\\mov x15, x30
|
|
\\mov x30, x16
|
|
: [ret] "={x15}" (-> usize),
|
|
: [ptr] "{x15}" (ptr),
|
|
: .{ .x16 = true });
|
|
}
|
|
|
|
return ptr;
|
|
}
|
|
};
|