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

Summary

In x86/64 architecture, the MOV to/from control/debug registers (0F 20 /r, 0F 22 /r) treat the ModR/M byte as purely register-to-register, regardless of the mod field. The mod field (bits 7-6) is ignored (effectively treated as mod=11), and no displacement bytes follow the ModR/M byte. Any attempt to encode a displacement (e.g., mod=01 or mod=10) results in undefined behavior—the CPU may treat the displacement bytes as part of the next instruction or simply ignore them, but they are not valid in standard disassembly.

Root Cause

The root cause is a historical design choice in the x86 instruction set:

  • Control/Debug registers (CR0-CR8, DR0-DR7) are system registers accessible only in privileged modes (Ring 0).
  • The instruction set does not support memory operands for these registers—only direct register-to-register moves.
  • The ModR/M byte is parsed as mod=11 (register addressing) regardless of the encoded mod field.
  • The disp8/disp32 fields are not read by the CPU in this context—they are skipped entirely.

Why This Happens in Real Systems

  1. Privilege Restriction: Moving data to/from control/debug registers requires Ring 0 (kernel-mode) privileges. Attempting this in user mode raises a #UD (Undefined Opcode) or #GP (General Protection Fault).
  2. No Memory Access: The CPU does not compute an effective address for these instructions. The mod field is ignored, and any displacement bytes are treated as part of the instruction stream (e.g., as the next instruction’s opcode).
  3. Disassembler Ambiguity: Some disassemblers (like NDISASM) fail to validate the mod field and may misinterpret the instruction, leading to incorrect decoding.

Real-World Impact

  • Debugging Failures: Tools like OllyDbg/x64dbg cannot execute these instructions outside kernel mode, making analysis difficult.
  • Incorrect Disassembly: Misinterpretation of mod values leads to wrong instruction boundaries (e.g., treating displacement bytes as opcode bytes).
  • Exploit Potential: Malformed instructions with non-mod=11 encodings could crash the CPU or execute unintended code.
  • Compiler Bugs: Compilers may generate invalid encodings if they don’t account for this rule, leading to silent failures in kernel code.

Example or Code

Valid encoding (mod=11):

MOV CR2, EBX  ; 0F 20 D3

Invalid encoding (mod=01 with disp8):

MOV CR2, [EBX+0xFF]  ; 0F 20 53 FF (INVALID)

What the CPU actually executes for 0F 20 53 FF:

  • Reads 0F 20 53 as MOV CR2, EBX (ignoring the mod field).
  • Treats 0xFF as the start of the next instruction (e.g., MOV AL, 0xFF).

How Senior Engineers Fix It

  1. Manual Encoding: Always OR the ModR/M byte with 0xC0 (mod=11) to force register-only addressing.
  2. Disassembler Validation: Implement a strict check for mod field—if it’s not 0b11, treat it as invalid or disassemble as raw bytes.
  3. CPU Documentation: Rely on Intel/AMD manuals (Vol. 2, Section 3.2) which explicitly state that only mod=11 is valid for these instructions.
  4. Kernel Debugging: Use hardware breakpoints or virtualized environments to test privileged instructions safely.

Why Juniors Miss It

  • Assumption of Uniformity: Juniors assume ModR/M always follows the same rules for all instructions.
  • Incomplete Testing: Testing only Ring 0 scenarios (where the CPU relaxes checks) vs. Ring 3 (where it faults).
  • Over-Reliance on Tools: Tools like NDISASM or GDB may hide the issue by silently decoding invalid encodings.
  • Lack of Deep Dive: Not reading Intel SDM or AMD APM—relying instead on blog posts or incomplete guides.