# 深入理解 EVM（三）

By [xyyme.eth](https://paragraph.com/@xyyme) · 2022-08-22

---

今天我们来聊聊调用合约方法在字节码层面是怎么实现的。同样地，我们以一个简单的合约作为例子：

编译合约
----

    // SPDX-License-Identifier: UNLICENSED
    pragma solidity 0.8.15;
    
    contract Demo {
        constructor() {}
    
        function func1() public {}
    
        function func2() public {}
    }
    

该合约中一共有两个方法，分别是 `func1` 与 `func2`。我们这里着重于理解方法调用的过程，因此简单起见就将方法内容置为空。

使用 `solc` 进行编译：

> solc Demo.sol --bin

得到的字节码为：

利用 `0xfe` 操作符分割后，得到的各部分分别为：

    6080604052348015600f57600080fd5b5060818061001e6000396000f3（init bytecode）
    
    6080604052348015600f57600080fd5b506004361060325760003560e01c806374135154146037578063b1ade4db14603f575b600080fd5b603d6047565b005b60456049565b005b565b56（runtime bytecode）
    
    a264697066735822122069532a763e3f61abcfbb422ae7dc4587126c8f8b2264e73bb837a5924ebb1d4964736f6c634300080f0033（metadata hash）
    

`init bytecode` 是合约部署部分，我们在[上篇文章](https://mirror.xyz/xyyme.eth/6vqE2DRsMzlPNmh3kYiwTdMBj-9hanmxyDuTHM7tZDU)中已经介绍过了。调用合约方法是与 `runtime bytecode` 这部分进行交互，我们主要来研究这一块。

我们知道，调用合约方法交易的 `data` 域就是合约签名与参数的拼接内容。具体来说，就是合约签名的 `keccak256` 哈希值的前 4 个字节再加上参数内容就构成了 `data` 部分。

在上面例子中，`func1` 与 `func2` 的哈希值前 4 个字节分别是 `0x74135154` 与 `0xb1ade4db`，同时由于这两个方法都没有参数，因此不用在后面拼接参数。如果 `fun1` 的方法签名为：

    function func1(uint _a) public {}
    

那么就需要添加参数，并且补齐 32 字节：

其中 `0x254e43db` 是 `func1(uint256)` 的哈希值前 4 个字节。

接下来我们就来看看在调用合约方法的过程中，在 EVM 字节码层面会发生什么。我们以调用 `func1` 方法为例，那么此时交易的 `data` 域内容就为 `0x74135154`。

我们先将 `runtime bytecode` 部分的字节码与其 opcodes 一一对应起来：

    0 -> 60 PUSH1
    1 -> 80 0x80
    2 -> 60 PUSH1
    3 -> 40 0x40
    4 -> 52 MSTORE
    5 -> 34 CALLVALUE
    6 -> 80 DUP1
    7 -> 15 ISZERO
    8 -> 60 PUSH1
    9 -> 0f 0xF
    10 -> 57 JUMPI
    11 -> 60 PUSH1
    12 -> 00 0x0
    13 -> 80 DUP1
    14 -> fd REVERT
    15 -> 5b JUMPDEST
    16 -> 50 POP
    17 -> 60 PUSH1
    18 -> 04 0x4
    19 -> 36 CALLDATASIZE
    20 -> 10 LT
    21 -> 60 PUSH1
    22 -> 32 0x32
    23 -> 57 JUMPI
    24 -> 60 PUSH1
    25 -> 00 0x0
    26 -> 35 CALLDATALOAD
    27 -> 60 PUSH1
    28 -> e0 0xE0
    29 -> 1c SHR
    30 -> 80 DUP1
    31 -> 63 PUSH4
    (32 ~ 35) -> 74135154 func1 方法签名哈希前四字节
    36 -> 14 EQ
    37 -> 60 PUSH1
    38 -> 37 0x37
    39 -> 57 JUMPI
    40 -> 80 DUP1
    41 -> 63 PUSH4
    (42 ~ 45) -> b1ade4db func2 方法签名哈希前四字节
    46 -> 14 EQ
    47 -> 60 PUSH1
    48 -> 3f 0x3F
    49 -> 57 JUMPI
    50 -> 5b JUMPDEST
    51 -> 60 PUSH1
    52 -> 00 0x0
    53 -> 80 DUP1
    54 -> fd REVERT
    55 -> 5b JUMPDEST
    56 -> 60 PUSH1
    57 -> 3d 0x3D
    58 -> 60 PUSH1
    59 -> 47 0x47
    60 -> 56 JUMP
    61 -> 5b JUMPDEST
    62 -> 00 STOP
    63 -> 5b JUMPDEST
    64 -> 60 PUSH1
    65 -> 45 0x45
    66 -> 60 PUSH1
    67 -> 49 0x49
    68 -> 56 JUMP
    69 -> 5b JUMPDEST
    70 -> 00 STOP
    71 -> 5b JUMPDEST
    72 -> 56 JUMP
    73 -> 5b JUMPDEST
    74 -> 56 JUMP
    

来看看这部分字节码都干了些什么。

调用合约方法流程
--------

0 - 4 行我们已经非常熟悉了，加载空闲内存指针。

5 - 14 行是校验 `msg.value` 必须为零，我们在上篇文章已经看到过这部分了。由于合约中没有 `payable` 方法，因此要求所有的合约调用的 `callvalue` 都要为零，否则会在 14 行`REVERT` 失败。

如果传入的 `value` 为零，则进入正常流程 15 行，在 16 行将栈中无用数据 pop 出。

17 - 18 行将 `0x4` 放入栈中，它代表正常的方法调用签名长度。此时栈为：

    | 4
    

19 行获取到 `data` 域的长度并放入栈中，`0x74135154` 的长度为 4 个字节。此时栈为：

    | 4 | 4
    

20 行从栈中获取两个数据，并判断第一个数字是否小于第二个数字，小于返回 1，否则返回 0。这里由于 4 = 4，因此返回 0。这部分是什么意思呢？上面我们说到第一次放入栈中的 4 代表正常的方法调用签名长度，也就是方法签名前四个字节的意思。无论我们调用了哪个方法，有没有参数，`data` 域的长度都至少应该是 4。如果某个交易的 `data` 域的长度小于 4，说明它并不是正常的方法调用，那么在接下来的流程中肯定就走不到正常的方法名匹配部分，要么交易失败，要么进入到 `fallback` 的调用流程中。我们这个例子中没有声明 `fallback` 方法，因此如果这时 `LT` 操作符返回 1（即 TRUE），交易就会失败。

在这里正常情况下，此时栈中为：

    | 0
    

21 - 22 行将 `0x32` 放入栈中：

    | 0 | 0x32
    

23 行 `JUMPI(0x32, 0)` 操作符根据栈中数据判断是否跳转。由于第二个参数是 0，因此不跳转，继续向下执行。这里假设第二个参数是 1，也就是上一步中的 `data` 域长度小于 4 字节，流程会跳转到 `0x32`，也就是 50 行，并最终在 54 行 `REVERT` 交易失败，验证了我们上面的说法。

24 - 25 行将 `0x0` 放入栈中：

    | 0
    

26 行加载 32 字节长度的 `data` 域放入栈中，开始位置从栈中获取，这里即为 `calldata[0]`，当前的 `data` 域内容为 `0x74135154`，不够 32 字节因此需要用 0 补齐。此时栈中为：

    | 7413515400000000000000000000000000000000000000000000000000000000
    

27 - 28 行将 `0xE0`，即 224 放入栈中：

    | 0x7413515400000000000000000000000000000000000000000000000000000000 | 0xE0
    

29 行 `SHR` 取出栈中数据，将 `7413515400000000000000000000000000000000000000000000000000000000` 右移 224 位，前者的长度是 256（即 64 \* 4） 位，右移之后变成 `0x74135154`，刚好是四个字节，这四个字节恰好是方法签名哈希值的前四个字节，是用来匹配合约中的方法的。此时栈为：

    | 0x74135154
    

30 行复制一份栈顶数据：

    | 0x74135154 | 0x74135154
    

31 - 35 将 `func1` 的方法签名哈希前四个字节放入栈中：

    | 0x74135154 | 0x74135154 | 0x74135154
    

36 行 `EQ` 判断栈顶两个元素是否相等，这里相等，因此返回 1:

    | 0x74135154 | 1
    

37 - 38 将 `0x37` 放入栈中：

    | 0x74135154 | 1 ｜ 0x37
    

39 行 `JUMPI(0x37, 1)` 判断是否跳转，此时会跳转到 `0x37`，即 55 行。栈为：

    | 0x74135154
    

这里如果第二个参数，也就是 `EQ` 的结果为 0，说明调用的方法与当前的方法不匹配，则不跳转，继续向下运行，寻找下一个方法签名进行匹配。

此时，我们已经找到了相匹配的合约方法，也就是说已经完成了匹配方法名的过程。接下来就是执行方法体内容了。

跳转到 55 行，56 - 59 将 `0x3D` 与 `0x47` 放入栈中：

    | 0x74135154 | 0x3D | 0x47
    

其中，`0x47`，即 71 是 `func1` 方法体所在的字节码开始位置。

60 行 `JUMP` 跳转到 71 行执行 `func1` 方法体，此时栈为：

    | 0x74135154 | 0x3D
    

由于 `func1` 内容为空，因此不需要执行什么操作。

72 行获取栈顶元素 `0x3D`，即 61，并跳转。

61 - 62 行这里会进行方法调用的结尾工作，通过 `STOP` 结束。

小结
--

这篇文章我们学习了合约方法调用在字节码层面的实现。我们通过一个简单的合约，了解了合约方法调用过程中，用户的请求是如何匹配到具体的方法的。其实内容也比较简单，就是在栈中与所有的方法签名哈希值一个一个进行比对，如果相同，则跳转到相应的部分执行对应方法内容。这里还是建议大家都亲手去 [evm.codes](https://www.evm.codes/playground) 这个网站去实际操作一下，感受整个流程中内存与栈内容的变化，会加深对整个流程的理解。

关于我
---

欢迎[和我交流](https://linktr.ee/xyymeeth)

参考
--

[

A deep-dive into Solidity - function selectors, encoding and state variables
----------------------------------------------------------------------------

In the last post, we have seen how the Solidity compiler creates code - the init bytecode - to prepare and deploy the actual bytecode executed at runtime. Today, we will look at a few standard patterns that we find when looking at this runtime bytecode.

http://leftasexercise.com

![](https://storage.googleapis.com/papyrus_images/10d90edfc0c38d43ffdf6c2314576bd9096bee4ff0da1bfd79139eacd83dd55b.webp)

](https://leftasexercise.com/2021/09/08/a-deep-dive-into-solidity-function-selectors-encoding-and-state-variables/)

[

EVM Deep Dives: The Path to Shadowy Super Coder 🥷 💻 - Part 1
--------------------------------------------------------------

Digging deep into the EVM mechanics during contract function calls

https://noxx.substack.com

![](https://storage.googleapis.com/papyrus_images/76799b336644e7786cc394c6e2467c03e6080db3551e9f668dd5ddf769518b68.jpg)

](https://noxx.substack.com/p/evm-deep-dives-the-path-to-shadowy?utm_source=%2Fprofile%2F80455042-noxx&utm_medium=reader2)

---

*Originally published on [xyyme.eth](https://paragraph.com/@xyyme/evm-3)*
