---
title: "Yul 合约调用是如何工作的？"
description: "了解 Yul 智能合约编程语言中合约调用的工作原理"
---

# Yul 合约调用是如何工作的？

[Yul](http://www.alchemy.com/overviews/what-is-yul) 是一种中间编程语言，可以用来在[智能合约](https://www.alchemy.com/overviews/solidity-smart-contract)中编写一种形式的汇编语言。现在你已经了解了 Yul 中的语法、存储和内存的工作方式，是时候来学习如何调用合约了！

## **合约调用在 Yul 中是如何工作的？**

在本系列的最后一部分，我们将学习合约调用在 Yul 中是如何工作的。在深入示例之前，我们需要先学习一些新的 Yul 操作。来看看吧。

<EmbeddedTable
  table={{
    columns: [
      { key: "1", width: 200, title: "Instruction", dataType: "object" },
      { key: "2", width: 200, title: "Explanation", dataType: "object" },
    ],
    data: [
      {
        "1": { title: "<p>gas()</p>", tooltip: "", icon: "" },
        "2": { title: "<p>剩余可用于执行的 gas 数量</p>", tooltip: "", icon: "" },
        id: 0,
      },
      {
        "1": { title: "<p>gasPrice()</p>", tooltip: "", icon: "" },
        "2": { title: "<p>交易的 gas 价格</p>", tooltip: "", icon: "" },
        id: 1,
      },
      {
        "1": { title: "<p>address()</p>", tooltip: "", icon: "" },
        "2": { title: "<p>当前合约的地址</p>", tooltip: "", icon: "" },
        id: 2,
      },
      {
        "1": { title: "<p>balance(a)</p>", tooltip: "", icon: "" },
        "2": { title: "<p>地址 a 的以太币余额（以 wei 为单位）</p>", tooltip: "", icon: "" },
        id: 3,
      },
      {
        "1": { title: "<p>selfbalance()</p>", tooltip: "", icon: "" },
        "2": { title: "<p>与调用 balance(address()) 相同，但略微省 gas</p>", tooltip: "", icon: "" },
        id: 4,
      },
      {
        "1": { title: "<p>caller()</p>", tooltip: "", icon: "" },
        "2": { title: "<p>调用该合约的地址（msg.sender）</p>", tooltip: "", icon: "" },
        id: 5,
      },
      {
        "1": { title: "<p>origin()</p>", tooltip: "", icon: "" },
        "2": { title: "<p>发起该交易的地址（tx.origin）</p>", tooltip: "", icon: "" },
        id: 6,
      },
      {
        "1": { title: "<p>callvalue()</p>", tooltip: "", icon: "" },
        "2": { title: "<p>该合约调用中发送的以太币数量（以 wei 为单位）（msg.value）</p>", tooltip: "", icon: "" },
        id: 7,
      },
      {
        "1": { title: "<p>calldataload(p)</p>", tooltip: "", icon: "" },
        "2": { title: "<p>从位置 p 开始的 calldata（仅 32 字节数据）</p>", tooltip: "", icon: "" },
        id: 8,
      },
      {
        "1": { title: "<p>calldatacopy(t, f, s)</p>", tooltip: "", icon: "" },
        "2": { title: "<p>将 calldata 复制到内存位置 t，从 calldata 中位置 f 开始，复制 s 字节的数据。</p>", tooltip: "", icon: "" },
        id: 9,
      },
      {
        "1": { title: "<p>extcodesize(a)</p>", tooltip: "", icon: "" },
        "2": { title: "<p>地址 a 处代码的大小</p>", tooltip: "", icon: "" },
        id: 10,
      },
      {
        "1": { title: "<p>returndatasize()</p>", tooltip: "", icon: "" },
        "2": { title: "<p>上一次 returndata 的大小</p>", tooltip: "", icon: "" },
        id: 11,
      },
      {
        "1": { title: "<p>returndatacopy(t, f, s)</p>", tooltip: "", icon: "" },
        "2": { title: "<p>从 return data 位置 f 开始，将 s 字节数据复制到内存位置 t</p>", tooltip: "", icon: "" },
        id: 12,
      },
      {
        "1": { title: "<p>timestamp()</p>", tooltip: "", icon: "" },
        "2": { title: "<p>当前区块的时间戳（以秒为单位）</p>", tooltip: "", icon: "" },
        id: 13,
      },
      {
        "1": { title: "<p>number()</p>", tooltip: "", icon: "" },
        "2": { title: "<p>当前区块号</p>", tooltip: "", icon: "" },
        id: 14,
      },
      {
        "1": { title: "<p>call(g, a, v, in, insize, out, outsize)</p>", tooltip: "", icon: "" },
        "2": { title: "<p>以 g 数量的 gas 调用地址 a 处的合约，将 v wei 作为 msg.value 传入，将内存位置 in - insize 处的数据作为 tx.data 传入，并将返回数据存储在内存位置 out - outsize 处。调用成功返回 1，否则返回 0。</p>", tooltip: "", icon: "" },
        id: 15,
      },
      {
        "1": { title: "<p>delegatecall(g, a, in, insize, out, outsize)</p>", tooltip: "", icon: "" },
        "2": { title: "<p>与 call() 类似。主要区别在于 delegatecall() 会更新调用方合约中的状态变量。常用于代理合约。需要注意的是，存储布局必须与被调用的合约完全一致，以避免覆盖不该覆盖的存储变量。注意参数 v 不存在。使用 delegatecall() 无法发送以太币。</p>", tooltip: "", icon: "" },
        id: 16,
      },
      {
        "1": { title: "<p>staticcall(g, a, in, insize, out, outsize)</p>", tooltip: "", icon: "" },
        "2": { title: "<p>与 call() 类似。区别在于它不能用于调用会改变区块链状态的合约（即标记为 pure 和 view 的函数）。注意参数 v 不存在。使用 staticcall() 无法发送以太币。</p>", tooltip: "", icon: "" },
        id: 17,
      },
    ],
  }}
/>

好了，现在让我们看看这些示例中要用到的一些新合约。首先，看看我们将要调用的合约。

这个合约有两个存储变量 var1 和 var2，分别存储在存储槽 1 和 2 中。

函数 a\(\) 要求用户向合约发送至少 1 个以太币，否则会 revert。

接下来，函数 a\(\) 会更新 var1 和 var2 并返回它们。

函数 b\(\) 只是简单地读取 var1 和 var2 并返回它们。

在讲解调用 CallMe 合约的合约之前，我们需要花点时间理解函数选择器（function selector）。

来看看下面这笔交易的 calldata `0x773d45e000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002`。

calldata 的前 4 个字节就是所谓的函数选择器（0x773d45e0）。这是 EVM 用来判断你想调用哪个函数的方式。我们通过对函数签名字符串取哈希，取其前 4 个字节来得到函数选择器。

那么函数 a\(\) 的签名就是 a\(uint256,uint256\)。

对这个字符串取哈希得到：`0x773d45e097aa76a22159880d254a5f1db8365bc2d0f0987a82bda7dfd3b9c8aa`。

查看前 4 个字节，我们发现它等于 0x773d45e0。

注意签名中没有空格。这一点很重要，因为加上空格会得到完全不同的哈希值。你不需要担心我们代码示例中的选择器如何获取，我会提供它们。

我们先来看一下存储布局。

注意 var1 和 var2 的布局与 CallMe 合约相同。你可能还记得我说过，为了让 delegatecall\(\) 正常工作，布局必须与另一个合约保持一致。

我们满足了这一要求，同时还能拥有其他变量（selectorA 和 selectorB），只要新变量被追加在末尾即可。这样可以避免存储冲突。

现在我们可以进行第一次合约调用了。

### **如何在 Yul 中使用 staticcall\(\)**

先从简单的开始，staticcall\(\)。这是我们的函数：

**这里发生的事情是：**

- 通过加载存储槽 2 从存储中取出 b\(\) 的函数选择器（两个选择器都被打包进同一个槽）
- 右移 4 字节（32 位）以分离出 selectorB
- 将函数选择器存入内存的暂存空间（scratch space）
- 进行 static call
- 传入 gas\(\)（你也可以指定 gas 数量）
- 传入参数 \_callMe 作为合约地址
- 检查函数调用是否成功，否则我们不带数据返回
- 从内存中返回数据，可以看到值 1 和 2

**注意：** 0x1c 和 0x20 表示我们想要传递之前存入暂存空间的数据的最后 4 个字节。staticcall\(\) 的最后两个参数指定我们想将返回数据存储在内存位置 0x80 - 0xc0。

### **如何在 Yul 中使用 call\(\)**

接下来看看 call\(\)。我们将调用 CallMe 中的函数 a\(\)。记得要向合约发送至少 1 个以太币！在这个示例中，我会传入 3 和 4 作为 \_var1 和 \_var2。

**代码如下：**

好的，与上一个示例类似，我们需要加载存储槽 2。不过这次，我们要通过掩码操作屏蔽掉 selectorB 以分离出 selectorA。

现在我们将选择器存储在 0x80。

由于我们需要来自 calldata 的参数，我们将使用 calldatacopy\(\)。我们告诉 calldatacopy\(\)：

- 将 calldata 存储在内存位置 0xa0
- 跳过前 36 个字节
- 存储 calldata 大小减去 36 字节的数据

**注意：** 我们跳过的前 4 个字节是 callA\(\) 的函数选择器，接下来的 32 个字节是 callMe 的地址。

现在我们可以进行合约调用了！

和上次一样，我们传入 gas\(\) 和 \_callMe。不过这次我们传入从 0x9c（0x80 内存序列的最后 4 个字节）到 0xe0 的 call data，并将数据存储在内存位置 0x100 - 0x120。

同样地，我们检查调用是否成功，然后返回输出结果。如果我们查看 CallMe 合约，会发现值已成功更新为 3 和 4。

为了进一步说明发生了什么，这是返回之前的内存布局：

<EmbeddedTable
  table={{
    columns: [
      { key: "1", width: 200, title: "Memory Location", dataType: "object" },
      { key: "2", width: 200, title: "Value Stored", dataType: "object" },
    ],
    data: [
      {
        "1": { title: "<p>0x00</p>", tooltip: "", icon: "" },
        "2": { title: "<p>暂存空间（空）</p>", tooltip: "", icon: "" },
        id: 0,
      },
      {
        "1": { title: "<p>0x20</p>", tooltip: "", icon: "" },
        "2": { title: "<p>暂存空间（空）</p>", tooltip: "", icon: "" },
        id: 1,
      },
      {
        "1": { title: "<p>0x40</p>", tooltip: "", icon: "" },
        "2": { title: "<p>0x80（我们从未更新过空闲内存指针，因为所有操作在 Yul 中都是手动完成的。）</p>", tooltip: "", icon: "" },
        id: 2,
      },
      {
        "1": { title: "<p>0x60</p>", tooltip: "", icon: "" },
        "2": { title: "<p>空</p>", tooltip: "", icon: "" },
        id: 3,
      },
      {
        "1": { title: "<p>0x80</p>", tooltip: "", icon: "" },
        "2": { title: "<p>0x0000000000000000000000000000000000000000000000000000000000773d45e0（函数选择器）</p>", tooltip: "", icon: "" },
        id: 4,
      },
      {
        "1": { title: "<p>0xa0</p>", tooltip: "", icon: "" },
        "2": { title: "<p>0x0000000000000000000000000000000000000000000000000000000000000003（_var1）</p>", tooltip: "", icon: "" },
        id: 5,
      },
      {
        "1": { title: "<p>0xc0</p>", tooltip: "", icon: "" },
        "2": { title: "<p>0x0000000000000000000000000000000000000000000000000000000000000004（_var2）</p>", tooltip: "", icon: "" },
        id: 6,
      },
      {
        "1": { title: "<p>0xe0</p>", tooltip: "", icon: "" },
        "2": { title: "<p>空</p>", tooltip: "", icon: "" },
        id: 7,
      },
      {
        "1": { title: "<p>0x100</p>", tooltip: "", icon: "" },
        "2": { title: "<p>0x0000000000000000000000000000000000000000000000000000000000000003（第一个返回值）</p>", tooltip: "", icon: "" },
        id: 8,
      },
      {
        "1": { title: "<p>0x120</p>", tooltip: "", icon: "" },
        "2": { title: "<p>0x0000000000000000000000000000000000000000000000000000000000000004（第二个返回值）</p>", tooltip: "", icon: "" },
        id: 9,
      },
      {
        "1": { title: "<p>0x140</p>", tooltip: "", icon: "" },
        "2": { title: "<p>空闲内存指针（空）</p>", tooltip: "", icon: "" },
        id: 10,
      },
    ],
  }}
/>

### **如何在 Yul 中使用 delegate call**

在最后一部分，我们将学习 delegatecall\(\)。这段代码看起来与 call\(\) 几乎相同，只有一处改动。

我们唯一做的改动是将 call\(\) 换成了 delegatecall\(\)，并移除了 callvalue\(\)。

我们不需要 callvalue\(\)，因为 delegate call 会在自己的状态中执行来自 CallMe 的代码。因此 a\(\) 中的 require\(\) 语句检查的是是否有以太币发送到我们的 Caller 合约。如果我们查看 CallMe 中的 var1 和 var2，会发现没有任何变化。然而，Caller 合约中的 var1 和 var2 已成功更新。

这就是关于合约调用部分的全部内容！接下来，了解 [Memory](http://www.alchemy.com/overviews/yul-memory)、[Storage](http://www.alchemy.com/overviews/yul-storage) 和 [Smart Contract Calls](http://www.alchemy.com/overviews/yul-contract-calls) 在 Yul 中是如何工作的。
