1. 程式人生 > >EOS虛擬機器與智慧合約詳解與分析

EOS虛擬機器與智慧合約詳解與分析

EOS智慧合約和虛擬機器分析

EOS虛擬機器同經典的EVM,是EOS中執行智慧合約的容器,但是從設計上講它與EOS.IO是分離的。進 一步指令碼語言和虛擬機器的技術設計與EOS.IO分離。從巨集觀來講任何語言或者虛擬機器,只要滿足條件適 合沙盒模式執行,同時滿足一定的執行效率,都可以通過滿足EOS.IO提供的API來加入到EOS.IO的消 息傳遞過程中。以下為github上官方的說明:

The EOS.IO software will be first and foremost a platform for coordinating the delivery of authenticated messages (called Actions) to accounts. The details  of scripting language and virtual machine are implementation specific details that are mostly independent from the design of the EOS.IO technology. Any  language or virtual machine that is deterministic and properly sandboxed with sufficient performance can be integrated with the EOS.IO software API.

本文就EOSIO中的智慧合約和虛擬機器進行分析來從更加全面的角度來看EOS是如何構建和實現。

目錄

總結

EOS智慧合約和虛擬機器分析 相關背景知識 LLVM相關內容 LLVM架構概述 LLVM IR介紹與分析 LLVM IR格式 LLVM IR指令集 LLVM IR型別系統 LLVM IR記憶體模型 LLVM IR函式呼叫 LLVM IR示例 LLVM JIT介紹與分析 LLVM JIT實現原理 LLVM JIT程式碼示例 WebAssembly相關內容 WebAssembly概述 WebAssembly格式介紹與分析 WebAssembly WAST格式介紹 WebAssembly WASM格式介紹 WASM執行介紹與分析 EOS智慧合約分析 EOS智慧合約概覽 EOS智慧合約模型和執行流程 EOS智慧合約與Action EOS智慧合約執行流程 inline Communication Deferred Communication 執行流程示例 EOS智慧合約示例說明 EOS智慧合約相關工具 EOS虛擬機器分析 EOS虛擬機器概覽 EOS虛擬機器實現思路分析 EOS虛擬機器架構概述 EOS虛擬機器實現與分析 EOS虛擬機器核心介面 EOS虛擬機器架構應用層 EOS虛擬機器客戶端合約部署 EOS虛擬機器服務端合約部署 EOS虛擬機器服務端合約的呼叫執行 EOS虛擬機器Module IR生成 VirtualMachine例項化 Binaryen底層直譯器 ModuleInstance的建立 Appply介面的實現和呼叫 CallFunction的實現 WAVM底層直譯器 ModuleInstance的生成 Apply介面實現和呼叫 InvokeFunction的實現 總結

相關背景知識

LLVM相關內容

LLVM相關技術的理解對於我們深入理解EOS虛擬機器的執行機制至關重要,所以必要的LLVM的相關知 識在這裡是需要的。同時LLVM作為一個成熟的編譯器後端實現,無論從架構還是相關設計思想以及相 關的工具的實現都是值得學習的。

LLVM架構概述

概括來講LLVM專案是一系列分模組、可重用的編譯工具鏈。它提供了一種程式碼良好的中間表示(IR), LLVM實現上可以作為多種語言的後端,還可以提供與語言無關的優化和針對多種CPU的程式碼生成功能。 最初UIUC的Chris Lattner主持開發了一套稱為LLVM(Low Level Virtual Machine)的編譯器工具庫套 件,但是後來隨著LLVM的範圍的不斷擴大,則這個簡寫並不代表底層虛擬機器的含義,而作為整個專案 的正式名稱使用,並一直延續至今。所以現在的LLVM並不代表Low Level Virtual Machine。

The LLVM Project is a collection of modular and reusable compiler and toolchain technologies. Despite its name, LLVM has little to do with traditional virtual machines. The name "LLVM" itself is not an acronym; it is the full name of the project.

LLVM不同於傳統的我們熟知的編譯器。傳統的靜態編譯器(如gcc)通常將編譯分為三個階段,分別 由三個元件來完成具體工作,分別為前端、優化器和後端,如下圖所示。

LLVM專案在整體上也分為三個部分,同傳統編譯器一致,如下圖所示,不同的語言的前端,統一的 優化器,以及針對不同平臺的機器碼生成。從圖2我們也可以得到啟發,如果想實現一門自定義的 語言,目前主要的工作可以集中在如何實現一個LLVM的前端上來。

LLVM的架構相對於傳統編譯器更加的靈活,有其他編譯器不具備的優勢,從LLVM整體的流程中我 們就可以看到這一點,如下圖所示為LLVM整體的流程,編譯前端將原始碼編譯成LLVM中間格式的文 件,然後使用LLVM Linker進行連結。Linker執行大量的連結時優化,特別是過程間優化。連結得 到的LLVM code最終會被翻譯成特定平臺的機器碼,另外LLVM支援JIT。原生代碼生成器會在程式碼 生成過程中插入一些輕量級的操作指令來收集執行時的一些資訊,例如識別hot region。執行時收 集到的資訊可以用於離線優化,執行一些更為激進的profile-driven的優化策略,調整native code 以適應特定的架構。

從圖中我們也可以得出LLVM突出的幾個優勢:

  • 持續的程式資訊,每個階段都可以獲得程式的資訊內容
  • 離線程式碼生成,產生較高的可執行程式
  • 便捷profiling及優化,方便優化的實施
  • 透明的執行時模型
  • 統一,全程式編譯

LLVM IR介紹與分析

根據編譯原理可知,編譯器不是直接將源語言翻譯為目標語言,而是翻譯為一種“中間語言”,即 "IR"。之後再由中間語言,利用後端程式翻譯為目標平臺的組合語言。由於中間語言相當於一款編 譯器前端和後端的“橋樑”,不同編譯器的中間語言IR是不一樣的,IR語言的設計直接會影響到編 譯器後端的優化工作。LLVM IR官方介紹見:http://llvm.org/docs/LangRef.html

LLVM IR格式

The LLVM code representation is designed to be used in three different forms: as an  in-memory compiler IR, as an on-disk bitcode representation (suitable for fast loading by a Just-In-Time compiler), and as a human readable assembly language representation. This allows LLVM to provide a powerful intermediate representation for efficient compiler transformations and analysis, while providing a natural means to debug and visualize the transformations.

由上訴的引用得知目前LLVM IR提供三種格式,分別是記憶體裡面的IR模型,儲存在磁碟上的二進位制 格式,儲存在磁碟上的文字可讀格式。三者本質上沒有區別,其中二進位制格式以bc為副檔名, 文字格式以ll為副檔名。除了以上兩個格式檔案外,和IR相關的檔案格式還有s和out檔案,這 兩種一個是由IR生成彙編的格式檔案,一個是生成的可執行檔案格式(linux下如ELF格式),

  • bc結尾,LLVM IR檔案,二進位制格式,可以通過lli執行
  • ll結尾,LLVM IR檔案,文字格式,可以通過lli執行
  • s結尾,本地彙編檔案
  • out, 本地可執行檔案

以上幾種不同檔案的轉化圖如下所示,整體上我們可以看一下這幾種格式的轉化關係,同時從中 我們也可以看出工具clang、llvm-dis、llvm-as等工具的作用和使用。

中間語言IR的表示,一般是按照如下的結構進行組織的由外到內分別是:

  • 模組(Module)
  • 函式(Function)
  • 程式碼塊(BasicBlock)
  • 指令(Instruction)

模組包含了函式,函式又包含了程式碼塊,後者又是由指令組成。除了模組以外,所有結構都是從 值產生而來的。如下為一個ll檔案的片段,從中可以簡單的看出這種組織關係。

; ModuleID = 'main.ll'
source_filename = "main.c"
target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-unknown-linux-gnu"
 
; Function Attrs: noinline nounwind uwtable
define i32 @add(i32, i32) #0 {
  %3 = alloca i32, align 4
  %4 = alloca i32, align 4
  store i32 %0, i32* %3, align 4
  store i32 %1, i32* %4, align 4
  %5 = load i32, i32* %3, align 4
  %6 = load i32, i32* %4, align 4
  %7 = add nsw i32 %5, %6
  ret i32 %7
}

LLVM IR指令集 指令集的分類大致可以分為基於棧的,基於運算器的還有基於暫存器的,基於棧的和基於暫存器 的虛擬機器目前是比較常見的,兩種不同之處主要在執行效率,指令集大小和效能三個方面。LLVM IR採用的是基於暫存器的滿足RISC架構以及load/store模式,也就是說只能通過將load和store 指令來進行CPU和記憶體間的資料交換。LLVM IR指令集擁有普通CPU一些關鍵的操作,遮蔽掉了 一些和機器相關的一些約束。LLVM提供了足夠多的暫存器來儲存基本型別值,暫存器是為SSA形 式(靜態單態賦值),這種形式的UD鏈(use-define chain, 賦值代表define, 使用變數代表use) 便於優化。LLVM指令集僅包含31條操作碼。LLVM中的記憶體地址沒有使用SSA形式,因為記憶體地 址有可能會存在別名或指標指向,這樣就很難構造出來一個緊湊可靠的SSA表示。在LLVM中一個 function就是一組基本塊的組合,一個基本塊就是一組連續執行的指令並以中指指令結束 (包括branch, return, unwind, 或者invoke等),中止指令指明瞭欲跳轉的目的地址。

LLVM IR型別系統 LLVM的型別系統為語言無關。每一個SSA暫存器或者顯示的記憶體物件都有其對應的型別。這些類 型和操作碼一起表明這個操作的語義,這些型別資訊讓LLVM能夠在低層次code的基礎上進行一 些高層次的分析與轉換,LLVM IR包含了一些語言共有的基本型別,並給他們一些預定義的大小, 從8bytes到64bytes不等,基本型別的定義保證了LLVM IR的移植性。同時LLVM又包含了四種複雜 型別,pointer,arrays, structures和functions。這四種類型足夠表示現有的所有語言型別。為 了支援型別轉換,LLVM提供了一個cast操作來實現型別的轉換,同時為了支援地址運算,LLVM 提供了getelementptr的命令。LLVM中的許多優化都是基於地址做的(後續的總結再分析)。

LLVM IR記憶體模型 LLVM提供特定型別的記憶體分配,可以使用malloc指令在堆上分配一個或多個同一型別的記憶體物件, free指令用來釋放malloc分配的記憶體(和C語言中的記憶體分配類似)。另外提供了alloca指令用於 在棧上分配記憶體物件,該記憶體物件在通常在函式結尾會被釋放。統一記憶體模型,所有能夠取地址的 物件都必須顯示分配。區域性變數也要使用alloca來顯示分配,沒有隱式地手段來獲取記憶體地址,這就 簡化了關於記憶體的分析。

LLVM IR函式呼叫 LLVM中對普通函式呼叫,LLVM提供了call指令來呼叫附帶型別資訊的函式指標。這種抽象遮蔽了 機器相關的呼叫慣例。還有一個不能忽略的就是異常處理,在LLVM中,LLVM提供了invoke和 unwind指令。invoke指令指定在棧展開的過程中必須要執行的程式碼,例如棧展開的時候需要析構 區域性物件等。而unwind指令用於丟擲異常並執行棧展開的操作。棧展開的過程會被invoke指令停 下來,執行catch塊中的行為或者執行在跳出當前活動記錄之前需的操作。執行完成後繼續程式碼執 行或者繼續棧展開操作。注意像C++的RTTI則由C++自己的庫處理,LLVM並不負責。

LLVM IR示例 下面我們編寫一個簡短的程式並編譯成LLVM IR的形式來看LLVM的IR的具體格式和結構如下為一 段程式,儲存為main.c

#include <stdio.h>
int add(int a, int b)
{
    return (a + b);
}
int main(int argc, char** argv)
{
    add(3, 5);
    return 0;
}

我們使用命令clang -o0 -emit-llvm main.c -S -o main.ll編譯生成ll檔案,ll檔案為文字可見 檔案,內容如下:

; ModuleID = 'main.c'
source_filename = "main.c"
target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-unknown-linux-gnu"
//函式特徵如inline
; Function Attrs: noinline nounwind uwtable
define i32 @add(i32, i32) #0 {    //@代表是全域性屬性 i32為資料型別
%3 = alloca i32, align 4          //申請空間存放變數,%為區域性屬性
%4 = alloca i32, align 4          //3,4用來存放傳入的引數,aling為位寬
store i32 %0, i32* %3, align 4    //將傳入的引數放到是對應的儲存位置
store i32 %1, i32* %4, align 4
%5 = load i32, i32* %3, align 4   //將引數存到待運算的臨時變數中
%6 = load i32, i32* %4, align 4
%7 = add nsw i32 %5, %6           //執行具體的相加操作
ret i32 %7                        //最後返回結果
}
; Function Attrs: noinline nounwind uwtable
define i32 @main(i32, i8**) #0 {
%3 = alloca i32, align 4
%4 = alloca i32, align 4
%5 = alloca i8**, align 8
store i32 0, i32* %3, align 4
store i32 %0, i32* %4, align 4
store i8** %1, i8*** %5, align 8
%6 = call i32 @add(i32 3, i32 5)
ret i32 0
}

以上程式碼不難發現函式add的展開中有部分臨時變數的浪費,更為簡潔的表達可以如下,當然 際的優化到什麼程度要看後續的具體的實現。

%3 = add nsw i32 %1, %0
ret i32 %3

LLVM JIT介紹與分析

JIT技術Just-In-Time Compiler,是一種動態編譯中間程式碼的方式,根據需要,在程式中編 譯並執行生成的機器碼,能夠大幅提升動態語言的執行速度。LLVM設計上考慮瞭解釋執行 的功能,這使它的IR可以跨平臺去使用,程式碼可以方便地跨平臺執行,同時又具有編譯型語言 的優勢,非常的方便。像Java語言,.NET平臺等,廣泛使用JIT技術,使得程式達到了非常 高的執行效率,逐漸接近原生機器語言程式碼的效能。

LLVM JIT實現原理 JIT引擎的工作原理並沒有那麼複雜,本質上是將原來編譯器要生成機器碼的部分要直接寫 入到當前的記憶體中,然後通過函式指標的轉換,找到對應的機器碼並進行執行。實際編寫 過程中往往需要處理例如記憶體的管理,符號的重定向,處理外部符號等問題。實現一個LLVM 的位元組碼(bc)的直譯器其實並不複雜最好的例項就是LLVM自身的直譯器lli,其總共不超過 800行程式碼實現了一個LLVM的位元組碼直譯器,其原始碼的github地址為:https://github.com/llvm-mirror/llvm/blob/master/tools/lli/lli.cpp

LLVM JIT程式碼示例 下面就以LLVM原始碼中的例子來解釋LLVM-JIT是如何使用和執行的,在這之前,我們需 要明確llvm中常用的語句表達結構為module-->function-->basicblock-->instruction -->operator 我們主要分析原始碼example/HowToUseJIT部分的程式碼,主要程式碼片段如下: 該例子中在記憶體中建立了一個LLVM的module,這個module包含如下兩個function:

int add1(int x) {
  return x+1;
}
int foo() {
  return add1(10);
}

針對以上兩個函式,建立LLVM記憶體中IR中間格式的程式碼如下:

//首先包含llvm JIT需要的相關標頭檔案


#include "llvm/ADT/STLExtras.h"
#include "llvm/ExecutionEngine/ExecutionEngine.h"
#include "llvm/ExecutionEngine/GenericValue.h"
...............
...............
#include "llvm/Support/raw_ostream.h"
#include <algorithm>
#include <cassert>
#include <memory>
#include <vector>
using namespace llvm;
 
int main() {
  InitializeNativeTarget(); //初始化本地執行環境,和具體的機器相關
  LLVMContext Context;      //定義一個LLVM的上下文變數
  //建立一個module物件,以便後續我們可以把function放入其中
  //這裡這個module物件的名字是text,關聯的上下文為上面宣告
  std::unique_ptr<Module> Owner = make_unique<Module>("test", Context);
  Module* M = Owner.get();
  //建立add1函式物件,並把該物件加入到module中,
  Function* Add1F = cast<Function>(M->getOrInsertFunction(
                                  "add1",  //函式的名字為add1
                                  Type::getInt32Ty(Context),//函式的引數為int32
                                  Type::getInt32Ty(Context))); //函式的返回值為int32
  //建立一個塊,並把塊關聯到add1函式上,注意函式的最後一個引數
  BasicBlock* BB = BasicBlock::Create(Context, "EntryBlock", Add1F);
  //建立一個basic block的builder,這個builder的工作就是將instructions新增到
  //basic block中去
  IRBuilder<> builder(BB);
  //獲得一個指向常量數字1的指標
  Value* One = builder.getInt32(1);
  //獲得指向函式add1第一個引數的指標
  assert(Add1F->arg_begin() != Add1F->arg_end()); // 確保有引數
  Argument* ArgX = &* Add1F->arg_begin();          // 獲得引數指標
  ArgX->setName("AnArg");        
      // 設定引數名稱,便於後續的查詢
  //建立加1的指令,並把指令放入到塊的尾部
  Value* Add = builder.CreateAdd(One, ArgX);
  //建立返回指令, 至此add1的函式已經建立完畢
  builder.CreateRet(Add);
  //建立函式foo
  Function* FooF = cast<Function>(M->getOrInsertFunction(
                                  "foo", Type::getInt32Ty(Context)));
  BB = BasicBlock::Create(Context, "EntryBlock", FooF);
  //通知builder關聯到一個新的block上
  builder.SetInsertPoint(BB);
  Value* Ten = builder.getInt32(10);
  //建立一個函式的呼叫,並把引數傳遞進去
  CallInst* Add1CallRes = builder.CreateCall(Add1F, Ten);
  Add1CallRes->setTailCall(true);
  //建立返回結果
  builder.CreateRet(Add1CallRes);
  // 建立JIT引擎,建立引數為上下文
  ExecutionEngine* EE = EngineBuilder(std::move(Owner)).create();
  outs() << "We just constructed this LLVM module:\n\n" << * M;
  outs() << "\n\nRunning foo: ";
  outs().flush();
  //呼叫函式foo
  std::vector<GenericValue> noargs;
  GenericValue gv = EE->runFunction(FooF, noargs);
  //獲得函式返回值
  outs() << "Result: " << gv.IntVal << "\n";
  delete EE;
  //關閉LLVM虛擬機器
  llvm_shutdown();
  return 0;
}

以上程式碼在記憶體中建立了LLVM IR,並呼叫LLVM JIT的執行引擎執行程式碼,從中我們得到啟 發是如果我們藉助LLVM JIT執行我們的合約程式碼,我們就需要將合約程式碼最終轉化為LLVM 能識別的中間程式碼IR上,下面將一步一步的分析EOS中是如何利用LLVM-JIT技術實現的虛 擬機執行。

WebAssembly相關內容

WebAssembly概述

WASM在瀏覽器中執行的效果和Java語言在瀏覽器上的表現幾近相同的時候,但是WASM 不是一種語言,確切的說WASM是一種技術方案,該技術方案允許應用諸如C、C++這種 程式語言編寫執行在web瀏覽其中的程式。更加細節的去講,WASM是一種新的位元組碼格 式,是一種全新的底層二進位制語法。突出的特點就是精簡,載入時間短以及高速的執行模 型。還有一點比較重要,那就是它設計為web多語言程式設計的目標檔案格式。具體可見官網 相關介紹:https://webassembly.org/

WebAssembly格式介紹與分析

WebAssembly同LLVM的IR類似,提供兩種格式,分別為可讀的文字格式wast和二進 制格式wasm,兩者最終是等價的,可以通過工具wast2wasm完成wast到wasm的格式轉 而工具wasm2wast則執行這一過程的返作用。

WebAssembly WAST格式介紹 為了能夠讓人閱讀和編輯WebAssembly,wasm二進位制格式提供了相應的文字表示。這 是一種用來在文字編輯器、瀏覽器開發者工具等工具中顯示的中間形式。下面將用基本 語法的方式解釋了這種文字表示是如何工作的以及它是如何與它表示的底層位元組碼。

無論是二進位制還是文字格式,WebAssembly程式碼中的基本單元是一個模組。在文字格式 中,一個模組被表示為一個S-表示式。S-表示式是一個非常古老和非常簡單的用來表示樹 的文字格式。具體介紹:https://en.wikipedia.org/wiki/S-expression 因此,我們可以 把一個模組想象為一棵由描述了模組結構和程式碼的節點組成的樹。與程式語言的抽象語 法樹不同的是,WebAssembly的樹是平坦的,也就是大部分包含了指令列表。樹上的 每個一個節點都有一對括號包圍。括號內的第一個標籤表示該節點的型別,其後跟隨的 是由空格分隔的屬性或孩子節點列表。因此WebAssembly的S表示式結構大概如下所示:

(module (memory 1) (func))

上面的表示式的含義是模組module包含兩個孩子節點,分別是屬性為1的記憶體節點,和 函式func節點。從上面我們知道一個空的模組定義為module,那將一個空的模組轉化為 wasm將是什麼格式,如下所示:

0000000: 0061 736d ; WASM_BINARY_MAGIC
0000004: 0d00 0000 ; WASM_BINARY_VERSION

WebAssembly模組中的所有程式碼都是包含函式裡面。函式的結構如下所示:

( func [signature] [locals] [body] )
  • signature 函式的簽名宣告函式的引數和返回值
  • local 區域性變數,聲明瞭具體的型別
  • body 為函式體,一個低階的的指令的線性列表

關於資料型別這裡簡單說明一下,wasm目前有四種可用的資料型別,分別為i32 i64 f32 f64 關於簽名我們來看一個簽名的具體例子,如下所示表示函式需要兩個引數,均為i32型別,  返回值是一個f64型別,引數可以看成是函式呼叫過程中傳遞過來的實參初始化後的區域性變數。

(func (param i32) (param i32) (result f64) ... )

關於區域性變數這裡需要注意兩個操作:get_local和set_local,先看下面的例子:

(func (param i32) (param f32) (local f64) get_local 0 get_local 1 get_local 2)
  • get_local 0會得到i32型別的引數
  • get_local 1會得到f32型別的引數
  • get_local 2會得到f64型別的區域性變數

為了便於識記,可以定義變數名的方式來取代索引的方式,具體如下:

(func (param $p1 i32) (param $p2 f32) (local $loc i32) …)

關於函式體,在具體介紹函式體之前,我們要明確的一點是,雖然wasm被設計成高效執行 的程式碼,但是最後wasm的執行依然是一個棧式機器定義的,下面我們參考如下程式碼:

(func (param $p i32) ..get_local $p get_local $p i32.add)

上面函式的功能概括為i+i,即計算表達是$p+$p的結果,結果將放在最後執行的棧的頂部。 現在我們完整的寫出一個module,該module就包含上述的功能,具體的S表示式如下:


(module
  (func (param $lhs i32) (param $rhs i32) (result i32)
    get_local $lhs
    get_local $rhs
    i32.ad
  )
)

上面的描述似乎缺少了什麼,那就我們如何才能使用這個函式,於是涉及到函式的匯出和呼叫。 wasm中是通過export來完成匯出的,通過call關鍵字來完成函式呼叫的,如下一個更加複雜 的例子:

(module
  (func $getNum (result i32)
    i32.const 42)
  (func (export "getPlus") (result i32)
    call $getNum
    i32.const 1
    i32.add
  )
)

函式執行最後的結果在棧頂儲存43這個元素,注意其中的(export "getPlus")也可以通過如下的 方式(export "getPlus" (func $getPlus))的方式匯出。最後一個問題wasm如何匯入函式? 下面我們看一個具體的例子 :

(module</br>
  (import "console" "log" (func $log (param i32)))
  (func (export "logIt")
    i32.const 13
    call $log))

WebAssembly使用了兩級名稱空間,這裡的匯入語句是說我們要求從console模組匯入log函 數。匯出的logIt函式使用call指令呼叫了匯入的函式。小結: 到目前為止我們熟悉了wast的具體格式,關於wast中的外部記憶體使用,表格等高階內容 可以單獨去了解。

WebAssembly WASM格式介紹 wasm為WebAssembly的二進位制格式,可以通過工具wast2wasm將wast轉化為wasm格式,下 面將如下wast轉化為wasm, 命令為wat2wasm simple.wast -o simple.wasm 上述工具的地址為:https://github.com/WebAssembly/wabt/

(module
  (func $getNum (result i32)
    i32.const 42)
  (func (export "getPlus") (result i32)
    call $getNum
    i32.const 1
    i32.add
  )
)

雖然編譯好的二進位制檔案沒有辦法進行直觀的讀取,但是可以藉助wat2wasm工具進行檢視其 verbose的輸出,命令為:./wat2wasm test.wat -v輸出結果為如下,通過對如下位元組流的理 我們可以清晰看到wasm的二進位制流格式是什麼樣的,以及它是如何執行的。基於以下的程式碼我 可以自己構建一個wasm的解析引擎,引擎需要使用暫存器的設計加上棧的執行控制。

0000000: 0061 736d                                 ; WASM_BINARY_MAGIC
0000004: 0100 0000                                 ; WASM_BINARY_VERSION
; section "Type" (1)
0000008: 01                                        ; section code
0000009: 00                                        ; section size (guess)
000000a: 01                                        ; num types
; type 0
000000b: 60                                        ; func
000000c: 00                                        ; num params
000000d: 01                                        ; num results
000000e: 7f                                        ; i32
0000009: 05                                        ; FIXUP section size
; section "Function" (3)
000000f: 03                                        ; section code
0000010: 00                                        ; section size (guess)
0000011: 02                                        ; num functions
0000012: 00                                        ; function 0 signature index
0000013: 00                                        ; function 1 signature index
0000010: 03                                        ; FIXUP section size
; section "Export" (7)
0000014: 07                                        ; section code
0000015: 00                                        ; section size (guess)
0000016: 01                                        ; num exports
0000017: 07                                        ; string length
0000018: 6765 7450 6c75 73                        getPlus  ; export name
000001f: 00                                        ; export kind
0000020: 01                                        ; export func index
0000015: 0b                                        ; FIXUP section size
; section "Code" (10)
0000021: 0a                                        ; section code
0000022: 00                                        ; section size (guess)
0000023: 02                                        ; num functions
; 上面的程式碼基本上都宣告和簽名,如下程式碼才是真正的函式體程式碼
; function body 0
0000024: 00                                        ; func body size (guess)
0000025: 00                                        ; local decl count
0000026: 41                                        ; i32.const
0000027: 2a                                        ; i32 literal
0000028: 0b                                        ; end
0000024: 04                                        ; FIXUP func body size
; function body 1
0000029: 00                                        ; func body size (guess)
000002a: 00                                        ; local decl count
000002b: 10                                        ; call
000002c: 00                                        ; function index
000002d: 41                                        ; i32.const
000002e: 01                                        ; i32 literal
000002f: 6a                                        ; i32.add
0000030: 0b                                        ; end
0000029: 07                                        ; FIXUP func body size
0000022: 0e                                        ; FIXUP section size

這裡我們要注意一點是wasm中不同section是有一定的排序的,具體的順序如下

user                       0
type                       1
import                     2
functionDeclarations       3  
table                      4
memory                     5
global                     6
export                     7
start                      8
elem                       9
functionDefinitions        10
data                       11

WASM執行介紹與分析 wasm目前主要的應用領域在於web應用,對於EOS其將作為智慧合約的最終格式,其目前執行 在WAVM上,其機制不同於目前瀏覽的執行和呼叫方式。首先我們先簡單瞭解一下wasm是如 在瀏覽器中執行,而WAVM的執行時分析將在EOS虛擬機器中進行。 瀏覽器執行的示例:https://webassembly.org/getting-started/developers-guide/  這裡可以看到利用emcc的工具生成的最終程式碼,其中主要有wasm檔案,js膠水檔案和html  呼叫檔案。

EOS智慧合約分析

EOS智慧合約概覽

EOS中的智慧合約概括的來講就是對多個輸入來組織商議輸出的過程,EOS中的合約不僅僅 可以實現例如轉賬的這種經濟行為,也可以描述遊戲規則。EOS中的合約作為註冊在EOS區 塊鏈上的應用程式並最終執行在EOS的節點上。EOS的智慧合約定義了相關的介面,這些接 口包含action,資料結構和相關的引數,同時智慧合約實現這些介面,最後被編譯成二進位制格 式,在EOS中為wasm,節點負責解析位元組碼來執行對應的智慧合約。對於區塊鏈而言,最 終儲存的是智慧合約的交易(transactions)。

EOS智慧合約模型和執行流程

EOS中的智慧合約由兩個部分組成分別為action集合和型別的定義:

  • action集合,定義和實現了智慧合約的行為和功能
  • 型別定義,定義了合約需要的內容和資料結構

EOS智慧合約與Action EOS中的action操作構建與一個訊息架構之上,客戶端通過傳送訊息來觸發action的執行, 我們知道智慧合約最終的儲存形式是一個transaction,那transaction和action之間是什麼關 系,在這裡一個transaction包含至少一個action,而一個action代表的是大一的操作。如下為 一個包含多個action的transaction。對於如下的transaction,當其中所有的action都成功的 時候,這個transaction才算成功。如果一個transaction成功後,則其receipt生成,但是此時 並不代表transaction已經確認,只是說明確認的概率大一些

{
  "expiration": "...",
  "region": 0,
  "ref_block_num": ...,
  "ref_block_prefix": ...,
  "net_usage_words": ..,
  "kcpu_usage": ..,
  "delay_sec": 0,
  "context_free_actions": [],
  "actions": [{
      "account": "...",
      "name": "...",
      "authorization": [{
          "actor": "...",
          "permission": "..."
        }
      ],
      "data": "..."
    }, {
      "account": "...",
      "name": "...",
      "authorization": [{
          "actor": "...",
          "permission": "..."
        }
      ],
      "data": "..."
    }
  ],
  "signatures": [
    ""
  ],
  "context_free_data": []
}

EOS的智慧合約提供一個action handler來完成對action的請求,每次一個action執行在實現 上通過呼叫apply方法,EOSIO通過建立一個apply的上下文來輔助action的執行,如下的圖 說明一個apply上下文的關鍵元素。

從全域性的角度看,EOS區塊鏈中的每個節點將獲得智慧合約中每個action的一個拷貝,在 所有節點的執行狀態中,一些節點在執行智慧合約的實際工作,而一些節點在做交易的驗 證,因此對於一個合約來說能比較重要的一點就是知道當前執行的實際的上下文是什麼, 也就是說目前處在哪個階段,在這裡上下文的標識被記錄在action的上下文中來完成上面 的工作,如上面圖所示這個上下文標識包括三個部分,分別是reciver,code和action。 receiver表示當前處理這個action的賬戶,code代表授權了這個合約賬戶,而action是 當前執行的action的ID。 根據上面我們知道transaction和action的關係,如果一個transaction失敗,所有在這個 transaction中的action的計算結果都需要被回滾,在一個action上下文中一個關鍵的資料 成員就是當前的transaction資料,它包含以下幾個部分:

  • transaction的頭
  • 包含transaction中所有的原始的action的容器,容器已經排好序
  • 包含transaction中的上下文無關的action的容器
  • 一個可以刪節的上下文無關的資料,這部分資料是被合約定義的,以一個二進位制長
  • 物件集合提供
  • 對上述二進位制長物件的索引

在EOS中每個action執行的時候都會重新的申請一塊新的記憶體,每個action上下文中的變數是 私有的,即使在同一個transaction中的action,他們的變數也是不可以共享,唯一的一種方式 來共享變數就是通過持久化資料到EOS的資料庫中,這些可以通過EOSIO的持久化API來實現。

EOS智慧合約執行流程 EOS中的智慧合約彼此可以進行通訊,例如一個合約呼叫另外的合約來完成相關操作來完成當 前的transaction,或者去觸發一個當前transaction的scope外的一個外來的transaction。 EOS中支援兩種不基本的通訊模型,分別是inline和deferred兩種,典型的在當前transaction 中的操作是inline的方式的action的例項,而被觸發的一個將要執行的transaction則是一個deferred action的例項。在智慧合約之間的通訊我們可以看做是非同步的。

inline Communication Inline的通訊模式主要體現在對需要執行的action的請求過程直接採用呼叫的方式,Inline方式 下的action在同一transaction的scope和認證下,同時action被組織起來用於執行當前的transaction Inline action可以被看做是transaction的巢狀,如果transaction的任何一個部分執行失敗,那麼 inline action也只會在transaction的剩下部分展開, 呼叫inline action不會產生任何對外的通知 無論其中是成功還是失敗,綜上也就是說inline action的作用範圍是在一個transaction中的。

Deferred Communication Deferred的通訊模式採用的是通過通知另一個節點transaction的方式來實現的。一個Deferred actions一般會稍後呼叫,對於出塊生產者來說並不保證其執行。對於創造Deferred action的 transaction來說它只能保證是否建立和提交成功,對於是否執行成功與否無法保證。對於一個 Deferred action來說其攜帶合約的驗證資訊進行傳遞。特殊的一個transaction可以取消一個 deferred的transaction。

執行流程示例 如下如未EOS wiki上給出的一個包含inline action的transaction的執行流程。

從圖中我們可以看到,這個transaction中有兩個inline action,分別是

  • employer::runpayroll
  • employer::dootherstuff

由上面的圖,我們可以很清晰的知道,action通過呼叫inline action並遞迴的呼叫最後來完成 整個transactio的執行。同上對於上面的一個轉賬發薪酬的場景也可以通過Deferred的方式 來完成,如下圖所示:

EOS智慧合約示例說明

EOS智慧合約一般用c++語言實現,可以通過工具來進行編譯成最後的智慧合約二進位制碼,一 段典型的智慧合約程式碼如下:

#include <eosiolib/eosio.hpp>
 
using namespace eosio;
 
class hello : public eosio::contract {
  public:
      using contract::contract;
      /// @abi action
      void hi( account_name user ) {
         print( "Hello, ", name{user} );
      }
};
 
EOSIO_ABI( hello, (hi) )

對於每一個智慧合約而言,其必須提供一個apply的介面,這個介面函式需要監聽所有輸入的aciton 並作出對應的動作,apply用recevier,code和action來過來輸入並執行特定的操作。形式如下:

if (code == N(${contract_name}) {
   // your handler to respond to particular action
}

EOS中的的巨集EOSIO_ABI遮蔽了底層實現的細節,巨集展開如下所示:

#define EOSIO_ABI( TYPE, MEMBERS ) \
extern "C" { \
   void apply( uint64_t receiver, uint64_t code, uint64_t action ) { \
      auto self = receiver; \
      if( action == N(onerror)) { \
         eosio_assert(code == N(eosio), \
         "onerror action's are only valid from the \"eosio\" system account"); \
      } \
      if( code == self || action == N(onerror) ) { \
         TYPE thiscontract( self ); \
         switch( action ) { \
            EOSIO_API( TYPE, MEMBERS ) \
         } \
         /* does not allow destructor of thiscontract to run: eosio_exit(0); * / \
      } \
   } \
} \

其中EOSIO_ABI的巨集定義如下:

#define EOSIO_API( TYPE,  MEMBERS ) \
   BOOST_PP_SEQ_FOR_EACH( EOSIO_API_CALL, TYPE, MEMBERS )

我們繼續展開巨集EOSIO_API_CALL如下:

#define EOSIO_API_CALL( r, OP, elem ) \
   case ::eosio::string_to_name( BOOST_PP_STRINGIZE(elem) ): \
      eosio::execute_action( &thiscontract, &OP::elem ); \
      break;

這樣我們就明確一個只能合約被呼叫的時候最後是如何反應到程式碼層面進行路由呼叫的。

EOS智慧合約相關工具

由上文我們知道一個智慧合約原始檔大概的樣子,現在我們來看一下如何生成EOS虛擬機器支援的格 式。EOS虛擬機器目前支援載入wast和wasm兩種格式的智慧合約。現在看下EOS中智慧合約是如何 構建的,如下程式碼為tools/eosiocpp.in中關於合約的編譯指令碼,其中省略部分非關鍵程式碼:

function build_contract {    
($PRINT_CMDS; @[email protected] -emit-llvm -O3 --std=c++14 --target=wasm32 -nostdinc \
  -nostdlib -nostdlibinc -ffreestanding -nostdlib -fno-threadsafe-statics -fno-rtti \
  -fno-exceptions -I ${EOSIO_INSTALL_DIR}/include \
  -I${EOSIO_INSTALL_DIR}/include/libc++/upstream/include \
  -I${EOSIO_INSTALL_DIR}/include/musl/upstream/include \
  -I${BOOST_INCLUDE_DIR} \
  -I $filePath \
  -c $file -o $workdir/built/$name)
 
  ($PRINT_CMDS; @[email protected] -only-needed -o $workdir/linked.bc $workdir/built/* \
    ${EOSIO_INSTALL_DIR}/usr/share/eosio/contractsdk/lib/eosiolib.bc \
    ${EOSIO_INSTALL_DIR}/usr/share/eosio/contractsdk/lib/libc++.bc \
    ${EOSIO_INSTALL_DIR}/usr/share/eosio/contractsdk/lib/libc.bc
  )
  ($PRINT_CMDS; @[email protected] -thread-model=single --asm-verbose=false -o \
    $workdir/assembly.s $workdir/linked.bc)
  ($PRINT_CMDS; ${EOSIO_INSTALL_DIR}/bin/eosio-s2wasm -o $outname -s \
    16384 $workdir/assembly.s)
  ($PRINT_CMDS; ${EOSIO_INSTALL_DIR}/bin/eosio-wast2wasm $outname \
    ${outname%.*}.wasm -n)
}

由上述的程式碼可知,智慧合約的編譯主要過程如下:

  • 利用clang以wasm32為目標,生成中間檔案bc
  • 利用LLVM-link連結上一個步驟生成bc檔案和標準庫bc檔案生成link.bc檔案
  • 利用LLVM的llc生成s彙編檔案assembly.s
  • 應用eosio-s2wasm工具講s檔案轉化為wast檔案
  • 應用eosio-wast2wasm工具將wast檔案轉化為最終的wast檔案

通過以上的步驟我們就生成了一個以wasm為格式的智慧合約,上面一共經歷了5個步驟才將我們的

原始檔變異成wasm,其實還可以應用開源工具emcc來編譯,但是該工具並不是針對智慧合約設計 工具比較龐大,我們把沒有應用emcc的wasm的生成方案統一稱為wasm without emcc。 由於上述的編譯過程很複雜,這裡需要分析說明一下為什麼採用這種方式?

The Runtime is the primary consumer of the byte code. It provides an API for  instantiating WebAssembly modules and calling functions exported from them.  To instantiate a module, it initializes the module's runtime environment  (globals, memory objects, and table objects), translates the byte code into LLVM IR, and uses LLVM to generate machine code for the module's functions.

由上文我們得知,WAVM是將wasm或者wast檔案轉化為LLVM的IR表示,然後通過LLVM執行程式碼來實現 最後的程式執行,那麼問題來了,對於智慧合約,為什麼我們不直接用clang生成bc檔案,然後修改 lli(前文介紹過程式碼不超過800行)來實現虛擬機器呢? 個人分析主要有以下幾個原因:

  • 如果EOS定義智慧合約二進位制格式為bc,文字方式為ll,也就是對標wasm和wast個人覺得利用lli

沒有問題,關鍵受限於LLVM。

  • 出於對未來的考慮,畢竟對wasm支援的解釋容器比較多,方便多種虛擬機器的接入,但是目前看大多數

容器都是瀏覽器js引擎,因此解決js膠水程式碼仍然是個問題,所以尋求一個wasm的虛擬機器目前看WAVM 比較合適

  • WAVM實現了wasm的虛擬機器,而且EOS也聲稱不提供虛擬機器,也就是說wasm的選型限制了以上的工具鏈

這裡還有個重要的檔案生成,那就是abi的檔案的構建,這個的實現也在eosiocpp.in中,abi這裡的 作用是什麼?就是它會描述一個合約對外暴露的介面,具體為JSON格式,使用者可以通過eosc工具構建 合適的message來呼叫對應的介面。eosiocpp中generate_abi的部分程式碼如下:

  ${ABIGEN} -extra-arg=-c -extra-arg=--std=c++14 -extra-arg=--target=wasm32 \
  -extra-arg=-nostdinc -extra-arg=-nostdinc++ -extra-arg=-DABIGEN \
  -extra-arg=-I${EOSIO_INSTALL_DIR}/include/libc++/upstream/include \
  -extra-arg=-I${EOSIO_INSTALL_DIR}/include/musl/upstream/include \
  -extra-arg=-I${BOOST_INCLUDE_DIR} \
  -extra-arg=-I${EOSIO_INSTALL_DIR}/include -extra-arg=-I$context_folder \
  -extra-arg=-fparse-all-comments -destination-file=${outname} -verbose=0 \
  -context=$context_folder $1 --}}

最後通過二進位制工具cleos來部署智慧合約,例如:

 cleos set contract eosio build/contracts/eosio.bios -j -p eosio

  • 第一個eosio為賬戶
  • 第二個eosio為許可權
  • -j 以json輸出結果
  • build/contracts/eosio.bios 為智慧合約所在目錄

同時可以通過cleos工具來推送action測試contract,例如如下命令:

cleos push action eosio.token create '{"issuer":"eosio", "maximum_supply":" 1000000000.0000 EOS", "can_freeze":0, "can_recall":0, "can_whitelist":0}' -j -p eosio.token

  • esoio.token為contract
  • create為action
  • 後面json格式的為具體的資料
  • -p為指定許可權

小結:EOS智慧合約通過複雜的工具鏈最後生成wasm或者wast,並配合abi檔案最後進行分發到EOS 系統中去。

EOS虛擬機器分析

EOS在技術白皮書中指明並不提供具體的虛擬機器實現,任何滿足沙盒機制的虛擬機器都可以執行在EOSIO 中,原始碼層面,EOS提供了一種虛擬機器的實現,虛擬機器以wasm為輸入,利用相關的技術完成程式碼的 快速執行。

EOS虛擬機器概覽

EOS虛擬機器程式碼實現來自WAVM,參見具體的檔案發現其基本上都是wasm-jit目錄下的內容從專案信 息可以看出其是fork AndrewScheidecker/WAVM的實現,這個也是為啥很多人瞧不起EOS虛擬機器的 原因,但是Andrew Scheidecker本人主要在提交程式碼,所以對他下結論為時尚早,作者 Andrew Scheidecker是虛幻引擎的主要貢獻者,程式碼質量至少能有所保障。 首先是EOS虛擬機器的程式碼,在github上有兩個地方可以檢視到EOS中虛擬機器的程式碼分別為:

  • libraries/chain,主要是定義虛擬機器相關介面
  • libraries/wasm-jit,主要是智慧合約執行的實現
  • contracts目錄下,為相關的ABI輔助原始碼

This is a standalone VM for WebAssembly. It can load both the standard binary format, and the text format defined by the WebAssembly reference interpreter. For the text format, it can load both the standard stack machine syntax and  the old-fashioned AST syntax used by the reference interpreter, and all of the testing commands

由上述的描述我們可以知道WAVM支援兩種的輸入分別是二進位制的輸入和文字格式的輸入,對應的具 體的格式是wasm和wast。參見WAVM使用說明如下:

The primary executable is wavm:
Usage: wavm [switches] [programfile] [--] [arguments]
in.wast|in.wasm Specify program file (.wast/.wasm)
-f|--function name Specify function name to run in module rather than main
-c|--check Exit after checking that the program is valid
-d|--debug Write additional debug information to stdout
-- Stop parsing arguments

由上我們得知EOS的智慧合約支援兩種格式分別就是上文描述的wasm和wast。

EOS虛擬機器實現思路分析

EOS在智慧合約目標格式選擇上應該做過一定的考慮,對於wasm的選擇可能出於社群支援和實現上 的雙重考慮,這點在採用LLVM-JIT技術就有所體現。EOS在選擇如何實現虛擬機器的方案上採用的是 開放的態度,即如白皮書所講。EOS為了使專案完整,需要提供一個的虛擬機器。首先選定wasm不僅 僅是因為支援的大廠比較多,還有出於多語言支援的考慮,敲定wasm目標格式後痛苦的事情就來了 目前需要一個能執行他的虛擬機器容器,目前都是瀏覽器支援,落地就是JS的解析引起支援,如果用JS 解析引擎,工程量大,釋出還要附帶js膠水程式碼加麻煩的還有結果如何安全獲取。於是需要的是一個 wasm的執行的輕量級虛擬機器,WAVM成了首選,多虧AndrewScheidecker之前寫過一個這樣的項 目,於是直接Fork,加些介面就完成了implementation。從另外一個角度看 如果不考慮生態的問題, LLVM中的bc也可以作為智慧合約的語言,通過修改lli來完成虛擬機器的實現,而且工程實踐更加簡單, 但是問題就是和LLVM綁定了,虛擬機器只能和LLVM混,這個限制太大。

EOS虛擬機器架構概述

EOS虛擬機器面對的編譯的智慧合約格式為wasm或者wast兩種格式,這兩種格式本質上沒有區別,那麼 如何解析這兩種格式並執行內部的相關指令就稱為虛擬機器主要考慮的問題,EOS的實現思路如下:

將wasm轉化為LLVM能識別的IR中間語言。 藉助LLVM-JIT技術來實現IR的語言的執行。 這裡有兩個關鍵點,一個是如何將wasm格式檔案轉化為IR中間檔案,第二個就是如何保證IR的相關 執行時環境的維護。以下幾個章節將解釋相關的問題。

EOS虛擬機器實現與分析

EOS虛擬機器核心介面 我們先High Level的看一下EOS虛擬機器是如何響應外部執行需求的,這個主要體現在對外接 層面EOS虛擬機器介面對外暴露虛擬機器例項建立和合約執行入口,具體宣告在如下路徑檔案中libraries/chain/inlcude/eosio/chain/webassembly/runtime_interface.hpp 檔案中主要對外暴露了兩個介面,分別為instantiate_module和apply,分別宣告在兩個不同 的類中,如下為介面的具體宣告:

class wasm_instantiated_module_interface {
public:
  virtual void apply(apply_context& context) = 0;
  virtual ~wasm_instantiated_module_interface();
};
class wasm_runtime_interface {
public:
  virtual std::unique_ptr<wasm_instantiated_module_interface>
  instantiate_module(const char* code_bytes,
                     size_t code_size,
                     std::vector<uint8_t> initial_memory) = 0;
  virtual ~wasm_runtime_interface();
};

介面apply實現在檔案\libraries\chain\include\eosio\chain\webassembly\wavm.hpp中 介面instantiate_module實現在\libraries\chain\webassembly\wavm.hpp中 介面apply的實現如下程式碼所示:

void apply(apply_context& context) override {
  //組織引數列表
  //這裡需要說明一下每個被分發的action通過scope就是account和
  //function就是name來定義的
  vector<Value> args = {
    Value(uint64_t(context.receiver)),//當前執行的程式碼
    Value(uint64_t(context.act.account)),//action中的賬戶
    Value(uint64_t(context.act.name))};//action的名稱
  call("apply", args, context);
}

下面來看call具體執行的邏輯功能,這裡我們將看到執行在虛擬機器上的程式碼是如何啟動的。 這裡我們一行一行來進行分析:

void call(
  const string &entry_point, //函式入口點,例如:main是一個exe的入口
  const vector <Value> &args, //函式引數列表
  apply_context &context) {//需要執行的具體的內容
  try {
    //首先根據entry_point(這裡為apply)獲取到傳入的程式碼中是否有名字為
    //entry_point的object,通俗的講就是根據函式名找到函式指標
    FunctionInstance* call = asFunctionNullable(
                      getInstanceExport(_ instance,entry_point));
    if( !call )//如果沒有找到函式的入口在直接返回,注意此處無異常
      return;
    //檢查傳入的引數個數和函式需要的個數是否相等,注意為什麼沒有檢查型別
    //因為由上述函式apply得知型別均為uint_64,內部對應型別IR::ValuType::i64
    FC_ASSERT( getFunctionType(call)->parameters.size() == args.size() );
 
    //獲得記憶體例項,在一個wavm_instantiated_modules中,記憶體例項是被重用的,
    //但是在wasm的例項中將不會看到getDefaultMemeory()
    MemoryInstance* default_mem = getDefaultMemory(_ instance);
    if(default_mem) {
      //重置memory的大小為初始化的大小,然後清零記憶體
      resetMemory(default_mem, _ module->memories.defs[0].type);
      char* memstart = &memoryRef<char>(getDefaultMemory(_ instance), 0);
      memcpy(memstart, _ initial_memory.data(), _ initial_memory.size());
    }
    //設定執行上下文的記憶體和執行的上下文資訊
    the_running_instance_context.memory = default_mem;
    the_running_instance_context.apply_ctx = &context;
    //重置全域性變數
    resetGlobalInstances(_ instance);
    //呼叫module的起始函式,這個函式做一些環境的初始化工作
    //其在instantiateModule函式中被設定
    runInstanceStartFunc(_ instance);
    //invoke call(上面已經指向apply函式的地址了)
    Runtime::invokeFunction(call,args);
  } catch( const wasm_exit& e ) {
  } catch( const Runtime::Exception& e ) {
    FC_THROW_EXCEPTION(wasm_execution_error,"cause: ${cause}\n${callstack}",
    ("cause", string(describeExceptionCause(e.cause)))
    ("callstack", e.callStack));
  } FC_CAPTURE_AND_RETHROW()
}

上述程式碼中通過call尋找entry_point名字的函式,這裡為apply,注意上一個主題中EOSIO_ABI 的展開中為apply函式的實現,如下:

#define EOSIO_ABI( TYPE, MEMBERS ) \
extern "C" { \
   void apply( uint64_t receiver, uint64_t code, uint64_t action ) { \
      auto self = receiver; \
      if( action == N(onerror)) { \
         eosio_assert(code == N(eosio), \

總結:上面通過介面的瞭解和程式碼的閱讀分析快速的從比較高的視角看到EOS虛擬機器執行 的大體過程,下面我們就從細節上來了解EOS虛擬的採用的技術和最後是如何應用在EOS系統中的。

EOS虛擬機器架構應用層 我們這裡將EOS虛擬機器關於智慧合約部署以及虛擬機器外層呼叫邏輯統一稱為虛擬機器應用層,現在分別進行 說明,先從虛擬機器的外圍了解其整體的工作流程。

EOS虛擬機器客戶端合約部署 首先看命令列工具cleos是如何將智慧合約傳送給EOSIO程式的,具體的程式碼見檔案:eosio\eos\programs\cleos\main.cpp。如下程式碼片段為新增命令列引數。

auto contractSubcommand = setSubcommand->add_subcommand(
  "contract",
  localized("Create or update the contract on an account"));
contractSubcommand->add_option(
  "account",
  account,
  localized("The account to publish a contract for"))
  ->required();
contractSubcommand->add_option(
  "contract-dir",
  contractPath,
  localized("The path containing the .wast and .abi"))
  ->required();
contractSubcommand->add_option(
  "wast-file",
  wastPath,
  localized("The file containing the contract WAST or WASM relative to contract-dir"));
auto abi = contractSubcommand->add_option(
  "abi-file,-a,--abi",
  abiPath,
  localized("The ABI for the contract relative to contract-dir"));

上述的命令為set命令的子命令,現在看一下命令是如何傳送出去的,主要在如下兩個回撥函式

  • set_code_callback
  • set_abi_callback

兩個回撥函式,我們以set_code_callback分析是如何執行的,關鍵程式碼如下:

actions.emplace_back( create_setcode(account, bytes(wasm.begin(), wasm.end()) ) );
if ( shouldSend ) {
  std::cout << localized("Setting Code...") << std::endl;
  send_actions(std::move(actions), 10000, packed_transaction::zlib);
}

如上程式碼知道其是呼叫send_actions將智慧合約的相關資訊已一個action的形式傳送出去, 而send_actions將呼叫push_action函式,最後push_action將呼叫關鍵函式call程式碼如下:

fc::variant call( const std::string& url,
  const std::string& path,
  const T& v ) {
try {
  eosio::client::http::connection_param * cp =
  new eosio::client::http::connection_param((std::string&)url, (std::string&)path,
  no_verify ? false : true, headers);
  return eosio::client::http::do_http_call( *cp, fc::variant(v) );
}
}

由上可知客戶端最後通過http的方式將部署智慧合約的程式碼傳送到了EOSIO上。注意其中的url具體為


const string chain_func_base = "/v1/chain";
const string push_txn_func = chain_func_base + "/push_transaction";

EOS虛擬機器服務端合約部署 上面我們瞭解了合約是如何通過客戶端傳遞到服務端的,現在我們重點分析一下服務端是如何部署 或者更加準確的說儲存合約的。我們重點分析一下nodeos(eosio\eos\programs\nodeos)是如何 處理push_transaction的,先看其主函式關鍵片段:

if(!app().initialize<chain_plugin, http_plugin, net_plugin, producer_plugin>
  (argc, argv))
return INITIALIZE_FAIL;
initialize_logging();
ilog("nodeos version ${ver}", \
  ("ver", eosio::utilities::common::itoh(static_cast<uint32_t>(app().version()))));
ilog("eosio root is ${root}", ("root", root.string()));
app().startup();
app().exec();

主要註冊了chain,http,net和producer幾個外掛,我們先看chain_api_plugin的關鍵實現:

auto ro_api = app().get_plugin<chain_plugin>().get_read_only_api();
auto rw_api = app().get_plugin<chain_plugin>().get_read_write_api();
 
app().get_plugin<http_plugin>().add_api(
CHAIN_RO_CALL(get_info, 200l),
CHAIN_RO_CALL(get_block, 200),
CHAIN_RO_CALL(get_account, 200),
CHAIN_RO_CALL(get_code, 200),
CHAIN_RO_CALL(get_table_rows, 200),
CHAIN_RO_CALL(get_currency_balance, 200),
CHAIN_RO_CALL(get_currency_stats, 200),
CHAIN_RO_CALL(get_producers, 200),
CHAIN_RO_CALL(abi_json_to_bin, 200),
CHAIN_RO_CALL(abi_bin_to_json, 200),
CHAIN_RO_CALL(get_required_keys, 200),
CHAIN_RW_CALL_ASYNC(push_block, chain_apis::read_write::push_block_results, 202),
CHAIN_RW_CALL_ASYNC(push_tran