前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >通过调试理解EVM(#4):结束/中止执行的5种指令

通过调试理解EVM(#4):结束/中止执行的5种指令

作者头像
Tiny熊
发布2023-01-09 17:19:09
8030
发布2023-01-09 17:19:09
举报

译文出自:登链翻译计划[1] 译者:翻译小组[2] 校对:Tiny 熊[3]

在 EVM 中,总共有 5 种方式来结束智能合约的执行。我们将在这篇文章中详细研究它们。让我们现在就开始吧!

这是通过调试理解 EVM 系列的第 4 篇 ,在这里你可以找到之前和接下来的部分:

  • 第 1 篇:理解汇编[4]
  • 第 2 篇:部署智能合约[5]
  • 第 3 篇:存储布局是如何工作的?[6]
  • 第 4 篇:结束/中止执行的 5 个指令[7]
  • 第 5 篇:执行流 if/else/for/函数[8]
  • 第 6 篇:完整的智能合约布局[9]
  • 第 7 篇:外部调用和合约部署[10]

1. STOP(停止)

我们将使用 EVM 中最简单的操作码来开始。

这是唯一一个消耗 0Gas 的操作码,顾名思义,它结束智能合约的执行,不返回任何数据。

代码语言:javascript
复制
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

contract Test {
    function test() external {

    }
}

你可以拆解这个非常简单的智能合约来弄清楚发生了什么。(函数的执行从第 45 指令开始)

代码语言:javascript
复制
045 JUMPDEST |function signature discarded|
046 PUSH1 33 |0x33|
048 PUSH1 35 |0x35|0x33|
050 JUMP     |0x33|053 JUMPDEST |0x33|
054 JUMP     ||051 JUMPDEST ||
052 STOP     ||

在结束时经过 2 次跳转。内存中没有任何东西。没有数据被存储,堆栈只包含函数签名,因此没有数据被返回。

就这样简单。

2. RETURN(返回)

RETURN 像 STOP 一样结束智能合约的执行,但与 STOP 不同,它也可能返回一些数据。我们将编译这个 solidity 代码:

代码语言:javascript
复制
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

contract Test {
    function test() external returns(uint) {
        return(8)
    }
}

并对该函数进行反汇编:

代码语言:javascript
复制
045 JUMPDEST ||
046 PUSH1 08 |0x08|  the return value of test()
048 PUSH1 40 |0x40|0x08|
050 MLOAD    |0x80|0x08|  mload(0x40) mloads the free memory pointer
051 SWAP1    |0x08|0x80|
052 DUP2     |0x80|0x08|0x80|
053 MSTORE   |0x80|       mstore(0x80,0x08) store the return value in memory[0x80]
054 PUSH1 20 |0x20|0x80|
056 ADD      |0xa0|
057 PUSH1 40 |0x40|0xa0|
059 MLOAD    |0x80|0xa0|
060 DUP1     |0x80|0x80|0xa0|
061 SWAP2    |0xa0|0x80|0x80|
062 SUB      |0x20|0x80|
063 SWAP1    |0x80|0x20|
064 RETURN   ||

在指令 45 和 50 之间,EVM mload(0x40) ,它返回 80。

在指令 51 和 53 之间, EVM mstore(0x80,0x08) ,80 是空闲内存地址,8 是 test 函数的返回值。

在指令 54 到 56 之间,EVM 在之前的结果(80)上加上 20,等于 a0(20=十进制的 32,因为这是一个内存插槽的大小,这里只有一个返回值)。

在指令 57 和 62 之间,它在 40 处重新加载内存(mload(0x40) ),并将结果与 0xa0(第 56 行的结果)相乘,即 0x20。

这里没有非常有趣的东西。在指令 64 时,内存 0x80 槽中有 0x08,栈中有 80 和 20。这 3 个值是什么意思?

根据文档的内容。当被调用时:

Stack(0) = 80 应包含返回数据在内存中的偏移量

Stack(1) = 20 应该包含返回数据的偏移后的大小。

这正是这个智能合约的情况,0x80 和 0xa0 之间的内存(=80+20 的十六进制)包含函数测试的返回值(8)。

所以智能合约返回内存[Stack(0):Stack(0)+Stack(1)]

3. REVERT 操作码(回退)

现在,我们来修改智能合约。

代码语言:javascript
复制
pragma solidity ^0.8.0;

contract Test {
    function test() external returns(uint) {
        revert("eight");
    }
}

你发现区别了吗?我没有使用return() ,而是使用了revert() ,参数是一个字符串(我不能在 "revert" 中使用数字,solidity 编译器不允许我编译)。

如果你调用 test(),你应该看到一个错误,但调试仍然是可能的!

下面是 test 函数的反汇编:

代码语言:javascript
复制
069 JUMPDEST          ||
070 PUSH1 40          |0x40|
072 MLOAD             |0x80|
073 PUSH3 461bcd      |0x461bcd|0x80|
077 PUSH1 e5          |0xe5|0x461bcd|0x80|
079 SHL               |0x08c379a000...000|0x80|  binary shift 197 times (e5 in hex), YES a binary shift can modify hex numbers...
080 DUP2              |0x80|0x08c379a000...000|0x80|
081 MSTORE            |0x80|
082 PUSH1 20          |0x20|0x80|
084 PUSH1 04          |0x04|0x20|0x80|
086 DUP3              |0x80|0x04|0x20|0x80|
087 ADD               |0x84|0x20|0x80|
088 MSTORE            |0x80|
089 PUSH1 05          |0x05|0x80|
091 PUSH1 24          |0x24|0x05|0x80|
093 DUP3              |0x80|0x24|0x05|0x80|
094 ADD               |0xa4|0x05|0x80|
095 MSTORE            |0x80|
096 PUSH5 195a59da1d  |0x195a59da1d|0x80|
102 PUSH1 da          |0xda|0x195a59da1d|0x80|
104 SHL               |0x00..195a59da1d..00|0x80|
105 PUSH1 44          |0x44|0x00..195a59da1d..00|0x80|
107 DUP3              |0x80|0x44|0x00..195a59da1d..00|0x80|
108 ADD               |0xc4|0x00..195a59da1d..00|0x80|
109 MSTORE            |0x80|
110 PUSH1 00          |0x00|0x80|
112 SWAP1             |0x80|0x00|
113 PUSH1 64          |0x64|0x80|0x00|
115 ADD               |0xe4|0x00|
116 PUSH1 40          |0x40|0xe4|0x00|
118 MLOAD             |0x80|0xe4|0x00|
119 DUP1              |0x80|0x80|0xe4|0x00|
120 SWAP2             |0xe4|0x80|0x80|0x00|
121 SUB               |0x64|0x80|0x00|
122 SWAP1             |0x80|0x64|0x00|
123 REVERT            |0x00|

和 RETURN 差不多,EVM 在内存中存储返回值,在堆栈中存储 2 个偏移。代码较长,但并不像看起来那么复杂。

在第 69 和 72 指令之间,空闲内存指针被检索出来,(mload(0x40) ,返回 0x80,所以我们下次可以在 0x80 处 mstore)。

之后在 73 和 81 指令之间,EVM 在内存中mstore(0x80, 0x08c379a000000000000000000000000000000000000000000000000000000000)

不要忘记,0x08c379a000...是通过二进制 0x461bcd 移 e5 次得到的

因此,内存[0x80:0x84]等于0x08c379a

在 82 和 88 指令之间,它也是这样做的。EVM 将 4 加到 80=84,然后 mstore(0x84 //80+4的结果,0x20) 在内存中,它添加了 0x04,因为它是内存中最后一个数据的大小,在 0x80 处,这个数据因此被存储在 0x08c379a0.... 后。

内存[0x84:0xa4]现在等于 0x20。

在字节 89 和 95 指令之间,EVM 通过使用与之前相同的方式将 0x05 存储在内存中 mstore(0xa4,0x05)

因此:内存[0xa4:0xc4] = 0x05。

在 96 和 104 指令之间,0x6569676874被推到堆栈中(并向左移位),所以0x6569676874000....0000在堆栈中。

如果我们把 6569676874 从十六进制转换为 ascii(文本),我们就可以找到 "eight"的字符串,这就是返回值。

在指令 105 和 109 之间:

  1. EVM 将 44 加到 80(空闲内存指针)= c4(在此内存插之前的槽被占用)
  2. 在内存执行 mstore(0xc4,0x6569676874000.000)

因此,结果是:

最后在指令 123,EVM 以 80 作为起始偏移量,64 作为大小。(这与 RETURN 操作码的情况完全相同)。

这意味着返回数据位于 0x80 和 0xe4 之间。

不同的是,EVM 返回了更多关于回退的信息,不仅仅是我们可能猜到的文本 "eight",还有另外 3 个参数:

那么这 3 个未知的值是什么?

  • 0x08c379a0 是 Error(string)函数的签名。每次有人在他的智能合约中使用带参数的 revert,当有一个错误时,其作为错误函数被返回。
  • 20
  • 5
  • 0x5569676874 是 eight 字符串

基本上,这只是意味着 revert 向区块链返回函数 Error(20,5, "eight")。

4. INVALID 无效操作码

在深入研究这个操作码之前,让我们先回答一个问题。

一个智能合约的大小是多少?

它可以在1 字节和 24.576Kb 之间,

智能合约只由操作码组成(比如我们已经知道的PUSH, POP, DUP, SSTORE),这些操作码被直接翻译成二进制。

每条没有参数的指令需要 1 个字节的内存。例如:

  • REVERT是 0xFD
  • SELFDESTRUCT是 0xFF。

一些有参数的指令可以占用 2 个或更多的字节

  • PUSH1 0x80是 6080(PUSH1 单独是 2 个第一字节:0x60 和 0x80 是指令的参数)。
  • DUP1 0x80是 8080
  • SWAP4 0xFFFFFFFF 是 93FFFFFFFF (仅 SWAP4 是 0x93)

而合约的字节码仅仅是所有指令字节码的连接。

但是一个问题出现了。有 16*16=256 个不同的操作码(00 到 FF)的组合,但只有一部分被分配。(大约有 145 个没有被分配。)

已分配的操作码

这些未分配的操作码被称为:INVALID 操作码。

通常情况下,如果你用 solidity 将你的智能合约编译成 EVM 的字节码,除非在编译过程中出现错误,否则不应该有可访问的无效操作码。

但是如果 EVM(通过任何方式)落入一个无效的操作码,它就会自动回退!这就是 EVM。

但实际上,有一种可能性是,一些无效的操作码存在于智能合约中,特别是在最后,但这段代码是不可触及的,这意味着无论向智能合约发送什么交易,EVM 都不会读取最后的代码,之前总会有一个 JUMP。

在第 54 指令的 JUMP 之后,截图中没有代码可以执行。

但为什么在第 54 指令后有一些代码?我们能不能把第 54 字节后的所有代码删除?

首先,这是编译后的智能合约的元数据的哈希值,但是哪个元数据?

当 Solidity 编译智能合约时,它会自动生成一个 JSON 文件,包含关于智能合约的所有数据。如果你进入 remix 的编译标签,点击编译细节和 "METADATA"(通常是列表中的第 2 个),你应该看到所有的元数据,其中包含:

  • 编译器版本(在我们的例子中是 0.8.7)。
  • 包含 Abi 的 "输出(output)"。
  • 编译设置(版本、优化器...)
  • 智能合约的路径

这意味着,两个完全相同的智能合约,用相同的版本编译,可以有不同的字节码! (区别只存在于最后)

为什么 solidity 编译后要这样做呢?

根据 solidity 文档,它是用来访问 Swarm 中及 ipfs 中的合约的元数据,你可以在这里[11]了解更多。

第二个问题:你可以删除这块数据以节省 Gas 吗?

是的,你可以在remix中进行配置。你只需要制作一个交易,并在手动删除智能合约的最后这 52 个字节。

在合约部署时,每一个字节都要花费 200 个 Gas,因为元数据的 IPFS 哈希值是 52 个字节的长度,你可以通过禁用这个选项来节省 10400 个 Gas,这并不小(相比之下,一个简单的转移要花费 21000 个 Gas)。

5. SELFDESTRUCT 操作码

你知道吗,可以通过调用一个操作码从区块链上删除一个智能合约?

以下是智能合约代码,我们将进行编译和测试。

代码语言:javascript
复制
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

contract Test {
    function test() external {
        selfdestruct(payable(0x0000000000000000000000000000000000000000));
    }
}

在对 test()函数进行反汇编后,我们得到:

代码语言:javascript
复制
53  JUMPDEST
54  PUSH1 0x00
56  PUSH20 0xffffffffffffffffffffffffffffffffffffffff
78  AND
79  SELFDESTRUCT

(在第 79 个指令之后,有前一节所讲的元数据的哈希值) (在这个例子中不需要显示堆栈)

0x0 和0xffffffffffffffffffffffffffffffffffffffff进行与运算,结果是 0x0,在指令 53 和 78 之间的堆栈中出现 0x0。

Stack(0)在第 78 指令后现在含有 0x00。在第 79 指令,SELFDESTRUCT指令被调用,参数为 Stack(0)(0 地址)。

但什么是SELDESTRUCT,为什么SELFDESTRUCT需要一个参数?

SELDESTRUCT 从区块链上删除智能合约。

如果销毁的智能合约包含一些 ETH,这些资金不能消失。因此,存储在智能合约中的所有资金将被发送到新的地址。这就是原因。

但是,一个问题出现了:如果新地址是一个智能合约,没有 receive 和 fallback 函数(或者 receive 功能回退了怎么办?),资金会去哪里?

答案很简单,在此案例中,以太坊会做一个例外:即使函数回退,智能合约仍然会得到资金!这意味着,在此案例中,智能合约有可能获得资金。

这意味着可以向智能合约发送 ETH 并强迫它接受资金。

如果一个智能合约的逻辑过于依赖 ETH 的余额,那么就会导致一个未定义的行为。这就是所谓的自毁安全漏洞。

最后一个问题,为什么使用这个操作码很有意思

如果你完成了一个智能合约,并且你不再需要它了。调用selfdestruct(address) 比让合约活着并手动转移资金要便宜。(例如使用转移、发送或调用)

这是因为selfdestruct(address) 释放了区块链的空间,所以 Gas 成本比简单的转移要便宜。

6. 结论

这一节相当简单,我想向你展示智能合约执行的所有可能的停止方式,以下是你学到的内容。

  • 5 条停止合约的指令。
  • 关于自毁的一些安全性。
  • 合约的元数据哈希值是什么?
  • REVERT 和 RETURN 的返回值

下次见 !


本翻译由 Duet Protocol[12] 赞助支持。

原文链接:https://trustchain.medium.com/reversing-and-debugging-evm-the-end-of-time-part-4-3eafe5b0511a

参考资料

[1]

登链翻译计划: https://github.com/lbc-team/Pioneer

[2]

翻译小组: https://learnblockchain.cn/people/412

[3]

Tiny 熊: https://learnblockchain.cn/people/15

[4]

第1篇:理解汇编: https://learnblockchain.cn/article/4913

[5]

第2篇:部署智能合约: https://learnblockchain.cn/article/4927

[6]

第3篇:存储布局是如何工作的?: https://learnblockchain.cn/article/4943

[7]

第4篇:结束/中止执行的5个指令: https://learnblockchain.cn/article/4965

[8]

第5篇:执行流 if/else/for/函数: https://medium.com/@TrustChain/reversing-and-debugging-evm-the-execution-flow-part-5-2ffc97ef0b77

[9]

第6篇:完整的智能合约布局: https://medium.com/@TrustChain/reversing-and-debugging-part-6-full-smart-contract-layout-f236c3121bd1

[10]

第7篇:外部调用和合约部署: https://medium.com/@TrustChain/reversing-and-debugging-theevm-part-7-2a20a44a555e

[11]

这里: https://learnblockchain.cn/docs/solidity/metadata.html

[12]

Duet Protocol: https://duet.finance/?utm_souce=learnblockchain

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-11-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 深入浅出区块链技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. STOP(停止)
  • 2. RETURN(返回)
  • 3. REVERT 操作码(回退)
  • 4. INVALID 无效操作码
  • 5. SELFDESTRUCT 操作码
  • 6. 结论
    • 参考资料
    相关产品与服务
    区块链
    云链聚未来,协同无边界。腾讯云区块链作为中国领先的区块链服务平台和技术提供商,致力于构建技术、数据、价值、产业互联互通的区块链基础设施,引领区块链底层技术及行业应用创新,助力传统产业转型升级,推动实体经济与数字经济深度融合。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档