它不是一台物理机器,而是一份被网络中每个节点严格复现的规范。同样的输入,在任何机器上都产出分毫不差的输出——这正是去中心化共识的前提。
没有寄存器。所有运算在一个后进先出(LIFO)的栈上完成,最深 1024 项,每项都是 256 位字。
没有随机、没有系统时间、没有网络 I/O。相同状态与交易永远得到相同结果,节点才能达成共识。
逻辑上可计算任意程序,但每一步都消耗 Gas。Gas 耗尽即停机——用经济手段驯服了停机问题。
每个操作码明码标价。计算、存储、内存扩展都要付费,从根本上杜绝了资源滥用与无限循环。
合约只能触及自己的存储与被显式传入的数据,无法访问宿主文件系统、进程或其他合约的私有状态。
字宽为 32 字节,天然契合 Keccak-256 哈希与椭圆曲线密码学,让链上加密运算成为一等公民。
EVM 处在整条流水线的中央:高级语言编译成字节码,EVM 执行字节码,执行结果落地为新的世界状态。它是"执行"这一环的唯一裁判。
Solidity / Vyper / Yul 编写的合约逻辑,面向开发者。→
solc 等把源码降级为 EVM 操作码序列。→
部署上链的十六进制指令流,如 60 80 60 40…→
逐条取指、解码、执行,读写栈/内存/存储。→
余额、存储、代码的更新落入 σ′,达成共识。
EVM 运行时由若干数据区与寄存器构成,它们的生命周期与读写权限各不相同。点击任意模块,检视它的定位。
把这些模块接起来,就是一台活的机器。字节码从 Code 出发,经 PC 定位、解码,交给执行引擎;引擎在栈上运算、按需读写内存与存储,每一步都从 Gas 里扣费——持久化的写入最终汇入世界状态。
只读来源:不可变的 Code 与调用方传入的 Calldata。引擎能读、不能改。
所有运算都在 栈 上进行;内存 作临时草稿,超出运算范围的数据放这里中转。
要永久保存就写 存储,它随交易汇入 世界状态;否则一切在调用结束时蒸发。
按 PC 指向的偏移,从字节码中取出下一个操作码字节(PUSH 系列还要连带取出其立即数)。
识别该字节对应哪个操作码,确定它需要几个栈操作数、要做什么动作、以及标价多少 Gas。
从栈取操作数、完成运算或读写、扣减 Gas,并推进 PC(JUMP 则改写 PC)。周而复始,直到 STOP / RETURN / REVERT 或 Gas 耗尽。
世界状态是一张从地址到账户的映射。账户分两种,但都由同样的四个字段描述。
用户钱包即属此类。持有余额、有 nonce,但没有代码。只有 EOA 能签名并主动发起交易——它是所有链上活动的起点。
部署时写入不可变的字节码,拥有专属持久存储。它从不主动行动,只在被调用时按代码执行——是纯粹的被动响应者。
| 账户状态字段 | 含义 | EOA | 合约 |
|---|---|---|---|
| nonce | 已发送交易数 / 已创建合约数,防重放 | ✓ | ✓ |
| balance | 该地址持有的 Wei 数量 | ✓ | ✓ |
| codeHash | 账户代码的 Keccak-256 哈希 | 空代码哈希 | 合约字节码哈希 |
| storageRoot | 该账户存储 Trie 的根哈希 | 空 | 持久键值状态 |
三个最易混淆的概念。区别的核心只有两点:活多久,以及多贵。
| 维度 | 栈 Stack | 内存 Memory | 存储 Storage |
|---|---|---|---|
| 结构 | LIFO 栈,≤1024 项 | 线性字节数组,按需扩展 | 256→256 位键值映射 |
| 寻址单位 | 整字(32 字节) | 字节,可读写任意偏移 | 32 字节槽位(slot) |
| 生命周期 | 单条指令间 | 单次调用内,调用结束清零 | 永久,跨交易存续 |
| 成本量级 | ≈3 gas | 低,但扩展成本二次增长 | 极高(首次写入 20000 gas) |
| 典型用途 | 操作数与运算中转 | 函数内临时缓冲、返回数据 | 合约的持久状态变量 |
| 是否上链 | 否 | 否 | 是(进入世界状态) |
合约字节码由一连串单字节操作码构成,总计约 140 余个。按类别筛选,检视它们各自吞吐的栈数据与价格。
这是一台真实运行的 EVM 子集解释器。选一段程序逐条推进,读右侧的逐指令旁白看每一步到底做了什么、扣了多少 Gas;或在底部粘贴你自己的字节码。
合约里没有"函数"这个概念——只有一段代码。所谓调用某函数,其实是把 calldata 的前 4 字节(函数选择器)与合约内预置的常量逐一比对,命中就跳转。下面是实时用 Keccak-256 算出的选择器。
Gas 是计算量的度量单位,与它的价格是两回事。EIP-1559 之后,一笔费用被拆成两部分。
baseFee 基础费:由协议按区块拥堵度自动调节,并被销毁(burn),不归任何人。
tip 优先费:付给出块验证者的小费,用来竞争打包优先级。
| 动作 | 操作码 | 约成本 (gas) | 为何如此 |
|---|---|---|---|
| 栈运算 | ADD / SUB / LT | 3 | 纯 CPU,几乎免费 |
| 读存储 | SLOAD | 100 / 2100 | 热槽位 100,冷槽位首访 2100 |
| 写存储(0→非0) | SSTORE | 20000 | 永久占用全网状态,代价最高 |
| 部署合约 | CREATE | 32000 + | 写入代码 200 gas/字节 |
| 交易基础费 | — | 21000 | 每笔交易固定入场费 |
前段近乎线性,几乎免费;但因 a² 项,随内存增大成本急剧抬升——这是对"用光内存"的隐形约束。
从钱包里的一次签名,到世界状态被永久改写,交易要穿过这条流水线。EVM 只是其中执行的那一环。
部署合约是一种特殊交易:它携带的不是最终代码,而是一段初始化代码(initcode)。EVM 先执行它,把它返回的字节作为合约的运行时代码永久落盘。
交易的 data 字段(to 为空)。含构造函数逻辑:设置初始存储、校验参数,最后返回运行时代码。
在新地址的上下文中执行。构造期可写存储、可调用他方合约,但此时合约代码尚未落盘。
initcode 的 RETURN 返回值即为部署字节码,写入账户 codeHash。此后每次被调用执行的都是它。
合约之间的每次交互都是一次"消息调用"。关键差异在于:代码在谁的上下文里执行、能不能改状态。切换看看上下文如何变化。
A 调 B、B 再调 C——每次调用都压入一个调用帧,形成执行栈。RETURN 弹出一帧并交还控制权;REVERT 则连同本帧的所有状态变更一起撤销。亲手推推看。
每一次执行都必然停机,但结局大不相同——决定了状态是被保留、被回滚,还是 Gas 被吞噬殆尽。
正常结束。状态变更全部生效;RETURN 还会附带返回数据。剩余 Gas 退还给发起者。
主动回滚。本次调用的所有状态变更撤销,可返回错误信息,剩余 Gas 退还——这是 require 失败的底层机制。
燃料耗尽。所有状态变更回滚,但已消耗的 Gas 不退——这是对占用算力的惩罚。
遇到未定义指令(如 0xfe)。等同于耗尽全部 Gas 并回滚,常用作断言失败的哨兵。
合约请求销毁自身并转出余额。语义在近期硬分叉中被大幅弱化,已不推荐使用。
通过硬分叉,EVM 的指令集与计价规则持续演化。点击时间轴上的里程碑,看它给虚拟机带来了什么。
有些运算(尤其是密码学)若用操作码硬算会极其昂贵。EVM 把它们做成预编译合约,安置在 0x01 起的固定地址:像普通合约一样被 CALL,却以原生代码高速执行。
| 地址 | 名称 | 作用 |
|---|---|---|
| 0x01 | ecRecover | 从签名恢复出签名者地址(ECDSA),支撑一切链上验签 |
| 0x02 | SHA-256 | SHA-256 哈希 |
| 0x03 | RIPEMD-160 | RIPEMD-160 哈希 |
| 0x04 | identity | 原样返回输入,常用于高效拷贝数据 |
| 0x05 | modexp | 大数模幂,RSA / 高级密码学的基石 |
| 0x06–08 | bn256 系列 | 椭圆曲线加法、乘法与配对,zk-SNARK 验证核心 |
| 0x09 | blake2f | BLAKE2 压缩函数,跨链验证等场景使用 |
| 0x0a | KZG point eval | 验证 blob 承诺(Cancun 随 Proto-Danksharding 引入) |