Trap-and-Emulate and Binary Translation

The Popek-Goldberg theorem told us when a machine can be virtualized. This lesson is about how: the concrete machinery a VMM uses to run a whole guest operating system on real hardware while staying in complete control. There are three historic techniques, and every hypervisor you will ever meet is built from one or more of them — trap-and-emulate (the textbook ideal), binary translation (VMware's answer to x86's leaks), and paravirtualization (Xen's answer). By the end you'll understand why x86 needed such cleverness before the hardware caught up.

The ideal: trap-and-emulate

Recall the plan. The VMM runs the guest — kernel and all — de-privileged, below the ring 0 it thinks it owns. Now the CPU does the policing for free:

The guest never notices: from its point of view the privileged instruction "just worked". This trap→emulate→resume loop is the beating heart of a classical VMM. It is exactly limited direct execution, lifted from processes to an entire OS. Trace the loop below.

The loop, drawn

The important economics: only the trap edges cost anything. If a workload almost never executes privileged instructions, it runs at nearly native speed. A syscall-heavy or interrupt-heavy kernel workload, though, can trap thousands of times a second, and each trap is a full mode-switch round trip into the VMM — this is the tax trap-and-emulate must pay, and the reason later hardware works so hard to make traps cheap.

Why the ideal breaks on x86

Trap-and-emulate has one non-negotiable prerequisite: every sensitive instruction must trap. On classic x86 that is false — there are 17 sensitive-but-unprivileged instructions (recall \texttt{popf}, \texttt{sgdt}, \texttt{smsw}). A de-privileged guest that executes \texttt{popf} to disable interrupts gets no trap; the interrupt-flag write is silently dropped. The VMM never finds out, the virtual machine's state drifts out of sync with what the guest believes, and eventually the guest crashes or misbehaves. Pure trap-and-emulate simply cannot run an unmodified x86 guest. Two very different rescues emerged.

Rescue 1 — VMware's dynamic binary translation

VMware's insight (1998–99): don't wait for a trap that will never come — rewrite the dangerous code before it runs. The VMM contains a just-in-time binary translator. As the guest kernel is about to execute a block of code, the translator scans it and produces a safe, functionally-equivalent version:

Crucially only guest kernel (ring 0) code needs translating; unprivileged guest user code has no sensitive instructions and runs native, untranslated. So a user-space-bound workload sees almost no overhead, while kernel-bound code pays for translation. VMware's translator was so good that on many workloads binary translation actually beat the first generation of hardware virtualization, because a translated \texttt{popf} is an inline callout, whereas a hardware trap was a slow VM-exit.

Rescue 2 — Xen's paravirtualization

Xen (Cambridge, 2003) took the opposite philosophy: if the problem is that the guest issues instructions the VMM can't safely intercept, then change the guest so it never issues them. In paravirtualization (PV) the guest OS is ported to a slightly different, virtualization-friendly interface. Where the guest kernel used to execute a privileged instruction, it now makes an explicit hypercall — a deliberate, well-defined call down into the hypervisor (the VM analogue of a syscall). "Load this page table", "update this PTE", "mask my virtual interrupts" all become clean API calls rather than trapped instructions.

The paravirtual idea never died — even today, "unmodified" guests on modern hardware still use paravirtual drivers (virtio) for fast I/O, which you'll meet in the I/O virtualization lesson. The lesson: even a leaky ISA can be virtualized if you are willing to translate the binary (VMware) or rewrite the guest (Xen).

Counting the traps

Let's make the trap tax concrete. A workload issues a stream of instructions; some fraction are privileged. Under pure trap-and-emulate, each privileged instruction is a costly trap. Binary translation replaces those with cheaper inline callouts, and a translation cache means each unique kernel block is only translated once. The simulation compares the total cost.

// Cost of running a kernel-heavy workload three ways. Costs in (illustrative) cycles. const TRAP = 1500; // a full trap/mode-switch round trip into the VMM const CALLOUT = 300; // a binary-translation inline call into the VMM const XLATE = 4000; // one-time cost to translate a unique block const NATIVE = 1; // an innocuous instruction const totalInsns = 1_000_000; const privFrac = 0.03; // 3% of executed insns are privileged/sensitive const priv = Math.round(totalInsns * privFrac); const uniqueBlocks = 800; // distinct kernel code blocks that get executed // Pure trap-and-emulate: every privileged insn traps (assume no silent leaks for the model). const teCost = (totalInsns - priv) * NATIVE + priv * TRAP; // Binary translation: each privileged insn becomes a callout; translate each unique block once. const btCost = (totalInsns - priv) * NATIVE + priv * CALLOUT + uniqueBlocks * XLATE; console.log(`executed instructions : ${totalInsns.toLocaleString()}`); console.log(`privileged/sensitive : ${priv.toLocaleString()} (${privFrac * 100}%)`); console.log(`trap-and-emulate cost : ${teCost.toLocaleString()} cycles`); console.log(`binary-translate cost : ${btCost.toLocaleString()} cycles`); console.log(`speedup of BT over T&E: ${(teCost / btCost).toFixed(2)}x`); console.log("\n=> when privileged ops are frequent, cheap inline callouts beat expensive traps —"); console.log(" exactly why VMware's translator out-ran first-gen hardware traps on kernel-heavy loads.");

The three techniques, side by side

TechniqueGuest modified?How sensitive ops are handledReal systemCatch
Trap-and-emulatenothey trap; VMM emulatesIBM VM/370needs sensitive ⊆ privileged (fails on x86)
Binary translationno (unmodified)translator rewrites them to VMM callouts, cachedVMware Workstation / ESXtranslation overhead; complex engine
Paravirtualizationyes (ported)guest issues explicit hypercalls insteadXen (PV mode)can't run closed/unmodifiable guests

It sounds terrifying — a VMM editing another OS's machine code as it runs. The safety comes from three disciplines. First, translation is overwhelmingly the identity: the translator changes only the handful of sensitive instructions and control-flow that leaves a block, so the guest's logic is preserved instruction-for-instruction. Second, the guest never executes its own kernel code directly — it only ever runs the translated copy out of the translation cache, so there is no way to "sneak past" the translator. Third, self-modifying guest code (and pages the guest writes to that hold translated code) is caught by write-protecting those pages, so the cache is invalidated and re-translated. It is precisely the same set of tricks a modern JavaScript JIT uses to safely run untrusted code faster than an interpreter.

A frequent muddle: hearing "the VMM emulates the instruction" and picturing a slow software CPU interpreting the whole guest, one instruction at a time (like QEMU's pure interpreter). That is not what trap-and-emulate or binary translation do. The overwhelming majority of guest instructions run natively on the real CPU; only the rare sensitive ones are emulated in software. "Emulate" applies to those individual leaking/privileged instructions, not to the guest as a whole. If the VMM interpreted everything it would satisfy fidelity and safety but violate performance — and by Popek-Goldberg's definition it would no longer be a virtual machine monitor at all, just an emulator.

The design law

Where next

Software heroics kept x86 virtualization alive for a decade. Then Intel and AMD did the honest thing and fixed the CPU. Next: hardware-assisted virtualization — VT-x and AMD-V — which added a real hypervisor mode and made textbook trap-and-emulate work again, this time in silicon.