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/SelfInfo.zig | |
| 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/SelfInfo.zig')
| -rw-r--r-- | lib/std/debug/SelfInfo.zig | 551 |
1 files changed, 0 insertions, 551 deletions
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); -}; |
