[PATCH v11 1/1] aarch64: Implement Structured Exception Handling (SEH) on AArch64
Alice Carlotti
alice.carlotti@arm.com
Wed Jul 8 15:42:01 GMT 2026
On Mon, Jul 06, 2026 at 06:39:39PM +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>
>
I'll start with the disappointing news - unfortunately I don't think this patch
can be ready in time for the upcoming Binutils release.
I've been struggling for a while to understand the code in
aarch64_emit_xdata_record and its callees, and I hoped that sorting out some of
the surrounding code legibility issues would make it clear enough to allow me
to give more thorough review comments. However, the code is still quite hard
to read, and I've now also realised that there are quite a few functionality
bugs remaining in that function.
These are a few specific issues that I have spotted in my latest review, but
this list won't be exhaustive:
- The code words field in the header seems to be counting all the unwind codes
used in the function, but the unwind code emission only emits unwind codes
needed for the current fragment, so the counts don't match
- The phantom prologue handling still seems fishy - it's various additions and
subtractions in different place and I haven't worked out whether the
resulting numbers are consistent.
- prologue_size is either incorrectly named or incorrectly defined.
- For a function fragment containing no epilogues, the emitted unwind codes
don't contain a valid phantom prologue.
Before identifying the above issues, I also wrote various minor review comments
on seh_aarch64_write_function_xdata. The higher volume of comments here is not
because that bit of the code has more issues, but because it's the bit I can
understand well enough to review more thoroughly.
...
> +/* Data writing routines. */
> +static void
> +seh_aarch64_emit_epilogue_scopes (const seh_context *seh_ctx,
> + const uint64_t fragment_offset,
> + const unsigned prologue_size,
> + const unsigned first_fragment_scope,
> + const unsigned last_fragment_scope,
> + const bool has_phantom_prologue)
> +{
> + unsigned start_index_offset = 0;
> + const seh_aarch64_epilogue_scope *scopes = seh_ctx->epilogue_scopes;
> + if (first_fragment_scope < seh_ctx->epilogue_scopes_count)
> + start_index_offset = scopes[first_fragment_scope].epilogue_start_index
> + - prologue_size;
> + if (has_phantom_prologue)
> + {
> + if (start_index_offset == 0)
> + as_bad (_("start index offset for the epilogue cannot be 0 when "
> + "phantom prologue is used"));
> + --start_index_offset;
> + }
> +
> + for (unsigned i = first_fragment_scope; i < last_fragment_scope; ++i)
> + {
> + seh_aarch64_epilogue_scope scope = seh_ctx->epilogue_scopes[i];
> + scope.epilogue_start_offset_reduced = (scope.epilogue_start_offset
> + - fragment_offset) >> 2;
> + scope.epilogue_start_index -= start_index_offset;
> + uint32_t scope_code;
> + memcpy (&scope_code, &scope, sizeof (scope_code));
> + md_number_to_chars (frag_more (4), scope_code, 4);
> + }
> +}
> +
> +static void
> +seh_aarch64_emit_unwind_codes (const seh_context *seh_ctx,
> + const unsigned prologue_size,
> + const unsigned first_epilogue_index,
> + const unsigned last_epilogue_index,
> + const bool has_phantom_prologue)
> +{
> + unsigned total_byte_count = 0;
> +
> + if (has_phantom_prologue)
> + {
> + ++total_byte_count;
> + const unsigned endc_code = 0xe5;
> + md_number_to_chars (frag_more (1), endc_code, 1);
> + }
> +
> + 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;
> + const unsigned byte_count
> + = aarch64_unwind_code_data[code->type].size;
> + unwind_bytes_offset += byte_count;
> +
> + if (unwind_bytes_offset > last_epilogue_index)
> + break;
> +
> + if (unwind_bytes_offset > prologue_size
> + && unwind_bytes_offset <= first_epilogue_index)
> + continue;
> +
> + /* 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. */
> + const unsigned required_padding = (-total_byte_count) % 4;
> + if (required_padding)
> + {
> + /* Use the nop unwind code for alignment. */
> + const uint32_t nop_chain = 0xe3e3e3e3;
> +
> + md_number_to_chars (frag_more (required_padding), nop_chain,
> + required_padding);
> + }
> +}
> +
> +static void
> +seh_aarch64_emit_xdata_record (struct seh_aarch64_context *seh_ctx,
> + 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)
> +{
> + const seh_aarch64_epilogue_scope *scopes = seh_ctx->epilogue_scopes;
> +
> + unsigned epilogue_count = last_fragment_scope - first_fragment_scope;
> +
> + /* Calculate epilogue indexes for the current fragment. */
> + unsigned first_epilogue_index = 0;
> + unsigned last_epilogue_index = 0;
> + if (!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 = fragment_offset != 0;
> + if (has_phantom_prologue && unwind_bytes)
> + {
> + /* One more epilogue scope and unwind code are emitted with phantom
> + prologue. */
> + unwind_bytes += 1;
> + ++epilogue_count;
> + }
> +
> + /* Calculate the number of code words with 4-byte alignment. */
> + unsigned code_words = (unwind_bytes + 3) / 4;
> +
> + /* Initialize the .xdata header. */
> + const unsigned char has_exception_data_shift = 20;
> + const unsigned char single_epilog_shift = 21;
> + const unsigned char epilogue_count_shift = 22;
> + const unsigned char code_words_shift = 27;
> + const unsigned char ext_epilogue_count_shift = 32;
> + const unsigned char ext_code_words_shift = 48;
> + const uint32_t func_length_encoded = frag_size >> 2;
> + uint64_t header = 0;
> + header |= func_length_encoded;
> + header |= seh_ctx->has_exception_data << has_exception_data_shift;
> +
> + /* Check if short or extended header for a .xdata record should be
> + used. */
> + unsigned header_size = 8;
> + bool single_epilog = false;
> + if ((code_words != 0 || epilogue_count != 0)
> + && code_words < 32
> + && epilogue_count < 32)
> + {
> + header_size = 4;
> + if (epilogue_count == 1)
> + {
> + single_epilog = true;
> + epilogue_count = 0;
> + if (!has_phantom_prologue)
> + {
> + const seh_aarch64_epilogue_scope *scope;
> + scope = scopes + first_fragment_scope;
> + epilogue_count = scope->epilogue_start_index;
> + }
> + }
> + header |= epilogue_count << epilogue_count_shift;
> + header |= code_words << code_words_shift;
> + }
> + else
> + {
> + header |= (uint64_t) epilogue_count << ext_epilogue_count_shift;
> + header |= (uint64_t) code_words << ext_code_words_shift;
> + }
> + header |= single_epilog << single_epilog_shift;
> +
> + md_number_to_chars (frag_more (header_size), header, header_size);
> +
> + if (epilogue_count && !single_epilog)
> + {
> + 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 (code_words)
> + seh_aarch64_emit_unwind_codes (seh_ctx, prologue_size, first_epilogue_index,
> + last_epilogue_index, has_phantom_prologue);
> +
> + if (seh_ctx->has_exception_data)
> + {
> + 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);
I've been thinking about this some more, and I'm still don't think the
condition is a good match for the error message. I also realised there is a
similar error message in obj_coff_seh_start_epilogue, and the code there looks
like a better match for what the error is reporting. So maybe you should do
something similar here?
> + 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;
Nit: the declaration and definition can be put on a single line.
> + uintptr_t fragment_offset = 0;
I think fragment_start might be a better name here. With the current name,
it's harder to remember whether this is the offset of the start of the
fragment, of the offset of something within the fragment.
> + unsigned first_fragment_scope = 0;
> + unsigned last_fragment_scope = 0;
This is a non-inclusive end-point, so I think fragment_scope_end (and fragment_scope_start) would be better names. (I'd expect "last_fragment_scope" to be the index of the last included scope, not the first excluded.)
> +
> + /* Large functions (>= 1MB) will be split into multiple fragments.
> + However, it is expected the most of the functions will have only one
expected *that*
> + fragment. This loop iterates fragments and emit them. */
emits
> + 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;
> +
> + /* 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;
The is_fragmented_function boolean seems redundant, and I found it confusing to
read the old last_fragment_scope use after the assignment of a new value three
line earlier (albeit in a different block, so they're not actually linked). I
think it would be clearer to replace the above lines with something like:
+ /* Epilogue scopes in this fragment start where the previous fragment
+ (if any) ended. */
+ fragment_scope_start = fragment_scope_end;
+ if (fragment_offset + frag_size == func_size)
+ last_fragment_scope = seh_ctx->epilogue_scopes_count;
+ else
+ {
> + 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;
I don't see why this if condition is used. However, I think we can just move
the assignment outside the loop, to...
> + }
...here:
+ fragment_scope_end = i;
> + }
> +
> +
> + /* Emit a .xdata record for the current fragment. */
> + seh_aarch64_emit_xdata_record (seh_ctx,
> + 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 (fragment_offset == func_size)
> + 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);
> +}
...
More information about the Binutils
mailing list