Skip to content

Ensure that x64 disassembly names operands at the size actually encoded - #131428

Open
tannergooding wants to merge 4 commits into
dotnet:mainfrom
tannergooding:tannergooding-corelib-pmi-codegen-analysis
Open

Ensure that x64 disassembly names operands at the size actually encoded#131428
tannergooding wants to merge 4 commits into
dotnet:mainfrom
tannergooding:tannergooding-corelib-pmi-codegen-analysis

Conversation

@tannergooding

Copy link
Copy Markdown
Member

The x64 disassembler prints some operands at a size that doesn't match the bytes it just encoded. This makes the printed text actively misleading -- movzx rax, al and xor rax, rax both suggest a 64-bit write where the encoder deliberately emitted a 32-bit one, and a dropped lane selector makes four different vextracti32x4 instructions render identically.

Everything here is display-only, confined to emitDispIns and its helpers. Codegen is unchanged.


movzx printed a 64-bit destination. emitOutputRR only adds REX.W when (size == EA_8BYTE) || (ins == INS_movsx), and for movzx that size is the source width, so REX.W is never set; TakesRexWPrefix excludes it as well. movsx printing 64-bit is correct -- all 5,263 in CoreLib genuinely carry REX.W -- so the two are split rather than sharing a branch. 4 sites (ARD/SRD/RRD/MRD).

GC-typed self-zeroing xor printed 64-bit. emitOutputRR drops REX.W for xor reg, reg and records id->idOpSize(EA_4BYTE), but emitDispIns derived attr from idGCref() first and so kept the pointer-sized name. Smoking gun in a single method: 4533C0 xor r8, r8 next to 4533FF xor r15d, r15d.

v{extract,insert}{f,i}32x4 swallowed the imm8 lane selector. emitDispConstant early-returns for every INS_FLAGS_HasPseudoName instruction. That's right for vcmp*/vpcmp*/pclmulqdq, where the immediate is the name, but these four are only pseudo-named for the VEX-era alias and still need the operand. Proof of loss: CoreLib uses selectors 00/01/02/03 and all four rendered identically.

movmskps/movmskpd printed a 64-bit GPR. REX_WIG is #defined as REX_W0, so TakesRexWPrefix returns false and the JIT never emits REX.W for these -- the destination is always r32. pmovmskb already had the case; the other two were missing.

cvtsi2*/vcvtusi2*/vmovd named their GPR operand from the vector size. Now taken from GetInputSizeInBytes, which reads the Input_32Bit/Input_64Bit flag. Definitive display-only proof: the identical bytes C4C17A2AC0 printed r8d in one method and r8 in another.

Narrowing converts named the destination at the source size in the memory forms. Only the register form narrowed; the memory paths printed emitRegName(idReg1(), attr) raw, giving vcvtpd2ps ymm0, ymmword ptr [...]. Extracted GetNarrowedDstAttr and shared it across the register and ARD/SRD/MRD paths.

Broadcasts printed the source at the destination width. Only the _gpr variants had cases; the vector-source ones fell to default: where srcAttr = attr. These are architecturally ymm1/zmm1, xmm2 -- the source is the low element. C4E27D19C1 is VEX.256.66.0F38.W0 19 /r = VBROADCASTSD ymm1, xmm2, but printed ymm1, ymm2.


Verification. Byte-accurate PMI corpus of System.Private.CoreLib (--nodiffable with DOTNET_JitDisasmWithCodeBytes=1), decoding the encoding prefixes mechanically and comparing encoded vs printed width for every instruction. Base and diff JITs built from the same source, differing only by this change:

base this PR
movzx with 64-bit dst 19,815 0
self-zeroing xor printed 64-bit 13,256 0
v?movmskp[sd] with 64-bit dst 211 0
broadcast printed ymm<-ymm/zmm<-zmm 89 0
vextract/vinsert*32x4 showing the selector 0 52

1,134,687 instructions checked, 0 remaining width mismatches. Codegen is unchanged: 75,012 methods compared by (mnemonic, encoded length), and the only 4 differences are P/Invoke inline-frame calls whose helper target moves in and out of rel32 range with ASLR -- confirmed by a control run of the same JIT twice reproducing the same set.

Broadcasts needed a targeted repro since CoreLib never emits the register form of vpbroadcastb/w/d/q or vbroadcast{f,i}32x2 at all. All eight flip correctly with identical encoded bytes:

before                                  after
C4E27D78C0    vpbroadcastb ymm0, ymm0   vpbroadcastb ymm0, xmm0
C4E27D79C0    vpbroadcastw ymm0, ymm0   vpbroadcastw ymm0, xmm0
C4E27D58C0    vpbroadcastd ymm0, ymm0   vpbroadcastd ymm0, xmm0
C4E27D59C0    vpbroadcastq ymm0, ymm0   vpbroadcastq ymm0, xmm0
C4E27D18C0    vbroadcastss ymm0, ymm0   vbroadcastss ymm0, xmm0
C4E27D19C0    vbroadcastsd ymm0, ymm0   vbroadcastsd ymm0, xmm0
62F27D4858C0  vpbroadcastd zmm0, zmm0   vpbroadcastd zmm0, xmm0
62F2FD4819C0  vbroadcastsd zmm0, zmm0   vbroadcastsd zmm0, xmm0

vbroadcastss xmm, xmm (236 in CoreLib) is correct and unchanged, as are the memory-source-only vbroadcast{f,i}32x4/64x2/32x8/64x4, which have no register form.


On coverage. GetNarrowedDstAttr is inherently a hand-maintained list: metadata alone can't separate a narrowing INS_TT_FULL convert from a same-size op, since cvtpd2ps (Input_64Bit, KMask_Base2) and vaddps (Input_32Bit, KMask_Base4) both compute 8-vs-8 and 4-vs-4. So I audited it statically, decoding element sizes from the mnemonics rather than from what CoreLib happens to emit: 13 /2 entries and 3 /4 entries, no missing narrowing instructions and no dead entries. The widening direction needs no list -- INS_TT_HALF/QUARTER_MEM/EIGHTH_MEM already handle both directions generically.

Known residual, not addressed here. emitDispIns is also called from emitDispIG during JitDump, before output, where the xor narrowing hasn't happened yet; that path still prints the wide name. It degrades gracefully rather than misprinting -- EA_SIZE(EA_GCREF) == idOpSize() there, so the new condition is simply false -- but the pre/post-encode difference is inherent to that call site.

CC. @dotnet/jit-contrib

Note

This PR was authored with GitHub Copilot.

tannergooding and others added 4 commits July 27, 2026 09:32
`movzx` never encodes REX.W, and `emitOutputRR` drops it for `xor reg, reg`,
but the display forced a pointer-sized destination in both cases -- the latter
because a GC type overrode the narrowed operand size. This affected 33,035
instructions in a PMI run over System.Private.CoreLib.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…width

`v{extract,insert}{f,i}32x4` were caught by the pseudo-name immediate
suppression meant for `cmp*`/`vpcmp*`/`pclmulqdq`, which encode the immediate in
the mnemonic. Their pseudo-name is only the VEX-era alias, so the lane selector
was dropped -- `vextracti32x4` rendered all four lanes identically.

`movmskps`/`movmskpd` named the destination from the vector operand size, giving
`rcx` for an encoding that is `REX_WIG` and always writes 32 bits. `pmovmskb`
already handled this.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The narrowing converts only adjusted the destination size in the register form,
so the memory forms named it at the source size instead. The scalar integer
converts and `vmovd` likewise named their general purpose operand from the
vector size.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The vector-source broadcasts fell to the default case, which names both operands at the destination size. The source is architecturally always xmm.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 27, 2026 17:22
@github-actions github-actions Bot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Jul 27, 2026
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 5 pipeline(s).
11 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

@tannergooding

Copy link
Copy Markdown
Member Author

This is just a fix to the disasm output, fixing some more of the edge cases that caused confusing or incorrect diffs. No changes to the actual codegen emitted.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the xarch disassembly output formatting in emitDispIns / emitDispConstant so operand register names (and certain immediates) reflect the size actually encoded in the instruction bytes, avoiding misleading 64-bit register names and missing lane selectors in printed disassembly.

Changes:

  • Teach emitDispConstant to still print the imm8 lane selector for {vextract,vinsert}{f,i}32x4 despite these instructions using pseudo-names.
  • Adjust operand-size selection in emitDispIns so specific instructions display the correct GPR/vector operand width (e.g., movzx destination, movmskps/movmskpd destination, broadcast register-source forms).
  • Introduce GetNarrowedDstAttr and apply it in the relevant display paths to correctly show narrowing-conversion destination sizes (including memory forms).
Show a summary per file
File Description
src/coreclr/jit/emitxarch.cpp Disassembly-only fixes in emitDispIns/helpers to print operand widths and immediates consistent with the encoded instruction.

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants