From 89e51da1ec537a5e30b547a716e30a311bfffe45 Mon Sep 17 00:00:00 2001 From: Tudor Andrei Dicu Date: Wed, 16 Sep 2026 12:15:05 +0300 Subject: [PATCH 1/2] Stack traces are back --- core/src/microzig.zig | 16 +++++- core/src/utilities.zig | 121 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 132 insertions(+), 5 deletions(-) diff --git a/core/src/microzig.zig b/core/src/microzig.zig index 49d92f372..b9b083866 100644 --- a/core/src/microzig.zig +++ b/core/src/microzig.zig @@ -3,6 +3,7 @@ //! //! But microzig employs a proxy tactic +const builtin = @import("builtin"); const std = @import("std"); const root = @import("root"); @@ -49,10 +50,17 @@ pub fn assert(expr: bool, opts: AssertOptions) void { pub const panic = std.debug.FullPanic(struct { pub fn panic_fn(message: []const u8, first_trace_address: ?usize) noreturn { std.log.err("panic: {s}", .{message}); - _ = first_trace_address; - // TODO: we no longer have StackIterator available to us, so we need to - // create our own. + var trace_index: usize = 0; + if (@errorReturnTrace()) |trace| trace_index = utilities.dump_error_trace(trace); + + // Skip if we can't use fp based stack tracing. + if (options.panic_stack_trace) { + var it: utilities.StackIterator = .init(first_trace_address orelse @returnAddress(), null); + while (it.next()) |address| : (trace_index += 1) { + utilities.dump_trace_line(trace_index, address); + } + } // Attach a breakpoint. this might trigger another panic internally, so // only do that if requested. @@ -85,6 +93,8 @@ pub const Options = struct { /// reduce code size as the string literals for error names no longer have to /// be included in the executable. simple_panic_if_main_errors: bool = false, + + panic_stack_trace: bool = builtin.optimize.runtimeSafety(), }; pub const options: Options = if (@hasDecl(root, "microzig_options")) root.microzig_options else .{}; diff --git a/core/src/utilities.zig b/core/src/utilities.zig index 17d6323b7..a806de8e6 100644 --- a/core/src/utilities.zig +++ b/core/src/utilities.zig @@ -1,3 +1,4 @@ +const builtin = @import("builtin"); const std = @import("std"); const assert = std.debug.assert; @@ -280,7 +281,7 @@ pub fn GenerateInterruptOptions(sources: []const Source) type { var field_names: [count][]const u8 = undefined; var field_types: [count]type = undefined; - var field_attrs: [count]std.builtin.Type.Struct.FieldAttributes = undefined; + var field_attrs: [count]std.lang.Type.Struct.FieldAttributes = undefined; var i: usize = 0; for (sources) |source| { @@ -472,7 +473,7 @@ test "SliceVector.Iterator.next_chunk" { } } -pub fn dump_stack_trace(trace: *std.builtin.StackTrace) usize { +pub fn dump_stack_trace(trace: *std.lang.StackTrace) usize { const frame_count = @min(trace.index, trace.instruction_addresses.len); var frame_index: usize = 0; @@ -704,3 +705,119 @@ pub fn IntFracDiv(int_bits: comptime_int, frac_bits: comptime_int) type { } }; } + +pub fn dump_error_trace(trace: *std.lang.StackTrace) usize { + const frame_count = @min(trace.index, trace.instruction_addresses.len); + + var frame_index: usize = 0; + var frames_left: usize = frame_count; + while (frames_left != 0) : ({ + frames_left -= 1; + frame_index = (frame_index + 1) % trace.instruction_addresses.len; + }) { + const address = trace.instruction_addresses[frame_index]; + dump_trace_line(frame_index, address); + } + + return frame_count; +} + +pub fn dump_trace_line(index: usize, address: usize) void { + std.log.err("{d: >3}: 0x{X:0>8}", .{ index, address }); +} + +pub const StackIterator = struct { + const native_arch = builtin.cpu.arch; + + // Last known value of the frame pointer register. + fp: usize, + first_address: ?usize, + + pub fn init(first_address: ?usize, fp: ?usize) StackIterator { + if (native_arch.isSPARC()) { + // Flush all the register windows on stack. + asm volatile (if (builtin.cpu.has(.sparc, .v9)) + "flushw" + else + "ta 3" // ST_FLUSH_WINDOWS + ::: .{ .memory = true }); + } + + return .{ + .first_address = first_address, + // TODO: this is a workaround for #16876 + //.fp = fp orelse @frameAddress(), + .fp = fp orelse blk: { + const fa = @frameAddress(); + break :blk fa; + }, + }; + } + + // Offset of the saved BP wrt the frame pointer. + const fp_offset = if (native_arch.isRISCV()) + // On RISC-V the frame pointer points to the top of the saved register + // area, on pretty much every other architecture it points to the stack + // slot where the previous frame pointer is saved. + 2 * @sizeOf(usize) + else if (native_arch.isSPARC()) + // On SPARC the previous frame pointer is stored at 14 slots past %fp+BIAS. + 14 * @sizeOf(usize) + else + 0; + + const fp_bias = if (native_arch.isSPARC()) + // On SPARC frame pointers are biased by a constant. + 2047 + else + 0; + + // Positive offset of the saved PC wrt the frame pointer. + const pc_offset = if (native_arch == .powerpc64le) + 2 * @sizeOf(usize) + else + @sizeOf(usize); + + pub fn next(it: *StackIterator) ?usize { + var address = it.next_internal() orelse return null; + + if (it.first_address) |first_address| { + while (address != first_address) { + address = it.next_internal() orelse return null; + } + it.first_address = null; + } + + return address; + } + + pub fn next_internal(it: *StackIterator) ?usize { + if (builtin.omit_frame_pointer) return null; + + const fp = if (comptime native_arch.isSPARC()) + // On SPARC the offset is positive. (!) + std.math.add(usize, it.fp, fp_offset) catch return null + else + std.math.sub(usize, it.fp, fp_offset) catch return null; + + // Sanity check. + if (fp == 0 or !std.mem.isAligned(fp, @alignOf(usize))) return null; + const new_fp = std.math.add(usize, load(usize, fp), fp_bias) catch + return null; + + // Sanity check: the stack grows down thus all the parent frames must be + // be at addresses that are greater (or equal) than the previous one. + // A zero frame pointer often signals this is the last frame, that case + // is gracefully handled by the next call to next_internal. + if (new_fp != 0 and new_fp < it.fp) return null; + const new_pc = load(usize, std.math.add(usize, fp, pc_offset) catch return null); + + it.fp = new_fp; + + return new_pc; + } + + fn load(T: type, address: usize) T { + return @as(*const T, @ptrFromInt(address)).*; + } +}; From e086fb420bd2744c94531d3e2e0a58e4f67aa41a Mon Sep 17 00:00:00 2001 From: Tudor Andrei Dicu Date: Wed, 16 Sep 2026 12:18:09 +0300 Subject: [PATCH 2/2] Enable stack traces by default only in debug mode --- core/src/microzig.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/microzig.zig b/core/src/microzig.zig index b9b083866..f3b959e76 100644 --- a/core/src/microzig.zig +++ b/core/src/microzig.zig @@ -94,7 +94,7 @@ pub const Options = struct { /// be included in the executable. simple_panic_if_main_errors: bool = false, - panic_stack_trace: bool = builtin.optimize.runtimeSafety(), + panic_stack_trace: bool = builtin.optimize == .debug, }; pub const options: Options = if (@hasDecl(root, "microzig_options")) root.microzig_options else .{};