Fun fact, Ada (GNAT) runs on IBM s/390

Hi all,

this week I was in a conference (well, actually two, for those interested MODELS 2026 – Model Driven Engineering Languages and Systems and https://langdevcon.org/) and I met quite a few interesting people. One of them was a Broadcom engineer, Vit Gottwald, with whom I had a quite interesting discussion. He is working on the mainframe project (for those who do not know, look into IBM Z Mainframe Servers and Software and IBM mainframe - Wikipedia) and we had a nice discussion about Ada/SPARK.

We both wondered whether we could run Ada on the s/390 architecture and if GNAT had support for it. He encouraged me to try to see if I could first run it on Linux/s390. That seemed to be a nice challenge, specially after my work on updating GNAT on NetBSD (where J. Marino did most of the work, see his work in GitHub - jrmarino/draco: GNAT-AUX / GCC-AUX / Draco Ada compiler development · GitHub). Well, it turns out… that GNAT already has support for the s390 architecture! I learned this thanks to Debian having full Linux/s390 support and having GNAT-v16 available in the repos (unstable). I still wanted to learn and see how good the support was, so I fired up an AI and instructed it to run the ACATS test suite (http://www.ada-auth.org/acats.html) on a virtualised Debian using QEMU. The results speak for themselves!

(the following text has been written by an AI, MiMo-v2.6-Flash)


GNAT/GCC on Linux s390 (s390x): ACATS baseline — first results

Set up a reproducible dev environment for validating/patching GNAT for
s390x-linux-gnu: a Debian sid (s390x) VM under qemu-system-s390x (TCG),
GCC pinned to releases/gcc-16.2.0, and ACATS 4.2 driven by GCC’s own
gcc/testsuite/ada/acats-4/run_all.sh harness.

Results — full ACATS 4.2 (49 chapters)

arm compiler passes harness “failures”
s390x (QEMU TCG VM) Debian gnat-16 = GCC 16.2.0-3 2606 1555
x86_64 control (host) GCC 16.2.1 2612 1549

The ~1550 shared “failures” are a harness artifact: GCC’s simplified runner
counts negative tests (code that is supposed to be rejected) as failures,
since it treats any non-zero gnatmake exit as FAIL. ACATS grades those with
its own -- ERROR: annotation mechanism, which run_all.sh doesn’t implement.

So the useful number is the failure-set diff. s390x fails exactly 6
tests
that pass on x86_64, in three clean themes:

theme tests symptom
A. abort / Terminated semantics c394001, cxc7005, c954a01 aborted (or abort-deferred) task still reports Terminated/Callable = False
B. protected-entry wakeup cxai033, cxai035 “Enqueue to empty queue didn’t unblock reader” (both Ada.Containers queue types)
C. task Storage_Error cb1010a process exits with no verdict where Storage_Error is expected (siblings cb1010c/d pass)

No test ever hit the harness’s 900 s timeout, so these are real behavioural
differences in libgnarl/tasking on s390, not TCG speed artifacts.

Smoke tests

8 self-checking programs (tasking + protected objects, exception
unwinding/controlled types, secondary stack, 128-bit modular arithmetic,
atomic shared variables, Ada.Real_Time, generics + tagged dispatching):
8/8 PASS on s390x, identical to x86_64.

Two findings worth reporting upstream

  1. make check-acats is broken for GCC ≥ 15 on every target.
    run_all.sh hardcodes gccflags="-O2" and compiles ACATS’s C support
    files (tests/cd/*.c, tests/cxb/*.c), which are K&R-style — since GCC 15
    defaults to -std=gnu23, implicit int is now a hard error:
    cd300051.c: error: type of 'Value' defaults to 'int'
    The suite aborts during “Compiling support files…”. Workaround is a
    one-liner at the script’s sanctioned customization point:
    gccflags="-O2 -std=gnu89".
  2. s390x GNAT is in much better shape than expected. The runtime wiring
    (gcc/ada/Makefile.rtl, “S390 Linux” block, system-linux-s390.ads) has
    been in place since ≤ GCC 9, and Debian has shipped native gnat for s390x
    for years. The 6 tests above are the only s390-specific deltas found in a
    full ACATS run.

Infrastructure

  • Debian sid/s390x rootfs built with debootstrap --foreign + second stage
    through binfmt + statically linked qemu-s390x, snapshot-pinned for
    reproducibility; disk image via mkfs.ext4 -d (the container has no loop
    devices); booted with qemu -kernel/-initrd, static 10.0.2.15, ssh on
    :2222.
  • ACATS assembled from the ada-auth 4.2 tarball plus GCC’s
    gcc/testsuite/ada/acats-4 driver (the tarball ships no driver scripts, and
    GCC’s support/ has files the tarball lacks, e.g. impbit.adb).
  • The same GCC driver runs unmodified against both compilers via a fake “gcc
    build tree” shim (xgcc/gnatmake/ada/rts symlinks), so the s390 and x86
    arms are directly comparable.
  • Full per-test logs, acats.sum files and the analysis are in
    docs/findings.md / docs/results/.

Next: root-cause the six (task abort handling, entry-queue wakeup, task stack
accounting) and turn fixes into draco-style patches against GCC 16.2.0.


Soooo, yeah, that is pretty cool! I do not know who introduced support for this architecture (AdaCore, Debian contributors, Marino, etc), but I just love seeing this kind of work out there in the wild!

Now, not everything is rosy. There were obviously some failures whose root needs to be found and fixed. Also, Debian maintains a set of patches that make GNAT work in all their cases, this is the same for other systems such as OpenBSD. I personally would love to see some community effort to try and take all these patches (including those from Draco) and upstreaming them to GCC. This way we will have a much more robust tool, the fixes wont be scattered, the maintainers’ work will be substantially less and we can always brag about having great tool support (which we do, but I could be better).

Best regards,
Fer

P.S: now the interesting thing would be to test GNAT on z/OS. From what I have seen, GCC does not support z/OS (though there is/was work to fix just that GitHub - ambitus/gcc: A port of GCC supporting z/OS as a target · GitHub) and IBM has preferential support for LLVM toolchains (which we also support via GitHub - AdaCore/gnat-llvm: LLVM based GNAT compiler · GitHub). So it could be a substantial amount of effort or very little. It remains to be seem. Maybe it will become easier if GCC-Cobol (GCOBOL(1) (gcc cobol compiler)) gets ported to z/OS, and there seems to be some interest in it, but that is greatly outside of our scope. Also, IBM is introducing the ARM architecture to their chips, so there may not be much interest in the s390-zOS port as we could just skip to aarch64-zOS directly.

Well, the AI did a diagnosis and only one failing test is actually failing, the others are just a problem of the (slow) virtualised environment. Here is what the AI found, which may be interesting to others:

Root-causing 6 ACATS failures on Linux s390x (GNAT/GCC 16.2)

Context: a full ACATS 4.2 run (GCC’s own gcc/testsuite/ada/acats-4/run_all.sh
driver, GCC 16.2.0-3, QEMU TCG) on s390x fails exactly 6 tests that pass on
x86_64 with the same suite. Results differ, causes differ — here is what
actually happens.

The 6 tests

theme tests symptom
A. abort / Terminated c394001, cxc7005, c954a01 after abort, Terminated/Is_Terminated still False
B. protected-entry wakeup cxai033, cxai035 “Enqueue to empty queue didn’t unblock reader”
C. task Storage_Error cb1010a process dies with SIGSEGV (exit 139), no verdict

Themes A + B: not s390 bugs — ACATS’s 1 ms wait budget

All five fail deterministically and pass when ACATS’s wait budget is raised.
The budget comes from GCC’s gcc/testsuite/ada/acats-4/support/impdef.a:

Minimum_Task_Switch  : constant Duration := 0.001;   -- 1 ms
Switch_To_New_Task   : constant Duration := 0.001;   -- 1 ms
-- "The above constant has been chosen for use with the GCC testsuite so
--  that they do not take too long, but may be too small."

Measured tasking latencies (own micro-benchmark, 3 runs each):

operation x86_64 host s390x under TCG ACATS budget
entry call → rendezvous start 0.006 ms 0.4 ms 1 ms
abort → 'Terminated = True 0.21 ms 6.5 – 7.9 ms 1 ms
protected entry released → blocked reader resumes 0.17 ms 0.9 – 1.5 ms 1 ms
10 × delay 0.001 wall clock (timer granularity) 10.6 ms 13 – 15 ms –

abort costs ~34× more under emulation, and the entry wakeup sits right at the
1 ms boundary. Re-running the five tests with … := 1.0:

c394001 PASS   cxc7005 PASS   c954a01 PASS   cxai033 PASS   cxai035 PASS

The Ada semantics are fine: tasks do finish their abort, queue readers do get
woken. Conclusion: platform-timing artifact of a GCC-testsuite-specific
budget
, not a compiler/runtime defect. (Worth noting: at 0.21 ms even on
x86_64, a 1 ms budget has <5× headroom, so slow real s390 hardware is likely
marginal too.)


Theme C (cb1010a): a real GCC bug on all non-x86 Linux targets

cb1010a deliberately overflows a task stack (4 KB frames) and expects
Storage_Error. On s390x the process is killed instead:

--- SIGSEGV {si_code=SEGV_MAPERR, si_addr=0x3ff975ff000} ---  ← stack probe faults
--- SIGSEGV {si_code=SI_KERNEL,  si_addr=NULL}          ---  ← kernel cannot deliver
+++ killed by SIGSEGV +++

Evidence chain:

  1. GNAT’s __gnat_error_handler (SIGSEGV → Storage_Error, registered with
    SA_ONSTACK) is what makes this test pass on x86 — strace shows 3 SIGSEGVs
    delivered and caught.

  2. On s390 the handler is installed, but System.Interrupt_Management
    (s-intman__posix.adb) registers its own handler afterwards and clobbers
    it. Dumping the struct passed to sigaction() (gdb) shows kernel-side

    sa_handler = __gnat_error_handler        ← replaced by notify_exception
    sa_flags   = 0x4c8                       ← this is the intended sa_mask!
    sa_mask    = (garbage)
    

    0x4c8 = bits for SIGILL(4), SIGBUS(7), SIGFPE(8), SIGSEGV(11) — i.e. the
    mask word landed in the flags field, and SA_SIGINFO/SA_ONSTACK are gone.

  3. Cause — gcc/ada/libgnarl/s-osinte__linux.ads hardcodes the x86 glibc
    layout
    :

    /* x86/x86_64 glibc */                 /* s390 & asm-generic glibc */
    handler  @0                            handler  @0
    mask     @8   (128 bytes)              flags    @8
    flags    @136                          restorer @16
    restorer @144                          mask     @24 (8 bytes)
    
    type struct_sigaction is record
       sa_handler  : System.Address;   -- 0
       sa_mask     : sigset_t;         -- 8   ← x86 order
       sa_flags    : int;              -- 16
       sa_restorer : System.Address;   -- 24
    end record;
    

    plus System.Linux: SA_ONSTACK : constant := 16#08000000#; (x86 value;
    asm-generic expects 16#08#) and sa_flags_pos := 128 + sa_mask_pos.

  4. Ruled out the obvious suspect: injecting a per-thread alternate signal stack
    via LD_PRELOAD (hook verified firing) changes nothing. The handler is
    registered without SA_ONSTACK, so when the stack probe faults with SP
    already below the mapped task stack, the kernel cannot deliver the signal at
    all → second SIGSEGV (SI_KERNEL) → process killed.

Conclusion: x86-specific struct sigaction layout and SA_* constants in
the shared Linux runtime make System.Interrupt_Management mis-register the
SIGSEGV handler on non-x86 Linux targets
, so “task stack overflow →
Storage_Error” degenerates into a fatal double fault.

This affects every Linux target using the asm-generic sigaction ABI —
s390x, arm64, riscv64, loongarch64, … — while x86 works by coincidence.

Candidate fix (sketch):

-- gcc/ada/libgnarl/s-osinte__linux.ads   (asm-generic targets)
type struct_sigaction is record
   sa_handler  : System.Address;   -- 0
   sa_flags    : unsigned_long;    -- 8
   sa_restorer : System.Address;   -- 16
   sa_mask     : sigset_t;         -- 24
end record;

with per-target SA_ONSTACK/SA_SIGINFO and matching sa_*_pos offsets in
System.Linux; the x86 layout must stay for x86 (an s-osinte__linux-<abi>.ads
split, as already used for other OS variants, is a natural carrier).