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 932e401758b299bed777814bb1c806445da48774..4a2487c83618f109c49d32830d7cf6f3da52a1f9 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 { -- 2.54.0 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 f10dc3cd638fd36b4bf186b036eddb331cdaba23..bee04a5eefeb6dcb1d885d26549ce1c920d10c3f 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, }, -- 2.54.0 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 7ac6c45903fbc10afa0c2e173e067e021d41238b..3f694bec1c2f572eb55d096bf2b5a70d8c319650 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 -- 2.54.0 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 dc77b41c9dc22138c4a1ac3040b08b703c71dab5..3450debba0e48aeff3622de73c0b4114c74eb8b4 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. -- 2.54.0 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 3f694bec1c2f572eb55d096bf2b5a70d8c319650..8501df175a80a36376865a7726ec7815203c8d72 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 3450debba0e48aeff3622de73c0b4114c74eb8b4..5dd2e283ed0457775f96a92e8e4e7faec9183c8d 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]), -- 2.54.0 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 5dd2e283ed0457775f96a92e8e4e7faec9183c8d..b08ab4977830c766a433f3a27e5ce7480eca2d32 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 { -- 2.54.0 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 98d7addb323420c5baec028f9fe1d4446e01bc0d..acd71eb4edc2135596b9d3e1c9417c2a563653f7 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, -- 2.54.0 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 8501df175a80a36376865a7726ec7815203c8d72..83322e8a6e673965f64f065a7a832151691bfa28 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, -- 2.54.0 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 83322e8a6e673965f64f065a7a832151691bfa28..b47aa540d21a8995352a81be10430037ae1b34f9 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; }; -- 2.54.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 b47aa540d21a8995352a81be10430037ae1b34f9..942c21212c8300d493ef0fc076da3bf3fb4e52aa 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; }; -- 2.54.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 942c21212c8300d493ef0fc076da3bf3fb4e52aa..5c6043c8c3c29c8aa6d508225e03704f48f0d3cd 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); } -- 2.54.0 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 5c6043c8c3c29c8aa6d508225e03704f48f0d3cd..5e4421475371629e6a8fe90ee5dc95aff63b63b5 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.*) { -- 2.54.0 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 5e4421475371629e6a8fe90ee5dc95aff63b63b5..83df76a872404026e8486456f794aafb2eb2949a 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; -- 2.54.0 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 83df76a872404026e8486456f794aafb2eb2949a..d9eb0cd9077436a7f2e34bc5c15c37b9400e93bc 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, -- 2.54.0