What is Ethereum Virtual Machine?
Understanding the Ethereum Virtual Machine (EVM)
What's a Virtual Machine?
The EVM, short for Ethereum Virtual Machine, emulates a physical machine via software. Unlike standard programs, it mimics an actual machine’s behavior.
Physical Machines
In computing, a "physical machine" typically refers to a CPU (Central Processing Unit) – the chip inside a computer that performs computations. CPUs are highly optimized for speed, executing instructions at incredible rates.
Virtual Machines
A virtual machine (VM) is software that emulates a physical machine, like a CPU. Similarly, the EVM reads, interprets, and executes bytecode instructions from smart contracts, updating contract state and returning data.
What's an Instruction?
Instructions are specific tasks machines perform. In the EVM, instructions are low-level tasks like arithmetic operations, memory writing, etc., each consisting of an opcode and relevant data.
On most platforms, the anatomy of an instruction consists of:
opcode + arguments
where an opcode is the task you want to perform, and the arguments are the relevant data you want to perform that task with.
Opcodes
An opcode in the EVM is represented in binary, but for human readability, it's commonly represented in hexadecimal. Opcodes like "ADD" (binary: 00000001) translate to their hexadecimal equivalent (e.g., 0x01).
First and foremost, machines operate on binary. Any instruction you send to a CPU is going to be sent as 0’s and 1’s in a pre-specified format.
What format? It depends on the platform! And since we’re talking about the EVM, let’s demystify an opcode right away:
Here is what the opcode for adding two numbers looks like:
00000001
That’s right, it’s just 8 numbers of binary – which is 8 bits, which is 1 byte. This is the entirety of an EVM opcode.
But as it turns out, reading binary isn’t that fun for humans. In our line of work, we rewrite the above as hexadecimal. Here are two examples of how that conversion happens:
00000001 - ADD opcode (binary)
=> 0000 0001
=> 0 1
=> 0x01 - ADD opcode (hex)
11111101 - REVERT opcode (binary)
=> 1111 1101
=> F D
=> 0xFD - REVERT opcode (hex)
This is much easier to read; it allows us to avoid counting and calculating which bits are which values, and read hex numbers and letters instead.
The 0x prefix represents hexadecimal. You may see a lot of protocols use this prefix in their name too (0xMacro, 0xSplits, etc.), even when it technically isn’t hexadecimal.
This is because it’s common for companies in the web3 space to do so, and thus it became a mark of being associated with the web3 industry.
And that's it! You just learned what the entirety of an EVM opcode looks like: a single byte of data.
Opcode Arguments
In the EVM, opcode arguments are often taken implicitly from the stack rather than explicitly specified in the bytecode.
You might be thinking, "but what about the arguments?".
Let’s take the ADD example again. While some platforms require you to specify where the two numbers are (e.g. registers), on the EVM, arguments are implicitly taken from the stack.
Opcodes for a Virtual Machine
So what does this have to do with machines vs virtual machines and the EVM? Well, to summarize:
A virtual machine is a software program that behaves like a hardware CPU.
The EVM is a virtual machine (hence the name).
Thus, the EVM executes instructions via opcodes, just like a hardware machine would.
Binary as Programs
EVM execution involves bytecode (program to be executed), a program counter (position of the executing opcode), and the execution environment (stack, memory, etc.).
How does the EVM execute binary?
Conceptually, you can think of execution as three parts:
I. The bytecode
- This is the program that will be executed. It can look something like this: 0x62020f0960405260206040f3
- It primarily consists of opcodes to run. For example, 0x62 is the PUSH3 opcode.
- Some opcodes take literal arguments, which are also part of the bytecode. For example, in 0x62020f09, the first byte (0x62) is a PUSH3 opcode, which reads the next 3 bytes an its argument (0x020f09).
II. The program counter
- This is the position of the currently running opcode.
- The program counter starts at zero (the leftmost byte) and increments after each opcode execution (moving rightwards).
- This is similar to an index of a string.
- Opcodes with literal arguments will increment the program counter beyond its arguments. For example, executing a PUSH32 will increment the program counter by 33 bytes.
- The JUMP and JUMPI opcodes allow you to set the program counter directly, to any position of a JUMPDEST opcode.
III. The execution environment.
- This is the stack, memory, contract storage, calldata, and various fields about the running transaction.
- Opcodes manipulate stack, memory, and storage when executed.
Running as Long as Possible
- The EVM executes bytecode until certain conditions occur, such as returning a value, encountering a STOP opcode, crashing due to invalid opcode, reaching the end of bytecode, or running out of gas.
- In other words, when executing bytecode, the EVM treats the first byte as an opcode, then runs the next byte as an opcode, and then the next, and then the next, and so on.
- It continues to do this until one of the following happens:
1. The bytecode returns a value (via the RETURN opcode).
2. The bytecode simply stops execution (via the STOP opcode).
3. The bytecode crashes, by e.g. reaching an invalid opcode.
4. The program counter reaches the end of the bytecode (rare).
5. The execution environment runs out of gas.
6. Because of that last one, EVM execution is guaranteed to halt. Even an infinite loop will eventually halt, due to the amount of gas always being finite.
Also note that execution may not always be strictly from left-to-right; the JUMP and JUMPI opcodes allow the program counter to jump to any other part of the bytecode – as long as the destination byte is a JUMPDEST byte.
All OpCodes Require:
- Target contract address
- Call data memory address
- Call data length
- Return output memory address
- Return data length
Questions to Ponder:
-
What happens if a contract returns more than the specified length?
- All memory up to the specified length is overwritten with return data.
- Extra data is truncated.
-
What happens if a contract returns less than the specified length?
- All returned data is written to memory.
- Extra memory remains unchanged.
-
While in stack, Loading address information directly from storage vs. memory?
Shout Outs:
- https://ethereum.org/en/developers/docs/evm/
- https://learnevm.com/
- https://solidity-by-example.org/
- https://soliditylang.org/