MODULE 1 · EVM 内部原理

Lesson 4: EVM 操作码

📖 阅读 ~15 分钟🧪 2 个实验❓ 2 道测验

EVM 是一台栈机器

以太坊虚拟机 (EVM) 是一台基于栈的虚拟机。它不像 x86 那样有寄存器,所有运算都在一个 1024 深的栈上进行。每个栈元素是 256 bit (32 字节)

// EVM 执行模型 Bytecode: 60 03 60 05 01 00 ↓ ↓ ↓ ↓ PUSH1 3 PUSH1 5 ADD STOP // 栈的变化过程: Step 1: PUSH1 3 Stack: [3] Step 2: PUSH1 5 Stack: [5, 3] Step 3: ADD Stack: [8] ← 弹出 5 和 3,压入 8 Step 4: STOP 执行结束
💡 关键理解 你写的 Solidity 代码最终被编译成一串操作码 (opcode) 字节序列。EVM 从第一个字节开始,逐条执行。理解操作码就是理解合约真正在做什么

EVM 的数据区域

EVM 有四个数据存放区域,理解它们的区别至关重要:

Stack(栈)
运算的核心,最多 1024 个元素。LIFO(后进先出)
免费使用,但深度有限
Memory(内存)
线性字节数组,交易结束后清空。按 32B 对齐扩展
MLOAD/MSTORE,3 gas 起
Storage(存储)
持久化 key-value 映射(上节课的内容)
SLOAD/SSTORE,最贵
Calldata(调用数据)
只读的输入数据,来自交易的 input 字段
CALLDATALOAD,3 gas

常用操作码速查

算术 & 比较

OpcodeHex描述Gas
ADD01a + b3
MUL02a × b5
SUB03a − b3
DIV04a ÷ b(整除)5
MOD06a % b5
LT / GT / EQ10/11/14小于 / 大于 / 等于3
ISZERO15== 0 ?3

栈操作

OpcodeHex描述Gas
PUSH1~PUSH3260~7f压入 1~32 字节常量3
POP50弹出栈顶2
DUP1~DUP1680~8f复制第 N 个栈元素到栈顶3
SWAP1~SWAP1690~9f交换栈顶与第 N+1 个元素3

环境 & 区块信息

Opcode描述Gas
CALLERmsg.sender(调用者地址)2
CALLVALUEmsg.value(发送的 ETH 数量)2
TIMESTAMP当前区块时间戳2
NUMBER当前区块号2
BALANCE查询地址的 ETH 余额2600
ORIGINtx.origin(最初发送者)2

存储 & 内存 & 调用

Opcode描述Gas
SLOAD读取 Storage slot2100 (cold)
SSTORE写入 Storage slot5000~22100
MLOAD从 Memory 读 32B3
MSTORE写 32B 到 Memory3
CALL调用外部合约2600+ (cold)
DELEGATECALL保持调用者上下文的外部调用2600+
STATICCALL只读外部调用(不能修改状态)2600+
RETURN返回 Memory 中的数据0
REVERT回滚状态并返回错误数据0
🔍 CALL vs DELEGATECALL CALL 在目标合约的上下文中执行(msg.sender = 调用者,storage = 目标的)。
DELEGATECALL 在调用者的上下文中执行(msg.sender 不变,storage = 调用者的)。
这就是代理合约 (Proxy Pattern) 的核心原理 —— 逻辑在别处,数据在本地。

函数选择器:前 4 字节

当你调用合约函数时,交易的 input data 的前 4 字节是函数选择器

// 函数签名 → 选择器 transfer(address,uint256) → keccak256("transfer(address,uint256)") → 0xa9059cbb... → 取前 4 字节: 0xa9059cbb // 完整的 calldata 结构: 0xa9059cbb ← 4B 选择器 000000000000000000000000d8da6bf26964af9d7eed9e03e534 ← 32B 参数1 (to) 0000000000000000000000000000000000000000000000000de0b ← 32B 参数2 (amount) // EVM 用 CALLDATALOAD 读取这些数据 // Solidity 编译器生成的 dispatcher 会匹配选择器
⚠️ 选择器碰撞 4 字节只有 2³² ≈ 43 亿种可能。不同函数签名可能产生相同的选择器(碰撞)。恶意合约可以利用这一点伪装函数。这就是为什么钱包需要 ABI 来正确解码交易。

🧪 动手实验

🔬 实验 1:解码一笔真实交易的 input data

找一笔 USDC transfer 交易,用 ChainLab 解码它的 calldata,识别函数选择器和参数。

chainlab tool decode 0xa9059cbb...(USDC transfer calldata)

思考:0xa9059cbb 对应哪个函数?参数 0x5f5e100 转换成十进制是多少 USDC?

🔬 实验 2:查看合约字节码大小

编译我们的合约,观察字节码长度。字节码越长,部署 Gas 越高。

chainlab lab sol compile SimpleToken.sol

思考:runtime bytecode 和 creation bytecode 有什么区别?

❓ 自测

检验你的理解

Q1: EVM 的栈元素大小是多少?

✅ 正确!EVM 的每个栈元素固定为 256 bit (32 字节)。这也是为什么 Solidity 的原生整数类型是 uint256。
❌ 提示:EVM 被设计为与 Keccak-256 哈希对齐。

Q2: DELEGATECALL 与 CALL 的关键区别是?

✅ 正确!DELEGATECALL 在调用者的上下文中运行目标代码 —— msg.sender、msg.value、storage 都是调用者的。这就是代理合约 (Proxy) 和可升级合约的核心原理。
❌ 关键区别在于执行上下文:DELEGATECALL 使用调用者的 storage 和 msg.sender。

📝 本课小结

核心要点 1. EVM 是基于栈的虚拟机,栈深 1024,每个元素 256 bit
2. 四个数据区域:Stack(免费)、Memory(便宜)、Storage(最贵)、Calldata(只读)
3. 函数选择器 = keccak256(函数签名) 的前 4 字节
4. CALL 在目标上下文执行,DELEGATECALL 在调用者上下文执行
5. 理解操作码 = 理解合约真正在做什么 = 安全审计的基础
上一课
Lesson 3: 存储槽
Module 1 完成!下一模块
Module 2: Gas 机制