The five bytes 0x6001600201 do one thing in the EVM: add 1 and 2, leaving 3 on top of the stack. They reference no register and never write down where the result should be stored; the two operands take their places implicitly on the stack, and the result goes back to the stack. All of the EVM's arithmetic and control flow is built on this model.
Earlier pieces have stayed on state machines and accounts; this one looks inward a level: what this machine is made of, how one turn of the execution loop runs, where the 256-bit word size and the 1024-item stack depth come from, and what this stack machine design buys and what it costs.
256-bit words: sized for cryptography, not for arithmetic
The Yellow Paper says only one sentence about word size: the machine word — that is, the width of a stack element — is 256 bits, a choice made to accommodate Keccak-256 hashing and elliptic curve operations. Unpacked, there are three concrete reasons.
Keccak-256 outputs 256 bits, so neither hash values nor storage keys need truncating or extending. Ethereum signs with ECDSA over secp256k1, and private keys and signature components fall inside the 256-bit space; the EVM-side counterpart is ecrecover, the precompiled contract at address 0x01 priced at 3000 gas. Addresses are 160 bits, which fit naturally into a 256-bit word.
There is a precision issue here that is often overlooked: the Keccak-256 Ethereum uses is not the NIST-standardized SHA3-256. The two have the same output width and the same permutation function, but different domain separation bytes in the padding: original Keccak uses 0x01, while SHA3-256 uses 0x06. When verifying hashes you have to choose a library that explicitly says Keccak-256; using SHA3-256 gives a completely different result.
The cost of that width is equally clear. All arithmetic is modulo 2^256, and overflow wraps silently instead of raising an exception; a boolean value also occupies a full 32 bytes; calldata and storage slots are aligned to 32 bytes, so short types have to be packed at the encoding layer — which is exactly what Solidity's storage packing does (see 0.19 “Storage Layout: How Solidity State Variables Land in Slots” in the same series). Wrapping semantics are also the root of a large share of integer overflow vulnerabilities historically. Since Solidity 0.8, checks are inserted by default and revert with Panic(0x11) — a language-level remedy; the EVM itself was not changed.
Machine state: what the stack, memory, and program counter each handle
The Yellow Paper writes machine state as the six-tuple μ = (g, pc, m, i, s, o): gas available, program counter, memory contents, the number of active words in memory, stack contents, and the return data buffer. The division of labor among the four components is easiest to see side by side:
| Component | Addressing and unit | Lifetime | Associated gas cost |
|---|---|---|---|
| Stack | Top visible only, 256 bits per item, capped at 1024 items | A single call frame | 2 to 3 gas on the opcode itself |
| Memory | Byte-addressed, expanded in 32-byte words | A single call frame; a new frame starts all zeros | 3a + ⌊a²/512⌋, where a is the number of active words |
| Program counter (PC) | Byte offset into the code | A single call frame | JUMP 8 gas, JUMPI 10 gas, JUMPDEST 1 gas |
| Gas counter | Gas available, a non-negative integer | The whole transaction; remaining gas moves between call frames according to the call arguments | Deducted per instruction by the fee schedule |
The stack is the only implicit operand area. It exposes only the top: the vast majority of instructions pop arguments from the top and push results back onto the top, and elements in the middle are invisible to them. The cap is 1024 items, each 256 bits.
Memory is byte-addressed, every position starts at zero, and it can be accessed with MLOAD/MSTORE (reading and writing 32-byte words) and MSTORE8 (writing one byte). Its cost is dynamic: expansion is billed by the number of active words, and the Yellow Paper's formula gives the total memory cost for a active words as 3a + ⌊a²/512⌋ gas. The quadratic term means that accessing an enormous offset can drain all the gas at once. So there is no such thing as a free large array in EVM memory, and no “reading uninitialized data” problem either: positions that were never written are always 0.
The program counter (PC) is the byte offset of the next instruction in the code. Control flow can change only through JUMP/JUMPI, and the Yellow Paper defines the set of legal jump targets as the positions in the code where a JUMPDEST instruction appears. That target set is therefore statically enumerable, and no address computed at runtime can be jumped to. This constraint is what makes offline control flow analysis possible.
The code itself is not kept on the stack, in memory, or in storage. The Yellow Paper states plainly that the machine is not von Neumann: the code is kept separately, in a virtual ROM reachable only through dedicated instructions. Deployed contract code therefore cannot rewrite itself, and the instruction stream cannot be replaced mid-execution. Storage and memory are mutable; code is not.
Fetch, decode, execute: one turn of the loop
The Yellow Paper defines “the instruction to execute now” as a piecewise function: if the program counter is less than the code length, the instruction is the byte at that position; otherwise the instruction is equivalent to STOP. In other words, reading past the end of the code is not an error — the specification defines natural termination this way.
Before an instruction can be executed, three things have to be known: how many items it pops (δ), how many it pushes (α), and how much gas it costs (the cost function C). These three quantities are decided by the instruction itself and written into its row of the opcode table. The loop can therefore be written as follows, omitting substate, access list, and gas refunds:
# Simplified execution loop, keeping the specification's order of checks
pc, gas, stack, memory = 0, gas_limit, [], bytearray()
while True:
# Fetch: running past the end of the code is equivalent to STOP
op = code[pc] if pc < len(code) else STOP
# Decode: look up the pop count, push count, and cost
delta, alpha, cost = OPCODE_TABLE[op]
# Validation happens before execution: insufficient gas, too few stack items, and stack overflow all halt exceptionally
if gas < cost or len(stack) < delta or len(stack) - delta + alpha > 1024:
raise ExceptionalHalt()
gas -= cost
# Execute: take operands from the top of the stack and push the results back
args = [stack.pop() for _ in range(delta)]
stack.extend(dispatch(op, args, memory, pc))
# Advance the PC; PUSH-family instructions also skip the immediate that follows
pc += 1 + immediates(op)
The comments correspond to three places that are easy to get wrong: the semantics of fetching past the end, the requirement that validation precede execution, and the fact that a PUSH immediate occupies code space.
The last point needs unpacking. PUSH1 through PUSH32 encode a constant directly after the opcode, so PUSH1 0x2a takes two bytes. That has a counterintuitive consequence: not every byte in bytecode is an instruction, so a jump target scan has to recognize and skip immediate data, or it may mistake a byte of a constant for a JUMPDEST.
Laying out the five bytes 60 2a 60 5b 56 makes it clearer. 60 2a is one instruction carrying an immediate; the second byte of 60 5b is the constant 0x5b, whose value is exactly the same as the JUMPDEST opcode, but it is not an instruction; 56 is the JUMP. When scanning for legal jump targets, an implementation must first recognize 0x60 and then skip the byte immediately after it. The rule itself is not complicated; it only requires any implementation doing control flow analysis to get PUSH lengths right, or the target set gets polluted by constant bytes.
This also explains why a dedicated 0x5b is needed as a jump marker instead of allowing jumps to arbitrary offsets.
Stack underflow and overflow: two exceptions, one outcome
The Yellow Paper's exceptional halt predicate Z lists every condition that immediately aborts execution, and two of them concern the stack: fewer items on the stack than the instruction wants to pop, which is underflow; and a stack height above 1024 after execution, which is overflow.
Both take the same path: exceptional halt, all remaining gas consumed, all state changes within this call frame discarded. The EIP-3855 spec test vectors give a clean controlled comparison: 1024 consecutive PUSH0 instructions execute successfully, while 1025 consecutive ones abort with stack overflow.
This needs distinguishing from another kind of “failure.” The REVERT instruction (0xfd, since Byzantium, EIP-140) also rolls back state, but does not consume the remaining gas and can return a stretch of memory to the caller as error data. A failed Solidity require normally compiles to REVERT, so the error message can travel back to the caller and the remaining gas is not consumed; stack underflow is a hard exception, showing up during debugging as gas exhausted with no return data. In debugging experience, a transaction that burns all its gas and yields no return data often got there because the bytecode hit a hard exception — the amount of computation itself need not have been large.
The boundary should be stated clearly too: if REVERT runs out of gas itself, or hits a stack underflow while executing, it degenerates into an ordinary exception and likewise consumes all the gas.
What the stack machine buys: implementation complexity pushed to a minimum
ethereum.org's explanation is that a stack structure is the preferred architecture for a virtual machine because it is easy to implement, which lowers the chance of bugs and security holes. That benefit can be broken down into several parts.
The decoder is minimal. An opcode is one byte, operand positions are implicitly determined by stack order, and bytecode does not need to encode a register number for each instruction. Apart from PUSH-family instructions carrying an immediate, instruction length is essentially fixed, so decoding is a single table lookup.
There is no register allocation layer in the specification. Allocation strategy is originally a compiler's freedom; once it enters the virtual machine specification, it becomes behavior every implementation has to reproduce bit for bit. A stack machine deletes “where the value lives” from the specification, shrinking the state description consensus needs to fix.
The semantics of stack operations are defined almost entirely one instruction at a time, gas costs can be attached directly to opcodes, and the search space for static analysis, fuzzing, and formal verification is smaller. This is one reason the EVM can have multiple independent implementations (geth, revm, evmone, and others) that stay byte-for-byte consistent.
What the stack machine costs: DUP, SWAP, and no random access
The price of exposing only the top of the stack shows up in the number of instructions in the bytecode.
To use an element near the top, DUP1 through DUP16 can copy it to the top, or SWAP1 through SWAP16 can swap it there. Sixteen is a hard ceiling: if the element you want to copy sits at depth 17, you first have to bring it into copyable range with something like SWAP16 before continuing, and the deeper the stack, the longer the chain of moves. The same computation that a register machine often does in one instruction expands here into a string of pushes, duplications, swaps, and pops.
Take a concrete case. A contract needs to compute f(a, b, c, d), and d sits at depth 4 of the stack. A register machine can reference the register holding d directly; on a stack machine the compiler either duplicates d ahead of time into a position closer to the top, or uses the SWAP family to bring it up before the call and swap it back afterward. This is why the Solidity compiler generates a lot of these rearrangement instructions when a function takes many parameters.
The compiler also has to maintain stack balance. Stack height should be predictable at the end of each basic block, or code after a jump cannot locate its operands. Solidity's compile phase therefore does dedicated stack scheduling, inserting DUP, SWAP, and POP to move operands around when there are many parameters and local variables. Those instructions are not expensive in themselves — most fall in the 3 gas tier — but they lengthen the interpreter's execution path.
For what a stack machine gives up relative to a register machine, an indirect reference point can be found in the JVM. Davis et al. translated JVM bytecode into a virtual register machine and reported the trade-off of fewer executed instructions against more bytecode fetches (Davis et al., 2003). That comparison comes from the JVM and cannot be transplanted directly to the EVM: it measures dispatch overhead in interpreted execution, while EVM gas pricing has already externalized most interpretive overhead. Only one directional judgment can be supported: for the same computation, a stack machine usually needs more executed instructions, and a register machine trades more fetches for fewer executed instructions.
The EVM's designers accepted that cost in return for implementation simplicity and a deterministic specification. There have been small incremental improvements in recent years as well, such as PUSH0 (EIP-3855, Shanghai), which replaced the 2-byte, 3-gas PUSH1 0x00 with a 1-byte, 2-gas instruction.
When this design becomes a burden
When expressions nest deeply and functions take many parameters, rearrangement instructions take up a larger share and the gas consumed by contract execution rises with them. On word widths, the EVM has no natural 8-bit, 32-bit, or 64-bit types — everything has to be explicitly truncated or masked, which deserves extra care when porting across languages.
The stack depth is 1024, but this 1024 is not the same as another 1024 it is often confused with. In its definition of CALL/CREATE, the Yellow Paper likewise limits call depth to 1024. The former caps the number of operands within a single frame, the latter caps the length of the call chain; hitting the former is a program bug, while hitting the latter usually means the recursion is written wrong.
One boundary also needs drawing: a stack machine is not an obstacle to parallel execution. Parallelism has to deal with read-write conflicts over globally mutable state, whereas where an operand sits on the stack is implicitly determined by stack order — a matter with no relation to conflict detection or execution determinism. The real constraint is at the state layer, which belongs to later pieces.
Sources
- Ethereum Yellow Paper (the machine state six-tuple, the 1024 stack cap, the memory cost formula, the JUMPDEST target set, exceptional halt function Z, the gas fee schedule and opcode table, ECREC precompile pricing): https://ethereum.github.io/yellowpaper/paper.pdf
- ethereum.org, Ethereum Virtual Machine (EVM) (stack depth 1024, and how the 256-bit word size relates to Keccak-256 / secp256k1): https://ethereum.org/developers/docs/evm/
- ethereum.org, Understanding the Yellow Paper's EVM Specifications (a stack machine is easy to implement and therefore less bug-prone, and the reasoning behind 256-bit words): https://ethereum.org/developers/tutorials/yellow-paper-evm/
- Keccak Team, Keccak specifications summary (the 0x06 suffix bits and padding process of SHA3-256): https://keccak.team/keccak_specs_summary.html
- Keccak Team, The Keccak reference, version 3.0 (the original Keccak's multi-rate pad10*1 padding): https://keccak.team/files/Keccak-reference-3.0.pdf
- NIST, FIPS 202 (SHA-3 padding 0x06): https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.202.pdf
- EIP-3855, PUSH0 instruction (0x5f, 2 gas, and the 1024-versus-1025 PUSH0 test vectors): https://eips.ethereum.org/EIPS/eip-3855
- EIP-140, REVERT instruction (rolls back without consuming all remaining gas, and the boundary at which it degrades into an exception): https://eips.ethereum.org/EIPS/eip-140
- Davis, Beatty, Casey, Gregg, Waldron, The Case for Virtual Register Machines, 2003 (translating JVM bytecode into a virtual register machine, reporting fewer executed instructions and more bytecode fetches): https://mural.maynoothuniversity.ie/id/eprint/10191/1/KC-Case-2003.pdf
- Solidity 0.8.0 Release Announcement (arithmetic checked by default, reverting with Panic(0x11)): https://www.soliditylang.org/blog/2020/12/16/solidity-v0.8.0-release-announcement/
- ethereum/execution-spec-tests (spec tests for PUSH0 and stack overflow): https://github.com/ethereum/execution-spec-tests