EVM and bytecode
Architecture
The EVM is a stack machine with three data areas:
| Area | Lifetime | Cost | Addressing |
|---|---|---|---|
| Stack | One execution frame | Cheapest | 1024 slots of 32 bytes; only the top 16 reachable |
| Memory | One execution frame | Cheap, grows quadratically | Byte-addressed, linear |
| Storage | Persistent on-chain | Very expensive | 32-byte keys to 32-byte values |
Storage writes dominate the cost of almost every contract. Reading a storage slot once into memory and working from there is the first optimisation to reach for — see Gas optimization.
Opcodes are one byte, so there are at most 256 of them. The authoritative list is in the Ethereum yellow paper; evm.codes is the practical reference, with current gas costs.
Common opcodes
0x15 ISZERO ; Logical NOT: pushes 1 if the top of stack is 0
0x34 CALLVALUE ; Wei sent with the call that triggered this execution
0x39 CODECOPY ; Copy code running in the current environment into memory
0x50 POP ; Discard the top stack item
0x52 MSTORE ; Write a 32-byte word to memory
0x54 SLOAD ; Read a word from storage
0x55 SSTORE ; Write a word to storage
0x56 JUMP ; Set the program counter
0x57 JUMPI ; Set the program counter if the condition is non-zero
0x5b JUMPDEST ; Mark a valid jump target
0x60 PUSH1 ; Push a 1-byte immediate onto the stack
0x80 DUP1 ; Duplicate the top stack item
0xf3 RETURN ; Halt and return output data
Note JUMPI, not "JUMP1" — the I is for if. Jumps may only land on a
JUMPDEST; any other target aborts execution, which is what stops an attacker
jumping into the middle of an instruction.
The program counter indexes into the bytecode, and every instruction is one
byte plus any immediate data. PUSH1 0x80 occupies two bytes, so the next
instruction sits at PC + 2.
Bytecode and deployed bytecode
Two distinct artefacts, and mixing them up wastes a lot of time:
| Artefact | What it is | Where it appears |
|---|---|---|
bytecode (creation code) | Constructor plus the runtime code it returns | The data field of the contract-creation transaction |
deployedBytecode (runtime code) | What lives at the contract address afterwards | Returned by eth_getCode |
// Runtime code at a deployed address
await web3.eth.getCode("0xCONTRACT_ADDRESS")
The creation code contains the runtime code — it ends by copying it into
memory with CODECOPY and returning it with RETURN. That return value is what
the network stores. So the runtime code is a subsequence of the creation code,
not the other way round; the constructor logic runs once and is then discarded.
This is why a constructor cannot be called again, and why constructor arguments are appended to the creation code rather than stored.
Storage layout
Storage is a mapping of 32-byte slots. The compiler assigns them in declaration order, packing smaller types together where they fit:
contract Layout {
uint128 a; // slot 0, bytes 0–15
uint128 b; // slot 0, bytes 16–31 — packed with a
uint256 c; // slot 1 — needs a full slot
uint8 d; // slot 2, byte 0
}
Packing saves storage but costs gas when only one packed variable is written, because the EVM reads the whole slot, modifies part of it and writes it back. Group variables that are read and written together.
Dynamic types
uint[] public cc; // length at slot p
// elements at keccak256(p) + index
mapping(address => uint) public balances; // value at keccak256(key . p)
For an array declared at slot p, the length lives in p and element i lives
at keccak256(p) + i. For a mapping, the value for key lives at
keccak256(key . p) — the key concatenated with the slot number, each padded to
32 bytes, then hashed.
Mappings have no length and cannot be enumerated, because the slots are scattered across the whole 2²⁵⁶ space by the hash.
// Read a raw slot, bypassing any getter
await web3.eth.getStorageAt(address, slot)
getStorageAt reads everything, including variables marked private. Nothing
in contract storage is confidential — private restricts access from other
contracts, not from observers.
Memory layout
Solidity reserves the first four 32-byte words:
| Range | Purpose |
|---|---|
0x00–0x3f | Scratch space for hashing |
0x40–0x5f | Free memory pointer |
0x60–0x7f | Zero slot — the initial value for empty dynamic arrays |
0x80 | Where allocation begins |
The free memory pointer at 0x40 always holds the address of the next unused
byte. Inline assembly that allocates memory must read it, use the space and
advance it; overwriting it corrupts every later allocation.
assembly {
let ptr := mload(0x40) // read the free memory pointer
mstore(ptr, someValue) // use the space
mstore(0x40, add(ptr, 0x20)) // advance it
}
mload(0xAB) loads the 32-byte word starting at byte 0xAB — note that memory
is byte-addressed, so loads may straddle word boundaries.
Memory is charged for the highest byte touched, and the cost grows quadratically past 724 bytes. Repeatedly expanding memory in a loop is a real gas trap.