Does Move to/from Control Registers ignore the field mod?

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 mod field of the ModR/M byte for these specific opcodes must be 11 (register addressing). Any other value is reserved.
  • Execution Pipeline: When the CPU decodes 0F 20 53 FF:
    1. The decoder reads 0x53 as the ModR/M byte.
    2. mod=01 indicates an 8-bit displacement follows (0xFF).
    3. The CPU calculates the effective address [EBX + 0xFF].
    4. The Execution Unit attempts to move data between the Control Register (CR2) and the memory address [EBX + 0xFF].
    5. Because Control Registers cannot be accessed via memory operands (only registers), the execution traps immediately.

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 MOV to/from CRn to Ring 0 only.
  • Invalid Opcode vs. #GP:
    • If the CPU ignored the mod field and treated 0F 20 53 FF as MOV 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).

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 FF as 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 != 11 for 0F 20/0F 22 as 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 != 3 for 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 mod bit 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 0x0f or 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 mod bits 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.”