VMCS and VMCB (Virtual Machine Control Structure)

Hardware-assisted virtualization needs one canonical, in-memory place to hold the entire processor worldview of a guest — every control register, segment register, the instruction pointer, the flags, the system descriptors — so that the CPU can swap from running the hypervisor to running the guest and back, atomically, without the hypervisor manually shuffling registers each time. On Intel this place is the VMCS (Virtual-Machine Control Structure); on AMD it is the VMCB (Virtual-Machine Control Block). Both are 4 KiB physical pages, but they embody two different design philosophies: Intel’s VMCS is opaque — software may touch it only through the VMREAD and VMWRITE instructions, never by ordinary memory load/store, because the CPU is free to cache parts of it on-chip in an implementation-defined format (Intel SDM Vol 3C). AMD’s VMCB is a plain, documented struct that the hypervisor reads and writes with normal mov instructions, its layout fixed by offset in the AMD architecture manual (AMD SVM Arch. Ref. Manual 33047). This note explains what lives inside each structure, why it must exist, how Linux’s KVM models it, and where the two vendors diverge. (Code and constants are pinned to Linux 6.12 LTS, x86, released 2024-11-17; the architectural facts come from the Intel SDM Vol 3C and AMD APM Vol 2 / the original SVM reference manual.)

The structure pairs with the entry/exit machinery: this note is the data, VM Entry and VM Exit Mechanics is the verb that consumes it, and Intel VMX and AMD SVM is the mode that the data describes. Read them together.

Mental Model

Think of the VMCS/VMCB as a save-game file for a whole CPU. When the hypervisor decides to run a guest, it does not push/pop dozens of registers by hand; instead it points the CPU at this 4 KiB page and issues a single instruction (VMLAUNCH/VMRESUME on Intel, VMRUN on AMD). The hardware then atomically spills the hypervisor’s CPU state into the structure’s host area, loads the guest’s CPU state from the structure’s guest area, and begins executing guest code. On the way back — a VM exit — the hardware does the reverse and stamps a reason code into the structure so the hypervisor knows why it regained control. The page therefore has four conceptual zones: guest state (loaded on entry, saved on exit), host state (loaded on exit), controls (the hypervisor’s policy: which guest instructions and events to trap), and exit information (filled by hardware, read by the hypervisor).

flowchart LR
  subgraph VMCS["VMCS / VMCB — one 4 KiB page per vCPU"]
    G["Guest-state area<br/>(CR0/3/4, RIP, RFLAGS,<br/>segments, system regs)"]
    H["Host-state area<br/>(where to return on exit)"]
    C["Control area<br/>(exec / entry / exit controls,<br/>intercept bitmaps, EPT/NPT ptr)"]
    X["Exit-information fields<br/>(reason, qualification,<br/>EXITINFO1/2)"]
  end
  HV["Hypervisor (KVM)"]
  CPU["CPU on VM entry / VM exit"]
  HV -->|"VMWRITE / mov: set controls + guest state"| C
  HV -->|"VMWRITE / mov"| G
  CPU -->|"entry: load guest, save host"| G
  CPU -->|"exit: save guest, load host, write reason"| X
  HV -->|"VMREAD / mov: read reason"| X

The four zones of the control structure. What it shows: the hypervisor populates the control and guest-state zones before entry; the CPU shuttles state between guest and host zones on each entry/exit and writes the exit reason into the exit-information zone; the hypervisor reads that reason to decide what to do. The insight to take: the structure exists so that one instruction can swap an entire CPU’s architectural state in one shot — the alternative (the hypervisor manually saving/restoring ~30 registers, MSRs, and descriptor tables on every transition) would be both slow and racy.

Why the Structure Must Exist

Before hardware virtualization, swapping CPU “worldviews” was the hypervisor’s manual job, and it was the central source of both complexity and bugs (see Why x86 Needed Hardware Virtualization and Shadow Page Tables). The processor running the guest must believe it owns ring 0, has its own CR3 page-table root, its own IDTR/GDTR, its own RFLAGS. The processor running the hypervisor needs its values back the instant the guest traps. If the hypervisor did this in software, every transition would be tens of loads and stores, and — worse — there would be a window where the CPU’s state is half guest, half host, during which any interrupt or fault would be catastrophic.

The VMCS/VMCB solves this by making the state swap a single, architecturally atomic hardware operation. The hypervisor writes the guest’s intended state and its own return state into the page once; thereafter each entry/exit is one instruction. This is the foundational data structure of VMX and SVM — without it, the new processor modes (Root Mode and Non-Root Mode) would have nothing to transition through.

A second reason it exists: it is the policy surface. The control fields decide which guest behaviours cause a trap. Should the guest’s HLT exit to the hypervisor or be allowed to run? Should CPUID be intercepted (it must, to lie about CPU features)? Should writes to CR3 trap, or is two-dimensional paging (Two-Dimensional Paging (EPT and NPT)) handling address translation so they need not? Every “should this guest action trap?” answer is a bit in the control area. The cost of running a guest is overwhelmingly the cost of these traps (VM Exit Reasons and Handling), so the control area is where a hypervisor tunes performance.

The Intel VMCS

Layout and the four region groups

Architecturally the VMCS is divided into six logical components (Intel SDM Vol 3C §25.1): the guest-state area, the host-state area, the VM-execution control fields, the VM-exit control fields, the VM-entry control fields, and the VM-exit information fields. The first 4 bytes of the page are not part of any field group: they hold the VMCS revision identifier (bits 0–30) plus a shadow-VMCS indicator (bit 31), and the next 4 bytes hold the VMCS abort indicator. KVM models exactly this preamble (arch/x86/kvm/vmx/vmcs.h, v6.12):

struct vmcs_hdr {
	u32 revision_id:31;
	u32 shadow_vmcs:1;
};
 
struct vmcs {
	struct vmcs_hdr hdr;   /* the 32-bit revision id + shadow bit  */
	u32 abort;             /* the VMCS abort indicator             */
	char data[];           /* the rest — opaque, touched only by VMREAD/VMWRITE */
};

The data[] member is deliberately an opaque flexible array: KVM never indexes into it. The CPU owns the format of everything past the first 8 bytes, and the only legal way to read or write those bytes is the VMREAD/VMWRITE instructions. The revision_id must match what the CPU reports in the IA32_VMX_BASIC MSR, or VMPTRLD (below) fails — this is how the CPU refuses to operate on a VMCS laid out for a different microarchitecture.

Field encoding — why fields have numbers, not offsets

Because the VMCS is opaque, fields are not named by byte offset; they are named by a 32-bit encoding. VMREAD reg, field_encoding reads, VMWRITE field_encoding, value writes. The encoding itself carries metadata, which KVM decodes with two small helpers (vmcs.h, v6.12):

static inline int vmcs_field_width(unsigned long field)
{
	if (0x1 & field)             /* bit 0 set -> a *_HIGH field, always 32 bit */
		return VMCS_FIELD_WIDTH_U32;
	return (field >> 13) & 0x3;  /* bits 14:13 encode the width class */
}
 
static inline int vmcs_field_readonly(unsigned long field)
{
	return (((field >> 10) & 0x3) == 1);  /* bits 11:10 == 01b -> read-only data */
}

Reading this is reading the encoding scheme directly: bits 14:13 select the width class (00b = 16-bit, 01b = 64-bit, 10b = 32-bit, 11b = natural-width i.e. 32/64 depending on mode), bits 11:10 select the type (00b = control, 01b = read-only data, 10b = guest state, 11b = host state), and bit 0 distinguishes the low and high halves of a 64-bit field. So the four “region groups” are not contiguous byte ranges you can memcpy; they are encoded into the field number itself. The actual encodings appear as the enum vmcs_field in the architecture header (arch/x86/include/asm/vmx.h, v6.12) — for example GUEST_CS_SELECTOR = 0x00000802 (type 10b = guest, width 00b = 16-bit), HOST_CS_SELECTOR = 0x00000c02 (type 11b = host), EPT_POINTER = 0x0000201a (control, 64-bit). That the read-only data fields exist as a distinct type is why VMWRITE to them fails with VMXERR_VMWRITE_READ_ONLY_VMCS_COMPONENT (error 13).

What the four groups hold, concretely

  • Guest-state area — the architectural CPU state the hardware loads on VM entry and saves on VM exit: CR0, CR3, CR4, RIP, RSP, RFLAGS, the segment registers (CS/SS/DS/ES/FS/GS) including their hidden base/limit/access-rights, LDTR, TR, GDTR, IDTR, and a pile of MSRs and the activity/interruptibility state. This is the guest’s whole worldview.
  • Host-state areaCR0/CR3/CR4, RIP, RSP, the host segment selectors and FS/GS bases, GDTR/IDTR bases. This is where the CPU lands on a VM exit. HOST_RIP in particular is the address at which the hypervisor resumes — KVM points it at its vmx_vmexit label, which is why control “magically” reappears there after the guest traps (see VM Entry and VM Exit Mechanics).
  • VM-execution / VM-entry / VM-exit control fields — the policy. Pin-based and processor-based execution controls decide which events trap (e.g. CPU_BASED_HLT_EXITING, CPU_BASED_USE_MSR_BITMAPS, SECONDARY_EXEC_ENABLE_EPT); the EPT_POINTER names the guest’s second-level page tables; the MSR_BITMAP/IO_BITMAP_A/B addresses point at bitmaps that select which specific MSRs and I/O ports trap rather than all of them. Entry controls govern injection of a pending event on the way in; exit controls govern what state is saved/loaded on the way out.
  • VM-exit information fields — read-only, written by hardware on exit: the exit reason (e.g. EXIT_REASON_EPT_VIOLATION = 48, EXIT_REASON_CPUID = 10, EXIT_REASON_HLT = 12, EXIT_REASON_IO_INSTRUCTION = 30), the exit qualification (a reason-specific detail, e.g. which CR was touched, or the faulting guest-physical address class for an EPT violation), and the VM-exit interruption-information fields. KVM reads these to dispatch the exit (VM Exit Reasons and Handling).

Active, current, and the on-chip cache

Here is the subtlety that the VMCS opacity exists to enable. A logical processor may have several active VMCSs at once, but only one current VMCS at any instant, and VMREAD/VMWRITE/VMLAUNCH/VMRESUME operate only on the current VMCS (Intel SDM Vol 3C; felixcloutier VMPTRLD). The hypervisor selects the current VMCS with VMPTRLD <phys-addr> (load VMCS pointer): “after execution, that VMCS is both active and current on the logical processor … loading the current-VMCS pointer with this operand” (felixcloutier VMPTRLD). It deactivates one with VMCLEAR <phys-addr>, which flushes any cached state back to memory and marks the VMCS launch-state “clear.” VMPTRST stores the current pointer.

Why this dance? Because the CPU is permitted to keep the current VMCS’s contents in implementation-specific on-chip memory for speed — a hidden cache. That is precisely why software must never mov into the VMCS page: the in-memory image may be stale relative to the on-chip copy. VMCLEAR is the instruction that says “I am done with this VMCS on this CPU; write the cache back to RAM.” This matters acutely when a vCPU migrates between physical CPUs: KVM must VMCLEAR on the old CPU (flush the cache to memory) before VMPTRLDing on the new one, or the new CPU would run with a half-stale VMCS.

KVM tracks all of this in struct loaded_vmcs, and keeps a per-CPU pointer to the live one (vmcs.h, v6.12):

DECLARE_PER_CPU(struct vmcs *, current_vmcs);
 
struct loaded_vmcs {
	struct vmcs *vmcs;
	struct vmcs *shadow_vmcs;
	int cpu;                 /* which physical CPU this VMCS is loaded on (-1 = none) */
	bool launched;           /* has VMLAUNCH run? -> use VMRESUME next time          */
	bool nmi_known_unmasked;
	...
	struct list_head loaded_vmcss_on_cpu_link;  /* all VMCSs loaded on this CPU */
	struct vmcs_host_state host_state;           /* write-through cache of host fields */
	struct vmcs_controls_shadow controls_shadow; /* cached control-field values */
};

The launched flag is the software memory of the VMCS launch state and is what decides VMLAUNCH vs VMRESUME (see VM Entry and VM Exit Mechanics). The loaded_vmcss_on_cpu_link list lets KVM VMCLEAR every VMCS still cached on a CPU that is going offline. The host_state and controls_shadow are software write-through caches: rather than issue a VMREAD (which is not free) to re-read a host field or a control word KVM already knows, KVM keeps a shadow copy in normal memory. The header comment is explicit: vmcs_host_state “is used as a write-through cache of the corresponding VMCS fields.” This is a direct consequence of VMCS opacity — because you cannot just dereference the field, caching what you wrote is a real optimization.

The AMD VMCB

One flat, documented struct

AMD took the opposite approach. The VMCB is a 4 KiB-aligned page whose layout is fixed and published by offset, and which the hypervisor reads and writes with ordinary memory accesses — no VMREAD/VMWRITE equivalent exists. The VMCB has just two areas: a control area (offset 0) and a state-save area (offset 0x400) (AMD SVM Arch. Ref. Manual 33047 §2.2). KVM mirrors this layout exactly as a packed C struct (arch/x86/include/asm/svm.h, v6.12):

struct vmcb {
	struct vmcb_control_area control;   /* 1024 bytes, offset 0x000 */
	union {
		struct vmcb_save_area save;     /* 744 bytes,  offset 0x400 */
		struct sev_es_save_area host_sev_es_save;  /* SEV-ES variant */
	};
} __packed;
 
/* build-time assertions that lock the offsets to the architecture */
#define EXPECTED_VMCB_CONTROL_AREA_SIZE 1024
#define EXPECTED_VMCB_SAVE_AREA_SIZE     744
BUILD_BUG_ON(offsetof(struct vmcb, save) != EXPECTED_VMCB_CONTROL_AREA_SIZE);  /* save @ 0x400 */

The BUILD_BUG_ON is doing real work: it makes the compiler fail the build if the struct ever drifts from the architectural offset of 0x400 for the save area. That AMD lets KVM use offsetof at all — that the layout is a real C struct, not an opaque blob — is the whole difference from Intel.

The control area: intercepts and exit info

The control area is the AMD analogue of Intel’s control + exit-information groups. Its head is the intercept vector — six 32-bit words enumerating every guest event the hypervisor wants to trap, decoded in the kernel as an enum (asm/svm.h, v6.12): INTERCEPT_CPUID, INTERCEPT_HLT, INTERCEPT_IOIO_PROT, INTERCEPT_MSR_PROT, INTERCEPT_VMRUN, and so on, plus per-CR and per-DR read/write intercepts and a 32-bit exception-intercept word. After the intercepts come the I/O- and MSR-permission bitmap base addresses, the tsc_offset, the asid, the interrupt-control fields, and then the hardware-written exit fields:

struct __attribute__ ((__packed__)) vmcb_control_area {
	u32 intercepts[MAX_INTERCEPT];   /* which guest events trap            */
	...
	u64 iopm_base_pa;                /* I/O permission bitmap              */
	u64 msrpm_base_pa;               /* MSR permission bitmap              */
	u64 tsc_offset;
	u32 asid;                        /* tagged-TLB address-space id        */
	...
	u32 exit_code;                   /* <- hardware writes the exit reason */
	u32 exit_code_hi;
	u64 exit_info_1;                 /* <- EXITINFO1: reason-specific       */
	u64 exit_info_2;                 /* <- EXITINFO2                        */
	u32 exit_int_info;
	...
	u64 nested_ctl;                  /* nested-paging enable               */
	u64 nested_cr3;                  /* NPT root (AMD's EPT analogue)       */
	...
	u64 next_rip;                    /* RIP after the intercepted insn     */
	u8  insn_len;                    /* length + bytes of the faulting insn */
	u8  insn_bytes[15];
	u64 vmsa_pa;                     /* SEV-ES encrypted save-area pointer  */
	...
};

On a VM exit (#VMEXIT) the processor “saves the reason for exiting the guest in the VMCB’s EXITCODE field; additional information may be saved in the EXITINFO1 or EXITINFO2 fields, depending on the intercept” (AMD 33047 §2.3). The exit codes themselves are enumerated in the UAPI header (uapi/asm/svm.h, v6.12) — e.g. SVM_EXIT_CPUID = 0x072, SVM_EXIT_HLT = 0x078, SVM_EXIT_IOIO = 0x07b, SVM_EXIT_NPF = 0x400 (nested-page-fault, AMD’s EPT-violation analogue), SVM_EXIT_VMRUN = 0x080. A genuinely useful AMD touch, absent on Intel: next_rip, insn_len, and insn_bytes mean that for many intercepts the hardware hands the hypervisor the decoded faulting instruction, so KVM need not re-walk guest page tables and re-fetch/decode it.

The state-save area

The save area is the AMD analogue of Intel’s guest-state area: the segment registers (each a vmcb_seg of selector/attrib/limit/base), CR0/CR2/CR3/CR4, DR6/DR7, RFLAGS, RIP, RSP, RAX, EFER, the SYSENTER/STAR/LSTAR MSRs, CR2, and so on (asm/svm.h, v6.12). Crucially the host state does not live in the VMCB on AMD — VMRUN saves host state to a separate page pointed to by the VM_HSAVE_PA MSR, not into the VMCB. So the AMD VMCB is single-tenant: it describes the guest plus the trap policy, while host return-state is parked elsewhere. (Under SEV-ES the guest save area moves out of the VMCB entirely into an encrypted VMSA page; the VMCB save area is then reused only to save/load host state, hence the union above — see AMD SEV and SEV-SNP.)

Intel vs AMD — the same job, two philosophies

DimensionIntel VMCSAMD VMCB
Size / alignment4 KiB, 4 KiB-aligned4 KiB, 4 KiB-aligned
Access methodonly VMREAD/VMWRITE (opaque)ordinary mov (documented offsets)
Layoutencoded field numbers, on-chip cache allowedflat C struct, offsets fixed by spec
Host statein the VMCS host-state areain a separate VM_HSAVE_PA page, not the VMCB
Currency modelactive/current; VMPTRLD/VMCLEAR swapVMRUN rAX names the VMCB per call; no “current” latch
Faulting-insn helpre-decode from guest memorynext_rip + insn_bytes provided by HW

The trade-off is real. Intel’s opacity buys the CPU freedom to cache and reorder the VMCS internally for speed, at the cost of forcing every access through a serializing instruction and forcing the hypervisor to maintain shadow caches (the host_state/controls_shadow in loaded_vmcs). AMD’s transparency makes the hypervisor’s life simpler — read a field, it is just a struct member — and the next_rip/insn_bytes gift removes a whole class of instruction-decode work, at the cost of a less abstracted hardware contract.

Uncertain

Verify: the exact byte offsets and the precise per-field membership of each VMCS group, and the claim that the VMCS abort indicator occupies bytes 4–7. Reason: these come from the Intel SDM Vol 3C §25.x, which was not fetched as a clean text extract during this task (the Intel PDF was located by search but not paragraph-verified); the KVM vmcs.h confirms the revision-id/shadow-bit/abort preamble and the encoding-decode helpers, but the full per-field offset table was corroborated via secondary references (OSDev VMX wiki, felixcloutier instruction pages) rather than the primary SDM tables. To resolve: read Intel SDM Vol 3C §25 “Virtual-Machine Control Structures” directly. The AMD-side offsets (control 1024 B at 0x000, save area 744 B at 0x400) are primary-verified against the kernel BUILD_BUG_ONs. uncertain

Uncertain

Verify: the AMD VMCB control-area size of 1024 bytes as an architectural guarantee vs a KVM implementation expectation. Reason: EXPECTED_VMCB_CONTROL_AREA_SIZE = 1024 is the kernel’s build-time assertion (primary, asm/svm.h v6.12); the AMD SVM manual fetched here (33047 Rev 3.02, 2005) documents the save area at offset 0x400 but the full Appendix C offset tables were not paragraph-extracted. The current canonical reference is AMD APM Vol 2 (newer than 33047). To resolve: cross-check AMD APM Vol 2 Appendix B “Layout of VMCB.” uncertain

Failure Modes and Pitfalls

  • moving into an Intel VMCS. A hypervisor author who treats the VMCS page like a struct (as on AMD) writes garbage: the CPU may be holding the live copy on-chip, so the in-memory write is silently ignored or, after a later VMCLEAR, overwritten. The only correct access is VMWRITE. This is the single most common porting bug for people moving SVM code to VMX.
  • Forgetting to VMCLEAR on vCPU migration. If a vCPU thread is rescheduled to a different physical CPU while its VMCS is still “current/active” on the old CPU’s on-chip cache, the new CPU’s VMPTRLD loads a stale RAM image. KVM’s loaded_vmcs.cpu field and the per-CPU loaded_vmcss_on_cpu_link list exist precisely to drive the VMCLEAR-then-VMPTRLD sequence correctly.
  • Wrong revision id. Copying a VMCS captured on one CPU model to another, or zeroing the page without writing the IA32_VMX_BASIC revision id, makes VMPTRLD fail with VMXERR_VMPTRLD_INCORRECT_VMCS_REVISION_ID (error 11, uapi/asm/vmx.h v6.12).
  • VMWRITE to a read-only field. Trying to write an exit-information field (type 01b) fails with error 13. The vmcs_field_readonly() helper exists so KVM can assert this in software.
  • AMD: stale host LDTR after VMEXIT. The AMD manual warns that “immediately after VMEXIT, the processor still contains the guest value for LDTR,” so the hypervisor must reload it; touching descriptor-table-relative state before doing so reads guest values (AMD 33047 §2.3). This is the kind of asymmetry the structures encode but do not fully automate.

Production Notes

KVM keeps the VMCS/VMCB hot. Beyond VMREAD avoidance via the controls_shadow/host_state write-through caches, modern Intel CPUs support VMCS shadowing (the shadow_vmcs bit in vmcs_hdr), which lets a nested L1 hypervisor’s VMREAD/VMWRITE hit a shadow VMCS directly instead of trapping to L0 — a large win for Nested Virtualization, where naive emulation would trap on every field access. On the AMD side, the next_rip/insn_bytes fields in the control area routinely save KVM an instruction-fetch-and-decode on common intercepts. When debugging a guest, trace-cmd/perf on the kvm:kvm_exit tracepoint shows the exit reason straight out of these structures (the symbolic names come from the VMX_EXIT_REASONS / SVM_EXIT_REASONS string tables in the UAPI headers), and the per-vCPU exit counters in vcpu->stat aggregate them — the universal first step in any “why is my VM slow” hunt is to read those reason codes, every one of which the CPU stamped into a VMCS/VMCB exit field.

See Also