Skip to content

Fix x64 disassembler chase for MOV reg, imm64 + CALL/JMP [reg] - #3210

Merged
timcassell merged 1 commit into
dotnet:masterfrom
timcassell:fix-x64-mov-imm64-call-slot
Jul 27, 2026
Merged

Fix x64 disassembler chase for MOV reg, imm64 + CALL/JMP [reg]#3210
timcassell merged 1 commit into
dotnet:masterfrom
timcassell:fix-x64-mov-imm64-call-slot

Conversation

@timcassell

Copy link
Copy Markdown
Collaborator

The x64 JIT sometimes emits an indirect call as mov reg, imm64 ; call qword ptr [reg] instead of the shorter RIP-relative call qword ptr [rip+disp32] — typically when the target slot is out of ±2 GB range from the call site. The RIP-relative form is handled by IntelDisassembler today; the 2-instruction form is not, because TryGetReferencedAddress only inspects the current instruction (the CALL) and its memory displacement is 0 ([reg] with no disp).

Symptom

On the InProcessEmit toolchain, when the auto-generated __ForDisassemblyDiagnoser__ wrapper's call site landed on the 2-instruction form, DisassemblyDiagnoser recovered only the wrapper and never followed the call to the benchmark method. Surfaces as a rare flake in CanDisassembleInlinableBenchmarks(RyuJit, X64, InProcessEmit) in the full test suite context on Windows x64 with .NET 10 — memory fragmentation from the ~20k tests running first makes the wider-encoding call site more likely.

Fix

Add a per-Decode Dictionary<Register, ulong> that tracks MOV reg, imm64 loads and gets invalidated on any subsequent write to the same register. When a CALL/JMP [reg] (or [reg+disp]) is seen and the base register has a tracked value, use regValue + disp as the referenced slot address and let the existing indirect-resolution path handle the pointer dereference and precode chase.

Verification

Reproduced the flake on an AMD EPYC 9V74 windows-latest runner (~1-in-15 dispatch attempts pre-fix) with disassembler diagnostic instrumentation confirming the wrapper contained the mov rax, imm64 ; call qword ptr [rax] pattern and the disassembler was skipping resolution. Post-fix, the same repro passes cleanly on the same CPU class: JustReturn appears as its own entry in the disassembly results, matching the .NET 8 and RIP-relative-form behavior. Run: https://github.com/timcassell/BenchmarkDotNet/actions/runs/30260019118

Relation to #3208

Same theme (make the disassembler chase .NET 10-era indirect-call patterns) but different architecture. #3208 taught the ARM64 disassembler about the new DMB ISH-prefixed FixupPrecodeCode_Fixup shape; this teaches the x64 disassembler about the MOV reg, imm64 ; CALL [reg] form the JIT falls back to for out-of-range call sites. Both fixes are independent and orthogonal.

The x64 JIT sometimes emits an indirect call as `mov reg, imm64 ; call qword ptr [reg]` instead of the shorter RIP-relative `call qword ptr [rip+disp32]` — typically when the target slot is out of ±2 GB range from the call site. The RIP-relative form is handled by IntelDisassembler today; the 2-instruction form is not, because TryGetReferencedAddress only inspects the current instruction (the CALL) and its memory displacement is `0` (`[reg]` with no disp).

Symptom: on the InProcessEmit toolchain, when the auto-generated `__ForDisassemblyDiagnoser__` wrapper's call site landed on the 2-instruction form, DisassemblyDiagnoser recovered only the wrapper and never followed the call to the benchmark method. Surfaces as a rare flake in `CanDisassembleInlinableBenchmarks(RyuJit, X64, InProcessEmit)` in the full test suite context (memory fragmentation makes the wider-encoding call site more likely).

Add a per-Decode `Dictionary<Register, ulong>` that tracks `MOV reg, imm64` loads and gets invalidated on any subsequent write to the same register. When a `CALL/JMP [reg]` (or `[reg+disp]`) is seen and the base register has a tracked value, use `regValue + disp` as the referenced slot address and let the existing indirect-resolution path handle the pointer dereference and precode chase.

Verified by reproducing the flake on AMD EPYC 9V74 Windows-latest runner (~1-in-15 dispatch attempts pre-fix) and observing that the same repro passes cleanly with the fix: JustReturn appears as its own entry in the disassembly results, matching the .NET 8 and RIP-relative-form behavior.
@timcassell
timcassell merged commit 17f2185 into dotnet:master Jul 27, 2026
21 checks passed
@timcassell
timcassell deleted the fix-x64-mov-imm64-call-slot branch July 27, 2026 21:37
@timcassell timcassell added this to the v0.16.0 milestone Jul 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant