[PATCH v9 1/1] aarch64: Implement Structured Exception Handling (SEH) on AArch64
Alice Carlotti
alice.carlotti@arm.com
Mon Jun 22 14:06:01 GMT 2026
On Wed, Jun 17, 2026 at 10:39:47AM +0200, Evgeny Karpov wrote:
> The patch reuses shared helpers for SEH and implements SEH on AArch64.
> The implementation is based on
> (https://learn.microsoft.com/en-us/cpp/build/arm64-exception-handling?view=msvc-170)
> and pdata/xdata SEH records are emitted from md_finish.
>
> When .pdata/.xdata is emitted, the function size is required.
> Function sizes are calculated as late as possible, and the code segment needs
> to be relaxed to be able to calculate the function sizes.
>
> Initially, obj_coff_generate_pdata was called in write_object_file.
> Before the change, obj_coff_generate_pdata was used only to validate
> syntax, which was sufficient for that purpose. However, that location
> seems incorrect, as it is too late to emit .pdata/.xdata records
> in the AArch64 case.
>
> md_finish has been declared for AArch64 and extended with
> seh_aarch64_write_data to emit .pdata/.xdata records after all
> assembly has been completed.
>
> Signed-off-by: Evgeny Karpov <evgeny@kmaps.co>
>
This looks better. However, there are some points from my previous review that
still need fixing (or I need a clearer explanation of why they're actually not
wrong). I've copied the previous comments to this thread:
On Thu, Jun 11, 2026 at 12:53:52PM +0200, Evgeny Karpov wrote:
> On Thu, 16 Apr 2026, Alice Carlotti wrote:
> > > + addressT start_offset, end_offset;
> > > + start_frag = symbol_get_frag_and_value (seh_ctx->start_addr, &start_offset);
> > > + end_frag = symbol_get_frag_and_value (seh_ctx->end_addr, &end_offset);
> > > +
> > > + intptr_t func_size = end_frag->fr_address + end_offset
> > > + - start_frag->fr_address - start_offset;
> > > + if (func_size < 0)
> >
> > Does this always hold when the function size "has not been evaluated"?
>
> Yes, it makes sense to stop compilation when function size cannot be properly calculated.
> It means the assembler has an internal issue and should be fixed.
I think you missed my point - I'm asking whether "the function size has not
been evaluated" only happens when func_size < 0, or can it happen with
func_size >= 0 as well?
...
> > You're missing handling for too many epilogs or too many code words.
>
> Unwind capacity is handled in seh_aarch64_add_unwind_element,
> and epilogue capacity is handled in obj_coff_seh_startepilogue,
> by AARCH64_MAX_UNWIND_CODES_SIZE and AARCH64_MAX_EPILOGUE_SCOPES.
I see - we currently apply the per-fragment limits on number of unwind codes
and epilogue scopes to the entire function, so we don't need to check those
limits when we later split the function into fragments. But the downside is
that we bail out in some cases where we could have used fragments instead.
I'm ok with this limitation remaining in the current patch for now, as long as
it is explicitly called out in both:
- any lists of currently unsupported functionality, and
- a comment in this function explaining why we don't currently consider those
limits during fragmentation (the comment above the "while (true)" would be a
good place to mention this).
...
> > > +
> > > +/* SEH COFF AArch64 implementation partially intersects with the x64
> > > + version, however it has a different extension to the unwind codes.
> > > + It emits SEH data to pdata and xdata sections. In some cases SEH
> > > + data could be emitted to a packed record in the pdata section
> > > + without the need for data in the xdata section. However, the packed
> > > + pdata record is not implemented yet. */
> >
> > Can we have a separate list of things that aren't yet supported (at least as a
> > separate paragraph). As well as packed unwind data, I think this includes:
> > - Support for SVE/SME (at least, I think that's what the unimplemented unwind
> > codes are used for);
> > - Reuse of identical unwind code sequences.
>
> Ok, it makes sense.
> "Reuse of identical unwind code sequences" is not applicable for
> aarch64-w64-mingw32.
Really? I see no reason why it wouldn't be applicable, and the documentation
itself implicitly mentions reusing unwind codes:
These assumptions are made in the exception handling description:
- Prologs and epilogs tend to mirror each other. By taking advantage of
this common trait, the size of the metadata needed to describe unwinding
can be greatly reduced. Within the body of the function, it doesn't
matter whether the prolog's operations are undone, or the epilog's
operations are done in a forward manner. Both should produce identical
results.
(https://learn.microsoft.com/en-us/cpp/build/arm64-exception-handling?view=msvc-170#assumptions)
> The description will be extended with text below.
>
> /* The current implementation does not include:
> - Packed pdata record.
> - Support for AdvSIMD and SVE.
> - Handling for pacibsp. */
>
...
> > > +
> > > + subsegT subsection;
> > > +
> > > + union {
> > > + seh_aarch64_xdata_header xdata_header;
> > > + valueT xdata_header_value;
> > > + };
> >
> > I think this type punning is incorrect for a big endian host, and there might
> > be other portability issues beyond this. Type punning to a struct of two
> > uint32_t values is more likely to work, but that's still not guaranteed by the
> > C standard to be correct, so it might be best just to build uint32_t values
> > from the individual bitfields piece by piece.
>
> The big endian host should be supported properly by md_number_to_chars that is used
> for emitting pdata/xdata records.
That only ensures that the conversion from valueT to bytes-on-disk is handled
correctly. The issue is with the bit-field to valueT conversion - on a
big-endian host I think this would pack the bitfields in the wrong order.
> Regards,
> Evgeny
>
(On Wed, Jun 17, 2026 at 10:39:47AM +0200, Evgeny Karpov wrote:)
...
> +struct aarch64_unwind_info {
> + const char *directive;
> + char reg_type;
> + unsigned char offset_bits;
> + unsigned char reg_bits;
> + unsigned char reg_max;
> + unsigned char code_bits;
> + unsigned char code;
> + unsigned char offset_shift;
> + unsigned char offset_addend;
> + unsigned char reg_shift;
> + unsigned char reg_addend;
> + unsigned char size;
> +};
Could you reorder the fields in the struct definition as well, to match the
improved instantiation order?
...
> +/* Obtain available unwind element. */
> +static void
> +seh_aarch64_add_unwind_element (const seh_aarch64_unwind_types unwind_type,
> + unsigned offset, unsigned reg)
> +{
> + const struct aarch64_unwind_info *info
> + = aarch64_unwind_code_data + unwind_type;
> + unsigned value_offset_bits = 0;
> +
> + if ((seh_ctx_cur->unwind_codes_byte_count
> + + info->size) > AARCH64_MAX_UNWIND_CODES_SIZE)
> + as_bad (_("no unwind element available."));
> +
> + unsigned value = 0;
> +
> + if (info->offset_bits)
> + {
> + const unsigned divided_by = 1u << info->offset_shift;
offset_multiplier would be a better name
> + if (offset & (divided_by - 1))
> + as_bad (_("offset should be divided by %u"), divided_by);
"offset should be a multiple of %u"
> + offset = (offset >> info->offset_shift) - info->offset_addend;
> + if (offset >= (1u << info->offset_bits))
> + as_bad (_("offset overflows expected range"));
> + value |= offset << value_offset_bits;
> + value_offset_bits += info->offset_bits;
> + }
> +
> + if (info->reg_bits)
> + {
> + const unsigned regn_divided_by = 1u << info->reg_shift;
similiarly reg_multiplier
> + reg -= info->reg_addend;
> + if (reg & (regn_divided_by - 1))
> + as_bad (_("unexpected register number"));
> + reg >>= info->reg_shift;
> + if (reg >= (1u << info->reg_bits))
> + as_bad (_("unexpected register number"));
> + value |= reg << value_offset_bits;
> + value_offset_bits += info->reg_bits;
> + }
...
> + if (info->reg_bits)
> + {
> + char name_end = get_symbol_name (&symbol_name);
> + if (info->reg_type != *symbol_name)
> + as_bad ("unexpected register name");
> +
> + reg = atoi (symbol_name + 1);
> + (void) restore_line_pointer (name_end);
> +
> + if (!skip_whitespace_and_comma (1))
> + return;
> +
> + const unsigned reg_max = info->reg_max ? info->reg_max: 30;
> + if (reg > reg_max)
> + as_bad (_("unexpected register number"));
> + }
This feels awkward, because the maximum isn't actually 30 in some cases. I
think it would be clearer to have a reg_pair field (instead of the reg_max)
field, and replace this check with:
+ /* Most range checks are applied during encoding. Check here that we
+ don't reference registers higher than x30. */
+ if (info->reg_type == 'x'
+ && reg + (info->reg_pair ? 1 : 0) > 30)
+ as_bad (_("unexpected register number"));
Although now I look at it some more, I think we should put all of the range
checking in the same place (checking it during parsing might give better error
locations, but I haven't verified this).
...
> +/* Write out the xdata information for one function. */
> +static void
> +seh_aarch64_write_function_xdata (struct seh_aarch64_context *seh_ctx)
> +{
> + if (!seh_ctx->unwind_codes_byte_count)
> + return;
> +
> + const segT save_seg = now_seg;
> + const subsegT save_subseg = now_subseg;
> +
> + switch_xdata (seh_ctx->subsection, seh_ctx->code_seg);
> +
> + /* Set 4-byte alignment. */
> + frag_align (2, 0, 0);
> +
> + uintptr_t func_size = 0;
> + if (!seh_function_size (seh_ctx, &func_size))
> + {
> + as_bad (_("the function size for %s has not been evaluated"),
> + seh_ctx->func_name);
> + return;
> + }
> +
> + /* The large functions should be split into fragments smaller than 1MB with
> + 4 bytes alignment, based on
> + "Microsoft ARM64 exception handling, large functions documentation". */
> + const unsigned max_frag_size = (1 << 20) - 4;
> + const bool is_fragmented_function = func_size > max_frag_size;
> +
> + unsigned prologue_insn_count = 0;
> + for (unsigned i = 0; i < seh_ctx->unwind_codes_count; ++i)
> + {
> + if (seh_ctx->unwind_codes[i].type == unwind_end)
> + {
> + prologue_insn_count = i + 1;
> + break;
> + }
> + }
> +
> + unsigned prologue_size = seh_ctx->unwind_codes_byte_count;
> +
> + seh_aarch64_func_fragment *fragment;
> + fragment = &seh_ctx->func_fragment;
> + uintptr_t fragment_offset = 0;
> + unsigned first_fragment_scope = 0;
> + unsigned last_fragment_scope = 0;
> +
> + /* Large functions (>= 1MB) will be split into multiple fragments.
> + However, it is expected the most of the functions will have only one
> + fragment. This loop iterates fragments and emit them. */
> + while (true)
> + {
> + fragment->xdata_addr = symbol_temp_new_now ();
> + fragment->offset = fragment_offset;
> + fragment->next = NULL;
> +
> + /* Calculate current fragment size. */
> + uintptr_t frag_size = func_size - fragment_offset;
> + if (frag_size > max_frag_size)
> + frag_size = max_frag_size;
> +
> + const bool is_last_frag = (fragment_offset + frag_size) == func_size;
> +
> + /* If it is a fragmented function, the epilogue range should be calculated
> + and will be emitted for the current fragment, otherwise all epilogues
> + will be emitted. */
> + if (!is_fragmented_function)
> + last_fragment_scope = seh_ctx->epilogue_scopes_count;
> + else
> + {
> + first_fragment_scope = last_fragment_scope;
> + for (unsigned i = first_fragment_scope;
> + i < seh_ctx->epilogue_scopes_count; ++i)
> + {
> + const seh_aarch64_epilogue_scope *scope
> + = seh_ctx->epilogue_scopes;
> + scope += i;
> + if (scope->epilogue_start_offset >= (fragment_offset + frag_size))
> + break;
> +
> + if (scope->epilogue_end_offset >= (fragment_offset + frag_size))
> + {
> + frag_size = scope->epilogue_start_offset - fragment_offset;
> + break;
> + }
> +
> + if (scope->epilogue_start_offset >= fragment_offset)
> + last_fragment_scope = i + 1;
> + }
> + }
> +
> + seh_aarch64_xdata_header *header = &seh_ctx->xdata_header;
> + const seh_aarch64_epilogue_scope *scopes = seh_ctx->epilogue_scopes;
> +
> + /* Initialize the .xdata record. */
> + const uint32_t func_length_encoded = frag_size >> 2;
> + header->func_length = func_length_encoded;
> + header->vers = 0;
> + header->e = 0;
> + header->code_words = 0;
> + header->epilogue_count = 0;
> +
> + header->ext_code_words = 0;
> + header->ext_epilogue_count = last_fragment_scope
> + - first_fragment_scope;
> + header->reserved = 0;
> +
> + /* Calculate epilogue indexes for the current fragment. */
> + unsigned first_epilogue_index = 0;
> + unsigned last_epilogue_index = 0;
> + if (!header->ext_epilogue_count)
> + {
> + first_epilogue_index = prologue_size;
> + last_epilogue_index = prologue_size;
> + }
> + else
> + {
> + const seh_aarch64_epilogue_scope *scope;
> + scope = scopes + first_fragment_scope;
> + first_epilogue_index = scope->epilogue_start_index;
> + last_epilogue_index = seh_ctx->unwind_codes_byte_count;
> + }
> +
> + /* Calculate how many unwind bytes will be emitted in .xdata record. */
> + unsigned unwind_bytes = prologue_size;
> +
> + /* Check if current fragment has a phantom prologue. If yes, then
> + the unwinding size should be adjusted. */
> + const bool has_phantom_prologue = is_fragmented_function && is_last_frag;
> + if (has_phantom_prologue && unwind_bytes)
> + {
> + /* One more epilogue scope and unwind code are emitted with phantom
> + prologue. */
> + unwind_bytes += 1;
> + ++header->ext_epilogue_count;
> + }
> +
> + /* Calculate the number of code words with 4-byte alignment. */
> + header->ext_code_words = (unwind_bytes + 3) / 4;
> +
> + /* Check if short or extended header for a .xdata record should be
> + used. */
> + unsigned header_size = 8;
> + if ((header->ext_code_words != 0 || header->ext_epilogue_count != 0)
> + && header->ext_code_words < 32
> + && header->ext_epilogue_count < 32)
> + {
> + header_size = 4;
> + header->code_words = header->ext_code_words;
> + header->epilogue_count = header->ext_epilogue_count;
> + if (header->epilogue_count == 1)
> + {
> + header->e = 1;
> + if (has_phantom_prologue)
> + header->ext_epilogue_count = 0;
> + else
> + {
> + const seh_aarch64_epilogue_scope *scope;
> + scope = scopes + first_fragment_scope;
> + header->ext_epilogue_count = scope->epilogue_start_index;
> + }
> + }
> + }
> +
> + /* Emit a .xdata record for the current fragment. */
> + seh_aarch64_emit_xdata_record(seh_ctx, frag_size, fragment_offset,
> + header_size,
> + prologue_size, prologue_insn_count,
> + has_phantom_prologue,
> + first_fragment_scope, last_fragment_scope,
> + first_epilogue_index, last_epilogue_index);
> +
> + fragment_offset += frag_size;
> + /* Exit the loop if it is the latest fragment. */
> + if (is_last_frag)
> + break;
> +
> + /* Allocate a new fragment that will be used also for emitting a .pdata
> + record. */
> + fragment->next = XCNEW (seh_aarch64_func_fragment);
> + fragment = fragment->next;
> + }
> +
> + subseg_set (save_seg, save_subseg);
> +}
> +
...
> diff --git a/gas/testsuite/gas/pe/seh-aarch64.d b/gas/testsuite/gas/pe/seh-aarch64.d
> new file mode 100644
> index 00000000000..218366ab6b0
> --- /dev/null
> +++ b/gas/testsuite/gas/pe/seh-aarch64.d
> @@ -0,0 +1,66 @@
> +#objdump: -s -j .xdata
> +#name: PEP aarch64 SEH
> +
> +.*: file format pe-aarch64-little
> +
> +Contents of section .xdata:
> +
> +# .xdata SEH record
> +# 0x98700000 code-words: 19
> +# epilogue-count: 1
> +# single-epilogue-in-header: 1
> +# exception-data: 1
> +# version: 0
> +# function-size: 0
> +# 0xe6 .seh_save_next
> +# 0xe1 .seh_save_fp
> +# 0xfc .seh_pac_sign_lr
> +# 0xe3 .seh_nop
> +# 0xe218 .seh_add_fp 192
> +# 0x36 .seh_save_r19r20_x 176
> +# 0x93 .seh_save_fplr_x 160
> +# 0x52 .seh_save_fplr 144
> +# 0xdeef .seh_save_freg_x d15, 128
> +# 0xde2f .seh_save_freg_x d9, 128
> +# 0xde0f .seh_save_freg_x d8, 128
> +# 0xddce .seh_save_freg d15, 112
> +# 0xdc4e .seh_save_freg d9, 112
> +# 0xdc0e .seh_save_freg d8, 112
> +# 0xdbcb .seh_save_fregp_x d15, 96
> +# 0xda4b .seh_save_fregp_x d9, 96
> +# 0xda0b .seh_save_fregp_x d8, 96
> +# 0xd9ca .seh_save_fregp d15, 80
> +# 0xd84a .seh_save_fregp d9, 80
> +# 0xd80a .seh_save_fregp d8, 80
> +# 0xd748 .seh_save_lrpair x29, 64
> +# 0xd648 .seh_save_lrpair x21, 64
> +# 0xd608 .seh_save_lrpair x19, 64
> +# 0xce85 .seh_save_regp_x x29, 48
> +# 0xcc45 .seh_save_regp_x x20, 48
> +# 0xcc05 .seh_save_regp_x x19, 48
> +# 0xca84 .seh_save_regp x29, 32
> +# 0xc844 .seh_save_regp x20, 32
> +# 0xc804 .seh_save_regp x19, 32
> +# 0xd561 .seh_save_reg_x x30, 16
> +# 0xd421 .seh_save_reg_x x20, 16
> +# 0xd401 .seh_save_reg_x x19, 16
> +# 0xd2c0 .seh_save_reg x30, 0
> +# 0xd040 .seh_save_reg x20, 0
> +# 0xd000 .seh_save_reg x19, 0
> +# 0xe0ffffff .seh_stackalloc 268435440
> +# 0xe0000800 .seh_stackalloc 32768
> +# 0xc020 .seh_stackalloc 512
> +# 0x01 .seh_stackalloc 16
> +# 0xe4 nop
> +# 0xe4 nop
> +# 0x00000000 seh-handler
> +# 0x00000000 fragment-offset
> +# 0x0000005c seh-handle-data-address
> +# 0x00000001 long 1 (seh_handlerdata)
> +
> + 0000 00007098 e6e1fce3 e2183693 52deefde .*
> + 0010 2fde0fdd cedc4edc 0edbcbda 4bda0bd9 .*
> + 0020 cad84ad8 0ad748d6 48d608ce 85cc45cc .*
> + 0030 05ca84c8 44c804d5 61d421d4 01d2c0d0 .*
> + 0040 40d000e0 ffffffe0 000800c0 2001e4e4 .*
> + 0050 00000000 00000000 5c000000 01000000 .*
> \ No newline at end of file
> diff --git a/gas/testsuite/gas/pe/seh-aarch64.s b/gas/testsuite/gas/pe/seh-aarch64.s
> new file mode 100644
> index 00000000000..5868a7f57dc
> --- /dev/null
> +++ b/gas/testsuite/gas/pe/seh-aarch64.s
> @@ -0,0 +1,49 @@
> + .text
> + .seh_proc foo
> + .seh_stackalloc 16
> + .seh_stackalloc 512
> + .seh_stackalloc 32768
> + .seh_stackalloc 268435440
> + .seh_save_reg x19, 0
> + .seh_save_reg x20, 0
> + .seh_save_reg x30, 0
> + .seh_save_reg_x x19, 16
> + .seh_save_reg_x x20, 16
> + .seh_save_reg_x x30, 16
> + .seh_save_regp x19, 32
> + .seh_save_regp x20, 32
> + .seh_save_regp x29, 32
> + .seh_save_regp_x x19, 48
> + .seh_save_regp_x x20, 48
> + .seh_save_regp_x x29, 48
> + .seh_save_lrpair x19, 64
> + .seh_save_lrpair x21, 64
> + .seh_save_lrpair x29, 64
> + .seh_save_fregp d8, 80
> + .seh_save_fregp d9, 80
> + .seh_save_fregp d15, 80
> + .seh_save_fregp_x d8, 96
> + .seh_save_fregp_x d9, 96
> + .seh_save_fregp_x d15, 96
> + .seh_save_freg d8, 112
> + .seh_save_freg d9, 112
> + .seh_save_freg d15, 112
> + .seh_save_freg_x d8, 128
> + .seh_save_freg_x d9, 128
> + .seh_save_freg_x d15, 128
> + .seh_save_fplr 144
> + .seh_save_fplr_x 160
> + .seh_save_r19r20_x 176
> + .seh_add_fp 192
> + .seh_nop
> + .seh_pac_sign_lr
> + .seh_set_fp
> + .seh_save_next
> + .seh_endprologue
> + .seh_handler _ZN9exception6handleEPvS0_S0_S0_, @except
> + .seh_handlerdata
> + .long 1
> + .seh_code
> + .seh_startepilogue
> + .seh_endepilogue
> + .seh_endproc
Shouldn't seh directives be interleaved with the matching assembly
instructions? We don't check the offsets of individual seh unwind codes, but
this test file looks like it would have a prologue that overlaps the epilogue
and extends beyond the end of the function (which is something that we should
probably check for).
Additionally, why is there a mismatch between .seh_set_fp near the end of the
prologue in the .s file, and .seh_save_fp near the start of the reversed
prologue in the .d file?
Thanks,
Alice
More information about the Binutils
mailing list