Re: [PATCH v10 1/1] aarch64: Implement Structured Exception Handling (SEH) on AArch64

Alice Carlotti <[email protected]>
Newsgroups gmane.comp.gnu.binutils
Message-ID <[email protected]>
On Fri, Jun 26, 2026 at 03:30:13PM +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 <[email protected]>



> Changes in v10:
> - Rename offset_shift/reg_shift to offset_multiplier/reg_multiplier.
I think you misinterpreted my review - I wanted you to rename the local
variables in seh_aarch64_add_unwind_element:
divided_by -> offset_multipler
regn_divided_by -> reg_multiplier

The offset_shift and reg_shift field names were fine, and the new names are
wrong (because the values are shift amounts, not multipliers).  Please change
the field names back to offset_shift and reg_shift.


From previous review:
> > > 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?
> 
> Yes, something should be wrong if func_size < 0, no need to check if func_size >= 0.

Can you explain why there's no need to check when func_size >= 0?  It looks to
me like we could end up in that case without the function size being fully
evaluated, but I don't really understand what this is checking.


> > > 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. 
> 
> The description will be extended with text that big endian host has not been validated.
> It will be validated, and a separate patch will be submitted if needed. 

I think deliberately committing code that is broken for a big endian host, and
then testing and fixing it later, is the wrong approach when doing it correctly
is no more complex (and, in my opinion, might be a bit clearer).

> > It would be clearer to use separate variables here, and only combine them into
> > a single header value within seh_aarch64_emit_xdata_record.   We can't do the
> > type-punning anyway, so there's no benefit to putting the values into a struct.
> > This also avoids any ambiguity about whether the short or extended fields
> > should be used for computations in this function.
> 
> This can be done after validating big endian host if needed.

Or just do it now - see below.


From patch:
> +static void
> +obj_coff_seh_save_reg (const int type)
> +{
> +  gas_assert (type >= 0 && type <= unwind_last_type);
> +
> +  const struct aarch64_unwind_info *info
> +    = aarch64_unwind_code_data + type;
> +
> +  SKIP_WHITESPACE ();
> +
> +  char *symbol_name = NULL;
> +  unsigned reg = -1;
> +
> +  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;
> +
> +      /* Check that reference registers are not higher than x30.  */

Nit: s/reference/referenced/

> +      if (info->reg_type == 'x' && (reg + (info->reg_pair ? 1 : 0)) > 30)
> +	as_bad (_("unexpected register number"));
> +    }
> +

...
> +static void
> +seh_aarch64_emit_xdata_record (struct seh_aarch64_context *seh_ctx,
> +			       const bool is_fragmented_function,
> +			       const bool is_last_frag,
> +			       const uintptr_t frag_size,
> +			       const uintptr_t fragment_offset,
> +			       const unsigned prologue_size,
> +			       const unsigned prologue_insn_count,
> +			       const unsigned first_fragment_scope,
> +			       const unsigned last_fragment_scope)
> +{
> +  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;
This condition is still wrong.


> +  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;
This looks wrong - surely the start index is 1 in this case (with unwind code
index 0 being end_c, to indicate the phantom prologue).

> +	  else
> +	    {
> +	      const seh_aarch64_epilogue_scope *scope;
> +	      scope = scopes + first_fragment_scope;
> +	      header->ext_epilogue_count = scope->epilogue_start_index;
> +	    }
> +	}
> +    }
> +
> +  md_number_to_chars (frag_more (header_size), seh_ctx->xdata_header_value,
> +		      header_size);

I think we should just build up the headers manually - this avoids endianess or
other platform-specific issues, and simplifies the code a bit.  So something
like:

+  unsigned code_words = ((unwind_bytes + 3) >> 2);
+
+  /* Compute and emit the xdata record header.  */
+  uint32_t header = frag_size >> 2;
+  if (seh_ctx->has_exception_data)
+    header |= 1 << 20;
+  if (epilogue_count <= 31 && code_words <= 31)
+    {
+      header |= epilogue_count << 22;
+      header |= code_words << 27;
+      md_number_to_chars (frag_more (4), header, 4));
+    }
+  else
+    {
+      md_number_to_chars (frag_more (4), header, 4));
+      uint32_t header2 = epilogue_count | (code_words << 16)
+      md_number_to_chars (frag_more (4), header2, 4));
+    }

This doesn't use the E=1 format (which only saves 4 bytes) - if we wanted to
include that then we'd need to add an initial check for:
+  if (epilogue_count == 1 && code_words <= 31
+      && /* Check epilogue start index <= 31.  */
+      && /* Check epilogue is at the end of the function - this doesn't seem
to be documented, but it's the only sensible way I can see to specify the
offset in this case.  */)

However, I can't see clear documentation for how this works, so it might be
better to ignore the E=1 format for now, and seek clarification before
implementing it.

> +
> +  if (header->ext_epilogue_count && !header->e)
> +    {
> +      seh_aarch64_emit_epilogue_scopes (seh_ctx,
> +					fragment_offset, prologue_size,
> +					first_fragment_scope,
> +					last_fragment_scope,
> +					has_phantom_prologue);
> +      if (has_phantom_prologue)
> +	{
> +	  const uint32_t epilogue_start_index_encoded = 1 << 22;
> +	  const uint32_t epilogue_start_offset_encoded
> +	    = (frag_size - prologue_insn_count * 4) >> 2;
> +	  md_number_to_chars (frag_more (4),
> +			      epilogue_start_index_encoded
> +			      | epilogue_start_offset_encoded, 4);
> +	}
> +    }
> +
> +  if (header->ext_code_words)
> +    seh_aarch64_emit_unwind_codes (seh_ctx, prologue_size, first_epilogue_index,
> +				   last_epilogue_index, has_phantom_prologue);
> +
> +  if (header->x == 1)
> +    {
> +      if (seh_ctx->handler.X_op == O_symbol)
> +	seh_ctx->handler.X_op = O_symbol_rva;
> +
> +      emit_expr (&seh_ctx->handler, 4);
> +
> +      /* Emit the fragment offset.  */
> +      md_number_to_chars (frag_more (4), fragment_offset, 4);
> +
> +      /* Use the same SEH handler data for all fragments.
> +	 The SEH handler data is emitted after the last fragment.  */
> +      expressionS exp;
> +      memset (&exp, 0, sizeof (expressionS));
> +      exp.X_op = O_symbol_rva;
> +      exp.X_add_symbol = seh_ctx->handler_data_xdata_addr;
> +      emit_expr (&exp, 4);
> +    }
> +}
> +
> +static bool
> +seh_function_size (const struct seh_aarch64_context *seh_ctx,
> +		  uintptr_t *size)
> +{
> +  fragS *start_frag, *end_frag;
> +  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)
> +    return false;
> +
> +  *size = func_size;
> +  return true;
> +}
> +
> +/* 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;
This value shouldn't be needed in the function call, so we can just inline this
condition into the loop termination check.

> +
> +      /* 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;
> +	    }
> +	}
> +
> +
> +      /* Emit a .xdata record for the current fragment.  */
> +      seh_aarch64_emit_xdata_record (seh_ctx, is_fragmented_function,
> +				     is_last_frag, frag_size, fragment_offset,
> +				     prologue_size, prologue_insn_count,
> +				     first_fragment_scope, last_fragment_scope);
> +
> +      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);
> +}

...

> +typedef struct seh_aarch64_context
> +{
> +  struct seh_aarch64_context *next;
> +
> +  /* Initial code-segment.  */
> +  segT code_seg;
> +  /* Function name.  */
> +  char *func_name;
> +  /* BeginAddress.  */
> +  symbolS *start_addr;
> +  /* EndAddress.  */
> +  symbolS *end_addr;
> +  /* PrologueEnd.  */
> +  symbolS *endprologue_addr;
> +
> +  symbolS *handler_data_xdata_addr;
> +  /* ExceptionHandler.  */
> +  expressionS handler;
> +  /* ExceptionHandlerData.  */
> +  expressionS handler_data;
> +
> +  subsegT subsection;
> +
> +  union {
> +    seh_aarch64_xdata_header xdata_header;
> +    valueT xdata_header_value;
> +  };
The only header field that is now used outside of seh_aarch64_emit_xdata_record
is xdata_header->x.  Please replace this union with a single boolean value
seh_ctx_cur->has_exception_data (or some similar name).  We can keep details
of the header layout contained to the single function that writes those other
fields.

> +  unsigned unwind_codes_count;
> +  unsigned unwind_codes_byte_count;
> +  seh_aarch64_unwind_code unwind_codes[AARCH64_MAX_UNWIND_CODES];
> +  unsigned epilogue_scopes_count;
> +  unsigned epilogue_scopes_capacity;
> +  seh_aarch64_epilogue_scope *epilogue_scopes;
> +  /* The function fragments.  */
> +  seh_aarch64_func_fragment func_fragment;
> +} seh_context;
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.