From 0ace906477d0dd1d6e7cb4a02180fc6c09684870 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20R=C3=B8nne=20Petersen?= Date: Tue, 14 Oct 2025 09:28:09 +0200 Subject: [PATCH 01/14] std.os.windows.CONTEXT: add sp field to getRegs() result for x86 --- lib/std/os/windows.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index 932e401758..4a2487c836 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -4229,8 +4229,8 @@ pub const CONTEXT = switch (native_arch) { SegSs: DWORD, ExtendedRegisters: [512]BYTE, - pub fn getRegs(ctx: *const CONTEXT) struct { bp: usize, ip: usize } { - return .{ .bp = ctx.Ebp, .ip = ctx.Eip }; + pub fn getRegs(ctx: *const CONTEXT) struct { bp: usize, ip: usize, sp: usize } { + return .{ .bp = ctx.Ebp, .ip = ctx.Eip, .sp = ctx.Esp }; } }, .x86_64 => extern struct { From f21a78b5a31b13d5b6bb84c23c4a35e64402d0e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20R=C3=B8nne=20Petersen?= Date: Tue, 14 Oct 2025 23:18:20 +0200 Subject: [PATCH 02/14] std.debug.SelfInfo.Elf: don't support DWARF unwinding for Hexagon and PowerPC As for SPARC, FP-based unwinding is superior on these. --- lib/std/debug/SelfInfo/Elf.zig | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/lib/std/debug/SelfInfo/Elf.zig b/lib/std/debug/SelfInfo/Elf.zig index f10dc3cd63..bee04a5eef 100644 --- a/lib/std/debug/SelfInfo/Elf.zig +++ b/lib/std/debug/SelfInfo/Elf.zig @@ -94,28 +94,22 @@ pub const can_unwind: bool = s: { // Notably, we are yet to support unwinding on ARM. There, unwinding is not done through // `.eh_frame`, but instead with the `.ARM.exidx` section, which has a different format. const archs: []const std.Target.Cpu.Arch = switch (builtin.target.os.tag) { - // Not supported yet: arm, m68k, sparc64 + // Not supported yet: arm, m68k .haiku => &.{ .aarch64, - .powerpc, .riscv64, .x86, .x86_64, }, - // Not supported yet: arc, arm/armeb/thumb/thumbeb, csky, m68k, or1k, sparc/sparc64, xtensa + // Not supported yet: arc, arm/armeb/thumb/thumbeb, csky, m68k, or1k, xtensa .linux => &.{ .aarch64, .aarch64_be, - .hexagon, .loongarch64, .mips, .mipsel, .mips64, .mips64el, - .powerpc, - .powerpcle, - .powerpc64, - .powerpc64le, .riscv32, .riscv64, .s390x, @@ -134,28 +128,23 @@ pub const can_unwind: bool = s: { // Not supported yet: arm .freebsd => &.{ .aarch64, - .powerpc64, - .powerpc64le, .riscv64, .x86_64, }, - // Not supported yet: arm/armeb, m68k, mips64/mips64el, sparc/sparc64 + // Not supported yet: arm/armeb, m68k, mips64/mips64el .netbsd => &.{ .aarch64, .aarch64_be, .mips, .mipsel, - .powerpc, .x86, .x86_64, }, - // Not supported yet: arm, sparc64 + // Not supported yet: arm .openbsd => &.{ .aarch64, .mips64, .mips64el, - .powerpc, - .powerpc64, .riscv64, .x86, .x86_64, @@ -165,7 +154,6 @@ pub const can_unwind: bool = s: { .x86, .x86_64, }, - // Not supported yet: sparc64 .solaris => &.{ .x86_64, }, From 3d3aff0da9ef2af703a81097b9ca73de9b89adae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20R=C3=B8nne=20Petersen?= Date: Tue, 14 Oct 2025 09:37:51 +0200 Subject: [PATCH 03/14] std.debug: flush SPARC register windows from a new window flushw and ta 3 flush all windows *except* the current one. So we need to do this in a new register window to get all of the ones we care about. --- lib/std/debug.zig | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/std/debug.zig b/lib/std/debug.zig index 7ac6c45903..3f694bec1c 100644 --- a/lib/std/debug.zig +++ b/lib/std/debug.zig @@ -807,14 +807,7 @@ const StackIterator = union(enum) { /// `@frameAddress` and `cpu_context.Native.current` as the caller's stack frame and /// our own are one and the same. inline fn init(opt_context_ptr: ?CpuContextPtr) error{CannotUnwindFromContext}!StackIterator { - if (builtin.cpu.arch.isSPARC()) { - // Flush all the register windows on stack. - if (builtin.cpu.has(.sparc, .v9)) { - asm volatile ("flushw" ::: .{ .memory = true }); - } else { - asm volatile ("ta 3" ::: .{ .memory = true }); // ST_FLUSH_WINDOWS - } - } + flushSparcWindows(); if (opt_context_ptr) |context_ptr| { if (SelfInfo == void or !SelfInfo.can_unwind) return error.CannotUnwindFromContext; // Use `di_first` here so we report the PC in the context before unwinding any further. @@ -842,6 +835,15 @@ const StackIterator = union(enum) { } } + noinline fn flushSparcWindows() void { + // Flush all register windows except the current one (hence `noinline`). This ensures that + // we actually see meaningful data on the stack when we walk the frame chain. + if (comptime builtin.target.cpu.has(.sparc, .v9)) + asm volatile ("flushw" ::: .{ .memory = true }) + else + asm volatile ("ta 3" ::: .{ .memory = true }); // ST_FLUSH_WINDOWS + } + const FpUsability = enum { /// FP unwinding is impractical on this target. For example, due to its very silly ABI /// design decisions, it's not possible to do generic FP unwinding on MIPS without a From 12b1d57df1e0efed6e5a3eb3933de7f08d42d6d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20R=C3=B8nne=20Petersen?= Date: Tue, 14 Oct 2025 09:32:12 +0200 Subject: [PATCH 04/14] std.debug.cpu_context: add Sparc context --- lib/std/debug/cpu_context.zig | 88 +++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/lib/std/debug/cpu_context.zig b/lib/std/debug/cpu_context.zig index dc77b41c9d..3450debba0 100644 --- a/lib/std/debug/cpu_context.zig +++ b/lib/std/debug/cpu_context.zig @@ -10,6 +10,7 @@ else switch (native_arch) { .loongarch32, .loongarch64 => LoongArch, .mips, .mipsel, .mips64, .mips64el => Mips, .powerpc, .powerpcle, .powerpc64, .powerpc64le => Powerpc, + .sparc, .sparc64 => Sparc, .riscv32, .riscv32be, .riscv64, .riscv64be => Riscv, .s390x => S390x, .x86 => X86, @@ -858,6 +859,93 @@ const Powerpc = extern struct { } }; +/// This is an `extern struct` so that inline assembly in `current` can use field offsets. +const Sparc = extern struct { + g: [8]Gpr, + o: [8]Gpr, + l: [8]Gpr, + i: [8]Gpr, + pc: Gpr, + + pub const Gpr = if (native_arch == .sparc64) u64 else u32; + + pub inline fn current() Sparc { + var ctx: Sparc = undefined; + asm volatile (if (Gpr == u64) + \\ stx %g0, [%l0 + 0] + \\ stx %g1, [%l0 + 8] + \\ stx %g2, [%l0 + 16] + \\ stx %g3, [%l0 + 24] + \\ stx %g4, [%l0 + 32] + \\ stx %g5, [%l0 + 40] + \\ stx %g6, [%l0 + 48] + \\ stx %g7, [%l0 + 56] + \\ stx %o0, [%l0 + 64] + \\ stx %o1, [%l0 + 72] + \\ stx %o2, [%l0 + 80] + \\ stx %o3, [%l0 + 88] + \\ stx %o4, [%l0 + 96] + \\ stx %o5, [%l0 + 104] + \\ stx %o6, [%l0 + 112] + \\ stx %o7, [%l0 + 120] + \\ stx %l0, [%l0 + 128] + \\ stx %l1, [%l0 + 136] + \\ stx %l2, [%l0 + 144] + \\ stx %l3, [%l0 + 152] + \\ stx %l4, [%l0 + 160] + \\ stx %l5, [%l0 + 168] + \\ stx %l6, [%l0 + 176] + \\ stx %l7, [%l0 + 184] + \\ stx %i0, [%l0 + 192] + \\ stx %i1, [%l0 + 200] + \\ stx %i2, [%l0 + 208] + \\ stx %i3, [%l0 + 216] + \\ stx %i4, [%l0 + 224] + \\ stx %i5, [%l0 + 232] + \\ stx %i6, [%l0 + 240] + \\ stx %i7, [%l0 + 248] + \\ call 1f + \\ stx %o7, [%l0 + 256] + \\1: + else + \\ std %g0, [%l0 + 0] + \\ std %g2, [%l0 + 8] + \\ std %g4, [%l0 + 16] + \\ std %g6, [%l0 + 24] + \\ std %o0, [%l0 + 32] + \\ std %o2, [%l0 + 40] + \\ std %o4, [%l0 + 48] + \\ std %o6, [%l0 + 56] + \\ std %l0, [%l0 + 64] + \\ std %l2, [%l0 + 72] + \\ std %l4, [%l0 + 80] + \\ std %l6, [%l0 + 88] + \\ std %i0, [%l0 + 96] + \\ std %i2, [%l0 + 104] + \\ std %i4, [%l0 + 112] + \\ std %i6, [%l0 + 120] + \\ call 1f + \\ st %o7, [%l0 + 128] + \\1: + : + : [gprs] "{l0}" (&ctx), + : .{ .o7 = true, .memory = true }); + return ctx; + } + + pub fn dwarfRegisterBytes(ctx: *Sparc, register_num: u16) DwarfRegisterError![]u8 { + switch (register_num) { + 0...7 => return @ptrCast(&ctx.g[register_num]), + 8...15 => return @ptrCast(&ctx.o[register_num - 8]), + 16...23 => return @ptrCast(&ctx.l[register_num - 16]), + 24...31 => return @ptrCast(&ctx.i[register_num - 24]), + 32 => return @ptrCast(&ctx.pc), + + else => return error.InvalidRegister, + } + } +}; + /// This is an `extern struct` so that inline assembly in `current` can use field offsets. const Riscv = extern struct { /// The numbered general-purpose registers r0 - r31. r0 must be zero. From b8dd40fde8aed212ffa8a98b45821ccdf0c2e17d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20R=C3=B8nne=20Petersen?= Date: Tue, 14 Oct 2025 09:39:15 +0200 Subject: [PATCH 05/14] std.debug.cpu_context.Sparc: flush register windows in current() It's better to do this here than in StackIterator.init() so that std.debug.cpu_context.Native.current() isn't a footgun on SPARC. --- lib/std/debug.zig | 2 +- lib/std/debug/cpu_context.zig | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/std/debug.zig b/lib/std/debug.zig index 3f694bec1c..8501df175a 100644 --- a/lib/std/debug.zig +++ b/lib/std/debug.zig @@ -807,7 +807,6 @@ const StackIterator = union(enum) { /// `@frameAddress` and `cpu_context.Native.current` as the caller's stack frame and /// our own are one and the same. inline fn init(opt_context_ptr: ?CpuContextPtr) error{CannotUnwindFromContext}!StackIterator { - flushSparcWindows(); if (opt_context_ptr) |context_ptr| { if (SelfInfo == void or !SelfInfo.can_unwind) return error.CannotUnwindFromContext; // Use `di_first` here so we report the PC in the context before unwinding any further. @@ -826,6 +825,7 @@ const StackIterator = union(enum) { // in our caller's frame and above. return .{ .di = .init(&.current()) }; } + flushSparcWindows(); return .{ .fp = @frameAddress() }; } fn deinit(si: *StackIterator) void { diff --git a/lib/std/debug/cpu_context.zig b/lib/std/debug/cpu_context.zig index 3450debba0..5dd2e283ed 100644 --- a/lib/std/debug/cpu_context.zig +++ b/lib/std/debug/cpu_context.zig @@ -870,6 +870,8 @@ const Sparc = extern struct { pub const Gpr = if (native_arch == .sparc64) u64 else u32; pub inline fn current() Sparc { + flushWindows(); + var ctx: Sparc = undefined; asm volatile (if (Gpr == u64) \\ stx %g0, [%l0 + 0] @@ -933,6 +935,15 @@ const Sparc = extern struct { return ctx; } + noinline fn flushWindows() void { + // Flush all register windows except the current one (hence `noinline`). This ensures that + // we actually see meaningful data on the stack when we walk the frame chain. + if (comptime builtin.target.cpu.has(.sparc, .v9)) + asm volatile ("flushw" ::: .{ .memory = true }) + else + asm volatile ("ta 3" ::: .{ .memory = true }); // ST_FLUSH_WINDOWS + } + pub fn dwarfRegisterBytes(ctx: *Sparc, register_num: u16) DwarfRegisterError![]u8 { switch (register_num) { 0...7 => return @ptrCast(&ctx.g[register_num]), From b2732645b7ffc08d9227bd5d82e1de1a6fbedb8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20R=C3=B8nne=20Petersen?= Date: Tue, 14 Oct 2025 09:44:42 +0200 Subject: [PATCH 06/14] std.debug.cpu_context: add sparc*-linux context conversion support It's not really a ucontext_t at all. Lovely stuff. --- lib/std/debug/cpu_context.zig | 69 ++++++++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 13 deletions(-) diff --git a/lib/std/debug/cpu_context.zig b/lib/std/debug/cpu_context.zig index 5dd2e283ed..b08ab49778 100644 --- a/lib/std/debug/cpu_context.zig +++ b/lib/std/debug/cpu_context.zig @@ -40,6 +40,26 @@ pub fn fromPosixSignalContext(ctx_ptr: ?*const anyopaque) ?Native { }, .pc = @truncate(uc.mcontext.pc), }; + } else if (native_arch.isSPARC() and native_os == .linux) { + const SparcStackFrame = extern struct { + l: [8]usize, + i: [8]usize, + _x: [8]usize, + }; + + // When invoking a signal handler, the kernel builds an `rt_signal_frame` structure on the + // stack and passes a pointer to its `info` field to the signal handler. This implies that + // prior to said `info` field, we will find the `ss` field which, among other things, + // contains the incoming and local registers of the interrupted code. + const frame = @as(*const SparcStackFrame, @ptrFromInt(@as(usize, @intFromPtr(ctx_ptr)) - @sizeOf(SparcStackFrame))); + + return .{ + .g = uc.mcontext.g, + .o = uc.mcontext.o, + .l = frame.l, + .i = frame.i, + .pc = uc.mcontext.pc, + }; } // Only unified conversions from here. @@ -1370,9 +1390,32 @@ const signal_ucontext_t = switch (native_os) { lr: u32, }, }, - // https://github.com/torvalds/linux/blob/cd5a0afbdf8033dc83786315d63f8b325bdba2fd/arch/sparc/include/uapi/asm/uctx.h - .sparc => @compileError("sparc-linux ucontext_t missing"), - .sparc64 => @compileError("sparc64-linux ucontext_t missing"), + // https://github.com/torvalds/linux/blob/cd5a0afbdf8033dc83786315d63f8b325bdba2fd/arch/sparc/kernel/signal_32.c#L48-L49 + .sparc => extern struct { + // Not actually a `ucontext_t` at all because, uh, reasons? + + _info: std.os.linux.siginfo_t, + mcontext: extern struct { + _psr: u32, + pc: u32, + _npc: u32, + _y: u32, + g: [8]u32, + o: [8]u32, + }, + }, + // https://github.com/torvalds/linux/blob/cd5a0afbdf8033dc83786315d63f8b325bdba2fd/arch/sparc/kernel/signal_64.c#L247-L248 + .sparc64 => extern struct { + // Ditto... + + _info: std.os.linux.siginfo_t, + mcontext: extern struct { + g: [8]u64, + o: [8]u64, + _tstate: u64, + pc: u64, + }, + }, else => unreachable, }, // https://github.com/freebsd/freebsd-src/blob/55c28005f544282b984ae0e15dacd0c108d8ab12/sys/sys/_ucontext.h @@ -1497,14 +1540,14 @@ const signal_ucontext_t = switch (native_os) { }, }, // This needs to be audited by someone with access to the Solaris headers. - .solaris => extern struct { - _flags: u64, - _link: ?*signal_ucontext_t, - _sigmask: std.c.sigset_t, - _stack: std.c.stack_t, - mcontext: switch (native_arch) { - .sparc64 => @compileError("sparc64-solaris mcontext_t missing"), - .x86_64 => extern struct { + .solaris => switch (native_arch) { + .sparc64 => @compileError("sparc64-solaris ucontext_t missing"), + .x86_64 => extern struct { + _flags: u64, + _link: ?*signal_ucontext_t, + _sigmask: std.c.sigset_t, + _stack: std.c.stack_t, + mcontext: extern struct { r15: u64, r14: u64, r13: u64, @@ -1524,8 +1567,8 @@ const signal_ucontext_t = switch (native_os) { _err: i64, rip: u64, }, - else => unreachable, }, + else => unreachable, }, // https://github.com/illumos/illumos-gate/blob/d4ce137bba3bd16823db6374d9e9a643264ce245/usr/src/uts/intel/sys/ucontext.h .illumos => extern struct { @@ -1637,7 +1680,7 @@ const signal_ucontext_t = switch (native_os) { }, }, // https://github.com/openbsd/src/blob/42468faed8369d07ae49ae02dd71ec34f59b66cd/sys/arch/sparc64/include/signal.h - .sparc64 => @compileError("sparc64-openbsd mcontext_t missing"), + .sparc64 => @compileError("sparc64-openbsd ucontext_t missing"), // https://github.com/openbsd/src/blob/42468faed8369d07ae49ae02dd71ec34f59b66cd/sys/arch/i386/include/signal.h .x86 => extern struct { mcontext: extern struct { From a36dab2f90020c5544fddf68520c2057edc2ec00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20R=C3=B8nne=20Petersen?= Date: Tue, 14 Oct 2025 09:41:20 +0200 Subject: [PATCH 07/14] std.debug.Dwarf: add SPARC register number mappings --- lib/std/debug/Dwarf.zig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/std/debug/Dwarf.zig b/lib/std/debug/Dwarf.zig index 98d7addb32..acd71eb4ed 100644 --- a/lib/std/debug/Dwarf.zig +++ b/lib/std/debug/Dwarf.zig @@ -1437,6 +1437,7 @@ pub fn ipRegNum(arch: std.Target.Cpu.Arch) ?u16 { .powerpc, .powerpcle, .powerpc64, .powerpc64le => 67, .riscv32, .riscv32be, .riscv64, .riscv64be => 65, .s390x => 65, + .sparc, .sparc64 => 32, .x86 => 8, .x86_64 => 16, else => null, @@ -1453,6 +1454,7 @@ pub fn fpRegNum(arch: std.Target.Cpu.Arch) u16 { .powerpc, .powerpcle, .powerpc64, .powerpc64le => 1, .riscv32, .riscv32be, .riscv64, .riscv64be => 8, .s390x => 11, + .sparc, .sparc64 => 30, .x86 => 5, .x86_64 => 6, else => unreachable, @@ -1469,6 +1471,7 @@ pub fn spRegNum(arch: std.Target.Cpu.Arch) u16 { .powerpc, .powerpcle, .powerpc64, .powerpc64le => 1, .riscv32, .riscv32be, .riscv64, .riscv64be => 2, .s390x => 15, + .sparc, .sparc64 => 14, .x86 => 4, .x86_64 => 7, else => unreachable, From 62a8cfd5fed1380331b10d20fbb7b08f206cf541 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20R=C3=B8nne=20Petersen?= Date: Tue, 14 Oct 2025 23:07:22 +0200 Subject: [PATCH 08/14] std.debug: fix an invalid read in StackIterator.next() We're overwriting the memory that unwind_context sits in, so we need to do the getFp() call earlier. --- lib/std/debug.zig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/std/debug.zig b/lib/std/debug.zig index 8501df175a..83322e8a6e 100644 --- a/lib/std/debug.zig +++ b/lib/std/debug.zig @@ -925,7 +925,8 @@ const StackIterator = union(enum) { const di_gpa = getDebugInfoAllocator(); const ret_addr = di.unwindFrame(di_gpa, unwind_context) catch |err| { const pc = unwind_context.pc; - it.* = .{ .fp = unwind_context.getFp() }; + const fp = unwind_context.getFp(); + it.* = .{ .fp = fp }; return .{ .switch_to_fp = .{ .address = pc, .err = err, From ebc0b90eb709b31e66daddfef26f32e36f169fc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20R=C3=B8nne=20Petersen?= Date: Tue, 14 Oct 2025 09:54:48 +0200 Subject: [PATCH 09/14] std.debug: rename some constants for clarity --- lib/std/debug.zig | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/lib/std/debug.zig b/lib/std/debug.zig index 83322e8a6e..b47aa540d2 100644 --- a/lib/std/debug.zig +++ b/lib/std/debug.zig @@ -938,8 +938,8 @@ const StackIterator = union(enum) { .fp => |fp| { if (fp == 0) return .end; // we reached the "sentinel" base pointer - const bp_addr = applyOffset(fp, bp_offset) orelse return .end; - const ra_addr = applyOffset(fp, ra_offset) orelse return .end; + const bp_addr = applyOffset(fp, fp_to_bp_offset) orelse return .end; + const ra_addr = applyOffset(fp, fp_to_ra_offset) orelse return .end; if (bp_addr == 0 or !mem.isAligned(bp_addr, @alignOf(usize)) or ra_addr == 0 or !mem.isAligned(ra_addr, @alignOf(usize))) @@ -950,7 +950,7 @@ const StackIterator = union(enum) { const bp_ptr: *const usize = @ptrFromInt(bp_addr); const ra_ptr: *const usize = @ptrFromInt(ra_addr); - const bp = applyOffset(bp_ptr.*, bp_bias) orelse return .end; + const bp = applyOffset(bp_ptr.*, stack_bias) orelse return .end; // The stack grows downards, so `bp > fp` should always hold. If it doesn't, this // frame is invalid, so we'll treat it as though it we reached end of stack. The @@ -967,29 +967,35 @@ const StackIterator = union(enum) { } /// Offset of the saved base pointer (previous frame pointer) wrt the frame pointer. - const bp_offset = off: { - // 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. + const fp_to_bp_offset = off: { + // On LoongArch and RISC-V, the frame pointer points to the top of the saved register area, + // in which the base pointer is the first word. if (native_arch.isLoongArch() or native_arch.isRISCV()) break :off -2 * @sizeOf(usize); - // On SPARC the previous frame pointer is stored at 14 slots past %fp+BIAS. + // On SPARC, the frame pointer points to the save area which holds 16 slots for the local + // and incoming registers. The base pointer (i6) is stored in its customary save slot. if (native_arch.isSPARC()) break :off 14 * @sizeOf(usize); + // Everywhere else, the frame pointer points directly to the location of the base pointer. break :off 0; }; /// Offset of the saved return address wrt the frame pointer. - const ra_offset = off: { - if (native_arch.isLoongArch() or native_arch.isRISCV()) break :off -1 * @sizeOf(usize); - if (native_arch.isSPARC()) break :off 15 * @sizeOf(usize); + const fp_to_ra_offset = off: { + // On LoongArch and RISC-V, the frame pointer points to the top of the saved register area, + // in which the return address is the second word. + if (native_arch.isRISCV() or native_arch.isLoongArch()) break :off -1 * @sizeOf(usize); if (native_arch.isPowerPC64()) break :off 2 * @sizeOf(usize); // On s390x, r14 is the link register and we need to grab it from its customary slot in the // register save area (ELF ABI s390x Supplement ยง1.2.2.2). if (native_arch == .s390x) break :off 14 * @sizeOf(usize); + // On SPARC, the frame pointer points to the save area which holds 16 slots for the local + // and incoming registers. The return address (i7) is stored in its customary save slot. + if (native_arch.isSPARC()) break :off 15 * @sizeOf(usize); break :off @sizeOf(usize); }; - /// Value to add to a base pointer after loading it from the stack. Yes, SPARC really does this. - const bp_bias = bias: { + /// Value to add to the stack pointer and frame/base pointers to get the real location being + /// pointed to. Yes, SPARC really does this. + const stack_bias = bias: { if (native_arch.isSPARC()) break :bias 2047; break :bias 0; }; From 78bc5d46e07f238f7ec90b1ef99001278ccc2174 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20R=C3=B8nne=20Petersen?= Date: Tue, 14 Oct 2025 22:32:37 +0200 Subject: [PATCH 10/14] std.debug: the SPARC stack bias is only used on the 64-bit ABI --- lib/std/debug.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/std/debug.zig b/lib/std/debug.zig index b47aa540d2..942c21212c 100644 --- a/lib/std/debug.zig +++ b/lib/std/debug.zig @@ -996,7 +996,7 @@ const StackIterator = union(enum) { /// Value to add to the stack pointer and frame/base pointers to get the real location being /// pointed to. Yes, SPARC really does this. const stack_bias = bias: { - if (native_arch.isSPARC()) break :bias 2047; + if (native_arch == .sparc64) break :bias 2047; break :bias 0; }; From 6de2d61a0cbcf535b455770b4d7397d580eac6c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20R=C3=B8nne=20Petersen?= Date: Tue, 14 Oct 2025 09:49:20 +0200 Subject: [PATCH 11/14] std.debug: work around latest SPARC register window not being spilled on signal I have no idea if this is a QEMU bug or real kernel behavior. Either way, the register save area specifically exists for asynchronous spilling of incoming and local registers, so there should be no harm in doing this. --- lib/std/debug.zig | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lib/std/debug.zig b/lib/std/debug.zig index 942c21212c..5c6043c8c3 100644 --- a/lib/std/debug.zig +++ b/lib/std/debug.zig @@ -1438,6 +1438,22 @@ fn handleSegfaultPosix(sig: i32, info: *const posix.siginfo_t, ctx_ptr: ?*anyopa break :info .{ addr, name }; }; const opt_cpu_context: ?cpu_context.Native = cpu_context.fromPosixSignalContext(ctx_ptr); + + if (native_arch.isSPARC()) { + // It's unclear to me whether this is a QEMU bug or also real kernel behavior, but in the + // former, I observed that the most recent register window wasn't getting spilled on the + // stack as expected when a signal arrived. A `flushw` from the signal handler does not + // appear to be sufficient either. On the other hand, when doing a synchronous stack trace + // and using `flushw`, this all appears to work as expected. So, *probably* a QEMU bug, but + // someone with real SPARC hardware should verify. + // + // In any case, the register save area exists specifically so that register windows can be + // spilled asynchronously. This means that it should be perfectly fine for us to manually do + // so here. + const ctx = opt_cpu_context.?; + @as(*[16]usize, @ptrFromInt(ctx.o[6] + StackIterator.stack_bias)).* = ctx.l ++ ctx.i; + } + handleSegfault(addr, name, if (opt_cpu_context) |*ctx| ctx else null); } From 912fed338019487ec025ecbe3cd27134d75fe6e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20R=C3=B8nne=20Petersen?= Date: Tue, 14 Oct 2025 09:52:10 +0200 Subject: [PATCH 12/14] std.debug: use the SP as the initial FP on SPARC The FP would point to the register save area for the previous frame, while the SP points to the register save area for the current frame. So use the latter. --- lib/std/debug.zig | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/std/debug.zig b/lib/std/debug.zig index 5c6043c8c3..5e44214753 100644 --- a/lib/std/debug.zig +++ b/lib/std/debug.zig @@ -825,8 +825,19 @@ const StackIterator = union(enum) { // in our caller's frame and above. return .{ .di = .init(&.current()) }; } - flushSparcWindows(); - return .{ .fp = @frameAddress() }; + return .{ + // On SPARC, the frame pointer will point to the previous frame's save area, + // meaning we will read the previous return address and thus miss a frame. + // Instead, start at the stack pointer so we get the return address from the + // current frame's save area. The addition of the stack bias cannot fail here + // since we know we have a valid stack pointer. + .fp = if (native_arch.isSPARC()) sp: { + flushSparcWindows(); + break :sp asm ("" + : [_] "={o6}" (-> usize), + ) + stack_bias; + } else @frameAddress(), + }; } fn deinit(si: *StackIterator) void { switch (si.*) { From dd7819220af9f4eead99bc8fbd969c98323b8258 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20R=C3=B8nne=20Petersen?= Date: Tue, 14 Oct 2025 09:57:36 +0200 Subject: [PATCH 13/14] std.debug: fix return addresses being off on SPARC The return address points to the call instruction on SPARC, so the actual return address is 8 bytes after. This means that we shouldn't do the return address adjustment that we normally do. --- lib/std/debug.zig | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/std/debug.zig b/lib/std/debug.zig index 5e44214753..83df76a872 100644 --- a/lib/std/debug.zig +++ b/lib/std/debug.zig @@ -728,7 +728,7 @@ pub noinline fn writeCurrentStackTrace(options: StackUnwindOptions, writer: *Wri } // `ret_addr` is the return address, which is *after* the function call. // Subtract 1 to get an address *in* the function call for a better source location. - try printSourceAtAddress(di_gpa, di, writer, ret_addr -| 1, tty_config); + try printSourceAtAddress(di_gpa, di, writer, ret_addr -| StackIterator.ra_call_offset, tty_config); printed_any_frame = true; }, }; @@ -777,7 +777,7 @@ pub fn writeStackTrace(st: *const std.builtin.StackTrace, writer: *Writer, tty_c for (st.instruction_addresses[0..captured_frames]) |ret_addr| { // `ret_addr` is the return address, which is *after* the function call. // Subtract 1 to get an address *in* the function call for a better source location. - try printSourceAtAddress(di_gpa, di, writer, ret_addr -| 1, tty_config); + try printSourceAtAddress(di_gpa, di, writer, ret_addr -| StackIterator.ra_call_offset, tty_config); } if (n_frames > captured_frames) { tty_config.setColor(writer, .bold) catch {}; @@ -1011,6 +1011,13 @@ const StackIterator = union(enum) { break :bias 0; }; + /// On some oddball architectures, a return address points to the call instruction rather than + /// the instruction following it. + const ra_call_offset = off: { + if (native_arch.isSPARC()) break :off 0; + break :off 1; + }; + fn applyOffset(addr: usize, comptime off: comptime_int) ?usize { if (off >= 0) return math.add(usize, addr, off) catch return null; return math.sub(usize, addr, -off) catch return null; From e0f10da2703fa4a1390c9daf91c0997270920c4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20R=C3=B8nne=20Petersen?= Date: Tue, 14 Oct 2025 09:47:29 +0200 Subject: [PATCH 14/14] std.debug: FP-based unwinding is ideal on SPARC The way SPARC works due to its ABI built around register windows means that we can always do fast FP-based unwinding. --- lib/std/debug.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/std/debug.zig b/lib/std/debug.zig index 83df76a872..d9eb0cd907 100644 --- a/lib/std/debug.zig +++ b/lib/std/debug.zig @@ -886,6 +886,8 @@ const StackIterator = union(enum) { .powerpcle, .powerpc64, .powerpc64le, + .sparc, + .sparc64, => .ideal, // https://developer.apple.com/documentation/xcode/writing-arm64-code-for-apple-platforms#Respect-the-purpose-of-specific-CPU-registers .aarch64 => if (builtin.target.os.tag.isDarwin()) .safe else .unsafe,