Zen 3 to Zen 5: What Changes, What Doesn't
I moved from a Ryzen 7 5800XT (Zen 3) to a 9850X3D (Zen 5). Same core count -- 8 cores, 16 threads. Same SVM interface. Same VMCB layout. The hypervisor binary I built on Zen 3 should work on Zen 5 without changes. But "should" is a word I've learned not to trust when hardware is involved.
This post is about what I checked, what I expected to change, and what actually mattered when moving a bare-metal hypervisor between CPU generations.
SVM is architectural
The AMD Secure Virtual Machine extension is defined by the AMD64 architecture specification. It's not a per-generation feature that changes with microarchitecture. The VMCB layout -- the 4KB page that controls every aspect of virtualization -- has been the same since SVM was introduced. The control area fields are at the same offsets. The save area fields are at the same offsets. VMRUN, VMSAVE, VMLOAD, CLGI, STGI -- same instructions, same semantics.
// These VMCB offsets are the same on Zen 1 through Zen 5:
const VMCB_CTRL_INTERCEPTS: usize = 0x000;
const VMCB_CTRL_TLB: usize = 0x058;
const VMCB_CTRL_EXIT_CODE: usize = 0x070;
const VMCB_CTRL_NCR3: usize = 0x0B0;
const VMCB_SAVE_RIP: usize = 0x578;
const VMCB_SAVE_RSP: usize = 0x5D8;
const VMCB_SAVE_RAX: usize = 0x5F8;
// No recompile needed. Same binary works.
This is one of the things I appreciate about AMD's approach. Intel changes their VMX structures between generations more aggressively. AMD keeps the VMCB stable. I can load the same .sys driver on Zen 3 and Zen 5 and it just works.
What might trip me up
Even though the SVM interface is stable, the CPU behind it changes. Three things I checked before booting the hypervisor on Zen 5.
New MSRs
Every CPU generation adds new model-specific registers. My MSR intercept handler has a default case that returns zero for unknown MSRs (a lesson learned from the GIF=0 debugging saga). But if a new MSR is important and I'm returning zero for it, the guest might behave differently than on bare metal.
// MSR handler — default case handles new MSRs gracefully
match msr_index {
0xC001_0114 => { // VM_CR — always intercepted }
0xC001_0115 => { // IGNNE }
// ... known MSRs ...
_ => {
// unknown MSR — return 0 on read, ignore write
// prevents #GP, but guest gets wrong value
// add match arms as I discover new ones
if is_read { regs.rax = 0; regs.rdx = 0; }
}
}
New CR4 bits
Zen 5 supports CET (Control-flow Enforcement Technology) shadow stacks. That means new bits in CR4 that I need to pass through correctly. My CR4 intercept exists to protect specific bits -- I need to make sure I'm not accidentally trapping or masking the CET bits.
// CR4 bits I care about:
const CR4_SMEP: u64 = 1 << 20;
const CR4_SMAP: u64 = 1 << 21;
const CR4_PKE: u64 = 1 << 22;
const CR4_CET: u64 = 1 << 23;
// Rule: only intercept bits I need to control.
// Pass through everything else unchanged.
// New bits I don't know about should flow through.
New CPUID leaves
Zen 5 reports new feature flags through CPUID. My CPUID handler intercepts specific leaves to lie about hypervisor presence and feature support. New leaves that I don't intercept pass through natively, which is the correct default behavior.
The board: B650M C V3
The motherboard matters more than most people think. Two things I checked on the B650M.
Q-Flash Plus -- this lets me flash a BIOS image via USB without a working CPU. Critical for SmmInfect development. If I brick the BIOS with a bad SMM module, I can recover without desoldering the flash chip.
PSB unfused -- Platform Secure Boot. When fused, the CPU will only boot firmware signed by a specific vendor key. On this board it's unfused, meaning I can flash modified firmware without PSB blocking it. Once PSB is fused it's permanent -- the fuse is literally blown in silicon.
match arms for new MSRs and CPUID leaves as I discover them. That's not a rewrite -- it's maintenance.Performance differences
Zen 5 has deeper pipelines and wider execution. VMRUN/#VMEXIT round-trip cost might be different. On Zen 3 I measured an average of ~150 cycles. I'll need to re-measure on Zen 5 and update the TSC offset accordingly. If the exit cost is lower, my TSC compensation will over-correct and the guest will see time moving slightly backward -- which is worse than slightly forward from a detection standpoint.
// TSC offset needs re-calibration per microarchitecture:
//
// Zen 3 (5800XT): avg exit cost ~150 cycles
// tsc_offset = -150
//
// Zen 5 (9850X3D): need to measure
// tsc_offset = -(measured_avg)
//
// calibration: run 10000 CPUID exits,
// measure RDTSC delta, compute average
per_cpu.tsc_offset = -(calibrate_exit_cost() as i64);
The calibration runs automatically at virtualization time -- it's not hardcoded. So in theory the binary self-adjusts. But I want to verify the Zen 5 numbers manually to make sure the auto-calibration is within the expected range.
Same binary, new silicon
The move from Zen 3 to Zen 5 is about as smooth as it gets for a hypervisor. SVM doesn't change. The VMCB doesn't change. The instructions don't change. What changes is the microarchitecture underneath -- deeper buffers, wider ports, different branch prediction. None of that affects correctness, only performance.
I'll need to re-measure timing, audit new MSRs, and make sure CET bits pass through CR4 correctly. But none of that is a rewrite. It's adding a few match arms and re-running the calibration. The foundation is the same.
The 9850X3D also has 3D V-Cache -- 96MB of L3 cache on the CCD. That's interesting for the hypervisor because the VMCB, NPT page tables, MSR permission bitmap, and all per-CPU data structures fit comfortably in L3. On Zen 3 with 32MB L3, some of those structures would get evicted under pressure. More cache means fewer memory stalls during VMRUN and VMEXIT. I expect exit latency to be slightly lower, but I won't know until I measure.
That's the benefit of building on an architectural specification rather than implementation details. Microarchitectures come and go. SVM stays.