Summary
This postmortem investigates the behavior of x86 MOV instructions to and from control registers (CR0-CR7) when the ModR/M byte’s mod field is not 11 (register-to-register). The instruction in question is 0F 20 /r (MOV r32, CRn) and 0F 22 /r (MOV CRn, r32). The investigation concludes that encoding with mod < 3 is architecturally invalid for these instructions. While the instruction 0F 20 53 FF decodes syntactically by the CPU’s instruction decoder, it triggers a General Protection Fault (#GP) upon execution because the effective address calculation is invalid for this operation. Disassemblers like NDISASM failing to show this is a tooling flaw; the hardware strictly enforces the mod=3 rule.
Root Cause
The root cause is a combination of architectural definition constraints and hardware execution logic.
- Architectural Constraint: The Intel SDM explicitly defines that the
modfield of the ModR/M byte for these specific opcodes must be11(register addressing). Any other value is reserved. - Execution Pipeline: When the CPU decodes
0F 20 53 FF:- The decoder reads
0x53as the ModR/M byte. mod=01indicates an 8-bit displacement follows (0xFF).- The CPU calculates the effective address
[EBX + 0xFF]. - The Execution Unit attempts to move data between the Control Register (CR2) and the memory address
[EBX + 0xFF]. - Because Control Registers cannot be accessed via memory operands (only registers), the execution traps immediately.
- The decoder reads
Why This Happens in Real Systems
This behavior stems from the Privilege Level (Ring) protections inherent to x86.
- Ring 0 Protection: Control registers are critical system resources. Accessing them modifies memory management, paging, and CPU behavior.
- Restricted Access: To prevent user-mode applications (Ring 3) from crashing the system, Intel restricted
MOVto/fromCRnto Ring 0 only. - Invalid Opcode vs. #GP:
- If the CPU ignored the
modfield and treated0F 20 53 FFasMOV CR2, EBX, it would be bypassing the privilege check required for memory access. - Instead, the CPU attempts to perform the memory access. Since memory accesses by Ring 3 code are allowed (unless the page is protected), the invalid part is the operation itself. The instruction fails with a General Protection Fault (#GP), not an Invalid Opcode (#UD).
- If the CPU ignored the
Real-World Impact
- System Stability: Attempting to execute these encodings in kernel mode results in a Blue Screen of Death (BSOD) or kernel panic, as the CPU cannot resolve the instruction’s intent.
- Security Implications: If a kernel exploited allowed writing to a control register via memory (e.g., if the CPU hardware didn’t enforce
mod=3), it could potentially bypass kernel protections or Spectre mitigations. - Debugging Difficulty: As noted in the prompt, user-mode debuggers (OllyDbg, x64dbg) cannot execute these instructions to test them. They will always crash or refuse to step over them.
- Disassembly Confusion: Tools like NDISASM that treat
0F 20 53 FFas valid data or partially decoded instructions create false positives for security researchers or reverse engineers analyzing binary blobs.
Example or Code
; Valid Encoding (Mod = 11)
; 0F 20 D0 => MOV EAX, CR0
; ModR/M: 11 010 000
; Invalid Encoding (Mod = 01)
; 0F 20 53 FF => MOV CR2, [EBX + disp8]
; ModR/M: 01 010 011 (RBX)
; Disp8: 0xFF
; The CPU decodes this as:
; 1. Read CR2 (Privileged)
; 2. Write to memory address [EBX + 0xFF] (User-accessible memory)
; 3. This is a violation of architectural constraints and causes #GP.
How Senior Engineers Fix It
Senior engineers approach this by enforcing strict validation layers and understanding hardware intent.
- Strict Disassemblers: Update disassembler logic to flag
mod != 11for0F 20/0F 22as Invalid / Reserved, rather than outputting raw bytes or guessing. - Micro-architectural Simulation: Do not rely on user-mode execution to verify instruction behavior. Use hardware manuals and architectural simulators (like Intel XED or Capstone with analysis enabled) to validate instruction streams.
- Encoder Guards: In custom assemblers or JIT compilers, explicitly check the ModR/M byte. If
mod != 3for these opcodes, throw an assembly error immediately. - Emulation Handling: If writing an emulator (like QEMU), the fix is to implement a check:
if (opcode == 0x0F20 && mod != 3) { raise_exception(GP_FAULT); }
Why Juniors Miss It
Junior engineers often fall into the trap of “syntactic decoding” rather than “architectural decoding”.
- Ignoring Mod Field Constraints: They see “Move Register to Register” logic and assume the
modbit just toggles between register and memory operands, not realizing some instructions are Register-Only. - Reliance on User-Mode Tools: The prompt’s author tried to run it in a debugger. Since it crashed, they assumed it was a privilege issue to be bypassed, rather than a fundamental encoding error.
- Trust in Flawed Tools: Seeing NDISASM output
db 0x0for similar misleading data validates their confusion. They assume the tool is right and the CPU manual is wrong. - Misunderstanding the “Ignore” Rule: The manual says the
modbits are “ignored” by the instruction logic (meaning the instruction doesn’t use memory addressing), but the hardware decoder still checks them for validity. Juniors interpret “ignored” as “can be set to anything.”