From 91fa2c61aacd002228c37cb651fa0a350bd7ac59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20R=C3=B8nne=20Petersen?= Date: Fri, 3 Oct 2025 03:22:02 +0200 Subject: [PATCH 1/5] compiler: control the s390x backchain feature through the frame pointer option This is a little different from how C/C++ compilers do this, but I think it's justified because it's what users actually *mean* when the use frame pointer options. This is another one of those LLVM "CPU" features that have nothing to do with CPU at all and should really be a TargetMachine option or something. One day we'll figure out a better way of dealing with these... --- src/Compilation.zig | 8 ++++++-- src/Package/Module.zig | 5 ++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Compilation.zig b/src/Compilation.zig index 86b1356a3f11..f13a232e47b8 100644 --- a/src/Compilation.zig +++ b/src/Compilation.zig @@ -7193,6 +7193,9 @@ pub fn addCCArgs( } try argv.append(if (mod.omit_frame_pointer) "-fomit-frame-pointer" else "-fno-omit-frame-pointer"); + if (target.cpu.arch == .s390x) { + try argv.append(if (mod.omit_frame_pointer) "-mbackchain" else "-mno-backchain"); + } const ssp_buf_size = mod.stack_protector; if (ssp_buf_size != 0) { @@ -7258,9 +7261,10 @@ pub fn addCCArgs( const is_enabled = target.cpu.features.isEnabled(index); if (feature.llvm_name) |llvm_name| { - // We communicate float ABI to Clang through the dedicated options. + // We communicate these to Clang through the dedicated options. if (std.mem.startsWith(u8, llvm_name, "soft-float") or - std.mem.startsWith(u8, llvm_name, "hard-float")) + std.mem.startsWith(u8, llvm_name, "hard-float") or + (target.cpu.arch == .s390x and std.mem.eql(u8, llvm_name, "backchain"))) continue; // Ignore these until we figure out how to handle the concept of omitting features. diff --git a/src/Package/Module.zig b/src/Package/Module.zig index 440f764b3189..cd7f573046fd 100644 --- a/src/Package/Module.zig +++ b/src/Package/Module.zig @@ -343,7 +343,10 @@ pub fn create(arena: Allocator, options: CreateOptions) !*Package.Module { // See https://github.com/ziglang/zig/issues/23539 if (target_util.isDynamicAMDGCNFeature(target, feature)) continue; - const is_enabled = target.cpu.features.isEnabled(feature.index); + var is_enabled = target.cpu.features.isEnabled(feature.index); + if (target.cpu.arch == .s390x and @as(std.Target.s390x.Feature, @enumFromInt(feature.index)) == .backchain) { + is_enabled = !omit_frame_pointer; + } if (is_enabled) { try buf.ensureUnusedCapacity(2 + llvm_name.len); From 0f56d7afe298ae8f2b49d6c3b14fd935c971f1bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20R=C3=B8nne=20Petersen?= Date: Fri, 3 Oct 2025 03:24:21 +0200 Subject: [PATCH 2/5] std.debug: use correct return address offset for s390x Makes FP-based unwinding work. --- lib/std/debug.zig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/std/debug.zig b/lib/std/debug.zig index 2b1ea3492fc2..6d705f3c8633 100644 --- a/lib/std/debug.zig +++ b/lib/std/debug.zig @@ -952,6 +952,9 @@ const StackIterator = union(enum) { /// Offset of the saved return address wrt the frame pointer. const ra_offset = off: { if (native_arch == .powerpc64le) 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); break :off @sizeOf(usize); }; From 006bc5a8ca815e5c20438fa747bfbb336a6b5066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20R=C3=B8nne=20Petersen?= Date: Fri, 3 Oct 2025 03:25:16 +0200 Subject: [PATCH 3/5] std.os.linux: improve the s390x mcontext_t definition The old one was correct in terms of layout but very user-hostile. --- lib/std/os/linux/s390x.zig | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/std/os/linux/s390x.zig b/lib/std/os/linux/s390x.zig index 71594d4a6509..86495f6f7953 100644 --- a/lib/std/os/linux/s390x.zig +++ b/lib/std/os/linux/s390x.zig @@ -269,7 +269,12 @@ pub const ucontext_t = extern struct { }; pub const mcontext_t = extern struct { - __regs1: [18]u64, - __regs2: [18]u32, - __regs3: [16]f64, + psw: extern struct { + mask: u64, + addr: u64, + }, + gregs: [16]u64, + aregs: [16]u32, + fpc: u32, + fregs: [16]f64, }; From 8263f55ab271c499a8a2133ef52e615850846bd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20R=C3=B8nne=20Petersen?= Date: Fri, 3 Oct 2025 03:27:02 +0200 Subject: [PATCH 4/5] std.debug: add s390x-linux unwind support --- lib/std/debug/Dwarf.zig | 3 + lib/std/debug/Dwarf/SelfUnwinder.zig | 6 +- lib/std/debug/SelfInfo/Elf.zig | 1 + lib/std/debug/cpu_context.zig | 87 ++++++++++++++++++++++++++++ 4 files changed, 96 insertions(+), 1 deletion(-) diff --git a/lib/std/debug/Dwarf.zig b/lib/std/debug/Dwarf.zig index ab064617eaf2..e510c84f15dd 100644 --- a/lib/std/debug/Dwarf.zig +++ b/lib/std/debug/Dwarf.zig @@ -1433,6 +1433,7 @@ pub fn ipRegNum(arch: std.Target.Cpu.Arch) ?u16 { .arm, .armeb, .thumb, .thumbeb => 15, .loongarch32, .loongarch64 => 32, .riscv32, .riscv32be, .riscv64, .riscv64be => 32, + .s390x => 65, .x86 => 8, .x86_64 => 16, else => null, @@ -1445,6 +1446,7 @@ pub fn fpRegNum(arch: std.Target.Cpu.Arch) u16 { .arm, .armeb, .thumb, .thumbeb => 11, .loongarch32, .loongarch64 => 22, .riscv32, .riscv32be, .riscv64, .riscv64be => 8, + .s390x => 11, .x86 => 5, .x86_64 => 6, else => unreachable, @@ -1457,6 +1459,7 @@ pub fn spRegNum(arch: std.Target.Cpu.Arch) u16 { .arm, .armeb, .thumb, .thumbeb => 13, .loongarch32, .loongarch64 => 3, .riscv32, .riscv32be, .riscv64, .riscv64be => 2, + .s390x => 15, .x86 => 4, .x86_64 => 7, else => unreachable, diff --git a/lib/std/debug/Dwarf/SelfUnwinder.zig b/lib/std/debug/Dwarf/SelfUnwinder.zig index 8ee08180ddc7..c0a44b6a09e7 100644 --- a/lib/std/debug/Dwarf/SelfUnwinder.zig +++ b/lib/std/debug/Dwarf/SelfUnwinder.zig @@ -176,7 +176,11 @@ fn nextInner(unwinder: *SelfUnwinder, gpa: Allocator, cache_entry: *const CacheE break :cfa try applyOffset(ptr.*, ro.offset); }, .expression => |expr| cfa: { - // On all implemented architectures, the CFA is defined to be the previous frame's SP + // On most implemented architectures, the CFA is defined to be the previous frame's SP. + // + // On s390x, it's defined to be SP + 160 (ELF ABI s390x Supplement §1.6.3); however, + // what this actually means is that there will be a `def_cfa r15 + 160`, so nothing + // special for us to do. const prev_cfa_val = (try regNative(&unwinder.cpu_state, sp_reg_num)).*; unwinder.expr_vm.reset(); const value = try unwinder.expr_vm.run(expr, gpa, .{ diff --git a/lib/std/debug/SelfInfo/Elf.zig b/lib/std/debug/SelfInfo/Elf.zig index ebb19c2a7bad..9772ec0aac7c 100644 --- a/lib/std/debug/SelfInfo/Elf.zig +++ b/lib/std/debug/SelfInfo/Elf.zig @@ -90,6 +90,7 @@ pub const can_unwind: bool = s: { .loongarch64, .riscv32, .riscv64, + .s390x, .x86, .x86_64, }, diff --git a/lib/std/debug/cpu_context.zig b/lib/std/debug/cpu_context.zig index 981174e95472..0e25d00480a5 100644 --- a/lib/std/debug/cpu_context.zig +++ b/lib/std/debug/cpu_context.zig @@ -8,6 +8,7 @@ else switch (native_arch) { .arm, .armeb, .thumb, .thumbeb => Arm, .loongarch32, .loongarch64 => LoongArch, .riscv32, .riscv32be, .riscv64, .riscv64be => Riscv, + .s390x => S390x, .x86 => X86, .x86_64 => X86_64, else => noreturn, @@ -189,6 +190,17 @@ pub fn fromPosixSignalContext(ctx_ptr: ?*const anyopaque) ?Native { }, else => null, }, + .s390x => switch (builtin.os.tag) { + .linux => .{ + .r = uc.mcontext.gregs, + .f = uc.mcontext.fregs, + .psw = .{ + .mask = uc.mcontext.psw.mask, + .addr = uc.mcontext.psw.addr, + }, + }, + else => null, + }, else => null, }; } @@ -677,6 +689,81 @@ pub const Riscv = extern struct { } }; +/// This is an `extern struct` so that inline assembly in `current` can use field offsets. +pub const S390x = extern struct { + /// The numbered general-purpose registers r0 - r15. + r: [16]u64, + /// The numbered floating-point registers f0 - f15. Yes, really - they can be used in DWARF CFI. + f: [16]f64, + /// The program counter. + psw: extern struct { + mask: u64, + addr: u64, + }, + + pub inline fn current() S390x { + var ctx: S390x = undefined; + asm volatile ( + \\ stmg %%r0, %%r15, 0(%%r2) + \\ std %%f0, 128(%%r2) + \\ std %%f1, 136(%%r2) + \\ std %%f2, 144(%%r2) + \\ std %%f3, 152(%%r2) + \\ std %%f4, 160(%%r2) + \\ std %%f5, 168(%%r2) + \\ std %%f6, 176(%%r2) + \\ std %%f7, 184(%%r2) + \\ std %%f8, 192(%%r2) + \\ std %%f9, 200(%%r2) + \\ std %%f10, 208(%%r2) + \\ std %%f11, 216(%%r2) + \\ std %%f12, 224(%%r2) + \\ std %%f13, 232(%%r2) + \\ std %%f14, 240(%%r2) + \\ std %%f15, 248(%%r2) + \\ epsw %%r0, %%r1 + \\ stm %%r0, %%r1, 256(%%r2) + \\ larl %%r0, . + \\ stg %%r0, 264(%%r2) + \\ lg %%r0, 0(%%r2) + \\ lg %%r1, 8(%%r2) + : + : [gprs] "{r2}" (&ctx), + : .{ .memory = true }); + return ctx; + } + + pub fn dwarfRegisterBytes(ctx: *S390x, register_num: u16) DwarfRegisterError![]u8 { + switch (register_num) { + 0...15 => return @ptrCast(&ctx.r[register_num]), + // Why??? + 16 => return @ptrCast(&ctx.f[0]), + 17 => return @ptrCast(&ctx.f[2]), + 18 => return @ptrCast(&ctx.f[4]), + 19 => return @ptrCast(&ctx.f[6]), + 20 => return @ptrCast(&ctx.f[1]), + 21 => return @ptrCast(&ctx.f[3]), + 22 => return @ptrCast(&ctx.f[5]), + 23 => return @ptrCast(&ctx.f[7]), + 24 => return @ptrCast(&ctx.f[8]), + 25 => return @ptrCast(&ctx.f[10]), + 26 => return @ptrCast(&ctx.f[12]), + 27 => return @ptrCast(&ctx.f[14]), + 28 => return @ptrCast(&ctx.f[9]), + 29 => return @ptrCast(&ctx.f[11]), + 30 => return @ptrCast(&ctx.f[13]), + 31 => return @ptrCast(&ctx.f[15]), + 64 => return @ptrCast(&ctx.psw.mask), + 65 => return @ptrCast(&ctx.psw.addr), + + 48...63 => return error.UnsupportedRegister, // a0 - a15 + 68...83 => return error.UnsupportedRegister, // v16 - v31 + + else => return error.InvalidRegister, + } + } +}; + const signal_ucontext_t = switch (native_os) { .linux => std.os.linux.ucontext_t, .emscripten => std.os.emscripten.ucontext_t, From 95bdb0c1c65c128923ffac3f4be6b4619eb4a54b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20R=C3=B8nne=20Petersen?= Date: Fri, 3 Oct 2025 03:45:52 +0200 Subject: [PATCH 5/5] std.debug.Dwarf.SelfUnwinder: default some s390x registers to the same-value rule --- lib/std/debug/Dwarf/SelfUnwinder.zig | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/std/debug/Dwarf/SelfUnwinder.zig b/lib/std/debug/Dwarf/SelfUnwinder.zig index c0a44b6a09e7..3a7b70b03733 100644 --- a/lib/std/debug/Dwarf/SelfUnwinder.zig +++ b/lib/std/debug/Dwarf/SelfUnwinder.zig @@ -197,9 +197,13 @@ fn nextInner(unwinder: *SelfUnwinder, gpa: Allocator, cache_entry: *const CacheE // If unspecified, we'll use the default rule for the return address register, which is // typically equivalent to `.undefined` (meaning there is no return address), but may be // overriden by ABIs. - var has_return_address: bool = builtin.cpu.arch.isAARCH64() and - return_address_register >= 19 and - return_address_register <= 28; + var has_return_address: bool = switch (builtin.cpu.arch) { + // DWARF for the Arm 64-bit Architecture (AArch64) §4.3, p1 + .aarch64, .aarch64_be => return_address_register >= 19 and return_address_register <= 28, + // ELF ABI s390x Supplement §1.6.4 + .s390x => return_address_register >= 6 and return_address_register <= 15, + else => false, + }; // Create a copy of the CPU state, to which we will apply the new rules. var new_cpu_state = unwinder.cpu_state;