日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

深入了解以太坊虚拟机

發(fā)布時間:2025/3/15 编程问答 17 豆豆
生活随笔 收集整理的這篇文章主要介紹了 深入了解以太坊虚拟机 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

本文由幣乎社區(qū)(bihu.com)內(nèi)容支持計劃贊助。

譯者說,深入了解以太坊虛擬機是一個系列的文章,一共5篇! 本文是第1篇,主要介紹的是以太坊虛擬機匯編代碼基礎(chǔ)。后續(xù)的4篇譯文鏈接在本文的結(jié)尾處。

Solidity提供了很多高級語言的抽象概念,但是這些特性讓人很難明白在運行程序的時候到底發(fā)生了什么。我閱讀了Solidity的文檔,但依舊存在著幾個基本的問題沒有弄明白。

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

  • 該在什么地方使用哪個類型?
  • 將 string 轉(zhuǎn)換成bytes時會怎么樣?可以轉(zhuǎn)換成byte[]嗎?
  • 它們的存儲成本是多少?

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

  • 為什么不能刪除一個映射?
  • 可以有映射的映射嗎?(可以,但是怎樣映射?)
  • 為什么存在存儲映射,但是卻沒有內(nèi)存映射?

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

  • 合約是如何創(chuàng)建的?
  • 到底什么是構(gòu)造器?
  • 什么是 fallback 函數(shù)?

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

  • Solidity不是最后一種語言。更好的EVM語言正在到來。(拜托?)
  • EVM是一個數(shù)據(jù)庫引擎。要理解智能合約是如何以任意EVM語言來工作的,就必須要明白數(shù)據(jù)是如何被組織的,被存儲的,以及如何被操作的。
  • 知道如何成為貢獻者。以太坊的工具鏈還處于早期,理解EVM可以幫助你實現(xiàn)一個超棒的工具給自己和其他人使用。
  • 智力的挑戰(zhàn)。EVM可以讓你有個很好的理由在密碼學(xué)、數(shù)據(jù)結(jié)構(gòu)、編程語言設(shè)計的交集之間進行翱翔。
  • 在這個系列的文章中,我會拆開一個簡單的Solidity合約,來讓大家明白它是如何以EVM字節(jié)碼(bytecode)來運行的。

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

    • EVM字節(jié)碼的基礎(chǔ)認(rèn)識
    • 不同類型(映射,數(shù)組)是如何表示的
    • 當(dāng)一個新合約創(chuàng)建之后會發(fā)生什么
    • 當(dāng)一個方法被調(diào)用時會發(fā)生什么
    • ABI如何橋接不同的EVM語言

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

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

    一個簡單的合約

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

    // c1.sol pragma 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))0x0dup1revert tag_1: tag_2:/* "c1.sol":84:85 1 */0x1/* "c1.sol":80:81 a */0x0/* "c1.sol":80:85 a = 1 */dup2swap1sstorepop/* "c1.sol":59:92 function C() {... */ tag_3:/* "c1.sol":26:94 contract C {... */ tag_4:dataSize(sub_0)dup1dataOffset(sub_0)0x0codecopy0x0return stop sub_0: assembly {/* "c1.sol":26:94 contract C {... */mstore(0x40, 0x60)tag_1:0x0dup1revert auxdata: 0xa165627a7a72305820af3193f6fd31031a0e0d2de1ad2c27352b1ce081b4f3c92b5650ca4dd542bb770029 } Binary: 60606040523415600e57600080fd5b5b60016000819055505b5b60368060266000396000f30060606040525b600080fd00a165627a7a72305820af3193f6fd31031a0e0d2de1ad2c27352b1ce081b4f3c92b5650ca4dd542bb770029

    6060604052...這串?dāng)?shù)字就是EVM實際運行的字節(jié)碼。

    一小步一小步的來

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

    a = 1

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

    60 01 60 00 81 90 55 50

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

    tag_2:// 60 010x1// 60 000x0// 81dup2// 90swap1// 55sstore// 50pop

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

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

    模擬EVM

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

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

    [1 2]

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

    [3]

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

    // 空棧 stack: [] // 有3個數(shù)據(jù)的棧,棧頂項為3,棧底項為1 stack: [3 2 1]

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

    // 空存儲 store: {} // 數(shù)值0x1被保存在0x0的位置上 store: { 0x0 => 0x1 }

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

    // 60 01:將1壓入棧中 0x1stack: [0x1] // 60 00: 將0壓入棧中 0x0stack: [0x0 0x1] // 81: 復(fù)制棧中的第二項 dup2stack: [0x1 0x0 0x1] // 90: 交換棧頂?shù)膬身棓?shù)據(jù) swap1stack: [0x0 0x1 0x1] // 55: 將數(shù)值0x01存儲在0x0的位置上 // 這個操作會消耗棧頂兩項數(shù)據(jù) sstorestack: [0x1]store: { 0x0 => 0x1 } // 50: pop (丟棄棧頂數(shù)據(jù)) popstack: []store: { 0x0 => 0x1 }

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

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

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

    // a = 1 sstore(0x0, 0x1)

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

    0x1 0x0 sstore

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

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

    兩個存儲變量

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

    // c2.sol pragma 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 */dup2swap1sstorepop/* "c2.sol":112:113 2 */0x2/* "c2.sol":108:109 b */0x1/* "c2.sol":108:113 b = 2 */dup2swap1sstorepop

    匯編的偽代碼:

    // a = 1 sstore(0x0, 0x1) // b = 2 sstore(0x1, 0x2)

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

    存儲打包

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

    我們將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 = 10x10x0dup10x100expdup2sloaddup20xffffffffffffffffffffffffffffffffmulnotandswap1dup40xffffffffffffffffffffffffffffffffandmulorswap1sstorepop// b = 20x20x00x100x100expdup2sloaddup20xffffffffffffffffffffffffffffffffmulnotandswap1dup40xffffffffffffffffffffffffffffffffandmulorswap1sstorepop

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

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

    進行打包的原因是因為目前最昂貴的操作就是存儲的使用:

    • sstore指令第一次寫入一個新位置需要花費20000 gas
    • sstore指令后續(xù)寫入一個已存在的位置需要花費5000 gas
    • sload指令的成本是500 gas
    • 大多數(shù)的指令成本是3~10 gas

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

    更多優(yōu)化

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

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

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

    這樣產(chǎn)生的匯編代碼只有一個sload指令和一個sstore指令:

    tag_2:/* "c3.sol":95:96 a */0x0/* "c3.sol":95:100 a = 1 */dup1sload/* "c3.sol":108:113 b = 2 */0x200000000000000000000000000000000not(sub(exp(0x2, 0x80), 0x1))/* "c3.sol":95:100 a = 1 */swap1swap2and/* "c3.sol":99:100 1 */0x1/* "c3.sol":95:100 a = 1 */orsub(exp(0x2, 0x80), 0x1)/* "c3.sol":108:113 b = 2 */andorswap1sstore

    字節(jié)碼是:

    600080547002000000000000000000000000000000006001608060020a03199091166001176001608060020a0316179055

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

    // push 0x0 60 00 // dup1 80 // sload 54 // push17 將下面17個字節(jié)作為一個32個字的數(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個神奇的數(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ù)值進行了一些位的轉(zhuǎn)換來達到想要的結(jié)果:

    16:32 0x00000000000000000000000000000002 00:16 0x00000000000000000000000000000001

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

    Gas 的使用

    600080547002000000000000000000000000000000006001608060020a03199091166001176001608060020a0316179055

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

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

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

    來計算下兩個表示方式所花費的gas成本:

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

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

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

    總結(jié)

    EVM的編譯器實際上不會為字節(jié)碼的大小、速度或內(nèi)存高效性進行優(yōu)化。相反,它會為gas的使用進行優(yōu)化,這間接鼓勵了計算的排序,讓以太坊區(qū)塊鏈可以更高效一點。

    我們也看到了EVM一些奇特的地方:

    • EVM是一個256位的機器。以32字節(jié)來處理數(shù)據(jù)是最自然的
    • 持久存儲是相當(dāng)昂貴的
    • Solidity編譯器會為了減少gas的使用而做出相應(yīng)的優(yōu)化選擇

    Gas成本的設(shè)置有一點武斷,也許未來會改變。當(dāng)成本改變的時候,編譯器也會做出不同的優(yōu)化選擇。

    本系列文章其他部分譯文鏈接:

    • 固定長度數(shù)據(jù)類型的表示方法(第2部分)
    • 動態(tài)數(shù)據(jù)類型的表示方法(第3部分)
    • ABI編碼外部方法調(diào)用的方式(第4部分)
    • 一個新合約被創(chuàng)建后會發(fā)生什么(第5部分)

    翻譯作者: 許莉
    原文地址:Diving Into The Ethereum VM Part One



    作者:Lilymoana
    鏈接:https://www.jianshu.com/p/1969f3761208
    來源:簡書
    著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處。

    總結(jié)

    以上是生活随笔為你收集整理的深入了解以太坊虚拟机的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。