The Dispatch Loop: The Heart of a Virtual Machine
A virtual machine has parts, but only one of them moves. The bytecode array just sits there. The instruction pointer is only a number. The stack only holds values. The thing that actually does anything is the dispatch loop, and it is astonishingly simple for how much it carries. This lesson is about that one loop, because if you understand it you understand the whole engine, and you understand exactly what a reverse engineer finds when they open virtualized output.
Three steps, forever
The loop repeats the same cycle until the program ends. Fetch: read the instruction the pointer is aimed at. Decode: look at its opcode and decide which operation it names. Execute: carry that operation out against the stack, and adjust the pointer. Then loop. That is the entire life of a processor, real or virtual, compressed into three words that never stop turning.
loop forever:
instr = code[ip] # fetch: read the current instruction
ip = ip + 1 # advance to the next slot by default
switch instr.opcode: # decode: which operation is this?
PUSH: stack.push(instr.operand)
ADD: b = stack.pop(); a = stack.pop(); stack.push(a + b)
SUB: b = stack.pop(); a = stack.pop(); stack.push(a - b)
MUL: b = stack.pop(); a = stack.pop(); stack.push(a * b)
JUMP: ip = instr.operand # control flow: overwrite ip
HALT: break # stop the loop
# execute happened inside the switch; loop againRead that loop once and you have read the beating heart of virtualization. Notice how small the decode step is: a switch on a number. Notice how every operation is a few lines. Notice that jump is not special machinery, it just writes the pointer. And notice halt, the one instruction that ends the whole thing by leaving the loop.
Picture a vending machine that only knows how to do one thing at a time. It reads a button code, looks up what that code means on a little chart, does that one action, then waits for the next code. It has no idea what sequence of buttons you plan to press or what snack you are ultimately after. It just handles one code, then the next. The dispatch loop is that vending machine, running your whole program one button at a time.
The switch is the decode step
The heart of decode is that switch: a fan-out from an opcode number to the block of code that handles it. Each branch of the switch is called a handler. Push has a handler, add has a handler, jump has a handler. The loop's only job is to keep aiming the right handler at the current instruction. This is why the meaning of the opcode numbers is the secret worth protecting: the handlers spell out what each number does, and an attacker who reads the handlers has read the machine's whole vocabulary.
Step the machine and watch the loop from the outside. Each press is one full fetch-decode-execute cycle: the pointer reads an instruction, the switch routes it to the right handler, and the stack changes. The narration tells you which handler just ran. You are hand-cranking the exact loop written above.
Why an attacker sees something generic
Here is the strategic point. The dispatch loop has to ship as runnable code, so a reverse engineer can always read it. But the loop is generic. It says fetch, decode, execute, and nothing about your program. Your logic lives in the bytecode array, and to that array the loop is indifferent. So the attacker gets a readable engine that reveals the shape of the machine but not your intent, and a data blob that holds your intent but only in a numbering they must first reconstruct handler by handler.
This is also where obfuscators layer further defense. The handlers can be split, duplicated, and shuffled so the switch is not a tidy table. The decode step can be indirected through a lookup so the opcode-to-handler mapping is itself data rather than visible cases. None of that changes the fetch-decode-execute skeleton; it only makes reading the skeleton slower. The loop is still doing exactly three things forever.
The overhead of virtualization lives right here. Running your addition natively is one machine instruction. Running it through this loop costs a memory read to fetch, a branch to decode, the pops and pushes to execute, and a pointer update, for every single original operation. That multiplier, often five to twenty times or worse per operation, is why virtualization is applied to the parts of a program that are worth protecting rather than to everything. The dispatch loop is both the source of the protection and the source of the cost, because the indirection that hides your logic is the same indirection that slows it down.
Fetch, decode, execute, repeat. The dispatch loop is the only moving part of a VM: a switch from opcode to handler, running one instruction at a time. It ships as readable code but reveals only the machine, not your logic, and the very indirection that hides your program is what makes it slower to run.
Frequently asked questions
Is the dispatch loop really just a switch statement?
At its core, yes. A switch on the opcode routes each instruction to its handler. Real obfuscators dress this up, splitting and indirecting the switch so it is harder to read, but the underlying fetch-decode-execute cycle is always there, because it is the only way to run a stream of instructions one at a time.
Why is virtualized code slower?
Because every original operation now pays for a fetch, a decode branch, stack traffic, and a pointer update, instead of running as one native instruction. That overhead, commonly several times the native cost per operation, is why virtualization is applied selectively to the parts of a program worth the price rather than to the whole thing.
Can an attacker just read the handlers to break the machine?
Reading the handlers reveals what each opcode does, which is real progress for the attacker, and it is exactly why the mapping is obscured and renumbered per build. Even with the handlers understood, they still have to replay the bytecode to recover behaviour, and the work does not carry to a differently numbered build.