eos虛擬機與智能合約詳解與分析

1. 相關背景知識

1.1 LLVM相關內容

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

1.1.1 LLVM架構概述

LLVM項目是一系列分模塊、可重用的編譯工具鏈。它提供了一種代碼良好的中間表示(IR),
LLVM實現上可以作爲多種語言的後端,還可以提供與語言無關的優化和針對多種CPU的代碼生成功能。

最初UIUC的Chris Lattner主持開發了一套稱爲LLVM(Low Level Virtual Machine)的編譯器工具庫套
,但是後來隨着LLVM的範圍的不斷擴大,則這個簡寫並不代表底層虛擬機的含義,而作爲整個項目,現在的LLVM並不代表Low Level Virtual Machine。

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

image

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

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

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

1.1.2 LLVM IR介紹與分析

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

目前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等工具的作用和使用。

image

中間語言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
}
1.1.2.1 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等),中止指令指明瞭欲跳轉的目的地址。

1.1.2.2 LLVM IR類型系統

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

1.1.2.3 LLVM IR內存模型

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

1.1.2.4 LLVM IR函數調用

LLVM中對普通函數調用,LLVM提供了call指令來調用附帶類型信息的函數指針。這種抽象屏蔽了
機器相關的調用慣例。還有一個不能忽略的就是異常處理,在LLVM中,LLVM提供了invoke和
unwind指令。invoke指令指定在棧展開的過程中必須要執行的代碼,例如棧展開的時候需要析構
局部對象等。而unwind指令用於拋出異常並執行棧展開的操作。棧展開的過程會被invoke指令停
下來,執行catch塊中的行爲或者執行在跳出當前活動記錄之前需的操作。執行完成後繼續代碼執
行或者繼續棧展開操作。注意像C++的RTTI則由C++自己的庫處理,LLVM並不負責。

1.1.2.5 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

1.1.3 LLVM JIT介紹與分析

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

1.1.3.1 LLVM JIT實現原理

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

1.1.3.2 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技術實現的虛
擬機運行。

1.2. WebAssembly相關內容

1.2.1 WebAssembly概述

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

1.2.1.1 WebAssembly格式介紹與分析

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

1.2.1.2 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+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中的外部內存使用,表格等高級內容
可以單獨去了解。

1.2.1.3 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
1.2.1.4 WASM運行介紹與分析

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

image

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上下文的關鍵元素。
image

從全局的角度看,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的執行流程。
image

  • employer::runpayroll
  • employer::dootherstuff

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

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_API的宏定義如下:

#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; @WASM_CLANG@ -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; @WASM_LLVM_LINK@ -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; @WASM_LLC@ -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中虛擬機的代碼分別爲:

https://github.com/EOSIO/eos

https://github.com/EOSIO/WAVM

其中eos目錄下爲這個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_transaction, \
  chain_apis::read_write::push_transaction_results, 202),
CHAIN_RW_CALL_ASYNC(push_transactions, \
  chain_apis::read_write::push_transactions_results, 202) \
)

我們下一步具體詳細的看一下http_plugin中的add_api的具體實現代碼如下:

void add_api(const api_description& api) {
  for (const auto& call : api)
    add_handler(call.first, call.second);
}
void http_plugin::add_handler(const string& url, const url_handler& handler) {
  ilog( "add api url: ${c}", ("c",url) );
  //註冊api函數,可以參看asio的pos示例
  app().get_io_service().post([=](){
  my->url_handlers.insert(std::make_pair(url,handler));
});
}

由上面的函數我們得知,對url(例如/push_transactions)的請求通過註冊的機制放入asio中。
我們來看一下處理http請求的函數的關鍵代碼,片段如下:

auto handler_itr = url_handlers.find( resource );
if( handler_itr != url_handlers.end()) {
con->defer_http_response();
  //這裏將數據傳遞給了api相關的函數
  handler_itr->second( resource, body, [con]( auto code, auto&& body ) {
  con->set_body( std::move( body ));
  con->set_status( websocketpp::http::status_code::value( code ));
  con->send_http_response();
} );
}

小結 :由上面的代碼分析,我們基本清楚了一個請求過來是如何關聯到具體的api函數的,下面
我們來看一下如何實現合約的部署。 我們先回到如下的代碼片段,看具體處理函數是如何運行的

CHAIN_RW_CALL_ASYNC(push_transaction, \
  chain_apis::read_write::push_transaction_results, 202),

將對應的宏進行展開如下:

#define CHAIN_RW_CALL_ASYNC(call_name, call_result, http_response_code) \
  CALL_ASYNC(chain, rw_api, chain_apis::read_write, call_name, call_result, http_response_code)

繼續進行展開爲如下的lamda表達式函數。

#define CALL_ASYNC(api_name, api_handle, api_namespace, call_name, call_result, \
  http_response_code) \
{std::string("/v1/" #api_name "/" #call_name), \
   [this, api_handle](string, string body, url_response_callback cb) \
   mutable { \
     if (body.empty()) body = "{}"; \
     api_handle.call_name(\
       fc::json::from_string(body).as<api_namespace::call_name ## _ params>(),\
         [cb, body](const fc::static_variant<fc::exception_ptr, call_result>& result){\
            if (result.contains<fc::exception_ptr>()) {\
               try {\
                  result.get<fc::exception_ptr>()->dynamic_rethrow_exception();\
               } catch (...) {\
                  http_plugin::handle_exception(#api_name, #call_name, body, cb);\
               }\
            } else {\
               cb(http_response_code, result.visit(async_result_visitor()));\
            }\
         });\
   }\
}

由上述的關鍵代碼,我們對應得到具體處理的函數cb爲函數rw_api,下面我們來看一下chain_pulgin
下的rw_api的具體實現,由如下的代碼片段我們得知關鍵處理的類爲read_write

chain_apis::read_write chain_plugin::get_read_write_api() {
   return chain_apis::read_write(chain());
}

我們現在看一下類chain_apis::read_write中push_transaction的具體實現。

void read_write::push_transaction(const read_write::push_transaction_params& params,
   next_function<read_write::push_transaction_results> next) {
   try {
      .............
      .............
      //關鍵處理在get_method方法所獲得的具體的處理函數
      app().get_method<incoming::methods::transaction_async>()(
        pretty_input,//輸入數據
        true,
        [this, next](const fc::static_variant<fc::exception_ptr,
          transaction_trace_ptr>& result) -> void{
         if (result.contains<fc::exception_ptr>()) {
           //執行函數next
            next(result.get<fc::exception_ptr>());
         } else {
            auto trx_trace_ptr = result.get<transaction_trace_ptr>();
            try {
               fc::variant pretty_output;
               pretty_output = db.to_variant_with_abi(*trx_trace_ptr);
               chain::transaction_id_type id = trx_trace_ptr->id;
               //執行next函數
               next(read_write::push_transaction_results{id, pretty_output});
            } CATCH_AND_CALL(next);
         }
      });
}}

我們來看一下incoming::methods::transaction_async對應的具體的處理函數:

namespace methods {
 // synchronously push a block/trx to a single provider
 using block_sync            = method_decl<chain_plugin_interface,
 void(const signed_block_ptr&), first_provider_policy>;
 using transaction_async     = method_decl<chain_plugin_interface,
 void(const packed_transaction_ptr&, bool, next_function<transaction_trace_ptr>),
 first_provider_policy>;
}

這裏重點關注method_decl(聲明在libraries/appbase/include/appbase/method.h)下。 其原型爲:

//@tparam Tag - API specific discriminator used to distinguish between otherwise
// identical method signatures
//@tparam FunctionSig - the signature of the method
//@tparam DispatchPolicy - dispatch policy that dictates how providers
//for a method are accessed defaults to @ref first_success_policy
template< typename Tag, typename FunctionSig,
          template <typename> class DispatchPolicy = first_success_policy>
struct method_decl {
  using method_type = method<FunctionSig, DispatchPolicy<FunctionSig>>;
  using tag_type = Tag;
};

最後我們回到開始的get_methodincoming::methods::transaction_async來看下其具體做了
什麼,參看文件爲eos/libraries/appbase/include/appbase/application.hpp

//fetch a reference to the method declared by the passed in type.  This will
//construct the method on first access.  This allows loose and deferred
//binding between plugins
//@tparam MethodDecl - @ref appbase::method_decl
//@return reference to the method described by the declaration
 
template<typename MethodDecl>
auto get_method() ->
std::enable_if_t<is_method_decl<MethodDecl>::value, typename MethodDecl::method_type&>
{
  //我們展開後得到method_type的類型爲incoming::methods::method_decl::
  //method<void(const packed_transaction_ptr&, bool, next_function<transaction_trace_ptr>),
  //first_provider_policy>, 這個類型看似比較複雜,但是抓住關鍵就是函數簽名
  //void(const packed_transaction_ptr&, bool , next_function<transaction_trace_ptr>)
  //
  using method_type = typename MethodDecl::method_type;
  auto key = std::type_index(typeid(MethodDecl));
  auto itr = methods.find(key);
  if (itr != methods.end()) {
    //這裏我們得到了具體的函數,那麼下一步就是看函數如何運行的。
    return * method_type::get_method(itr->second);
  } else {
    methods.emplace(std::make_pair(key, method_type::make_unique()));
    return  * method_type::get_method(methods.at(key));
  }
}

通過上面的代碼我們可以得出結論關鍵的transaction處理函數原型如下:

void(const packed_transaction_ptr&, bool , next_function<transaction_trace_ptr>)

按圖索驥,於是我們找到了producer_plugin.cpp的實現,在函數plugin_initialize的結尾處我們
看到如下的代碼:

my->_incoming_transaction_async_provider = app().
  get_method<incoming::methods::transaction_async>().register_provider([this](
    //注意此處的函數的簽名
    const packed_transaction_ptr& trx,
    bool persist_until_expired,
    next_function<transaction_trace_ptr> next) -> void {
      return my->on_incoming_transaction_async(trx, persist_until_expired, next );
});

這裏是將具體處理的函數進行註冊到具體的關聯的type上,那麼下一步我們就着重分析函數:
on_incoming_transaction_async就可以了。其實現在文件producer_plugin.cpp中。其中
關鍵函數代碼爲:

auto send_response = [this, &trx, &next](const fc::static_variant<fc::exception_ptr,
  transaction_trace_ptr>& response) {
  next(response);
  if (response.contains<fc::exception_ptr>()) {
    _ transaction_ack_channel.publish(std::pair<fc::exception_ptr, packed_transaction_ptr>
      (response.get<fc::exception_ptr>(), trx));
  } else {
    //將數據發入到channel中,具體的訂閱者將會進行處理。
    _ transaction_ack_channel.publish(std::pair<fc::exception_ptr, packed_transaction_ptr>
      (nullptr, trx));
  }
};

上面的transaction_ack_channel由net_plugin進行訂閱,這點我們可以理解,主要是發送ack返回消息。 
進一步在函數on_incoming_transaction_async中如下代碼對transaction進行了處理:

//調用chain的push_transaction來處理transaction,

auto trace = chain.push_transaction(std::make_shared<transaction_metadata>(*trx), deadline);
if (trace->except) {
  if (failure_is_subjective(*trace->except, deadline_is_subjective)) {
    _ pending_incoming_transactions.emplace_back(trx, persist_until_expired, next);
  } else {
    auto e_ptr = trace->except->dynamic_copy_exception();
    send_response(e_ptr);
  }
}

跟蹤代碼最後我們知道chain的類型爲eosio::chain::controller,具體見文件:
eos\libraries\chain\include\eosio\chain\controller.h,代碼如下:

transaction_trace_ptr controller::push_transaction(const transaction_metadata_ptr& trx,
  fc::time_point deadline,
  uint32_t billed_cpu_time_us ) {
  //其中my的類型爲controller_impl,
  return my->push_transaction(trx, deadline, false, billed_cpu_time_us);
}

最後我們看一下controller_impl中的具體是如何實現push_transaction的,關鍵的代碼如下,注意
其中的具體的註釋:

transaction_trace_ptr push_transaction( const transaction_metadata_ptr& trx,
                                        fc::time_point deadline,
                                        bool implicit,
                                        uint32_t billed_cpu_time_us)
{
  try {
    //首先生成transaction的context上下文,    
    transaction_context trx_context(self, trx->trx, trx->id);
    trx_context.deadline = deadline;
    trx_context.billed_cpu_time_us = billed_cpu_time_us;
    trace = trx_context.trace;
    try {
      ......
      //檢查actor是否有在黑名單中的
      if (trx_context.can_subjectively_fail &&
          pending->_block_status == controller::block_status::incomplete ) {
        check_actor_list( trx_context.bill_to_accounts );
      }
      //進行權限檢查
      trx_context.delay = fc::seconds(trx->trx.delay_sec);
      if(!self.skip_auth_check() && !implicit ) {
        authorization.check_authorization(
        trx->trx.actions,
        trx->recover_keys( chain_id ),
        {},
        trx_context.delay,
        [](){}
        false
        );
      }
      //執行transaction 這裏是關鍵的步驟,這裏將涉及到具體的transaction是如何繼續
      //往下走的
      trx_context.exec();
      trx_context.finalize();
      emit(self.applied_transaction, trace);
 
      trx_context.squash();
      restore.cancel();
      .....
      //此處省略和本次介紹無關的代碼
   } catch (const fc::exception& e) {
      trace->except = e;
      trace->except_ptr = std::current_exception();
   }
   return trace;
  } FC_CAPTURE_AND_RETHROW((trace))
}

我們先重點看下transaction_context的exec函數是如何實現的,關鍵代碼片段如下,從中我們可以
看到action的相關延遲處理邏輯,以及對是否是上下文無關的處理。

if( apply_context_free ) {
  for( const auto& act : trx.context_free_actions ) {
    trace->action_traces.emplace_back();
    dispatch_action( trace->action_traces.back(), act, true );
  }
}
//有延遲的函數被認定爲上下文相關
if( delay == fc::microseconds() ) {
  for( const auto& act : trx.actions ) {
    trace->action_traces.emplace_back();
    dispatch_action( trace->action_traces.back(), act );
  }
} else {
  schedule_transaction();
}

我們看下dispacth是如何進行分發action的,這裏需要注意,我們上一步關注的實體還是transaction,
這裏已經細化到action,後面將看到具體是如何處理的,例如inline action的處理。

void transaction_context::dispatch_action(
  action_trace& trace,//trance跟蹤
  const action& a, //傳入的action
  account_name receiver, //account 其值爲a.account
  bool context_free, //是否上下文無關
  uint32_t recurse_depth ) { //遞歸的層數,這裏用於inline action的遞歸調用
   //生成apply_context對象
   apply_context  acontext( control, * this, a, recurse_depth );
   acontext.context_free = context_free;
   acontext.receiver     = receiver;//這裏設置了賬戶,來自action
   try {
      acontext.exec();//關鍵函數,執行apply操作
   } catch( ... ) {
      trace = move(acontext.trace);
      throw;
   }
   trace = move(acontext.trace);
}

我們來看一下apply_context的exec的具體執行流程,如下代碼所示。我們可以看到其中遞歸的調用
但是在遞歸調用之前調用了exec_one函數,這個最終的關鍵:

_notified.push_back(receiver);
trace = exec_one();//關鍵函數
.......//此處省略非重要代碼
for ( const auto& inline_action : _ cfa_inline_actions ) {
  trace.inline_traces.emplace_back();
  trx_context.dispatch_action( trace.inline_traces.back(),
    inline_action,
    inline_action.account,
    true, recurse_depth + 1 );
}
//還是上下文無關的分開處理
for ( const auto& inline_action : _ inline_actions ) {
  trace.inline_traces.emplace_back();
  trx_context.dispatch_action( trace.inline_traces.back(),
  inline_action,
  inline_action.account,
  false, recurse_depth + 1 );
}

我們繼續展開最後的函數exec_one函數,其涉及到智能合約的關鍵代碼片段如下:

const auto &a = control.get_account(receiver);
  privileged = a.privileged;
  //這裏有兩種不同的過程要進行處理,分別是native的和傳入的,
  //更加準確的是系統的和智能合約的兩種不同的形式
  auto native = control.find_apply_handler(receiver, act.account, act.name);
  if( native ) {
     if( trx_context.can_subjectively_fail && control.is_producing_block() ) {
        control.check_contract_list( receiver );
        control.check_action_list( act.account, act.name );
     }
     (* native)(* this);
  }
  //如果說其code大於0,並且賬戶費系統並且非setcode則執行
  if( a.code.size() > 0 &&
    !(act.account == config::system_account_name &&
      act.name == N(setcode) && receiver == config::system_account_name) )
  {
     if( trx_context.can_subjectively_fail && control.is_producing_block() ) {
        control.check_contract_list( receiver );
        control.check_action_list( act.account, act.name );
     }
     try {
        //最後執行的apply函數
        //調用具體的apply函數進行執行
        control.get_wasm_interface().apply(a.code_version, a.code, *this);
     } catch ( const wasm_exit& ){}
  }

這裏我們先分析find_apply_handler的過程,首先我們先找到具體的handler註冊的機制
具體見文件eosio\eos\libraries\chain\controller.cpp

void set_apply_handler( account_name receiver, account_name contract,
  action_name action, apply_handler v ) {
  //具體實現爲一個map數據結構
  apply_handlers[receiver][make_pair(contract,action)] = v;
}

在controller_impl的初始化函數中,我們看到如下代碼片段,到此我們看到了我們的set contract
實際上是調用了系統的一個預設的合約或者說是函數。

#define SET_APP_HANDLER( receiver, contract, action) \
   set_apply_handler( #receiver, #contract, #action, \
     &BOOST_PP_CAT(apply_, BOOST_PP_CAT(contract, BOOST_PP_CAT(_ ,action) ) ) )
 
   SET_APP_HANDLER( eosio, eosio, newaccount );
   SET_APP_HANDLER( eosio, eosio, setcode );
   SET_APP_HANDLER( eosio, eosio, setabi );
   SET_APP_HANDLER( eosio, eosio, updateauth );
   SET_APP_HANDLER( eosio, eosio, deleteauth );
   SET_APP_HANDLER( eosio, eosio, linkauth );
   SET_APP_HANDLER( eosio, eosio, unlinkauth );

這裏我們看apply_contract_action的具體實現,代碼在eosio\eos\libraries\chain\eosio_contract.cpp
中,這個文件定義了系統的contract的具體實現,關鍵代碼片段如下:

auto& db = context.db;
//這裏set_code的具體格式如下
//struct setcode {
//   account_name     account;
//   uint8_t          vmtype = 0;
//   uint8_t          vmversion = 0;
//   bytes            code;
//}
auto  act = context.act.data_as<setcode>();
context.require_authorization(act.account);
fc::sha256 code_id; /// default ID == 0
if( act.code.size() > 0 ) {
   //計算具體的code_id
   code_id = fc::sha256::hash( act.code.data(), (uint32_t)act.code.size() );
   wasm_interface::validate(context.control, act.code);
}
const auto& account = db.get<account_object,by_name>(act.account);
int64_t code_size = (int64_t)act.code.size();
int64_t old_size  = (int64_t)account.code.size() * config::setcode_ram_bytes_multiplier;
int64_t new_size  = code_size * config::setcode_ram_bytes_multiplier;
//檢查前後的code的版本
FC_ASSERT( account.code_version != code_id,
  "contract is already running this version of code" );
//將code更新到db中
db.modify( account, [&]( auto& a ) {
  a.last_code_update = context.control.pending_block_time();
  a.code_version = code_id;
  a.code.resize( code_size );
  if( code_size > 0 )
     memcpy(a.code.data(), act.code.data(), code_size );
 
  });
}

小結 最後我們看到了我們的合約代碼被更新到對應的account中去,也就是智能合約賬戶中去。

EOS虛擬機服務端合約的調用執行

在EOS虛擬機核心接口一章中我們瞭解到調用虛擬機執行智能合約的接口函數爲apply,通過上面
的分析我們得知在執行action的時候我們發現在exec_one中有如下代碼片段:

try {
   //最後執行的apply函數
   //調用具體的apply函數進行執行
   control.get_wasm_interface().apply(a.code_version, a.code, *this);
} catch ( const wasm_exit& ){}

這樣我們就能整體的把握了具體的流程,代替順序如下:

transaction分發到nodeos
nodeos驗證transaction然後進行執行
由於transaction是由action組成的,所以最終落到具體的action上
在執行(exec_one)中調用apply的接口將具體的合約傳遞到虛擬機去執行 我們現在看一下wasm_interface的apply的實現:

void wasm_interface::apply( const digest_type& code_id,
const shared_string& code,
apply_context& context ) {
my->get_instantiated_module(code_id, code, context.trx_context)->apply(context);
}

其中get_instantiated_module的實現在wasm_interface_private.hpp中,具體如下:

auto it = instantiation_cache.find(code_id);
//如果內部沒有該智能合約的緩存則進行創建
if(it == instantiation_cache.end()) {
auto timer_pause = fc::make_scoped_exit([&](){
  trx_context.resume_billing_timer();
});
trx_context.pause_billing_timer();
IR::Module module;
try {
    //加載wasm二進制序列化對象
    Serialization::MemoryInputStream stream((const U8*)code.data(), code.size());
    WASM::serialize(stream, module);
    module.userSections.clear();
 } catch(const Serialization::FatalSerializationException& e) {
    EOS_ASSERT(false, wasm_serialization_error, e.message.c_str());
 } catch(const IR::ValidationException& e) {
    EOS_ASSERT(false, wasm_serialization_error, e.message.c_str());
 }
 //執行相關的注入代碼
 wasm_injections::wasm_binary_injection injector(module);
 injector.inject();
 
 std::vector<U8> bytes;
 try {
    Serialization::ArrayOutputStream outstream;
    WASM::serialize(outstream, module);
    bytes = outstream.getBytes();
 } catch(const Serialization::FatalSerializationException& e) {
    EOS_ASSERT(false, wasm_serialization_error, e.message.c_str());
 } catch(const IR::ValidationException& e) {
    EOS_ASSERT(false, wasm_serialization_error, e.message.c_str());
 }
 //生成新的wasm_instantiated_module_interface對象插入到map中去。
 it = instantiation_cache.emplace(code_id,
    runtime_interface->instantiate_module((const char*)bytes.data(),
     bytes.size(), parse_initial_memory(module))).first;
}
return it->second;
}

小結 通過以上代碼我們得知最後我們獲得的是一個wasm_instantiated_module_interface的對象,
然後調用apply函數來實現最後的作用,至此虛擬機應用層分析告一段落。上面代碼具體涉及到的
WASM-JIT範疇的內容,下一章將繼續詳細介紹。

EOS虛擬機Module IR生成

由上文得知函數get_instantiated_module會從傳入的二進制字節碼生成一個Module實例,下面我們就
具體分析一下其是如何進行解析生成的。現分析get_instantiated_module(wasm_interface_private.hpp)
中。首先看如下代碼片段:

IR::Module module;
try {
    Serialization::MemoryInputStream stream((const U8*)code.data(), code.size());
    WASM::serialize(stream, module); //該步驟完成從stream到module的轉化
    module.userSections.clear();
 }

函數WASM::serialize的函數原型如下,這裏先生成中間語言IR下的Module,然後再進行校驗。

void serialize(Serialization::InputStream& stream,Module& module)
{
  //函數serializeModule有兩個重載,在於一個參數是InputStream
  //而另外一個是OutputStream
  serializeModule(stream,module);
  IR::validateDefinitions(module);
}

現在看關鍵函數serializeModule的實現, 以下的爲關鍵代碼片段。具體的參見文件:
eos\libraries\wasm-jit\Source\WASM\WASMSerializatin.cpp

//首先讀取WASM文件頭部的MagicNumber和版本號
serializeConstant(moduleStream,"magic number",U32(magicNumber));
serializeConstant(moduleStream,"version",U32(currentVersion));
SectionType lastKnownSectionType = SectionType::unknown;
while(moduleStream.capacity())
{
  const SectionType sectionType = *(SectionType*)moduleStream.peek(sizeof(SectionType));
  if(sectionType != SectionType::user)
  {
    //這裏要求解析的的section的順序需要和已知的順序一致,具體的順序可以參考
    //類型SectionType的定義
    if(sectionType > lastKnownSectionType) { lastKnownSectionType = sectionType; }
    else { throw FatalSerializationException("incorrect order for known section"); }
  }
  switch(sectionType)
  {
    //如果解析的字節對應的類型是type,那麼調用反序列化接口
    case SectionType::type: serializeTypeSection(moduleStream,module); break;
    case SectionType::import: serializeImportSection(moduleStream,module); break;
    ........
    case SectionType::user:
    {2
          UserSection& userSection = * module.userSections.insert(
        module.userSections.end(),UserSection());
            serialize(moduleStream,userSection);
            break;
         }
         default: throw FatalSerializationException("unknown section ID");
    }
  ;
}

由上面函數的的代碼片段我們得知處理的整體思路是按照已知的SectionType的類型依次向下進行進行
這裏我們舉例分析serializeTypeSection,如下面的WASM的二進制格式我們得知,首先我們獲得一個字節
的type標識,然後一個字節是這個塊的大小,目前爲00,最後是這個塊裏面有多少個這樣的type的描述。
即其中num types對應的行。

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

我們現在看這裏我們舉例分析serializeTypeSection是如何處理的

serializeSection(moduleStream,SectionType::type,[&module](Stream& sectionStream)
{
  //函數serializeArray用來處理數組形式的type
  serializeArray(sectionStream,module.types,[](Stream& stream,
    const FunctionType*& functionType)
    {
      serializeConstant(stream,"function type tag",U8(0x60));
      if(Stream::isInput)
      {
          std::vector<ValueType> parameterTypes;
          ResultType returnType;
          serialize(stream,parameterTypes);
          serialize(stream,returnType);
      //根據參數列表和返回值列表生成函數的類型是
          functionType = FunctionType::get(returnType,parameterTypes);
      }
      else
      {
          serialize(stream,const_cast<std::vector<ValueType>&>(functionType->parameters));
          serialize(stream,const_cast<ResultType&>(functionType->ret));
      }
    });
});

這裏用來lamda表達式我們先看serializeArray函數的函數原型,如下:

template<typename Stream,typename Element,typename Allocator,typename SerializeElement>
void serializeArray(Stream& stream,std::vector<Element,Allocator>& vector,
  SerializeElement serializeElement)
{
//此處省略非重要的代碼
for(Uptr index = 0;index < size;++index)
{
    vector.push_back(Element());
  //以下函數代碼將調用匿名的lamda函數,
    serializeElement(stream,vector.back());
}
vector.shrink_to_fit()
}

通過上述的代碼我們得知serializeElement調用了匿名lamda函數,回到函數serializeSection中我們
得知最後module.types將存儲types的函數描述vector容器。
小結 通過上面的代碼描述我們知道從一段字節碼最後轉換爲Module對象,Module對象對後續的執行
有很大的幫助。
我們來看一下Module對象中的具體數據結構。

struct Module
{
    std::vector<const FunctionType*> types;
 
    IndexSpace<FunctionDef,IndexedFunctionType> functions;
    IndexSpace<TableDef,TableType> tables;
    IndexSpace<MemoryDef,MemoryType> memories;
    IndexSpace<GlobalDef,GlobalType> globals;
 
    std::vector<Export> exports;
    std::vector<DataSegment> dataSegments;
    std::vector<TableSegment> tableSegments;
    std::vector<UserSection> userSections;
 
    Uptr startFunctionIndex;
 
    Module() : startFunctionIndex(UINTPTR_MAX) {}
};

從上面的的代碼我們得知一個Module的具體內部結構,但是我們還沒有能進入IR層面。
現在我們回到函數void serialize(Serialization::InputStream& stream,Module& module)
在執行完成serializeModule後將執行IR::validateDefinitions(module);,我們來看下
具體的關鍵實現,具體見文件\eos\libraries\wasm-jit\Source\IR\Validate.cpp

//檢查FunctionType的參數        
for(Uptr typeIndex = 0;typeIndex < module.types.size();++typeIndex)
{
  const FunctionType* functionType = module.types[typeIndex];
  for(auto parameterType : functionType->parameters) { validate(parameterType); }
  validate(functionType->ret);
}
 
.......
//本處代碼爲依次檢查function_import memory_import table_import global_import
//function_def global_def table_def memory_def 以及export的內容
//一下的代碼用來獲取起始函數的函數類型
//這裏函數分支只有單獨運行虛擬機通過loadTextModule纔會使startFunctionIndex爲有效值
//我們這裏不需要,因爲入口函數就是apply
if(module.startFunctionIndex != UINTPTR_MAX)
{
    VALIDATE_INDEX(module.startFunctionIndex,module.functions.size());
    const FunctionType* startFunctionType = module.types[module.functions.
    getType(module.startFunctionIndex).index];
    VALIDATE_UNLESS("start function must not have any parameters or
    results: ",startFunctionType != FunctionType::get());
}
//剩下爲各種segment的檢查


 
接下來執行如下部分代碼
 
wasm_injections::wasm_binary_injection injector(module);
injector.inject();

主要向其中注入check_time函數代碼,通過add_export函數來具體實現,這裏就不描述。

VirtualMachine實例化

由上一個小節的介紹我們得知調用wasm_inteface的get_instantiated_module獲得一個Module
函數get_instantiated的最後代碼會調用自身數據成員runtime_interface的initantiate_module
函數來生成wasm_instantiated_module_interface的相關對象。在這裏有兩個類繼承了
wasm_instantiated_module_interface接口分別是:

  • binaryen_instantiated_module
  • wavm_instantiated_module

由上圖以及過程中的調用關係我們得到,首先我們確認runtime的類型是wavm還是binaryen,然後
我們就能確認接口函數instatiate_module返回的wasm_instantiated_moudle_interface的
具體類型是wavm_instantiated_module還是binayen_instantiated_module。

在類controller中具體見文件\eos\libraries\chain\controller.cpp中的成員定義:

wasm_interface::vm_type  wasm_runtime = chain::config::default_wasm_runtime;

而在文件controller.hpp中:

const static eosio::chain::wasm_interface::vm_type default_wasm_runtime =
eosio::chain::wasm_interface::vm_type::binaryen;

在controller_imp的構造函數中我們可以看到wasmif成員的初始化,如下所示

controller_impl( const controller::config& cfg, controller& s  )
   :self(s),
    db( cfg.state_dir,
        cfg.read_only ? database::read_only : database::read_write,
        cfg.state_size ),
    reversible_blocks( cfg.blocks_dir/config::reversible_blocks_dir_name,
        cfg.read_only ? database::read_only : database::read_write,
        cfg.reversible_cache_size ),
    blog( cfg.blocks_dir ),
    fork_db( cfg.state_dir ),
    wasmif( cfg.wasm_runtime ),//初始化wasm虛擬機的runtime
    resource_limits( db ),

因此我們得出結論,如果不是命令行指定虛擬機的種類,這裏默認爲binaryen類型。所以關鍵部分
展開後就爲binaryen_runtime的instantiate_module函數得到binaryen_instantiated_module
對象,最後調用其apply的方法。這裏需要關注文件eos\libraries\chain\webassembly下的文件

binaryen.cpp

  • binaryen_instantiated_module的定義
  • wasm_instantiated_module_interface的apply接口實現
  • binaryen_runtime的instantiate_module方法實現

wavm.cpp

  • wasm_instantiated_module_interface的apply接口實現
  • wasm_runtime接的instantiate_module方法實現

兩種不同的解釋器底層,在instantiated_module上不同畢竟一個用的是LLVM的JIT一個是用的
是Binaryen的解釋器。

Binaryen底層解釋器

ModuleInstance的創建
首先我們看一下binaryen_runtime的instantiate_module方法是如何生成binaryed_instatiated_module
的,具體相關代碼如下:

//首先創建WasmBinaryBuilder對象,這裏需要注意下,類WasnBinaryBuilder的實現
//在外部編譯依賴Binaryen中,文件位於external/binaryen/src/wasm-binaryen.h中
vector<char> code(code_bytes, code_bytes + code_size);
//Module類型爲Binaryen中的Module類型並非項目中IR的Module類型
unique_ptr<Module> module(new Module());
WasmBinaryBuilder builder(*module, code, false);
builder.read();
//獲取全局變量數值,並保存在global中
TrivialGlobalManager globals;
for (auto& global : module->globals) {
  globals[global->name] = ConstantExpressionRunner<TrivialGlobalManager>(globals).
  visit(global->init).value;
}
//間接調用表
call_indirect_table_type table;
table.resize(module->table.initial);
拷貝segment中的內容到間接調用表中去
for (auto& segment : module->table.segments) {
  Address offset = ConstantExpressionRunner<TrivialGlobalManager>(globals).
  visit(segment.offset).value.geti32();//獲得該段的大小
  FC_ASSERT( uint64_t(offset) + segment.data.size() <= module->table.initial);
  for (size_t i = 0; i != segment.data.size(); ++i) {
    table[offset + i] = segment.data[i];
  }
}
//獲得import的相關函數,用map數據結構去維護
import_lut_type import_lut;
import_lut.reserve(module->imports.size());
for (auto& import : module->imports) {
  std::string full_name = string(import->module.c_str()) + "." + string(import->base.c_str());
  if (import->kind == ExternalKind::Function) {
    auto& intrinsic_map = intrinsic_registrator::get_map();
    auto intrinsic_itr = intrinsic_map.find(full_name);
    if (intrinsic_itr != intrinsic_map.end()) {
       import_lut.emplace(make_pair((uintptr_t)import.get(), intrinsic_itr->second));
       continue;
    }
  }
}
//最後返回具體的instiated_module
return std::make_unique<binaryen_instantiated_module>(_ memory, initial_memory,
move(table), move(import_lut), move(module))
}

Appply接口的實現和調用
由上面的代碼我們得知binaryen的類型的instantiated_module需要的參數爲內存,訪問表
(線性的)以及導入的對象列表。當我們拿到一個instantiated_module後,我們看一下是如何
執行apply函數的。首先我們看一下函數apply的實現,會調用call函數,而從參數裏面我們知道
對於binaryen的相關內存的訪問都是線性的。

void apply(apply_context& context) override {
  LiteralList args = {Literal(uint64_t(context.receiver)),
  Literal(uint64_t(context.act.account)),
  Literal(uint64_t(context.act.name))};
  call("apply", args, context);
}

下面我們詳細的分析一下call函數具體執行了哪些操作

void call(const string& entry_point, LiteralList& args, apply_context& context){
  const unsigned initial_memory_size = _ module->memory.initial * Memory::kPageSize;
  //聲明一個解釋器接口,傳入的參數關鍵的爲導入的對象map即_import_lut
  interpreter_interface local_interface(_ shared_linear_memory, _ table, _ import_lut,
    initial_memory_size, context);
  //初始化內存和數據
  //zero out the initial pages
  memset(_ shared_linear_memory.data, 0, initial_memory_size);
  //copy back in the initial data
  memcpy(_ shared_linear_memory.data, _ initial_memory.data(), _ initial_memory.size());
 
  //生成module instance,這裏的初始化會調用start function
  ModuleInstance instance(* _ module.get(), &local_interface);
  //調用具體執行的函數
  instance.callExport(Name(entry_point), args);
}

如下兩個類型需要詳細的說明一下:

  • interpreter_interface
  • ModuleInstance

首先是interpreter_insterface,其類型如下

struct interpreter_interface : ModuleInstance::ExternalInterface

位於文件\eos\libraries\chain\include\eosio\chain\webassembly\binaryen.hpp
其中關鍵的函數爲callImport和callTable,現在簡要的說明一下:

Literal callImport(Import * import, LiteralList& args) override
{
  //由於import_lut中存儲的就是導入的函數或者對象的基本信息,
  //則這裏直接進行map的查找操作
  auto fn_iter = import_lut.find((uintptr_t)import);
  EOS_ASSERT(fn_iter != import_lut.end(), wasm_execution_error,\
   "unknown import ${m}:${n}", ("m", import->module.c_str())("n",\
    import->module.c_str()));
  return fn_iter->second(this, args);
}

這裏import_lut的類型爲unordered_map<uintptr_t, intrinsic_registrator::intrinsic_fn>
我們可以看到具體的導入的函數描述類型爲intrinsic_registrator::intrinsic_fn>這裏我們
看一下instrinsic_fn的具體類型:

struct intrinsic_registrator {
  using intrinsic_fn = Literal(*)(interpreter_interface*, LiteralList&);
  ......
}
}

參數就是一個實現了ModuleInstance::ExternalInterface的類和參數列表 下面我們看一下callTable的操作

Literal callTable(Index index, LiteralList& arguments, WasmType result,
  ModuleInstance& instance) override
{
  EOS_ASSERT(index < table.size(), wasm_execution_error, "callIndirect: bad pointer");
  //根據函數表類似於ELF中的GOT來獲取函數的指針
  auto* func = instance.wasm.getFunctionOrNull(table[index]);
  EOS_ASSERT(func, wasm_execution_error, "callIndirect: uninitialized element");
  EOS_ASSERT(func->params.size() == arguments.size(), \
  wasm_execution_error, "callIndirect: bad # of arguments");
  //進行參數檢查
  for (size_t i = 0; i < func->params.size(); i++) {
     EOS_ASSERT(func->params[i] == arguments[i].type,\
        wasm_execution_error, "callIndirect: bad argument type");
  }
  EOS_ASSERT(func->result == result, wasm_execution_error, "callIndirect: bad result type");
  //調用函數,這裏的invoke機制就是最後程序執行的最根本依賴,下面將詳細的分析一下
  return instance.callFunctionInternal(func->name, arguments);
}

從上面我們可以看到interpreter_interface封裝了函數的調用,無論是外部的還是內部自己實現的
下面我們來看一下ModuleInstance的具體實現,文件位於如下的位置:
eos\externals\binaryen\src\wasm-interpreter.h
其他實現相關具體的需要看ModuleInstanceBase,在其中有很多load函數的實現,主要是加載對應
的數據類型到內存中,現在我們看一下它的構造函數的實現:

ModuleInstanceBase(Module& wasm, ExternalInterface* externalInterface) :
  wasm(wasm),
  externalInterface(externalInterface) {
  // 導入外部全局的數據
  externalInterface->importGlobals(globals, wasm);
  // 準備內存
  memorySize = wasm.memory.initial;
  // 處理內部的全局數據
  for (auto& global : wasm.globals) {
    globals[global->name] = ConstantExpressionRunner<GlobalManager>(globals).
    visit(global->init).value;
  }
  //處理外部函數接口相關內容,這裏就是上面講到的interpter_inferface  
  externalInterface->init(wasm, *self());
  //運行函數starFunction
  if (wasm.start.is()) {
    LiteralList arguments;
    callFunction(wasm.start, arguments);
  }
}

CallFunction的實現

現在我們重點看一下callFunction是如何實現的,這樣對於我們理解最外層的callExport函數有一定
的幫助作用。下面我們來看下具體實現原理,如下爲其代碼片段,我們看到了久違的棧。

Literal callFunction(Name name, LiteralList& arguments) {
  callDepth = 0;
  functionStack.clear();//用到了棧
  return callFunctionInternal(name, arguments);
}

現在看關鍵函數callFunctionInternal的實現,在函數的實現中實現了兩個內部類,我們先看其主要
的流程:

Literal callFunctionInternal(Name name, LiteralList& arguments) {
  if (callDepth > maxCallDepth)
    externalInterface->trap("stack limit");
  auto previousCallDepth = callDepth;
  callDepth++;
  //保留之前函數的棧信息
  auto previousFunctionStackSize = functionStack.size();
  functionStack.push_back(name); //將函數名字入棧
  //獲得函數指針,這裏的函數指針所指向的內容後文將有所介紹
  Function* function = wasm.getFunction(name);
  ASSERT_THROW(function);
  //FunctinScope沒有具體的實際操作。基本上都是參數檢查和
  //返回值檢查
  FunctionScope scope(function, arguments);
  //這個類比較重要,這裏涉及到了具體的執行流程控制.
  RuntimeExpressionRunner rer(* this, scope);
  Flow flow = rer.visit(function->body);
  ASSERT_THROW(!flow.breaking() || flow.breakTo == RETURN_FLOW);
  Literal ret = flow.value; //最後獲得執行的結果
  if (function->result != ret.type) {
    if (rer.last_call.value.type == function->result && ret.type == 0) {
       ret = rer.last_call.value;
    }
    else {
      std::cerr << "calling " << function->name << " resulted in " << ret
      << " but the function type is " << function->result << '\n';
      WASM_UNREACHABLE();
    }
  }
  return ret;
}

下面我們重點分析一下如下代碼段,這段代碼段控制了這個數據流程。

RuntimeExpressionRunner rer(* this, scope);
Flow flow = rer.visit(function->body);

下面我們進入visit函數的實現,具體如下:

return Visitor<SubType, Flow>::visit(curr);

最後我們進入Vistior的具體定義實現:

struct Visitor {
  // Expression visitors
  ReturnType visitBlock(Block* curr) {}
  ReturnType visitIf(If* curr) {}
  .........
  // Module-level visitors
  ReturnType visitFunctionType(FunctionType* curr) {}
  ReturnType visitImport(Import* curr) {}
  ReturnType visitExport(Export* curr) {}
  ReturnType visitGlobal(Global* curr) {}
  ReturnType visitFunction(Function* curr) {}
  ReturnType visitTable(Table* curr) {}
  ReturnType visitMemory(Memory* curr) {}
  ReturnType visitModule(Module* curr) {}
  ///從這段代碼我們可以知道主要是SubType最後會影響
  ///我們創建的Visitior的類型,並影響調用的方法
  ReturnType visit(Expression* curr) {
    ASSERT_THROW(curr);
    #define DELEGATE(CLASS_TO_VISIT) \
      return static_cast<SubType*>(this)-> \
          visit##CLASS_TO_VISIT(static_cast<CLASS_TO_VISIT*>(curr))
 
    switch (curr->_id) {
      case Expression::Id::BlockId: DELEGATE(Block);
      case Expression::Id::IfId: DELEGATE(If);
      ......
      case Expression::Id::GetGlobalId: DELEGATE(GetGlobal);
      case Expression::Id::NopId: DELEGATE(Nop);
      case Expression::Id::UnreachableId: DELEGATE(Unreachable);
      case Expression::Id::InvalidId:
      default: WASM_UNREACHABLE();
    }
 
    #undef DELEGATE
  }
};

由調用關係我們知道SubType其實最後是受Expression的類型的影響,來自function->body
我們下面看一下Expresssion的類型以及具體的實現,在這裏Expression作爲Function類內重要
的成員,其定義如下:

class Expression {
public:
  enum Id {
    InvalidId = 0,
    BlockId,
    IfId,
    LoopId,
    .....
    HostId,
    NopId,
    UnreachableId,
    NumExpressionIds
  };
  Id _id;
 
  WasmType type; // the type of the expression: its *output*, not necessarily its input(s)
 
  Expression(Id id) : _ id(id), type(none) {}
 
  void finalize() {}
 
  template<class T>
  bool is() {
    return int(_ id) == int(T::SpecificId);
  }

我們自然想到其中枚舉的每一個類型都會有一個對應的子類,例如IfId,其對應的子類如下所示:

class If : public SpecificExpression<Expression::IfId> {
public:
  If() : ifFalse(nullptr) {}
  If(MixedArena& allocator) : If() {}
 
  Expression* condition;
  Expression* ifTrue;
  Expression* ifFalse;
 
  // set the type given you know its type, which is the case when parsing
  // s-expression or binary, as explicit types are given. the only additional work
  // this does is to set the type to unreachable in the cases that is needed.
  void finalize(WasmType type_);
 
  // set the type purely based on its contents.
  void finalize();
};

下面我們來看具體的實現

void If::finalize() {
  if (condition->type == unreachable) {
    type = unreachable;
  } else if (ifFalse) {
    if (ifTrue->type == ifFalse->type) {
      type = ifTrue->type;
    } else if (isConcreteWasmType(ifTrue->type) && ifFalse->type == unreachable) {
      type = ifTrue->type;
    } else if (isConcreteWasmType(ifFalse->type) && ifTrue->type == unreachable) {
      type = ifFalse->type;
    } else {
      type = none;
    }
  } else {
    type = none; // if without else
  }
}

上面爲對Expression的類型的介紹,對於語言無論什麼樣的程序塊,最後都會有一個類型。現在
我們以If爲例看其如何生成Visitor的,Visitor中的宏展開如下:

#define DELEGATE(CLASS_TO_VISIT) \
     return static_cast<SubType*>(this)-> \
         visit##CLASS_TO_VISIT(static_cast<CLASS_TO_VISIT*>(curr))
--------------------------------------------------------------------
return static_cast<RuntimeExpressionRunner*>(this)-> \
   visitIf(static_cast<If*>(curr))

這裏需要注意RuntimeExpressionRunner::public ExpressionRunner
雖然在RuntimeExpressionRunner中沒有visitIf的實現,但是在public ExpressionRunner中已經
已經有相關具體的代碼實現,這裏要區分類的繼承關係,具體代碼片段如下:

Flow visitIf(If *curr) {
  NOTE_ENTER("If");
  Flow flow = visit(curr->condition);//先訪問具體的條件
  if (flow.breaking()) return flow;
  NOTE_EVAL1(flow.value);
  if (flow.value.geti32()) {
    Flow flow = visit(curr->ifTrue);
    //處理是否跳轉到else中去繼續執行
    if (!flow.breaking() && !curr->ifFalse) flow.value = Literal();
    return flow;
  }
  if (curr->ifFalse) return visit(curr->ifFalse);
  return Flow();
}

目前我們會比較疑惑,那麼例如a < b這種是如何處理,這個是在visitBinary上來實現的
具體代碼如下:

Flow visitBinary(Binary *curr) {
  Flow flow = visit(curr->left);
  if (flow.breaking()) return flow;
  Literal left = flow.value;
  flow = visit(curr->right);
  if (flow.breaking()) return flow;
  Literal right = flow.value;
  ....
  case NeInt64:   return left.ne(right);
  case LtSInt64:  return left.ltS(right);
  case LtUInt64:  return left.ltU(right);
  case LeSInt64:  return left.leS(right);
  case LeUInt64:  return left.leU(right);
  case GtSInt64:  return left.gtS(right);
  case GtUInt64:  return left.gtU(right);
  case GeSInt64:  return left.geS(right);
  case GeUInt64:  return left.geU(right);
}

當然例如加法,減法,位操作等均有具體的case進行處理。
小結 根據上面的描述我們知道了如何從最上層面的函數,到最後的表達計算是如何實現。由於wast是
表達式形式,這裏是一種自頂向向下的計算方式。至此,我們從根本上了解了函數的執行流程。

WAVM底層解釋器

ModuleInstance的生成

WAVM的底層的實現不同於Binaryen,具體的Module的實現在如下的文件中:
eos\libraries\chain\webassembly\wavm.cpp.我們先看一下是如何生成具體的ModuleInstance的
具體代碼如下:

std::unique_ptr<Module> module = std::make_unique<Module>();
try {
  Serialization::MemoryInputStream stream((const U8*)code_bytes, code_size);
  WASM::serialize(stream, *module);
} catch(const Serialization::FatalSerializationException& e) {
  EOS_ASSERT(false, wasm_serialization_error, e.message.c_str());
} catch(const IR::ValidationException& e) {
  EOS_ASSERT(false, wasm_serialization_error, e.message.c_str());
}
//上面的代碼和Binaryen的沒有具體的區別,
eosio::chain::webassembly::common::root_resolver resolver;
//用來解決導入的外部函數符號的問題
LinkResult link_result = linkModule(*module, resolver);
ModuleInstance *instance = instantiateModule(*module,
  std::move(link_result.resolvedImports));
FC_ASSERT(instance != nullptr);
return std::make_unique<wavm_instantiated_module>(instance,
  std::move(module), initial_memory);

這裏我們要格外的注意如下代碼片段,這段代碼內部會調用LLVMJIT的compile方法,把模
塊編譯
成本地可以執行的代碼。

ModuleInstance *instance = instantiateModule(*module,
  std::move(link_result.resolvedImports));

函數內部將執行如下代碼:

LLVMJIT::instantiateModule(module,moduleInstance);
///這段代碼展開如下:
--------------------------------------------------
auto llvmModule = emitModule(module,moduleInstance);
// Construct the JIT compilation pipeline for this module.
auto jitModule = new JITModule(moduleInstance);
moduleInstance->jitModule = jitModule;
// Compile the module.
jitModule->compile(llvmModule);
Apply接口實現和調用

我們具體看下apply函數的代碼實現,在實現上他不同用戶Binaryen的方式,首先獲取函數運行
的指針,然後初始化相關需要使用的內存,最後調用Invoke來運行函數。

FunctionInstance* call = asFunctionNullable(getInstanceExport(_ instance,entry_point));
if( !call )
  return;
MemoryInstance* default_mem = getDefaultMemory(_ instance);
if(default_mem) {
   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);
runInstanceStartFunc(_ instance);
Runtime::invokeFunction(call,args);
}

有上main的代碼我們得知,其核心的爲invokeFunction實現,其實現如何將中間代碼進行運行。

InvokeFunction的實現

我們現在分析一下InvokeFunction函數的實現,並從中我們看一下WAVM是如何實現代碼運行的

Result invokeFunction(FunctionInstance* function,const std::vector<Value>& parameters)
{
const FunctionType* functionType = function->type;
//進行簡單的參數檢查
if(parameters.size() != functionType->parameters.size())
{
   throw Exception {Exception::Cause::invokeSignatureMismatch};
}
//爲函數的返回值和參數申請對應的內存
U64* thunkMemory = (U64*)alloca((functionType->parameters.size() +
getArity(functionType->ret)) * sizeof(U64));
//檢查函數的參數類型
for(Uptr parameterIndex = 0;parameterIndex < functionType->parameters.size();
  ++parameterIndex)
{
	if(functionType->parameters[parameterIndex] != parameters[parameterIndex].type)
	{
		throw Exception {Exception::Cause::invokeSignatureMismatch};
	}
	thunkMemory[parameterIndex] = parameters[parameterIndex].i64;
}
//獲得函數可以執行的指針,這裏將用到LLVM相關的IR技術,後面進行詳細介紹
LLVMJIT::InvokeFunctionPointer invokeFunctionPointer =
LLVMJIT::getInvokeThunk(functionType);
Result result;
Platform::HardwareTrapType trapType;
Platform::CallStack trapCallStack;
Uptr trapOperand;
trapType = Platform::catchHardwareTraps(trapCallStack,trapOperand,
	[&]
	{
		//調用函數,注意這裏的invokeFunctionPointer已經爲LLVM可以運行
    //的函數指針
		(* invokeFunctionPointer)(function->nativeFunction,thunkMemory);
    //獲得具體的返回值
    if(functionType->ret != ResultType::none)
		{
			result.type = functionType->ret;
			result.i64 = thunkMemory[functionType->parameters.size()];
		}
	});
}

由上面的代碼可以知道其關鍵的流程是如何從Module描述的代碼中得到對應的可以執行的函數
代碼段,我們先在仔細的分析一下其具體實現,可以先參考LLVM官網的實例,這樣會更好的理解
如下函數的具體實現:

//cache 重用已經解析過的函數
auto mapIt = invokeThunkTypeToSymbolMap.find(functionType);
if(mapIt != invokeThunkTypeToSymbolMap.end()) {
  return reinterpret_cast<InvokeFunctionPointer>(mapIt->second->baseAddress);
}
//--------------------------------------------------------------------------
//按照LLVM-JIT的要求現,先聲稱LLVM的Module對象
auto llvmModule = new llvm::Module("",context);
auto llvmFunctionType = llvm::FunctionType::get(
	llvmVoidType,
	{asLLVMType(functionType)->getPointerTo(),llvmI64Type->getPointerTo()},
	false);
//--------------------------------------------------------------------------
//創建function對象,這裏會根據FunctionType中的parameter和return的類型來創建對應
//的函數原型
auto llvmFunction = llvm::Function::Create(
  llvmFunctionType,
  llvm::Function::ExternalLinkage,
  "invokeThunk",
  llvmModule);
auto argIt = llvmFunction->args().begin();
llvm::Value* functionPointer = &*argIt++;
llvm::Value* argBaseAddress = &*argIt;
//--------------------------------------------------------------------------
//接着我們創建function的下一個層次,即爲block的結構
auto entryBlock = llvm::BasicBlock::Create(context,"entry",llvmFunction);
llvm::IRBuilder<> irBuilder(entryBlock);
//加載函數的參數,
td::vector<llvm::Value*> structArgLoads;
for(Uptr parameterIndex = 0;parameterIndex < functionType->parameters.size();
++parameterIndex)
{
	structArgLoads.push_back(irBuilder.CreateLoad(
		irBuilder.CreatePointerCast(
			irBuilder.CreateInBoundsGEP(argBaseAddress,{emitLiteral((Uptr)parameterIndex)}),
			asLLVMType(functionType->parameters[parameterIndex])->getPointerTo()
			)
		));
}
//調用irBuilder創建本地可以執行的函數指針
auto returnValue = irBuilder.CreateCall(functionPointer,structArgLoads);

// 如果有返回值,則創建存儲,並存儲返回值.
if(functionType->ret != ResultType::none)
{
	auto llvmResultType = asLLVMType(functionType->ret);
	irBuilder.CreateStore(
		returnValue,
		irBuilder.CreatePointerCast(
			irBuilder.CreateInBoundsGEP(argBaseAddress,
        {emitLiteral((Uptr)functionType->parameters.size())}),
			llvmResultType->getPointerTo()
			)
		);
}

irBuilder.CreateRetVoid();

//連接調用的函數
auto jitUnit = new JITInvokeThunkUnit(functionType);
jitUnit->compile(llvmModule);
//最後返回結果,只要將native可以執行的代碼傳遞給這個指針函數就可以運行
return reinterpret_cast<InvokeFunctionPointer>(jitUnit->symbol->baseAddress);

由上面的代碼我們得知,LLVMJIT的方式主要還是依賴於LLVM將函數代碼編譯成本地代碼,而上層
需要我們創建Module->Function->Block的這種關係,和對應的參數。

總結

本文深入的分析了EOS虛擬的實現所涉及到的相關技術,從上層到底層,從客戶端到服務端分析了
EOS中智能合約是如何運行和落地的,希望對你有所幫助。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章