Tattoo
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明
原文链接:
https://blog.csdn.net/Programmer_CJC/article/details/80217672
https://blog.csdn.net/Programmer_CJC/article/details/80218649
https://blog.csdn.net/Programmer_CJC/article/details/80219720
最近在学习solidity方面的知识,看到这几篇博文主要介绍evm bytecode的,通俗易懂,故转载分享。
在解释EVM是如何执行之前,先来解释一下BasicBlock(基本块)。一个基本块由一系列的指令构成,有一个入口和一个出口,入口就是第一个指令,出口就是最后一个指令。
出口的类型有:
条件跳转(JUMPI)
非条件跳转(JUMP)
结束指令(RETURN,REVERT)
什么都没有,直接fall to下一个block
EVM中条件跳转的指令是JUMPI,它会从stack中读取2个元素,分别代表跳转的条件和pc(programmer counter)。下面是一个JUMPI的跳转例子:
Block1: JUMPDEST CALLVALUE ISZERO PUSH2 0x0100 JUMPI
Block2: PUSH1 0x00 DUP1 REVERT
Block1由5个指令构成(PUSH2 0x0100是一个), JUMPDEST表明这个Block是一个跳转的起始位置,CALLVALUE代表从transaction中获得的值,比如用户发送的ether额度,就可以用该指令获得。ISZERO判断CALLVALUE获得的值是否为0, PUSH指令向stack中放入了一个值。最后执行到JUMPI,它从条件中读取了两个值:
ISZERO(CALLVALUE)
0x0100
如果满足1,则跳转到0x0100指向的block, 否则继续执行下一个BasicBlock(Block2) (PS: 如果满足条件跳转之后,执行完跳转的Block会继续往下执行Block2,执行的方式是深度优先遍历的方式)。
EVM中的非条件跳转由JUMP指令触发, 每次执行到JUMP指令时,都会从stack读出1个值,表示要跳转的pc。和JUMPI指令类似,执行完跳转块后,也会继续向下执行,执行方式是深度优先遍历。
EVM的某些基本块没有跳转指令也没有结束指令,对于这些指令,执行完最后一个指令后会继续执行下一个指令。当然对于条件跳转来说,也会有fall to的情况。如在条件跳转中举的例子,在执行完Block1之后,会继续执行Block2。或者Block1的JUMPI跳转条件不满足,也会继续执行Block2。
该小节用一个具体的smart contract以及对应的指令来具体解释EVM bytecode的文件结构以及bytecode如何执行。
pragma solidity ^0.4.22;
contract Demo{
uint public value1 = 0;
uint public value2 = 0;
function A(uint v) public returns(uint){
value1 += v;
return value1;
}
function B(uint v) public{
value2 += A(v);
}
}
上面的智能合约来做例子,由于Bytecode过长就不上传,可以将该代码贴到 http://remix.ethereum.org/#optimize=false&version=soljson-v0.4.22+commit.4cb486ee.js ,直接点击右侧的Details来查看Bytecode:
下面开始解释一下Bytecode的结构:
从上面的图来看,Bytecode由两部分构成。第一部分的.code包含了一些smart contract初始化的代码,比如构造函数,state variable(全局变量)的赋值等操作。区块链上,这些都是EOA在部署合约时就执行完成的,在区块链浏览器,如Etherscan,都是无法看到这部分的代码的(某些开源合约会公开这部分的信息,默认是没有的)。
从.data开始,是smart contract的runtime bytecode,也就是在区块链上保存的合约的bytecode。想要获得该部分的bytecode,可以安装solidity( https://github.com/ethereum/solidity ),通过命令 solc —bin-runtime filePath获得。
Remix的结构有点不太一样,是由若干个tag组成的,每个tag由若干个基本块组成。以JUMPDEST或者结束指令(RETURN,REVERT,STOP)划分。.code部分是Bytecode的入口,这部分的指令包含了所有能够被外部调用的函数的函数签名和跳转pc(programmer counter)值。
上面的5个框分别是该合约的5个跳转函数。可能会奇怪合约就2个函数,为何会有5个可跳转函数。这5个跳转函数分别是:1. fallback(回退函数),2个public全局变量,2个public函数。
首先解释一下回退函数,在EVM中,回退函数是唯一一个未命名的函数,可以发现其他4个框前面都有一个函数签名,如第二个框的3033413B,只有fallback function没有。因此如果我们调用了一个合约中没有的函数,没有一个函数签名能满足,接下来的四个框都不会满足跳转条件,因此会通过fall to的形式执行tag 1,tag 1也就是fallback函数的开始位置。
接下来说一下什么是函数签名。函数签名是一个4byte的hash值,用来唯一标识smart contract中的函数。它是通过sha3(“functionName(type1, type2)”),取前4bytes得到的。也就是说该函数签名只与函数名,函数类型有关。
总结一下.code部分,该部分包含了合约能调用的所有函数的跳转地址,从上图中体现就是tag1-5. tag 1-5分别是5个函数的起始位置。
下面用函数B为例,解释一下EVM的bytecode是如何跳转的:
要调用函数B,首先EVM会接受到函数签名(DAC0EB07),在.code部分中,跳转到tag 5
tag 5是函数B的开始部分,tag 5中有一个JUMPI,假设跳转条件满足,EVM会跳转到tag 15,如果不满足条件,则会执行PUSH, DUP1, REVERT. REVERT是终止指令,程序终于。该部分通常是用来判断一个函数是否是payable的。比如CALLVALUE指令会得到transacation是否发了Ether,如果发了ether,ISZERO的结果就会是false,因此不会执行跳转
执行tag 15, 执行到最后有一个JUMP指令,会从EVM stack读出一个值, 上一个push到stack的值是tag 17,因此跳转到tag 17
执行tag 17,同tag 15,tag17最后的tag 15会使pc跳转到tag14(tag 14也就是函数A的函数体部分)
执行tag 14,执行到最后有一个JUMP指令,这时JUMP指令读到的是tag 17中push的tag 20
执行tag 20, tag20最后的JUMP指令,执行的是tag15中的push tag 16, 因此会跳转到tag 16
执行tag 16,执行到stop指令,程序终止
以函数A为例:
要调用函数A,首先EVM会接收到函数签名(A17A9E66),在.code部分中,跳转到tag 4
tag 4是函数A的开始部分,假设满足JUMPI的跳转条件,则跳转到tag 12,如果不满足,则继续执行下面的三个指令
tag 12代表函数读取参数的过程,函数B没有参数因此没有这一部分。最后由JUMP指令跳转到tag 14
执行tag 14,最后的JUMP读取到的是tag 12中的PUSH tag 13
执行tag 13, tag 13最后的终止指令是RETURN,代表函数执行结束并返回值
首先需要安装solc和evm:
编译一个smart contract可以通过指令来得到bytecode:
solc --bin-runtime filepath
反编译bytecode可以通过:
evm --dissam bytecodeFilePath
反编译以后的文件如下:
前面的数字就是pc(programmer counter), 以20行的指令为例,0x008d代表21行的JUMPI跳转的pc值是141.
solc还有下面几个非常好用的指令,可以获得合约的ast,asm(汇编码),opcode,bin,abi,函数签名等: