[PATCH v7 3/3] aarch64: Implement Structured Exception Handling (SEH) on AArch64

Evgeny Karpov evgeny.karpov@arm.com
Thu Jun 11 10:53:52 GMT 2026


On Thu, 16 Apr 2026, Alice Carlotti wrote:
> > 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 declared in write_object_file.
> 
> I think you mean "called in write_object_file".

Yes, it will be fixed.
 
> > 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>
> 
> Some general remarks:
> 
> - Epilogue/epilog and prologue/prolog in various names.  We should be
>   consistent with Microsoft's description, so remove the "ue"s.
> 
> - It hard to know whether the input parsing is correct when this patch doesn't
>   include any tests or documentation for the assembler directives.

As it was mentioned in another message, prologue and epilogue will be used.
The patch series has been validated with CI tests. These tests will be added to binutils.

obj-coff-seh-aarch64.h contains a description for the assembler directives and 
obj-coff-seh-aarch64.cpp provides the link to the documentation.
One more link will be added to obj-coff-seh-aarch64.h.

> > +  unsigned code_bits;
> > +  unsigned code;
> > +  unsigned offset_right_shift;
> > +  unsigned offset;
> > +  unsigned reg_right_shift;
> > +  unsigned reg_offset;
> > +  unsigned size;
> 
> The non-pointer members can all use 'unsigned char'.
> 
> The word "offset" has two different meanings here - how about using
> offset_addend and reg_addend instead?
> 
> Specifying the shift direction in the names is unnecessary.
> 
> Also see below about field ordering.

unsigned char will be used instead.
offset will be renamed to offset_addend and reg_offset to reg_addend.
Shift direction will be removed.

> > +};
> > +
> > +static const struct aarch64_unwind_code_pack_info
> > +aarch64_unwind_code_pack_data[] = {
> > +/* Unwind codes packing for AArch64 is described at
> 
> Nit: s/is/are/, and I think this comment belongs above the array type rather
> than inside the braces.
> 
> These codes are for the non-packed unwind format, so 'pack' should be dropped
> from the names and description.  You could also add that the array is indexed
> by the seh_aarch64_unwind_types enum.

It will be fixed and moved slightly higher.
The type will be renamed and the description will be extended.
 
> > +   https://learn.microsoft.com/en-us/cpp/build/arm64-exception-handling?view=msvc-170#unwind-codes
> > +   and calculated in seh_aarch64_add_unwind_element function.  */
> > +  {
> > +    .directive = NULL, .offset_bits = 5, .reg_bits = 0,
> > +    .code_bits = 3, .code = AARCH64_UNOP_ALLOCS, .offset_right_shift = 4,
> > +    .offset = 0, .reg_right_shift = 0, .reg_offset = 0, .size = 1
> > +  },
> 
> I find this layout rather difficult to read.  Reordering the field definitions
> and adding more line breaks would help - for example:
> 
> +  {
> +    .directive = NULL,
> +    .size = 1,
> +    .code_bits = 3, .code = AARCH64_UNOP_ALLOCS,
> +    .offset_bits = 5, .offset_shift = 4, .offset_addend = 0,
> +    .reg_bits = 0, .reg_shift = 0, .reg_addend = 0
> +  },
> 
> The AARCH64_UNOP_* defines add a level of indirection without (in my eyes)
> improving clarity, so I'd drop those (but add in a comment with the unwind code
> name when this can't be deduced from the directive).
> 
> Members that are NULL or 0 can be empty initialised.  If you also eliminate the
> code_bits member (reducing the struct size to 16 bytes), then this reduces the
> above example to:
> 
> +  {
> +    /* alloc_s */
> +    .size = 1, .code = 0b000,
> +    .offset_bits = 5, .offset_right_shift = 4,
> +  },
 
The reordering will be done.
AARCH64_UNOP_ will be removed.
Members that are NULL or 0 will be removed.
 
> > +static void
> > +seh_aarch64_add_unwind_element (const seh_aarch64_unwind_types unwind_type,
> > +				unsigned offset, unsigned reg)
> > +{
> > +  gas_assert (in_seh_proc);
> > +
> > +  const struct aarch64_unwind_code_pack_info *unwind_code_pack_info
> 
> This variable could just be called 'info'; the shorter name makes the rest of
> the function much more readable.

unwind_code_pack_info will be renamed to info.

> > +      + unwind_code_pack_info->size) > AARCH64_MAX_UNWIND_CODES_SIZE)
> > +    as_bad (_("no unwind element available."));
> > +
> > +  seh_aarch64_unwind_code *aarch64_element;
> > +  aarch64_element = seh_ctx_cur->unwind_codes
> > +		    + seh_ctx_cur->unwind_codes_count++;
> > +  aarch64_element->value = 0;
> 
> I think these four lines would be better placed at the end of the function
> (after computing the value).  We also don't need to specify aarch64 in a local
> variable name.

aarch64_element will be renamed to element and 4 lines will be moved to the end of the function.

> > +    {
> > +      reg = (reg >> unwind_code_pack_info->reg_right_shift)
> > +	    - unwind_code_pack_info->reg_offset;
> > +      reg &= (1 << unwind_code_pack_info->reg_bits) - 1;
> > +      aarch64_element->value |= reg << value_offset_bits;
> > +      value_offset_bits += unwind_code_pack_info->reg_bits;
> > +    }
> 
> Instead of silently masking out overflowing bits in offset/reg, I think it
> would be better to explicitly check and reject.

The code will be extended with a validation for offset and reg.
 
> > +
> > +  if (unwind_code_pack_info->code_bits)
> > +    {
> > +      unsigned code = unwind_code_pack_info->code;
> > +      code &= (1 << unwind_code_pack_info->code_bits) - 1;
> > +      aarch64_element->value |= code << value_offset_bits;
> 
> I think the whole function flows a bit better if you build the value starting
> from the code.  I'd suggest the following (to replace these three if blocks):
> 
> if (offset & ~(((1 << info->offset_bits) - 1) << info->offset_shift))
>   as_bad (...)
> if (reg & ~(((1 << info->reg_bits) - 1) << info->reg_shift))
>   as_bad (...)
> 
> value = info->code;
> if (info->reg_bits)
>   value = (value << info->reg_bits)
> 	  | ((reg - info->reg_addend) >> info->reg_shift)
> if (info->offset_bits)
>   value = (value << info->offset_bits)
> 	  | ((offset - info->offset_addend) >> info->offset_shift)
> 
> 
> (The 'if's are actually unnecessary here, but they might make the code a bit
> clearer.  This also makes the 'code_bits' value unused, so you could drop that.)

It makes sense to drop the condition for code_bits.
The rest will be slightly changed as it requires value_offset_bits.

> > +{
> > +  if (!verify_context (".seh_startepilogue")
> > +      || !seh_validate_seg (".seh_startepilogue"))
> > +    return;
> > +  demand_empty_rest_of_line ();
> > +
> > +  const unsigned max_epilogue_scopes = AARCH64_MAX_EPILOGUE_SCOPES;
> 
> This could be inlined into the condition.

Ok. It will be inlined.

> > +	    S_GET_NAME (epilogue_start_addr));
> > +
> > +  if (seh_ctx_cur->epilogue_scopes_count
> > +      >= seh_ctx_cur->epilogue_scopes_capacity)
> > +    {
> > +      const unsigned initial_capacity = 32;
> 
> This variable could be inlined.

It makes sense to keep const unsigned defined to keep meaning for the value.

> > +
> > +  char *symbol_name = NULL;
> > +  unsigned reg = -1;
> > +
> > +  if (unwind_code_pack_info->reg_bits)
> > +    {
> > +      char name_end = get_symbol_name (&symbol_name);
> 
> We should check the first character - is it required to match the register
> name?

The check for the register name will be added and unwinding code will be
extended with reg_type.

> > +    }
> > +
> > +  unsigned unwind_bytes_offset = 0;
> > +  for (unsigned i = 0; i < seh_ctx->unwind_codes_count; ++i)
> > +    {
> > +      const seh_aarch64_unwind_code *code = seh_ctx->unwind_codes
> > +					    + i;
> 
> Why is this line wrapped?  I think up to 79 chars is ok.

It will be changed.

> > +      /*  emit unwind code bytes in big endian.  */
> > +      number_to_chars_bigendian (frag_more (byte_count), code->value,
> > +				 byte_count);
> > +      total_byte_count += byte_count;
> > +    }
> > +
> > +    /* handle word alignment.  */
> 
> nit: Capital H

The uppercase will be used.

> > +    unsigned required_padding = (4 - total_byte_count % 4) % 4;
> 
> This could just be:
> 
> +    unsigned required_padding = (- total_byte_count) % 4;

A good point. It will be updated.

> > +    if (required_padding)
> > +      {
> > +	/* Use AARCH64_UNOP_NOP for alignment.  */
> > +	const uint32_t nop_chain = (AARCH64_UNOP_NOP << 24)
> > +				   | (AARCH64_UNOP_NOP << 16)
> > +				   | (AARCH64_UNOP_NOP << 8)
> > +				   | AARCH64_UNOP_NOP;
> 
> How about removing the define and using:
> 
> +	/* Use the nop unwind code for alignment.  */
> +	const uint32_t nop_chain = 0xe3e3e3e3;

Ok, it will be changed.

> > +  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.

> > +  /* The large functions should be split into fragments smaller than 1MB with
> > +     4 bytes alignment.
> > +     https://learn.microsoft.com/en-us/cpp/build/arm64-exception-handling?view=msvc-170#large-functions.  */
> > +  const unsigned max_frag_size = (1 << 20) - 4;
> > +  const bool is_fragmented_function = func_size > max_frag_size;
> > +
> > +  /* [first_fragment_scope, last_fragment_scope).  */
> 
> This comment is in the wrong place.

The comment will be removed to avoid the confusion.

> > +  while (true)
> > +    {
> > +      fragment->xdata_addr = symbol_temp_new_now ();
> > +      fragment->offset = fragment_offset;
> > +      fragment->next = NULL;
> > +
> > +      uintptr_t frag_size = func_size - fragment_offset;
> 
> I think it would be marginally better to use a frag_end variable instead of
> frag_size.

It is hard to say. frag_size looks more readable than frag_end and represents
the size of the current fragment.

> > +		  break;
> > +		}
> > +
> > +	      if (scope->epilogue_start_offset >= fragment_offset)
> > +		last_fragment_scope = i + 1;
> > +	    }
> > +	}
> 
> 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.

> > +		  scope = scopes + first_fragment_scope;
> > +		  header->ext_epilogue_count = scope->epilogue_start_index;
> > +		}
> > +	    }
> > +	  md_number_to_chars (frag_more (4),
> > +			      seh_ctx->xdata_header_value, 4);
> > +	}
> 
> I think it might be clearer if this header emission were split off into a
> separated function.  It might also help to avoid tying the internal struct
> layout to the on disk format (and perhaps just passing it as separate
> arguments).

xdata record writing will be moved to a separate function.

> > +	break;
> > +
> > +      fragment->next = XCNEW (seh_aarch64_func_fragment);
> > +      fragment = fragment->next;
> > +    }
> > +
> > +  subseg_set (save_seg, save_subseg);
> 
> I've struggled to follow this function; please add some inline comments to explain the flow (and/ore refactor it to make the flow clearer).

The comments will be added.

> > +
> > +/* 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.
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.

> >  enum pointer_auth_key {
> >    AARCH64_PAUTH_KEY_A,
> > @@ -363,6 +368,7 @@ extern void aarch64_handle_align (struct frag *);
> >  extern int tc_aarch64_regname_to_dw2regnum (char *regname);
> >  extern void tc_aarch64_frame_initial_instructions (void);
> >  extern bool aarch64_fix_adjustable (struct fix *);
> > +extern void seh_aarch64_write_data (void);
> 
> Should this declaration be gated by OBJ_COFF?

seh_aarch64_write_data will be moved under OBJ_COFF scope.

Regards,
Evgeny



More information about the Binutils mailing list