字符串,字节32,字节[],字节之间有什么区别?
EVM如何存储映射?
编译的合同如何看待EVM?
我认为了解像Solidity这样的高级语言如何在以太坊VM(EVM)上运行是一项很好的投资。 由于几个原因。
在一系列文章中,我想解构简单的Solidity合约,以便了解它如何用作EVM字节码。
我希望学习和写作的概述:
我的最终目标是能够理解整个编译的Solidity合同。 首先阅读一些基本的EVM字节码!
此EVM指令集表格将是一个有用的参考。
我们的第一份合同有一个构造函数和一个状态变量:
// c1.sol 杂注扎实0.4.11;
合同C { uint256 a;
函数C(){ a = 1; } }
用solc
编译这个合同:
$ solc --bin --asm c1.sol
======= c1.sol:C ======= EVM组件: / *“c1.sol”:26:94合约C {... * / mstore(0x40,0x60) / *“c1.sol”:59:92 function C(){... * / jumpi(tag_1,iszero(callvalue)) 为0x0 DUP1 还原 TAG_1: TAG_2: / *“c1.sol”:84:85 1 * / 为0x1 / *“c1.sol”:80:81 a * / 为0x0 / *“c1.sol”:80:85 a = 1 * / DUP2 swap1 sstore 流行的 / *“c1.sol”:59:92 function C(){... * / tag_3: / *“c1.sol”:26:94合约C {... * / tag_4: 数据尺寸(sub_0) DUP1 dataOffset(sub_0) 为0x0 codecopy 为0x0 返回 停止
sub_0:程序集{ / *“c1.sol”:26:94合约C {... * / mstore(0x40,0x60) TAG_1: 为0x0 DUP1 还原
auxdata:0xa165627a7a72305820af3193f6fd31031a0e0d2de1ad2c27352b1ce081b4f3c92b5650ca4dd542bb770029 }
二进制: 60606040523415600e57600080fd5b5b60016000819055505b5b60368060266000396000f30060606040525b600080fd00a165627a7a72305820af3193f6fd31031a0e0d2de1ad2c27352b1ce081b4f3c92b5650ca4dd542bb770029
数字6060604052...
是EVM实际运行的字节码。
一半的编译程序集样板文件在大多数Solidity程序中都是相似的。 我们稍后再看看。 现在,让我们来看看我们合约的独特部分,简单的存储变量赋值:
a = 1
这个任务由字节码6001600081905550
表示。 让我们把它分解成每行一条指令:
60 01 60 00 81 90 55 50
EVM基本上是一个从上到下执行每条指令的循环。 让我们使用相应的字节码来标注汇编代码(在标签tag_2
下缩进),以更好地了解它们的关联方式:
TAG_2: // 60 01 为0x1 // 60 00 为0x0 // 81 DUP2 // 90 swap1 // 55 sstore // 50 流行的
请注意,汇编代码中的0x1
实际上是push(0x1)
的简写push(0x1)
。 该指令将数字1推入堆栈。
盯着它看看发生了什么仍然很难。 不要担心,很容易一行一行地模拟EVM。
EVM是一个堆栈机器。 指令可能使用堆栈中的值作为参数,并将值作为结果推送到堆栈。 让我们考虑操作add
。
假设堆栈中有两个值:
[1 2]
当EVM看到add
,它将前两项添加到一起,并将答案推回到堆栈上,导致:
[3]
在下面的内容中,我们将用[]
注释堆栈:
//空的堆栈 堆栈:[] //堆叠三个项目。 最上面的项目是3.最下面的项目是1。 堆叠:[3 2 1]
并用{}
标记合约存储空间:
//存储空间不存在 商店:{} //值0x1存储在位置0x0。 存储:{0x0 => 0x1}
现在我们来看看一些真正的字节码。 我们将按照EVM模拟字节码序列6001600081905550
,并在每条指令后打印机器状态:
// 60 01:将1推入堆栈 为0x1 堆栈:[0x1]
// 60 00:将0推入堆栈 为0x0 堆栈:[0x0 0x1]
// 81:复制堆栈中的第二个项目 DUP2 堆栈:[0x1 0x0 0x1]
// 90:交换前两个项目 swap1 堆栈:[0x0 0x1 0x1]
// 55:将值0x1存储在位置0x0 //这条指令消耗前两项 sstore 堆栈:[0x1] 存储:{0x0 => 0x1}
// 50:流行(丢掉顶级商品) 流行的 堆栈:[] 存储:{0x0 => 0x1}
结束。 堆栈是空的,并且存储中有一个项目。
值得注意的是, uint256 a
决定将状态变量uint256 a
存储在位置0x0
。 其他语言可以选择在别处存储状态变量。
在伪代码中,EVM为6001600081905550
的实质上是:
// a = 1 sstore(0x0,0x1)
仔细看,你会发现dup2,swap1,pop是多余的。 汇编代码可能更简单:
为0x1 为0x0 sstore
您可以尝试模拟上述3条指令,并确保它们确实导致相同的机器状态:
堆栈:[] 存储:{0x0 => 0x1}
我们添加一个相同类型的额外存储变量:
// c2.sol 杂注扎实0.4.11;
合同C { uint256 a; uint256 b;
函数C(){ a = 1; b = 2; } }
编译,重点关注tag_2
:
$ solc --bin --asm c2.sol
// ...更多的东西被省略
TAG_2: / *“c2.sol”:99:100 1 * / 为0x1 / *“c2.sol”:95:96 a * / 为0x0 / *“c2.sol”:95:100 a = 1 * / DUP2 swap1 sstore 流行的 / *“c2.sol”:112:113 2 * / 0X2 / *“c2.sol”:108:109 b * / 为0x1 / *“c2.sol”:108:113 b = 2 * / DUP2 swap1 sstore 流行的
伪代码中的程序集:
// a = 1 sstore(0x0,0x1) // b = 2 sstore(0x1,0x2)
我们在这里学到的是两个存储变量0x0
定位,位置为0x0
,位置为0x1
。
每个插槽存储可以存储32个字节。 如果一个变量只需要16个字节,那么使用全部32个字节是浪费的。 如果可能,通过将两个较小的数据类型打包到一个存储插槽中,Solidity可优化存储效率。
让我们改变a
和b
使它们每个只有16个字节:
杂注扎实0.4.11;
合同C { uint128 a; uint128 b;
函数C(){ a = 1; b = 2; } }
编制合同:
$ solc --bin --asm c3.sol
生成的程序集现在更复杂:
TAG_2: // a = 1 为0x1 为0x0 DUP1 为0x100 EXP DUP2 SLOAD DUP2 0xffffffffffffffffffffffffffffffff MUL 不 和 swap1 dup4 0xffffffffffffffffffffffffffffffff 和 MUL 要么 swap1 sstore 流行的
// b = 2 0X2 为0x0 为0x10 为0x100 EXP DUP2 SLOAD DUP2 0xffffffffffffffffffffffffffffffff MUL 不 和 swap1 dup4 0xffffffffffffffffffffffffffffffff 和 MUL 要么 swap1 sstore 流行的
上述汇编代码将这两个变量一起打包在一个存储位置( 0x0
)中,如下所示:
[b] [a] [16字节/ 128位] [16字节/ 128位]
打包的理由是因为目前最昂贵的操作是存储使用情况:
sstore
花费20000瓦斯首先写入新的位置。 sstore
花费5000瓦斯用于后续写入现有位置。 sload
500个天然气。 通过使用相同的存储位置,Solidity为第二个存储变量支付5000而不是20000,为我们节省了15000个气体。
应该可以将两个128位的数字一起打包在内存中,然后使用一个sstore
存储它们,从而节省额外的5000个气体,而不是使用两个单独的sstore
指令来存储a
和b
。
您可以通过打开optimize
标志来让Solidity进行优化:
$ solc --bin --asm --optimize c3.sol
它生成仅使用一个sload和一个sstore的汇编代码:
TAG_2: / *“c3.sol”:95:96 a * / 为0x0 / *“c3.sol”:95:100 a = 1 * / DUP1 SLOAD / *“c3.sol”:108:113 b = 2 * / 0x200000000000000000000000000000000 not(sub(exp(0x2,0x80),0x1)) / *“c3.sol”:95:100 a = 1 * / swap1 swap2 和 / *“c3.sol”:99:100 1 * / 为0x1 / *“c3.sol”:95:100 a = 1 * / 要么 sub(exp(0x2,0x80),0x1) / *“c3.sol”:108:113 b = 2 * / 和 要么 swap1 sstore
字节码是:
600080547002000000000000000000000000000000006001608060020a03199091166001176001608060020a0316179055
并将字节码格式化为每行一条指令:
//按下0x0 60 00 // dup1 80 // sload 54 // push17将下一个17字节作为32字节的数字 70 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
/ * not(sub(exp(0x2,0x80),0x1))* / //推送0x1 60 01 //按0x80(32) 60 80 //按0x80(2) 60 02 // exp 0A //子 03 //不是 19
// swap1 90 // swap2 91 //和 16 //推送0x1 60 01 // 要么 17
/ * sub(exp(0x2,0x80),0x1)* / //推送0x1 60 01 //按下0x80 60 80 //推送0x02 60 02 // exp 0A //子 03
//和 16 // 要么 17 // swap1 90 // sstore 55
汇编代码中使用了四个魔术值:
//在字节码中表示为0x01
16:32 0x00000000000000000000000000000000 00:16 0x00000000000000000000000000000001
//在字节码中表示为0x200000000000000000000000000000000
16:32 0x00000000000000000000000000000002 00:16 0x00000000000000000000000000000000
not(sub(exp(0x2, 0x80), 0x1))
//高16位字节的位掩码
16:32 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF 00:16 0x00000000000000000000000000000000
sub(exp(0x2, 0x80), 0x1)
//低16位字节的位掩码
16:32 0x00000000000000000000000000000000 00:16 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
该代码执行一些比特 - 使用这些值进行混洗以达到期望的结果:
16:32 0x00000000000000000000000000000002 00:16 0x00000000000000000000000000000001
最后,这个32字节的值被存储在位置0x0
。
60008054700 200000000000000000000000000000000 6001608060020a03199091166001176001608060020a0316179055
请注意, 0x200000000000000000000000000000000
嵌入在字节码中。 但是编译器也可以选择用指令exp(0x2, 0x81)
来计算值,这会导致较短的字节码序列。
但事实证明, 0x200000000000000000000000000000000
比exp(0x2, 0x81)
便宜。 我们来看看所涉及的天然气费用:
让我们来比较一下在天然气中的代表费用。
0x200000000000000000000000000000000
。 它有很多零,价格便宜。 (1 * 68)+(16 * 4)= 196。
608160020a
。 更短,但没有零。 5 * 68 = 340。
更多零的更长序列实际上更便宜!
EVM编译器不会针对字节码大小,速度或内存效率进行精确优化。 相反,它优化了天然气使用量,这是一个间接的层面,可以激励以太坊区块链可以有效地进行计算。
我们已经看到了EVM的一些古怪的方面:
天然气成本有些任意设定,未来可能会发生改变。 随着成本的变化,编译器会做出不同的选择。
https://blog.qtum.org/diving-into-the-ethereum-vm-6e8d5d2f3c30