Commit 89ef004646

kcbanner <kcbanner@gmail.com>
2023-06-27 08:02:49
debug: x86 unwinding support, more unwinding fixes
- Fix unwindFrame using the previous FDE row instead of the current one - Handle unwinding through noreturn functions - Add x86-linux getcontext - Fixup x86_64-linux getcontext not restoring the fp env - Fix start_addr filtering on x86-windows
1 parent 5cd8ab2
Changed files (6)
lib/std/dwarf/call_frame.zig
@@ -386,7 +386,9 @@ pub const VirtualMachine = struct {
     }
 
     /// Runs the CIE instructions, then the FDE instructions. Execution halts
-    /// once the row that corresponds to `pc` is known, and it is returned.
+    /// once the row that corresponds to `pc` is known (and set as `current_row`).
+    ///
+    /// The state of the row prior to the last execution step is returned.
     pub fn runTo(
         self: *VirtualMachine,
         allocator: std.mem.Allocator,
lib/std/os/linux/x86.zig
@@ -389,3 +389,57 @@ pub const SC = struct {
     pub const recvmmsg = 19;
     pub const sendmmsg = 20;
 };
+
+fn gpRegisterOffset(comptime reg_index: comptime_int) usize {
+    return @offsetOf(ucontext_t, "mcontext") + @offsetOf(mcontext_t, "gregs") + @sizeOf(usize) * reg_index;
+}
+
+pub inline fn getcontext(context: *ucontext_t) usize {
+    asm volatile (
+        \\ movl %%edi, (%[edi_offset])(%[context])
+        \\ movl %%esi, (%[esi_offset])(%[context])
+        \\ movl %%ebp, (%[ebp_offset])(%[context])
+        \\ movl %%esp, (%[esp_offset])(%[context])
+        \\ movl %%ebx, (%[ebx_offset])(%[context])
+        \\ movl %%edx, (%[edx_offset])(%[context])
+        \\ movl %%ecx, (%[ecx_offset])(%[context])
+        \\ movl %%eax, (%[eax_offset])(%[context])
+        \\ xorl %%ecx, %%ecx
+        \\ movw %%fs, %%cx
+        \\ movl %%ecx, (%[fs_offset])(%[context])
+        \\ leal (%[regspace_offset])(%[context]), %%ecx
+        \\ movl %%ecx, (%[fpregs_offset])(%[context])
+        \\ fnstenv (%%ecx)
+        \\ fldenv (%%ecx)
+        \\ call getcontext_read_eip
+        \\ getcontext_read_eip: pop %%ecx
+        \\ movl %%ecx, (%[eip_offset])(%[context])
+        :
+        : [context] "{edi}" (context),
+          [edi_offset] "p" (comptime gpRegisterOffset(REG.EDI)),
+          [esi_offset] "p" (comptime gpRegisterOffset(REG.ESI)),
+          [ebp_offset] "p" (comptime gpRegisterOffset(REG.EBP)),
+          [esp_offset] "p" (comptime gpRegisterOffset(REG.ESP)),
+          [ebx_offset] "p" (comptime gpRegisterOffset(REG.EBX)),
+          [edx_offset] "p" (comptime gpRegisterOffset(REG.EDX)),
+          [ecx_offset] "p" (comptime gpRegisterOffset(REG.ECX)),
+          [eax_offset] "p" (comptime gpRegisterOffset(REG.EAX)),
+          [eip_offset] "p" (comptime gpRegisterOffset(REG.EIP)),
+          [fs_offset] "p" (comptime gpRegisterOffset(REG.FS)),
+          [fpregs_offset] "p" (@offsetOf(ucontext_t, "mcontext") + @offsetOf(mcontext_t, "fpregs")),
+          [regspace_offset] "p" (@offsetOf(ucontext_t, "regspace")),
+        : "memory", "ecx"
+    );
+
+    // TODO: Read CS/SS registers?
+    // TODO: Store mxcsr state, need an actual definition of fpstate for that
+
+    // TODO: `flags` isn't present in the getcontext man page, figure out what to write here
+    context.flags = 0;
+    context.link = null;
+
+    const altstack_result = linux.sigaltstack(null, &context.stack);
+    if (altstack_result != 0) return altstack_result;
+
+    return linux.sigprocmask(0, null, &context.sigmask);
+}
lib/std/os/linux/x86_64.zig
@@ -425,6 +425,7 @@ pub inline fn getcontext(context: *ucontext_t) usize {
         \\ leaq (%[fpmem_offset])(%[context]), %%rcx
         \\ movq %%rcx, (%[fpstate_offset])(%[context])
         \\ fnstenv (%%rcx)
+        \\ fldenv (%%rcx)
         \\ stmxcsr (%[mxcsr_offset])(%[context])
         :
         : [context] "{rdi}" (context),
lib/std/debug.zig
@@ -135,7 +135,7 @@ pub fn dumpCurrentStackTrace(start_addr: ?usize) void {
 
 pub const StackTraceContext = blk: {
     if (native_os == .windows) {
-        break :blk @typeInfo(@TypeOf(os.windows.CONTEXT.getRegs)).Fn.return_type.?;
+        break :blk std.os.windows.CONTEXT;
     } else if (@hasDecl(os.system, "ucontext_t")) {
         break :blk os.ucontext_t;
     } else {
@@ -166,7 +166,14 @@ pub fn dumpStackTraceFromBase(context: *const StackTraceContext) void {
         };
         const tty_config = io.tty.detectConfig(io.getStdErr());
         if (native_os == .windows) {
-            writeCurrentStackTraceWindows(stderr, debug_info, tty_config, context.ip) catch return;
+            // On x86_64 and aarch64, the stack will be unwound using RtlVirtualUnwind using the context
+            // provided by the exception handler. On x86, RtlVirtualUnwind doesn't exist. Instead, a new backtrace
+            // will be captured and frames prior to the exception will be filtered.
+            // The caveat is that RtlCaptureStackBackTrace does not include the KiUserExceptionDispatcher frame,
+            // which is where the IP in `context` points to, so it can't be used as start_addr.
+            // Instead, start_addr is recovered from the stack.
+            const start_addr = if (builtin.cpu.arch == .x86) @as(*const usize, @ptrFromInt(context.getRegs().bp + 4)).* else null;
+            writeStackTraceWindows(stderr, debug_info, tty_config, context, start_addr) catch return;
             return;
         }
 
@@ -196,12 +203,12 @@ pub fn captureStackTrace(first_address: ?usize, stack_trace: *std.builtin.StackT
     if (native_os == .windows) {
         const addrs = stack_trace.instruction_addresses;
         const first_addr = first_address orelse {
-            stack_trace.index = walkStackWindows(addrs[0..]);
+            stack_trace.index = walkStackWindows(addrs[0..], null);
             return;
         };
         var addr_buf_stack: [32]usize = undefined;
         const addr_buf = if (addr_buf_stack.len > addrs.len) addr_buf_stack[0..] else addrs;
-        const n = walkStackWindows(addr_buf[0..]);
+        const n = walkStackWindows(addr_buf[0..], null);
         const first_index = for (addr_buf[0..n], 0..) |addr, i| {
             if (addr == first_addr) {
                 break i;
@@ -218,7 +225,7 @@ pub fn captureStackTrace(first_address: ?usize, stack_trace: *std.builtin.StackT
         }
         stack_trace.index = slice.len;
     } else {
-        // TODO: This should use the dwarf unwinder if it's available
+        // TODO: This should use the DWARF unwinder if .eh_frame_hdr is available (so that full debug info parsing isn't required)
         var it = StackIterator.init(first_address, null);
         defer it.deinit();
         for (stack_trace.instruction_addresses, 0..) |*addr, i| {
@@ -415,10 +422,18 @@ pub fn writeStackTrace(
 
 inline fn getContext(context: *StackTraceContext) bool {
     if (native_os == .windows) {
-        @compileError("Syscall please!");
+        context.* = std.mem.zeroes(windows.CONTEXT);
+        windows.ntdll.RtlCaptureContext(context);
+        return true;
     }
 
-    return @hasDecl(os.system, "getcontext") and os.system.getcontext(context) == 0;
+    const supports_getcontext = @hasDecl(os.system, "getcontext") and
+        (builtin.os.tag != .linux or switch (builtin.cpu.arch) {
+        .x86, .x86_64 => true,
+        else => false,
+    });
+
+    return supports_getcontext and os.system.getcontext(context) == 0;
 }
 
 pub const StackIterator = struct {
@@ -431,6 +446,7 @@ pub const StackIterator = struct {
     // stacks with frames that don't use a frame pointer (ie. -fomit-frame-pointer).
     debug_info: ?*DebugInfo,
     dwarf_context: if (supports_context) DW.UnwindContext else void = undefined,
+
     pub const supports_context = @hasDecl(os.system, "ucontext_t") and
         (builtin.os.tag != .linux or switch (builtin.cpu.arch) {
         .mips, .mipsel, .mips64, .mips64el, .riscv64 => false,
@@ -548,19 +564,20 @@ pub const StackIterator = struct {
         }
     }
 
-    fn next_dwarf(self: *StackIterator) !void {
+    fn next_dwarf(self: *StackIterator) !usize {
         const module = try self.debug_info.?.getModuleForAddress(self.dwarf_context.pc);
         if (try module.getDwarfInfoForAddress(self.debug_info.?.allocator, self.dwarf_context.pc)) |di| {
             self.dwarf_context.reg_ctx.eh_frame = true;
             self.dwarf_context.reg_ctx.is_macho = di.is_macho;
-            try di.unwindFrame(self.debug_info.?.allocator, &self.dwarf_context, module.base_address);
+            return di.unwindFrame(self.debug_info.?.allocator, &self.dwarf_context, module.base_address);
         } else return error.MissingDebugInfo;
     }
 
     fn next_internal(self: *StackIterator) ?usize {
         if (supports_context and self.debug_info != null) {
-            if (self.next_dwarf()) |_| {
-                return self.dwarf_context.pc;
+            if (self.dwarf_context.pc == 0) return null;
+            if (self.next_dwarf()) |return_address| {
+                return return_address;
             } else |err| {
                 if (err != error.MissingFDE) print("DWARF unwind error: {}\n", .{err});
 
@@ -611,12 +628,13 @@ pub fn writeCurrentStackTrace(
     tty_config: io.tty.Config,
     start_addr: ?usize,
 ) !void {
+    var context: StackTraceContext = undefined;
+    const has_context = getContext(&context);
     if (native_os == .windows) {
-        return writeCurrentStackTraceWindows(out_stream, debug_info, tty_config, start_addr);
+        return writeStackTraceWindows(out_stream, debug_info, tty_config, &context, start_addr);
     }
 
-    var context: StackTraceContext = undefined;
-    var it = (if (getContext(&context)) blk: {
+    var it = (if (has_context) blk: {
         break :blk StackIterator.initWithContext(start_addr, debug_info, &context) catch null;
     } else null) orelse StackIterator.init(start_addr, null);
     defer it.deinit();
@@ -632,7 +650,7 @@ pub fn writeCurrentStackTrace(
     }
 }
 
-pub noinline fn walkStackWindows(addresses: []usize) usize {
+pub noinline fn walkStackWindows(addresses: []usize, existing_context: ?*const windows.CONTEXT) usize {
     if (builtin.cpu.arch == .x86) {
         // RtlVirtualUnwind doesn't exist on x86
         return windows.ntdll.RtlCaptureStackBackTrace(0, addresses.len, @as(**anyopaque, @ptrCast(addresses.ptr)), null);
@@ -640,8 +658,13 @@ pub noinline fn walkStackWindows(addresses: []usize) usize {
 
     const tib = @as(*const windows.NT_TIB, @ptrCast(&windows.teb().Reserved1));
 
-    var context: windows.CONTEXT = std.mem.zeroes(windows.CONTEXT);
-    windows.ntdll.RtlCaptureContext(&context);
+    var context: windows.CONTEXT = undefined;
+    if (existing_context) |context_ptr| {
+        context = context_ptr.*;
+    } else {
+        context = std.mem.zeroes(windows.CONTEXT);
+        windows.ntdll.RtlCaptureContext(&context);
+    }
 
     var i: usize = 0;
     var image_base: usize = undefined;
@@ -683,14 +706,15 @@ pub noinline fn walkStackWindows(addresses: []usize) usize {
     return i;
 }
 
-pub fn writeCurrentStackTraceWindows(
+pub fn writeStackTraceWindows(
     out_stream: anytype,
     debug_info: *DebugInfo,
     tty_config: io.tty.Config,
+    context: *const windows.CONTEXT,
     start_addr: ?usize,
 ) !void {
     var addr_buf: [1024]usize = undefined;
-    const n = walkStackWindows(addr_buf[0..]);
+    const n = walkStackWindows(addr_buf[0..], context);
     const addrs = addr_buf[0..n];
     var start_i: usize = if (start_addr) |saddr| blk: {
         for (addrs, 0..) |addr, i| {
@@ -2164,16 +2188,15 @@ fn handleSegfaultWindowsExtra(
 }
 
 fn dumpSegfaultInfoWindows(info: *windows.EXCEPTION_POINTERS, msg: u8, label: ?[]const u8) void {
-    const regs = info.ContextRecord.getRegs();
     const stderr = io.getStdErr().writer();
     _ = switch (msg) {
         0 => stderr.print("{s}\n", .{label.?}),
         1 => stderr.print("Segmentation fault at address 0x{x}\n", .{info.ExceptionRecord.ExceptionInformation[1]}),
-        2 => stderr.print("Illegal instruction at address 0x{x}\n", .{regs.ip}),
+        2 => stderr.print("Illegal instruction at address 0x{x}\n", .{info.ContextRecord.getRegs().ip}),
         else => unreachable,
     } catch os.abort();
 
-    dumpStackTraceFromBase(&regs);
+    dumpStackTraceFromBase(info.ContextRecord);
 }
 
 pub fn dumpStackPointerAddr(prefix: []const u8) void {
lib/std/dwarf.zig
@@ -1577,9 +1577,9 @@ pub const DwarfInfo = struct {
         }
     }
 
-    pub fn unwindFrame(di: *const DwarfInfo, allocator: mem.Allocator, context: *UnwindContext, module_base_address: usize) !void {
+    pub fn unwindFrame(di: *const DwarfInfo, allocator: mem.Allocator, context: *UnwindContext, module_base_address: usize) !usize {
         if (!comptime abi.isSupportedArch(builtin.target.cpu.arch)) return error.UnsupportedCpuArchitecture;
-        if (context.pc == 0) return;
+        if (context.pc == 0) return 0;
 
         // TODO: Handle unwinding from a signal frame (ie. use_prev_instr in libunwind)
 
@@ -1626,8 +1626,11 @@ pub const DwarfInfo = struct {
         }
 
         context.vm.reset();
+        context.reg_ctx.eh_frame = cie.version != 4;
+
+        _ = try context.vm.runToNative(allocator, mapped_pc, cie, fde);
+        const row = &context.vm.current_row;
 
-        const row = try context.vm.runToNative(allocator, mapped_pc, cie, fde);
         context.cfa = switch (row.cfa.rule) {
             .val_offset => |offset| blk: {
                 const register = row.cfa.register orelse return error.InvalidCFARule;
@@ -1650,7 +1653,7 @@ pub const DwarfInfo = struct {
         var next_ucontext = context.ucontext;
 
         var has_next_ip = false;
-        for (context.vm.rowColumns(row)) |column| {
+        for (context.vm.rowColumns(row.*)) |column| {
             if (column.register) |register| {
                 const dest = try abi.regBytes(&next_ucontext, register, context.reg_ctx);
                 if (register == cie.return_address_register) {
@@ -1670,6 +1673,14 @@ pub const DwarfInfo = struct {
         }
 
         mem.writeIntSliceNative(usize, try abi.regBytes(&context.ucontext, abi.spRegNum(context.reg_ctx), context.reg_ctx), context.cfa.?);
+
+        // The call instruction will have pushed the address of the instruction that follows the call as the return address
+        // However, this return address may be past the end of the function if the caller was `noreturn`.
+        // TODO: Check this on non-x86_64
+        const return_address = context.pc;
+        if (context.pc > 0) context.pc -= 1;
+
+        return return_address;
     }
 };
 
src/crash_report.zig
@@ -233,10 +233,9 @@ fn handleSegfaultWindows(info: *os.windows.EXCEPTION_POINTERS) callconv(os.windo
 fn handleSegfaultWindowsExtra(info: *os.windows.EXCEPTION_POINTERS, comptime msg: WindowsSegfaultMessage) noreturn {
     PanicSwitch.preDispatch();
 
-    const stack_ctx = if (@hasDecl(os.windows, "CONTEXT")) ctx: {
-        const regs = info.ContextRecord.getRegs();
-        break :ctx StackContext{ .exception = regs };
-    } else ctx: {
+    const stack_ctx = if (@hasDecl(os.windows, "CONTEXT"))
+        StackContext{ .exception = info.ContextRecord }
+    else ctx: {
         const addr = @intFromPtr(info.ExceptionRecord.ExceptionAddress);
         break :ctx StackContext{ .current = .{ .ret_addr = addr } };
     };
@@ -251,7 +250,7 @@ fn handleSegfaultWindowsExtra(info: *os.windows.EXCEPTION_POINTERS, comptime msg
         },
         .illegal_instruction => {
             const ip: ?usize = switch (stack_ctx) {
-                .exception => |ex| ex.ip,
+                .exception => |ex| ex.getRegs().ip,
                 .current => |cur| cur.ret_addr,
                 .not_supported => null,
             };
@@ -272,7 +271,7 @@ const StackContext = union(enum) {
     current: struct {
         ret_addr: ?usize,
     },
-    exception: debug.StackTraceContext,
+    exception: *const debug.StackTraceContext,
     not_supported: void,
 
     pub fn dumpStackTrace(ctx: @This()) void {