The Ethereum Virtual Machine is a deterministic, stack-based computer embedded in Ethereum's state-transition rules. A transaction or message call selects an account, supplies calldata and gas, and causes bytecode to execute one opcode at a time. The machine manipulates 256-bit words, expands temporary memory, reads or writes persistent storage, creates logs, calls other accounts, and either returns successfully or halts with a defined failure.

That compact description hides the compatibility surface developers actually use. EVM behavior includes account context, transaction envelopes, contract creation, precompiles, call variants, revert data, event logs, gas rules, and application binary interface conventions. Understanding those layers makes debugging and security review much easier than treating Solidity source as if it ran directly.

From source code to runtime bytecode

Solidity and other high-level languages compile into EVM bytecode. Deployment uses a creation transaction whose initialization code runs once. That code can validate constructor arguments, initialize state, and return a second byte sequence: the runtime bytecode stored at the new contract address. Future calls execute the runtime portion, not the constructor.

Compilers also emit an ABI, source maps, metadata, and sometimes storage-layout information. The ABI describes how external function selectors and arguments are encoded; it is a tooling convention rather than an opcode feature. Source maps connect instruction offsets back to source files and lines. When reviewing a deployed contract, bytecode is the authority, while compiler artifacts make it intelligible.

The official Ethereum EVM documentation describes the machine as a stack architecture with volatile memory and persistent storage. Those three data areas behave differently enough that confusing them is a common source of performance and security mistakes.

The 256-bit operand stack

Most opcodes take operands from the top of the stack and push results back. Arithmetic, comparisons, bitwise operations, hashing setup, memory access, storage access, control flow, and calls all use this pattern. The word size is 256 bits, which aligns with common cryptographic values and addresses padded into words, but it also means small integers occupy full stack slots during execution.

The stack is not random-access general memory. Duplication and swap instructions reach only a limited range near the top, so compilers schedule values carefully. Deep expressions can create “stack too deep” pressure at the source level because too many live values must remain accessible. Optimizing compilers may move values through memory or restructure code to manage this constraint.

Raw traces list program counter, opcode, gas, and sometimes stack values. Reading them is easier when you treat each call frame separately. The same bytecode can run under different address or storage context depending on the call type, so opcode rows alone do not tell the full story.

Memory is temporary and byte-addressed

EVM memory begins empty for each message call. It is a linear byte array used for ABI decoding, hashing input, dynamic arrays, return data, and temporary structures. Instructions read and write 32-byte words, single bytes, or copied regions. Memory disappears when the current call frame ends.

Memory expansion costs gas. The charge depends on the highest region touched and grows nonlinearly, so an accidental pointer to a huge offset can consume the remaining budget. Compilers maintain a free-memory pointer by convention, but the VM itself does not understand high-level object lifetimes. Assembly code must avoid overwriting reserved regions or aliasing data unexpectedly.

Calldata is separate. It is a read-only byte sequence supplied to the call and is often cheaper to use directly for external-function parameters than copying everything into memory. Return data is another distinct buffer populated after an external call. Keeping these regions conceptually separate helps explain the source-language location keywords calldata, memory, and storage.

Storage is persistent contract state

Each contract account has a persistent mapping from 256-bit slots to 256-bit values. High-level variables are assigned to slots according to compiler layout rules. Fixed-size values may be packed together, while mappings and dynamic arrays derive locations through hashing. A source-level structure can therefore generate several storage reads, hashes, and writes.

Storage is expensive because every node must maintain the resulting shared state. Gas cost depends on whether a slot is cold or already accessed, whether the old value is zero, whether the new value clears or sets data, and which protocol rules are active. The transaction maintains an access context, so repeated reads in one execution can cost less than the first.

Writes are journaled. If a call reverts, changes made in that frame and its successful descendants are discarded according to call semantics. Logs are reverted too. This makes storage look transactional, but external interactions can still create reentrancy: another contract may call back before the original function has completed its intended state sequence.

Gas is both a limit and a semantic input

A transaction provides a gas limit, and opcodes deduct from the remaining amount. If execution reaches zero before a successful halt, the current call fails with an out-of-gas exception. Because gas affects whether later instructions execute, the budget is part of program behavior. Forwarded gas to nested calls, stipends, memory expansion, and dynamic opcode charges can change the observed path.

A deliberate REVERT differs from running out of gas. It rolls back state in the frame but can return structured bytes and leaves unused gas available to the caller. High-level custom errors and revert strings are encoded into that data. A caller may bubble the failure up, decode it, or handle it and continue.

Gas estimates are simulations against a particular state and transaction context. They can become inaccurate when storage values, access lists, base fees, block context, or called contracts change. Production systems should include reasonable headroom and monitor actual consumption rather than treating one estimate as a permanent constant.

Calls change code and context in different ways

A regular call executes the target account's code with the target's storage and address context. The caller can send value and choose how much gas to forward. A static call adds a read-only restriction that prevents state-changing operations. A delegate call executes another account's code while retaining the caller's address, storage, original sender, and value context.

Delegate call enables reusable libraries and proxy upgrade patterns, but it also couples code to the caller's storage layout. If implementation versions assign variables to different slots, new logic can overwrite existing state. Authorization is equally important because implementation code can modify the proxy's assets and roles as if it were the proxy.

Calls can fail without automatically reverting the caller. Low-level call instructions return a success flag and bytes. High-level interfaces usually turn failure into an exception, but assembly and low-level wrappers must check the flag explicitly. Ignoring it can convert a failed transfer or authorization step into an apparently successful transaction.

Control flow, halting, and invalid execution

The program counter identifies the next opcode. Jumps are allowed only to valid jump destinations in legacy EVM bytecode, and compilers build branches and loops from those primitives. Execution can halt successfully with no data, return a byte range, revert with data, or encounter an exceptional condition such as invalid opcode, stack underflow, bad jump, or out of gas.

Nested execution creates a tree of frames. A child can succeed and return data, fail and return revert data, or consume its gas. The parent decides what to do next unless its own language wrapper automatically propagates the exception. Debuggers that render the call tree and decoded inputs usually reveal the cause faster than a flat opcode list.

Logs are receipts, not contract-readable storage

Contracts emit logs with topics and data. Nodes include them in transaction receipts, and external indexers use them to build searchable event histories. Logs are cheaper than persistent storage for information that contracts do not need to read later, but they are not available to future EVM execution as state.

Applications should not confuse an emitted event with enforcement. A contract must still update the storage that future calls rely on. Indexers must also handle chain reorganizations and removed logs. An event interface is an off-chain consumption surface, not an independent source of protocol truth.

A practical trace-reading workflow

  1. Confirm transaction sender, target, value, calldata, gas limit, and chain context.
  2. Decode the top-level selector and arguments using the correct ABI.
  3. Open the call tree and find the deepest frame that first failed.
  4. Inspect its revert bytes, source location, and nearby storage or memory operations.
  5. Walk upward to see whether callers propagated, wrapped, or ignored the failure.
  6. Compare state reads with the assumptions made by the test or user interface.
  7. Reproduce on a pinned local fork when the failure depends on live contracts.

Conclusion

The EVM is small enough to describe precisely but rich enough to support a large software ecosystem. The stack carries immediate operands, memory holds temporary bytes, storage persists contract state, calldata supplies read-only input, and gas bounds every path. Calls add context, logs expose receipt data, and reverts define transactional failure boundaries.

For a focused resource model, read gas, bytecode, and determinism. For hands-on workflow, continue to the local EVM development stack and the virtual-machine security review.