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
常用操作码速查
算术 & 比较
| Opcode | Hex | 描述 | Gas |
| ADD | 01 | a + b | 3 |
| MUL | 02 | a × b | 5 |
| SUB | 03 | a − b | 3 |
| DIV | 04 | a ÷ b(整除) | 5 |
| MOD | 06 | a % b | 5 |
| LT / GT / EQ | 10/11/14 | 小于 / 大于 / 等于 | 3 |
| ISZERO | 15 | == 0 ? | 3 |
栈操作
| Opcode | Hex | 描述 | Gas |
| PUSH1~PUSH32 | 60~7f | 压入 1~32 字节常量 | 3 |
| POP | 50 | 弹出栈顶 | 2 |
| DUP1~DUP16 | 80~8f | 复制第 N 个栈元素到栈顶 | 3 |
| SWAP1~SWAP16 | 90~9f | 交换栈顶与第 N+1 个元素 | 3 |
环境 & 区块信息
| Opcode | 描述 | Gas |
| CALLER | msg.sender(调用者地址) | 2 |
| CALLVALUE | msg.value(发送的 ETH 数量) | 2 |
| TIMESTAMP | 当前区块时间戳 | 2 |
| NUMBER | 当前区块号 | 2 |
| BALANCE | 查询地址的 ETH 余额 | 2600 |
| ORIGIN | tx.origin(最初发送者) | 2 |
存储 & 内存 & 调用
| Opcode | 描述 | Gas |
| SLOAD | 读取 Storage slot | 2100 (cold) |
| SSTORE | 写入 Storage slot | 5000~22100 |
| MLOAD | 从 Memory 读 32B | 3 |
| MSTORE | 写 32B 到 Memory | 3 |
| 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 有什么区别?
❓ 自测
📝 本课小结
核心要点
1. EVM 是基于栈的虚拟机,栈深 1024,每个元素 256 bit
2. 四个数据区域:Stack(免费)、Memory(便宜)、Storage(最贵)、Calldata(只读)
3. 函数选择器 = keccak256(函数签名) 的前 4 字节
4. CALL 在目标上下文执行,DELEGATECALL 在调用者上下文执行
5. 理解操作码 = 理解合约真正在做什么 = 安全审计的基础
←
Module 1 完成!下一模块
Module 2: Gas 机制
→