By:Flush@查找币安全团队
导语:理解 EVM 是安全与开发的基石
在智能合约的世界里,以太坊虚拟机(EVM)及其算法与数据结构构成了最底层的“第一性原理”。我们编写的每一行 Solidity 代码,最终都会被编译并部署到 EVM 上执行。无论你是希望成为顶尖的 Solidity 开发者,还是立志于智能合约安全审计的专业人员,深入理解 EVM 的内部运行机制都是不可或缺的必修课。
本系列文章将基于对知名安全研究员 noxx 研究成果的深度解读,带领大家逐步揭开 EVM 的神秘面纱。本篇作为开篇,将聚焦于一个核心问题:当用户调用一个智能合约函数时,EVM 是如何从字节码中定位并执行对应函数的?
核心流程:Solidity → 字节码 → 操作码
在深入技术细节之前,我们先建立一个基础认知框架。智能合约代码(如 Solidity)在部署到以太坊网络之前,必须经过编译阶段,生成二进制字节码。EVM 本身并不理解 Solidity 语法,它只识别并执行这些字节码。而字节码又由一系列更底层的操作码(Opcode) 组成,每个操作码代表一个原子级的计算或存储操作。
编译后的字节码包含了整个合约的全部逻辑,包括多个可调用的函数。那么,EVM 是如何在茫茫字节码中,精准地找到用户想要调用的那个函数呢?答案就藏在合约字节码的函数选择器(Function Selector) 机制中。
实战分析:1_Storage.sol 的字节码与操作码
我们使用 Remix IDE 编译一个经典的 Storage 合约,作为分析样本。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Storage {
uint256 number;
function store(uint256 num) public {
number = num;
}
function retrieve() public view returns (uint256) {
return number;
}
}
该合约包含两个函数:store() 和 retrieve()。在 Remix 的编译面板中,我们可以查看完整的字节码。其中,有一段关键的字节码片段,正是 EVM 用于判断函数调用目标的选择器逻辑。
操作码解析
通过 Ethervm.io 我们可以查阅 EVM 操作码列表。一个操作码长度为 1 个字节(byte),理论上支持 256 种不同的指令,但 EVM 目前仅使用了其中的 140 个。当我们将上述字节码解析为操作码序列时,EVM 会按顺序在调用栈上执行这些指令。
函数调用的底层原理:ABI 编码与选择器
在深入操作码之前,我们需要理解智能合约函数调用是如何在底层传递的。用户发起一个交易调用合约函数时,calldata 中包含了编码后的函数签名和参数。Solidity 提供了多种 ABI 编码方式:
abi.encode(...):标准 ABI 编码abi.encodePacked(...):紧密打包编码abi.encodeWithSelector(bytes4 selector, ...):指定函数选择器编码abi.encodeWithSignature(string signature, ...):通过函数签名计算选择器并编码(等同于abi.encodeWithSelector(bytes4(keccak256(signature))))abi.encodeCall(function functionPointer, (...)):类型安全的调用编码
以调用 store(10) 为例,使用 abi.encodeWithSignature("store(uint256)", 10) 生成的编码数据如下:
0x6057361d000000000000000000000000000000000000000000000000000000000000000a
- 前 4 个字节
0x6057361d是store(uint256)的函数选择器(通过对函数签名进行 keccak256 哈希,取前 4 字节)。 - 后续 32 个字节是参数
10的 ABI 编码。
EVM 如何匹配选择器
当 EVM 收到这笔交易时,它会执行字节码中的选择器逻辑。本质上,这是一组简单的“if-else”语句:将 calldata 的前 4 个字节与合约中每个函数的 keccak256 签名前 4 字节逐一比对。匹配成功后,EVM 通过 JUMPI 操作码跳转到对应函数的字节码起始位置。
例如:
- 如果
calldata[0:4] == 0x6057361d,则跳转到store()函数代码块。 - 如果
calldata[0:4] == 0x2e64cec1,则跳转到retrieve()函数代码块。
交互式学习:EVM Playground 实操
为了更直观地理解上述过程,推荐使用 EVM Playground 进行交互式学习。
- 将目标合约的字节码粘贴到 Playground 中。
- 设置
calldata为0x6057361d000000000000000000000000000000000000000000000000000000000000000a。 - 点击“运行”,通过右上角的“单步调试”按钮,逐条执行操作码。
在调试过程中,你可以清晰观察到:
- 程序计数器(PC) 的移动:每条指令旁都有对应的偏移量。
- 栈的变化:
PUSH、DUP、JUMPI等操作如何影响栈顶数据。 - JUMPDEST 目标:
JUMPI之后,程序如何跳转到正确的函数入口。
尝试将 calldata 改为 retrieve() 的选择器 0x2e64cec1,观察执行路径的差异。这种交互式体验,能帮助你从“纸上谈兵”进阶为“眼见为实”。
总结与预告
通过本篇文章,我们揭示了 EVM 处理函数调用的核心机制:函数选择器 作为字节码中的“路由表”,通过简单的条件跳转,将执行流导向正确的函数代码块。这是 EVM 设计中最精妙也最基础的部分之一。
理解这一过程,对于智能合约开发者而言,意味着能更准确地控制合约行为;对于安全研究人员而言,则是识别重入攻击、选择器碰撞等漏洞的关键前提。
在下一篇文章《EVM 深入探讨 - Part 2》中,我们将继续深入,探索 合约内存(Memory) 的本质,以及它在 EVM 中的工作方式。敬请期待!
本文由查找币安全团队整理发布