以太坊 Calldata 和字节码:EVM 如何知道要调用哪个函数

本文详细解释了以太坊calldata的工作原理,包括EVM如何通过函数选择器确定要调用的函数,如何计算4字节选择器,参数如何在32字节槽中编码,合约字节码的结构,以及calldata在EVM中的执行过程。文章通过实例展示了calldata的构成,包括函数选择器和ABI编码的参数,并解释了EVM如何解析和执行这些数据。

当你向智能合约发送交易时,你发送的不是纯文本的“命令”。 你发送的是一个精确的字节序列,calldata,它准确地告诉 EVM 要执行哪个函数以及使用哪些参数。 你通过 ethers.js、Foundry 或 MetaMask 发出的每个函数调用最终都会转换为这个 calldata。 它以一个 4 字节的函数选择器开始(从函数签名派生),后跟 ABI 编码的参数。 在这篇文章中,我们将逐步分解 calldata,你将学习: 1. EVM 如何确定要调用哪个函数 2. 如何计算 4 字节选择器 3. 参数如何以 32 字节槽进行编码 4. 合约字节码是什么样的 5. 当执行 calldata 时,EVM 内部会发生什么 最后,你将能够读取交易的 data 字段,并准确理解它在做什么——没有黑盒,只有字节。

协作

我目前对区块链、智能合约和全栈系统领域的合作和开发项目持开放态度,如果你正在构建有趣的东西,请随时在 LinkedIn 上联系

什么是 Calldata

在我们理解了 EVM 的堆栈(来自之前的文章)之后,让我们深入了解 calldatacalldata 是 EVM 中的一个只读数据位置,用于保存外部函数调用的输入参数。它随交易一起传递,并且在执行期间无法修改。 当一个交易被发送到智能合约时,交易中的 **data** 字段告诉合约要运行哪个函数以及使用哪些参数。 data 字段的前 4 个字节被称为 函数选择器。这个值告诉 EVM 要调用合约中的哪个函数。 (你可以在 4byte-directory 中搜索著名的选择器)

EVM 如何确定要调用哪个函数

函数选择器是函数签名的 Keccak-256 哈希的前 4 个字节。 例如:

function set(uint256 x)
  • 函数签名(作为字符串):"set(uint256)"
  • 哈希值:
    keccak256("set(uint256)")
    0x60fe47b1...
  • 前 4 个字节(8 个十六进制字符):0x60fe47b1
    → 这就是函数选择器

因此,如果你在 calldata 的开头看到 0x60fe47b1,你就知道这是一个对 set(uint256) 的调用。 示例交易 Calldata: 假设你使用值 69420 调用此函数。

set(69420)

Calldata 看起来会是这样:

0x60fe47b1
0000000000000000000000000000000000000000000000000000000000010f2c

分解:

  • 0x60fe47b1 → 函数选择器 (set(uint256))
  • 接下来的 32 个字节:0x...010f2c69420,编码为填充的 uint256

通用 Calldata 布局:

在你的收件箱中获取 Andrey Obruchkov 的故事

免费加入 Medium 以获取此作者的更新。 对于任何函数调用:

<4 bytes>    函数选择器(keccak256 的前 4 个字节)
<32 bytes>   参数 1(填充)
<32 bytes>   参数 2(填充)
...

需要了解的注意事项

  • 选择器必须完全匹配,否则调用将恢复为 fallback()receive()
  • Solidity 使用 ABI 编码,它是标准化的——多种工具(例如,ethers.js、Foundry)可以解码/编码它。
  • 当手动与合约交互时(例如,通过 eth_sendTransaction),你必须自己构建此 calldata。

Foundry 中的示例

cast calldata "set(uint256)" 69420
## 输出:
## 0x60fe47b1000000000000000000000000000000000000000000000000000000010f2c

示例:从源代码到字节码

当你用 Solidity 编写智能合约时,你真正创建的是一个高级蓝图,以太坊虚拟机 (EVM) 最终会将其作为称为 操作码 的低级指令执行。 让我们逐步了解 Solidity 代码是如何编译的以及 EVM 如何处理它。

pragma solidity >=0.4.16 <0.9.0;

contract MiniExample {
    uint data;
    function set(uint x) public {
        data = x;
    }
    function get() public view returns (uint) {
        return data;
    }
}

这很容易阅读,但 EVM 不理解 Solidity。它需要 字节码,它由 Solidity 编译器 (solc) 生成。编译过程产生:

  • 一个 .bin 文件,包含原始字节码
  • 一个 ABI 文件,它定义了与合约交互的接口

如果合约有一个构造函数,那么该逻辑会被捆绑到 部署字节码 中,它只运行一次。部署后,链上存储的内容称为 运行时字节码

字节码是什么样的?

上面合约的编译字节码如下所示

6080604052348015600e575f5ffd5b506101298061001c5f395ff3fe6080604052348015600e575f5ffd5b50600436106030575f3560e01c806360fe47b11460345780636d4ce63c14604c575b5f5ffd5b604a60048036038101906046919060a9565b6066565b005b6052606f565b604051605d919060dc565b60405180910390f35b805f8190555050565b5f5f54905090565b5f5ffd5b5f819050919050565b608b81607b565b81146094575f5ffd5b50565b5f8135905060a3816084565b92915050565b5f6020828403121560bb5760ba6077565b5b5f60c6848285016097565b91505092915050565b60d681607b565b82525050565b5f60208201905060ed5f83018460cf565b9291505056fea2646970667358221220ec163686bf86159ebb242a8ca38f68fe4e9bf9be12def4ec8af94737310b0c6364736f6c634300081e0033

这个十六进制字符串是合约逻辑的直接表示。EVM 将其解释为操作码列表。例如:

  • 60PUSH1
  • 80 → 将值 0x80 推送到堆栈上
  • 52MSTORE(将值存储在内存中)

所以前几个操作是:

[00]    PUSH1   80 // 将 1 字节的值 80 推送到堆栈上
[02]    PUSH1   40 // 将 1 字节的值 40 推送到堆栈上
[04]    MSTORE   // 内存存储
[05]    CALLVALUE   // 从调用中获取已存入的值
[06]    DUP1
[07]    ISZERO  // 一个条件操作码
[08]    PUSH1   0xR // 推送 2 字节
[0b]    JUMPI   // 跳转到堆栈上的另一个位置
...
...
[138]

每个操作码都是 EVM 执行的简单指令,程序计数器 (PC) 逐步执行列表中的每一项。 当你发送交易时会发生什么: 智能合约调用是通过 交易 发出的,其中包括以下字段:

{
  "to": "0x8a19ba...",
  "from": "0xf9db21...",
  "value": "0x0",
  "gasPrice": 700000,
  "gasLimit": 210000,
  "data": "0x60fe47b10000000000000000000000000000000000000000000000000000000000010f2c"
}

以下是 EVM 内部发生的情况:

  • 交易被接收和解码。
  • 使用 vrs 值验证签名
  • EVM 设置一个具有新堆栈内存上下文的隔离环境。
  • 它逐条指令地执行字节码,更新堆栈、内存,并且可能更新存储。
  • 如果执行成功完成,它会返回结果或更新状态。否则,它会回滚。

Calldata 在此流程中的工作方式: 让我们看一下交易中的 data 字段:

0x60fe47b10000000000000000000000000000000000000000000000000000000000010f2c
  • 前 4 个字节:0x60fe47b1 → 这是 函数选择器set(uint256) 的哈希值
  • 剩余的 32 个字节: → 编码的输入:0x...010f2c,等于十进制的 69420

此数据告诉 EVM:“调用 set 函数,值为 69420。” 在合约的字节码中,函数选择器被匹配,并在正确的代码段开始执行。

总结

理解 calldata 可以让你透视调用合约时真正发生的事情。 无论你是调试失败的交易还是构建自定义工具,这些知识都会将原始十六进制转换为可读的逻辑。

  • 原文链接: medium.com/@andrey_obruc...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Andrey Obruchkov
Andrey Obruchkov
江湖只有他的大名,没有他的介绍。