上一篇中初步介绍了一下EVM的结构,这篇文章主要是介绍solidity高级语言编写的智能合约编译成字节码过程及EVM执行过程
在这之前,为什么理解EVM对我们来说很重要:
1、solidity高级语言编写的智能合约是如何在EVM上执行的
2、理解EVM是如何组织数据、存储和操作的
3、如何编写出更优Gas的智能合约
使用solidity开发出来的合约(人能够理解的)会被编译成上图中右则的字节码(它是一种用16进制表示的编码,是EVM能够理解的语言),EVM在执行的时候实际上是把字节码翻译成上图左边的带有操作指令的汇编码。在EOA在调用合约时,EVM按照解释出来的操作指令顺序执行,直到结束或者遇到错误后停止或者gas不足后中断。
对于人来说我们可以理解一条条的语句,对于EVM来说是怎么理解要执行的那串东西?我们知道EVM是一个虚拟机堆栈机,它是一个运算虚拟机器,只要把要执行的东西丢进去它就能给出对应的结果。
EVM之所以能理解那一串操作,是人为设置了一些预定义指令叫做操作码,有了这个操作码之后,EVM就知道把操作数放入哪些存储位置和如何操作了。
使用1个字节定义了最多256个操作指令,具体可以参考
-
EIP-150 操作码 Gas 成本
定义的操作指令及花费情况
或者到 https://www.evm.codes/?fork=shanghai这里看
接下来通过一个简单智能合约来理解如何从高级语言到字节码再到解析成操作码的过程
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Test{
uint256 i;
function setI() external {
i = 1;
}
}
我这里使用的是remix,可能通过 下面的“编译详情”或者“Bytecode”复制字节码
6080604052348015600e575f80fd5b50606a80601a5f395ff3fe6080604052348015600e575f80fd5b50600436106026575f3560e01c80636322ce3814602a575b5f80fd5b603260015f55565b00fea2646970667358221220d94b4a3dd0484e9634e33c01d93beb7b61eebac191ef0cb1e31f75ce427b1e4a64736f6c63430008180033
也可以使用命令行的方式来编译
MACOS安装地址:https://docs.soliditylang.org/en/latest/installing-solidity.html#macos-packages
brew update
brew upgrade
brew tap ethereum/ethereum
brew install solidity
安装完后使用的版本是最新的 0.8.24
solc命令的参数超多,我这里就不贴出来了
solc --help
solc --bin --asm Test.sol
======= contracts/Test.sol:Test =======
EVM assembly:
/* "contracts/Test.sol":70:154 contract Test{... */
mstore(0x40, 0x80)
callvalue
dup1
iszero
tag_1
jumpi
0x00
dup1
revert
tag_1:
pop
dataSize(sub_0)
dup1
dataOffset(sub_0)
0x00
codecopy
0x00
return
stop
sub_0: assembly {
/* "contracts/Test.sol":70:154 contract Test{... */
mstore(0x40, 0x80)
callvalue
dup1
iszero
tag_1
jumpi
0x00
dup1
revert
tag_1:
pop
jumpi(tag_2, lt(calldatasize, 0x04))
shr(0xe0, calldataload(0x00))
dup1
0x6322ce38
eq
tag_3
jumpi
tag_2:
0x00
dup1
revert
/* "contracts/Test.sol":105:152 function setI() external {... */
tag_3:
tag_4
tag_5
jump // in
tag_4:
stop
tag_5:
/* "contracts/Test.sol":144:145 1 */
0x01
/* "contracts/Test.sol":140:141 i */
0x00
/* "contracts/Test.sol":140:145 i = 1 */
dup2
swap1
sstore
pop
/* "contracts/Test.sol":105:152 function setI() external {... */
jump // out
auxdata: 0xa26469706673582212201305b3c86700efce9739e3838b2e286fa6189828eccafda7c7db4c84f32e965564736f6c63430008180033
}
Binary:
6080604052348015600e575f80fd5b50607180601a5f395ff3fe6080604052348015600e575f80fd5b50600436106026575f3560e01c80636322ce3814602a575b5f80fd5b60306032565b005b60015f8190555056fea26469706673582212201305b3c86700efce9739e3838b2e286fa6189828eccafda7c7db4c84f32e965564736f6c63430008180033
上面那部份我加入了--asm参数,就是让它生成 汇编码方看它是怎么解释的
最后那一段 Binary 实际就是字节码
最后通过graph图来看一下字节码最后的指令结构图
https://www.evm.codes/playground?fork=shanghai
也提供了方便的操作
我们先看一下字节码前面的 60 80 60 40 实际对应的就是opcodes里的
push1 80 push1 40
60或PUSH1操作码告诉我们合约将1 字节大小的值添加到堆栈中。(当您添加2字节大小的值时,需要使用PUSH2或61)
[ 60 80 ]
在第一个操作中,我们使用操作码60 或 PUSH1将值 80 加载到堆栈中。
[ 60 40 ]
在第二个操作中,我们使用代码60 或 PUSH1将值 40 加载到堆栈中。
我们可以 evm.codes 中尝试执行一个代码
点击右上角的 step into时,把80推入了堆顶
再点一次是把40推到栈顶
执行完 MSTORE指令后
上面三个步实际是执行了操作指令MSTORE(0X40,0X80),这个指令的作用就是偏移64个字节的地方保存值80(16进制值)
计算了一下刚好是 192位,每2位代表的是1个字节,就是96个字节(偏移64个字节+80的存的是32个字节为一个slot =96)
现在回头看一下我们之前写的合约
i = 1;
字节码 60015f5556 对应的操作码指令
tag_5:
/* "contracts/Test.sol":144:145 1 */
0x01
/* "contracts/Test.sol":140:141 i */
0x00
/* "contracts/Test.sol":140:145 i = 1 */
dup2
swap1
sstore
pop
/* "contracts/Test.sol":105:152 function setI() external {... */ jump // out
运行一下,首先把01压进栈
然后把0压进栈顶
最后把01值存储进STORAGE
多出两个东西,一个是slot,这个是槽的位置,上面的contract暂时不解,后面理解了再补回来
总结:
1、我们通过solidity编写智能合约后,最终会被编绎成字节码,字节码在编译的过程已经加入了EVM定义的操作码;
2、操作码定义占了一个字节,总共支持256个指令,实际上目前只用到了一部份
3、EVM在执行的指令时,通过指令的意义把操作数存入对应的存储类型中