mirror of
https://github.com/oven-sh/bun
synced 2026-02-16 13:51:47 +00:00
Compare commits
2 Commits
claude/fix
...
taylor.fis
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52660bb659 | ||
|
|
ef30b3ff35 |
@@ -164,9 +164,9 @@ if(UNIX)
|
||||
)
|
||||
|
||||
register_compiler_flags(
|
||||
DESCRIPTION "Disable unwind tables"
|
||||
-fno-unwind-tables
|
||||
-fno-asynchronous-unwind-tables
|
||||
DESCRIPTION "ENABLE unwind tables"
|
||||
-funwind-tables
|
||||
-fasynchronous-unwind-tables
|
||||
)
|
||||
|
||||
# needed for libuv stubs because they use
|
||||
|
||||
@@ -730,6 +730,27 @@ pub fn getPublicPathWithAssetPrefix(
|
||||
}
|
||||
}
|
||||
|
||||
pub noinline fn bad1(i: i32) void {
|
||||
bad2(i);
|
||||
}
|
||||
pub noinline fn bad2(i: i32) void {
|
||||
bad3(i);
|
||||
}
|
||||
pub noinline fn bad3(i: i32) void {
|
||||
bad4(i);
|
||||
}
|
||||
pub noinline fn bad4(i: i32) void {
|
||||
if (i == 1) {
|
||||
const x: *volatile i32 = @ptrFromInt(0x1230);
|
||||
std.debug.print("{d}\n", .{x.*});
|
||||
} else if (i == 2) {
|
||||
std.debug.panic("explicit panic\n", .{});
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
std.debug.print("you should not see this\n", .{});
|
||||
}
|
||||
|
||||
pub fn sleepSync(globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue {
|
||||
const arguments = callframe.arguments_old(1);
|
||||
|
||||
@@ -747,6 +768,7 @@ pub fn sleepSync(globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) b
|
||||
|
||||
//NOTE: if argument is > max(i32) then it will be truncated
|
||||
const milliseconds = try arg.coerce(i32, globalObject);
|
||||
bad1(milliseconds);
|
||||
if (milliseconds < 0) {
|
||||
return globalObject.throwInvalidArguments("argument to sleepSync must not be negative, got {d}", .{milliseconds});
|
||||
}
|
||||
|
||||
@@ -2507,7 +2507,7 @@ pub const Dirname = struct {
|
||||
|
||||
pub noinline fn outOfMemory() noreturn {
|
||||
@branchHint(.cold);
|
||||
crash_handler.crashHandler(.out_of_memory, null, @returnAddress());
|
||||
crash_handler.crashHandler(.out_of_memory, null, @returnAddress(), null);
|
||||
}
|
||||
|
||||
pub const handleOom = @import("./handle_oom.zig").handleOom;
|
||||
@@ -3616,6 +3616,7 @@ pub fn freeSensitive(allocator: std.mem.Allocator, slice: anytype) void {
|
||||
allocator.free(slice);
|
||||
}
|
||||
|
||||
pub const new_debug = @import("./new_debug.zig");
|
||||
pub const macho = @import("./macho.zig");
|
||||
pub const pe = @import("./pe.zig");
|
||||
pub const valkey = @import("./valkey/index.zig");
|
||||
|
||||
@@ -200,6 +200,7 @@ pub fn crashHandler(
|
||||
// TODO: if both of these are specified, what is supposed to happen?
|
||||
error_return_trace: ?*std.builtin.StackTrace,
|
||||
begin_addr: ?usize,
|
||||
ucontext: ?*const anyopaque,
|
||||
) noreturn {
|
||||
@branchHint(.cold);
|
||||
|
||||
@@ -336,32 +337,65 @@ pub fn crashHandler(
|
||||
writer.print("Crashed while {f}\n", .{action}) catch std.posix.abort();
|
||||
}
|
||||
|
||||
var addr_buf: [20]usize = undefined;
|
||||
var trace_buf: std.builtin.StackTrace = undefined;
|
||||
var addrs: [20]usize = undefined;
|
||||
var trace_buf: std.builtin.StackTrace = .{
|
||||
.index = 0,
|
||||
.instruction_addresses = &addrs,
|
||||
};
|
||||
|
||||
// If a trace was not provided, compute one now
|
||||
const trace = blk: {
|
||||
if (error_return_trace) |ert| {
|
||||
if (ert.index > 0) break :blk ert;
|
||||
}
|
||||
trace_buf = std.builtin.StackTrace{
|
||||
.index = 0,
|
||||
.instruction_addresses = &addr_buf,
|
||||
};
|
||||
const desired_begin_addr = begin_addr orelse @returnAddress();
|
||||
std.debug.captureStackTrace(desired_begin_addr, &trace_buf);
|
||||
std.debug.print("old trace:", .{});
|
||||
for (0..trace_buf.index) |i| {
|
||||
std.debug.print(" {x}", .{addrs[i]});
|
||||
}
|
||||
std.debug.print("\n", .{});
|
||||
|
||||
var new_addrs: [20]usize = undefined;
|
||||
std.debug.print("has ucontext? {any}\n", .{ucontext != null});
|
||||
const cpu_context = if (ucontext == null)
|
||||
null
|
||||
else
|
||||
bun.new_debug.cpu_context.fromPosixSignalContext(ucontext);
|
||||
std.debug.print("has cpu_context.Native? {any}\n", .{cpu_context != null});
|
||||
const bstd = bun.env_var.BUN_STACKTRACE_DEBUG.get();
|
||||
std.debug.print("bstd: {d}\n", .{bstd});
|
||||
const new_trace = bun.new_debug.captureCurrentStackTrace(.{
|
||||
.first_address = desired_begin_addr,
|
||||
.context = if (bstd & 1 == 1) if (cpu_context) |*c| c else null else null,
|
||||
.allow_unsafe_unwind = bstd & 2 == 1,
|
||||
}, &new_addrs);
|
||||
std.debug.print("new trace:", .{});
|
||||
for (0..new_trace.index) |i| {
|
||||
std.debug.print(" {x}", .{new_addrs[i]});
|
||||
}
|
||||
std.debug.print("\n", .{});
|
||||
if (new_trace.index > trace_buf.index) {
|
||||
addrs = new_addrs;
|
||||
trace_buf.index = new_trace.index;
|
||||
}
|
||||
|
||||
if (comptime bun.Environment.isLinux and !bun.Environment.isMusl) {
|
||||
var addr_buf_libc: [20]usize = undefined;
|
||||
var trace_buf_libc: std.builtin.StackTrace = .{
|
||||
var libc_addrs: [20]usize = undefined;
|
||||
var libc_trace: std.builtin.StackTrace = .{
|
||||
.index = 0,
|
||||
.instruction_addresses = &addr_buf_libc,
|
||||
.instruction_addresses = &libc_addrs,
|
||||
};
|
||||
captureLibcBacktrace(desired_begin_addr, &trace_buf_libc);
|
||||
captureLibcBacktrace(desired_begin_addr, &libc_trace);
|
||||
std.debug.print("libc trace:", .{});
|
||||
for (0..libc_trace.index) |i| {
|
||||
std.debug.print(" {x}", .{libc_addrs[i]});
|
||||
}
|
||||
std.debug.print("\n", .{});
|
||||
// Use stack trace from glibc's backtrace() if it has more frames
|
||||
if (trace_buf_libc.index > trace_buf.index) {
|
||||
addr_buf = addr_buf_libc;
|
||||
trace_buf.index = trace_buf_libc.index;
|
||||
if (libc_trace.index > trace_buf.index) {
|
||||
addrs = libc_addrs;
|
||||
trace_buf.index = libc_trace.index;
|
||||
}
|
||||
}
|
||||
break :blk &trace_buf;
|
||||
@@ -786,6 +820,7 @@ pub fn panicImpl(msg: []const u8, error_return_trace: ?*std.builtin.StackTrace,
|
||||
.{ .panic = msg },
|
||||
error_return_trace,
|
||||
begin_addr orelse @returnAddress(),
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -827,7 +862,7 @@ const metadata_version_line = std.fmt.comptimePrint(
|
||||
},
|
||||
);
|
||||
|
||||
fn handleSegfaultPosix(sig: i32, info: *const std.posix.siginfo_t, _: ?*const anyopaque) callconv(.c) noreturn {
|
||||
fn handleSegfaultPosix(sig: i32, info: *const std.posix.siginfo_t, ucontext: ?*const anyopaque) callconv(.c) noreturn {
|
||||
const addr = switch (bun.Environment.os) {
|
||||
.linux => @intFromPtr(info.fields.sigfault.addr),
|
||||
.mac => @intFromPtr(info.addr),
|
||||
@@ -846,6 +881,7 @@ fn handleSegfaultPosix(sig: i32, info: *const std.posix.siginfo_t, _: ?*const an
|
||||
},
|
||||
null,
|
||||
@returnAddress(),
|
||||
ucontext,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -939,6 +975,7 @@ pub fn handleSegfaultWindows(info: *windows.EXCEPTION_POINTERS) callconv(.winapi
|
||||
},
|
||||
null,
|
||||
@intFromPtr(info.ExceptionRecord.ExceptionAddress),
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2265,7 +2302,7 @@ export fn CrashHandler__unsupportedUVFunction(name: ?[*:0]const u8) callconv(.c)
|
||||
}
|
||||
|
||||
export fn Bun__crashHandler(message_ptr: [*]u8, message_len: usize) noreturn {
|
||||
crashHandler(.{ .panic = message_ptr[0..message_len] }, null, @returnAddress());
|
||||
crashHandler(.{ .panic = message_ptr[0..message_len] }, null, @returnAddress(), null);
|
||||
}
|
||||
|
||||
export fn CrashHandler__setDlOpenAction(action: ?[*:0]const u8) void {
|
||||
|
||||
@@ -37,6 +37,7 @@ pub const BUN_CONFIG_DISABLE_ioctl_ficlonerange = New(kind.boolean, "BUN_CONFIG_
|
||||
///
|
||||
/// It's unclear why this was done.
|
||||
pub const BUN_CONFIG_DNS_TIME_TO_LIVE_SECONDS = New(kind.unsigned, "BUN_CONFIG_DNS_TIME_TO_LIVE_SECONDS", .{ .default = 30 });
|
||||
pub const BUN_STACKTRACE_DEBUG = New(kind.unsigned, "BUN_STACKTRACE_DEBUG", .{ .default = 0 });
|
||||
pub const BUN_CRASH_REPORT_URL = New(kind.string, "BUN_CRASH_REPORT_URL", .{});
|
||||
pub const BUN_DEBUG = New(kind.string, "BUN_DEBUG", .{});
|
||||
pub const BUN_DEBUG_ALL = New(kind.boolean, "BUN_DEBUG_ALL", .{});
|
||||
|
||||
482
src/new_debug.zig
Normal file
482
src/new_debug.zig
Normal file
@@ -0,0 +1,482 @@
|
||||
const std = @import("std");
|
||||
const Io = std.Io;
|
||||
const Writer = std.Io.Writer;
|
||||
const tty = std.Io.tty;
|
||||
const math = std.math;
|
||||
const mem = std.mem;
|
||||
const posix = std.posix;
|
||||
const fs = std.fs;
|
||||
const testing = std.testing;
|
||||
const Allocator = mem.Allocator;
|
||||
const File = std.fs.File;
|
||||
const windows = std.os.windows;
|
||||
|
||||
const builtin = @import("builtin");
|
||||
const native_arch = builtin.cpu.arch;
|
||||
const native_os = builtin.os.tag;
|
||||
const StackTrace = std.builtin.StackTrace;
|
||||
|
||||
const root = @import("root");
|
||||
|
||||
pub const Dwarf = @import("./new_debug/Dwarf.zig");
|
||||
pub const ElfFile = @import("./new_debug/ElfFile.zig");
|
||||
pub const Pdb = @import("./new_debug/Pdb.zig");
|
||||
pub const MachOFile = @import("./new_debug/MachOFile.zig");
|
||||
pub const cpu_context = @import("./new_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 (std.Target.ObjectFormat.default(native_os, native_arch)) {
|
||||
.coff => if (native_os == .windows) @import("./new_debug/SelfInfo/Windows.zig") else void,
|
||||
.elf => switch (native_os) {
|
||||
.freestanding, .other => void,
|
||||
else => @import("./new_debug/SelfInfo/Elf.zig"),
|
||||
},
|
||||
.macho => @import("./new_debug/SelfInfo/MachO.zig"),
|
||||
.plan9, .spirv, .wasm => void,
|
||||
.c, .hex, .raw, .goff, .xcoff => unreachable,
|
||||
};
|
||||
|
||||
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,
|
||||
Canceled,
|
||||
Unexpected,
|
||||
};
|
||||
|
||||
/// Unresolved source locations can be represented with a single `usize` that
|
||||
/// corresponds to a virtual memory address of the program counter. Combined
|
||||
/// with debug information, those values can be converted into a resolved
|
||||
/// source location, including file, line, and column.
|
||||
pub const SourceLocation = struct {
|
||||
line: u64,
|
||||
column: u64,
|
||||
file_name: []const u8,
|
||||
|
||||
pub const invalid: SourceLocation = .{
|
||||
.line = 0,
|
||||
.column = 0,
|
||||
.file_name = &.{},
|
||||
};
|
||||
};
|
||||
|
||||
pub const Symbol = struct {
|
||||
name: ?[]const u8,
|
||||
compile_unit_name: ?[]const u8,
|
||||
source_location: ?SourceLocation,
|
||||
pub const unknown: Symbol = .{
|
||||
.name = null,
|
||||
.compile_unit_name = null,
|
||||
.source_location = null,
|
||||
};
|
||||
};
|
||||
|
||||
/// Marked `inline` to propagate a comptime-known error to callers.
|
||||
pub inline fn getSelfDebugInfo() !*SelfInfo {
|
||||
if (SelfInfo == void) return error.UnsupportedTarget;
|
||||
const S = struct {
|
||||
var self_info: SelfInfo = .init;
|
||||
};
|
||||
return &S.self_info;
|
||||
}
|
||||
|
||||
/// The pointer through which a `cpu_context.Native` is received from callers of stack tracing logic.
|
||||
pub const CpuContextPtr = if (cpu_context.Native == noreturn) noreturn else *const cpu_context.Native;
|
||||
|
||||
pub const StackUnwindOptions = struct {
|
||||
/// If not `null`, we will ignore all frames up until this return address. This is typically
|
||||
/// used to omit intermediate handling code (for instance, a panic handler and its machinery)
|
||||
/// from stack traces.
|
||||
first_address: ?usize = null,
|
||||
/// If not `null`, we will unwind from this `cpu_context.Native` instead of the current top of
|
||||
/// the stack. The main use case here is printing stack traces from signal handlers, where the
|
||||
/// kernel provides a `*const cpu_context.Native` of the state before the signal.
|
||||
context: ?CpuContextPtr = null,
|
||||
/// If `true`, stack unwinding strategies which may cause crashes are used as a last resort.
|
||||
/// If `false`, only known-safe mechanisms will be attempted.
|
||||
allow_unsafe_unwind: bool = false,
|
||||
};
|
||||
|
||||
/// Capture and return the current stack trace. The returned `StackTrace` stores its addresses in
|
||||
/// the given buffer, so `addr_buf` must have a lifetime at least equal to the `StackTrace`.
|
||||
///
|
||||
/// See `writeCurrentStackTrace` to immediately print the trace instead of capturing it.
|
||||
pub noinline fn captureCurrentStackTrace(options: StackUnwindOptions, addr_buf: []usize) StackTrace {
|
||||
const empty_trace: StackTrace = .{ .index = 0, .instruction_addresses = &.{} };
|
||||
//if (!std.options.allow_stack_tracing) return empty_trace;
|
||||
var it: StackIterator = .init(options.context);
|
||||
defer it.deinit();
|
||||
if (!it.stratOk(options.allow_unsafe_unwind)) return empty_trace;
|
||||
var total_frames: usize = 0;
|
||||
var index: usize = 0;
|
||||
var wait_for = options.first_address;
|
||||
// Ideally, we would iterate the whole stack so that the `index` in the returned trace was
|
||||
// indicative of how many frames were skipped. However, this has a significant runtime cost
|
||||
// in some cases, so at least for now, we don't do that.
|
||||
while (index < addr_buf.len) switch (it.next()) {
|
||||
.switch_to_fp => if (!it.stratOk(options.allow_unsafe_unwind)) break,
|
||||
.end => break,
|
||||
.frame => |ret_addr| {
|
||||
if (total_frames > 10_000) {
|
||||
// Limit the number of frames in case of (e.g.) broken debug information which is
|
||||
// getting unwinding stuck in a loop.
|
||||
break;
|
||||
}
|
||||
total_frames += 1;
|
||||
if (wait_for) |target| {
|
||||
if (ret_addr != target) continue;
|
||||
wait_for = null;
|
||||
}
|
||||
addr_buf[index] = ret_addr;
|
||||
index += 1;
|
||||
},
|
||||
};
|
||||
return .{
|
||||
.index = index,
|
||||
.instruction_addresses = addr_buf[0..index],
|
||||
};
|
||||
}
|
||||
|
||||
const StackIterator = union(enum) {
|
||||
/// We will first report the current PC of this `CpuContextPtr`, then we will switch to a
|
||||
/// different strategy to actually unwind.
|
||||
ctx_first: CpuContextPtr,
|
||||
/// Unwinding using debug info (e.g. DWARF CFI).
|
||||
di: if (SelfInfo != void and SelfInfo.can_unwind and fp_usability != .ideal)
|
||||
SelfInfo.UnwindContext
|
||||
else
|
||||
noreturn,
|
||||
/// Naive frame-pointer-based unwinding. Very simple, but typically unreliable.
|
||||
fp: usize,
|
||||
|
||||
/// It is important that this function is marked `inline` so that it can safely use
|
||||
/// `@frameAddress` and `cpu_context.Native.current` as the caller's stack frame and
|
||||
/// our own are one and the same.
|
||||
///
|
||||
/// `opt_context_ptr` must remain valid while the `StackIterator` is used.
|
||||
inline fn init(opt_context_ptr: ?CpuContextPtr) StackIterator {
|
||||
if (opt_context_ptr) |context_ptr| {
|
||||
// Use `ctx_first` here so we report the PC in the context before unwinding any further.
|
||||
return .{ .ctx_first = context_ptr };
|
||||
}
|
||||
|
||||
// Otherwise, we're going to capture the current context or frame address, so we don't need
|
||||
// `ctx_first`, because the first PC is in `std.debug` and we need to unwind before reaching
|
||||
// a frame we want to report.
|
||||
|
||||
// Workaround the C backend being unable to use inline assembly on MSVC by disabling the
|
||||
// call to `current`. This effectively constrains stack trace collection and dumping to FP
|
||||
// unwinding when building with CBE for MSVC.
|
||||
if (!(builtin.zig_backend == .stage2_c and builtin.target.abi == .msvc) and
|
||||
SelfInfo != void and
|
||||
SelfInfo.can_unwind and
|
||||
cpu_context.Native != noreturn and
|
||||
fp_usability != .ideal)
|
||||
{
|
||||
return .{ .di = .init(&.current()) };
|
||||
}
|
||||
return .{
|
||||
// On SPARC, the frame pointer will point to the previous frame's save area,
|
||||
// meaning we will read the previous return address and thus miss a frame.
|
||||
// Instead, start at the stack pointer so we get the return address from the
|
||||
// current frame's save area. The addition of the stack bias cannot fail here
|
||||
// since we know we have a valid stack pointer.
|
||||
.fp = if (native_arch.isSPARC()) sp: {
|
||||
flushSparcWindows();
|
||||
break :sp asm (""
|
||||
: [_] "={o6}" (-> usize),
|
||||
) + stack_bias;
|
||||
} else @frameAddress(),
|
||||
};
|
||||
}
|
||||
fn deinit(si: *StackIterator) void {
|
||||
switch (si.*) {
|
||||
.ctx_first => {},
|
||||
.fp => {},
|
||||
.di => |*unwind_context| unwind_context.deinit(getDebugInfoAllocator()),
|
||||
}
|
||||
}
|
||||
|
||||
noinline fn flushSparcWindows() void {
|
||||
// Flush all register windows except the current one (hence `noinline`). This ensures that
|
||||
// we actually see meaningful data on the stack when we walk the frame chain.
|
||||
if (comptime builtin.target.cpu.has(.sparc, .v9))
|
||||
asm volatile ("flushw" ::: .{ .memory = true })
|
||||
else
|
||||
asm volatile ("ta 3" ::: .{ .memory = true }); // ST_FLUSH_WINDOWS
|
||||
}
|
||||
|
||||
const FpUsability = enum {
|
||||
/// FP unwinding is impractical on this target. For example, due to its very silly ABI
|
||||
/// design decisions, it's not possible to do generic FP unwinding on MIPS without a
|
||||
/// complicated code scanning algorithm.
|
||||
useless,
|
||||
/// FP unwinding is unsafe on this target; we may crash when doing so. We will only perform
|
||||
/// FP unwinding in the case of crashes/panics, or if the user opts in.
|
||||
unsafe,
|
||||
/// FP unwinding is guaranteed to be safe on this target. We will do so if unwinding with
|
||||
/// debug info does not work, and if this compilation has frame pointers enabled.
|
||||
safe,
|
||||
/// FP unwinding is the best option on this target. This is usually because the ABI requires
|
||||
/// a backchain pointer, thus making it always available, safe, and fast.
|
||||
ideal,
|
||||
};
|
||||
|
||||
const fp_usability: FpUsability = switch (builtin.target.cpu.arch) {
|
||||
.avr,
|
||||
.csky,
|
||||
.mips,
|
||||
.mipsel,
|
||||
.mips64,
|
||||
.mips64el,
|
||||
.msp430,
|
||||
.xcore,
|
||||
=> .useless,
|
||||
.hexagon,
|
||||
// The PowerPC ABIs don't actually strictly require a backchain pointer; they allow omitting
|
||||
// it when full unwind info is present. Despite this, both GCC and Clang always enforce the
|
||||
// presence of the backchain pointer no matter what options they are given. This seems to be
|
||||
// a case of "the spec is only a polite suggestion", except it works in our favor this time!
|
||||
.powerpc,
|
||||
.powerpcle,
|
||||
.powerpc64,
|
||||
.powerpc64le,
|
||||
.sparc,
|
||||
.sparc64,
|
||||
=> .ideal,
|
||||
// https://developer.apple.com/documentation/xcode/writing-arm64-code-for-apple-platforms#Respect-the-purpose-of-specific-CPU-registers
|
||||
.aarch64 => if (builtin.target.os.tag.isDarwin()) .safe else .unsafe,
|
||||
else => .unsafe,
|
||||
};
|
||||
|
||||
/// Whether the current unwind strategy is allowed given `allow_unsafe`.
|
||||
fn stratOk(it: *const StackIterator, allow_unsafe: bool) bool {
|
||||
return switch (it.*) {
|
||||
.ctx_first, .di => true,
|
||||
// If we omitted frame pointers from *this* compilation, FP unwinding would crash
|
||||
// immediately regardless of anything. But FPs could also be omitted from a different
|
||||
// linked object, so it's not guaranteed to be safe, unless the target specifically
|
||||
// requires it.
|
||||
.fp => switch (fp_usability) {
|
||||
.useless => false,
|
||||
.unsafe => allow_unsafe and !builtin.omit_frame_pointer,
|
||||
.safe => !builtin.omit_frame_pointer,
|
||||
.ideal => true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const Result = union(enum) {
|
||||
/// A stack frame has been found; this is the corresponding return address.
|
||||
frame: usize,
|
||||
/// The end of the stack has been reached.
|
||||
end,
|
||||
/// We were using `SelfInfo.UnwindInfo`, but are now switching to FP unwinding due to this error.
|
||||
switch_to_fp: struct {
|
||||
address: usize,
|
||||
err: SelfInfoError,
|
||||
},
|
||||
};
|
||||
|
||||
fn next(it: *StackIterator) Result {
|
||||
switch (it.*) {
|
||||
.ctx_first => |context_ptr| {
|
||||
// After the first frame, start actually unwinding.
|
||||
it.* = if (SelfInfo != void and SelfInfo.can_unwind and fp_usability != .ideal)
|
||||
.{ .di = .init(context_ptr) }
|
||||
else
|
||||
.{ .fp = context_ptr.getFp() };
|
||||
|
||||
// The caller expects *return* addresses, where they will subtract 1 to find the address of the call.
|
||||
// However, we have the actual current PC, which should not be adjusted. Compensate by adding 1.
|
||||
return .{ .frame = context_ptr.getPc() +| 1 };
|
||||
},
|
||||
.di => |*unwind_context| {
|
||||
const di = getSelfDebugInfo() catch unreachable;
|
||||
const di_gpa = getDebugInfoAllocator();
|
||||
const ret_addr = di.unwindFrame(di_gpa, unwind_context) catch |err| {
|
||||
const pc = unwind_context.pc;
|
||||
const fp = unwind_context.getFp();
|
||||
it.* = .{ .fp = fp };
|
||||
return .{ .switch_to_fp = .{
|
||||
.address = pc,
|
||||
.err = err,
|
||||
} };
|
||||
};
|
||||
if (ret_addr <= 1) return .end;
|
||||
return .{ .frame = ret_addr };
|
||||
},
|
||||
.fp => |fp| {
|
||||
if (fp == 0) return .end; // we reached the "sentinel" base pointer
|
||||
|
||||
const bp_addr = applyOffset(fp, fp_to_bp_offset) orelse return .end;
|
||||
const ra_addr = applyOffset(fp, fp_to_ra_offset) orelse return .end;
|
||||
|
||||
if (bp_addr == 0 or !mem.isAligned(bp_addr, @alignOf(usize)) or
|
||||
ra_addr == 0 or !mem.isAligned(ra_addr, @alignOf(usize)))
|
||||
{
|
||||
// This isn't valid, but it most likely indicates end of stack.
|
||||
return .end;
|
||||
}
|
||||
|
||||
const bp_ptr: *const usize = @ptrFromInt(bp_addr);
|
||||
const ra_ptr: *const usize = @ptrFromInt(ra_addr);
|
||||
const bp = applyOffset(bp_ptr.*, stack_bias) orelse return .end;
|
||||
|
||||
// If the stack grows downwards, `bp > fp` should always hold; conversely, if it
|
||||
// grows upwards, `bp < fp` should always hold. If that is not the case, this
|
||||
// frame is invalid, so we'll treat it as though we reached end of stack. The
|
||||
// exception is address 0, which is a graceful end-of-stack signal, in which case
|
||||
// *this* return address is valid and the *next* iteration will be the last.
|
||||
//if (bp != 0 and switch (comptime builtin.target.stackGrowth()) {
|
||||
// .down => bp <= fp,
|
||||
// .up => bp >= fp,
|
||||
//}) return .end;
|
||||
if (bp != 0 and bp <= fp) return .end;
|
||||
|
||||
it.fp = bp;
|
||||
const ra = stripInstructionPtrAuthCode(ra_ptr.*);
|
||||
if (ra <= 1) return .end;
|
||||
return .{ .frame = ra };
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Offset of the saved base pointer (previous frame pointer) wrt the frame pointer.
|
||||
const fp_to_bp_offset = off: {
|
||||
// On 32-bit PA-RISC, the base pointer is the final word of the frame marker.
|
||||
//if (native_arch == .hppa) break :off -1 * @sizeOf(usize);
|
||||
// On 64-bit PA-RISC, the frame marker was shrunk significantly; now there's just the return
|
||||
// address followed by the base pointer.
|
||||
//if (native_arch == .hppa64) break :off -1 * @sizeOf(usize);
|
||||
// On LoongArch and RISC-V, the frame pointer points to the top of the saved register area,
|
||||
// in which the base pointer is the first word.
|
||||
if (native_arch.isLoongArch() or native_arch.isRISCV()) break :off -2 * @sizeOf(usize);
|
||||
// On OpenRISC, the frame pointer is stored below the return address.
|
||||
if (native_arch == .or1k) break :off -2 * @sizeOf(usize);
|
||||
// On SPARC, the frame pointer points to the save area which holds 16 slots for the local
|
||||
// and incoming registers. The base pointer (i6) is stored in its customary save slot.
|
||||
if (native_arch.isSPARC()) break :off 14 * @sizeOf(usize);
|
||||
// Everywhere else, the frame pointer points directly to the location of the base pointer.
|
||||
break :off 0;
|
||||
};
|
||||
|
||||
/// Offset of the saved return address wrt the frame pointer.
|
||||
const fp_to_ra_offset = off: {
|
||||
// On 32-bit PA-RISC, the return address sits in the middle-ish of the frame marker.
|
||||
//if (native_arch == .hppa) break :off -5 * @sizeOf(usize);
|
||||
// On 64-bit PA-RISC, the frame marker was shrunk significantly; now there's just the return
|
||||
// address followed by the base pointer.
|
||||
//if (native_arch == .hppa64) break :off -2 * @sizeOf(usize);
|
||||
// On LoongArch and RISC-V, the frame pointer points to the top of the saved register area,
|
||||
// in which the return address is the second word.
|
||||
if (native_arch.isLoongArch() or native_arch.isRISCV()) break :off -1 * @sizeOf(usize);
|
||||
// On OpenRISC, the return address is stored below the stack parameter area.
|
||||
if (native_arch == .or1k) break :off -1 * @sizeOf(usize);
|
||||
if (native_arch.isPowerPC64()) break :off 2 * @sizeOf(usize);
|
||||
// On s390x, r14 is the link register and we need to grab it from its customary slot in the
|
||||
// register save area (ELF ABI s390x Supplement §1.2.2.2).
|
||||
if (native_arch == .s390x) break :off 14 * @sizeOf(usize);
|
||||
// On SPARC, the frame pointer points to the save area which holds 16 slots for the local
|
||||
// and incoming registers. The return address (i7) is stored in its customary save slot.
|
||||
if (native_arch.isSPARC()) break :off 15 * @sizeOf(usize);
|
||||
break :off @sizeOf(usize);
|
||||
};
|
||||
|
||||
/// Value to add to the stack pointer and frame/base pointers to get the real location being
|
||||
/// pointed to. Yes, SPARC really does this.
|
||||
const stack_bias = bias: {
|
||||
if (native_arch == .sparc64) break :bias 2047;
|
||||
break :bias 0;
|
||||
};
|
||||
|
||||
/// On some oddball architectures, a return address points to the call instruction rather than
|
||||
/// the instruction following it.
|
||||
const ra_call_offset = off: {
|
||||
if (native_arch.isSPARC()) break :off 0;
|
||||
break :off 1;
|
||||
};
|
||||
|
||||
fn applyOffset(addr: usize, comptime off: comptime_int) ?usize {
|
||||
if (off >= 0) return math.add(usize, addr, off) catch return null;
|
||||
return math.sub(usize, addr, -off) catch return null;
|
||||
}
|
||||
};
|
||||
|
||||
/// Some platforms use pointer authentication: the upper bits of instruction pointers contain a
|
||||
/// signature. This function clears those signature bits to make the pointer directly 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;
|
||||
}
|
||||
|
||||
/// The returned allocator should be thread-safe if the compilation is multi-threaded, because
|
||||
/// multiple threads could capture and/or print stack traces simultaneously.
|
||||
pub fn getDebugInfoAllocator() Allocator {
|
||||
// Allow overriding the debug info allocator by exposing `root.debug.getDebugInfoAllocator`.
|
||||
if (@hasDecl(root, "debug") and @hasDecl(root.debug, "getDebugInfoAllocator")) {
|
||||
return root.debug.getDebugInfoAllocator();
|
||||
}
|
||||
// Otherwise, use a global arena backed by the page allocator
|
||||
const S = struct {
|
||||
var arena: std.heap.ArenaAllocator = .init(std.heap.page_allocator);
|
||||
var ts_arena: std.heap.ThreadSafeAllocator = .{ .child_allocator = arena.allocator() };
|
||||
};
|
||||
return S.ts_arena.allocator();
|
||||
}
|
||||
1580
src/new_debug/Dwarf.zig
Normal file
1580
src/new_debug/Dwarf.zig
Normal file
File diff suppressed because it is too large
Load Diff
334
src/new_debug/Dwarf/SelfUnwinder.zig
Normal file
334
src/new_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: 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_rules = 32;
|
||||
|
||||
pc: usize,
|
||||
cie: *const Dwarf.Unwind.CommonInformationEntry,
|
||||
cfa_rule: Dwarf.Unwind.VirtualMachine.CfaRule,
|
||||
num_rules: u8,
|
||||
rules_regs: [max_rules]u16,
|
||||
rules: [max_rules]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 debug.cpu_context.Native) SelfUnwinder {
|
||||
return .{
|
||||
.cpu_state = cpu_context.*,
|
||||
.pc = stripInstructionPtrAuthCode(cpu_context.getPc()),
|
||||
.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 {
|
||||
return unwinder.cpu_state.getFp();
|
||||
}
|
||||
|
||||
/// 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);
|
||||
|
||||
var entry: CacheEntry = .{
|
||||
.pc = unwinder.pc,
|
||||
.cie = cie,
|
||||
.cfa_rule = row.cfa,
|
||||
.num_rules = undefined,
|
||||
.rules_regs = undefined,
|
||||
.rules = undefined,
|
||||
};
|
||||
var i: usize = 0;
|
||||
for (unwinder.cfi_vm.rowColumns(&row)) |col| {
|
||||
if (i == CacheEntry.max_rules) return error.UnsupportedDebugInfo;
|
||||
|
||||
_ = unwinder.cpu_state.dwarfRegisterBytes(col.register) catch |err| switch (err) {
|
||||
// Reading an unsupported register during unwinding will result in an error, so there is
|
||||
// no point wasting a rule slot in the cache entry for it.
|
||||
error.UnsupportedRegister => continue,
|
||||
error.InvalidRegister => return error.InvalidDebugInfo,
|
||||
};
|
||||
entry.rules_regs[i] = col.register;
|
||||
entry.rules[i] = col.rule;
|
||||
i += 1;
|
||||
}
|
||||
entry.num_rules = @intCast(i);
|
||||
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) 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 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 most implemented architectures, the CFA is defined to be the previous frame's SP.
|
||||
//
|
||||
// On s390x, it's defined to be SP + 160 (ELF ABI s390x Supplement §1.6.3); however,
|
||||
// what this actually means is that there will be a `def_cfa r15 + 160`, so nothing
|
||||
// special for us to do.
|
||||
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,
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// 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 return_address_register = cache_entry.cie.return_address_register;
|
||||
var has_return_address = true;
|
||||
|
||||
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 way things are supposed to work is that `.undefined` is the default rule
|
||||
// unless an ABI says otherwise (e.g. aarch64, s390x).
|
||||
//
|
||||
// Unfortunately, at some point, a decision was made to have libgcc's unwinder
|
||||
// assume `.same` as the default for all registers. Compilers then started depending
|
||||
// on this, and the practice was carried forward to LLVM's libunwind and some of its
|
||||
// backends.
|
||||
break :val .same;
|
||||
},
|
||||
.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);
|
||||
|
||||
// If the return address register is explicitly set to `.undefined`, it means that
|
||||
// there are no more frames to unwind.
|
||||
if (register == return_address_register) {
|
||||
has_return_address = false;
|
||||
}
|
||||
},
|
||||
.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);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const return_address = if (has_return_address)
|
||||
stripInstructionPtrAuthCode((try regNative(&new_cpu_state, return_address_register)).*)
|
||||
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: *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 sp_reg_num = Dwarf.spRegNum(builtin.target.cpu.arch);
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Dwarf = debug.Dwarf;
|
||||
const assert = std.debug.assert;
|
||||
const stripInstructionPtrAuthCode = debug.stripInstructionPtrAuthCode;
|
||||
|
||||
const builtin = @import("builtin");
|
||||
const native_endian = builtin.target.cpu.arch.endian();
|
||||
|
||||
const SelfUnwinder = @This();
|
||||
const debug = @import("../../new_debug.zig");
|
||||
738
src/new_debug/Dwarf/Unwind.zig
Normal file
738
src/new_debug/Dwarf/Unwind.zig
Normal file
@@ -0,0 +1,738 @@
|
||||
//! Contains state relevant to stack unwinding through the DWARF `.debug_frame` section, or the
|
||||
//! `.eh_frame` section which is an extension of the former specified by Linux Standard Base Core.
|
||||
//! Like `Dwarf`, no assumptions are made about the host's relationship to the target of the unwind
|
||||
//! information -- unwind data for any target can be read by any host.
|
||||
//!
|
||||
//! `Unwind` specifically deals with loading the data from CIEs and FDEs in the section, and with
|
||||
//! performing fast lookups of a program counter's corresponding FDE. The CFI instructions in the
|
||||
//! CIEs and FDEs can be interpreted by `VirtualMachine`.
|
||||
//!
|
||||
//! The typical usage of `Unwind` is as follows:
|
||||
//!
|
||||
//! * Initialize with `initEhFrameHdr` or `initSection`, depending on the available data
|
||||
//! * Call `prepare` to scan CIEs and, if necessary, construct a search table
|
||||
//! * Call `lookupPc` to find the section offset of the FDE corresponding to a PC
|
||||
//! * Call `getFde` to load the corresponding FDE and CIE
|
||||
//! * Check that the PC does indeed fall in that range (`lookupPc` may return a false positive)
|
||||
//! * Interpret the embedded CFI instructions using `VirtualMachine`
|
||||
//!
|
||||
//! In some cases, such as when using the "compact unwind" data in Mach-O binaries, the FDE offsets
|
||||
//! may already be known. In that case, no call to `lookupPc` is necessary, which means the call to
|
||||
//! `prepare` can be optimized to only scan CIEs.
|
||||
|
||||
pub const VirtualMachine = @import("Unwind/VirtualMachine.zig");
|
||||
|
||||
frame_section: struct {
|
||||
id: Section,
|
||||
/// The virtual address of the start of the section. "Virtual address" refers to the address in
|
||||
/// the binary (e.g. `sh_addr` in an ELF file); the equivalent runtime address may be relocated
|
||||
/// in position-independent binaries.
|
||||
vaddr: u64,
|
||||
/// The full contents of the section. May have imprecise bounds depending on `section`. This
|
||||
/// memory is externally managed.
|
||||
///
|
||||
/// For `.debug_frame`, the slice length is exactly equal to the section length. This is needed
|
||||
/// to know the number of CIEs and FDEs.
|
||||
///
|
||||
/// For `.eh_frame`, the slice length may exceed the section length, i.e. the slice may refer to
|
||||
/// more bytes than are in the second. This restriction exists because `.eh_frame_hdr` only
|
||||
/// includes the address of the loaded `.eh_frame` data, not its length. It is not a problem
|
||||
/// because unlike `.debug_frame`, the end of the CIE/FDE list is signaled through a sentinel
|
||||
/// value. If this slice does have bounds, they will still be checked, preventing crashes when
|
||||
/// reading potentially-invalid `.eh_frame` data from files.
|
||||
bytes: []const u8,
|
||||
},
|
||||
|
||||
/// A structure allowing fast lookups of the FDE corresponding to a particular PC. We use a binary
|
||||
/// search table for the lookup; essentially, a list of all FDEs ordered by PC range. `null` means
|
||||
/// the lookup data is not yet populated, so `prepare` must be called before `lookupPc`.
|
||||
lookup: ?union(enum) {
|
||||
/// The `.eh_frame_hdr` section contains a pre-computed search table which we can use.
|
||||
eh_frame_hdr: struct {
|
||||
/// Virtual address of the `.eh_frame_hdr` section.
|
||||
vaddr: u64,
|
||||
table: EhFrameHeader.SearchTable,
|
||||
},
|
||||
/// There is no pre-computed search table, so we have built one ourselves.
|
||||
/// Allocated into `gpa` and freed by `deinit`.
|
||||
sorted_fdes: []SortedFdeEntry,
|
||||
},
|
||||
|
||||
/// Initially empty; populated by `prepare`.
|
||||
cie_list: std.MultiArrayList(struct {
|
||||
offset: u64,
|
||||
cie: CommonInformationEntry,
|
||||
}),
|
||||
|
||||
const SortedFdeEntry = struct {
|
||||
/// This FDE's value of `pc_begin`.
|
||||
pc_begin: u64,
|
||||
/// Offset into the section of the corresponding FDE, including the entry header.
|
||||
fde_offset: u64,
|
||||
};
|
||||
|
||||
pub const Section = enum { debug_frame, eh_frame };
|
||||
|
||||
/// Initialize with unwind information from a header loaded from an `.eh_frame_hdr` section, and a
|
||||
/// pointer to the contents of the `.eh_frame` section.
|
||||
///
|
||||
/// `.eh_frame_hdr` may embed a binary search table of FDEs. If it does, we will use that table for
|
||||
/// PC lookups rather than spending time constructing our own search table.
|
||||
pub fn initEhFrameHdr(header: EhFrameHeader, section_vaddr: u64, section_bytes_ptr: [*]const u8) Unwind {
|
||||
return .{
|
||||
.frame_section = .{
|
||||
.id = .eh_frame,
|
||||
.bytes = maxSlice(section_bytes_ptr),
|
||||
.vaddr = header.eh_frame_vaddr,
|
||||
},
|
||||
.lookup = if (header.search_table) |table| .{ .eh_frame_hdr = .{
|
||||
.vaddr = section_vaddr,
|
||||
.table = table,
|
||||
} } else null,
|
||||
.cie_list = .empty,
|
||||
};
|
||||
}
|
||||
|
||||
/// Initialize with unwind information from the contents of a `.debug_frame` or `.eh_frame` section.
|
||||
///
|
||||
/// If the `.eh_frame_hdr` section is available, consider instead using `initEhFrameHdr`, which
|
||||
/// allows the implementation to use a search table embedded in that section if it is available.
|
||||
pub fn initSection(section: Section, section_vaddr: u64, section_bytes: []const u8) Unwind {
|
||||
return .{
|
||||
.frame_section = .{
|
||||
.id = section,
|
||||
.bytes = section_bytes,
|
||||
.vaddr = section_vaddr,
|
||||
},
|
||||
.lookup = null,
|
||||
.cie_list = .empty,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(unwind: *Unwind, gpa: Allocator) void {
|
||||
if (unwind.lookup) |lookup| switch (lookup) {
|
||||
.eh_frame_hdr => {},
|
||||
.sorted_fdes => |fdes| gpa.free(fdes),
|
||||
};
|
||||
for (unwind.cie_list.items(.cie)) |*cie| {
|
||||
if (cie.last_row) |*lr| {
|
||||
gpa.free(lr.cols);
|
||||
}
|
||||
}
|
||||
unwind.cie_list.deinit(gpa);
|
||||
}
|
||||
|
||||
/// Decoded version of the `.eh_frame_hdr` section.
|
||||
pub const EhFrameHeader = struct {
|
||||
/// The virtual address (i.e. as given in the binary, before relocations) of the `.eh_frame`
|
||||
/// section. This value is important when using `.eh_frame_hdr` to find debug information for
|
||||
/// the current binary, because it allows locating where the `.eh_frame` section is loaded in
|
||||
/// memory (by adding it to the ELF module's base address).
|
||||
eh_frame_vaddr: u64,
|
||||
search_table: ?SearchTable,
|
||||
|
||||
pub const SearchTable = struct {
|
||||
/// The byte offset of the search table into the `.eh_frame_hdr` section.
|
||||
offset: u8,
|
||||
encoding: EH_PE,
|
||||
fde_count: usize,
|
||||
/// The actual table entries are viewed as a plain byte slice because `encoding` causes the
|
||||
/// size of entries in the table to vary.
|
||||
entries: []const u8,
|
||||
|
||||
/// Returns the vaddr of the FDE for `pc`, or `null` if no matching FDE was found.
|
||||
fn findEntry(
|
||||
table: *const SearchTable,
|
||||
eh_frame_hdr_vaddr: u64,
|
||||
pc: u64,
|
||||
addr_size_bytes: u8,
|
||||
endian: Endian,
|
||||
) !?u64 {
|
||||
const table_vaddr = eh_frame_hdr_vaddr + table.offset;
|
||||
const entry_size = try entrySize(table.encoding, addr_size_bytes);
|
||||
var left: usize = 0;
|
||||
var len: usize = table.fde_count;
|
||||
while (len > 1) {
|
||||
const mid = left + len / 2;
|
||||
var entry_reader: Reader = .fixed(table.entries[mid * entry_size ..][0..entry_size]);
|
||||
const pc_begin = try readEhPointer(&entry_reader, table.encoding, addr_size_bytes, .{
|
||||
.pc_rel_base = table_vaddr + left * entry_size,
|
||||
.data_rel_base = eh_frame_hdr_vaddr,
|
||||
}, endian);
|
||||
if (pc < pc_begin) {
|
||||
len /= 2;
|
||||
} else {
|
||||
left = mid;
|
||||
len -= len / 2;
|
||||
}
|
||||
}
|
||||
if (len == 0) return null;
|
||||
var entry_reader: Reader = .fixed(table.entries[left * entry_size ..][0..entry_size]);
|
||||
// Skip past `pc_begin`; we're now interested in the fde offset
|
||||
_ = try readEhPointerAbs(&entry_reader, table.encoding.type, addr_size_bytes, endian);
|
||||
const fde_ptr = try readEhPointer(&entry_reader, table.encoding, addr_size_bytes, .{
|
||||
.pc_rel_base = table_vaddr + left * entry_size,
|
||||
.data_rel_base = eh_frame_hdr_vaddr,
|
||||
}, endian);
|
||||
return fde_ptr;
|
||||
}
|
||||
|
||||
fn entrySize(table_enc: EH_PE, addr_size_bytes: u8) !u8 {
|
||||
return switch (table_enc.type) {
|
||||
.absptr => 2 * addr_size_bytes,
|
||||
.udata2, .sdata2 => 4,
|
||||
.udata4, .sdata4 => 8,
|
||||
.udata8, .sdata8 => 16,
|
||||
.uleb128, .sleb128 => return bad(), // this is a binary search table; all entries must be the same size
|
||||
_ => return bad(),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub fn parse(
|
||||
eh_frame_hdr_vaddr: u64,
|
||||
eh_frame_hdr_bytes: []const u8,
|
||||
addr_size_bytes: u8,
|
||||
endian: Endian,
|
||||
) !EhFrameHeader {
|
||||
var r: Reader = .fixed(eh_frame_hdr_bytes);
|
||||
|
||||
const version = try r.takeByte();
|
||||
if (version != 1) return bad();
|
||||
|
||||
const eh_frame_ptr_enc: EH_PE = @bitCast(try r.takeByte());
|
||||
const fde_count_enc: EH_PE = @bitCast(try r.takeByte());
|
||||
const table_enc: EH_PE = @bitCast(try r.takeByte());
|
||||
|
||||
const eh_frame_ptr = try readEhPointer(&r, eh_frame_ptr_enc, addr_size_bytes, .{
|
||||
.pc_rel_base = eh_frame_hdr_vaddr + r.seek,
|
||||
}, endian);
|
||||
|
||||
const table: ?SearchTable = table: {
|
||||
if (fde_count_enc == EH_PE.omit) break :table null;
|
||||
if (table_enc == EH_PE.omit) break :table null;
|
||||
const fde_count = try readEhPointer(&r, fde_count_enc, addr_size_bytes, .{
|
||||
.pc_rel_base = eh_frame_hdr_vaddr + r.seek,
|
||||
}, endian);
|
||||
const entry_size = try SearchTable.entrySize(table_enc, addr_size_bytes);
|
||||
const bytes_offset = r.seek;
|
||||
const bytes_len = cast(usize, fde_count * entry_size) orelse return error.EndOfStream;
|
||||
const bytes = try r.take(bytes_len);
|
||||
break :table .{
|
||||
.encoding = table_enc,
|
||||
.fde_count = @intCast(fde_count),
|
||||
.entries = bytes,
|
||||
.offset = @intCast(bytes_offset),
|
||||
};
|
||||
};
|
||||
|
||||
return .{
|
||||
.eh_frame_vaddr = eh_frame_ptr,
|
||||
.search_table = table,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// The shared header of an FDE/CIE, containing a length in bytes (DWARF's "initial length field")
|
||||
/// and a value which differentiates CIEs from FDEs and maps FDEs to their corresponding CIEs. The
|
||||
/// `.eh_frame` format also includes a third variation, here called `.terminator`, which acts as a
|
||||
/// sentinel for the whole section.
|
||||
///
|
||||
/// `CommonInformationEntry.parse` and `FrameDescriptionEntry.parse` expect the `EntryHeader` to
|
||||
/// have been parsed first: they accept data stored in the `EntryHeader`, and only read the bytes
|
||||
/// following this header.
|
||||
const EntryHeader = union(enum) {
|
||||
cie: struct {
|
||||
format: Format,
|
||||
/// Remaining bytes in the CIE. These are parseable by `CommonInformationEntry.parse`.
|
||||
bytes_len: u64,
|
||||
},
|
||||
fde: struct {
|
||||
/// Offset into the section of the corresponding CIE, *including* its entry header.
|
||||
cie_offset: u64,
|
||||
/// Remaining bytes in the FDE. These are parseable by `FrameDescriptionEntry.parse`.
|
||||
bytes_len: u64,
|
||||
},
|
||||
/// The `.eh_frame` format includes terminators which indicate that the last CIE/FDE has been
|
||||
/// reached. However, `.debug_frame` does not include such a terminator, so the caller must
|
||||
/// keep track of how many section bytes remain when parsing all entries in `.debug_frame`.
|
||||
terminator,
|
||||
|
||||
fn read(r: *Reader, header_section_offset: u64, section: Section, endian: Endian) !EntryHeader {
|
||||
const unit_header = try Dwarf.readUnitHeader(r, endian);
|
||||
if (unit_header.unit_length == 0) return .terminator;
|
||||
|
||||
// Next is a value which will disambiguate CIEs and FDEs. Annoyingly, LSB Core makes this
|
||||
// value always 4-byte, whereas DWARF makes it depend on the `dwarf.Format`.
|
||||
const cie_ptr_or_id_size: u8 = switch (section) {
|
||||
.eh_frame => 4,
|
||||
.debug_frame => switch (unit_header.format) {
|
||||
.@"32" => 4,
|
||||
.@"64" => 8,
|
||||
},
|
||||
};
|
||||
const cie_ptr_or_id = switch (cie_ptr_or_id_size) {
|
||||
4 => try r.takeInt(u32, endian),
|
||||
8 => try r.takeInt(u64, endian),
|
||||
else => unreachable,
|
||||
};
|
||||
const remaining_bytes = unit_header.unit_length - cie_ptr_or_id_size;
|
||||
|
||||
// If this entry is a CIE, then `cie_ptr_or_id` will have this value, which is different
|
||||
// between the DWARF `.debug_frame` section and the LSB Core `.eh_frame` section.
|
||||
const cie_id: u64 = switch (section) {
|
||||
.eh_frame => 0,
|
||||
.debug_frame => switch (unit_header.format) {
|
||||
.@"32" => maxInt(u32),
|
||||
.@"64" => maxInt(u64),
|
||||
},
|
||||
};
|
||||
if (cie_ptr_or_id == cie_id) {
|
||||
return .{ .cie = .{
|
||||
.format = unit_header.format,
|
||||
.bytes_len = remaining_bytes,
|
||||
} };
|
||||
}
|
||||
|
||||
// This is an FDE -- `cie_ptr_or_id` points to the associated CIE. Unfortunately, the format
|
||||
// of that pointer again differs between `.debug_frame` and `.eh_frame`.
|
||||
const cie_offset = switch (section) {
|
||||
.eh_frame => try std.math.sub(u64, header_section_offset + unit_header.header_length, cie_ptr_or_id),
|
||||
.debug_frame => cie_ptr_or_id,
|
||||
};
|
||||
return .{ .fde = .{
|
||||
.cie_offset = cie_offset,
|
||||
.bytes_len = remaining_bytes,
|
||||
} };
|
||||
}
|
||||
};
|
||||
|
||||
pub const CommonInformationEntry = struct {
|
||||
version: u8,
|
||||
format: Format,
|
||||
|
||||
/// In version 4, CIEs can specify the address size used in the CIE and associated FDEs.
|
||||
/// This value must be used *only* to parse associated FDEs in `FrameDescriptionEntry.parse`.
|
||||
addr_size_bytes: u8,
|
||||
|
||||
/// Always 0 for versions which do not specify this (currently all versions other than 4).
|
||||
segment_selector_size: u8,
|
||||
|
||||
code_alignment_factor: u32,
|
||||
data_alignment_factor: i32,
|
||||
return_address_register: u8,
|
||||
|
||||
fde_pointer_enc: EH_PE,
|
||||
is_signal_frame: bool,
|
||||
|
||||
augmentation_kind: AugmentationKind,
|
||||
|
||||
initial_instructions: []const u8,
|
||||
|
||||
last_row: ?struct {
|
||||
offset: u64,
|
||||
cfa: VirtualMachine.CfaRule,
|
||||
cols: []VirtualMachine.Column,
|
||||
},
|
||||
|
||||
pub const AugmentationKind = enum { none, gcc_eh, lsb_z };
|
||||
|
||||
/// This function expects to read the CIE starting with the version field.
|
||||
/// The returned struct references memory backed by `cie_bytes`.
|
||||
///
|
||||
/// `length_offset` specifies the offset of this CIE's length field in the
|
||||
/// .eh_frame / .debug_frame section.
|
||||
fn parse(
|
||||
format: Format,
|
||||
cie_bytes: []const u8,
|
||||
section: Section,
|
||||
default_addr_size_bytes: u8,
|
||||
) !CommonInformationEntry {
|
||||
// We only read the data through this reader.
|
||||
var r: Reader = .fixed(cie_bytes);
|
||||
|
||||
const version = try r.takeByte();
|
||||
switch (section) {
|
||||
.eh_frame => if (version != 1 and version != 3) return error.UnsupportedDwarfVersion,
|
||||
.debug_frame => if (version != 4) return error.UnsupportedDwarfVersion,
|
||||
}
|
||||
|
||||
const aug_str = try r.takeSentinel(0);
|
||||
const aug_kind: AugmentationKind = aug: {
|
||||
if (aug_str.len == 0) break :aug .none;
|
||||
if (aug_str[0] == 'z') break :aug .lsb_z;
|
||||
if (std.mem.eql(u8, aug_str, "eh")) break :aug .gcc_eh;
|
||||
// We can't finish parsing the CIE if we don't know what its augmentation means.
|
||||
return bad();
|
||||
};
|
||||
|
||||
switch (aug_kind) {
|
||||
.none => {}, // no extra data
|
||||
.lsb_z => {}, // no extra data yet, but there is a bit later
|
||||
.gcc_eh => try r.discardAll(default_addr_size_bytes), // unsupported data
|
||||
}
|
||||
|
||||
const addr_size_bytes = if (version == 4) try r.takeByte() else default_addr_size_bytes;
|
||||
const segment_selector_size: u8 = if (version == 4) try r.takeByte() else 0;
|
||||
const code_alignment_factor = try r.takeLeb128(u32);
|
||||
const data_alignment_factor = try r.takeLeb128(i32);
|
||||
const return_address_register = if (version == 1) try r.takeByte() else try r.takeLeb128(u8);
|
||||
|
||||
// This is where LSB's augmentation might add some data.
|
||||
const fde_pointer_enc: EH_PE, const is_signal_frame: bool = aug: {
|
||||
const default_fde_pointer_enc: EH_PE = .{ .type = .absptr, .rel = .abs };
|
||||
if (aug_kind != .lsb_z) break :aug .{ default_fde_pointer_enc, false };
|
||||
const aug_data_len = try r.takeLeb128(u32);
|
||||
var aug_data: Reader = .fixed(try r.take(aug_data_len));
|
||||
var fde_pointer_enc: EH_PE = default_fde_pointer_enc;
|
||||
var is_signal_frame = false;
|
||||
for (aug_str[1..]) |byte| switch (byte) {
|
||||
'L' => _ = try aug_data.takeByte(), // we ignore the LSDA pointer
|
||||
'P' => {
|
||||
const enc: EH_PE = @bitCast(try aug_data.takeByte());
|
||||
const endian: Endian = .little; // irrelevant because we're discarding the value anyway
|
||||
_ = try readEhPointerAbs(&aug_data, enc.type, addr_size_bytes, endian); // we ignore the personality routine; endianness is irrelevant since we're discarding
|
||||
},
|
||||
'R' => fde_pointer_enc = @bitCast(try aug_data.takeByte()),
|
||||
'S' => is_signal_frame = true,
|
||||
'B', 'G' => {},
|
||||
else => return bad(),
|
||||
};
|
||||
break :aug .{ fde_pointer_enc, is_signal_frame };
|
||||
};
|
||||
|
||||
return .{
|
||||
.format = format,
|
||||
.version = version,
|
||||
.addr_size_bytes = addr_size_bytes,
|
||||
.segment_selector_size = segment_selector_size,
|
||||
.code_alignment_factor = code_alignment_factor,
|
||||
.data_alignment_factor = data_alignment_factor,
|
||||
.return_address_register = return_address_register,
|
||||
.fde_pointer_enc = fde_pointer_enc,
|
||||
.is_signal_frame = is_signal_frame,
|
||||
.augmentation_kind = aug_kind,
|
||||
.initial_instructions = r.buffered(),
|
||||
.last_row = null,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const FrameDescriptionEntry = struct {
|
||||
pc_begin: u64,
|
||||
pc_range: u64,
|
||||
instructions: []const u8,
|
||||
|
||||
/// This function expects to read the FDE starting at the PC Begin field.
|
||||
/// The returned struct references memory backed by `fde_bytes`.
|
||||
fn parse(
|
||||
/// The virtual address of the FDE we're parsing, *excluding* its entry header (i.e. the
|
||||
/// address is after the header). If `fde_bytes` is backed by the memory of a loaded
|
||||
/// module's `.eh_frame` section, this will equal `fde_bytes.ptr`.
|
||||
fde_vaddr: u64,
|
||||
fde_bytes: []const u8,
|
||||
cie: *const CommonInformationEntry,
|
||||
endian: Endian,
|
||||
) !FrameDescriptionEntry {
|
||||
if (cie.segment_selector_size != 0) return error.UnsupportedAddrSize;
|
||||
|
||||
var r: Reader = .fixed(fde_bytes);
|
||||
|
||||
const pc_begin = try readEhPointer(&r, cie.fde_pointer_enc, cie.addr_size_bytes, .{
|
||||
.pc_rel_base = fde_vaddr,
|
||||
}, endian);
|
||||
|
||||
// I swear I'm not kidding when I say that PC Range is encoded with `cie.fde_pointer_enc`, but ignoring `rel`.
|
||||
const pc_range = switch (try readEhPointerAbs(&r, cie.fde_pointer_enc.type, cie.addr_size_bytes, endian)) {
|
||||
.unsigned => |x| x,
|
||||
.signed => |x| cast(u64, x) orelse return bad(),
|
||||
};
|
||||
|
||||
switch (cie.augmentation_kind) {
|
||||
.none, .gcc_eh => {},
|
||||
.lsb_z => {
|
||||
// There is augmentation data, but it's irrelevant to us -- it
|
||||
// only contains the LSDA pointer, which we don't care about.
|
||||
const aug_data_len = try r.takeLeb128(usize);
|
||||
_ = try r.discardAll(aug_data_len);
|
||||
},
|
||||
}
|
||||
|
||||
return .{
|
||||
.pc_begin = pc_begin,
|
||||
.pc_range = pc_range,
|
||||
.instructions = r.buffered(),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// Builds the CIE list and FDE lookup table if they are not already built. It is required to call
|
||||
/// this function at least once before calling `lookupPc` or `getFde`. If only `getFde` is needed,
|
||||
/// then `need_lookup` can be set to `false` to make this function more efficient.
|
||||
pub fn prepare(
|
||||
unwind: *Unwind,
|
||||
gpa: Allocator,
|
||||
addr_size_bytes: u8,
|
||||
endian: Endian,
|
||||
need_lookup: bool,
|
||||
/// The `__eh_frame` section in Mach-O binaries deviates from the standard `.eh_frame` section
|
||||
/// in one way which this function needs to be aware of.
|
||||
is_macho: bool,
|
||||
) !void {
|
||||
if (unwind.cie_list.len > 0 and (!need_lookup or unwind.lookup != null)) return;
|
||||
unwind.cie_list.clearRetainingCapacity();
|
||||
|
||||
if (is_macho) assert(unwind.lookup == null or unwind.lookup.? != .eh_frame_hdr);
|
||||
|
||||
const section = unwind.frame_section;
|
||||
|
||||
var r: Reader = .fixed(section.bytes);
|
||||
var fde_list: std.ArrayList(SortedFdeEntry) = .empty;
|
||||
defer fde_list.deinit(gpa);
|
||||
|
||||
const saw_terminator = while (r.seek < r.buffer.len) {
|
||||
const entry_offset = r.seek;
|
||||
switch (try EntryHeader.read(&r, entry_offset, section.id, endian)) {
|
||||
.cie => |cie_info| {
|
||||
// We will pre-populate a list of CIEs for efficiency: this avoids work re-parsing
|
||||
// them every time we look up an FDE. It also lets us cache the result of evaluating
|
||||
// the CIE's initial CFI instructions, which is useful because in the vast majority
|
||||
// of cases those instructions will be needed to reach the PC we are unwinding to.
|
||||
const bytes_len = cast(usize, cie_info.bytes_len) orelse return error.EndOfStream;
|
||||
const idx = unwind.cie_list.len;
|
||||
try unwind.cie_list.append(gpa, .{
|
||||
.offset = entry_offset,
|
||||
.cie = try .parse(cie_info.format, try r.take(bytes_len), section.id, addr_size_bytes),
|
||||
});
|
||||
errdefer _ = unwind.cie_list.pop().?;
|
||||
try VirtualMachine.populateCieLastRow(gpa, &unwind.cie_list.items(.cie)[idx], addr_size_bytes, endian);
|
||||
continue;
|
||||
},
|
||||
.fde => |fde_info| {
|
||||
const bytes_len = cast(usize, fde_info.bytes_len) orelse return error.EndOfStream;
|
||||
if (!need_lookup) {
|
||||
try r.discardAll(bytes_len);
|
||||
continue;
|
||||
}
|
||||
const cie = unwind.findCie(fde_info.cie_offset) orelse return error.InvalidDebugInfo;
|
||||
const fde: FrameDescriptionEntry = try .parse(section.vaddr + r.seek, try r.take(bytes_len), cie, endian);
|
||||
try fde_list.append(gpa, .{
|
||||
.pc_begin = fde.pc_begin,
|
||||
.fde_offset = entry_offset,
|
||||
});
|
||||
},
|
||||
.terminator => break true,
|
||||
}
|
||||
} else false;
|
||||
const expect_terminator = switch (section.id) {
|
||||
.eh_frame => !is_macho, // `.eh_frame` indicates the end of the CIE/FDE list with a sentinel entry, though macOS omits this
|
||||
.debug_frame => false, // `.debug_frame` uses the section bounds and does not specify a sentinel entry
|
||||
};
|
||||
if (saw_terminator != expect_terminator) return bad();
|
||||
|
||||
if (need_lookup) {
|
||||
std.mem.sortUnstable(SortedFdeEntry, fde_list.items, {}, struct {
|
||||
fn lessThan(ctx: void, a: SortedFdeEntry, b: SortedFdeEntry) bool {
|
||||
ctx;
|
||||
return a.pc_begin < b.pc_begin;
|
||||
}
|
||||
}.lessThan);
|
||||
|
||||
// This temporary is necessary to avoid an RLS footgun where `lookup` ends up non-null `undefined` on OOM.
|
||||
const final_fdes = try fde_list.toOwnedSlice(gpa);
|
||||
unwind.lookup = .{ .sorted_fdes = final_fdes };
|
||||
}
|
||||
}
|
||||
|
||||
fn findCie(unwind: *const Unwind, offset: u64) ?*const CommonInformationEntry {
|
||||
const offsets = unwind.cie_list.items(.offset);
|
||||
if (offsets.len == 0) return null;
|
||||
var start: usize = 0;
|
||||
var len: usize = offsets.len;
|
||||
while (len > 1) {
|
||||
const mid = len / 2;
|
||||
if (offset < offsets[start + mid]) {
|
||||
len = mid;
|
||||
} else {
|
||||
start += mid;
|
||||
len -= mid;
|
||||
}
|
||||
}
|
||||
if (offsets[start] != offset) return null;
|
||||
return &unwind.cie_list.items(.cie)[start];
|
||||
}
|
||||
|
||||
/// Given a program counter value, returns the offset of the corresponding FDE, or `null` if no
|
||||
/// matching FDE was found. The returned offset can be passed to `getFde` to load the data
|
||||
/// associated with the FDE.
|
||||
///
|
||||
/// Before calling this function, `prepare` must return successfully at least once, to ensure that
|
||||
/// `unwind.lookup` is populated.
|
||||
///
|
||||
/// The return value may be a false positive. After loading the FDE with `loadFde`, the caller must
|
||||
/// validate that `pc` is indeed in its range -- if it is not, then no FDE matches `pc`.
|
||||
pub fn lookupPc(unwind: *const Unwind, pc: u64, addr_size_bytes: u8, endian: Endian) !?u64 {
|
||||
const sorted_fdes: []const SortedFdeEntry = switch (unwind.lookup.?) {
|
||||
.eh_frame_hdr => |eh_frame_hdr| {
|
||||
const fde_vaddr = try eh_frame_hdr.table.findEntry(
|
||||
eh_frame_hdr.vaddr,
|
||||
pc,
|
||||
addr_size_bytes,
|
||||
endian,
|
||||
) orelse return null;
|
||||
return std.math.sub(u64, fde_vaddr, unwind.frame_section.vaddr) catch bad(); // convert vaddr to offset
|
||||
},
|
||||
.sorted_fdes => |sorted_fdes| sorted_fdes,
|
||||
};
|
||||
if (sorted_fdes.len == 0) return null;
|
||||
var start: usize = 0;
|
||||
var len: usize = sorted_fdes.len;
|
||||
while (len > 1) {
|
||||
const half = len / 2;
|
||||
if (pc < sorted_fdes[start + half].pc_begin) {
|
||||
len = half;
|
||||
} else {
|
||||
start += half;
|
||||
len -= half;
|
||||
}
|
||||
}
|
||||
// If any FDE matches, it'll be the one at `start` (maybe false positive).
|
||||
return sorted_fdes[start].fde_offset;
|
||||
}
|
||||
|
||||
/// Get the FDE at a given offset, as well as its associated CIE. This offset typically comes from
|
||||
/// `lookupPc`. The CFI instructions within can be evaluated with `VirtualMachine`.
|
||||
pub fn getFde(unwind: *const Unwind, fde_offset: u64, endian: Endian) !struct { *const CommonInformationEntry, FrameDescriptionEntry } {
|
||||
const section = unwind.frame_section;
|
||||
|
||||
if (fde_offset > section.bytes.len) return error.EndOfStream;
|
||||
var fde_reader: Reader = .fixed(section.bytes[@intCast(fde_offset)..]);
|
||||
const fde_info = switch (try EntryHeader.read(&fde_reader, fde_offset, section.id, endian)) {
|
||||
.fde => |info| info,
|
||||
.cie, .terminator => return bad(), // This is meant to be an FDE
|
||||
};
|
||||
|
||||
const cie = unwind.findCie(fde_info.cie_offset) orelse return error.InvalidDebugInfo;
|
||||
const fde: FrameDescriptionEntry = try .parse(
|
||||
section.vaddr + fde_offset + fde_reader.seek,
|
||||
try fde_reader.take(cast(usize, fde_info.bytes_len) orelse return error.EndOfStream),
|
||||
cie,
|
||||
endian,
|
||||
);
|
||||
|
||||
return .{ cie, fde };
|
||||
}
|
||||
|
||||
const EhPointerContext = struct {
|
||||
/// The address of the pointer field itself
|
||||
pc_rel_base: u64,
|
||||
// These relative addressing modes are only used in specific cases, and
|
||||
// might not be available / required in all parsing contexts
|
||||
data_rel_base: ?u64 = null,
|
||||
text_rel_base: ?u64 = null,
|
||||
function_rel_base: ?u64 = null,
|
||||
};
|
||||
/// Returns `error.InvalidDebugInfo` if the encoding is `EH.PE.omit`.
|
||||
fn readEhPointerAbs(r: *Reader, enc_ty: EH_PE.Type, addr_size_bytes: u8, endian: Endian) !union(enum) {
|
||||
signed: i64,
|
||||
unsigned: u64,
|
||||
} {
|
||||
return switch (enc_ty) {
|
||||
.absptr => .{
|
||||
.unsigned = switch (addr_size_bytes) {
|
||||
2 => try r.takeInt(u16, endian),
|
||||
4 => try r.takeInt(u32, endian),
|
||||
8 => try r.takeInt(u64, endian),
|
||||
else => return error.UnsupportedAddrSize,
|
||||
},
|
||||
},
|
||||
.uleb128 => .{ .unsigned = try r.takeLeb128(u64) },
|
||||
.udata2 => .{ .unsigned = try r.takeInt(u16, endian) },
|
||||
.udata4 => .{ .unsigned = try r.takeInt(u32, endian) },
|
||||
.udata8 => .{ .unsigned = try r.takeInt(u64, endian) },
|
||||
.sleb128 => .{ .signed = try r.takeLeb128(i64) },
|
||||
.sdata2 => .{ .signed = try r.takeInt(i16, endian) },
|
||||
.sdata4 => .{ .signed = try r.takeInt(i32, endian) },
|
||||
.sdata8 => .{ .signed = try r.takeInt(i64, endian) },
|
||||
else => return bad(),
|
||||
};
|
||||
}
|
||||
/// Returns `error.InvalidDebugInfo` if the encoding is `EH.PE.omit`.
|
||||
fn readEhPointer(r: *Reader, enc: EH_PE, addr_size_bytes: u8, ctx: EhPointerContext, endian: Endian) !u64 {
|
||||
const offset = try readEhPointerAbs(r, enc.type, addr_size_bytes, endian);
|
||||
if (enc.indirect) return bad(); // GCC extension; not supported
|
||||
const base: u64 = switch (enc.rel) {
|
||||
.abs, .aligned => 0,
|
||||
.pcrel => ctx.pc_rel_base,
|
||||
.textrel => ctx.text_rel_base orelse return bad(),
|
||||
.datarel => ctx.data_rel_base orelse return bad(),
|
||||
.funcrel => ctx.function_rel_base orelse return bad(),
|
||||
_ => return bad(),
|
||||
};
|
||||
return switch (offset) {
|
||||
.signed => |s| if (s >= 0)
|
||||
try std.math.add(u64, base, @intCast(s))
|
||||
else
|
||||
try std.math.sub(u64, base, @intCast(-s)),
|
||||
// absptr can actually contain signed values in some cases (aarch64 MachO)
|
||||
.unsigned => |u| u +% base,
|
||||
};
|
||||
}
|
||||
|
||||
/// Like `Reader.fixed`, but when the length of the data is unknown and we just want to allow
|
||||
/// reading indefinitely.
|
||||
fn maxSlice(ptr: [*]const u8) []const u8 {
|
||||
const len = std.math.maxInt(usize) - @intFromPtr(ptr);
|
||||
return ptr[0..len];
|
||||
}
|
||||
|
||||
pub const EH_PE = packed struct(u8) {
|
||||
type: Type,
|
||||
rel: Rel,
|
||||
/// Undocumented GCC extension
|
||||
indirect: bool = false,
|
||||
|
||||
/// This is a special encoding which does not correspond to named `type`/`rel` values.
|
||||
pub const omit: EH_PE = @bitCast(@as(u8, 0xFF));
|
||||
|
||||
pub const Type = enum(u4) {
|
||||
absptr = 0x0,
|
||||
uleb128 = 0x1,
|
||||
udata2 = 0x2,
|
||||
udata4 = 0x3,
|
||||
udata8 = 0x4,
|
||||
sleb128 = 0x9,
|
||||
sdata2 = 0xA,
|
||||
sdata4 = 0xB,
|
||||
sdata8 = 0xC,
|
||||
_,
|
||||
};
|
||||
|
||||
/// The specification considers this a `u4`, but the GCC `indirect` field extension conflicts
|
||||
/// with that, so we consider it a `u3` instead.
|
||||
pub const Rel = enum(u3) {
|
||||
abs = 0x0,
|
||||
pcrel = 0x1,
|
||||
textrel = 0x2,
|
||||
datarel = 0x3,
|
||||
funcrel = 0x4,
|
||||
aligned = 0x5,
|
||||
_,
|
||||
};
|
||||
};
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
const assert = std.debug.assert;
|
||||
const bad = Dwarf.bad;
|
||||
const cast = std.math.cast;
|
||||
const DW = std.dwarf;
|
||||
const debug = @import("../../new_debug.zig");
|
||||
const Dwarf = debug.Dwarf;
|
||||
const EH = DW.EH;
|
||||
const Endian = std.builtin.Endian;
|
||||
const Format = DW.Format;
|
||||
const maxInt = std.math.maxInt;
|
||||
const missing = Dwarf.missing;
|
||||
const Reader = std.Io.Reader;
|
||||
const std = @import("std");
|
||||
const Unwind = @This();
|
||||
472
src/new_debug/Dwarf/Unwind/VirtualMachine.zig
Normal file
472
src/new_debug/Dwarf/Unwind/VirtualMachine.zig
Normal file
@@ -0,0 +1,472 @@
|
||||
//! Virtual machine that evaluates DWARF call frame instructions
|
||||
|
||||
/// See section 6.4.1 of the DWARF5 specification for details on each
|
||||
pub const RegisterRule = union(enum) {
|
||||
/// The spec says that the default rule for each column is the undefined rule.
|
||||
/// However, it also allows ABI / compiler authors to specify alternate defaults, so
|
||||
/// there is a distinction made here.
|
||||
default,
|
||||
undefined,
|
||||
same_value,
|
||||
/// offset(N)
|
||||
offset: i64,
|
||||
/// val_offset(N)
|
||||
val_offset: i64,
|
||||
/// register(R)
|
||||
register: u8,
|
||||
/// expression(E)
|
||||
expression: []const u8,
|
||||
/// val_expression(E)
|
||||
val_expression: []const u8,
|
||||
};
|
||||
|
||||
pub const CfaRule = union(enum) {
|
||||
none,
|
||||
reg_off: struct {
|
||||
register: u8,
|
||||
offset: i64,
|
||||
},
|
||||
expression: []const u8,
|
||||
};
|
||||
|
||||
/// Each row contains unwinding rules for a set of registers.
|
||||
pub const Row = struct {
|
||||
/// Offset from `FrameDescriptionEntry.pc_begin`
|
||||
offset: u64 = 0,
|
||||
cfa: CfaRule = .none,
|
||||
/// The register fields in these columns define the register the rule applies to.
|
||||
columns: ColumnRange = .{ .start = undefined, .len = 0 },
|
||||
};
|
||||
|
||||
pub const Column = struct {
|
||||
register: u8,
|
||||
rule: RegisterRule,
|
||||
};
|
||||
|
||||
const ColumnRange = struct {
|
||||
start: usize,
|
||||
len: u8,
|
||||
};
|
||||
|
||||
columns: std.ArrayList(Column) = .empty,
|
||||
stack: std.ArrayList(struct {
|
||||
cfa: CfaRule,
|
||||
columns: ColumnRange,
|
||||
}) = .empty,
|
||||
current_row: Row = .{},
|
||||
|
||||
/// The result of executing the CIE's initial_instructions
|
||||
cie_row: ?Row = null,
|
||||
|
||||
pub fn deinit(self: *VirtualMachine, gpa: Allocator) void {
|
||||
self.stack.deinit(gpa);
|
||||
self.columns.deinit(gpa);
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
pub fn reset(self: *VirtualMachine) void {
|
||||
self.stack.clearRetainingCapacity();
|
||||
self.columns.clearRetainingCapacity();
|
||||
self.current_row = .{};
|
||||
self.cie_row = null;
|
||||
}
|
||||
|
||||
/// Return a slice backed by the row's non-CFA columns
|
||||
pub fn rowColumns(self: *const VirtualMachine, row: *const Row) []Column {
|
||||
if (row.columns.len == 0) return &.{};
|
||||
return self.columns.items[row.columns.start..][0..row.columns.len];
|
||||
}
|
||||
|
||||
/// Either retrieves or adds a column for `register` (non-CFA) in the current row.
|
||||
fn getOrAddColumn(self: *VirtualMachine, gpa: Allocator, register: u8) !*Column {
|
||||
for (self.rowColumns(&self.current_row)) |*c| {
|
||||
if (c.register == register) return c;
|
||||
}
|
||||
|
||||
if (self.current_row.columns.len == 0) {
|
||||
self.current_row.columns.start = self.columns.items.len;
|
||||
} else {
|
||||
assert(self.current_row.columns.start + self.current_row.columns.len == self.columns.items.len);
|
||||
}
|
||||
self.current_row.columns.len += 1;
|
||||
|
||||
const column = try self.columns.addOne(gpa);
|
||||
column.* = .{
|
||||
.register = register,
|
||||
.rule = .default,
|
||||
};
|
||||
|
||||
return column;
|
||||
}
|
||||
|
||||
pub fn populateCieLastRow(
|
||||
gpa: Allocator,
|
||||
cie: *Unwind.CommonInformationEntry,
|
||||
addr_size_bytes: u8,
|
||||
endian: std.builtin.Endian,
|
||||
) !void {
|
||||
assert(cie.last_row == null);
|
||||
|
||||
var vm: VirtualMachine = .{};
|
||||
defer vm.deinit(gpa);
|
||||
|
||||
try vm.evalInstructions(
|
||||
gpa,
|
||||
cie,
|
||||
std.math.maxInt(u64),
|
||||
cie.initial_instructions,
|
||||
addr_size_bytes,
|
||||
endian,
|
||||
);
|
||||
|
||||
cie.last_row = .{
|
||||
.offset = vm.current_row.offset,
|
||||
.cfa = vm.current_row.cfa,
|
||||
.cols = try gpa.dupe(Column, vm.rowColumns(&vm.current_row)),
|
||||
};
|
||||
}
|
||||
|
||||
/// Runs the CIE instructions, then the FDE instructions. Execution halts
|
||||
/// once the row that corresponds to `pc` is known, and the row is returned.
|
||||
pub fn runTo(
|
||||
vm: *VirtualMachine,
|
||||
gpa: Allocator,
|
||||
pc: u64,
|
||||
cie: *const Unwind.CommonInformationEntry,
|
||||
fde: *const Unwind.FrameDescriptionEntry,
|
||||
addr_size_bytes: u8,
|
||||
endian: std.builtin.Endian,
|
||||
) !Row {
|
||||
assert(vm.cie_row == null);
|
||||
|
||||
const target_offset = pc - fde.pc_begin;
|
||||
assert(target_offset < fde.pc_range);
|
||||
|
||||
const instruction_bytes: []const u8 = insts: {
|
||||
if (target_offset < cie.last_row.?.offset) {
|
||||
break :insts cie.initial_instructions;
|
||||
}
|
||||
// This is the more common case: start from the CIE's last row.
|
||||
assert(vm.columns.items.len == 0);
|
||||
vm.current_row = .{
|
||||
.offset = cie.last_row.?.offset,
|
||||
.cfa = cie.last_row.?.cfa,
|
||||
.columns = .{
|
||||
.start = 0,
|
||||
.len = @intCast(cie.last_row.?.cols.len),
|
||||
},
|
||||
};
|
||||
try vm.columns.appendSlice(gpa, cie.last_row.?.cols);
|
||||
vm.cie_row = vm.current_row;
|
||||
break :insts fde.instructions;
|
||||
};
|
||||
|
||||
try vm.evalInstructions(
|
||||
gpa,
|
||||
cie,
|
||||
target_offset,
|
||||
instruction_bytes,
|
||||
addr_size_bytes,
|
||||
endian,
|
||||
);
|
||||
return vm.current_row;
|
||||
}
|
||||
|
||||
/// Evaluates instructions from `instruction_bytes` until `target_addr` is reached or all
|
||||
/// instructions have been evaluated.
|
||||
fn evalInstructions(
|
||||
vm: *VirtualMachine,
|
||||
gpa: Allocator,
|
||||
cie: *const Unwind.CommonInformationEntry,
|
||||
target_addr: u64,
|
||||
instruction_bytes: []const u8,
|
||||
addr_size_bytes: u8,
|
||||
endian: std.builtin.Endian,
|
||||
) !void {
|
||||
var fr: std.Io.Reader = .fixed(instruction_bytes);
|
||||
while (fr.seek < fr.buffer.len) {
|
||||
switch (try Instruction.read(&fr, addr_size_bytes, endian)) {
|
||||
.nop => {
|
||||
// If there was one nop, there's a good chance we've reached the padding and so
|
||||
// everything left is a nop, which is represented by a 0 byte.
|
||||
if (std.mem.allEqual(u8, fr.buffered(), 0)) return;
|
||||
},
|
||||
|
||||
.remember_state => {
|
||||
try vm.stack.append(gpa, .{
|
||||
.cfa = vm.current_row.cfa,
|
||||
.columns = vm.current_row.columns,
|
||||
});
|
||||
const cols_len = vm.current_row.columns.len;
|
||||
const copy_start = vm.columns.items.len;
|
||||
assert(vm.current_row.columns.start == copy_start - cols_len);
|
||||
try vm.columns.ensureUnusedCapacity(gpa, cols_len); // to prevent aliasing issues
|
||||
vm.columns.appendSliceAssumeCapacity(vm.columns.items[copy_start - cols_len ..]);
|
||||
vm.current_row.columns.start = copy_start;
|
||||
},
|
||||
.restore_state => {
|
||||
const restored = vm.stack.pop() orelse return error.InvalidOperation;
|
||||
vm.columns.shrinkRetainingCapacity(restored.columns.start + restored.columns.len);
|
||||
|
||||
vm.current_row.cfa = restored.cfa;
|
||||
vm.current_row.columns = restored.columns;
|
||||
},
|
||||
|
||||
.advance_loc => |delta| {
|
||||
const new_addr = vm.current_row.offset + delta * cie.code_alignment_factor;
|
||||
if (new_addr > target_addr) return;
|
||||
vm.current_row.offset = new_addr;
|
||||
},
|
||||
.set_loc => |new_addr| {
|
||||
if (new_addr <= vm.current_row.offset) return error.InvalidOperation;
|
||||
if (cie.segment_selector_size != 0) return error.InvalidOperation; // unsupported
|
||||
// TODO: Check cie.segment_selector_size != 0 for DWARFV4
|
||||
|
||||
if (new_addr > target_addr) return;
|
||||
vm.current_row.offset = new_addr;
|
||||
},
|
||||
|
||||
.register => |reg| {
|
||||
const column = try vm.getOrAddColumn(gpa, reg.index);
|
||||
column.rule = switch (reg.rule) {
|
||||
.restore => rule: {
|
||||
const cie_row = &(vm.cie_row orelse return error.InvalidOperation);
|
||||
for (vm.rowColumns(cie_row)) |cie_col| {
|
||||
if (cie_col.register == reg.index) break :rule cie_col.rule;
|
||||
}
|
||||
break :rule .default;
|
||||
},
|
||||
.undefined => .undefined,
|
||||
.same_value => .same_value,
|
||||
.offset_uf => |off| .{ .offset = @as(i64, @intCast(off)) * cie.data_alignment_factor },
|
||||
.offset_sf => |off| .{ .offset = off * cie.data_alignment_factor },
|
||||
.val_offset_uf => |off| .{ .val_offset = @as(i64, @intCast(off)) * cie.data_alignment_factor },
|
||||
.val_offset_sf => |off| .{ .val_offset = off * cie.data_alignment_factor },
|
||||
.register => |callee_reg| .{ .register = callee_reg },
|
||||
.expr => |len| .{ .expression = try takeExprBlock(&fr, len) },
|
||||
.val_expr => |len| .{ .val_expression = try takeExprBlock(&fr, len) },
|
||||
};
|
||||
},
|
||||
.def_cfa => |cfa| vm.current_row.cfa = .{ .reg_off = .{
|
||||
.register = cfa.register,
|
||||
.offset = @intCast(cfa.offset),
|
||||
} },
|
||||
.def_cfa_sf => |cfa| vm.current_row.cfa = .{ .reg_off = .{
|
||||
.register = cfa.register,
|
||||
.offset = cfa.offset_sf * cie.data_alignment_factor,
|
||||
} },
|
||||
.def_cfa_reg => |register| switch (vm.current_row.cfa) {
|
||||
.none => {
|
||||
// According to the DWARF specification, this is not valid, because this
|
||||
// instruction can only be used to replace the register if the rule is already a
|
||||
// `.reg_off`. However, this is emitted in practice by GNU toolchains for some
|
||||
// targets, and so by convention is interpreted as equivalent to `.def_cfa` with
|
||||
// an offset of 0.
|
||||
vm.current_row.cfa = .{ .reg_off = .{
|
||||
.register = register,
|
||||
.offset = 0,
|
||||
} };
|
||||
},
|
||||
.expression => return error.InvalidOperation,
|
||||
.reg_off => |*ro| ro.register = register,
|
||||
},
|
||||
.def_cfa_offset => |offset| switch (vm.current_row.cfa) {
|
||||
.none, .expression => return error.InvalidOperation,
|
||||
.reg_off => |*ro| ro.offset = @intCast(offset),
|
||||
},
|
||||
.def_cfa_offset_sf => |offset_sf| switch (vm.current_row.cfa) {
|
||||
.none, .expression => return error.InvalidOperation,
|
||||
.reg_off => |*ro| ro.offset = offset_sf * cie.data_alignment_factor,
|
||||
},
|
||||
.def_cfa_expr => |len| {
|
||||
vm.current_row.cfa = .{ .expression = try takeExprBlock(&fr, len) };
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn takeExprBlock(r: *std.Io.Reader, len: usize) error{ ReadFailed, InvalidOperand }![]const u8 {
|
||||
return r.take(len) catch |err| switch (err) {
|
||||
error.ReadFailed => |e| return e,
|
||||
error.EndOfStream => return error.InvalidOperand,
|
||||
};
|
||||
}
|
||||
|
||||
const OpcodeByte = packed struct(u8) {
|
||||
low: packed union {
|
||||
operand: u6,
|
||||
extended: enum(u6) {
|
||||
nop = 0,
|
||||
set_loc = 1,
|
||||
advance_loc1 = 2,
|
||||
advance_loc2 = 3,
|
||||
advance_loc4 = 4,
|
||||
offset_extended = 5,
|
||||
restore_extended = 6,
|
||||
undefined = 7,
|
||||
same_value = 8,
|
||||
register = 9,
|
||||
remember_state = 10,
|
||||
restore_state = 11,
|
||||
def_cfa = 12,
|
||||
def_cfa_register = 13,
|
||||
def_cfa_offset = 14,
|
||||
def_cfa_expression = 15,
|
||||
expression = 16,
|
||||
offset_extended_sf = 17,
|
||||
def_cfa_sf = 18,
|
||||
def_cfa_offset_sf = 19,
|
||||
val_offset = 20,
|
||||
val_offset_sf = 21,
|
||||
val_expression = 22,
|
||||
_,
|
||||
},
|
||||
},
|
||||
opcode: enum(u2) {
|
||||
extended = 0,
|
||||
advance_loc = 1,
|
||||
offset = 2,
|
||||
restore = 3,
|
||||
},
|
||||
};
|
||||
|
||||
pub const Instruction = union(enum) {
|
||||
nop,
|
||||
remember_state,
|
||||
restore_state,
|
||||
advance_loc: u32,
|
||||
set_loc: u64,
|
||||
|
||||
register: struct {
|
||||
index: u8,
|
||||
rule: union(enum) {
|
||||
restore, // restore from cie
|
||||
undefined,
|
||||
same_value,
|
||||
offset_uf: u64,
|
||||
offset_sf: i64,
|
||||
val_offset_uf: u64,
|
||||
val_offset_sf: i64,
|
||||
register: u8,
|
||||
/// Value is the number of bytes in the DWARF expression, which the caller must read.
|
||||
expr: usize,
|
||||
/// Value is the number of bytes in the DWARF expression, which the caller must read.
|
||||
val_expr: usize,
|
||||
},
|
||||
},
|
||||
|
||||
def_cfa: struct {
|
||||
register: u8,
|
||||
offset: u64,
|
||||
},
|
||||
def_cfa_sf: struct {
|
||||
register: u8,
|
||||
offset_sf: i64,
|
||||
},
|
||||
def_cfa_reg: u8,
|
||||
def_cfa_offset: u64,
|
||||
def_cfa_offset_sf: i64,
|
||||
/// Value is the number of bytes in the DWARF expression, which the caller must read.
|
||||
def_cfa_expr: usize,
|
||||
|
||||
pub fn read(
|
||||
reader: *std.Io.Reader,
|
||||
addr_size_bytes: u8,
|
||||
endian: std.builtin.Endian,
|
||||
) !Instruction {
|
||||
const inst: OpcodeByte = @bitCast(try reader.takeByte());
|
||||
return switch (inst.opcode) {
|
||||
.advance_loc => .{ .advance_loc = inst.low.operand },
|
||||
.offset => .{ .register = .{
|
||||
.index = inst.low.operand,
|
||||
.rule = .{ .offset_uf = try reader.takeLeb128(u64) },
|
||||
} },
|
||||
.restore => .{ .register = .{
|
||||
.index = inst.low.operand,
|
||||
.rule = .restore,
|
||||
} },
|
||||
.extended => switch (inst.low.extended) {
|
||||
.nop => .nop,
|
||||
.remember_state => .remember_state,
|
||||
.restore_state => .restore_state,
|
||||
.advance_loc1 => .{ .advance_loc = try reader.takeByte() },
|
||||
.advance_loc2 => .{ .advance_loc = try reader.takeInt(u16, endian) },
|
||||
.advance_loc4 => .{ .advance_loc = try reader.takeInt(u32, endian) },
|
||||
.set_loc => .{ .set_loc = switch (addr_size_bytes) {
|
||||
2 => try reader.takeInt(u16, endian),
|
||||
4 => try reader.takeInt(u32, endian),
|
||||
8 => try reader.takeInt(u64, endian),
|
||||
else => return error.UnsupportedAddrSize,
|
||||
} },
|
||||
|
||||
.offset_extended => .{ .register = .{
|
||||
.index = try reader.takeLeb128(u8),
|
||||
.rule = .{ .offset_uf = try reader.takeLeb128(u64) },
|
||||
} },
|
||||
.offset_extended_sf => .{ .register = .{
|
||||
.index = try reader.takeLeb128(u8),
|
||||
.rule = .{ .offset_sf = try reader.takeLeb128(i64) },
|
||||
} },
|
||||
.restore_extended => .{ .register = .{
|
||||
.index = try reader.takeLeb128(u8),
|
||||
.rule = .restore,
|
||||
} },
|
||||
.undefined => .{ .register = .{
|
||||
.index = try reader.takeLeb128(u8),
|
||||
.rule = .undefined,
|
||||
} },
|
||||
.same_value => .{ .register = .{
|
||||
.index = try reader.takeLeb128(u8),
|
||||
.rule = .same_value,
|
||||
} },
|
||||
.register => .{ .register = .{
|
||||
.index = try reader.takeLeb128(u8),
|
||||
.rule = .{ .register = try reader.takeLeb128(u8) },
|
||||
} },
|
||||
.val_offset => .{ .register = .{
|
||||
.index = try reader.takeLeb128(u8),
|
||||
.rule = .{ .val_offset_uf = try reader.takeLeb128(u64) },
|
||||
} },
|
||||
.val_offset_sf => .{ .register = .{
|
||||
.index = try reader.takeLeb128(u8),
|
||||
.rule = .{ .val_offset_sf = try reader.takeLeb128(i64) },
|
||||
} },
|
||||
.expression => .{ .register = .{
|
||||
.index = try reader.takeLeb128(u8),
|
||||
.rule = .{ .expr = try reader.takeLeb128(usize) },
|
||||
} },
|
||||
.val_expression => .{ .register = .{
|
||||
.index = try reader.takeLeb128(u8),
|
||||
.rule = .{ .val_expr = try reader.takeLeb128(usize) },
|
||||
} },
|
||||
|
||||
.def_cfa => .{ .def_cfa = .{
|
||||
.register = try reader.takeLeb128(u8),
|
||||
.offset = try reader.takeLeb128(u64),
|
||||
} },
|
||||
.def_cfa_sf => .{ .def_cfa_sf = .{
|
||||
.register = try reader.takeLeb128(u8),
|
||||
.offset_sf = try reader.takeLeb128(i64),
|
||||
} },
|
||||
.def_cfa_register => .{ .def_cfa_reg = try reader.takeLeb128(u8) },
|
||||
.def_cfa_offset => .{ .def_cfa_offset = try reader.takeLeb128(u64) },
|
||||
.def_cfa_offset_sf => .{ .def_cfa_offset_sf = try reader.takeLeb128(i64) },
|
||||
.def_cfa_expression => .{ .def_cfa_expr = try reader.takeLeb128(usize) },
|
||||
|
||||
_ => switch (@intFromEnum(inst.low.extended)) {
|
||||
0x1C...0x3F => return error.UnimplementedUserOpcode,
|
||||
else => return error.InvalidOpcode,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const debug = @import("../../../new_debug.zig");
|
||||
const Unwind = debug.Dwarf.Unwind;
|
||||
|
||||
const VirtualMachine = @This();
|
||||
1583
src/new_debug/Dwarf/expression.zig
Normal file
1583
src/new_debug/Dwarf/expression.zig
Normal file
File diff suppressed because it is too large
Load Diff
539
src/new_debug/ElfFile.zig
Normal file
539
src/new_debug/ElfFile.zig
Normal file
@@ -0,0 +1,539 @@
|
||||
//! A helper type for loading an ELF file and collecting its DWARF debug information, unwind
|
||||
//! information, and symbol table.
|
||||
|
||||
is_64: bool,
|
||||
endian: Endian,
|
||||
|
||||
/// This is `null` iff any of the required DWARF sections were missing. `ElfFile.load` does *not*
|
||||
/// call `Dwarf.open`, `Dwarf.scanAllFunctions`, etc; that is the caller's responsibility.
|
||||
dwarf: ?Dwarf,
|
||||
|
||||
/// If non-`null`, describes the `.eh_frame` section, which can be used with `Dwarf.Unwind`.
|
||||
eh_frame: ?UnwindSection,
|
||||
/// If non-`null`, describes the `.debug_frame` section, which can be used with `Dwarf.Unwind`.
|
||||
debug_frame: ?UnwindSection,
|
||||
|
||||
/// If non-`null`, this is the contents of the `.strtab` section.
|
||||
strtab: ?[]const u8,
|
||||
/// If non-`null`, describes the `.symtab` section.
|
||||
symtab: ?SymtabSection,
|
||||
|
||||
/// Binary search table lazily populated by `searchSymtab`.
|
||||
symbol_search_table: ?[]usize,
|
||||
|
||||
/// The memory-mapped ELF file, which is referenced by `dwarf`. This field is here only so that
|
||||
/// this memory can be unmapped by `ElfFile.deinit`.
|
||||
mapped_file: []align(std.heap.page_size_min) const u8,
|
||||
/// Sometimes, debug info is stored separately to the main ELF file. In that case, `mapped_file`
|
||||
/// is the mapped ELF binary, and `mapped_debug_file` is the mapped debug info file. Both must
|
||||
/// be unmapped by `ElfFile.deinit`.
|
||||
mapped_debug_file: ?[]align(std.heap.page_size_min) const u8,
|
||||
|
||||
arena: std.heap.ArenaAllocator.State,
|
||||
|
||||
pub const UnwindSection = struct {
|
||||
vaddr: u64,
|
||||
bytes: []const u8,
|
||||
};
|
||||
pub const SymtabSection = struct {
|
||||
entry_size: u64,
|
||||
bytes: []const u8,
|
||||
};
|
||||
|
||||
pub const DebugInfoSearchPaths = struct {
|
||||
/// The location of a debuginfod client directory, which acts as a search path for build IDs. If
|
||||
/// given, we can load from this directory opportunistically, but make no effort to populate it.
|
||||
/// To avoid allocation when building the search paths, this is given as two components which
|
||||
/// will be concatenated.
|
||||
debuginfod_client: ?[2][]const u8,
|
||||
/// All "global debug directories" on the system. These are used as search paths for both debug
|
||||
/// links and build IDs. On typical systems this is just "/usr/lib/debug".
|
||||
global_debug: []const []const u8,
|
||||
/// The path to the dirname of the ELF file, which acts as a search path for debug links.
|
||||
exe_dir: ?[]const u8,
|
||||
|
||||
pub const none: DebugInfoSearchPaths = .{
|
||||
.debuginfod_client = null,
|
||||
.global_debug = &.{},
|
||||
.exe_dir = null,
|
||||
};
|
||||
|
||||
pub fn native(exe_path: []const u8) DebugInfoSearchPaths {
|
||||
return .{
|
||||
.debuginfod_client = p: {
|
||||
if (std.posix.getenv("DEBUGINFOD_CACHE_PATH")) |p| {
|
||||
break :p .{ p, "" };
|
||||
}
|
||||
if (std.posix.getenv("XDG_CACHE_HOME")) |cache_path| {
|
||||
break :p .{ cache_path, "/debuginfod_client" };
|
||||
}
|
||||
if (std.posix.getenv("HOME")) |home_path| {
|
||||
break :p .{ home_path, "/.cache/debuginfod_client" };
|
||||
}
|
||||
break :p null;
|
||||
},
|
||||
.global_debug = &.{
|
||||
"/usr/lib/debug",
|
||||
},
|
||||
.exe_dir = std.fs.path.dirname(exe_path) orelse ".",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub fn deinit(ef: *ElfFile, gpa: Allocator) void {
|
||||
if (ef.dwarf) |*dwarf| dwarf.deinit(gpa);
|
||||
if (ef.symbol_search_table) |t| gpa.free(t);
|
||||
var arena = ef.arena.promote(gpa);
|
||||
arena.deinit();
|
||||
|
||||
std.posix.munmap(ef.mapped_file);
|
||||
if (ef.mapped_debug_file) |m| std.posix.munmap(m);
|
||||
|
||||
ef.* = undefined;
|
||||
}
|
||||
|
||||
pub const LoadError = error{
|
||||
OutOfMemory,
|
||||
Overflow,
|
||||
TruncatedElfFile,
|
||||
InvalidCompressedSection,
|
||||
InvalidElfMagic,
|
||||
InvalidElfVersion,
|
||||
InvalidElfClass,
|
||||
InvalidElfEndian,
|
||||
// The remaining errors all occur when attemping to stat or mmap a file.
|
||||
SystemResources,
|
||||
MemoryMappingNotSupported,
|
||||
AccessDenied,
|
||||
LockedMemoryLimitExceeded,
|
||||
ProcessFdQuotaExceeded,
|
||||
SystemFdQuotaExceeded,
|
||||
Streaming,
|
||||
Canceled,
|
||||
Unexpected,
|
||||
};
|
||||
|
||||
pub fn load(
|
||||
gpa: Allocator,
|
||||
elf_file: std.fs.File,
|
||||
opt_build_id: ?[]const u8,
|
||||
di_search_paths: *const DebugInfoSearchPaths,
|
||||
) LoadError!ElfFile {
|
||||
var arena_instance: std.heap.ArenaAllocator = .init(gpa);
|
||||
errdefer arena_instance.deinit();
|
||||
const arena = arena_instance.allocator();
|
||||
|
||||
var result = loadInner(arena, elf_file, null) catch |err| switch (err) {
|
||||
error.CrcMismatch => unreachable, // we passed crc as null
|
||||
else => |e| return e,
|
||||
};
|
||||
errdefer std.posix.munmap(result.mapped_mem);
|
||||
|
||||
// `loadInner` did most of the work, but we might need to load an external debug info file
|
||||
|
||||
const di_mapped_mem: ?[]align(std.heap.page_size_min) const u8 = load_di: {
|
||||
if (result.sections.get(.debug_info) != null and
|
||||
result.sections.get(.debug_abbrev) != null and
|
||||
result.sections.get(.debug_str) != null and
|
||||
result.sections.get(.debug_line) != null)
|
||||
{
|
||||
// The info is already loaded from this file alone!
|
||||
break :load_di null;
|
||||
}
|
||||
|
||||
// We're missing some debug info---let's try and load it from a separate file.
|
||||
|
||||
build_id: {
|
||||
const build_id = opt_build_id orelse break :build_id;
|
||||
if (build_id.len < 3) break :build_id;
|
||||
|
||||
for (di_search_paths.global_debug) |global_debug| {
|
||||
if (try loadSeparateDebugFile(arena, &result, null, "{s}/.build-id/{x}/{x}.debug", .{
|
||||
global_debug,
|
||||
build_id[0..1],
|
||||
build_id[1..],
|
||||
})) |mapped| break :load_di mapped;
|
||||
}
|
||||
|
||||
if (di_search_paths.debuginfod_client) |components| {
|
||||
if (try loadSeparateDebugFile(arena, &result, null, "{s}{s}/{x}/debuginfo", .{
|
||||
components[0],
|
||||
components[1],
|
||||
build_id,
|
||||
})) |mapped| break :load_di mapped;
|
||||
}
|
||||
}
|
||||
|
||||
debug_link: {
|
||||
const section = result.sections.get(.gnu_debuglink) orelse break :debug_link;
|
||||
const debug_filename = std.mem.sliceTo(section.bytes, 0);
|
||||
const crc_offset = std.mem.alignForward(usize, debug_filename.len + 1, 4);
|
||||
if (section.bytes.len < crc_offset + 4) break :debug_link;
|
||||
const debug_crc = std.mem.readInt(u32, section.bytes[crc_offset..][0..4], result.endian);
|
||||
|
||||
const exe_dir = di_search_paths.exe_dir orelse break :debug_link;
|
||||
|
||||
if (try loadSeparateDebugFile(arena, &result, debug_crc, "{s}/{s}", .{
|
||||
exe_dir,
|
||||
debug_filename,
|
||||
})) |mapped| break :load_di mapped;
|
||||
if (try loadSeparateDebugFile(arena, &result, debug_crc, "{s}/.debug/{s}", .{
|
||||
exe_dir,
|
||||
debug_filename,
|
||||
})) |mapped| break :load_di mapped;
|
||||
for (di_search_paths.global_debug) |global_debug| {
|
||||
// This looks like a bug; it isn't. They really do embed the absolute path to the
|
||||
// exe's dirname, *under* the global debug path.
|
||||
if (try loadSeparateDebugFile(arena, &result, debug_crc, "{s}/{s}/{s}", .{
|
||||
global_debug,
|
||||
exe_dir,
|
||||
debug_filename,
|
||||
})) |mapped| break :load_di mapped;
|
||||
}
|
||||
}
|
||||
|
||||
break :load_di null;
|
||||
};
|
||||
errdefer comptime unreachable;
|
||||
|
||||
return .{
|
||||
.is_64 = result.is_64,
|
||||
.endian = result.endian,
|
||||
.dwarf = dwarf: {
|
||||
if (result.sections.get(.debug_info) == null or
|
||||
result.sections.get(.debug_abbrev) == null or
|
||||
result.sections.get(.debug_str) == null or
|
||||
result.sections.get(.debug_line) == null)
|
||||
{
|
||||
break :dwarf null; // debug info not present
|
||||
}
|
||||
var sections: Dwarf.SectionArray = @splat(null);
|
||||
inline for (@typeInfo(Dwarf.Section.Id).@"enum".fields) |f| {
|
||||
if (result.sections.get(@field(Section.Id, f.name))) |s| {
|
||||
sections[f.value] = .{ .data = s.bytes, .owned = false };
|
||||
}
|
||||
}
|
||||
break :dwarf .{ .sections = sections };
|
||||
},
|
||||
.eh_frame = if (result.sections.get(.eh_frame)) |s| .{
|
||||
.vaddr = s.header.sh_addr,
|
||||
.bytes = s.bytes,
|
||||
} else null,
|
||||
.debug_frame = if (result.sections.get(.debug_frame)) |s| .{
|
||||
.vaddr = s.header.sh_addr,
|
||||
.bytes = s.bytes,
|
||||
} else null,
|
||||
.strtab = if (result.sections.get(.strtab)) |s| s.bytes else null,
|
||||
.symtab = if (result.sections.get(.symtab)) |s| .{
|
||||
.entry_size = s.header.sh_entsize,
|
||||
.bytes = s.bytes,
|
||||
} else null,
|
||||
.symbol_search_table = null,
|
||||
.mapped_file = result.mapped_mem,
|
||||
.mapped_debug_file = di_mapped_mem,
|
||||
.arena = arena_instance.state,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn searchSymtab(ef: *ElfFile, gpa: Allocator, vaddr: u64) error{
|
||||
NoSymtab,
|
||||
NoStrtab,
|
||||
BadSymtab,
|
||||
OutOfMemory,
|
||||
}!std.debug.Symbol {
|
||||
const symtab = ef.symtab orelse return error.NoSymtab;
|
||||
const strtab = ef.strtab orelse return error.NoStrtab;
|
||||
|
||||
if (symtab.bytes.len % symtab.entry_size != 0) return error.BadSymtab;
|
||||
|
||||
const swap_endian = ef.endian != @import("builtin").cpu.arch.endian();
|
||||
|
||||
switch (ef.is_64) {
|
||||
inline true, false => |is_64| {
|
||||
const Sym = if (is_64) elf.Elf64_Sym else elf.Elf32_Sym;
|
||||
if (symtab.entry_size != @sizeOf(Sym)) return error.BadSymtab;
|
||||
const symbols: []align(1) const Sym = @ptrCast(symtab.bytes);
|
||||
if (ef.symbol_search_table == null) {
|
||||
ef.symbol_search_table = try buildSymbolSearchTable(gpa, ef.endian, Sym, symbols);
|
||||
}
|
||||
const search_table = ef.symbol_search_table.?;
|
||||
const SearchContext = struct {
|
||||
swap_endian: bool,
|
||||
target: u64,
|
||||
symbols: []align(1) const Sym,
|
||||
fn predicate(ctx: @This(), sym_index: usize) bool {
|
||||
// We need to return `true` for the first N items, then `false` for the rest --
|
||||
// the index we'll get out is the first `false` one. So, we'll return `true` iff
|
||||
// the target address is after the *end* of this symbol. This synchronizes with
|
||||
// the logic in `buildSymbolSearchTable` which sorts by *end* address.
|
||||
var sym = ctx.symbols[sym_index];
|
||||
if (ctx.swap_endian) std.mem.byteSwapAllFields(Sym, &sym);
|
||||
const sym_end = sym.st_value + sym.st_size;
|
||||
return ctx.target >= sym_end;
|
||||
}
|
||||
};
|
||||
const sym_index_index = std.sort.partitionPoint(usize, search_table, @as(SearchContext, .{
|
||||
.swap_endian = swap_endian,
|
||||
.target = vaddr,
|
||||
.symbols = symbols,
|
||||
}), SearchContext.predicate);
|
||||
if (sym_index_index == search_table.len) return .unknown;
|
||||
var sym = symbols[search_table[sym_index_index]];
|
||||
if (swap_endian) std.mem.byteSwapAllFields(Sym, &sym);
|
||||
if (vaddr < sym.st_value or vaddr >= sym.st_value + sym.st_size) return .unknown;
|
||||
return .{
|
||||
.name = std.mem.sliceTo(strtab[sym.st_name..], 0),
|
||||
.compile_unit_name = null,
|
||||
.source_location = null,
|
||||
};
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn buildSymbolSearchTable(gpa: Allocator, endian: Endian, comptime Sym: type, symbols: []align(1) const Sym) error{
|
||||
OutOfMemory,
|
||||
BadSymtab,
|
||||
}![]usize {
|
||||
var result: std.ArrayList(usize) = .empty;
|
||||
defer result.deinit(gpa);
|
||||
|
||||
const swap_endian = endian != @import("builtin").cpu.arch.endian();
|
||||
|
||||
for (symbols, 0..) |sym_orig, sym_index| {
|
||||
var sym = sym_orig;
|
||||
if (swap_endian) std.mem.byteSwapAllFields(Sym, &sym);
|
||||
if (sym.st_name == 0) continue;
|
||||
if (sym.st_shndx == elf.SHN_UNDEF) continue;
|
||||
try result.append(gpa, sym_index);
|
||||
}
|
||||
|
||||
const SortContext = struct {
|
||||
swap_endian: bool,
|
||||
symbols: []align(1) const Sym,
|
||||
fn lessThan(ctx: @This(), lhs_sym_index: usize, rhs_sym_index: usize) bool {
|
||||
// We sort by *end* address, not start address. This matches up with logic in `searchSymtab`.
|
||||
var lhs_sym = ctx.symbols[lhs_sym_index];
|
||||
var rhs_sym = ctx.symbols[rhs_sym_index];
|
||||
if (ctx.swap_endian) {
|
||||
std.mem.byteSwapAllFields(Sym, &lhs_sym);
|
||||
std.mem.byteSwapAllFields(Sym, &rhs_sym);
|
||||
}
|
||||
const lhs_val = lhs_sym.st_value + lhs_sym.st_size;
|
||||
const rhs_val = rhs_sym.st_value + rhs_sym.st_size;
|
||||
return lhs_val < rhs_val;
|
||||
}
|
||||
};
|
||||
std.mem.sort(usize, result.items, @as(SortContext, .{
|
||||
.swap_endian = swap_endian,
|
||||
.symbols = symbols,
|
||||
}), SortContext.lessThan);
|
||||
|
||||
return result.toOwnedSlice(gpa);
|
||||
}
|
||||
|
||||
/// Only used locally, during `load`.
|
||||
const Section = struct {
|
||||
header: elf.Elf64_Shdr,
|
||||
bytes: []const u8,
|
||||
const Id = enum {
|
||||
// DWARF sections: see `Dwarf.Section.Id`.
|
||||
debug_info,
|
||||
debug_abbrev,
|
||||
debug_str,
|
||||
debug_str_offsets,
|
||||
debug_line,
|
||||
debug_line_str,
|
||||
debug_ranges,
|
||||
debug_loclists,
|
||||
debug_rnglists,
|
||||
debug_addr,
|
||||
debug_names,
|
||||
// Then anything else we're interested in.
|
||||
gnu_debuglink,
|
||||
eh_frame,
|
||||
debug_frame,
|
||||
symtab,
|
||||
strtab,
|
||||
};
|
||||
const Array = std.enums.EnumArray(Section.Id, ?Section);
|
||||
};
|
||||
|
||||
fn loadSeparateDebugFile(arena: Allocator, main_loaded: *LoadInnerResult, opt_crc: ?u32, comptime fmt: []const u8, args: anytype) Allocator.Error!?[]align(std.heap.page_size_min) const u8 {
|
||||
const path = try std.fmt.allocPrint(arena, fmt, args);
|
||||
const elf_file = std.fs.cwd().openFile(path, .{}) catch return null;
|
||||
defer elf_file.close();
|
||||
|
||||
const result = loadInner(arena, elf_file, opt_crc) catch |err| switch (err) {
|
||||
error.OutOfMemory => |e| return e,
|
||||
error.CrcMismatch => return null,
|
||||
else => return null,
|
||||
};
|
||||
errdefer comptime unreachable;
|
||||
|
||||
const have_debug_sections = inline for (@as([]const []const u8, &.{
|
||||
"debug_info",
|
||||
"debug_abbrev",
|
||||
"debug_str",
|
||||
"debug_line",
|
||||
})) |name| {
|
||||
const s = @field(Section.Id, name);
|
||||
if (main_loaded.sections.get(s) == null and result.sections.get(s) == null) {
|
||||
break false;
|
||||
}
|
||||
} else true;
|
||||
|
||||
if (result.is_64 != main_loaded.is_64 or
|
||||
result.endian != main_loaded.endian or
|
||||
!have_debug_sections)
|
||||
{
|
||||
std.posix.munmap(result.mapped_mem);
|
||||
return null;
|
||||
}
|
||||
|
||||
inline for (@typeInfo(Dwarf.Section.Id).@"enum".fields) |f| {
|
||||
const id = @field(Section.Id, f.name);
|
||||
if (main_loaded.sections.get(id) == null) {
|
||||
main_loaded.sections.set(id, result.sections.get(id));
|
||||
}
|
||||
}
|
||||
|
||||
return result.mapped_mem;
|
||||
}
|
||||
|
||||
const LoadInnerResult = struct {
|
||||
is_64: bool,
|
||||
endian: Endian,
|
||||
sections: Section.Array,
|
||||
mapped_mem: []align(std.heap.page_size_min) const u8,
|
||||
};
|
||||
fn loadInner(
|
||||
arena: Allocator,
|
||||
elf_file: std.fs.File,
|
||||
opt_crc: ?u32,
|
||||
) (LoadError || error{ CrcMismatch, Streaming, Canceled })!LoadInnerResult {
|
||||
const mapped_mem: []align(std.heap.page_size_min) const u8 = mapped: {
|
||||
const file_len = std.math.cast(
|
||||
usize,
|
||||
elf_file.getEndPos() catch |err| switch (err) {
|
||||
error.PermissionDenied => unreachable, // not asking for PROT_EXEC
|
||||
else => |e| return e,
|
||||
},
|
||||
) orelse return error.Overflow;
|
||||
|
||||
break :mapped std.posix.mmap(
|
||||
null,
|
||||
file_len,
|
||||
std.posix.PROT.READ,
|
||||
.{ .TYPE = .SHARED },
|
||||
elf_file.handle,
|
||||
0,
|
||||
) catch |err| switch (err) {
|
||||
error.MappingAlreadyExists => unreachable, // not using FIXED_NOREPLACE
|
||||
error.PermissionDenied => unreachable, // not asking for PROT_EXEC
|
||||
else => |e| return e,
|
||||
};
|
||||
};
|
||||
|
||||
if (opt_crc) |crc| {
|
||||
if (std.hash.crc.Crc32.hash(mapped_mem) != crc) {
|
||||
return error.CrcMismatch;
|
||||
}
|
||||
}
|
||||
errdefer std.posix.munmap(mapped_mem);
|
||||
|
||||
var fr: std.Io.Reader = .fixed(mapped_mem);
|
||||
|
||||
const header = elf.Header.read(&fr) catch |err| switch (err) {
|
||||
error.ReadFailed => unreachable,
|
||||
error.EndOfStream => return error.TruncatedElfFile,
|
||||
|
||||
error.InvalidElfMagic,
|
||||
error.InvalidElfVersion,
|
||||
error.InvalidElfClass,
|
||||
error.InvalidElfEndian,
|
||||
=> |e| return e,
|
||||
};
|
||||
const endian = header.endian;
|
||||
|
||||
const shstrtab_shdr_off = try std.math.add(
|
||||
u64,
|
||||
header.shoff,
|
||||
try std.math.mul(u64, header.shstrndx, header.shentsize),
|
||||
);
|
||||
fr.seek = std.math.cast(usize, shstrtab_shdr_off) orelse return error.Overflow;
|
||||
const shstrtab: []const u8 = if (header.is_64) shstrtab: {
|
||||
const shdr = fr.takeStruct(elf.Elf64_Shdr, endian) catch return error.TruncatedElfFile;
|
||||
if (shdr.sh_offset + shdr.sh_size > mapped_mem.len) return error.TruncatedElfFile;
|
||||
break :shstrtab mapped_mem[@intCast(shdr.sh_offset)..][0..@intCast(shdr.sh_size)];
|
||||
} else shstrtab: {
|
||||
const shdr = fr.takeStruct(elf.Elf32_Shdr, endian) catch return error.TruncatedElfFile;
|
||||
if (shdr.sh_offset + shdr.sh_size > mapped_mem.len) return error.TruncatedElfFile;
|
||||
break :shstrtab mapped_mem[@intCast(shdr.sh_offset)..][0..@intCast(shdr.sh_size)];
|
||||
};
|
||||
|
||||
var sections: Section.Array = .initFill(null);
|
||||
|
||||
var it = header.iterateSectionHeadersBuffer(mapped_mem);
|
||||
while (it.next() catch return error.TruncatedElfFile) |shdr| {
|
||||
if (shdr.sh_type == elf.SHT_NULL or shdr.sh_type == elf.SHT_NOBITS) continue;
|
||||
if (shdr.sh_name > shstrtab.len) return error.TruncatedElfFile;
|
||||
const name = std.mem.sliceTo(shstrtab[@intCast(shdr.sh_name)..], 0);
|
||||
|
||||
const section_id: Section.Id = inline for (@typeInfo(Section.Id).@"enum".fields) |s| {
|
||||
if (std.mem.eql(u8, "." ++ s.name, name)) {
|
||||
break @enumFromInt(s.value);
|
||||
}
|
||||
} else continue;
|
||||
|
||||
if (sections.get(section_id) != null) continue;
|
||||
|
||||
if (shdr.sh_offset + shdr.sh_size > mapped_mem.len) return error.TruncatedElfFile;
|
||||
const raw_section_bytes = mapped_mem[@intCast(shdr.sh_offset)..][0..@intCast(shdr.sh_size)];
|
||||
const section_bytes: []const u8 = bytes: {
|
||||
if ((shdr.sh_flags & elf.SHF_COMPRESSED) == 0) break :bytes raw_section_bytes;
|
||||
|
||||
var section_reader: std.Io.Reader = .fixed(raw_section_bytes);
|
||||
const ch_type: elf.COMPRESS, const ch_size: u64 = if (header.is_64) ch: {
|
||||
const chdr = section_reader.takeStruct(elf.Elf64_Chdr, endian) catch return error.InvalidCompressedSection;
|
||||
break :ch .{ chdr.ch_type, chdr.ch_size };
|
||||
} else ch: {
|
||||
const chdr = section_reader.takeStruct(elf.Elf32_Chdr, endian) catch return error.InvalidCompressedSection;
|
||||
break :ch .{ chdr.ch_type, chdr.ch_size };
|
||||
};
|
||||
if (ch_type != .ZLIB) {
|
||||
// The compression algorithm is unsupported, but don't make that a hard error; the
|
||||
// file might still be valid, and we might still be okay without this section.
|
||||
continue;
|
||||
}
|
||||
|
||||
const buf = try arena.alloc(u8, std.math.cast(usize, ch_size) orelse return error.Overflow);
|
||||
var fw: std.Io.Writer = .fixed(buf);
|
||||
var decompress: std.compress.flate.Decompress = .init(§ion_reader, .zlib, &.{});
|
||||
const n = decompress.reader.streamRemaining(&fw) catch |err| switch (err) {
|
||||
// If a write failed, then `buf` filled up, so `ch_size` was incorrect
|
||||
error.WriteFailed => return error.InvalidCompressedSection,
|
||||
// If a read failed, flate expected the section to have more data
|
||||
error.ReadFailed => return error.InvalidCompressedSection,
|
||||
};
|
||||
// It's also an error if the data is shorter than expected.
|
||||
if (n != buf.len) return error.InvalidCompressedSection;
|
||||
break :bytes buf;
|
||||
};
|
||||
sections.set(section_id, .{ .header = shdr, .bytes = section_bytes });
|
||||
}
|
||||
|
||||
return .{
|
||||
.is_64 = header.is_64,
|
||||
.endian = endian,
|
||||
.sections = sections,
|
||||
.mapped_mem = mapped_mem,
|
||||
};
|
||||
}
|
||||
|
||||
const std = @import("std");
|
||||
const Endian = std.builtin.Endian;
|
||||
const debug = @import("../new_debug.zig");
|
||||
const Dwarf = debug.Dwarf;
|
||||
const ElfFile = @This();
|
||||
const Allocator = std.mem.Allocator;
|
||||
const elf = std.elf;
|
||||
549
src/new_debug/MachOFile.zig
Normal file
549
src/new_debug/MachOFile.zig
Normal file
@@ -0,0 +1,549 @@
|
||||
mapped_memory: []align(std.heap.page_size_min) const u8,
|
||||
symbols: []const Symbol,
|
||||
strings: []const u8,
|
||||
text_vmaddr: u64,
|
||||
|
||||
/// Key is index into `strings` of the file path.
|
||||
ofiles: std.AutoArrayHashMapUnmanaged(u32, Error!OFile),
|
||||
|
||||
pub const Error = error{
|
||||
InvalidMachO,
|
||||
InvalidDwarf,
|
||||
MissingDebugInfo,
|
||||
UnsupportedDebugInfo,
|
||||
ReadFailed,
|
||||
OutOfMemory,
|
||||
};
|
||||
|
||||
pub fn deinit(mf: *MachOFile, gpa: Allocator) void {
|
||||
for (mf.ofiles.values()) |*maybe_of| {
|
||||
const of = &(maybe_of.* catch continue);
|
||||
posix.munmap(of.mapped_memory);
|
||||
of.dwarf.deinit(gpa);
|
||||
of.symbols_by_name.deinit(gpa);
|
||||
}
|
||||
mf.ofiles.deinit(gpa);
|
||||
gpa.free(mf.symbols);
|
||||
posix.munmap(mf.mapped_memory);
|
||||
}
|
||||
|
||||
pub fn load(gpa: Allocator, path: []const u8, arch: std.Target.Cpu.Arch) Error!MachOFile {
|
||||
switch (arch) {
|
||||
.x86_64, .aarch64 => {},
|
||||
else => unreachable,
|
||||
}
|
||||
|
||||
const all_mapped_memory = try mapDebugInfoFile(path);
|
||||
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.InvalidMachO;
|
||||
const magic = std.mem.readInt(u32, all_mapped_memory.ptr[0..4], .little);
|
||||
|
||||
// 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").
|
||||
var fat_r: Io.Reader = .fixed(all_mapped_memory);
|
||||
const hdr = fat_r.takeStruct(macho.fat_header, .big) catch |err| switch (err) {
|
||||
error.ReadFailed => unreachable,
|
||||
error.EndOfStream => return error.InvalidMachO,
|
||||
};
|
||||
const want_cpu_type = switch (arch) {
|
||||
.x86_64 => macho.CPU_TYPE_X86_64,
|
||||
.aarch64 => macho.CPU_TYPE_ARM64,
|
||||
else => unreachable,
|
||||
};
|
||||
for (0..hdr.nfat_arch) |_| {
|
||||
const fat_arch = fat_r.takeStruct(macho.fat_arch, .big) catch |err| switch (err) {
|
||||
error.ReadFailed => unreachable,
|
||||
error.EndOfStream => return error.InvalidMachO,
|
||||
};
|
||||
if (fat_arch.cputype != want_cpu_type) continue;
|
||||
if (fat_arch.offset + fat_arch.size > all_mapped_memory.len) return error.InvalidMachO;
|
||||
break :mapped_macho all_mapped_memory[fat_arch.offset..][0..fat_arch.size];
|
||||
}
|
||||
// `arch` 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.InvalidMachO,
|
||||
};
|
||||
|
||||
var r: Io.Reader = .fixed(mapped_macho);
|
||||
const hdr = r.takeStruct(macho.mach_header_64, .little) catch |err| switch (err) {
|
||||
error.ReadFailed => unreachable,
|
||||
error.EndOfStream => return error.InvalidMachO,
|
||||
};
|
||||
|
||||
if (hdr.magic != macho.MH_MAGIC_64)
|
||||
return error.InvalidMachO;
|
||||
|
||||
const symtab: macho.symtab_command, const text_vmaddr: u64 = lcs: {
|
||||
var it: macho.LoadCommandIterator = try .init(&hdr, mapped_macho[@sizeOf(macho.mach_header_64)..]);
|
||||
var symtab: ?macho.symtab_command = null;
|
||||
var text_vmaddr: ?u64 = null;
|
||||
while (try it.next()) |cmd| switch (cmd.hdr.cmd) {
|
||||
.SYMTAB => symtab = cmd.cast(macho.symtab_command) orelse return error.InvalidMachO,
|
||||
.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 :lcs .{
|
||||
symtab orelse return error.MissingDebugInfo,
|
||||
text_vmaddr orelse return error.MissingDebugInfo,
|
||||
};
|
||||
};
|
||||
|
||||
const strings = mapped_macho[symtab.stroff..][0 .. symtab.strsize - 1];
|
||||
|
||||
var symbols: std.ArrayList(Symbol) = try .initCapacity(gpa, symtab.nsyms);
|
||||
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, symtab.nsyms);
|
||||
|
||||
var ofile: u32 = undefined;
|
||||
var last_sym: Symbol = undefined;
|
||||
var state: enum {
|
||||
init,
|
||||
oso_open,
|
||||
oso_close,
|
||||
bnsym,
|
||||
fun_strx,
|
||||
fun_size,
|
||||
ensym,
|
||||
} = .init;
|
||||
|
||||
var sym_r: Io.Reader = .fixed(mapped_macho[symtab.symoff..]);
|
||||
for (0..symtab.nsyms) |_| {
|
||||
const sym = sym_r.takeStruct(macho.nlist_64, .little) catch |err| switch (err) {
|
||||
error.ReadFailed => unreachable,
|
||||
error.EndOfStream => return error.InvalidMachO,
|
||||
};
|
||||
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 = Symbol.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.InvalidMachO,
|
||||
},
|
||||
.bnsym => switch (state) {
|
||||
.oso_open, .ensym => {
|
||||
state = .bnsym;
|
||||
last_sym = .{
|
||||
.strx = 0,
|
||||
.addr = sym.n_value,
|
||||
.ofile = ofile,
|
||||
};
|
||||
},
|
||||
else => return error.InvalidMachO,
|
||||
},
|
||||
.fun => switch (state) {
|
||||
.bnsym => {
|
||||
state = .fun_strx;
|
||||
last_sym.strx = sym.n_strx;
|
||||
},
|
||||
.fun_strx => {
|
||||
state = .fun_size;
|
||||
},
|
||||
else => return error.InvalidMachO,
|
||||
},
|
||||
.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.InvalidMachO,
|
||||
},
|
||||
.so => switch (state) {
|
||||
.init, .oso_close => {},
|
||||
.oso_open, .ensym => {
|
||||
state = .oso_close;
|
||||
},
|
||||
else => return error.InvalidMachO,
|
||||
},
|
||||
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.InvalidMachO, // 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(Symbol, symbols_slice, {}, Symbol.addressLessThan);
|
||||
|
||||
return .{
|
||||
.mapped_memory = all_mapped_memory,
|
||||
.symbols = symbols_slice,
|
||||
.strings = strings,
|
||||
.ofiles = .empty,
|
||||
.text_vmaddr = text_vmaddr,
|
||||
};
|
||||
}
|
||||
pub fn getDwarfForAddress(mf: *MachOFile, gpa: Allocator, vaddr: u64) !struct { *Dwarf, u64 } {
|
||||
const symbol = Symbol.find(mf.symbols, vaddr) orelse return error.MissingDebugInfo;
|
||||
|
||||
if (symbol.ofile == Symbol.unknown_ofile) return error.MissingDebugInfo;
|
||||
|
||||
// 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(mf.strings[symbol.strx..], 0);
|
||||
|
||||
const gop = try mf.ofiles.getOrPut(gpa, symbol.ofile);
|
||||
if (!gop.found_existing) {
|
||||
const name = mem.sliceTo(mf.strings[symbol.ofile..], 0);
|
||||
gop.value_ptr.* = loadOFile(gpa, name);
|
||||
}
|
||||
const of = &(gop.value_ptr.* catch |err| return err);
|
||||
|
||||
const symbol_index = of.symbols_by_name.getKeyAdapted(
|
||||
@as([]const u8, stab_symbol),
|
||||
@as(OFile.SymbolAdapter, .{ .strtab = of.strtab, .symtab_raw = of.symtab_raw }),
|
||||
) orelse return error.MissingDebugInfo;
|
||||
|
||||
const symbol_ofile_vaddr = vaddr: {
|
||||
var sym = of.symtab_raw[symbol_index];
|
||||
if (builtin.cpu.arch.endian() != .little) std.mem.byteSwapAllFields(macho.nlist_64, &sym);
|
||||
break :vaddr sym.n_value;
|
||||
};
|
||||
|
||||
return .{ &of.dwarf, symbol_ofile_vaddr + address_symbol_offset };
|
||||
}
|
||||
pub fn lookupSymbolName(mf: *MachOFile, vaddr: u64) error{MissingDebugInfo}![]const u8 {
|
||||
const symbol = Symbol.find(mf.symbols, vaddr) orelse return error.MissingDebugInfo;
|
||||
return mem.sliceTo(mf.strings[symbol.strx..], 0);
|
||||
}
|
||||
|
||||
const OFile = struct {
|
||||
mapped_memory: []align(std.heap.page_size_min) const u8,
|
||||
dwarf: Dwarf,
|
||||
strtab: []const u8,
|
||||
symtab_raw: []align(1) const macho.nlist_64,
|
||||
/// All named symbols in `symtab_raw`. Stored `u32` key is the index into `symtab_raw`. 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_raw: []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;
|
||||
var b_sym = ctx.symtab_raw[b_sym_index];
|
||||
if (builtin.cpu.arch.endian() != .little) std.mem.byteSwapAllFields(macho.nlist_64, &b_sym);
|
||||
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 Symbol = struct {
|
||||
strx: u32,
|
||||
addr: u64,
|
||||
/// Value may be `unknown_ofile`.
|
||||
ofile: u32,
|
||||
const unknown_ofile = std.math.maxInt(u32);
|
||||
fn addressLessThan(context: void, lhs: Symbol, rhs: Symbol) bool {
|
||||
_ = context;
|
||||
return lhs.addr < rhs.addr;
|
||||
}
|
||||
/// Assumes that `symbols` is sorted in order of ascending `addr`.
|
||||
fn find(symbols: []const Symbol, address: usize) ?*const Symbol {
|
||||
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 Symbol = &.{
|
||||
.{ .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 {
|
||||
_ = Symbol;
|
||||
}
|
||||
|
||||
fn loadOFile(gpa: Allocator, o_file_name: []const u8) !OFile {
|
||||
const all_mapped_memory, const mapped_ofile = map: {
|
||||
const open_paren = paren: {
|
||||
if (std.mem.endsWith(u8, o_file_name, ")")) {
|
||||
if (std.mem.findScalarLast(u8, o_file_name, '(')) |i| {
|
||||
break :paren i;
|
||||
}
|
||||
}
|
||||
// Not an archive, just a normal path to a .o file
|
||||
const m = try mapDebugInfoFile(o_file_name);
|
||||
break :map .{ m, m };
|
||||
};
|
||||
|
||||
// We have the form 'path/to/archive.a(entry.o)'. Map the archive and find the object file in question.
|
||||
|
||||
const archive_path = o_file_name[0..open_paren];
|
||||
const target_name_in_archive = o_file_name[open_paren + 1 .. o_file_name.len - 1];
|
||||
const mapped_archive = try mapDebugInfoFile(archive_path);
|
||||
errdefer posix.munmap(mapped_archive);
|
||||
|
||||
var ar_reader: Io.Reader = .fixed(mapped_archive);
|
||||
const ar_magic = ar_reader.take(8) catch return error.InvalidMachO;
|
||||
if (!std.mem.eql(u8, ar_magic, "!<arch>\n")) return error.InvalidMachO;
|
||||
while (true) {
|
||||
if (ar_reader.seek == ar_reader.buffer.len) return error.MissingDebugInfo;
|
||||
|
||||
const raw_name = ar_reader.takeArray(16) catch return error.InvalidMachO;
|
||||
ar_reader.discardAll(12 + 6 + 6 + 8) catch return error.InvalidMachO;
|
||||
const raw_size = ar_reader.takeArray(10) catch return error.InvalidMachO;
|
||||
const file_magic = ar_reader.takeArray(2) catch return error.InvalidMachO;
|
||||
if (!std.mem.eql(u8, file_magic, "`\n")) return error.InvalidMachO;
|
||||
|
||||
const size = std.fmt.parseInt(u32, mem.sliceTo(raw_size, ' '), 10) catch return error.InvalidMachO;
|
||||
const raw_data = ar_reader.take(size) catch return error.InvalidMachO;
|
||||
|
||||
const entry_name: []const u8, const entry_contents: []const u8 = entry: {
|
||||
if (!std.mem.startsWith(u8, raw_name, "#1/")) {
|
||||
break :entry .{ mem.sliceTo(raw_name, '/'), raw_data };
|
||||
}
|
||||
const len = std.fmt.parseInt(u32, mem.sliceTo(raw_name[3..], ' '), 10) catch return error.InvalidMachO;
|
||||
if (len > size) return error.InvalidMachO;
|
||||
break :entry .{ mem.sliceTo(raw_data[0..len], 0), raw_data[len..] };
|
||||
};
|
||||
|
||||
if (std.mem.eql(u8, entry_name, target_name_in_archive)) {
|
||||
break :map .{ mapped_archive, entry_contents };
|
||||
}
|
||||
}
|
||||
};
|
||||
errdefer posix.munmap(all_mapped_memory);
|
||||
|
||||
var r: Io.Reader = .fixed(mapped_ofile);
|
||||
const hdr = r.takeStruct(macho.mach_header_64, .little) catch |err| switch (err) {
|
||||
error.ReadFailed => unreachable,
|
||||
error.EndOfStream => return error.InvalidMachO,
|
||||
};
|
||||
if (hdr.magic != std.macho.MH_MAGIC_64) return error.InvalidMachO;
|
||||
|
||||
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 = try .init(&hdr, mapped_ofile[@sizeOf(macho.mach_header_64)..]);
|
||||
while (try it.next()) |lc| switch (lc.hdr.cmd) {
|
||||
.SEGMENT_64 => seg_cmd = lc,
|
||||
.SYMTAB => symtab_cmd = lc.cast(macho.symtab_command) orelse return error.InvalidMachO,
|
||||
else => {},
|
||||
};
|
||||
break :cmds .{
|
||||
seg_cmd orelse return error.MissingDebugInfo,
|
||||
symtab_cmd orelse return error.MissingDebugInfo,
|
||||
};
|
||||
};
|
||||
|
||||
if (mapped_ofile.len < symtab_cmd.stroff + symtab_cmd.strsize) return error.InvalidMachO;
|
||||
if (mapped_ofile[symtab_cmd.stroff + symtab_cmd.strsize - 1] != 0) return error.InvalidMachO;
|
||||
const strtab = mapped_ofile[symtab_cmd.stroff..][0 .. symtab_cmd.strsize - 1];
|
||||
|
||||
const n_sym_bytes = symtab_cmd.nsyms * @sizeOf(macho.nlist_64);
|
||||
if (mapped_ofile.len < symtab_cmd.symoff + n_sym_bytes) return error.InvalidMachO;
|
||||
const symtab_raw: []align(1) const macho.nlist_64 = @ptrCast(mapped_ofile[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_raw.len));
|
||||
for (symtab_raw, 0..) |sym_raw, sym_index| {
|
||||
var sym = sym_raw;
|
||||
if (builtin.cpu.arch.endian() != .little) std.mem.byteSwapAllFields(macho.nlist_64, &sym);
|
||||
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_raw = symtab_raw }),
|
||||
);
|
||||
if (gop.found_existing) return error.InvalidMachO;
|
||||
gop.key_ptr.* = @intCast(sym_index);
|
||||
}
|
||||
|
||||
var sections: Dwarf.SectionArray = @splat(null);
|
||||
for (seg_cmd.getSections()) |sect_raw| {
|
||||
var sect = sect_raw;
|
||||
if (builtin.cpu.arch.endian() != .little) std.mem.byteSwapAllFields(macho.section_64, §);
|
||||
|
||||
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_ofile.len < sect.offset + sect.size) return error.InvalidMachO;
|
||||
const section_bytes = mapped_ofile[sect.offset..][0..sect.size];
|
||||
sections[section_index] = .{
|
||||
.data = section_bytes,
|
||||
.owned = false,
|
||||
};
|
||||
}
|
||||
|
||||
if (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)
|
||||
{
|
||||
return error.MissingDebugInfo;
|
||||
}
|
||||
|
||||
var dwarf: Dwarf = .{ .sections = sections };
|
||||
errdefer dwarf.deinit(gpa);
|
||||
dwarf.open(gpa, .little) catch |err| switch (err) {
|
||||
error.InvalidDebugInfo,
|
||||
error.EndOfStream,
|
||||
error.Overflow,
|
||||
error.StreamTooLong,
|
||||
=> return error.InvalidDwarf,
|
||||
|
||||
error.MissingDebugInfo,
|
||||
error.ReadFailed,
|
||||
error.OutOfMemory,
|
||||
=> |e| return e,
|
||||
};
|
||||
|
||||
return .{
|
||||
.mapped_memory = all_mapped_memory,
|
||||
.dwarf = dwarf,
|
||||
.strtab = strtab,
|
||||
.symtab_raw = symtab_raw,
|
||||
.symbols_by_name = symbols_by_name.move(),
|
||||
};
|
||||
}
|
||||
|
||||
/// 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,
|
||||
file.getEndPos() catch return error.ReadFailed,
|
||||
) orelse return error.ReadFailed;
|
||||
|
||||
return posix.mmap(
|
||||
null,
|
||||
file_len,
|
||||
posix.PROT.READ,
|
||||
.{ .TYPE = .SHARED },
|
||||
file.handle,
|
||||
0,
|
||||
) catch return error.ReadFailed;
|
||||
}
|
||||
|
||||
const debug = @import("../new_debug.zig");
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Dwarf = debug.Dwarf;
|
||||
const Io = std.Io;
|
||||
const assert = std.debug.assert;
|
||||
const posix = std.posix;
|
||||
const macho = std.macho;
|
||||
const mem = std.mem;
|
||||
const testing = std.testing;
|
||||
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const MachOFile = @This();
|
||||
618
src/new_debug/Pdb.zig
Normal file
618
src/new_debug/Pdb.zig
Normal file
@@ -0,0 +1,618 @@
|
||||
const std = @import("std");
|
||||
const File = std.fs.File;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const pdb = std.pdb;
|
||||
const assert = std.debug.assert;
|
||||
|
||||
const Pdb = @This();
|
||||
|
||||
file_reader: *File.Reader,
|
||||
msf: Msf,
|
||||
allocator: Allocator,
|
||||
string_table: ?*MsfStream,
|
||||
dbi: ?*MsfStream,
|
||||
modules: []Module,
|
||||
sect_contribs: []pdb.SectionContribEntry,
|
||||
guid: [16]u8,
|
||||
age: u32,
|
||||
|
||||
pub const Module = struct {
|
||||
mod_info: pdb.ModInfo,
|
||||
module_name: []u8,
|
||||
obj_file_name: []u8,
|
||||
// The fields below are filled on demand.
|
||||
populated: bool,
|
||||
symbols: []u8,
|
||||
subsect_info: []u8,
|
||||
checksum_offset: ?usize,
|
||||
|
||||
pub fn deinit(self: *Module, allocator: Allocator) void {
|
||||
allocator.free(self.module_name);
|
||||
allocator.free(self.obj_file_name);
|
||||
if (self.populated) {
|
||||
allocator.free(self.symbols);
|
||||
allocator.free(self.subsect_info);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub fn init(gpa: Allocator, file_reader: *File.Reader) !Pdb {
|
||||
return .{
|
||||
.file_reader = file_reader,
|
||||
.allocator = gpa,
|
||||
.string_table = null,
|
||||
.dbi = null,
|
||||
.msf = try Msf.init(gpa, file_reader),
|
||||
.modules = &.{},
|
||||
.sect_contribs = &.{},
|
||||
.guid = undefined,
|
||||
.age = undefined,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Pdb) void {
|
||||
const gpa = self.allocator;
|
||||
self.msf.deinit(gpa);
|
||||
for (self.modules) |*module| {
|
||||
module.deinit(gpa);
|
||||
}
|
||||
gpa.free(self.modules);
|
||||
gpa.free(self.sect_contribs);
|
||||
}
|
||||
|
||||
pub fn parseDbiStream(self: *Pdb) !void {
|
||||
var stream = self.getStream(pdb.StreamType.dbi) orelse
|
||||
return error.InvalidDebugInfo;
|
||||
|
||||
const gpa = self.allocator;
|
||||
const reader = &stream.interface;
|
||||
|
||||
const header = try reader.takeStruct(std.pdb.DbiStreamHeader, .little);
|
||||
if (header.version_header != 19990903) // V70, only value observed by LLVM team
|
||||
return error.UnknownPDBVersion;
|
||||
// if (header.Age != age)
|
||||
// return error.UnmatchingPDB;
|
||||
|
||||
const mod_info_size = header.mod_info_size;
|
||||
const section_contrib_size = header.section_contribution_size;
|
||||
|
||||
var modules = std.array_list.Managed(Module).init(gpa);
|
||||
errdefer modules.deinit();
|
||||
|
||||
// Module Info Substream
|
||||
var mod_info_offset: usize = 0;
|
||||
while (mod_info_offset != mod_info_size) {
|
||||
const mod_info = try reader.takeStruct(pdb.ModInfo, .little);
|
||||
var this_record_len: usize = @sizeOf(pdb.ModInfo);
|
||||
|
||||
var module_name: std.Io.Writer.Allocating = .init(gpa);
|
||||
defer module_name.deinit();
|
||||
this_record_len += try reader.streamDelimiterLimit(&module_name.writer, 0, .limited(1024));
|
||||
assert(reader.buffered()[0] == 0); // TODO change streamDelimiterLimit API
|
||||
reader.toss(1);
|
||||
this_record_len += 1;
|
||||
|
||||
var obj_file_name: std.Io.Writer.Allocating = .init(gpa);
|
||||
defer obj_file_name.deinit();
|
||||
this_record_len += try reader.streamDelimiterLimit(&obj_file_name.writer, 0, .limited(1024));
|
||||
assert(reader.buffered()[0] == 0); // TODO change streamDelimiterLimit API
|
||||
reader.toss(1);
|
||||
this_record_len += 1;
|
||||
|
||||
if (this_record_len % 4 != 0) {
|
||||
const round_to_next_4 = (this_record_len | 0x3) + 1;
|
||||
const march_forward_bytes = round_to_next_4 - this_record_len;
|
||||
try stream.seekBy(@as(isize, @intCast(march_forward_bytes)));
|
||||
this_record_len += march_forward_bytes;
|
||||
}
|
||||
|
||||
try modules.append(.{
|
||||
.mod_info = mod_info,
|
||||
.module_name = try module_name.toOwnedSlice(),
|
||||
.obj_file_name = try obj_file_name.toOwnedSlice(),
|
||||
|
||||
.populated = false,
|
||||
.symbols = undefined,
|
||||
.subsect_info = undefined,
|
||||
.checksum_offset = null,
|
||||
});
|
||||
|
||||
mod_info_offset += this_record_len;
|
||||
if (mod_info_offset > mod_info_size)
|
||||
return error.InvalidDebugInfo;
|
||||
}
|
||||
|
||||
// Section Contribution Substream
|
||||
var sect_contribs = std.array_list.Managed(pdb.SectionContribEntry).init(gpa);
|
||||
errdefer sect_contribs.deinit();
|
||||
|
||||
var sect_cont_offset: usize = 0;
|
||||
if (section_contrib_size != 0) {
|
||||
const version = reader.takeEnum(std.pdb.SectionContrSubstreamVersion, .little) catch |err| switch (err) {
|
||||
error.InvalidEnumTag, error.EndOfStream => return error.InvalidDebugInfo,
|
||||
error.ReadFailed => return error.ReadFailed,
|
||||
};
|
||||
_ = version;
|
||||
sect_cont_offset += @sizeOf(u32);
|
||||
}
|
||||
while (sect_cont_offset != section_contrib_size) {
|
||||
const entry = try sect_contribs.addOne();
|
||||
entry.* = try reader.takeStruct(pdb.SectionContribEntry, .little);
|
||||
sect_cont_offset += @sizeOf(pdb.SectionContribEntry);
|
||||
|
||||
if (sect_cont_offset > section_contrib_size)
|
||||
return error.InvalidDebugInfo;
|
||||
}
|
||||
|
||||
self.modules = try modules.toOwnedSlice();
|
||||
self.sect_contribs = try sect_contribs.toOwnedSlice();
|
||||
}
|
||||
|
||||
pub fn parseInfoStream(self: *Pdb) !void {
|
||||
var stream = self.getStream(pdb.StreamType.pdb) orelse return error.InvalidDebugInfo;
|
||||
const reader = &stream.interface;
|
||||
|
||||
// Parse the InfoStreamHeader.
|
||||
const version = try reader.takeInt(u32, .little);
|
||||
const signature = try reader.takeInt(u32, .little);
|
||||
_ = signature;
|
||||
const age = try reader.takeInt(u32, .little);
|
||||
const guid = try reader.takeArray(16);
|
||||
|
||||
if (version != 20000404) // VC70, only value observed by LLVM team
|
||||
return error.UnknownPDBVersion;
|
||||
|
||||
self.guid = guid.*;
|
||||
self.age = age;
|
||||
|
||||
const gpa = self.allocator;
|
||||
|
||||
// Find the string table.
|
||||
const string_table_index = str_tab_index: {
|
||||
const name_bytes_len = try reader.takeInt(u32, .little);
|
||||
const name_bytes = try reader.readAlloc(gpa, name_bytes_len);
|
||||
defer gpa.free(name_bytes);
|
||||
|
||||
const HashTableHeader = extern struct {
|
||||
size: u32,
|
||||
capacity: u32,
|
||||
|
||||
fn maxLoad(cap: u32) u32 {
|
||||
return cap * 2 / 3 + 1;
|
||||
}
|
||||
};
|
||||
const hash_tbl_hdr = try reader.takeStruct(HashTableHeader, .little);
|
||||
if (hash_tbl_hdr.capacity == 0)
|
||||
return error.InvalidDebugInfo;
|
||||
|
||||
if (hash_tbl_hdr.size > HashTableHeader.maxLoad(hash_tbl_hdr.capacity))
|
||||
return error.InvalidDebugInfo;
|
||||
|
||||
const present = try readSparseBitVector(reader, gpa);
|
||||
defer gpa.free(present);
|
||||
if (present.len != hash_tbl_hdr.size)
|
||||
return error.InvalidDebugInfo;
|
||||
const deleted = try readSparseBitVector(reader, gpa);
|
||||
defer gpa.free(deleted);
|
||||
|
||||
for (present) |_| {
|
||||
const name_offset = try reader.takeInt(u32, .little);
|
||||
const name_index = try reader.takeInt(u32, .little);
|
||||
if (name_offset > name_bytes.len)
|
||||
return error.InvalidDebugInfo;
|
||||
const name = std.mem.sliceTo(name_bytes[name_offset..], 0);
|
||||
if (std.mem.eql(u8, name, "/names")) {
|
||||
break :str_tab_index name_index;
|
||||
}
|
||||
}
|
||||
return error.MissingDebugInfo;
|
||||
};
|
||||
|
||||
self.string_table = self.getStreamById(string_table_index) orelse
|
||||
return error.MissingDebugInfo;
|
||||
}
|
||||
|
||||
pub fn getSymbolName(self: *Pdb, module: *Module, address: u64) ?[]const u8 {
|
||||
_ = self;
|
||||
std.debug.assert(module.populated);
|
||||
|
||||
var symbol_i: usize = 0;
|
||||
while (symbol_i != module.symbols.len) {
|
||||
const prefix: *align(1) pdb.RecordPrefix = @ptrCast(&module.symbols[symbol_i]);
|
||||
if (prefix.record_len < 2)
|
||||
return null;
|
||||
switch (prefix.record_kind) {
|
||||
.lproc32, .gproc32 => {
|
||||
const proc_sym: *align(1) pdb.ProcSym = @ptrCast(&module.symbols[symbol_i + @sizeOf(pdb.RecordPrefix)]);
|
||||
if (address >= proc_sym.code_offset and address < proc_sym.code_offset + proc_sym.code_size) {
|
||||
return std.mem.sliceTo(@as([*:0]u8, @ptrCast(&proc_sym.name[0])), 0);
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
symbol_i += prefix.record_len + @sizeOf(u16);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn getLineNumberInfo(self: *Pdb, module: *Module, address: u64) !std.debug.SourceLocation {
|
||||
std.debug.assert(module.populated);
|
||||
const subsect_info = module.subsect_info;
|
||||
const gpa = self.allocator;
|
||||
|
||||
var sect_offset: usize = 0;
|
||||
var skip_len: usize = undefined;
|
||||
const checksum_offset = module.checksum_offset orelse return error.MissingDebugInfo;
|
||||
while (sect_offset != subsect_info.len) : (sect_offset += skip_len) {
|
||||
const subsect_hdr: *align(1) pdb.DebugSubsectionHeader = @ptrCast(&subsect_info[sect_offset]);
|
||||
skip_len = subsect_hdr.length;
|
||||
sect_offset += @sizeOf(pdb.DebugSubsectionHeader);
|
||||
|
||||
switch (subsect_hdr.kind) {
|
||||
.lines => {
|
||||
var line_index = sect_offset;
|
||||
|
||||
const line_hdr: *align(1) pdb.LineFragmentHeader = @ptrCast(&subsect_info[line_index]);
|
||||
if (line_hdr.reloc_segment == 0)
|
||||
return error.MissingDebugInfo;
|
||||
line_index += @sizeOf(pdb.LineFragmentHeader);
|
||||
const frag_vaddr_start = line_hdr.reloc_offset;
|
||||
const frag_vaddr_end = frag_vaddr_start + line_hdr.code_size;
|
||||
|
||||
if (address >= frag_vaddr_start and address < frag_vaddr_end) {
|
||||
// There is an unknown number of LineBlockFragmentHeaders (and their accompanying line and column records)
|
||||
// from now on. We will iterate through them, and eventually find a SourceLocation that we're interested in,
|
||||
// breaking out to :subsections. If not, we will make sure to not read anything outside of this subsection.
|
||||
const subsection_end_index = sect_offset + subsect_hdr.length;
|
||||
|
||||
while (line_index < subsection_end_index) {
|
||||
const block_hdr: *align(1) pdb.LineBlockFragmentHeader = @ptrCast(&subsect_info[line_index]);
|
||||
line_index += @sizeOf(pdb.LineBlockFragmentHeader);
|
||||
const start_line_index = line_index;
|
||||
|
||||
const has_column = line_hdr.flags.have_columns;
|
||||
|
||||
// All line entries are stored inside their line block by ascending start address.
|
||||
// Heuristic: we want to find the last line entry
|
||||
// that has a vaddr_start <= address.
|
||||
// This is done with a simple linear search.
|
||||
var line_i: u32 = 0;
|
||||
while (line_i < block_hdr.num_lines) : (line_i += 1) {
|
||||
const line_num_entry: *align(1) pdb.LineNumberEntry = @ptrCast(&subsect_info[line_index]);
|
||||
line_index += @sizeOf(pdb.LineNumberEntry);
|
||||
|
||||
const vaddr_start = frag_vaddr_start + line_num_entry.offset;
|
||||
if (address < vaddr_start) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// line_i == 0 would mean that no matching pdb.LineNumberEntry was found.
|
||||
if (line_i > 0) {
|
||||
const subsect_index = checksum_offset + block_hdr.name_index;
|
||||
const chksum_hdr: *align(1) pdb.FileChecksumEntryHeader = @ptrCast(&module.subsect_info[subsect_index]);
|
||||
const strtab_offset = @sizeOf(pdb.StringTableHeader) + chksum_hdr.file_name_offset;
|
||||
try self.string_table.?.seekTo(strtab_offset);
|
||||
const source_file_name = s: {
|
||||
const string_reader = &self.string_table.?.interface;
|
||||
var source_file_name: std.Io.Writer.Allocating = .init(gpa);
|
||||
defer source_file_name.deinit();
|
||||
_ = try string_reader.streamDelimiterLimit(&source_file_name.writer, 0, .limited(1024));
|
||||
assert(string_reader.buffered()[0] == 0); // TODO change streamDelimiterLimit API
|
||||
string_reader.toss(1);
|
||||
break :s try source_file_name.toOwnedSlice();
|
||||
};
|
||||
errdefer gpa.free(source_file_name);
|
||||
|
||||
const line_entry_idx = line_i - 1;
|
||||
|
||||
const column = if (has_column) blk: {
|
||||
const start_col_index = start_line_index + @sizeOf(pdb.LineNumberEntry) * block_hdr.num_lines;
|
||||
const col_index = start_col_index + @sizeOf(pdb.ColumnNumberEntry) * line_entry_idx;
|
||||
const col_num_entry: *align(1) pdb.ColumnNumberEntry = @ptrCast(&subsect_info[col_index]);
|
||||
break :blk col_num_entry.start_column;
|
||||
} else 0;
|
||||
|
||||
const found_line_index = start_line_index + line_entry_idx * @sizeOf(pdb.LineNumberEntry);
|
||||
const line_num_entry: *align(1) pdb.LineNumberEntry = @ptrCast(&subsect_info[found_line_index]);
|
||||
|
||||
return .{
|
||||
.file_name = source_file_name,
|
||||
.line = line_num_entry.flags.start,
|
||||
.column = column,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Checking that we are not reading garbage after the (possibly) multiple block fragments.
|
||||
if (line_index != subsection_end_index) {
|
||||
return error.InvalidDebugInfo;
|
||||
}
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
if (sect_offset > subsect_info.len)
|
||||
return error.InvalidDebugInfo;
|
||||
}
|
||||
|
||||
return error.MissingDebugInfo;
|
||||
}
|
||||
|
||||
pub fn getModule(self: *Pdb, index: usize) !?*Module {
|
||||
if (index >= self.modules.len)
|
||||
return null;
|
||||
|
||||
const mod = &self.modules[index];
|
||||
if (mod.populated)
|
||||
return mod;
|
||||
|
||||
// At most one can be non-zero.
|
||||
if (mod.mod_info.c11_byte_size != 0 and mod.mod_info.c13_byte_size != 0)
|
||||
return error.InvalidDebugInfo;
|
||||
if (mod.mod_info.c13_byte_size == 0)
|
||||
return error.InvalidDebugInfo;
|
||||
|
||||
const stream = self.getStreamById(mod.mod_info.module_sym_stream) orelse
|
||||
return error.MissingDebugInfo;
|
||||
const reader = &stream.interface;
|
||||
|
||||
const signature = try reader.takeInt(u32, .little);
|
||||
if (signature != 4)
|
||||
return error.InvalidDebugInfo;
|
||||
|
||||
const gpa = self.allocator;
|
||||
|
||||
mod.symbols = try reader.readAlloc(gpa, mod.mod_info.sym_byte_size - 4);
|
||||
mod.subsect_info = try reader.readAlloc(gpa, mod.mod_info.c13_byte_size);
|
||||
|
||||
var sect_offset: usize = 0;
|
||||
var skip_len: usize = undefined;
|
||||
while (sect_offset != mod.subsect_info.len) : (sect_offset += skip_len) {
|
||||
const subsect_hdr: *align(1) pdb.DebugSubsectionHeader = @ptrCast(&mod.subsect_info[sect_offset]);
|
||||
skip_len = subsect_hdr.length;
|
||||
sect_offset += @sizeOf(pdb.DebugSubsectionHeader);
|
||||
|
||||
switch (subsect_hdr.kind) {
|
||||
.file_checksums => {
|
||||
mod.checksum_offset = sect_offset;
|
||||
break;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
if (sect_offset > mod.subsect_info.len)
|
||||
return error.InvalidDebugInfo;
|
||||
}
|
||||
|
||||
mod.populated = true;
|
||||
return mod;
|
||||
}
|
||||
|
||||
pub fn getStreamById(self: *Pdb, id: u32) ?*MsfStream {
|
||||
if (id >= self.msf.streams.len) return null;
|
||||
return &self.msf.streams[id];
|
||||
}
|
||||
|
||||
pub fn getStream(self: *Pdb, stream: pdb.StreamType) ?*MsfStream {
|
||||
const id = @intFromEnum(stream);
|
||||
return self.getStreamById(id);
|
||||
}
|
||||
|
||||
/// https://llvm.org/docs/PDB/MsfFile.html
|
||||
const Msf = struct {
|
||||
directory: MsfStream,
|
||||
streams: []MsfStream,
|
||||
|
||||
fn init(gpa: Allocator, file_reader: *File.Reader) !Msf {
|
||||
const superblock = try file_reader.interface.takeStruct(pdb.SuperBlock, .little);
|
||||
|
||||
if (!std.mem.eql(u8, &superblock.file_magic, pdb.SuperBlock.expect_magic))
|
||||
return error.InvalidDebugInfo;
|
||||
if (superblock.free_block_map_block != 1 and superblock.free_block_map_block != 2)
|
||||
return error.InvalidDebugInfo;
|
||||
if (superblock.num_blocks * superblock.block_size != try file_reader.getSize())
|
||||
return error.InvalidDebugInfo;
|
||||
switch (superblock.block_size) {
|
||||
// llvm only supports 4096 but we can handle any of these values
|
||||
512, 1024, 2048, 4096 => {},
|
||||
else => return error.InvalidDebugInfo,
|
||||
}
|
||||
|
||||
const dir_block_count = blockCountFromSize(superblock.num_directory_bytes, superblock.block_size);
|
||||
if (dir_block_count > superblock.block_size / @sizeOf(u32))
|
||||
return error.UnhandledBigDirectoryStream; // cf. BlockMapAddr comment.
|
||||
|
||||
try file_reader.seekTo(superblock.block_size * superblock.block_map_addr);
|
||||
const dir_blocks = try gpa.alloc(u32, dir_block_count);
|
||||
errdefer gpa.free(dir_blocks);
|
||||
for (dir_blocks) |*b| {
|
||||
b.* = try file_reader.interface.takeInt(u32, .little);
|
||||
}
|
||||
var directory_buffer: [64]u8 = undefined;
|
||||
var directory = MsfStream.init(superblock.block_size, file_reader, dir_blocks, &directory_buffer);
|
||||
|
||||
const begin = directory.logicalPos();
|
||||
const stream_count = try directory.interface.takeInt(u32, .little);
|
||||
const stream_sizes = try gpa.alloc(u32, stream_count);
|
||||
defer gpa.free(stream_sizes);
|
||||
|
||||
// Microsoft's implementation uses @as(u32, -1) for inexistent streams.
|
||||
// These streams are not used, but still participate in the file
|
||||
// and must be taken into account when resolving stream indices.
|
||||
const nil_size = 0xFFFFFFFF;
|
||||
for (stream_sizes) |*s| {
|
||||
const size = try directory.interface.takeInt(u32, .little);
|
||||
s.* = if (size == nil_size) 0 else blockCountFromSize(size, superblock.block_size);
|
||||
}
|
||||
|
||||
const streams = try gpa.alloc(MsfStream, stream_count);
|
||||
errdefer gpa.free(streams);
|
||||
|
||||
for (streams, stream_sizes) |*stream, size| {
|
||||
if (size == 0) {
|
||||
stream.* = .empty;
|
||||
continue;
|
||||
}
|
||||
const blocks = try gpa.alloc(u32, size);
|
||||
errdefer gpa.free(blocks);
|
||||
for (blocks) |*block| {
|
||||
const block_id = try directory.interface.takeInt(u32, .little);
|
||||
// Index 0 is reserved for the superblock.
|
||||
// In theory, every page which is `n * block_size + 1` or `n * block_size + 2`
|
||||
// is also reserved, for one of the FPMs. However, LLVM has been observed to map
|
||||
// these into actual streams, so allow it for compatibility.
|
||||
if (block_id == 0 or block_id >= superblock.num_blocks) return error.InvalidBlockIndex;
|
||||
block.* = block_id;
|
||||
}
|
||||
const buffer = try gpa.alloc(u8, 64);
|
||||
errdefer gpa.free(buffer);
|
||||
stream.* = .init(superblock.block_size, file_reader, blocks, buffer);
|
||||
}
|
||||
|
||||
const end = directory.logicalPos();
|
||||
if (end - begin != superblock.num_directory_bytes)
|
||||
return error.InvalidStreamDirectory;
|
||||
|
||||
return .{
|
||||
.directory = directory,
|
||||
.streams = streams,
|
||||
};
|
||||
}
|
||||
|
||||
fn deinit(self: *Msf, gpa: Allocator) void {
|
||||
gpa.free(self.directory.blocks);
|
||||
for (self.streams) |*stream| {
|
||||
gpa.free(stream.interface.buffer);
|
||||
gpa.free(stream.blocks);
|
||||
}
|
||||
gpa.free(self.streams);
|
||||
}
|
||||
};
|
||||
|
||||
const MsfStream = struct {
|
||||
file_reader: *File.Reader,
|
||||
next_read_pos: u64,
|
||||
blocks: []u32,
|
||||
block_size: u32,
|
||||
interface: std.Io.Reader,
|
||||
err: ?Error,
|
||||
|
||||
const Error = File.Reader.SeekError;
|
||||
|
||||
const empty: MsfStream = .{
|
||||
.file_reader = undefined,
|
||||
.next_read_pos = 0,
|
||||
.blocks = &.{},
|
||||
.block_size = undefined,
|
||||
.interface = .ending_instance,
|
||||
.err = null,
|
||||
};
|
||||
|
||||
fn init(block_size: u32, file_reader: *File.Reader, blocks: []u32, buffer: []u8) MsfStream {
|
||||
return .{
|
||||
.file_reader = file_reader,
|
||||
.next_read_pos = 0,
|
||||
.blocks = blocks,
|
||||
.block_size = block_size,
|
||||
.interface = .{
|
||||
.vtable = &.{ .stream = stream },
|
||||
.buffer = buffer,
|
||||
.seek = 0,
|
||||
.end = 0,
|
||||
},
|
||||
.err = null,
|
||||
};
|
||||
}
|
||||
|
||||
fn stream(r: *std.Io.Reader, w: *std.Io.Writer, limit: std.Io.Limit) std.Io.Reader.StreamError!usize {
|
||||
const ms: *MsfStream = @alignCast(@fieldParentPtr("interface", r));
|
||||
|
||||
var block_id: usize = @intCast(ms.next_read_pos / ms.block_size);
|
||||
if (block_id >= ms.blocks.len) return error.EndOfStream;
|
||||
var block = ms.blocks[block_id];
|
||||
var offset = ms.next_read_pos % ms.block_size;
|
||||
|
||||
ms.file_reader.seekTo(block * ms.block_size + offset) catch |err| {
|
||||
ms.err = err;
|
||||
return error.ReadFailed;
|
||||
};
|
||||
|
||||
var remaining = @intFromEnum(limit);
|
||||
while (remaining != 0) {
|
||||
const stream_len: usize = @min(remaining, ms.block_size - offset);
|
||||
const n = try ms.file_reader.interface.stream(w, .limited(stream_len));
|
||||
remaining -= n;
|
||||
offset += n;
|
||||
|
||||
// If we're at the end of a block, go to the next one.
|
||||
if (offset == ms.block_size) {
|
||||
offset = 0;
|
||||
block_id += 1;
|
||||
if (block_id >= ms.blocks.len) break; // End of Stream
|
||||
block = ms.blocks[block_id];
|
||||
ms.file_reader.seekTo(block * ms.block_size) catch |err| {
|
||||
ms.err = err;
|
||||
return error.ReadFailed;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const total = @intFromEnum(limit) - remaining;
|
||||
ms.next_read_pos += total;
|
||||
return total;
|
||||
}
|
||||
|
||||
pub fn logicalPos(ms: *const MsfStream) u64 {
|
||||
return ms.next_read_pos - ms.interface.bufferedLen();
|
||||
}
|
||||
|
||||
pub fn seekBy(ms: *MsfStream, len: i64) !void {
|
||||
ms.next_read_pos = @as(u64, @intCast(@as(i64, @intCast(ms.logicalPos())) + len));
|
||||
if (ms.next_read_pos >= ms.blocks.len * ms.block_size) return error.EOF;
|
||||
ms.interface.tossBuffered();
|
||||
}
|
||||
|
||||
pub fn seekTo(ms: *MsfStream, len: u64) !void {
|
||||
ms.next_read_pos = len;
|
||||
if (ms.next_read_pos >= ms.blocks.len * ms.block_size) return error.EOF;
|
||||
ms.interface.tossBuffered();
|
||||
}
|
||||
|
||||
fn getSize(ms: *const MsfStream) u64 {
|
||||
return ms.blocks.len * ms.block_size;
|
||||
}
|
||||
|
||||
fn getFilePos(ms: *const MsfStream) u64 {
|
||||
const pos = ms.logicalPos();
|
||||
const block_id = pos / ms.block_size;
|
||||
const block = ms.blocks[block_id];
|
||||
const offset = pos % ms.block_size;
|
||||
|
||||
return block * ms.block_size + offset;
|
||||
}
|
||||
};
|
||||
|
||||
fn readSparseBitVector(reader: *std.Io.Reader, allocator: Allocator) ![]u32 {
|
||||
const num_words = try reader.takeInt(u32, .little);
|
||||
var list = std.array_list.Managed(u32).init(allocator);
|
||||
errdefer list.deinit();
|
||||
var word_i: u32 = 0;
|
||||
while (word_i != num_words) : (word_i += 1) {
|
||||
const word = try reader.takeInt(u32, .little);
|
||||
var bit_i: u5 = 0;
|
||||
while (true) : (bit_i += 1) {
|
||||
if (word & (@as(u32, 1) << bit_i) != 0) {
|
||||
try list.append(word_i * 32 + bit_i);
|
||||
}
|
||||
if (bit_i == std.math.maxInt(u5)) break;
|
||||
}
|
||||
}
|
||||
return try list.toOwnedSlice();
|
||||
}
|
||||
|
||||
fn blockCountFromSize(size: u32, block_size: u32) u32 {
|
||||
return (size + block_size - 1) / block_size;
|
||||
}
|
||||
529
src/new_debug/SelfInfo/Elf.zig
Normal file
529
src/new_debug/SelfInfo/Elf.zig
Normal file
@@ -0,0 +1,529 @@
|
||||
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, io: Io, address: usize) Error!std.debug.Symbol {
|
||||
_ = io;
|
||||
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 fn getModuleSlide(si: *SelfInfo, gpa: Allocator, address: usize) Error!usize {
|
||||
const module = try si.findModule(gpa, address, .shared);
|
||||
defer si.rwlock.unlockShared();
|
||||
return module.load_offset;
|
||||
}
|
||||
|
||||
pub const can_unwind: bool = s: {
|
||||
// The DWARF code can't deal with ILP32 ABIs yet: https://github.com/ziglang/zig/issues/25447
|
||||
switch (builtin.target.abi) {
|
||||
.gnuabin32,
|
||||
.muslabin32,
|
||||
.gnux32,
|
||||
.muslx32,
|
||||
=> break :s false,
|
||||
else => {},
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// Not supported yet: arm
|
||||
.haiku => &.{
|
||||
.aarch64,
|
||||
.m68k,
|
||||
.riscv64,
|
||||
.x86,
|
||||
.x86_64,
|
||||
},
|
||||
// Not supported yet: arm/armeb/thumb/thumbeb, xtensa/xtensaeb
|
||||
.linux => &.{
|
||||
.aarch64,
|
||||
.aarch64_be,
|
||||
.arc,
|
||||
.csky,
|
||||
.loongarch64,
|
||||
.m68k,
|
||||
.mips,
|
||||
.mipsel,
|
||||
.mips64,
|
||||
.mips64el,
|
||||
.or1k,
|
||||
.riscv32,
|
||||
.riscv64,
|
||||
.s390x,
|
||||
.x86,
|
||||
.x86_64,
|
||||
},
|
||||
.serenity => &.{
|
||||
.aarch64,
|
||||
.x86_64,
|
||||
.riscv64,
|
||||
},
|
||||
|
||||
.dragonfly => &.{
|
||||
.x86_64,
|
||||
},
|
||||
// Not supported yet: arm
|
||||
.freebsd => &.{
|
||||
.aarch64,
|
||||
.riscv64,
|
||||
.x86_64,
|
||||
},
|
||||
// Not supported yet: arm/armeb, mips64/mips64el
|
||||
.netbsd => &.{
|
||||
.aarch64,
|
||||
.aarch64_be,
|
||||
.m68k,
|
||||
.mips,
|
||||
.mipsel,
|
||||
.x86,
|
||||
.x86_64,
|
||||
},
|
||||
// Not supported yet: arm
|
||||
.openbsd => &.{
|
||||
.aarch64,
|
||||
.mips64,
|
||||
.mips64el,
|
||||
.riscv64,
|
||||
.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: 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 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 debug.ElfFile.load(gpa, file, mod.build_id, &.native(path));
|
||||
};
|
||||
|
||||
var elf_file = load_result catch |err| switch (err) {
|
||||
error.OutOfMemory,
|
||||
error.Unexpected,
|
||||
error.Canceled,
|
||||
=> |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,
|
||||
error.Streaming,
|
||||
=> 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.type) {
|
||||
// .NOTE => {
|
||||
// // Look for .note.gnu.build-id
|
||||
// const segment_ptr: [*]const u8 = @ptrFromInt(info.addr + phdr.vaddr);
|
||||
// var r: std.Io.Reader = .fixed(segment_ptr[0..phdr.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.vaddr);
|
||||
// gnu_eh_frame = segment_ptr[0..phdr.memsz];
|
||||
// },
|
||||
// else => {},
|
||||
//}
|
||||
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 debug = @import("../../new_debug.zig");
|
||||
|
||||
const std = @import("std");
|
||||
const Io = std.Io;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Dwarf = debug.Dwarf;
|
||||
const Error = debug.SelfInfoError;
|
||||
const assert = std.debug.assert;
|
||||
|
||||
const builtin = @import("builtin");
|
||||
const native_endian = builtin.target.cpu.arch.endian();
|
||||
|
||||
const SelfInfo = @This();
|
||||
676
src/new_debug/SelfInfo/MachO.zig
Normal file
676
src/new_debug/SelfInfo/MachO.zig
Normal file
@@ -0,0 +1,676 @@
|
||||
mutex: std.Thread.Mutex,
|
||||
/// Accessed through `Module.Adapter`.
|
||||
modules: std.ArrayHashMapUnmanaged(Module, void, Module.Context, false),
|
||||
|
||||
pub const init: SelfInfo = .{
|
||||
.mutex = .{},
|
||||
.modules = .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);
|
||||
}
|
||||
file: {
|
||||
const f = &(module.file orelse break :file catch break :file);
|
||||
f.deinit(gpa);
|
||||
}
|
||||
}
|
||||
si.modules.deinit(gpa);
|
||||
}
|
||||
|
||||
pub fn getSymbol(si: *SelfInfo, gpa: Allocator, io: Io, address: usize) Error!std.debug.Symbol {
|
||||
_ = io;
|
||||
const module = try si.findModule(gpa, address);
|
||||
defer si.mutex.unlock();
|
||||
|
||||
const file = try module.getFile(gpa);
|
||||
|
||||
// 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.
|
||||
const vaddr_offset = module.text_base - file.text_vmaddr;
|
||||
|
||||
const vaddr = address - vaddr_offset;
|
||||
|
||||
const ofile_dwarf, const ofile_vaddr = file.getDwarfForAddress(gpa, vaddr) catch {
|
||||
// Return at least the symbol name if available.
|
||||
return .{
|
||||
.name = try file.lookupSymbolName(vaddr),
|
||||
.compile_unit_name = null,
|
||||
.source_location = null,
|
||||
};
|
||||
};
|
||||
|
||||
const compile_unit = ofile_dwarf.findCompileUnit(native_endian, ofile_vaddr) catch {
|
||||
// Return at least the symbol name if available.
|
||||
return .{
|
||||
.name = try file.lookupSymbolName(vaddr),
|
||||
.compile_unit_name = null,
|
||||
.source_location = null,
|
||||
};
|
||||
};
|
||||
|
||||
return .{
|
||||
.name = ofile_dwarf.getSymbolName(ofile_vaddr) orelse
|
||||
try file.lookupSymbolName(vaddr),
|
||||
.compile_unit_name = compile_unit.die.getAttrString(
|
||||
ofile_dwarf,
|
||||
native_endian,
|
||||
std.dwarf.AT.name,
|
||||
ofile_dwarf.section(.debug_str),
|
||||
compile_unit,
|
||||
) catch |err| switch (err) {
|
||||
error.MissingDebugInfo, error.InvalidDebugInfo => null,
|
||||
},
|
||||
.source_location = ofile_dwarf.getLineNumberInfo(
|
||||
gpa,
|
||||
native_endian,
|
||||
compile_unit,
|
||||
ofile_vaddr,
|
||||
) 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 fn getModuleSlide(si: *SelfInfo, gpa: Allocator, address: usize) Error!usize {
|
||||
const module = try si.findModule(gpa, address);
|
||||
defer si.mutex.unlock();
|
||||
const header: *std.macho.mach_header_64 = @ptrFromInt(module.text_base);
|
||||
const raw_macho: [*]u8 = @ptrCast(header);
|
||||
var it = initLoadCommandIterator(header, raw_macho[@sizeOf(macho.mach_header_64)..][0..header.sizeofcmds]) catch unreachable;
|
||||
const text_vmaddr = while (it.next()) |load_cmd| {
|
||||
if (load_cmd.hdr.cmd != .SEGMENT_64) continue;
|
||||
const segment_cmd = load_cmd.cast(macho.segment_command_64).?;
|
||||
if (!mem.eql(u8, segment_cmd.segName(), "__TEXT")) continue;
|
||||
break segment_cmd.vmaddr;
|
||||
} else unreachable;
|
||||
return module.text_base - text_vmaddr;
|
||||
}
|
||||
|
||||
pub const can_unwind: bool = true;
|
||||
pub const UnwindContext = 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,
|
||||
error.Canceled,
|
||||
=> |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 => 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);
|
||||
}
|
||||
}
|
||||
|
||||
// We intentionally skip restoring `frame.d_reg_pairs`; we know we don't support
|
||||
// vector registers in the AArch64 `cpu_context` anyway, so there's no reason to
|
||||
// fail a legitimate unwind just because we're asked to restore the registers here.
|
||||
// If some weird/broken unwind info tells us to read them later, we will fail then.
|
||||
reg_addr += 16 * @as(usize, @popCount(@as(u4, @bitCast(frame.d_reg_pairs))));
|
||||
|
||||
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 = 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: dl_info = undefined;
|
||||
if (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,
|
||||
.file = null,
|
||||
};
|
||||
}
|
||||
return gop.key_ptr;
|
||||
}
|
||||
|
||||
const Module = struct {
|
||||
text_base: usize,
|
||||
name: []const u8,
|
||||
unwind: ?(Error!Unwind),
|
||||
file: ?(Error!MachOFile),
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
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_64 = @ptrFromInt(module.text_base);
|
||||
|
||||
const raw_macho: [*]u8 = @ptrCast(header);
|
||||
var it = initLoadCommandIterator(header, raw_macho[@sizeOf(macho.mach_header_64)..][0..header.sizeofcmds]) catch unreachable;
|
||||
const sections, const text_vmaddr = while (it.next()) |load_cmd| {
|
||||
if (load_cmd.hdr.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 getFile(module: *Module, gpa: Allocator) Error!*MachOFile {
|
||||
if (module.file == null) module.file = MachOFile.load(gpa, module.name, builtin.cpu.arch) catch |err| switch (err) {
|
||||
error.InvalidMachO, error.InvalidDwarf => error.InvalidDebugInfo,
|
||||
error.MissingDebugInfo, error.OutOfMemory, error.UnsupportedDebugInfo, error.ReadFailed => |e| e,
|
||||
};
|
||||
return if (module.file.?) |*f| f else |err| err;
|
||||
}
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
extern "c" fn dladdr(addr: *const anyopaque, info: *dl_info) c_int;
|
||||
|
||||
const dl_info = extern struct {
|
||||
fname: [*:0]const u8,
|
||||
fbase: *anyopaque,
|
||||
sname: ?[*:0]const u8,
|
||||
saddr: ?*anyopaque,
|
||||
};
|
||||
|
||||
fn initLoadCommandIterator(
|
||||
hdr: *const macho.mach_header_64,
|
||||
cmds_buf_overlong: []const u8,
|
||||
) error{InvalidMachO}!macho.LoadCommandIterator {
|
||||
if (cmds_buf_overlong.len < hdr.sizeofcmds) return error.InvalidMachO;
|
||||
if (hdr.ncmds > 0 and hdr.sizeofcmds < @sizeOf(macho.load_command)) return error.InvalidMachO;
|
||||
const cmds_buf = cmds_buf_overlong[0..hdr.sizeofcmds];
|
||||
return .{
|
||||
.index = 0,
|
||||
.ncmds = hdr.ncmds,
|
||||
.buffer = cmds_buf,
|
||||
};
|
||||
}
|
||||
|
||||
const std = @import("std");
|
||||
const Io = std.Io;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Dwarf = debug.Dwarf;
|
||||
const Error = debug.SelfInfoError;
|
||||
const MachOFile = debug.MachOFile;
|
||||
const assert = std.debug.assert;
|
||||
const posix = std.posix;
|
||||
const macho = std.macho;
|
||||
const mem = std.mem;
|
||||
const testing = std.testing;
|
||||
const debug = @import("../../new_debug.zig");
|
||||
const dwarfRegNative = debug.Dwarf.SelfUnwinder.regNative;
|
||||
|
||||
const builtin = @import("builtin");
|
||||
const native_endian = builtin.target.cpu.arch.endian();
|
||||
|
||||
const SelfInfo = @This();
|
||||
571
src/new_debug/SelfInfo/Windows.zig
Normal file
571
src/new_debug/SelfInfo/Windows.zig
Normal file
@@ -0,0 +1,571 @@
|
||||
mutex: std.Thread.Mutex,
|
||||
modules: std.ArrayList(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, io: Io, 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, io);
|
||||
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 fn getModuleSlide(si: *SelfInfo, gpa: Allocator, address: usize) Error!usize {
|
||||
si.mutex.lock();
|
||||
defer si.mutex.unlock();
|
||||
const module = try si.findModule(gpa, address);
|
||||
return module.base_address;
|
||||
}
|
||||
|
||||
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 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 => .{
|
||||
.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,
|
||||
io: Io,
|
||||
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 {
|
||||
const io = di.io;
|
||||
if (di.dwarf) |*dwarf| dwarf.deinit(gpa);
|
||||
if (di.pdb) |*pdb| {
|
||||
pdb.file_reader.file.close(io);
|
||||
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, io: Io) Error!*DebugInfo {
|
||||
if (module.di == null) module.di = loadDebugInfo(module, gpa, io);
|
||||
return if (module.di.?) |*di| di else |err| err;
|
||||
}
|
||||
fn loadDebugInfo(module: *const Module, gpa: Allocator, io: Io) 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 name_w = name_buffer[0 .. len + 4 :0];
|
||||
var threaded: Io.Threaded = .init_single_threaded;
|
||||
const coff_file = threaded.dirOpenFileWtf16(null, name_w, .{}) catch |err| switch (err) {
|
||||
error.Canceled => |e| return e,
|
||||
error.Unexpected => |e| return e,
|
||||
error.FileNotFound => return error.MissingDebugInfo,
|
||||
|
||||
error.FileTooBig,
|
||||
error.IsDir,
|
||||
error.NotDir,
|
||||
error.SymLinkLoop,
|
||||
error.NameTooLong,
|
||||
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(io);
|
||||
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 = .adaptFromNewApi(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(Io.File.Reader);
|
||||
pdb_reader.* = pdb_file.reader(io, 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(io);
|
||||
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,
|
||||
.io = io,
|
||||
.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 debug = @import("../../new_debug.zig");
|
||||
const std = @import("std");
|
||||
const Io = std.Io;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Dwarf = debug.Dwarf;
|
||||
const Pdb = debug.Pdb;
|
||||
const Error = 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();
|
||||
2691
src/new_debug/cpu_context.zig
Normal file
2691
src/new_debug/cpu_context.zig
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user