AArch64-Win64: Fix SEH local unwind for Exit/Break/Continue in try/finally
Fixes #40203
Summary
This patch fixes Structured Exception Handling (SEH) for local unwind operations on Windows ARM64. When Exit/Break/Continue is used inside a try/finally block, the finally block now executes correctly before control transfers to the target.
Root Cause
On AArch64-Win64, RtlUnwindEx validates that TargetFrame >= EstablisherFrame. The EstablisherFrame is computed by Windows as SP-at-function-entry. Due to the standard ARM64 prolog stp fp,lr,[sp,#-16]!, we have:
- FP = SP_at_entry - 16
- Therefore SP_at_entry = FP + 16
Passing just FP as TargetFrame fails validation with STATUS_INVALID_UNWIND_TARGET (0xC0000028).
Additionally, ARM64 uses SP-relative addressing for temps (positive offsets from SP), unlike x64 which uses FP-relative (negative offsets from RBP). The $fin$ handler has its own SP but receives the parent's FP, so it cannot access parent's temps without address conversion.
Changes
compiler/aarch64/cgcpu.pas
-
Implement
g_local_unwind: Generates call to_fpc_local_unwind(FP+16, target_label)for Exit/Break/Continue in try/finally blocks. Passes FP+16 to satisfy RtlUnwindEx's TargetFrame validation. -
Store
total_stackframe_size: Saves the offset from FP to SP after prolog completes, needed for temp address conversion in fin handlers. -
Skip
mov sp,fpin exceptfilter epilog: The prolog already correctly skipsmov fp, spfor exceptfilters (since the exceptfilter receives the parent's FP). However, commit 959804798c addedmov sp, fpto the epilog for Windows without a corresponding exceptfilter check. Since exceptfilter's FP points to the parent's frame (not its own local frame), executingmov sp, fpcorrupts the stack. This causes memory corruption when -O2 optimization is used, as callee-saved registers (x19-x28) trigger theregsstoredcode path which includes this instruction. Fix: Addpotype_exceptfilterchecks at all three locations ing_proc_exitwheremov sp, fpis generated.
compiler/ncgbas.pas
Convert SP-relative temps to FP-relative for exceptfilter: The $fin$ handler receives the parent's FP but has its own SP. Temps allocated as [SP + X] in the parent are converted to [FP + (X - total_stackframe_size)] so the handler can access them correctly.
compiler/procinfo.pas
Add total_stackframe_size field: Stores the offset from FP to SP after prolog, used by ncgbas for temp address conversion.
compiler/ogcoff.pas
Fix ADRP relocation calculation: The addend must be added to the target address BEFORE computing the page difference, not after. This fixes incorrect address calculations for page-relative relocations.
compiler/aarch64/agcpugas.pas
Document xdata header format and set epilog start index: Adds comments clarifying the ARM64 xdata header bit layout and sets epilog start index to 1.
rtl/win/sysosh.inc
Use SYSTEM_USE_WIN_SEH: Changes from FPC_USE_WIN64_SEH to SYSTEM_USE_WIN_SEH for the _fpc_local_unwind declaration, making it available on ARM64.
rtl/win/systhrd.inc
8-byte alignment for threadvars: Ensures thread variable data is allocated at 8-byte boundaries on 64-bit platforms. ARM64 atomic operations (ldxr/stxr) require natural alignment.
Testing
Extensively tested on Windows 11 ARM64 with 44+ test cases covering:
test_seh_suite.pas (19 tests)
- Basic exception handling with try/except/finally
- Exit inside try/finally
- Break inside try/finally in loop
- Continue inside try/finally in loop
- Nested try/finally with exit
- Nested exceptions
- Re-raise exception
- Exit with result value
- Multiple exits in try/finally
- Deep nesting (5+ levels) with exit
- Exit with many local variables (integers, doubles, pointers, strings, arrays)
- Break/Continue with many locals and globals
- Multiple exits with padding arrays
- String manipulation in finally blocks
- Global variable modification in finally
- Exit inside finally block itself
- Nested exit in finally blocks
forintest_extended.pas (25 tests)
- For-in with Exit inside try/finally
- For-in with Exit inside try/except
- For-in with exception raised and caught
- For-in with exception in try/finally (propagation)
- Nested for-in with Exit
- For-in with Break inside try/finally
- For-in with Continue inside try/finally
- For-in with multiple Exit points
- For-in with re-raised exception
- For-in with exception replaced
- For-in over array with Exit
- For-in with string characters and Exit
- For-in with deeply nested try blocks (3 levels)
- For-in with local variables preservation
- For-in with Exit inside nested procedure
- For-in with Exit and var parameters
- For-in with multiple lists and Exit
- For-in with complex control flow
test_minimal.pas
- Minimal reproduction case from issue #40203
- Verifies finally block executes on Exit
All 44+ tests pass on Windows ARM64.
Test Files
test_minimal.pas test_seh_suite.pas forintest_extended.pas