authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-09-12 03:09:37+01:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-09-30 13:44:53+01:00
loge6eccc3c8f075b6feed6e089564fd6495758dd9d
tree595df39db5f1ab92d7d3190bc4823484cb8d7a95
parent1a8a8c610d9d5256df25090b0c8ca47cbe94ef1b
signaturelock-open Commit is signed but in an unrecognized format.

SelfInfo: remove x86-windows unwinding path

Turns out that RtlCaptureStackBackTrace is actually just doing FP (ebp) unwinding under the hood, making this logic completely redundant with our own FP-walking implementation; see added comment for details.

1 files changed, 46 insertions(+), 74 deletions(-)

lib/std/debug/SelfInfo/WindowsModule.zig+46-74
...@@ -264,86 +264,58 @@ pub const DebugInfo = struct {...@@ -264,86 +264,58 @@ pub const DebugInfo = struct {
264 }264 }
265};265};
266266
267pub const supports_unwinding: bool = true;267pub const supports_unwinding: bool = switch (builtin.cpu.arch) {
268pub const UnwindContext = switch (builtin.cpu.arch) {268 else => true,
269 .x86 => struct {269 // On x86, `RtlVirtualUnwind` does not exist. We could in theory use `RtlCaptureStackBackTrace`
270 pc: usize,270 // instead, but on x86, it turns out that function is just... doing FP unwinding with esp! It's
271 frames: []usize,271 // hard to find implementation details to confirm that, but the most authoritative source I have
272 frames_capacity: usize,272 // is an entry in the LLVM mailing list from 2020/08/16 which contains this quote:
273 next_index: usize,273 //
274 /// Marked `noinline` to ensure that `RtlCaptureStackBackTrace` includes our caller.274 // > x86 doesn't have what most architectures would consider an "unwinder" in the sense of
275 pub noinline fn init(ctx: *windows.CONTEXT, gpa: Allocator) Allocator.Error!UnwindContext {275 // > restoring registers; there is simply a linked list of frames that participate in SEH and
276 const frames_buf = try gpa.alloc(usize, 1024);276 // > that desire to be called for a dynamic unwind operation, so RtlCaptureStackBackTrace
277 errdefer comptime unreachable;277 // > assumes that EBP-based frames are in use and walks an EBP-based frame chain on x86 - not
278 const frames_len = windows.ntdll.RtlCaptureStackBackTrace(0, frames_buf.len, @ptrCast(frames_buf.ptr), null);278 // > all x86 code is written with EBP-based frames so while even though we generally build the
279 const regs = ctx.getRegs();279 // > OS that way, you might always run the risk of encountering external code that uses EBP as a
280 const first_index = for (frames_buf[0..frames_len], 0..) |ret_addr, idx| {280 // > general purpose register for which such an unwind attempt for a stack trace would fail.
281 if (ret_addr == regs.ip) break idx;281 //
282 } else i: {282 // Regardless, it's easy to effectively confirm this hypothesis just by compiling some code with
283 // If we were called by an exception handler, `regs.ip` wasn't in the trace because283 // `-fomit-frame-pointer -OReleaseFast` and observing that `RtlCaptureStackBackTrace` returns an
284 // RtlCaptureStackBackTrace omits the KiUserExceptionDispatcher frame, which is the284 // empty trace when it's called in such an application. Note that without `-OReleaseFast` or
285 // one in `regs.ip`. In that case, we have to start one frame shallower instead, and285 // similar, LLVM seems reluctant to ever clobber ebp, so you'll get a trace returned which just
286 // we can figure out that frame's ip from the context's bp.286 // contains all of the kernel32/ntdll frames but none of your own. Don't be deceived---this is
287 const start_addr_ptr: *const usize = @ptrFromInt(regs.bp + 4);287 // just coincidental!
288 const start_addr = start_addr_ptr.*;288 //
289 for (frames_buf[0..frames_len], 0..) |ret_addr, idx| {289 // Anyway, the point is, the only stack walking primitive on x86-windows is FP unwinding. We
290 if (ret_addr == start_addr) break :i idx;290 // *could* ask Microsoft to do that for us with `RtlCaptureStackBackTrace`... but better to just
291 }291 // use our existing FP unwinder in `std.debug`!
292 // The IP in the context can't be found; return an empty trace.292 .x86 => false,
293 gpa.free(frames_buf);293};
294 return .{ .pc = 0, .frames = &.{}, .frames_capacity = 0, .next_index = 0 };294pub const UnwindContext = struct {
295 };295 pc: usize,
296 return .{296 cur: windows.CONTEXT,
297 .pc = @returnAddress(),297 history_table: windows.UNWIND_HISTORY_TABLE,
298 .frames = frames_buf[0..frames_len],298 pub fn init(ctx: *const windows.CONTEXT, gpa: Allocator) Allocator.Error!UnwindContext {
299 .frames_capacity = 0,299 _ = gpa;
300 .next_index = first_index,300 return .{
301 };301 .pc = @returnAddress(),
302 }302 .cur = ctx.*,
303 pub fn deinit(ctx: *UnwindContext, gpa: Allocator) void {303 .history_table = std.mem.zeroes(windows.UNWIND_HISTORY_TABLE),
304 gpa.free(ctx.frames.ptr[0..ctx.frames_capacity]);304 };
305 ctx.* = undefined;305 }
306 }306 pub fn deinit(ctx: *UnwindContext, gpa: Allocator) void {
307 pub fn getFp(ctx: *UnwindContext) usize {307 _ = ctx;
308 _ = ctx;308 _ = gpa;
309 return 0;309 }
310 }310 pub fn getFp(ctx: *UnwindContext) usize {
311 },311 return ctx.cur.getRegs().bp;
312 else => struct {312 }
313 pc: usize,
314 cur: windows.CONTEXT,
315 history_table: windows.UNWIND_HISTORY_TABLE,
316 pub fn init(ctx: *const windows.CONTEXT, gpa: Allocator) Allocator.Error!UnwindContext {
317 _ = gpa;
318 return .{
319 .pc = @returnAddress(),
320 .cur = ctx.*,
321 .history_table = std.mem.zeroes(windows.UNWIND_HISTORY_TABLE),
322 };
323 }
324 pub fn deinit(ctx: *UnwindContext, gpa: Allocator) void {
325 _ = ctx;
326 _ = gpa;
327 }
328 pub fn getFp(ctx: *UnwindContext) usize {
329 return ctx.cur.getRegs().bp;
330 }
331 },
332};313};
333pub fn unwindFrame(module: *const WindowsModule, gpa: Allocator, di: *DebugInfo, context: *UnwindContext) !usize {314pub fn unwindFrame(module: *const WindowsModule, gpa: Allocator, di: *DebugInfo, context: *UnwindContext) !usize {
334 _ = module;315 _ = module;
335 _ = gpa;316 _ = gpa;
336 _ = di;317 _ = di;
337318
338 if (builtin.cpu.arch == .x86) {
339 const i = context.next_index;
340 if (i == context.frames.len) return 0;
341 context.next_index += 1;
342 const ip = context.frames[i];
343 context.pc = ip -| 1;
344 return ip;
345 }
346
347 const current_regs = context.cur.getRegs();319 const current_regs = context.cur.getRegs();
348 var image_base: windows.DWORD64 = undefined;320 var image_base: windows.DWORD64 = undefined;
349 if (windows.ntdll.RtlLookupFunctionEntry(current_regs.ip, &image_base, &context.history_table)) |runtime_function| {321 if (windows.ntdll.RtlLookupFunctionEntry(current_regs.ip, &image_base, &context.history_table)) |runtime_function| {