第一部分·前言
想象一下,你正在驾驶着1969年的Mustang Mach在美国西部的公路上快速行驶,阳光照耀在华丽的镀金轮辋上,整条道路只有你和沙漠,一望无际的地平线见证着你和落日的追逐… …
心旷神怡间,突然一身巨响,你335马力的快马被滚滚的白烟吞没,瞬间变成了蒸汽机车,于是你被迫停在路边。
你打算看看出现了什么问题,当你翻开前车盖,发现自己完全看不懂。你根本就不知道这该死的机器是怎么工作的,于是你拿起手机准备求救,发现附近没有信号… …
上面描述的情形是不是和你正在做的DApp开发很像?在开发Dapp的过程中,在类比中,豪车是你的智能合约,轮辋和改造过的地方是那些经过深思熟虑的小细节。而一旦出现问题,你就需要在智能合约EVM字节码中寻找答案,大部分情况下你都完全不知道发生了什么。
如果你是Dapp的开发者,并且遇到过上面的尴尬状况的话,那么以后再也不用担心了!
因为,本系列文章的目的就是解构一个简单的Solidity合约,查看其字节码,并将其分解为可识别的结构,直至最低级别。我们将打开Solidity这台跑车的引擎盖。在本系列的最后,您在查看或调试EVM字节码时应该感觉很舒服。本系列的重点是揭开Solidity编译器生成的EVM字节码的神秘面纱,它真的比它看起来简单得多。
以下是我们解构时要使用到的智能合约代码:
pragma solidity ^0.4.24;
contract BasicToken {
uint256 totalSupply_;
mapping(address => uint256) balances;
constructor(uint256 _initialSupply) public {
totalSupply_ = _initialSupply;
balances[msg.sender] = _initialSupply;
}
function totalSupply() public view returns (uint256) {
return totalSupply_;
}
function transfer(address _to, uint256 _value) public returns (bool) {
require(_to != address(0));
require(_value <= balances[msg.sender]);
balances[msg.sender] = balances[msg.sender] – _value;
balances[_to] = balances[_to] + _value;
return true;
}
function balanceOf(address _owner) public view returns (uint256) {
return balances[_owner];
}
}
注意:此合约容易受到溢出攻击,我们只是为了说明问题,所以力求简洁。
编译合约
为了编译合约,我们将使用Remix(地址:https://remix.ethereum.org)。
当你打开Remix编译器,单击文件浏览器区域上方左上角的+按钮,创建新的智能合约。将文件名设置为BasicToken.sol。创建好之后,将上面的代码粘贴到编辑器上。
在右侧,转到“ Setting”的选项中,确保选中了“ Enable Personal Mode”。另外,要注意选择的Solidity编译器版本是
“ version:0.4.24 +commit.e67f0147.Emscripten.clang ”。
这两个细节非常重要,否则你将无法查看文中讨论的字节码。
接下来,你可以进入Compile选项并单击Details按钮,你会看到一个弹出窗口,里面包含Solidity编译器生成的所有东西,其中一个是名为BYTECODE的JSON对象,它具有“object”属性,这个就是编译的合约代码,它的代码是这样的:
608060405234801561001057600080fd5b5060405160208061021783398101604090815290516000818155338152600160205291909120556101d1806100466000396000f3006080604052600436106100565763ffffffff7c010000000000000000000000000000000000000000000000000000000060003504166318160ddd811461005b57806370a0823114610082578063a9059cbb146100b0575b600080fd5b34801561006757600080fd5b506100706100f5565b60408051918252519081900360200190f35b34801561008e57600080fd5b5061007073ffffffffffffffffffffffffffffffffffffffff600435166100fb565b3480156100bc57600080fd5b506100e173ffffffffffffffffffffffffffffffffffffffff60043516602435610123565b604080519115158252519081900360200190f35b60005490565b73ffffffffffffffffffffffffffffffffffffffff1660009081526001602052604090205490565b600073ffffffffffffffffffffffffffffffffffffffff8316151561014757600080fd5b3360009081526001602052604090205482111561016357600080fd5b503360009081526001602081905260408083208054859003905573ffffffffffffffffffffffffffffffffffffffff85168352909120805483019055929150505600a165627a7a72305820a5d999f4459642872a29be93a490575d345e40fc91a7cccb2cf29c88bcdaf3be0029
没错,这些代码完全没有可读性(至少对于正常人而言)。
部署合约
接下来,转到Remix中的Run部分。首先,确保您使用的是Javascript VM。这基本上是一个嵌入式Javascript EVM +网络,理想的以太坊训练场。确保在ComboBox中选择了BasicToken ,并在Deploy输入框中输入数字10000 。接下来,单击“ Deploy”按钮进行部署。这部署的是我们创建的BasicToken智能合约实例,最初提供的10000个token由当前在ComboBox帐户顶部选择的帐户拥有,该帐户会保留我们设置的所有token供应。
在“ Run”选项卡的“Deployed Contracts”中,您可以看到已部署的智能合约,其中包含与合约进行交互的三个字段:transfer,balanceOf和totalSupply。在这里,我们能够与刚刚部署的智能合约实例进行交互。
但在此之前,让我们来看看合同的“Deploy”(部署)究竟是什么意思:
在页面底部的控制台区域中,你可以看到一条日志“creation of BasicToken pending …”,然后是一个包含各种字段的事务条目:from,to,value,data,logs和hash。单击此条目以展开交易信息,你应该看到transaction的date、input,以及我们上面提到的字节码。所以,创建一个智能合约实例,其中会包含自己的地址和代码。
我们将在下一篇文章中详细研究这transaction个过程。
反汇编字节码
在控制台中心,transaction框框的右侧,有个“ debug”(调试)按钮。点击这个按钮,你将激活Remix右侧区域中的Debugger选项。我们可以一起看看Instructions部分,如果向下滚动,应该是出现以下内容:
000 PUSH1 80
002 PUSH1 40
004 MSTORE
005 CALLVALUE
006 DUP1
007 ISZERO
008 PUSH2 0010
011 JUMPI
012 PUSH1 00
014 DUP1
015 REVERT
016 JUMPDEST
017 POP
018 PUSH1 40
020 MLOAD
021 PUSH1 20
023 DUP1
024 PUSH2 0217
027 DUP4
028 CODECOPY
029 DUP2
030 ADD
031 PUSH1 40
033 SWAP1
034 DUP2
035 MSTORE
036 SWAP1
037 MLOAD
038 PUSH1 00
040 DUP2
041 DUP2
042 SSTORE
043 CALLER
044 DUP2
045 MSTORE
046 PUSH1 01
048 PUSH1 20
050 MSTORE
051 SWAP2
…(缩写)
为了确保你前面的操作没有出错,你可以把你所操作的Remix编译器中看到的内容与上面进行比较。
这其实就是合约的反汇编字节码。如果您按字节扫描原始字节码(一次两个字符),则EVM会识别与特定操作关联的特定操作码。例如:
0x60 => PUSH
0x01 => ADD
0x02 => MUL
0x00 => STOP
…
反汇编的代码非常低级并且很难看懂,但是我们可以通过这种方式可以开始理解它。
Opcode
在解构智能合约代码开始之前,你将需要一个基本的工具集理解单个opcode,如PUSH,ADD,SWAP,DUP等的操作码,到最后,每个操作吗只能从EVM的堆栈,内存或属于合约的存储中压入一个项或消费一个项。
要查看EVM可以处理的所有可用操作码,可以查看Pyethereum,上面显示了操作码列表。要了解每个操作码的工作原理,Solidity官方的汇编文档也是一个很好的参考。即使它不是与原始操作码一一对应,但是非常接近(它实际上是Yul,介于Solidity和EVM字节码之间的中间语言)。如果你能读懂技术文档,可以阅读以太坊黄皮书,其实归根结底都是上面的内容。
虽然和大家推荐了这么多文档,现在从头到尾阅读这些资源没有什么意义,你只要记住有这么个资料就行,我们将在需要的时候使用到它们。
指令
上面反汇编代码中的每一行都是EVM执行的操作指令,每条指令都包含一个操作码,例如,让我们采用其中一条指令,指令88,将数字4推送到堆栈。这个特殊的反汇编程序解释说明如下:
88 PUSH1 0x04
| | |
| | Hex value for push.
| Opcode
Instruction number
尽管反汇编的代码能让我们更加理解底层的东西,但它还是让人摸不着头脑。我们需要一个能够解构所有问题的方法。
策略
任何一开始看上去不可能完成的任务,其实都可以通过不断的拆解,分解成可以解决的任务,我们遇到的问题也不例外,面对这个问题,我所采取的策略就是“分而治之”。
我们可以试图找到反汇编代码的分叉点,并逐渐分解,直到分解成很小的块,我们将在Remix的调试器中逐步完成。
在下图中,我们可以看到我们对反汇编代码进行的第一次拆分(我将在下一篇文章中对其进行全面分析)。
如果你不了解图表,也不要担心,你不用一开始就什么都会,我们的系列文章将会循序渐进的介绍。现在就跟着我们的节奏,不断深入你的豪车的内部结构吧。