区块链以太坊虚拟机的汇编代码是什么

发布时间:2022-01-19 10:12:10 作者:iii
来源:亿速云 阅读:125

今天小编给大家分享一下区块链以太坊虚拟机的汇编代码是什么的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。

string, bytes32, byte[], bytes之间的区别是什么?

EVM是如何存储映射( mappings)的?

编译的合约在EVM看来是什么样子的?

我觉得学习在以太坊虚拟机(EVM)上运行的类似Solidity 高级语言是一种很好的投资,有几个原因:

在这个系列的文章中,我会拆开一个简单的Solidity合约,来让大家明白它是如何以EVM字节码(bytecode)来运行的。

我希望能够学习以及会书写的文章大纲:

我的最终目标是整体的理解一个编译的Solidity合约。让我们从阅读一些基本的EVM字节码开始。

EVM指令集将是一个比较有帮助的参考。

1

一个简单的合约

我们的第一个合约有一个构造器和一个状态变量:

// c1.solpragma solidity ^0.4.11;
contract C {
   uint256 a;    function C() {
     a = 1;
   }
}

用solc来编译此合约:

$ solc --bin --asm c1.sol

 ======= c1.sol:C =======

 EVM assembly:

    /* "c1.sol":26:94  contract C {... */

  mstore(0x40, 0x60)

    /* "c1.sol":59:92  function C() {... */

  jumpi(tag_1, iszero(callvalue))

  0x0

  dup1

  revert

 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

  pop

    /* "c1.sol":59:92  function C() {... */

tag_3:

    /* "c1.sol":26:94  contract C {... */

tag_4:

  dataSize(sub_0)

  dup1

  dataOffset(sub_0)

  0x0

  codecopy

  0x0

  return

stop

 sub_0: assembly {

        /* "c1.sol":26:94  contract C {... */ 

      mstore(0x40, 0x60) 

    tag_1: 

     0x0

      dup1

      revert

 auxdata: 0xa165627a7a72305820af3193f6fd31031a0e0d2de1ad2c27352b1ce081b4f3c92b5650ca4dd542bb770029

} Binary:60606040523415600e57600080fd5b5b60016000819055505b5b60368060266000396000f30060606040525b600080fd00a165627a7a72305820af3193f6fd31031a0e0d2de1ad2c27352b1ce081b4f3c92b5650ca4dd542bb770029

6060604052...这串数字就是EVM实际运行的字节码。

2

一小步一小步地来

上面一半的编译汇编是大多数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
 pop

注意0x1在汇编代码中实际上是push(0x1)的速记。这条指令将数值1压入栈中。

只是盯着它依然很难明白到底发生了什么,不过不用担心,一行一行的模拟EVM是比较简单的。

3

模拟EVM

EVM是个堆栈机器。指令可能会使用栈上的数值作为参数,也会将值作为结果压入栈中。让我们来思考一下add操作。

假设栈上有两个值:

[1 2]

当EVM看见了add,它会将栈顶的2项相加,然后将答案压入栈中,结果是:

[3]

接下来,我们用[]符号来标识栈:

// 空栈

stack: []

// 有3个数据的栈,栈顶项为3,栈底项为1

stack: [3 2 1]

用{}符号来标识合约存储器:

// 空存储

store: {}

// 数值0x1被保存在0x0的位置上

store: { 0x0 => 0x1 }

现在让我们来看看真正的字节码。我们将会像EVM那样来模拟6001600081905550字节序列,并打印出每条指令的机器状态:

// 60 01:将1压入栈中

0x1

  stack: [0x1]

// 60 00: 将0压入栈中

0x0

  stack: [0x0 0x1]

// 81: 复制栈中的第二项

dup2

  stack: [0x1 0x0 0x1]

// 90: 交换栈顶的两项数据

swap1

  stack: [0x0 0x1 0x1]

// 55: 将数值0x01存储在0x0的位置上

// 这个操作会消耗栈顶两项数据

sstore

  stack: [0x1]

  store: { 0x0 => 0x1 }

// 50: pop (丢弃栈顶数据)

pop

  stack: [] 

  store: { 0x0 => 0x1 }

最后,栈就为空栈,而存储器里面有一项数据。

值得注意的是Solidity已经决定将状态变量uint256 a保存在0x0的位置上。其他语言完全可以选择将状态变量存储在其他的任何位置上。

6001600081905550字节序列在本质上用EVM的操作伪代码来表示就是:

// a = 1

sstore(0x0, 0x1)

仔细观察,你就会发现dup2,swap1,pop都是多余的,汇编代码可以更简单一些:

0x1
0x0
sstore

你可以模拟上面的3条指令,然后会发现他们的机器状态结果都是一样的:

stack: []
store: { 0x0 => 0x1 }

4

两个存储变量

让我们再额外的增加一个相同类型的存储变量:

// c2.solpragma solidity ^0.4.11;
contract C {
   uint256 a;
   uint256 b;    function 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

  pop

    /* "c2.sol":112:113  2 */

  0x2

    /* "c2.sol":108:109  b */

  0x1

    /* "c2.sol":108:113  b = 2 */

  dup2

  swap1

  sstore

  pop

汇编的伪代码:

// a = 1

sstore(0x0, 0x1)//

 b = 2

sstore(0x1, 0x2)

我们可以看到两个存储变量的存储位置是依次排列的,a在0x0的位置而b在0x1的位置。

5

存储打包

每个存储槽都可以存储32个字节。如果一个变量只需要16个字节但是使用全部的32个字节会很浪费。Solidity为了高效存储,提供了一个优化方案:如果可以的话,就将两个小一点的数据类型进行打包然后存储在一个存储槽中。

我们将a和b修改成16字节的变量:

pragma solidity ^0.4.11;
contract C {
   uint128 a;
   uint128 b;    function C() {
     a = 1;
     b = 2;
   }
}

编译此合约:

$ solc --bin --asm c3.sol

产生的汇编代码现在更加的复杂一些:

tag_2:

  // a = 1

  0x1

  0x0

  dup1

  0x100

  exp

  dup2

  sload

  dup2

  0xffffffffffffffffffffffffffffffff

  mul

  not

  and

  swap1

  dup4

  0xffffffffffffffffffffffffffffffff

  and

  mul

  or

  swap1

  sstore

  pop

  // b = 2

  0x2

  0x0

  0x10

  0x100

  exp

  dup2

  sload

  dup2

  0xffffffffffffffffffffffffffffffff

  mul

  not

  and

  swap1

  dup4

  0xffffffffffffffffffffffffffffffff

  and

  mul

  or

  swap1

  sstore

  pop

上面的汇编代码将这两个变量打包放在一个存储位置(0x0)上,就像这样:

[         b         ][         a         ]
[16 bytes / 128 bits][16 bytes / 128 bits]

进行打包的原因是因为目前最昂贵的操作就是存储的使用:

通过使用相同的存储位置,Solidity为存储第二个变量支付5000 gas,而不是20000 gas,节约了15000 gas。

6

更多优化

应该可以将两个128位的数打包成一个数放入内存中,然后使用一个'sstore'指令进行存储操作,而不是使用两个单独的sstore命令来存储变量a和b,这样就额外的又省了5000 gas。

你可以通过添加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

  and

    /* "c3.sol":99:100  1 */
  0x1

    /* "c3.sol":95:100  a = 1 */

  or

  sub(exp(0x2, 0x80), 0x1)

    /* "c3.sol":108:113  b = 2 */

  and
  or

  swap1

  sstore

字节码是:

600080547002000000000000000000000000000000006001608060020a03199091166001176001608060020a0316179055

将字节码解析成一行一指令:

// push 0x0

60 00

// dup1

80

// sload

54

// push27 将下面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)) */

// push 0x1

60 01

// push 0x80 (32)

60 80

// push 0x80 (2)

60 02

// exp

0a

// sub

03

// not

19

// swap1

90

// swap2

91

// and

16

// push 0x1

60 01

// or

17

/* sub(exp(0x2, 0x80), 0x1) */

// push 0x1

60 01

// push 0x80

60 80

// push 0x02

60 02

// exp

0a

// sub

03

// and

16

// or

17

// swap1

90

// sstore

55

上面的汇编代码中使用了4个神奇的数值:

*   0x1(16字节),使用低16字节

// 在字节码中表示为0x01

16:32 0x00000000000000000000000000000000

00:16 0x00000000000000000000000000000001

*   0x2(16字节),使用高16字节

//在字节码中表示为0x200000000000000000000000000000000

 16:32 0x00000000000000000000000000000002

00:16 0x00000000000000000000000000000000

*   not(sub(exp(0x2, 0x80), 0x1))

// 高16字节的掩码

16:32 0x00000000000000000000000000000000 

00:16 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF

*   sub(exp(0x2, 0x80), 0x1)

// 低16字节的掩码

16:32 0x00000000000000000000000000000000 

00:16 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF

代码将这些数值进行了一些位的转换来达到想要的结果:

16:32 0x00000000000000000000000000000002 

00:16 0x00000000000000000000000000000001

最后,该32字节的数值被保存在了0x0的位置上。

7

Gas的使用

600080547002000000000000000000000000000000006001608060020a03199091166001176001608060020a0316179055

注意0x200000000000000000000000000000000被嵌入到了字节码中。但是编译器也可能选择使用exp(0x2, 0x81)指令来计算数值,这会导致更短的字节码序列。

但结果是0x200000000000000000000000000000000比exp(0x2, 0x81)更便宜。让我们看看与gas费用相关的信息:

来计算下两个表示方式所花费的gas成本:

更长的字节码序列有很多的0,所以实际上更加的便宜!

以上就是“区块链以太坊虚拟机的汇编代码是什么”这篇文章的所有内容,感谢各位的阅读!相信大家阅读完这篇文章都有很大的收获,小编每天都会为大家更新不同的知识,如果还想学习更多的知识,请关注亿速云行业资讯频道。

推荐阅读:
  1. 区块链 Hello World -- 基于以太坊的投票Dap
  2. 以太坊开发方法是什么

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

区块链 以太坊 虚拟机

上一篇:SpringBoot整合MybatisPlus如何分解质因数

下一篇:html5中有哪些常用框架

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》