JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

The EVM as a stack machine: opcodes, stack, and 256-bit words

A smart contract is just a string of bytes, and the EVM is the tiny, deterministic CPU that runs them one opcode at a time. Watch it add 2 and 3 on a stack and you'll understand the whole machine.

The world computer needs a CPU

In the earlier rungs you learned that Ethereum is often called a world computer: a single shared machine whose memory is the global state and whose programs are smart contracts. But where exactly does a contract run? When you deploy a contract you don't upload Solidity source — you upload a string of bytes. Every node on the network must execute those bytes and arrive at exactly the same result, or the chain would fork. The component that does this execution, identically on thousands of machines, is the Ethereum Virtual Machine (EVM).

The EVM is a virtual machine — it doesn't exist as silicon, it's a specification implemented in software (Geth, Reth, Nethermind, and others). And it is deliberately one of the simplest CPU designs imaginable: a stack machine. If you've ever used an old HP calculator in Reverse Polish Notation, where you type `2`, `3`, `+` instead of `2 + 3`, you already have the right mental model. The EVM has no registers and no random-access scratch pad for its arithmetic; it has a single stack, and almost every operation pushes values onto it or pops values off it.

Anatomy of the machine: stack, words, and the program counter

An EVM execution has a handful of moving parts. The star is the stack: a last-in-first-out list that holds up to 1024 entries. Every entry is a 256-bit word — that is, 32 bytes wide. The other pieces, which the next guides cover in depth, are memory (a temporary byte array, cleared after the call), storage (the contract's permanent key–value map), calldata (the read-only input), and the program counter (PC), an index that says which byte of the code we're about to execute.

Why 256 bits, when most CPUs work in 32 or 64? Because the EVM's whole world is built on cryptography. A Keccak-256 hash is 256 bits. The output of the curve arithmetic behind addresses is 256 bits. Native integers in Solidity are `uint256`. By making the word size match the hash size, the EVM lets you hold a full hash, a full storage key, or a full balance in one stack slot without splitting it — at the cost of being wasteful for small numbers, where 255 of the 256 bits are zeros.

EVM execution context (simplified)

  code:    the contract's bytecode, a byte array   e.g. 60 02 60 03 01
  pc:      program counter, index into code        starts at 0
  stack:   up to 1024 words, each 256 bits          [ ] (empty at start)
  memory:  volatile byte array, grows as needed     (cleared each call)
  storage: persistent map  slot(256b) -> word(256b) (survives the call)
  gas:     remaining gas, decremented per opcode
The pieces the EVM juggles while running a contract. This guide focuses on the stack and the program counter.

Execution is a tight loop: read the opcode at `pc`, do what it says (touching the stack and maybe memory/storage), charge its gas, advance `pc`, repeat. The loop ends when the code runs out, or hits a `STOP`/`RETURN`, or hits an error such as running out of gas — in which case the EVM does a revert, throwing away every state change made during the call.

Opcodes: one byte at a time

The contract's code is a sequence of opcodes — and because each opcode is a single byte, there can be at most 256 of them. The EVM defines roughly 140. They fall into a few families: arithmetic (`ADD`, `MUL`, `SUB`, `DIV`, `MOD`, `EXP`), comparison and bitwise (`LT`, `GT`, `EQ`, `AND`, `OR`, `XOR`, `NOT`), hashing (`KECCAK256`), reading the environment (`CALLER`, `CALLVALUE`, `NUMBER`, `TIMESTAMP`), the data-location ops (`MLOAD`/`MSTORE`, `SLOAD`/`SSTORE`, `CALLDATALOAD`), flow control (`JUMP`, `JUMPI`, `JUMPDEST`, `PC`), and system ops (`CALL`, `CREATE`, `RETURN`, `REVERT`).

How do values get onto the stack in the first place? With the `PUSH` family. `PUSH1` pushes the next 1 byte of code as a 256-bit word; `PUSH2` pushes the next 2 bytes; all the way to `PUSH32` for a full word. These are the only opcodes that read their operand from the code stream itself — every other opcode takes its inputs from the stack. That's why bytecode looks like data and instructions interleaved: a `PUSH1` is always followed by the literal byte it pushes.

Because the stack is strictly last-in-first-out, you can't reach down and grab the third item from the bottom. Two families exist precisely to work around this: `DUP1`…`DUP16` copy the n-th item from the top back onto the top, and `SWAP1`…`SWAP16` exchange the top with the n-th item below it. A surprising amount of compiled Solidity is `DUP`/`SWAP` shuffling, because keeping the right value within reach on a stack is real work.

PUSH1  0x60  push the next 1 byte as a 256-bit word
ADD    0x01  pop a, pop b, push (a + b) mod 2^256
MUL    0x02  pop a, pop b, push (a * b) mod 2^256
SUB    0x03  pop a, pop b, push (a - b) mod 2^256   <- order matters: top - next
POP    0x50  pop one item and discard it
DUP1   0x80  duplicate the top item
SWAP1  0x90  swap the top two items
JUMPDEST 0x5b a legal jump target (does nothing else)
A few opcodes with their one-byte values and stack effects. The full set is the EVM's instruction set.

A program, traced: 2 + 3 on the stack

Let's run the canonical first program. As bytecode it is just five bytes: `0x6002600301`. Read left to right, that is `PUSH1 0x02`, `PUSH1 0x03`, `ADD`. We'll step the program counter through it and watch the stack change. The stack is drawn with the top on the right, which is the end the opcodes act on.

bytecode:  60 02 60 03 01
           ^pc=0  ^pc=2  ^pc=4

--- step 1 ---  pc=0  read 0x60 = PUSH1, operand 0x02
   action: push 2
   stack:  [ 2 ]                       gas: -3

--- step 2 ---  pc=2  read 0x60 = PUSH1, operand 0x03
   action: push 3
   stack:  [ 2, 3 ]   <- top is 3      gas: -3

--- step 3 ---  pc=4  read 0x01 = ADD
   action: pop 3, pop 2, push (2 + 3)
   stack:  [ 5 ]                        gas: -3

pc=5: end of code -> halt. Result 5 sits on top of the stack.
Total gas for computation: 3 + 3 + 3 = 9.
Stepping 0x6002600301 instruction by instruction. PUSH1 advances pc by 2 (opcode + operand byte); ADD advances pc by 1.

Notice the detail in step 1 and step 2: `PUSH1` is one byte for the opcode plus one byte for the literal it carries, so after the first push the program counter jumps from 0 to 2, not 0 to 1. The operand byte `0x02` is data sitting inside the code, never executed as an instruction. `ADD`, by contrast, takes no operand from the code — it reads both inputs from the stack — so it is a single byte and advances the counter by just 1.

Control flow: jumps, JUMPDEST, and loops

Straight-line arithmetic is not enough for a real program — you need `if` and `while`. The EVM has no high-level control structures; it has exactly two jump opcodes that move the program counter. `JUMP` pops a destination off the stack and sets `pc` to it. `JUMPI` (jump-if) pops a destination and a condition, and jumps only if the condition is non-zero — that single opcode is how every `if`, `require`, and loop is built.

There is one crucial safety rule: you may only jump to a byte that holds a `JUMPDEST` opcode (`0x5b`). `JUMPDEST` itself does nothing — it's a label marking a legal landing spot. Without this rule, an attacker could jump into the middle of a `PUSH`'s operand bytes and trick the EVM into reinterpreting data as instructions. The EVM scans the code for valid `JUMPDEST`s and rejects any jump to a non-destination with a revert.

// goal: if (x == 0) return 99; else return x;   (x already on the stack)

  DUP1            // [x, x]   keep a copy to return later
  ISZERO          // [x, x==0 ? 1 : 0]
  PUSH1 0x09      // [x, cond, 0x09]   address of the JUMPDEST below
  JUMPI           // [x]   pop dest 0x09 and cond; jump there iff cond != 0
  // ---- fall-through path: x was non-zero, x is on top, return it ----
  // (RETURN logic would go here)
  // ---- pc 0x09: the zero case ----
  JUMPDEST        // legal landing pad at byte 0x09
  POP             // discard x
  PUSH1 0x63      // push 99
  // (RETURN logic would go here)
A branch built from JUMPI and JUMPDEST. Every if/else and loop in Solidity compiles down to this shape.

A loop is the same trick pointed backward: do some work, test a condition, and `JUMPI` back to an earlier `JUMPDEST` if you should iterate again. There is no separate loop instruction. This is also why an infinite loop is not a catastrophe on Ethereum the way it is on a normal computer: every iteration burns gas, and when the gas runs out the EVM halts and reverts. Gas, which the third guide in this rung covers, is the leash that keeps even buggy or malicious code from running forever.

Why a stack machine? Determinism, gas, and trade-offs

The design choices now make sense as a whole. The EVM is a stack machine because a stack machine is small and unambiguous: there are no registers to allocate, the instruction set is tiny, and the same bytecode produces the same stack on every implementation. That determinism is non-negotiable — consensus requires that a node in Tokyo and a node in São Paulo compute byte-for-byte identical results, so the EVM forbids anything non-deterministic (no real randomness, no floating point, no wall-clock time except the block's own fields).

A stack machine also makes gas metering clean: because every opcode has fixed, known stack effects, you can assign each one a fixed gas price and sum the cost deterministically as you go. `ADD` and `PUSH1` cost 3 gas each; `JUMPDEST` costs 1; `JUMP` 8; a fresh storage write costs 20,000. Pricing is per-opcode precisely because the machine is per-opcode.

The trade-offs are real, and worth naming honestly. The 256-bit word size is enormous for typical values — looping a counter from 0 to 100 still shoves 32-byte words around — which wastes computation and is one reason 256-bit math is expensive relative to native hardware. The stack-only model means lots of `DUP`/`SWAP` overhead that register machines avoid. This is exactly why proposals like eWASM once explored compiling contracts to WebAssembly instead. For now, though, the EVM's simplicity has won: its tiny, deterministic core is precisely what makes it possible to formally reason about, re-implement in many clients, and trust across the whole network.