diff options
| author | Alex Rønne Petersen <alex@alexrp.com> | 2025-10-16 10:14:05 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-10-16 10:14:05 +0200 |
| commit | 48f8133beaef31121caea82e39190291bdfdc633 (patch) | |
| tree | bdef017a179e3ef0450b2cb6dd71e15543ffd8e9 /lib/std | |
| parent | 493ad58ff72ac79528cbd20e9506daca964ec37b (diff) | |
| parent | e0f10da2703fa4a1390c9daf91c0997270920c4a (diff) | |
| download | zig-48f8133beaef31121caea82e39190291bdfdc633.tar.gz zig-48f8133beaef31121caea82e39190291bdfdc633.zip | |
Merge pull request #25569 from alexrp/std-debug-sparc
`std.debug`: implement `sparc*-linux` unwinding
Diffstat (limited to 'lib/std')
| -rw-r--r-- | lib/std/debug.zig | 97 | ||||
| -rw-r--r-- | lib/std/debug/Dwarf.zig | 3 | ||||
| -rw-r--r-- | lib/std/debug/SelfInfo/Elf.zig | 20 | ||||
| -rw-r--r-- | lib/std/debug/cpu_context.zig | 168 | ||||
| -rw-r--r-- | lib/std/os/windows.zig | 4 |
5 files changed, 235 insertions, 57 deletions
diff --git a/lib/std/debug.zig b/lib/std/debug.zig index 7ac6c45903..d9eb0cd907 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 {}; @@ -807,14 +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 { - 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 - } - } 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. @@ -833,7 +825,19 @@ const StackIterator = union(enum) { // in our caller's frame and above. return .{ .di = .init(&.current()) }; } - 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.*) { @@ -842,6 +846,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 @@ -873,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, @@ -923,7 +938,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, @@ -935,8 +951,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))) @@ -947,7 +963,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 @@ -964,33 +980,46 @@ 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: { - if (native_arch.isSPARC()) break :bias 2047; + /// 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 == .sparc64) break :bias 2047; 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; @@ -1429,6 +1458,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); } 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, 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, }, diff --git a/lib/std/debug/cpu_context.zig b/lib/std/debug/cpu_context.zig index dc77b41c9d..b08ab49778 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, @@ -39,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. @@ -859,6 +880,104 @@ 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 { + flushWindows(); + + 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; + } + + 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]), + 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. x: [32]Gpr, @@ -1271,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 @@ -1398,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, @@ -1425,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 { @@ -1538,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 { 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 { |
