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