$1,000,000 in security audit grants are live now, Apply here →

← Resource Hub

How the EVM Works: Stack, Memory, Calldata and Storage

Updated 8 min read

Quick answer. The EVM executes bytecode as a sequence of opcodes that move 32-byte words between the stack, memory, calldata and storage. The stack, memory and calldata last one transaction; storage is permanent and the most expensive.

Understanding how the EVM works is a big part of going from a beginner or intermediate developer or auditor to an advanced blockchain engineer or security researcher.

This guide lays out the structure of the EVM, then follows a real function call through it opcode by opcode.

Along the way you will pick up a few gas optimization tips.

Where Data Lives

There are six places the EVM can store data:

  1. The stack
  2. Memory
  3. Calldata
  4. Storage
  5. Code, which lives at an address
  6. Logs, a write-only way to emit events

This guide focuses on the first four. Once you know how they are laid out, you can see how the EVM uses each one while it executes a transaction.

1. The Stack

A stack is a simple data structure. You push an item onto it, then push another on top.

When you pop, the item comes off the top, so the last thing you pushed is the first thing you get back.

The EVM's stack holds 32-byte words. To store the number 1, you push a word of 31 zero bytes followed by the byte 01.

Each pair of hex characters is one byte. Each hex character is 4 bits, so the byte 01 is the bits 0000 and 0001.

0x0000000000000000000000000000000000000000000000000000000000000001

Besides push and pop, there are opcodes that swap items around on the stack.

Many opcodes work on the top one or two items of the stack. Those items are things like uints, bools, addresses, and pointers to areas in memory and storage.

2. Memory

Memory also works in 32-byte words, but there is no pushing or popping. Instead, you write to a specific memory word and choose exactly which bytes to write.

Memory stores things like structs, and Solidity uses it internally behind the scenes. Because of that, it has a particular layout.

The memory layout

Memory words sit at offsets of 32 bytes: 0x00, then 0x20 (32 in decimal), then 0x40, and so on.

OffsetPurpose
0x00 and 0x20Scratch space Solidity uses for work such as hashing
0x40The free memory pointer, initially 0x80
0x60The zero slot: always empty, the initial value for dynamic arrays
0x80 onwardFree for new data

The free memory pointer

The free memory pointer holds the first word that nothing has been written to and nothing has reserved.

Read it before writing to memory and you know where it is safe to write without overwriting old data.

It starts at 0x80, not 0x60, because 0x60 is meant to stay empty forever.

It is the zero value for dynamic arrays and should never be written to, so the pointer skips it.

Why does the pointer sit at 0x40, before the zero slot? If a transaction never writes memory beyond the pointer's word, the EVM never has to allocate another word.

If the two were flipped, setting the pointer would always pull in one more empty word. It is a very slight optimization.

Memory expansion

Suppose you use MSTORE to write to a huge offset. That is extremely expensive, because now every memory word below it must be accounted for.

As memory expands, gas costs climb steeply, faster than the memory grows.

3. Calldata

Calldata also holds arbitrary bytes, but you can only read it. Nothing can be written to calldata, and that makes reading from it much cheaper.

So err on the side of reading from calldata. If you never need to modify a value, read it from calldata and do not copy it into memory.

The same goes for parameters: if a function takes a memory parameter it never modifies, declare it calldata.

// copies the array into memory
function total(uint256[] memory values) external pure returns (uint256 sum) {
    for (uint256 i; i < values.length; i++) sum += values[i];
}

// reads it straight from calldata, which is cheaper
function totalCd(uint256[] calldata values) external pure returns (uint256 sum) {
    for (uint256 i; i < values.length; i++) sum += values[i];
}

For how calldata is encoded, read Ethereum calldata explained.

4. Storage

Storage is the most expensive place to keep data, and the only permanent one. The stack, memory and calldata are all temporary: they go away when the transaction ends.

Think of storage as the blockchain's database. It works a bit like memory, with 32-byte words, except they sit in storage slots.

There are no reserved areas and no free memory pointer.

Storage in the blockchain state

The blockchain state is a set of accounts. Each account has a nonce and an ether balance.

If the account is home to a contract, it also has code, and likely storage. That storage breaks down into slots.

Each account is keyed by a 160-bit number, its address. Each storage slot is keyed by a 256-bit number, a full word.

Both the account state and each contract's storage are kept in Patricia Merkle tries. That is how storage is laid out as a persistent part of the blockchain state.

Opcodes

What makes all these areas interact is opcodes. An opcode is a specific instruction telling the EVM to do something.

Imagine a robot with four places to put things.

You hand it items and instructions for what to do with them and where to put them. That is essentially the EVM.

The Ethereum yellow paper lists every opcode. Each has a one-byte hex code, which is how opcodes become something the EVM can actually execute.

Opcodes for each area

  • ADD takes the top two items on the stack, adds them, and puts the result on the stack.
  • PUSH1 to PUSH32 push an item of 1 to 32 bytes onto the stack. It stops at 32 because a full word is 32 bytes. Push one byte, and the other 31 are padded with zeros.
  • POP (0x50) removes the top item from the stack.
  • MSTORE saves a whole word to memory. It takes the top two stack items as the position and the value.
  • MLOAD does the opposite: it reads the word at a memory position and puts it on the stack.
  • MSTORE8 saves a single byte to memory.
  • SLOAD and SSTORE load and store a word at a storage slot.

There are many more. Read the yellow paper and get familiar with all of them.

Opcodes in Bytecode

You can see opcode bytes directly in a contract's code. In Foundry's Chisel, set up an example contract and print address(this).code.

Now add one variable, uint256 i = type(uint256).max, and print the code again.

A new run of 32 bytes of FF appears: the maximum uint256, where every bit of every byte is 1.

Right before those 32 bytes is the byte 7F. In the yellow paper, 0x7F is PUSH32. The bytecode pushes your 32-byte value onto the stack, exactly as you would expect.

Tracing a Function Call

The EVM Codes playground lets you look at the bare opcodes. Its default example is a counter contract.

Run its count method and you can step through every opcode, watching how the stack, memory and storage change.

Setting the free memory pointer

The first opcodes push 0x80, push 0x40, then MSTORE. That stores 0x80 at 0x40: the free memory pointer. Every transaction starts by setting it.

The non-payable check

Next comes a check that the call sent no ether, because count is not payable. Here is the trace so far.

PUSH1 0x80
PUSH1 0x40
MSTORE        // free memory pointer: 0x80 at 0x40
CALLVALUE
DUP1
ISZERO        // 1 if no ether was sent
PUSH2 0x0010
JUMPI         // if so, jump to 0x10
...           // otherwise, fall through to a revert
JUMPDEST      // 0x10

CALLVALUE puts the value sent on the stack, and DUP1 duplicates it. ISZERO puts a 1 if the value is zero, or a 0 if not. PUSH2 pushes the two bytes 0x0010.

JUMPI conditionally alters the program counter, the position in the program. If the second stack item is not zero, it jumps to the destination on top of the stack.

No ether was sent, so ISZERO left a 1 and execution jumps to 0x10. A jump must always land on a JUMPDEST, or the whole program reverts.

The calldata size check

After a little stack cleanup, the code pushes 4, then CALLDATASIZE, then runs LT. That checks whether the calldata is less than four bytes long.

If it is, JUMPI goes to 0x4c, which leads to a revert. Calldata under four bytes cannot even hold a function selector, so it is clearly invalid.

Function selectors

A function selector is the first four bytes of the Keccak hash of the function's signature. Foundry's cast sig computes it for you, for get() or count().

When you call count, its selector is the first four bytes of the calldata. That is how the call says which function it wants.

Extracting the selector

The calldata is long enough, so there is no jump. CALLDATALOAD loads one word of calldata onto the stack, then the code shifts it right by 0xE0 with SHR.

0xE0 is 224.

Shifting a 256-bit word right by 224 bits leaves 32 bits on the right. 32 bits divided by 8 bits per byte is four bytes: the function selector.

The dispatcher

Next, a series of PUSH4 opcodes pushes the selectors of the contract's functions. Each is compared with the selector from the calldata.

If one matches, the code pushes the location where that function's opcodes begin and jumps there. If not, it moves on to the next selector.

Here the very first comparison matches: count's selector begins 0x066. Execution jumps to 0x51 and eventually reaches an SLOAD to read the storage slot. The getter then returns that value.

Why selector order matters for gas

count was checked first, but not because it appears first in the contract. The selectors are sorted by numerical value, ascending.

With four functions or fewer, the dispatcher does a linear search, checking one by one.

So at the opcode level, calling a function with a lower selector value is technically cheaper: it skips the comparisons for every other function.

With more than four functions, the dispatcher switches to a binary search, which works because the selectors are sorted.

To manipulate memory directly at this level, continue with Yul memory explained.

How Guardian Helps

Guardian's smart contract audits are carried out by senior researchers who attack protocol logic, integrations and privileged flows with exploit-driven review.

An invariant suite built around the protocol's safety properties stays with your team after the audit.

Frequently Asked Questions

What are the main data locations in the EVM?

The stack, memory, calldata and storage, plus code and logs. The stack, memory and calldata are temporary, lasting one transaction. Storage is permanent and the most expensive.

What is the free memory pointer?

The word at memory offset 0x40. It holds the first free memory location, initially 0x80, so code knows where it can write without overwriting existing data.

Why is calldata cheaper than memory?

Calldata is read-only, which makes reading from it much cheaper. If a function never modifies a parameter, declare it calldata instead of memory to avoid copying it.

How does the EVM know which function to call?

The first four bytes of calldata are the function selector, from the Keccak hash of the signature. The dispatcher compares it with each function's selector and jumps to the match.

Sources

Get low-level code reviewed by experts

Guardian's researchers read contracts down to the EVM, so bugs in assembly, memory and storage do not slip through.