Skip to content

chore: Add unit tests for arm64 disassembler - #3245

Open
filzrev wants to merge 7 commits into
dotnet:masterfrom
filzrev:chore-add-arm64-unittests
Open

chore: Add unit tests for arm64 disassembler#3245
filzrev wants to merge 7 commits into
dotnet:masterfrom
filzrev:chore-add-arm64-unittests

Conversation

@filzrev

@filzrev filzrev commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

This PR contains following changes.

1. Cleanup arm64 disassembler related code to preparing to add unit tests

See following PR comment for details.

2. Add AsmArm64 package reference

AsmArm64 package to unit test project.
Currently it's used for test purpose.
It's expected existing arm64 disassembler is replaced to AsmArm64 based implementation. (#3246)

3. Add arm64 disassembler related unit tests.

To ensure existing arm64 disassembler behavior.
Unit test codes are added for major code paths. (It can confirm code coverage results with Analyze Code Coverage on VS)

Note:
Almost of unit tests on .NET Framework are excluded by #if NET directive.

  • private methods are tested with UnsafeAccessor (It requires .NET 8 or later)
  • Capstone's native dependencies seems not copied to bin directory when using xUnit v2 (Because it's Library project)

@timcassell

Copy link
Copy Markdown
Collaborator
AI review

Accumulator bugs

Arm64RegisterValueAccumulator.cs:64ExpectingAdd never resets. Unlike ExpectingMovk, this case has no fall-through to LookingForPossibleLdr, so any instruction that isn't the expected ADD
leaves the state machine parked with the stale ADRP value. adrp x0,#0x1000 ; movz x0,#0x100 ; add x0,x0,#0x100 gives HasValue == true, Value == 0x1100 after x0 was clobbered, so a following BR/BLR x0
resolves to a bogus address and the disassembly prints the wrong symbol. The PR's own skipped AdrpThenOther_ThenAdd_ShouldResetValue fails on this today.

Arm64RegisterValueAccumulator.cs:68ADD ignores lsl #12. The match checks only Operands[2].Type == Immediate, then does _value | Immediate. adrp x0,#0x1000000 ; add x0,x0,#0xfff, lsl #12
yields page | 0xFFF instead of page + 0xFFF000, with HasValue still true — a silently wrong address rather than a bail-out. At minimum, reject a shifted immediate. (Skipped
AdrpThenAdd_WithShiftedImm_ShouldCalculateAddress covers it.)

Arm64RegisterValueAccumulator.cs:41MOVZ discards the shift. Same class: _value = details.Operands[1].Immediate drops the lsl amount, so movz x0,#0x1234, lsl #16 seeds 0x1234 instead of
0x12340000.

Arm64RegisterValueAccumulatorTests.Movz.cs:31 — skipped test asserts MOVK semantics. Movz_WithShiftedImmediateValue_ShouldStartNewValue expects 0x3333_2222_1111 from three MOVZs. A real MOVZ
zeroes the rest of the register, so the architectural result is 0x3333_0000_0000. Whoever un-skips this and "fixes" the accumulator to satisfy it will encode a decoding bug.

Tests that can't fail

Arm64DisassemblerTests.TryFollowJumpTrampoline.cs:73 — slot displacement untested. In all four stub tests the getPointer callback returns ExpectedResultAddress for any address, unlike the
TryResolvePrecode tests which assert the requested slot. Changing parseBase + (ulong)(long)off0 to parseBase + 4 + (ulong)(long)off0 in the StubPrecode branch leaves the whole suite green. Assert the
address inside getPointer, as TryResolvePrecode_* does.

Arm64DisassemblerTests.TryFollowJumpTrampoline.cs:178NonStubHead bails on length, not shape. The test supplies one instruction, so the reader returns 4 bytes and TryReadStubHead bails at read < 12; the stub-shape rejection it names is never reached, and the test passes with all stub matching deleted. Pad to >= 4 non-stub instructions.

Arm64InstructionFormatterTests.cs:19 — padding disabled. The comment says "Use DisassemblyDiagnoserConfig default config value" but FirstOperandCharIndex = 10 is commented out, so the theory runs at
Iced's default of 0 and asserts strings BDN never emits ("b #8" vs. production "b #8"DisassemblyDiagnoserConfig.cs:86). The shipped column width is only incidentally covered by the one
FirstOperandCharIndex = 6 case.

Arm64InstructionFormatterTests.cs:61 — empty symbols map. FormatInstruction_B_WithReferencedAddress passes no symbols, so TryGetValue always misses and the one thing gated on ReferencedAddress
— the Operand.Replace($"#0x{addr:x}", name) substitution — is never run. Add an entry mapping 0x10000 to a name.

Arm64DisassemblerTests.TryGetReferencedAddress.cs:11 — helper skips Init(runtime). _runtime stays null; it works only because no test here feeds an LDR. The first one that does will NRE inside
Feed. The sibling helper (Arm64RegisterValueAccumulatorTests.cs:15) does call Init.

Arm64DisassemblerTests.TryGetReferencedAddress.cs:23_With_BL builds BR X0. Duplicate of _With_BLR; BL is never covered.

Minor

Arm64Disassembler.cs:145 — constant is ISHLD, not ISH. 0xD50339BF has CRm = 0b1001 (DMB ISHLD); DMB ISH is 0xD5033BBF. The new tests construct Arm64BarrierOperationLimitKind.ISHLD,
confirming it. As written a stub prefixed with a plain DMB ISH isn't recognised and precode resolution silently fails — fix the comment/name or widen the match.

ClrMdDisassembler.cs:114IClrRuntime switch introduced unchecked downcasts. foreach (ClrModule module in state.Runtime.EnumerateModules()) and the nested foreach (ClrType type in ...) went from
statically-typed iteration to runtime casts, since both interface members are explicit implementations returning IClrModule/IClrType. Nothing breaks against ClrMD 4.0.732401 (the concrete instances
still come back), but FilterAndEnqueue now throws InvalidCastException for any other IClrRuntime — including the MockClrRuntime this PR adds, which makes that path untestable by the harness being
introduced.

Helpers/Arm64TestInstructions.cs:180ValidateMultipleOf8 throws "...must be multiple of 4".

It looks like it found some possible bugs in the accumulator (I did not verify myself). Fine if you want to fix them here, or defer for out-of-scope.

@filzrev
filzrev force-pushed the chore-add-arm64-unittests branch from 7aa8d2b to 52dc08c Compare September 6, 2026 23:23
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