Commit 1392a7af17

mlugg <mlugg@mlugg.co.uk>
2025-09-08 21:08:28
std.debug: unwinding on Windows
...using `RtlVirtualUnwind` on x86_64 and aarch64, and `RtaCaptureStackBackTrace` on x86.
1 parent ac4d633
lib/std/debug/SelfInfo/DarwinModule.zig
@@ -252,10 +252,11 @@ pub fn getSymbolAtAddress(module: *const DarwinModule, gpa: Allocator, di: *Debu
     };
 }
 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: *DwarfUnwindContext) Error!usize {
+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,
@@ -274,7 +275,7 @@ pub fn unwindFrame(module: *const DarwinModule, gpa: Allocator, di: *DebugInfo,
         => return error.InvalidDebugInfo,
     };
 }
-fn unwindFrameInner(module: *const DarwinModule, gpa: Allocator, di: *DebugInfo, context: *DwarfUnwindContext) !usize {
+fn unwindFrameInner(module: *const DarwinModule, gpa: Allocator, di: *DebugInfo, context: *UnwindContext) !usize {
     if (di.unwind == null) di.unwind = module.loadUnwindInfo();
     const unwind = &di.unwind.?;
 
@@ -575,7 +576,7 @@ fn unwindFrameInner(module: *const DarwinModule, gpa: Allocator, di: *DebugInfo,
         else => comptime unreachable, // unimplemented
     };
 
-    context.pc = DwarfUnwindContext.stripInstructionPtrAuthCode(new_ip);
+    context.pc = UnwindContext.stripInstructionPtrAuthCode(new_ip);
     if (context.pc > 0) context.pc -= 1;
     return new_ip;
 }
@@ -819,7 +820,6 @@ const macho = std.macho;
 const mem = std.mem;
 const posix = std.posix;
 const testing = std.testing;
-const DwarfUnwindContext = std.debug.SelfInfo.DwarfUnwindContext;
 const Error = std.debug.SelfInfo.Error;
 const regBytes = Dwarf.abi.regBytes;
 const regValueNative = Dwarf.abi.regValueNative;
lib/std/debug/SelfInfo/ElfModule.zig
@@ -193,7 +193,7 @@ fn loadUnwindInfo(module: *const ElfModule, gpa: Allocator, di: *DebugInfo) Erro
         else => unreachable,
     }
 }
-pub fn unwindFrame(module: *const ElfModule, gpa: Allocator, di: *DebugInfo, context: *DwarfUnwindContext) Error!usize {
+pub fn unwindFrame(module: *const ElfModule, gpa: Allocator, di: *DebugInfo, context: *UnwindContext) Error!usize {
     if (di.unwind[0] == null) try module.loadUnwindInfo(gpa, di);
     std.debug.assert(di.unwind[0] != null);
     for (&di.unwind) |*opt_unwind| {
@@ -205,6 +205,7 @@ pub fn unwindFrame(module: *const ElfModule, gpa: Allocator, di: *DebugInfo, con
     }
     return error.MissingDebugInfo;
 }
+pub const UnwindContext = std.debug.SelfInfo.DwarfUnwindContext;
 pub const supports_unwinding: bool = s: {
     const archs: []const std.Target.Cpu.Arch = switch (builtin.target.os.tag) {
         .linux => &.{ .x86, .x86_64, .arm, .armeb, .thumb, .thumbeb, .aarch64, .aarch64_be },
@@ -233,7 +234,6 @@ const Allocator = std.mem.Allocator;
 const Dwarf = std.debug.Dwarf;
 const elf = std.elf;
 const mem = std.mem;
-const DwarfUnwindContext = std.debug.SelfInfo.DwarfUnwindContext;
 const Error = std.debug.SelfInfo.Error;
 
 const builtin = @import("builtin");
lib/std/debug/SelfInfo/WindowsModule.zig
@@ -102,7 +102,7 @@ fn loadDebugInfo(module: *const WindowsModule, gpa: Allocator, di: *DebugInfo) !
         if (create_section_rc != .SUCCESS) return error.MissingDebugInfo;
         errdefer windows.CloseHandle(section_handle);
         var coff_len: usize = 0;
-        var section_view_ptr: [*]const u8 = undefined;
+        var section_view_ptr: ?[*]const u8 = null;
         const map_section_rc = windows.ntdll.NtMapViewOfSection(
             section_handle,
             process_handle,
@@ -116,8 +116,8 @@ fn loadDebugInfo(module: *const WindowsModule, gpa: Allocator, di: *DebugInfo) !
             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];
+        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,
@@ -246,7 +246,116 @@ pub const DebugInfo = struct {
         };
     }
 };
-pub const supports_unwinding: bool = false;
+
+pub const supports_unwinding: bool = true;
+pub const UnwindContext = switch (builtin.cpu.arch) {
+    .x86 => struct {
+        pc: usize,
+        frames: []usize,
+        frames_capacity: usize,
+        next_index: usize,
+        /// Marked `noinline` to ensure that `RtlCaptureStackBackTrace` includes our caller.
+        pub noinline fn init(ctx: *windows.CONTEXT, gpa: Allocator) Allocator.Error!UnwindContext {
+            const frames_buf = try gpa.alloc(usize, 1024);
+            errdefer comptime unreachable;
+            const frames_len = windows.ntdll.RtlCaptureStackBackTrace(0, frames_buf.len, @ptrCast(frames_buf.ptr), null);
+            const regs = ctx.getRegs();
+            const first_index = for (frames_buf[0..frames_len], 0..) |ret_addr, idx| {
+                if (ret_addr == regs.ip) break idx;
+            } else i: {
+                // If we were called by an exception handler, `regs.ip` wasn't in the trace because
+                // RtlCaptureStackBackTrace omits the KiUserExceptionDispatcher frame, which is the
+                // one in `regs.ip`. In that case, we have to start one frame shallower instead, and
+                // we can figure out that frame's ip from the context's bp.
+                const start_addr_ptr: *const usize = @ptrFromInt(regs.bp + 4);
+                const start_addr = start_addr_ptr.*;
+                for (frames_buf[0..frames_len], 0..) |ret_addr, idx| {
+                    if (ret_addr == start_addr) break :i idx;
+                }
+                // The IP in the context can't be found; return an empty trace.
+                gpa.free(frames_buf);
+                return .{ .pc = 0, .frames = &.{}, .frames_capacity = 0, .next_index = 0 };
+            };
+            return .{
+                .pc = @returnAddress(),
+                .frames = frames_buf[0..frames_len],
+                .frames_capacity = 0,
+                .next_index = first_index,
+            };
+        }
+        pub fn deinit(ctx: *UnwindContext, gpa: Allocator) void {
+            gpa.free(ctx.frames.ptr[0..ctx.frames_capacity]);
+            ctx.* = undefined;
+        }
+        pub fn getFp(ctx: *UnwindContext) usize {
+            _ = ctx;
+            return 0;
+        }
+    },
+    else => struct {
+        pc: usize,
+        cur: windows.CONTEXT,
+        history_table: windows.UNWIND_HISTORY_TABLE,
+        pub fn init(ctx: *const windows.CONTEXT, gpa: Allocator) Allocator.Error!UnwindContext {
+            _ = gpa;
+            return .{
+                .pc = @returnAddress(),
+                .cur = ctx.*,
+                .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;
+
+    if (builtin.cpu.arch == .x86) {
+        const i = context.next_index;
+        if (i == context.frames.len) return 0;
+        context.next_index += 1;
+        const ip = context.frames[i];
+        context.pc = ip -| 1;
+        return ip;
+    }
+
+    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)) {
+        return 0;
+    }
+    context.pc = next_regs.ip -| 1;
+    return next_regs.ip;
+}
 
 const WindowsModule = @This();
 
lib/std/debug/Dwarf.zig
@@ -282,13 +282,13 @@ pub const Die = struct {
                     .@"32" => {
                         const byte_offset = compile_unit.str_offsets_base + 4 * index;
                         if (byte_offset + 4 > debug_str_offsets.len) return bad();
-                        const offset = mem.readInt(u32, debug_str_offsets[byte_offset..][0..4], endian);
+                        const offset = mem.readInt(u32, debug_str_offsets[@intCast(byte_offset)..][0..4], endian);
                         return getStringGeneric(opt_str, offset);
                     },
                     .@"64" => {
                         const byte_offset = compile_unit.str_offsets_base + 8 * index;
                         if (byte_offset + 8 > debug_str_offsets.len) return bad();
-                        const offset = mem.readInt(u64, debug_str_offsets[byte_offset..][0..8], endian);
+                        const offset = mem.readInt(u64, debug_str_offsets[@intCast(byte_offset)..][0..8], endian);
                         return getStringGeneric(opt_str, offset);
                     },
                 }
lib/std/debug/SelfInfo.zig
@@ -42,6 +42,8 @@ pub const target_supported: bool = Module != void;
 /// For whether DWARF unwinding is *theoretically* possible, see `Dwarf.abi.supportsUnwinding`.
 pub const supports_unwinding: bool = Module.supports_unwinding;
 
+pub const UnwindContext = if (supports_unwinding) Module.UnwindContext;
+
 pub const init: SelfInfo = .{
     .modules = .empty,
     .lookup_cache = if (Module.LookupCache != void) .init,
@@ -53,7 +55,7 @@ pub fn deinit(self: *SelfInfo, gpa: Allocator) void {
     if (Module.LookupCache != void) self.lookup_cache.deinit(gpa);
 }
 
-pub fn unwindFrame(self: *SelfInfo, gpa: Allocator, context: *DwarfUnwindContext) Error!usize {
+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 gop = try self.modules.getOrPut(gpa, module.key());
@@ -113,14 +115,23 @@ pub fn getModuleNameForAddress(self: *SelfInfo, gpa: Allocator, 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 inside the function of the last unwound frame.
+///     pc: usize,
+///     pub fn init(tc: *std.debug.ThreadContext, 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 next return address (which may be 0 indicating end of stack). This is currently
-/// /// specialized to DWARF unwinding.
+/// /// the next return address (which may be 0 indicating end of stack).
 /// pub fn unwindFrame(
 ///     mod: *const Module,
 ///     gpa: Allocator,
 ///     di: *DebugInfo,
-///     ctx: *SelfInfo.DwarfUnwindContext,
+///     ctx: *UnwindContext,
 /// ) SelfInfo.Error!usize;
 /// ```
 const Module: type = Module: {
@@ -136,6 +147,8 @@ const Module: type = Module: {
     };
 };
 
+/// 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,
@@ -144,8 +157,9 @@ pub const DwarfUnwindContext = struct {
     vm: Dwarf.Unwind.VirtualMachine,
     stack_machine: Dwarf.expression.StackMachine(.{ .call_frame_context = true }),
 
-    pub fn init(thread_context: *std.debug.ThreadContext) DwarfUnwindContext {
+    pub fn init(thread_context: *std.debug.ThreadContext, gpa: Allocator) error{}!DwarfUnwindContext {
         comptime assert(supports_unwinding);
+        _ = gpa;
 
         const ip_reg_num = Dwarf.abi.ipRegNum(native_arch).?;
         const raw_pc_ptr = regValueNative(thread_context, ip_reg_num, null) catch {
@@ -169,8 +183,8 @@ pub const DwarfUnwindContext = struct {
         self.* = undefined;
     }
 
-    pub fn getFp(self: *const DwarfUnwindContext) !usize {
-        return (try regValueNative(self.thread_context, Dwarf.abi.fpRegNum(native_arch, self.reg_context), self.reg_context)).*;
+    pub fn getFp(self: *const DwarfUnwindContext) usize {
+        return (regValueNative(self.thread_context, Dwarf.abi.fpRegNum(native_arch, self.reg_context), self.reg_context) catch return 0).*;
     }
 
     /// Resolves the register rule and places the result into `out` (see regBytes)
lib/std/debug.zig
@@ -378,6 +378,8 @@ pub inline fn getContext(context: *ThreadContext) bool {
         }
         return true;
     }
+
+    return false;
 }
 
 /// Invokes detectable illegal behavior when `ok` is `false`.
@@ -619,7 +621,9 @@ pub const StackUnwindOptions = struct {
 /// See `writeCurrentStackTrace` to immediately print the trace instead of capturing it.
 pub fn captureCurrentStackTrace(options: StackUnwindOptions, addr_buf: []usize) std.builtin.StackTrace {
     var context_buf: ThreadContext = undefined;
-    var it: StackIterator = .init(options.context, &context_buf);
+    var it = StackIterator.init(options.context, &context_buf) catch {
+        return .{ .index = 0, .instruction_addresses = &.{} };
+    };
     defer it.deinit();
     if (!it.stratOk(options.allow_unsafe_unwind)) {
         return .{ .index = 0, .instruction_addresses = &.{} };
@@ -657,7 +661,14 @@ pub fn writeCurrentStackTrace(options: StackUnwindOptions, writer: *Writer, tty_
         },
     };
     var context_buf: ThreadContext = undefined;
-    var it: StackIterator = .init(options.context, &context_buf);
+    var it = StackIterator.init(options.context, &context_buf) catch |err| switch (err) {
+        error.OutOfMemory => {
+            tty_config.setColor(writer, .dim) catch {};
+            try writer.print("Cannot print stack trace: out of memory\n", .{});
+            tty_config.setColor(writer, .reset) catch {};
+            return;
+        },
+    };
     defer it.deinit();
     if (!it.stratOk(options.allow_unsafe_unwind)) {
         tty_config.setColor(writer, .dim) catch {};
@@ -751,14 +762,14 @@ pub fn dumpStackTrace(st: *const std.builtin.StackTrace) void {
 
 const StackIterator = union(enum) {
     /// Unwinding using debug info (e.g. DWARF CFI).
-    di: if (SelfInfo.supports_unwinding) SelfInfo.DwarfUnwindContext else noreturn,
+    di: if (SelfInfo.supports_unwinding) SelfInfo.UnwindContext else noreturn,
     /// Naive frame-pointer-based unwinding. Very simple, but typically unreliable.
     fp: usize,
 
     /// It is important that this function is marked `inline` so that it can safely use
     /// `@frameAddress` and `getContext` as the caller's stack frame and our own are one
     /// and the same.
-    inline fn init(context_opt: ?*const ThreadContext, context_buf: *ThreadContext) StackIterator {
+    inline fn init(context_opt: ?*const ThreadContext, context_buf: *ThreadContext) error{OutOfMemory}!StackIterator {
         if (builtin.cpu.arch.isSPARC()) {
             // Flush all the register windows on stack.
             if (builtin.cpu.has(.sparc, .v9)) {
@@ -770,10 +781,10 @@ const StackIterator = union(enum) {
         if (context_opt) |context| {
             context_buf.* = context.*;
             relocateContext(context_buf);
-            return .{ .di = .init(context_buf) };
+            return .{ .di = try .init(context_buf, getDebugInfoAllocator()) };
         }
         if (getContext(context_buf)) {
-            return .{ .di = .init(context_buf) };
+            return .{ .di = try .init(context_buf, getDebugInfoAllocator()) };
         }
         return .{ .fp = @frameAddress() };
     }
@@ -816,10 +827,10 @@ const StackIterator = union(enum) {
                     if (ra == 0) return .end;
                     return .{ .frame = ra };
                 } else |err| {
-                    const bad_pc = unwind_context.pc;
-                    it.* = .{ .fp = unwind_context.getFp() catch 0 };
+                    const pc = unwind_context.pc;
+                    it.* = .{ .fp = unwind_context.getFp() };
                     return .{ .switch_to_fp = .{
-                        .address = bad_pc,
+                        .address = pc,
                         .err = err,
                     } };
                 }