Skip to content

perf(aarch64): refuse a gap candidate the ARM64 PE unwind data places inside a routine - #312

Merged
danielplohmann merged 3 commits into
danielplohmann:masterfrom
r0ny123:upstream-aarch64-pdata-suppression
Sep 8, 2026
Merged

perf(aarch64): refuse a gap candidate the ARM64 PE unwind data places inside a routine#312
danielplohmann merged 3 commits into
danielplohmann:masterfrom
r0ny123:upstream-aarch64-pdata-suppression

Conversation

@r0ny123

@r0ny123 r0ny123 commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

The ARM64 counterpart of #309: an ARM64 PE declares an exception directory too, the engine already reads it for candidates, and it never consulted it again when the gap scan carved entries out of addresses the image had declared to belong to a routine starting earlier.

#309 explicitly left this case open, because there is no ARM64 PE in the sample set that PR was measured against and it was itself the argument against shipping an unmeasured suppression rule. This PR closes it against a set that does contain them.

What is different about ARM64

An x64 RUNTIME_FUNCTION carries EndAddress. An ARM64 one does not: the extent has to be reconstructed, either from the packed unwind data in the record itself or from the .xdata record it points at. That is the only difference. Once an extent is known, what to do with it is identical, which is why the lookup moves rather than being written twice.

declaredExceptionRangeContaining and its _pdata_ranges state move from intel/FunctionCandidateManager up into common/FunctionCandidateManager, and the ARM64 manager fills the same list from what it reconstructs. The x64 path is unchanged — same list, same bisect, same running-maximum walk, same deliberate refusal to merge adjacent extents.

USE_PE_ARM64_PDATA_INTERIOR_GAPS, on by default, mirrors the x64 flag: a fragment record suppresses on its own because it says the whole extent is part of another function, a primary record only once the function it names has actually been recovered, and the scan resumes at the extent's end rather than one word on.

Branch layout

This branch stacks on #309 and on #310 plus its follow-up series, because it hoists code #309 introduces and edits a gap-scan loop those change. Its own commit is the last one; everything before it is those two PRs. Taking them first leaves this as a single commit on top.

Tests

tests/testAArch64PdataExtraction.py gains the interior-gap cases over a synthetic ARM64 PE carrying .pdata and .xdata: an interior address inside a recovered primary extent refused and the scan resumed at its end, a fragment record suppressing from its own first byte, a primary record not suppressing while the function it names is unrecovered, and the flag off restoring the old behaviour.

Validation

ruff check / ruff format --check pass, ty check exit 0, full suite green on this branch, and the x64 .pdata suite from #309 passes unchanged after the hoist — which is the assertion that the move is a move.

Branch shape

Rebased onto current master (395c88d) as 14 flat commits: #309's one, #311's ten, and this branch's own three. Dropped the five merge commits, the three pre-squash copies of #310, and three duplicate fix(ci) commits that had accumulated through the merges.

Content-neutral: the tree is 0b2ce4d, byte for byte what the merge-based version resolved to. ruff check, ruff format --check and ty 0.0.74 clean; the full suite was 1959 passed / 1 skipped / 2593 subtests on this identical tree.

Once #309 and #311 land this reduces to its own three commits, which is the shape described above.

… inside a routine

The exception directory was read for seeding and never for refusing. Every
RUNTIME_FUNCTION record names a routine and the bytes belonging to it, so the gap
scan could book an address the image itself declares to be interior -- the same
defect, on the same evidence, that USE_PE_X64_PDATA_INTERIOR_GAPS closes for x64
and USE_ELF_FDE_INTERIOR_GAPS closes for ELF.

It is not the same fix. An ARM64 record is 8 bytes and carries no EndAddress: a
packed record spells the length in bits 2-12 of UnwindData, and a full record puts
it in bits 0-17 of the .xdata header -- the same word that already says whether
the record is valid, so the extent costs no extra read. Fragment records (flag 2)
declare an extent as well; what the unwinder names belongs to a routine whether or
not the record describes the whole of one, and a fragment's own first word is
interior too rather than an entry.

The rule and the index are the same ones the x64 exception-directory refusal uses, so declaredExceptionRangeContaining
moves to the shared base rather than being written twice: refuse a candidate strictly
inside an extent, take a fragment's first word as interior as well, require a primary
record's function to have been recovered before its extent suppresses anything, and
resume the scan at the extent's end rather than one word on.

Measured on 840 ground-truth functions from three ARM64 Windows binaries (IDA
labels; ping.exe, robocopy.exe, bcrypt.dll):

    TPR  96.905% -> 96.905%   (unchanged)
    PPV  89.352% -> 90.344%   (97 false starts -> 87)

Ten of the eleven addresses it refuses are the last word of the routine that
declares them. Recall does not move: the one true start inside a declared extent is
lost and one more is gained in bcrypt, where resuming at the extent's end walks the
scan onto an entry it used to step past.

The three corpora with no exception directory are bit-identical: 120 intel cells,
72 ARM64 ELF, 11 ARM64 Mach-O.

Of the 97 false starts on that corpus only 19 come from the gap scan at all; 70
come from the exception seeding itself, naming record starts that IDA does not call
functions. That is a larger and separate question, and not one this change addresses.
…seeds

The flag's comment claimed funclet and fragment records are filtered at
seeding. Only the packed fragment flag is; the entry-shape filter cannot catch
a separated chunk, because an MSVC-built chunk sets up its own frame and reads
as an entry.

Measured on ping/robocopy/bcrypt: 78 seeded starts land inside a function IDA
labels, 73 of them beginning exactly where another record's extent ends with
nothing referencing them. Microsoft's public PDBs settle which side is right --
they name 88.5% of the addresses IDA calls functions and 6.4% of those 78, a
fourteenfold gap, so the disputed addresses are chunks and this pass
over-reports on such images.

Refusing them by that shape would cost 13 addresses the PDBs do name, so the
comment records the exact test instead: whether the parent's .xdata scope table
points at the address. clang/llvm-mingw emits one record per function symbol and
is unaffected either way.
The first version of this comment said the parent's .xdata scope table names
the chunk, and pointed a future implementation at parsing it. That is only true
for exception funclets.

Measured across the three binaries: of the 145 records that continue another
record's extent and that Microsoft's PDBs do not name, only 25 have a parent
carrying handler data at all. The other 120 continue a record with the X bit
clear, or a packed record with no .xdata whatsoever -- so they are not exception
funclets, and the unwind data holds nothing that distinguishes them.

A scope-table parser would therefore have addressed about a sixth of the
population while reading as the complete answer.
@danielplohmann

Copy link
Copy Markdown
Owner

Merged. #309 and #311 went in first (68f2cfd, db5eff0), and this reduced to its own three commits exactly as you said it would.

The rebase checked out

I verified content-neutrality independently rather than take the tree hashes on trust — reconstructing each branch from the pre-rebase material I reviewed yesterday and comparing:

how I rebuilt it tree matches
#309 cherry-picked the pre-rebase commit onto master, resolved SmdaConfig.py by keeping both blocks 0fb222b yes
#311 cherry-picked the original ten onto master 38b27ba yes
#312 master + #309 + #311's ten + your three 0b2ce4d yes

Byte-identical in all three, so nothing needed re-measuring. The -> bool hazard you flagged is gone too: none of the three touches AArch64CapstoneVerification.py at all now.

The hoist is a move

This was the part I most wanted to confirm, since it edits a path #309 had just landed. declaredExceptionRangeContaining is character-identical between the two files apart from the chainedfragment rename and the added paragraph; bisect was already imported in common for the gap-index lookup, so dropping it from intel is correct and not a latent NameError. Both backends reset their own state and then call super().init(), which clears _pdata_ranges before locateCandidates(), so a reused manager cannot inherit the previous image's extents.

Scored every bundled fixture that can reach either .pdata path, before and after:

21 of 21 bit-identical function sets, including the six x64 PEs the hoist actually moves code under — rust_pe_gnu holds at 2201 with #309's suppression live, cxx_pe_gnu 87, msvc_cxx_pe 7, msvc_cxx_pdb_pe 5, cutwail 33, rust_pe_msvc_i686 4 — plus all 12 ARM64 Mach-O cells and aarch64_static at 278.

The length reconstruction

This is the only genuinely new logic, and a bit-field decode is where I would expect a mask to bleed, so I probed it against lengths computed by hand from the spec rather than through your assertions:

case expected result
packed primary, FunctionLength 0x40 words 0x100 bytes ok
packed fragment (flag 2), 0x10 words 0x40 bytes, is_fragment=True ok
packed, every field above FunctionLength set to 1s (RegF 7, RegI 15, H, CR 3, FrameSize 0x1FF) 0x11 words, unpolluted ok
packed maximum, 0x7FF words (the full 11 bits) 0x1FFC bytes ok
full .xdata, header FunctionLength 0x123 words 0x48C bytes ok
.xdata, CodeWords 31, EpilogCount 31, E, X all set 0x77 words, unpolluted ok
reserved flag 3 declares nothing ok
zero-length packed record declares nothing ok
.xdata with Vers != 0 refused ok

(unwind_data >> 2) & 0x7FF and header_word & 0x3FFFF are the right 11 and 18 bits, and neither picks up a neighbouring field at its boundary.

Dropping the xdata_rva % 4 test is sound, and for the reason your comment gives: flag == 0 is the statement that the low two bits are clear, so the alignment test could never have failed on the one path that reaches it. Single call site, so there is no second caller relying on the old contract.

Nine of the new tests fail against master, so the set is non-vacuous. Full suite here: 1958 passed, 2 skipped, 2593 subtests.

What I could not verify, and it is the headline

There is no ARM64 PE in the bundled fixtures. I scanned all 104 fixture files for a PE machine field: 8 × i386, 4 × AMD64, 1 × 0xFD1D (the ReadyToRun image), and zero 0xAA64. So this PR's suppression path is inert on everything in the repo — which is exactly why all 21 fixtures come back identical, and it is the reassuring half of that number rather than the impressive half. The mechanism is verified; the accuracy claim rests entirely on your corpus, the same position #310 ended up in.

That is not a reason to hold it — the rule is #309's, and #309 measured well on a fixture that does move — but I am recording it so the release notes say "measured on the contributor's ARM64 PE corpus" rather than implying we reproduced it.

Two notes, neither blocking

The seeding caveat is now on the record but still open. Your third commit documents that USE_PE_ARM64_PDATA_CANDIDATES, on by default, over-reports on MSVC-built ARM64 images — 78 seeded starts inside functions IDA labels, only 6.4% of them named by Microsoft's PDBs — and that the .xdata handler-data test you hoped would separate them does not reach 120 of the 145 chunks. I would rather have that written down than quietly true, and the self-correction in the third commit is the useful kind. I am tracking it on our side so it does not decay into a comment nobody revisits; no action wanted from you.

The x64 hook logs its suppression, the ARM64 one does not. intel emits a LOGGER.debug naming the refused address; the AArch64 gap loop has no logging anywhere, so this is consistent with its own file rather than an omission. Mentioning it only because the first ARM64 PE that suppresses something surprising will be harder to explain than the x64 equivalent.

Thanks for flattening all three — and for catching the leaked fork issue references in the commit messages. That is the kind of thing that is invisible until it is permanent.

@danielplohmann
danielplohmann force-pushed the upstream-aarch64-pdata-suppression branch from cae175f to 8e7594b Compare September 8, 2026 10:02
@danielplohmann

Copy link
Copy Markdown
Owner

Correction to the comment above: I posted "merged" a moment too early. #309 and #311 landing as squashes made this CONFLICTING — the same artifact you and I both predicted for this branch, arriving on schedule.

Rather than send it back to you for a third mechanical pass, I took it: force-pushed the rebase to upstream-aarch64-pdata-suppression as 8e7594b, your three commits on current master and nothing else.

before  cae175f  14 commits (#309's one, #311's ten, your three)
after   8e7594b   3 commits (yours)
tree    0b2ce4d  unchanged

The tree is the same 0b2ce4d you pushed and I verified, so nothing in the review above moves. The previous head was cae175f01662a3f3a065abf3f6d534f31cc22a47 if you want it back for any reason.

Merging once CI comes back green.

@danielplohmann
danielplohmann merged commit d111548 into danielplohmann:master Sep 8, 2026
25 checks passed
danielplohmann added a commit that referenced this pull request Sep 8, 2026
Finishes the draft now that #309, #311 and #312 have landed. Adds their
sections, bumps `VERSION` and `__version__` to 4.5.1, and moves the date to the
release day.

The three new entries: #309's exception-directory refusal of interior gap
candidates on x64, with the chained-versus-primary distinction and the
seeding-is-not-suppression principle that keeps carved records out of it; #311's
four candidate-quality defects, including the corrected entry at `0x40df30` on
`aarch64_static_xored` and the metadata-coverage gate; and #312's ARM64
counterpart, which reconstructs the extent an ARM64 `RUNTIME_FUNCTION` does not
carry and hoists the shared lookup into `common/`.

`USE_MACHO_ADDRESS_REF_CANDIDATES` gets its own **Defaults changed** section. It
is the only default whose value moves in this release, it changes recovery
output on Mach-O input with no action by the caller, and a default going from
off to on inside a follow-ups PR is exactly the kind of thing that gets lost.

The closing movement section is rewritten to describe v4.5.0 -> v4.5.1 rather
than any single PR, and both figures in it were measured on this tree against
independent truth rather than quoted:

  rust_pe_gnu_xored, vs its 2,186 retained COFF .text function symbols
    2,355 -> 2,201 starts, +26 real / -180 false
    recall    92.452% -> 93.641%
    precision 85.817% -> 93.003%

  11-sample ARM64 Mach-O corpus, vs LC_FUNCTION_STARTS (2,056 truth)
    TP 1,787 -> 1,818, FP 1,113 -> 1,107
    recall 86.916% -> 88.424%, no sample losing recall

Two headline figures are marked as the contributor's own because they cannot be
reproduced here at all: the `bti j` 803/0 split, since no bundled fixture holds
a single `bti j` word, and #312's ARM64 PE result, since a scan of all 104
fixture files finds 8 i386 PEs, 4 AMD64, one ReadyToRun image and no `0xAA64`.
Saying which figures are ours and which are theirs is more useful than a uniform
tone of confidence.

`Turtle_5f9cd91d8d1d`'s 12 dropped starts are attributed rather than left as an
unexplained delta: they bisect to #307's inbound-call fix and fall inside the
175 false positives that change already measured, and the sample carries no
LC_FUNCTION_STARTS to score them against individually.

Closes #319

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MDCLUmqGzE5fwJqUWsSStr
danielplohmann added a commit that referenced this pull request Sep 8, 2026
* chore: draft the v4.5.1 release notes

The changelog entry for what has landed since v4.5.0 (6cc5aca): #302, #303,
#304, #305, #306, #307, #308, #310 and #316, plus the Dependabot ty bump
572acd7 that #302 had to answer for.

Held as a draft rather than a release. The version is deliberately NOT
bumped here -- #309, #311 and #312 are reviewed and wanted but waiting on a
rebase, and splitting one coherent accuracy pass across two releases makes
both entries weaker. What this commit does is stop the writeup living in a
scratchpad while that happens.

To finish it: add sections for #309, #311 and #312, bump VERSION in
src/smda/SmdaConfig.py and __version__ in src/smda/__init__.py, and move the
date to the actual release day.

No escaper output changed anywhere in this set -- the only edit to
intel/definitions.py adds two GAP_SEQUENCES entries, which feed padding
detection and not escaping, and testEscaperFingerprint passes unchanged --
so ESCAPER_DOWNWARD_COMPATIBILITY stays at 4.4.5 and
INTEL_PIC_HASH_ESCAPE_VERSION at 4.3.5, and no report needs reprocessing.

Figures are the contributors' own except where this session reproduced them,
and the two that were reproduced are stated as measured here: the padding
cut's analysis-time cost (+3.4% claimed, +3.84% measured) and the fixture
movement on rust_pe_gnu_xored (+26 real starts against +8 false, scored
against that fixture's own COFF symbol table rather than the corpus macro
mean).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore: fold #318 into the v4.5.1 release notes

#318 landed as 395c88d after this entry was written. Its five corrections go
under Housekeeping: the README's metadata.language claim, the restored
`-> bool`, the ruff-check hook rename, the GAP_SEQUENCES reach note, and the
docstring on the queue-rebuild test.

Keeping this current as things merge is the point of the branch existing, so
it does not go stale between now and the release.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore: cut v4.5.1

Finishes the draft now that #309, #311 and #312 have landed. Adds their
sections, bumps `VERSION` and `__version__` to 4.5.1, and moves the date to the
release day.

The three new entries: #309's exception-directory refusal of interior gap
candidates on x64, with the chained-versus-primary distinction and the
seeding-is-not-suppression principle that keeps carved records out of it; #311's
four candidate-quality defects, including the corrected entry at `0x40df30` on
`aarch64_static_xored` and the metadata-coverage gate; and #312's ARM64
counterpart, which reconstructs the extent an ARM64 `RUNTIME_FUNCTION` does not
carry and hoists the shared lookup into `common/`.

`USE_MACHO_ADDRESS_REF_CANDIDATES` gets its own **Defaults changed** section. It
is the only default whose value moves in this release, it changes recovery
output on Mach-O input with no action by the caller, and a default going from
off to on inside a follow-ups PR is exactly the kind of thing that gets lost.

The closing movement section is rewritten to describe v4.5.0 -> v4.5.1 rather
than any single PR, and both figures in it were measured on this tree against
independent truth rather than quoted:

  rust_pe_gnu_xored, vs its 2,186 retained COFF .text function symbols
    2,355 -> 2,201 starts, +26 real / -180 false
    recall    92.452% -> 93.641%
    precision 85.817% -> 93.003%

  11-sample ARM64 Mach-O corpus, vs LC_FUNCTION_STARTS (2,056 truth)
    TP 1,787 -> 1,818, FP 1,113 -> 1,107
    recall 86.916% -> 88.424%, no sample losing recall

Two headline figures are marked as the contributor's own because they cannot be
reproduced here at all: the `bti j` 803/0 split, since no bundled fixture holds
a single `bti j` word, and #312's ARM64 PE result, since a scan of all 104
fixture files finds 8 i386 PEs, 4 AMD64, one ReadyToRun image and no `0xAA64`.
Saying which figures are ours and which are theirs is more useful than a uniform
tone of confidence.

`Turtle_5f9cd91d8d1d`'s 12 dropped starts are attributed rather than left as an
unexplained delta: they bisect to #307's inbound-call fix and fall inside the
175 false positives that change already measured, and the sample carries no
LC_FUNCTION_STARTS to score them against individually.

Closes #319

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MDCLUmqGzE5fwJqUWsSStr

* chore: record binweight's serialized type change in the v4.5.1 entry

Left open as a question in yesterday's review and resolved as "yes, it belongs
in the entry". #302 moved `SmdaFunction.binweight`'s class default from int `0`
to float `0.0` while clearing ty 0.0.74's diagnostics, and `binweight` is
serialized through `toDict()`.

The blast radius is small but real: the per-block accumulation already adds
`float(...)`, so every function with at least one block was a float before this
release too. Only a function with no blocks -- a zero-function or error report
-- keeps the class default, and that value now writes as `0.0` where it wrote
`0`. Verified on both trees rather than reasoned about: `SmdaFunction.binweight`
is `0` at 6cc5aca and `0.0` at d111548.

MCRIT stores these reports, so a consumer diffing them byte-for-byte will see
it even though nothing reads the field as an integer. That is exactly the kind
of change that costs someone an afternoon if it is not written down.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MDCLUmqGzE5fwJqUWsSStr

* chore: fold the two remaining #317 items into the v4.5.1 entry

#320 and #321 finish the review pass that #318 started, so the release entry
should carry all seven of its corrections rather than five.

Both are behaviour-neutral typing work, but the Housekeeping text says what each
one actually decided rather than listing them as annotations: `blocks` being
typed forces the PIC/OPC hash path to refuse an instruction with no bytes
instead of blanking it, and `extract_strings` could not be narrowed until three
helpers and three `SmdaReport` attributes were declared first.

Behaviour-neutrality is stated with the evidence rather than as a claim -- the
corpus benchmark ran on #320 and reports 0 of 155 files differing across
175,946 functions, which is a stronger statement than the bundled fixtures can
make.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MDCLUmqGzE5fwJqUWsSStr

* chore: format the v4.5.1 entry as a nested list

@r0ny123 asked on #319 whether the changelog could be formatted as a list to
make it easier to read. At ~2,700 words on a single line, this entry was the one
that provoked the question, so it gets the treatment now rather than waiting for
the wider keep-a-changelog adoption tracked in #323.

Deliberately narrow: only the v4.5.1 entry changes shape. The other 192 entries
keep the one-line format, because converting them is a large, low-value rewrite
that loses nuance in translation, and #323 proposes preserving them verbatim
under an `Older releases` heading instead.

Structure is the release summary as the top-level bullet, one sub-bullet per
section, and a further level inside the three sections that cover more than one
change -- Recovery (intel) splits into the switch-table fix, the alignment cut
and the exception-directory refusal; Recovery (AArch64) into the inbound-call
fix, the BTI work and the candidate-quality follow-ups; Housekeeping into the
`ty` bump, the `ruff` pin, the five #318 corrections, the `binweight`
serialization note and the two #317 typing items.

The text is unchanged. Verified mechanically rather than by eye: stripping list
markers and collapsing whitespace gives a string identical to the original entry.
The two exceptions are disclosed rather than silent -- the first sub-bullet of
Recovery (intel) and of Recovery (AArch64) had their opening letter capitalised,
because splitting the section header onto its own line left them starting a
bullet mid-sentence. Housekeeping's opens on a code span and needed nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MDCLUmqGzE5fwJqUWsSStr

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@r0ny123
r0ny123 deleted the upstream-aarch64-pdata-suppression branch September 9, 2026 10:14
danielplohmann added a commit that referenced this pull request Sep 10, 2026
Three review findings on #300, all in text or in one expression, fixed here rather
than sent back for another round.

The config comments for USE_LSDA_LANDING_PADS and USE_ELF_FDE_INTERIOR_GAPS carried
the per-rule figures measured before #304/#307/#309/#310/#311 landed -- the same
baseline the PR description retracted, because master's own PPV on that corpus had
moved from 76.676 to 91.497 while the branch waited. RESOLVE_TAILCALLS likewise still
described the AArch64 bl fall-through gate as it measured before #307 changed what
addTailcallCandidate does, and omitted the AArch64 ELF corpus entirely, which is the
one row where the gate is a trade. All three now carry the re-measured attribution
against the tree #300 landed on, and the tailcall block says what the 53 lost true
positives are and how USE_ELF_EH_FRAME_CANDIDATES repays them. A description records
a conversation; a config comment is what the next reader has.

getExceptionDirectory matched the data directory with "EXCEPTION" not in
str(directory.type), a substring of the enum's repr that would also claim any future
type whose name contains it. The typed lookup is already the idiom in two other
places, one of them the AArch64 walk over the same directory.

The aarch64_static baseline moved by two starts and the test comment explained one
of them. 0x40DF34 is refused, but it was not recovered before either, so the pair the
fixture actually lost is 0x400350 and 0x40DF30 -- both mid-function instructions
inside a declared FDE, which is a plainer reading than the Binary Ninja disagreement
the comment led with. 0x400350 is now asserted alongside the other two.

Also removes a comment left dangling in intel/FunctionCandidateManager.__init__ by
#312, which hoisted the attribute it documented into common/ where the same text
already sits.

Suite 2060 passed, 1 skipped, 2593 subtests; ruff clean; ty exit 0 with 0
error-level diagnostics.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
danielplohmann pushed a commit that referenced this pull request Sep 11, 2026
…ne (#333)

#312's interior-gap rule is driven end to end only over an image the
suite synthesises. That pins the extent decode and the refusal, but not
that the rule meets the shapes a compiler emits, and until now there was
no ARM64 PE among the bundled samples at all: scanning them for a PE
machine field finds eight i386, four AMD64 and one ReadyToRun image, and
no 0xAA64.

The fixture is built from source rather than taken from a system, so it
can be redistributed and rebuilt:

    aarch64-w64-mingw32-clang++ -O2 -g -gdwarf-4 probe.cpp -o probe.exe

with llvm-mingw 20250430. The source exercises catch funclets, cleanup
funclets and a [[noreturn]] cold path, the shapes that get their own
unwind record.

That also makes it a control. clang emits one .pdata record per named
function and splits none of them, so the over-seeding an MSVC-built ARM64
PE shows does not occur here; anything refused is refused on the extent
evidence rather than on a compiler's chunking habit. Eight addresses move,
each interior to a RUNTIME_FUNCTION extent whose own function the analysis
recovered, and they are listed rather than counted so a change that
refuses a different set fails instead of staying green on a total.

XORed like every other bundled sample. 200K, against 1.1M for the largest
already carried.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants