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