Fix x64 disassembler chase for MOV reg, imm64 + CALL/JMP [reg] - #3210
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The x64 JIT sometimes emits an indirect call as
mov reg, imm64 ; call qword ptr [reg]instead of the shorter RIP-relativecall 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 byIntelDisassemblertoday; the 2-instruction form is not, becauseTryGetReferencedAddressonly inspects the current instruction (the CALL) and its memory displacement is0([reg]with no disp).Symptom
On the InProcessEmit toolchain, when the auto-generated
__ForDisassemblyDiagnoser__wrapper's call site landed on the 2-instruction form,DisassemblyDiagnoserrecovered only the wrapper and never followed the call to the benchmark method. Surfaces as a rare flake inCanDisassembleInlinableBenchmarks(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-
DecodeDictionary<Register, ulong>that tracksMOV reg, imm64loads and gets invalidated on any subsequent write to the same register. When aCALL/JMP [reg](or[reg+disp]) is seen and the base register has a tracked value, useregValue + dispas 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 9V74windows-latestrunner (~1-in-15 dispatch attempts pre-fix) with disassembler diagnostic instrumentation confirming the wrapper contained themov 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:JustReturnappears 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/30260019118Relation 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 theMOV reg, imm64 ; CALL [reg]form the JIT falls back to for out-of-range call sites. Both fixes are independent and orthogonal.