译文出自:登链翻译计划[1] 译者:翻译小组[2] 校对:Tiny 熊[3]
在 EVM 中,总共有 5 种方式来结束智能合约的执行。我们将在这篇文章中详细研究它们。让我们现在就开始吧!
这是通过调试理解 EVM 系列的第 4 篇 ,在这里你可以找到之前和接下来的部分:
我们将使用 EVM 中最简单的操作码来开始。
这是唯一一个消耗 0Gas 的操作码,顾名思义,它结束智能合约的执行,不返回任何数据。
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
contract Test {
function test() external {
}
}
你可以拆解这个非常简单的智能合约来弄清楚发生了什么。(函数的执行从第 45 指令开始)
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 次跳转。内存中没有任何东西。没有数据被存储,堆栈只包含函数签名,因此没有数据被返回。
就这样简单。
RETURN 像 STOP 一样结束智能合约的执行,但与 STOP 不同,它也可能返回一些数据。我们将编译这个 solidity 代码:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
contract Test {
function test() external returns(uint) {
return(8)
}
}
并对该函数进行反汇编:
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)] 。
现在,我们来修改智能合约。
pragma solidity ^0.8.0;
contract Test {
function test() external returns(uint) {
revert("eight");
}
}
你发现区别了吗?我没有使用return() ,而是使用了revert() ,参数是一个字符串(我不能在 "revert" 中使用数字,solidity 编译器不允许我编译)。
如果你调用 test(),你应该看到一个错误,但调试仍然是可能的!
下面是 test 函数的反汇编:
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 之间:
因此,结果是:
最后在指令 123,EVM 以 80 作为起始偏移量,64 作为大小。(这与 RETURN 操作码的情况完全相同)。
这意味着返回数据位于 0x80 和 0xe4 之间。
不同的是,EVM 返回了更多关于回退的信息,不仅仅是我们可能猜到的文本 "eight",还有另外 3 个参数:
那么这 3 个未知的值是什么?
基本上,这只是意味着 revert 向区块链返回函数 Error(20,5, "eight")。
在深入研究这个操作码之前,让我们先回答一个问题。
一个智能合约的大小是多少?
它可以在1 字节和 24.576Kb 之间,。
智能合约只由操作码组成(比如我们已经知道的PUSH, POP, DUP, SSTORE),这些操作码被直接翻译成二进制。
每条没有参数的指令需要 1 个字节的内存。例如:
一些有参数的指令可以占用 2 个或更多的字节
而合约的字节码仅仅是所有指令字节码的连接。
但是一个问题出现了。有 16*16=256 个不同的操作码(00 到 FF)的组合,但只有一部分被分配。(大约有 145 个没有被分配。)
已分配的操作码
这些未分配的操作码被称为:INVALID 操作码。
通常情况下,如果你用 solidity 将你的智能合约编译成 EVM 的字节码,除非在编译过程中出现错误,否则不应该有可访问的无效操作码。
但是如果 EVM(通过任何方式)落入一个无效的操作码,它就会自动回退!这就是 EVM。
但实际上,有一种可能性是,一些无效的操作码存在于智能合约中,特别是在最后,但这段代码是不可触及的,这意味着无论向智能合约发送什么交易,EVM 都不会读取最后的代码,之前总会有一个 JUMP。
在第 54 指令的 JUMP 之后,截图中没有代码可以执行。
但为什么在第 54 指令后有一些代码?我们能不能把第 54 字节后的所有代码删除?
首先,这是编译后的智能合约的元数据的哈希值,但是哪个元数据?
当 Solidity 编译智能合约时,它会自动生成一个 JSON 文件,包含关于智能合约的所有数据。如果你进入 remix 的编译标签,点击编译细节和 "METADATA"(通常是列表中的第 2 个),你应该看到所有的元数据,其中包含:
这意味着,两个完全相同的智能合约,用相同的版本编译,可以有不同的字节码! (区别只存在于最后)
为什么 solidity 编译后要这样做呢?
根据 solidity 文档,它是用来访问 Swarm 中及 ipfs 中的合约的元数据,你可以在这里[11]了解更多。
第二个问题:你可以删除这块数据以节省 Gas 吗?
是的,你可以在remix中进行配置。你只需要制作一个交易,并在手动删除智能合约的最后这 52 个字节。
在合约部署时,每一个字节都要花费 200 个 Gas,因为元数据的 IPFS 哈希值是 52 个字节的长度,你可以通过禁用这个选项来节省 10400 个 Gas,这并不小(相比之下,一个简单的转移要花费 21000 个 Gas)。
你知道吗,可以通过调用一个操作码从区块链上删除一个智能合约?
以下是智能合约代码,我们将进行编译和测试。
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
contract Test {
function test() external {
selfdestruct(payable(0x0000000000000000000000000000000000000000));
}
}
在对 test()函数进行反汇编后,我们得到:
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 成本比简单的转移要便宜。
这一节相当简单,我想向你展示智能合约执行的所有可能的停止方式,以下是你学到的内容。
下次见 !
本翻译由 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