Hi Jon, Thanks for the review. This is a follow-up patch to: https://cygwin.com/pipermail/cygwin-patches/2026q3/015206.html Responses inline below; v3 addresses all of the review points. For testing, we built a standalone ucontext/altstack test suite and ran it against a freshly built cygwin1.dll on Windows ARM64. The tests cover getcontext/setcontext/swapcontext/makecontext round-trips, odd argument counts (9–13), NULL uc_link exit behaviour, a 5000-iteration swapcontext loop, and an SA_ONSTACK signal handler. We also ran a four-way isolation matrix where each branch reverted exactly one of the three fixes to verify that each change is independently required. Reverting the setcontext hand-restore causes every makecontext/swapcontext test to crash with STATUS_STACK_BUFFER_OVERRUN, matching the pre-port baseline. Reverting the trampoline (including the sp,x19 fix and mov w0,#0xff) causes the odd-argc tests to fail with STATUS_DATATYPE_MISALIGNMENT, and the NULL uc_link test exits with 0 instead of 0xff. I agree there's nothing in the upstream testsuite covering this yet. I'm happy to contribute these tests as a follow-up if that would be useful. Regarding the clobber list in call_signal_handler, the previous comment was misleading. The clobber list is serving two distinct purposes, and I've rewritten the comment to explain that. x0-x7, x9, x10, and x29 are explicitly used by the assembly as scratch/argument registers and are listed as clobbers to prevent GCC from allocating "r" input operands into registers that the assembly overwrites before reading. The x86_64 version doesn't need this because its inputs are "o" memory operands. x8, x11-x17, x30 (clobbered by blr), and v0-v7/v16-v31 are the normal caller-saved registers clobbered by the C call to altstack_wrapper. x18 (TEB) and v8-v15 (callee-saved on Windows) are preserved and therefore omitted. So the registers you pointed out aren't redundant with the explicit save/restore sequence. Removing them would allow GCC to place an input operand into a register that the assembly overwrites before reading it. That said, I'm also happy to convert this path to "o" memory operands like the x86_64 implementation if you think that's the cleaner approach. I kept "r" operands simply to minimise churn in the already-tested assembly. For RtlRestoreContext, unfortunately I haven't found any Microsoft documentation describing this behaviour. The STATUS_ILLEGAL_INSTRUCTION failure on a synthetic context was discovered empirically. I also tried setting ContextFlags = CONTEXT_FULL on the makecontext-generated context in case RtlRestoreContext required a particular flag mask, but it made no difference. The hand-restore is genuinely required. I've added a comment noting this. For the 272 constant, I've added a comment explaining that it's the byte offset of the V-register array within struct __mcontext, whose layout mirrors the Windows ARM64 CONTEXT structure. The comment now also documents all offsets used by the routine. Regarding x17 and CPSR not being restored in setcontext, this is the same asymmetry as in the earlier incyg fix, but you're right that it should be documented explicitly rather than left implicit. The omission is intentional. x16 (IP0) and x17 (IP1) are the ABI-defined intra-procedure-call scratch registers. This implementation uses x16 as the base register and x17 as the branch target, and the ABI already permits linker-inserted veneers to clobber both registers, so a ucontext consumer cannot rely on either surviving a setcontext() regardless. Their saved values are therefore intentionally not restored. CPSR is likewise not restored because there is no EL0 instruction that restores the entire register, and the only architecturally visible state that user code could reasonably care about (NZCV) is caller-clobbered across the makecontext()/swapcontext() boundary that this code serves. glibc and musl take the same approach on AArch64. I've added comments explaining this. Finally, I've fixed both the "return address on the stack" comment and the missing makecontext #error cases. The header comment now states that the continuation trampoline is reached via a stack return slot on x86_64 but via lr on AArch64. I also added the missing #else / #error unimplemented for this target to each of the four #if defined(x86_64) / #elif defined(aarch64) blocks in makecontext. Inline patch --- winsup/cygwin/exceptions.cc | 345 +++++++++++++++++++++++++++++++----- 1 file changed, 304 insertions(+), 41 deletions(-)
diff --git a/winsup/cygwin/exceptions.cc b/winsup/cygwin/exceptions.cc
index 1e129b319..4224cdfcf 100644
--- a/winsup/cygwin/exceptions.cc
+++ b/winsup/cygwin/exceptions.cc
@@ -1892,7 +1892,7 @@ _cygtls::call_signal_handler ()
/* In assembler: Save regs on new stack, move to alternate stack,
call thisfunc, revert stack regs. */
-#ifdef __x86_64__
+#if defined(__x86_64__)
/* Clobbered regs: rcx, rdx, r8, r9, r10, r11, rbp, rsp */
__asm__ ("\n\
movq %[NEW_SP], %%rax # Load alt stack into rax \n\
@@ -1930,6 +1930,66 @@ _cygtls::call_signal_handler ()
[FUNC] "o" (thisfunc),
[WRAPPER] "o" (altstack_wrapper)
: "memory");
+#elif defined(__aarch64__)
+ __asm__ ("\n\
+ mov x9, %[NEW_SP] // Load alt stack into x9 \n\
+ sub x9, x9, #0x60 // Make room on alt stack \n\
+ // for clobbered regs \n\
+ str x0, [x9, #0x00] // Save clobbered regs \n\
+ str x1, [x9, #0x08] \n\
+ str x2, [x9, #0x10] \n\
+ str x3, [x9, #0x18] \n\
+ str x4, [x9, #0x20] \n\
+ str x5, [x9, #0x28] \n\
+ str x6, [x9, #0x30] \n\
+ str x7, [x9, #0x38] \n\
+ str fp, [x9, #0x40] \n\
+ mov x10, sp // Copy sp into x10 for saving \n\
+ str x10, [x9, #0x48] \n\
+ str x30, [x9, #0x50] // Save link register \n\
+ mov x0, %[SIG] // thissig to 1st arg reg \n\
+ mov x1, %[SI] // &thissi to 2nd arg reg \n\
+ mov x2, %[CTX] // thiscontext to 3rd arg reg \n\
+ mov x3, %[FUNC] // thisfunc to x3 \n\
+ mov x4, %[WRAPPER] // wrapper address to x4 \n\
+ mov sp, x9 // Move alt stack into sp \n\
+ blr x4 // Call wrapper \n\
+ mov x9, sp // Restore clobbered regs \n\
+ ldr x30, [x9, #0x50] // Restore link register \n\
+ ldr x10, [x9, #0x48] \n\
+ ldr fp, [x9, #0x40] \n\
+ ldr x7, [x9, #0x38] \n\
+ ldr x6, [x9, #0x30] \n\
+ ldr x5, [x9, #0x28] \n\
+ ldr x4, [x9, #0x20] \n\
+ ldr x3, [x9, #0x18] \n\
+ ldr x2, [x9, #0x10] \n\
+ ldr x1, [x9, #0x08] \n\
+ ldr x0, [x9, #0x00] \n\
+ mov sp, x10 // Restore stack pointer \n"
+ : : [NEW_SP] "r" (new_sp),
+ [SIG] "r" (thissig),
+ [SI] "r" (&thissi),
+ [CTX] "r" (thiscontext),
+ [FUNC] "r" (thisfunc),
+ [WRAPPER] "r" (altstack_wrapper)
+ /* The clobber list serves two roles here. x0-x7, x9, x10
+ and x29 are hardcoded by this asm as scratch/argument
+ registers, so they are listed to stop gcc allocating the
+ "r" inputs into them. (The x86_64 path above needs no
+ such guard because its inputs are "o" memory operands.)
+ The remaining registers - x8, x11-x17, x30 (trampled by
+ the blr) and v0-v7/v16-v31 - are caller-saved and clobbered
+ by the ordinary C call to altstack_wrapper. x18 (the
+ Windows TEB pointer) and v8-v15 (callee-saved on Windows)
+ survive the call and are omitted. */
+ : "memory", "cc",
+ "x0", "x1", "x2", "x3", "x4", "x5", "x6", "x7",
+ "x8", "x9", "x10", "x11", "x12", "x13", "x14", "x15",
+ "x16", "x17", "x29", "x30",
+ "v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7",
+ "v16", "v17", "v18", "v19", "v20", "v21", "v22", "v23",
+ "v24", "v25", "v26", "v27", "v28", "v29", "v30", "v31");
#else
#error unimplemented for this target
#endif
@@ -2009,10 +2069,89 @@ setcontext (const ucontext_t *ucp)
{
PCONTEXT ctx = (PCONTEXT) &ucp->uc_mcontext;
set_signal_mask (_my_tls.sigmask, ucp->uc_sigmask);
+#if defined(__aarch64__)
+ /* On ARM64, RtlRestoreContext raises STATUS_ILLEGAL_INSTRUCTION when asked
+ to restore a synthetic context built by makecontext rather than one
+ captured by RtlCaptureContext/GetThreadContext: it rejects an arbitrary
+ PC and a stack outside the thread's registered range. Restore the
+ registers by hand and branch to the saved PC instead, as glibc/musl do
+ for aarch64.
+
+ The numeric offsets below are byte offsets into struct __mcontext, whose
+ layout mirrors the Windows ARM64 CONTEXT: x0/x1 @8, x2-x15 @24-120,
+ x18-x28/fp @152-232, lr @248, sp @256, pc @264, v[0..31] @272 (16 bytes
+ each), fpcr @784, fpsr @788.
+
+ x16 (IP0) and x17 (IP1) are the ABI intra-procedure-call scratch
+ registers: their values in the context are deliberately not restored.
+ We reuse x16 as the base pointer and x17 as the branch target, and the
+ ABI permits the linker to clobber x16/x17 in any call/branch veneer, so a
+ ucontext consumer may not rely on them surviving a setcontext. CPSR is
+ likewise not restored: it cannot be written from EL0 with a plain msr,
+ the only flags of interest (NZCV) are caller-clobbered across the
+ makecontext/swapcontext boundary this path serves, and glibc/musl take
+ the same approach for aarch64. */
+ register PCONTEXT base __asm__ ("x16") = ctx;
+ __asm__ __volatile__ ("\n\
+ add x17, x16, #272 \n\
+ ldp q0, q1, [x17, #0] \n\
+ ldp q2, q3, [x17, #32] \n\
+ ldp q4, q5, [x17, #64] \n\
+ ldp q6, q7, [x17, #96] \n\
+ ldp q8, q9, [x17, #128] \n\
+ ldp q10, q11, [x17, #160] \n\
+ ldp q12, q13, [x17, #192] \n\
+ ldp q14, q15, [x17, #224] \n\
+ ldp q16, q17, [x17, #256] \n\
+ ldp q18, q19, [x17, #288] \n\
+ ldp q20, q21, [x17, #320] \n\
+ ldp q22, q23, [x17, #352] \n\
+ ldp q24, q25, [x17, #384] \n\
+ ldp q26, q27, [x17, #416] \n\
+ ldp q28, q29, [x17, #448] \n\
+ ldp q30, q31, [x17, #480] \n\
+ /* Restore FPCR and FPSR */ \n\
+ ldr w17, [x16, #784] \n\
+ msr fpcr, x17 \n\
+ ldr w17, [x16, #788] \n\
+ msr fpsr, x17 \n\
+ /* Load PC into x17 (branch target, offset 264) */ \n\
+ ldr x17, [x16, #264] \n\
+ /* Restore callee-saved GPRs x18..x28, fp, lr */ \n\
+ ldp x18, x19, [x16, #152] \n\
+ ldp x20, x21, [x16, #168] \n\
+ ldp x22, x23, [x16, #184] \n\
+ ldp x24, x25, [x16, #200] \n\
+ ldp x26, x27, [x16, #216] \n\
+ ldp x28, x29, [x16, #232] \n\
+ ldr x30, [x16, #248] \n\
+ /* Restore caller-saved GPRs x2..x15 */ \n\
+ ldp x2, x3, [x16, #24] \n\
+ ldp x4, x5, [x16, #40] \n\
+ ldp x6, x7, [x16, #56] \n\
+ ldp x8, x9, [x16, #72] \n\
+ ldp x10, x11, [x16, #88] \n\
+ ldp x12, x13, [x16, #104] \n\
+ ldp x14, x15, [x16, #120] \n\
+ /* Restore x0, x1 */ \n\
+ ldp x0, x1, [x16, #8] \n\
+ /* Set SP from context (last use of x16 as base) */ \n\
+ ldr x16, [x16, #256] \n\
+ mov sp, x16 \n\
+ /* Branch to saved PC */ \n\
+ br x17 \n\
+"
+ : /* no outputs (noreturn) */
+ : "r" (base)
+ : "memory"
+ );
+ __builtin_unreachable ();
+#else
RtlRestoreContext (ctx, NULL);
/* If we got here, something was wrong. */
set_errno (EINVAL);
return -1;
+#endif
}
extern "C" int
@@ -2049,7 +2188,7 @@ swapcontext (ucontext_t *oucp, const ucontext_t *ucp)
/* Trampoline function to set the context to uc_link. The pointer to the
address of uc_link is stored in a callee-saved register, referenced by
_MC_uclinkReg from the C code. If uc_link is NULL, call exit. */
-#ifdef __x86_64__
+#if defined(__x86_64__)
/* _MC_uclinkReg == %rbx */
__asm__ (" \n\
.global __cont_link_context \n\
@@ -2070,19 +2209,50 @@ __cont_link_context: \n\
nop \n\
.seh_endproc \n\
");
-
+#elif defined(__aarch64__)
+/* _MC_uclinkReg == x19. x19 holds the address of the uc_link slot but is
+ only 8-byte aligned, so read through it and mask into SP in one step
+ rather than moving the unaligned value into SP first. setcontext and
+ cygwin_exit are noreturn, so tail-call them with 'b': this leaves x30
+ untouched and keeps the frame leaf, matching the empty SEH prologue. */
+__asm__ (" \n\
+ .global __cont_link_context \n\
+ .seh_proc __cont_link_context \n\
+__cont_link_context: \n\
+ .seh_endprologue \n\
+ ldr x0, [x19] \n\
+ and sp, x19, #~0xf \n\
+ cbnz x0, 1f \n\
+ mov w0, #0xff \n\
+ b cygwin_exit \n\
+1: \n\
+ b setcontext \n\
+ .seh_endproc \n"
+ );
#else
#error unimplemented for this target
#endif
/* makecontext is modelled after GLibc's makecontext. The stack from uc_stack
is prepared so that it starts with a pointer to the linked context uc_link,
- followed by the arguments to func, and finally at the bottom the "return"
- address set to __cont_link_context. In the ucp context, rbx/ebx is set to
- point to the stack address where the pointer to uc_link is stored. The
- requirement to make this work is that rbx/ebx are callee-saved registers
- per the ABI. If any function is called which doesn't follow the ABI
- conventions, e.g. assembler code, this method will break. But that's ok. */
+ followed by the arguments to func.
+
+ The trampoline __cont_link_context is reached differently per target: on
+ x86_64 its address is written at the bottom of the stack as the "return"
+ address, whereas on aarch64 it is placed in lr (see below), since the
+ AArch64 ABI returns through the link register rather than the stack.
+
+ x86_64: In the ucp context, rbx is set to point to the stack address where
+ the pointer to uc_link is stored. The requirement to make this work is that
+ rbx is a callee-saved register per the ABI.
+
+ ARM64: In the ucp context, x19 is set to point to the stack address where
+ the pointer to uc_link is stored. The requirement is that x19 is a
+ callee-saved register per the ARM64 ABI.
+
+ If any function is called which doesn't follow the ABI conventions, e.g.
+ assembler code, this method will break. But that's ok. */
+
extern "C" void
makecontext (ucontext_t *ucp, void (*func) (void), int argc, ...)
{
@@ -2090,65 +2260,158 @@ makecontext (ucontext_t *ucp, void (*func) (void), int
argc, ...)
uintptr_t *sp;
va_list ap;
+#if defined(__x86_64__)
+ /* x86_64: Arguments beyond the first 4 go on the stack.
+ However, we allocate shadow space for all args including register args. */
+ int stack_args = argc;
+
+#elif defined(__aarch64__)
+ /* ARM64: Arguments beyond the first 8 go on the stack.
+ We only allocate stack space for args beyond registers. */
+ int stack_args = (argc > 8) ? (argc - 8) : 0;
+
+#else
+#error unimplemented for this target
+#endif
+
/* Initialize sp to the top of the stack. */
sp = (uintptr_t *) ((uintptr_t) ucp->uc_stack.ss_sp + ucp->uc_stack.ss_size);
- /* Subtract slots required for arguments and the pointer to uc_link. */
- sp -= (argc + 1);
- /* Align. */
- sp = (uintptr_t *) ((uintptr_t) sp & ~0xf);
- /* Subtract one slot for setting the return address. */
+
+#if defined(__x86_64__)
+ /* x86_64: Subtract slots for all arguments + uc_link pointer
+ and return address. */
+ sp -= (stack_args + 1); /* argc + 1 for uc_link */
+ /* Align to 16 bytes. */
+ sp = (uintptr_t *) ((uintptr_t) sp & ~0xfUL);
+ /* Subtract one more slot for the return address. */
--sp;
- /* Set return address to the trampolin function __cont_link_context. */
+ /* Set return address to the trampoline function __cont_link_context. */
sp[0] = (uintptr_t) __cont_link_context;
- /* Fetch arguments and store them on the stack.
- x86_64:
+#elif defined(__aarch64__)
+ /* ARM64: Subtract slots for stack arguments + uc_link pointer. */
+ sp -= (stack_args + 1); /* stack_args + 1 for uc_link */
+ /* ARM64 requires 16-byte alignment at public interfaces. */
+ sp = (uintptr_t *) ((uintptr_t) sp & ~0xfUL);
- - Store first four args in the AMD64 ABI arg registers.
+#else
+#error unimplemented for this target
+#endif
+ /* Fetch arguments and store them.
+ x86_64:
+ - Store first four args in the AMD64 ABI arg registers (rcx, rdx, r8, r9).
- Note that the stack is not short by these four register args. The
reason is the shadow space for these regs required by the AMD64 ABI.
-
- The definition of makecontext only allows for "int" sized arguments to
func, 32 bit, likely for historical reasons. However, the argument
slots on x86_64 are 64 bit anyway, so we can fetch and store the args
as 64 bit values, and func can request 64 bit args without violating
the definition. This potentially allows porting 32 bit applications
- providing pointer values to func without additional porting effort. */
+ providing pointer values to func without additional porting effort.
+
+ ARM64:
+ - Store first eight args in ARM64 ABI arg registers (x0-x7).
+ - Arguments beyond 8 go on the stack.
+ - Similar to x86_64, we store as uintptr_t for pointer compatibility. */
+
va_start (ap, argc);
for (int i = 0; i < argc; ++i)
-#ifdef __x86_64__
- switch (i)
- {
- case 0:
- ucp->uc_mcontext.rcx = va_arg (ap, uintptr_t);
- break;
- case 1:
- ucp->uc_mcontext.rdx = va_arg (ap, uintptr_t);
- break;
- case 2:
- ucp->uc_mcontext.r8 = va_arg (ap, uintptr_t);
- break;
- case 3:
- ucp->uc_mcontext.r9 = va_arg (ap, uintptr_t);
- break;
- default:
- sp[i + 1] = va_arg (ap, uintptr_t);
- break;
- }
+ {
+#if defined(__x86_64__)
+ switch (i)
+ {
+ case 0:
+ ucp->uc_mcontext.rcx = va_arg (ap, uintptr_t);
+ break;
+ case 1:
+ ucp->uc_mcontext.rdx = va_arg (ap, uintptr_t);
+ break;
+ case 2:
+ ucp->uc_mcontext.r8 = va_arg (ap, uintptr_t);
+ break;
+ case 3:
+ ucp->uc_mcontext.r9 = va_arg (ap, uintptr_t);
+ break;
+ default:
+ /* Stack arguments start at sp[i + 1] because sp[0] is return
address */
+ sp[i + 1] = va_arg (ap, uintptr_t);
+ break;
+ }
+
+#elif defined(__aarch64__)
+ switch (i)
+ {
+ case 0:
+ ucp->uc_mcontext.x0 = va_arg (ap, uintptr_t);
+ break;
+ case 1:
+ ucp->uc_mcontext.x1 = va_arg (ap, uintptr_t);
+ break;
+ case 2:
+ ucp->uc_mcontext.x2 = va_arg (ap, uintptr_t);
+ break;
+ case 3:
+ ucp->uc_mcontext.x3 = va_arg (ap, uintptr_t);
+ break;
+ case 4:
+ ucp->uc_mcontext.x4 = va_arg (ap, uintptr_t);
+ break;
+ case 5:
+ ucp->uc_mcontext.x5 = va_arg (ap, uintptr_t);
+ break;
+ case 6:
+ ucp->uc_mcontext.x6 = va_arg (ap, uintptr_t);
+ break;
+ case 7:
+ ucp->uc_mcontext.x7 = va_arg (ap, uintptr_t);
+ break;
+ default:
+ /* Stack arguments beyond the first 8 registers. */
+ sp[i - 8] = va_arg (ap, uintptr_t);
+ break;
+ }
#else
#error unimplemented for this target
#endif
+ }
va_end (ap);
- /* Store pointer to uc_link at the top of the stack. */
+
+#if defined(__x86_64__)
+ /* Store pointer to uc_link at sp[argc + 1], after return address
+ and args. */
sp[argc + 1] = (uintptr_t) ucp->uc_link;
+
+#elif defined(__aarch64__)
+ /* Store pointer to uc_link at the top of our allocated area. */
+ sp[stack_args] = (uintptr_t) ucp->uc_link;
+
+#else
+#error unimplemented for this target
+#endif
+
/* Last but not least set the register in the context at ucp so that a
subsequent setcontext or swapcontext picks up the right values:
- Set instruction pointer to the target function.
- Set stack pointer to the just computed stack pointer value.
- Set Cygwin-specific uclink register to the address of the pointer
- to uc_link. */
+ to uc_link.
+
+ x86_64: uclink register is rbx (callee-saved)
+ ARM64: uclink register is x19 (callee-saved) */
+
ucp->uc_mcontext._MC_instPtr = (uint64_t) func;
ucp->uc_mcontext._MC_stackPtr = (uint64_t) sp;
+
+#if defined(__x86_64__)
ucp->uc_mcontext._MC_uclinkReg = (uint64_t) (sp + argc + 1);
+
+#elif defined(__aarch64__)
+ /* Set LR to __cont_link_context for ARM64 (used as return address). */
+ ucp->uc_mcontext.lr = (uint64_t) __cont_link_context;
+ ucp->uc_mcontext._MC_uclinkReg = (uint64_t) (sp + stack_args);
+
+#else
+#error unimplemented for this target
+#endif
}
--
2.50.1.windows.1
Cygwin-Add-AArch64-support-for-signal-handling-and-u.patch
Description: Cygwin-Add-AArch64-support-for-signal-handling-and-u.patch
