溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

區(qū)塊鏈以太坊虛擬機(jī)的匯編代碼是什么

發(fā)布時(shí)間:2022-01-19 10:12:10 來源:億速云 閱讀:125 作者:iii 欄目:互聯(lián)網(wǎng)科技

今天小編給大家分享一下區(qū)塊鏈以太坊虛擬機(jī)的匯編代碼是什么的相關(guān)知識點(diǎn),內(nèi)容詳細(xì),邏輯清晰,相信大部分人都還太了解這方面的知識,所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來了解一下吧。

string, bytes32, byte[], bytes之間的區(qū)別是什么?

  • 該在什么地方使用哪個(gè)類型?

  • 將 string 轉(zhuǎn)換成bytes時(shí)會怎么樣?可以轉(zhuǎn)換成byte[]嗎?

  • 它們的存儲成本是多少?

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

  • 為什么不能刪除一個(gè)映射?

  • 可以有映射的映射嗎?(可以,但是怎樣映射?)

  • 為什么存在存儲映射,但是卻沒有內(nèi)存映射?

編譯的合約在EVM看來是什么樣子的?

  • 合約是如何創(chuàng)建的?

  • 到底什么是構(gòu)造器?

  • 什么是 fallback 函數(shù)?

我覺得學(xué)習(xí)在以太坊虛擬機(jī)(EVM)上運(yùn)行的類似Solidity 高級語言是一種很好的投資,有幾個(gè)原因:

  • Solidity不是最后一種語言。更好的EVM語言正在到來。(拜托?)

  • EVM是一個(gè)數(shù)據(jù)庫引擎。要理解智能合約是如何以任意EVM語言來工作的,就必須要明白數(shù)據(jù)是如何被組織的,被存儲的,以及如何被操作的。

  • 知道如何成為貢獻(xiàn)者。以太坊的工具鏈還處于早期,理解EVM可以幫助你實(shí)現(xiàn)一個(gè)超棒的工具給自己和其他人使用。

  • 智力的挑戰(zhàn)。EVM可以讓你有個(gè)很好的理由在密碼學(xué)、數(shù)據(jù)結(jié)構(gòu)、編程語言設(shè)計(jì)的交集之間進(jìn)行翱翔。

在這個(gè)系列的文章中,我會拆開一個(gè)簡單的Solidity合約,來讓大家明白它是如何以EVM字節(jié)碼(bytecode)來運(yùn)行的。

我希望能夠?qū)W習(xí)以及會書寫的文章大綱:

  • EVM字節(jié)碼的基礎(chǔ)認(rèn)識

  • 不同類型(映射,數(shù)組)是如何表示的

  • 當(dāng)一個(gè)新合約創(chuàng)建之后會發(fā)生什么

  • 當(dāng)一個(gè)方法被調(diào)用時(shí)會發(fā)生什么

  • ABI如何橋接不同的EVM語言

我的最終目標(biāo)是整體的理解一個(gè)編譯的Solidity合約。讓我們從閱讀一些基本的EVM字節(jié)碼開始。

EVM指令集將是一個(gè)比較有幫助的參考。

1

一個(gè)簡單的合約

我們的第一個(gè)合約有一個(gè)構(gòu)造器和一個(gè)狀態(tài)變量:

// 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...這串?dāng)?shù)字就是EVM實(shí)際運(yùn)行的字節(jié)碼。

2

一小步一小步地來

上面一半的編譯匯編是大多數(shù)Solidity程序中都會存在的樣板語句。我們稍后再來看這些。現(xiàn)在,我們來看看合約中獨(dú)特的部分,簡單的存儲變量賦值:

a = 1

代表這個(gè)賦值的字節(jié)碼是6001600081905550。我們把它拆成一行一條指令:

60 01
60 00
81
90
55
50

EVM本質(zhì)上就是一個(gè)循環(huán),從上到下的執(zhí)行每一條命令。讓我們用相應(yīng)的字節(jié)碼來注釋匯編代碼(縮進(jìn)到標(biāo)簽tag_2下),來更好的看看他們之間的關(guān)聯(lián):

tag_2:  // 60 01
 0x1
 // 60 00
 0x0
 // 81
 dup2  // 90
 swap1  // 55
 sstore  // 50
 pop

注意0x1在匯編代碼中實(shí)際上是push(0x1)的速記。這條指令將數(shù)值1壓入棧中。

只是盯著它依然很難明白到底發(fā)生了什么,不過不用擔(dān)心,一行一行的模擬EVM是比較簡單的。

3

模擬EVM

EVM是個(gè)堆棧機(jī)器。指令可能會使用棧上的數(shù)值作為參數(shù),也會將值作為結(jié)果壓入棧中。讓我們來思考一下add操作。

假設(shè)棧上有兩個(gè)值:

[1 2]

當(dāng)EVM看見了add,它會將棧頂?shù)?項(xiàng)相加,然后將答案壓入棧中,結(jié)果是:

[3]

接下來,我們用[]符號來標(biāo)識棧:

// 空棧

stack: []

// 有3個(gè)數(shù)據(jù)的棧,棧頂項(xiàng)為3,棧底項(xiàng)為1

stack: [3 2 1]

用{}符號來標(biāo)識合約存儲器:

// 空存儲

store: {}

// 數(shù)值0x1被保存在0x0的位置上

store: { 0x0 => 0x1 }

現(xiàn)在讓我們來看看真正的字節(jié)碼。我們將會像EVM那樣來模擬6001600081905550字節(jié)序列,并打印出每條指令的機(jī)器狀態(tài):

// 60 01:將1壓入棧中

0x1

  stack: [0x1]

// 60 00: 將0壓入棧中

0x0

  stack: [0x0 0x1]

// 81: 復(fù)制棧中的第二項(xiàng)

dup2

  stack: [0x1 0x0 0x1]

// 90: 交換棧頂?shù)膬身?xiàng)數(shù)據(jù)

swap1

  stack: [0x0 0x1 0x1]

// 55: 將數(shù)值0x01存儲在0x0的位置上

// 這個(gè)操作會消耗棧頂兩項(xiàng)數(shù)據(jù)

sstore

  stack: [0x1]

  store: { 0x0 => 0x1 }

// 50: pop (丟棄棧頂數(shù)據(jù))

pop

  stack: [] 

  store: { 0x0 => 0x1 }

最后,棧就為空棧,而存儲器里面有一項(xiàng)數(shù)據(jù)。

值得注意的是Solidity已經(jīng)決定將狀態(tài)變量uint256 a保存在0x0的位置上。其他語言完全可以選擇將狀態(tài)變量存儲在其他的任何位置上。

6001600081905550字節(jié)序列在本質(zhì)上用EVM的操作偽代碼來表示就是:

// a = 1

sstore(0x0, 0x1)

仔細(xì)觀察,你就會發(fā)現(xiàn)dup2,swap1,pop都是多余的,匯編代碼可以更簡單一些:

0x1
0x0
sstore

你可以模擬上面的3條指令,然后會發(fā)現(xiàn)他們的機(jī)器狀態(tài)結(jié)果都是一樣的:

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

4

兩個(gè)存儲變量

讓我們再額外的增加一個(gè)相同類型的存儲變量:

// 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)

我們可以看到兩個(gè)存儲變量的存儲位置是依次排列的,a在0x0的位置而b在0x1的位置。

5

存儲打包

每個(gè)存儲槽都可以存儲32個(gè)字節(jié)。如果一個(gè)變量只需要16個(gè)字節(jié)但是使用全部的32個(gè)字節(jié)會很浪費(fèi)。Solidity為了高效存儲,提供了一個(gè)優(yōu)化方案:如果可以的話,就將兩個(gè)小一點(diǎn)的數(shù)據(jù)類型進(jìn)行打包然后存儲在一個(gè)存儲槽中。

我們將a和b修改成16字節(jié)的變量:

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

編譯此合約:

$ solc --bin --asm c3.sol

產(chǎn)生的匯編代碼現(xiàn)在更加的復(fù)雜一些:

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

上面的匯編代碼將這兩個(gè)變量打包放在一個(gè)存儲位置(0x0)上,就像這樣:

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

進(jìn)行打包的原因是因?yàn)槟壳白畎嘿F的操作就是存儲的使用:

  • sstore指令第一次寫入一個(gè)新位置需要花費(fèi)20000 gas

  • sstore指令后續(xù)寫入一個(gè)已存在的位置需要花費(fèi)5000 gas

  • sload指令的成本是500 gas

  • 大多數(shù)的指令成本是3~10 gas

通過使用相同的存儲位置,Solidity為存儲第二個(gè)變量支付5000 gas,而不是20000 gas,節(jié)約了15000 gas。

6

更多優(yōu)化

應(yīng)該可以將兩個(gè)128位的數(shù)打包成一個(gè)數(shù)放入內(nèi)存中,然后使用一個(gè)'sstore'指令進(jìn)行存儲操作,而不是使用兩個(gè)單獨(dú)的sstore命令來存儲變量a和b,這樣就額外的又省了5000 gas。

你可以通過添加optimize選項(xiàng)來讓Solidity實(shí)現(xiàn)上面的優(yōu)化:

$ solc --bin --asm --optimize c3.sol

這樣產(chǎn)生的匯編代碼只有一個(gè)sload指令和一個(gè)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

字節(jié)碼是:

600080547002000000000000000000000000000000006001608060020a03199091166001176001608060020a0316179055

將字節(jié)碼解析成一行一指令:

// push 0x0

60 00

// dup1

80

// sload

54

// push27 將下面17個(gè)字節(jié)作為一個(gè)32個(gè)字的數(shù)值壓入棧中

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個(gè)神奇的數(shù)值:

*   0x1(16字節(jié)),使用低16字節(jié)

// 在字節(jié)碼中表示為0x01

16:32 0x00000000000000000000000000000000

00:16 0x00000000000000000000000000000001

*   0x2(16字節(jié)),使用高16字節(jié)

//在字節(jié)碼中表示為0x200000000000000000000000000000000

 16:32 0x00000000000000000000000000000002

00:16 0x00000000000000000000000000000000

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

// 高16字節(jié)的掩碼

16:32 0x00000000000000000000000000000000 

00:16 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF

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

// 低16字節(jié)的掩碼

16:32 0x00000000000000000000000000000000 

00:16 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF

代碼將這些數(shù)值進(jìn)行了一些位的轉(zhuǎn)換來達(dá)到想要的結(jié)果:

16:32 0x00000000000000000000000000000002 

00:16 0x00000000000000000000000000000001

最后,該32字節(jié)的數(shù)值被保存在了0x0的位置上。

7

Gas的使用

600080547002000000000000000000000000000000006001608060020a03199091166001176001608060020a0316179055

注意0x200000000000000000000000000000000被嵌入到了字節(jié)碼中。但是編譯器也可能選擇使用exp(0x2, 0x81)指令來計(jì)算數(shù)值,這會導(dǎo)致更短的字節(jié)碼序列。

但結(jié)果是0x200000000000000000000000000000000比exp(0x2, 0x81)更便宜。讓我們看看與gas費(fèi)用相關(guān)的信息:

  • 一筆交易的每個(gè)零字節(jié)的數(shù)據(jù)或代碼費(fèi)用為 4 gas

  • 一筆交易的每個(gè)非零字節(jié)的數(shù)據(jù)或代碼的費(fèi)用為 68 gas

來計(jì)算下兩個(gè)表示方式所花費(fèi)的gas成本:

  • 0x200000000000000000000000000000000字節(jié)碼包含了很多的0,更加的便宜。 (1 * 68) + (32 * 4) = 196

  • 608160020a字節(jié)碼更短,但是沒有0。 5 * 68 = 340

更長的字節(jié)碼序列有很多的0,所以實(shí)際上更加的便宜!

以上就是“區(qū)塊鏈以太坊虛擬機(jī)的匯編代碼是什么”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會為大家更新不同的知識,如果還想學(xué)習(xí)更多的知識,請關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI