[PATCH v10 1/1] aarch64: Implement Structured Exception Handling (SEH) on AArch64
Alice Carlotti
alice.carlotti@arm.com
Fri Jun 26 19:17:53 GMT 2026
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 <evgeny@kmaps.co>
> 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;
More information about the Binutils
mailing list