• 3. XVM虚拟机
    • 3.1. 背景
    • 3.2. WASM简介
    • 3.3. WASM字节码编译加载流程
      • 3.3.1. 字节码编译
      • 3.3.2. 加载运行
    • 3.4. 语言运行环境
      • 3.4.1. c++运行环境
      • 3.4.2. go运行环境
    • 3.5. XuperBridge对接
    • 3.6. 资源消耗统计

    3. XVM虚拟机

    3.1. 背景

    XVM为合约提供一个稳定的沙盒运行环境,有如下目标:

    • 隔离性,合约运行环境和xchain运行环境互不影响,合约的崩溃不影响xchain。
    • 确定性,合约可以访问链上资源,但不能访问宿主机资源,保证在确定的输入下有确定的输出
    • 可停止性,设置资源quota,合约对资源的使用超quta自动停止
    • 可以统计合约的资源使用情况,如CPU,内存等
    • 运行速度尽量快。

    3.2. WASM简介

    WASM是WebAssembly的缩写,是一种运行在浏览器上的字节码,用于解决js在浏览器上的性能不足的问题。WASM的指令跟机器码很相似,因此很多高级语言如C,C++,Go,rust等都可以编译成WASM字节码从而可以运行在浏览器上。很多性能相关的模块可以通过用C/C++来编写,再编译成WASM来提高性能,如视频解码器,运行在网页的游戏引擎,React的虚拟Dom渲染算法等。

    WASM本身只是一个指令集,并没有限定运行环境,因此只要实现相应的解释器,WASM也可以运行在非浏览器环境。xchain的WASM合约正是这样的应用场景,通过用C++,go等高级语言来编写智能合约,再编译成WASM字节码,最后由XVM虚拟机来运行。XVM虚拟机在这里就提供了一个WASM的运行环境。

    3.3. WASM字节码编译加载流程

    WASM字节码的运行有两种方式,一种是解释执行,一种是编译成本地指令后再运行。前者针对每条指令挨个解释执行,后者通过把WASM指令映射到本地指令如(x86)来执行,解释执行有点是启动快,缺点是运行慢,编译执行由于有一个预先编译的过程因此启动速度比较慢,但运行速度很快。

    XVM选用的是编译执行模式。

    XVM编译加载流程XVM编译加载流程

    3.3.1. 字节码编译

    用户通过c++编写智能合约,通过emcc编译器生成wasm字节码,xvm加载字节码,生成加入了指令资源统计的代码以及一些运行时库符号查找的机制,最后编译成本地指令来运行。

    c++合约代码

    1. int add(int a, int b) {
    2. return a + b;
    3. }

    编译后的WASM文本表示

    1. (module
    2. (func $add (param i32 i32) (result i32)
    3. local.get 0
    4. local.get 1
    5. i32.add)
    6. (export "_add" (func $add)))

    XVM编译WASM到c,最后再生成动态链接库。

    1. static u32 _add(wasm_rt_handle_t* h, u32 p0, u32 p1) {
    2. FUNC_PROLOGUE;
    3. u32 i0, i1;
    4. ADD_AND_CHECK_GAS(3);
    5. i0 = p0;
    6. i1 = p1;
    7. i0 += i1;
    8. FUNC_EPILOGUE;
    9. return i0;
    10. }
    11. /* export: '_add' */
    12. u32 (*export__add)(wasm_rt_handle_t*, u32, u32);
    13.  
    14. static void init_exports(wasm_rt_handle_t* h) {
    15. /* export: '_add' */
    16. export__add = (&_add);
    17. }

    3.3.2. 加载运行

    在了解如何加载运行之前先看下如何使用xvm来发起对合约的调用,首先生成Code对象,Code对象管理静态的指令代码以及合约所需要的符号解析器Resolver。之后就可以通过实例化Context对象来发起一次合约调用,GasLimit等参数就是在这里传入的。Code和Context的关系类似Docker里面的镜像和容器的关系,一个是静态的,一个是动态的。

    1. func run(modulePath string, method string, args []string) error {
    2. code, err := exec.NewCode(modulePath, emscripten.NewResolver())
    3. if err != nil {
    4. return err
    5. }
    6. defer code.Release()
    7.  
    8. ctx, err := exec.NewContext(code, exec.DefaultContextConfig())
    9. if err != nil {
    10. return err
    11. }
    12. ret, err := ctx.Exec(method, []int64{int64(argc), int64(argv)})
    13. fmt.Println(ret)
    14. return err
    15. }

    转换后的c代码最终会编译成一个动态链接库来给XVM运行时来使用,在每个生成的动态链接库里面都有如下初始化函数。这个初始化函数会自动对wasm里面的各个模块进行初始化,包括全局变量、内存、table、外部符号解析等。

    1. typedef struct {
    2. void* user_ctx;
    3. wasm_rt_gas_t gas;
    4. u32 g0;
    5. uint32_t call_stack_depth;
    6. }wasm_rt_handle_t;
    7.  
    8.  
    9. void* new_handle(void* user_ctx) {
    10. wasm_rt_handle_t* h = (*g_rt_ops.wasm_rt_malloc)(user_ctx, sizeof(wasm_rt_handle_t));
    11. (h->user_ctx) = user_ctx;
    12. init_globals(h);
    13. init_memory(h);
    14. init_table(h);
    15. return h;
    16. }

    3.4. 语言运行环境

    3.4.1. c++运行环境

    c++因为没有runtime,因此运行环境相对比较简单,只需要设置基础的堆栈分布以及一些系统函数还有emscripten的运行时函数即可。

    c++合约的内存分布

    c++合约的内存分布c++合约的内存分布

    普通调用如何在xvm解释

    xvm符号解析xvm符号解析

    3.4.2. go运行环境

    go合约运行时结构go合约运行时结构

    3.5. XuperBridge对接

    XVM跟XuperBridge对接主要靠两个函数

    • call_method,这个函数向Bridge传递需要调用的方法和参数
    • fetch_response,这个函数向Bridge获取上次调用的结果
    1. extern "C" uint32_t call_method(const char* method, uint32_t method_len,
    2. const char* request, uint32_t request_len);
    3. extern "C" uint32_t fetch_response(char* response, uint32_t response_len);
    4.  
    5. static bool syscall_raw(const std::string& method, const std::string& request,
    6. std::string* response) {
    7. uint32_t response_len;
    8. response_len = call_method(method.data(), uint32_t(method.size()),
    9. request.data(), uint32_t(request.size()));
    10. if (response_len <= 0) {
    11. return true;
    12. }
    13. response->resize(response_len + 1, 0);
    14. uint32_t success;
    15. success = fetch_response(&(*response)[0u], response_len);
    16. return success == 1;
    17. }

    3.6. 资源消耗统计

    考虑到大部分指令都是顺序执行的,因此不需要在每个指令后面加上gas统计指令,只需要在control block最开头加上gas统计指令,所谓control block指的是loop, if等会引起跳转的指令。

    c++代码

    1. extern int get(void);
    2. extern void print(int);
    3.  
    4. int main() {
    5. int i = get();
    6. int n = get();
    7. if (i < n) {
    8. i += 1;
    9. print(i);
    10. }
    11. print(n);
    12. }

    编译后生成的wast代码

    1. (func (;2;) (type 1) (result i32)
    2. (local i32 i32)
    3. call 1
    4. local.tee 0
    5. call 1
    6. local.tee 1
    7. i32.lt_s
    8. if ;; label = @1
    9. local.get 0
    10. i32.const 1
    11. i32.add
    12. call 0
    13. end
    14. local.get 1
    15. call 0
    16. i32.const 0)

    生成的带统计指令的c代码

    1. static u32 wasm__main(wasm_rt_handle_t* h) {
    2. u32 l0 = 0, l1 = 0;
    3. FUNC_PROLOGUE;
    4. u32 i0, i1;
    5. ADD_AND_CHECK_GAS(11);
    6. i0 = wasm_env__get(h);
    7. l0 = i0;
    8. i1 = wasm_env__get(h);
    9. l1 = i1;
    10. i0 = (u32)((s32)i0 < (s32)i1);
    11. if (i0) {
    12. ADD_AND_CHECK_GAS(6);
    13. i0 = l0;
    14. i1 = 1u;
    15. i0 += i1;
    16. wasm_env__print(h, i0);
    17. }
    18. ADD_AND_CHECK_GAS(5);
    19. i0 = l1;
    20. wasm_env__print(h, i0);
    21. i0 = 0u;
    22. FUNC_EPILOGUE;
    23. return i0;
    24. }