以太坊虛擬機(EVM)架構和源碼簡析

EVM架構簡析和源碼分析

       EVM爲以太坊虛擬機。以太坊底層通過EVM模塊支持智能合約的執行和調用,調用時根據合約的地址獲
取到代碼,生成具體的執行環境,然後將代碼載入到EVM虛擬機中運行。通常目前開發智能合約的高級
語言爲Solidity,在利用solidity實現智能合約邏輯後,通過編譯器編譯成元數據(字節碼)最後發佈到以
坊上。

EVM架構概述

EVM本質上是一個堆棧機器,它最直接的的功能是執行智能合約,根據官方給出的設計原理,EVM的主

要的設計目標爲如下幾點:

  • 簡單性
  • 確定性
  • 空間節省
  • 爲區塊鏈服務
  • 安全性保證
  • 便於優化
針對以上幾點通過對EVM源代碼的閱讀來了解其具體的設計思想和工程實用性。

EVM存儲系統

機器位寬

       EVM機器位寬爲256位,即32個字節,256位機器字寬不同於我們經常見到主流的64位的機器

字寬,這就標明EVM設計上將考慮一套自己的關於操作,數據,邏輯控制的指令編碼。目前主流的處理

器原生的支持的計算數據類型有:8bits整數,16bits整數,32bits整數,64bits整數。一般情況下寬字

節的計算將更加的快一些,因爲它可能包含更多的指令被一次性加載到pc寄存器中,同時伴有內存訪問

次數的減少。目前在X86的架構中8bits的計算並不是完全的支持(除法和乘法),但基本的數學運算大概

在幾個時鐘週期內就能完成,也就是說主流的字節寬度基本上處理器能夠原生的支持,那爲什麼EVM要採

用256位的字寬。主要從以下兩個方面考慮:

  • 時間,智能合約是否能執行得更快
  • 空間,這樣是否整體字節碼的大小會有所減少
  • gas成本

時間上主要體現在執行的效率上,我們以兩個整形數相加來對比具體的操作時間消耗。32bits相加的X86

的彙編代碼

mov eax, dword [9876ABCD] //將地址9876ABCD中的32位數據放入eax數據寄存器
add eax, dword [1234DCBA] //將1234DCBA地址指向32位數和eax相加,結果保存在eax中

64bits相加的X86彙編代碼

mov rax, qword [123456789ABCDEF1] //將地址指向的64位數據放入64位寄存器
add rax, qword [1020304050607080] //計算相加的結果並將結果放入到64位寄存器中
下面我們看一下在64bits機器上如何完成256bits的加法
mov rax, qword [9876ABCD]
add qword [1234DCBA], rax
mov rax, qword [9876ABCD+8]
adc qword [1234DCBA+8], rax//這裏應用adc帶進位的加法指令,影響進位標記CF
mov rax, qword [9876ABCD+16]
adc qword [1234DCBA+16], rax
mov rax, qword [9876ABCD+24]
adc qword [1234DCBA+24], rax
由上面的的彙編指令我們可以看出256位操作要比系統原生支持的要複雜的多,從時間上考慮採用
256位這樣的字節寬度,實際的收益並不大。
空間上,由上面的彙編操作(在實際的EVM中操作類似)我們不難看到,如果直接對地址進行操
作似乎是一種快速的方式,並減少了操作數,進而操作碼也有所減少,相應的智能合約的字節流
大小就會小很多,gas花費也會有所下降。但是從另外一個層面來講,支持寬字節的數據類型勢必
會造成在處理低字節寬度的數據時候帶來存儲上的浪費(如添加標識用來區分類型)或者添加額外

的操作來進行數據的compact。從時間和空間角度來看,僅支持256字節寬度的選擇有利有弊,具

體還要看以太坊智能合約的具體應用。可能的幾點原因如下:

  • 256位的寬度方便進行密碼學方面的計算(sha256),但是成本有些高,場景比較少
  • 僅支持256位的比要支持其他類型的操作要少,單一,實現簡單可控
  • 和gas的計算相關,僅支持一種,方便計算,同時也考慮到了安全問題


內存分配

EVM中數據可以在三個地方進行存儲,分別是棧,臨時存儲,永久存儲。由於EVM是基於棧的虛擬機,

因此基本上所有的操作都是在棧上進行的,並且EVM中沒有寄存器的概念,這樣EVM對棧的依賴就更大,

雖然這樣的設計使實現比較簡單且易於理解,但是帶來的問題就是需要更多數據的相關操作。在EVM

中棧是唯一的免費(幾乎是)存放數據的地方。棧自然有深度的限制,目前的限制是1024

static constexpr int64_t stackLimit = 1024;

因爲棧的限制,因此棧上的臨時變量的使用會受限制。臨時內存存儲在每個VM實例中,並在合約執行完

後消失永久內存存儲在區塊鏈的狀態層。

EVM代碼簡析

本文分析EVM c++的代碼, github地址爲https://github.com/ethereum/cpp-ethereum
EVM c++實現了同go版本中的核心的功能,對於快速理解EVM的設計思想比較受用,對於熟悉C++的
可以先通過閱讀C++版本的實現,然後對標go版本的實現來進一步瞭解EVM的設計思路。

EVM代碼overview

主要代碼路徑爲cpp-ethereum/libevm和libethereum,代碼量不大,以下爲主要代碼。

EVMC.cpp
ExtVMFace.cpp
Instruction.cpp
interpreter.h
LegacyVMCalls.cpp
VM.cpp
VMCalls.cpp
VMFactory.cpp
VMOpt.cpp
VMSIMD.cpp
VmValidate.cpp
LegacyVM.cpp
Executive.cpp
ExtVm.cpp
下圖爲EVM中主要類的類圖:

以太坊中調用EVM相關代碼的入口存在於不同的階段,我們主要從以下幾個角度來看它是如何運作的</br>
  • 執行環境創建
  • 新合約的創建
  • 合約執行調用、
  • 執行後結果返回
  • 運行過程中的gas消費


執行環境的創建

以太坊虛擬中涉及到的虛擬機執行的關鍵類爲Executive(位於目錄libethereum)中,我們先從
創建運行環境入手,關鍵入口函數call入手,函數聲明爲:

bool call(Address const& _receiveAddress, //接收者地址
          Address const& _txSender,       //發送者地址
          u256 const& _txValue,           //transaction的值
          u256 const& _gasPrice,          //gas的價格
          bytesConstRef _txData,          //具體的transaction數據
          u256 const& _gas);              //提供的gas值
bool call(CallParameters const& _cp,      //運行參數,由上個函數的相關變量創建
          u256 const& _gasPrice,          //gas的價格
          Address const& _origin);        //源地址

兩個函數的功能是一樣的,只是第一個函數會將參數包裝成CallParameters然後在調用第二個函數,現在看

一下call函數的流程圖如下:



上述函數的關鍵點在於在不是預編譯合約(以太坊中內部定義好的合約多爲簽名計算),生成ExtVM

實例,這個對象將在函數go中返回重要的作用。注意函數最後會更改狀態指向函數transferBalance:

m_s.transferBalance(_p.senderAddress, _p.receiveAddress, _p.valueTransfer);

新合約的創建

如果調用的合約地址在系統中還沒有出存在,這個適合涉及到合約的創建,關鍵函數爲create:

bool create(Address const& _txSender, //目的地址
            u256 const& _endowment,   //花費的值
            u256 const& _gasPrice,    //gas的價錢
            u256 const& _gas,         //花費的gas數量
            bytesConstRef _init,      //傳入的字節碼
            Address const& _origin)   //源地址

函數將調用createOpcode函數,該函數在獲得sender的nonce值後生成合約地址,並且調用函數

executeCreate來完成最後的創建。

u256 nonce = m_s.getNonce(_sender);
//注意地址保存在m_newAddress中,這個是否足夠靈活
m_newAddress = right160(sha3(rlpList(_sender, nonce)));
return executeCreate(_sender, _endowment, _gasPrice, _gas, _init, _origin);

下面看關鍵的的函數executeCreate,其流程圖如下所示:


我們從代碼中不難看出,最後和call函數相同生成ExtVM對象,將要執行的字節碼放入其中。

這裏需要注意的是執行transferBalance時候如果沒有賬戶將創建一個新的賬戶。

合約執行調用

合約的執行的起始點在函數Executive::go中,函數的流程圖如下:



從流程圖中我們看到如果m_ext(create或者call函數中創建)不爲空的時候通過工廠方法創建VM,

這裏要正確的理解m_ext和vm的區別,我們先看工廠方法的實現:

std::unique_ptr<VMFace> VMFactory::create(VMKind _kind)
{
    switch (_kind)
    {
#ifdef ETH_EVMJIT
    case VMKind::JIT:
        return std::unique_ptr<VMFace>(new EVMC{evmjit_create()});
#endif
#ifdef ETH_HERA
    case VMKind::Hera:
        return std::unique_ptr<VMFace>(new EVMC{evmc_create_hera()});
#endif
    case VMKind::Interpreter:
        return std::unique_ptr<VMFace>(new EVMC{evmc_create_interpreter()});
    case VMKind::DLL:
        return std::unique_ptr<VMFace>(new EVMC{g_dllEvmcCreate()});
    case VMKind::Legacy:
    default:
        return std::unique_ptr<VMFace>(new LegacyVM);
    }
}
從代碼中我們不難看出,會根據虛擬機的類型創建對應的虛擬機對象,無參函數create將默認生成

類型爲VMKind::Legacy。在函數create和call中生成的m_ext是虛擬機和外部相關狀態進行交互的。

調用VM的exec函數完成虛擬機的執行。LegacyVM的exec函數關鍵代碼如下:

// trampoline to minimize depth of call stack when calling out
m_bounce = &LegacyVM::initEntry;//初始化m_bounce
do
    (this->*m_bounce)();
while (m_bounce);

初始化m_bounce的函數使initEntry,我們看下initEntry函數中實現的功能。

m_bounce = &LegacyVM::interpretCases;//設置m_bounce爲函數interpreCases
initMetrics();//初始化操作,參數和返回值,gas花費矩陣,
optimize();//優化,主要針對跳轉的優化,後續再做補充,目前程序彙總關閉

經過上面的代碼處理,m_bounce設置爲下一步將要運行的函數。初始化操作以及花費說明的矩陣,

矩陣關聯的Instruction部分相關信息如下:

static const std::map<Instruction,  InstructionInfo> c_instructionInfo =
{ //                                         Add,  Args,  Ret,  GasPriceTier
	{ Instruction::STOP,    { "STOP",           0,     0,    0,  Tier::Zero } },
	{ Instruction::ADD,     { "ADD",            0,     2,    1,  Tier::VeryLow } },
	{ Instruction::SUB,     { "SUB",            0,     2,    1,  Tier::VeryLow } },
	{ Instruction::MUL,     { "MUL",            0,     2,    1,  Tier::Low } },
	{ Instruction::DIV,     { "DIV",            0,     2,    1,  Tier::Low } },
	{ Instruction::SDIV,    { "SDIV",           0,     2,    1,  Tier::Low } },
	{ Instruction::MOD,     { "MOD",            0,     2,    1,  Tier::Low } },

經過上面的相關初始化,再運行循環將執行m_bounce指向的interpretCases函數。函數interpretCase

相對來說比較複雜一些,如下所示:

void LegacyVM::interpretCases()
{
  INIT_CASES
  DO_CASES
  {
    CASE(CREATE2)
    {
      ON_OP();
      if (!m_schedule->haveCreate2)
          throwBadInstruction();
          m_bounce = &LegacyVM::caseCreate;
		}
		BREAK
    CASE(CREATE)
    ........
    此處省略很多的case
    .......


    NEXT


    CASE(INVALID)
    DEFAULT
    {
        throwBadInstruction();
    }
	}  
  WHILE_CASES
}

上述代碼中對應了很多宏,,如果不對宏進行展開,直觀理解就是對每種操作執行對應的函數,並將

結果進行中間存儲,EVM中定義了相關宏開關,其中一些宏開關,都對應了上述代碼中不同的

宏展開,同時對應了不同的代碼組織形式。參見文件VMConfing.h,如下代碼爲相關的宏開關:

EIP_615                - 子程序和靜態跳轉的方式
EIP_616                - 單執行多數據流的方式
EVM_OPTIMIZE           - 優化開關,當值爲false時,所有的優化全部關掉
EVM_SWITCH_DISPATCH    - 通過loop和switch執行代碼
EVM_JUMP_DISPATCH      - 跳轉通過一個跳錶來實現,只針對gcc
EVM_USE_CONSTANT_POOL  - 應用靜態數據並在棧上直接賦值操作
EVM_REPLACE_CONST_JUMP - 帶預確認的跳轉來保證運行時的循環
EVM_TRACE              - 提供不同等級的trace操作

下面我們來看一下源代碼中是如何設置這些宏開關

首先EIP_615和EIP_616均爲關閉狀態
#ifndef EIP_615
#define EIP_615 false
#endif


#ifndef EIP_616
#define EIP_616 false
#endif
//--------------------------------------------------------------------------
//如果沒有定義EVM_JUMP_DISPATCH的情況下,如果是GNU的gcc,則打開EVM_JUMP_DISPATCH
//我們一般在linux編譯,則這裏我們可以得到的結論是開關EVM_JUMP_DISPATCH開關打開
ifndef EVM_JUMP_DISPATCH
#ifdef __GNUC__
#define EVM_JUMP_DISPATCH true
#else
#define EVM_JUMP_DISPATCH false
#endif
#endif


#if EVM_JUMP_DISPATCH
#ifndef __GNUC__
#error "address of label extension available only on Gnu"
#endif
#else
#define EVM_SWITCH_DISPATCH true
#endif
//---------------------------------------------------------------------------
//從下面的宏定義EVM_OPTIMIZE的開關是關閉的,因此開關EVM_REPLACE_CONST_JUMP</br>
//EVM_USE_CONSTANT_POOL EVM_DO_FIRST_PASS_OPTIMIZATION均爲false的狀態
#ifndef EVM_OPTIMIZE
#define EVM_OPTIMIZE false
#endif
#if EVM_OPTIMIZE
#define EVM_REPLACE_CONST_JUMP true
#define EVM_USE_CONSTANT_POOL true
#define EVM_DO_FIRST_PASS_OPTIMIZATION \
       (EVM_REPLACE_CONST_JUMP || EVM_USE_CONSTANT_POOL)
#endif
綜上宏開關的說明,最後開關EVM_JUMP_DISPATCH是打開的,其他的均處於關閉狀態。

在開關EVM_JUMP_DISPATCH打開的情況下對每一個宏展開進行說明,便於理解實際是如何運

行的。具體的參見文件VMConfig.h

宏INIT_CASES
在開關EVM_JUMP_DISPATCH打開的情況下該宏展開爲初始化一個靜態的跳轉表,代碼片段如下:
#define INIT_CASES                              \
                                                \
    static const void* const jumpTable[256] = { \
        &&STOP, /* 00 */                        \
        &&ADD,                                  \
        &&MUL,                                  \
        &&SUB,                                  \
        &&DIV,                                  \
        &&SDIV,                                 \
        &&MOD,                                  \
        &&SMOD,                                 \
        &&ADDMOD,                               \

其中的ADD等定義在文件Instruction.h中,其中枚舉了所有的虛擬機執行過程中的字節操作碼。

/// Virtual machine bytecode instruction.
enum class Instruction: uint8_t
{
	STOP = 0x00,        ///< halts execution
	ADD,                ///< addition operation
	MUL,                ///< mulitplication operation
	SUB,                ///< subtraction operation
	DIV,                ///< integer division operation
	SDIV,               ///< signed integer division operation
  .......
  .......

宏DO_CASES

在開關EVM_JUMP_DISPATCH打開的情況下該宏展開爲如下代碼片段:

#define DO_CASES        \
    fetchInstruction(); \
    goto* jumpTable[(int)m_OP];
由上面的代碼可知操作符定義爲一個字節,函數fetchInstruction執行後m_OP中存儲了當前要
執行的操作,接着switch將根據具體的操作來執行分支中的內容。
宏CASES
在開關EVM_JUMP_DISPATCH打開的情況下CASES展開的內容如下 :
#define CASE(name) \
    name:
宏NEXT

在開關EVM_JUMP_DISPATCH打開的情況下NEXT展開的內容如下 :

#define NEXT            \
    ++m_PC;             \
    fetchInstruction(); \
    goto* jumpTable[(int)m_OP];
宏CONTINUE
在開關EVM_JUMP_DISPATCH打開的情況下CONTINUE展開的內容如下 :
#define CONTINUE        \
    fetchInstruction(); \
    goto* jumpTable[(int)m_OP];
其他的幾個控制分支跳轉的宏展開如下:
#define BREAK return;
#define DEFAULT
#define WHILE_CASES

關鍵的執行操作的宏ON_OP展開如下:

#define ON_OP() onOperation()
通過上面的宏的展開我們可以看出過程主要依賴jumpTable和函數fetchInstructio來完成
執行過程。首先我們看一下函數fetchInstruction的具體執行流程,整個執行將從這裏開始
//初始化時m_PC爲0,這裏先獲取第一個操作符
m_OP = Instruction(m_code[m_PC]);
//獲取操作相關的參數,gas花費等信息
const InstructionMetric& metric = c_metrics[static_cast<size_t>(m_OP)];
//設置SP爲最後返回值所在的位置,同時檢查參數出棧和返回值入棧沒有超過棧的邊界
adjustStack(metric.args, metric.ret);


//計算運行的費用
m_runGas = toInt63(
         m_schedule->tierStepGas[static_cast<unsigned>(metric.gasPriceTier)]
        );
m_newMemSize = m_mem.size();
m_copyMemSize = 0;

下面我們看程序是如何利用跳錶和onOpearion來完成程序的執行,下面我們以ADD操作

爲例來進行說明,在進行ADD說明前,先簡單介紹一下EVM中棧工作的原理,例如如果我

們要執行一

加法操作,則通常表示爲c = a + b,把它翻譯成EVM中棧的操作序列僞操作碼序列如下:

push a into stack
push b into stack
pop a and b then cal a+b
push a+b into stack

我們先看一下ADD的前置操作PUSH是如何實現的,首先注意如下代碼:

CASE(PUSH2)
    .........
		CASE(PUSH23)
		CASE(PUSH24)
		CASE(PUSH25)
		CASE(PUSH26)
		CASE(PUSH27)
		CASE(PUSH28)
		CASE(PUSH29)
		CASE(PUSH30)
		CASE(PUSH31)
		CASE(PUSH32)
		{
      ON_OP();
      updateIOGas();
      int numBytes = (int)m_OP - (int)Instruction::PUSH1 + 1;
      m_SPP[0] = 0;
      for (++m_PC; numBytes--; ++m_PC)
      //這裏主要是處理256位寬的的情況
      m_SPP[0] = (m_SPP[0] << 8) | m_code[m_PC];
		}
		CONTINUE

EVM中對PUSH1的其他操作均採用上面的代碼,PUSH3的含義是將三個輸入的字節序列中的寬度

爲32字節的數據壓入到棧中,如下代碼:

m_SPP[0] = (m_SPP[0] << 8) | m_code[m_PC];

上面這段代碼主要是將傳入的字節流數據轉化爲256的數據,並放入棧上,同時我們的棧的定義如下:

u256 m_stack[1024];
這裏的設計其實有可以改進的地方,主要是以下幾點:
  • 即使編譯器部分做了數據緊湊的優化,但是執行的過程中又做了放大,失去了原來的意義。
  • 棧的寬度爲256位,拋開加密算法的影響(目前幾乎沒有棧上運算),實際有些浪費。
  • 從實際角度講可以選擇小的位寬的棧,數據更加緊密,但是可能會造成操作增加。

通過執行完PUSH操作後,棧上的已經放好了我們要做加法的數據,現在我們看一下ADD操作是如何的。

如下爲ADD操作的代碼片段:

CASE(ADD)
{
  ON_OP();
  updateIOGas();


  //pops two items and pushes their sum mod 2^256.
  m_SPP[0] = m_SP[0] + m_SP[1];
}
NEXT

上面的代碼通過宏展開後代碼如下:

ADD:
{
   onOperation();
   updateIOGas();
   m_SPP[0] = m_SP[0] + m_SP[1];
}
++m_PC;             
fetchInstruction();
goto* jumpTable[(int)m_OP];

現在step by step的看一下這部分是如何進行處理,首先是opOperaion函數,如下爲onOperation

函數的實現:

if (m_onOp)
  (m_onOp)(++m_nSteps, m_PC, m_OP,
  m_newMemSize > m_mem.size() ? (m_newMemSize - m_mem.size()) / 32 : uint64_t(0),
  m_runGas, m_io_gas, this, m_ext);

代碼中的m_onOp爲提供的回調函數,其中其中m_onOp的類型爲OnOpFunc,聲明如下如下所示:

using OnOpFunc = std::function<void(uint64_t /*steps*/,
                                    uint64_t /* PC */,
                                    Instruction /*instr*/,
                                    bigint /*newMemSize*/,
                                    bigint /*gasCost*/,
                                    bigint /*gas*/,
                                    VMFace const*,
                                    ExtVMFace const*)>;
函數設計主要是在虛擬執行的時候提供一個可以回調的接口,方便進行處理,比如做最簡單的tracing

接下來執行函數updateIOGas(),該函數功能主要是查看目前的gas消耗是否已經大於提供的gas,

如果大於,則拋出異常,虛擬機停止運行。代碼片段如下:

if (m_io_gas < m_runGas)
  throwOutOfGas();
m_io_gas -= m_runGas;

注意函數中的m_runGas在函數updateGas中被修改,會根據內存的使用來進行計算和消耗。接下來執

行的代碼爲m_SPP[0] = m_SP[0] + m_SP[1];主要功能是完成兩個數的相加計算結果會存儲在m_SPP[0]

中,m_SPP的含義是“指向下一個棧中可用的位置”,這裏指向m_SP[0]運行初始化開始時m_SPP = m_SP

接着執行++m_PC,這裏將指向代碼字節流中的下一個操作。接着執行數fetchInstruction(),這個時候

變量m_OP中存儲的是新的操作,繼續執行goto* jumpTable[(int)m_OP],這個時候將跳到下一個操作的

label去執行。

綜上,EVM虛擬機就是藉助棧和基本的操作來完成一個合約字節流的運行的。

執行後的結果返回

在操作序列的RETURN和REVERT操作,將棧上的結果數據拷貝到內存中去。以下爲RETURN部分代碼
CASE(RETURN)
{
  ON_OP();
  m_copyMemSize = 0;
  updateMem(memNeed(m_SP[0], m_SP[1]));
  updateIOGas();


  uint64_t b = (uint64_t)m_SP[0];
  uint64_t s = (uint64_t)m_SP[1];
  //m_output中存儲的就是最後返回的結果
  m_output = owning_bytes_ref{std::move(m_mem), b, s};
  m_bounce = 0;
}
BREAK

運行過程中的Gas的消費

上面描述ADD和所涉及到的PUSH操作的時候我們就已經看到會調用updateIOGas函數來計算gas

的消費。從代碼中我們可以看到不同的操作對應不同的gas消費具體如下:

no gasIO gas 1IO gas 2mem gas
CREATE2RETURNREVERTRETURN
CREATESUICIDESTOPREVERT
DELEGATECALLMLOADMSTOREMLOAD
STATICCALLMSTORE8SHA3MSTORE
CALLLOG0LOG1MSTORE8
CALLCODELOG2LOG3SHA3
JUMPTOEXPADDLOG1
JUMPIFMULSUBLOG2
JUMPVDIVSDIVLOG3
JUMPSUBMODSMODCALLDATACOPY
JUMPSUBVNOTLT 
RETURNSUBGTSLT 
BEGINSUBSGTEQ 
BEGINDATAISZEROAND 
GETLOCALORXOR 
PUTLOCALBYTESHL 
SHRSAR  
ADDMODMULMOD  
SIGNEXTENDADDRESS  
ORIGINBALANCE  
CALLERCALLVALUE  
CALLDATALOADCALLDATASIZE  
RETURNDATASIZECODESIZE  
EXTCODESIZECALLDATACOPY  
RETURNDATACOPYRETURNDATACOPY  
CODECOPYCODECOPY  
GASPRICEBLOCKHASH  
COINBASETIMESTAMP  
NUMBERDIFFICULTY  
GASLIMITPOP  
PUSHJUMP  
DUPSWAP  
SLOADSSTORE  
PCMSIZE  
GASJUMPDEST  
INVALID 
其中SSTORE是一條比較特殊的指令,他將消耗stack存儲的gas

小結

EVM如果精確的定位應該是一個基於棧的自定義字節碼解釋器,實現上並不複雜,代碼過程中遇到

的主要問題有以下幾點:

  • 字節寬度的設計,目前看主要是出於實現難度和實際需求(如SHA3的操作)
  • 設計上每次都會生成新的VM對象,出於安全和目前需求這樣的設計和實現是可以接受的,數據

共享和多線程處理在智能合約的層面目前還不需要。

  • EVM整體上實現了基本的操作,但是還需要和語言編譯器去結合來做具體的優化,JIT是一個方向。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章