主页 > imtoken钱包苹果版手机下载 > EOSIO源码分析-Wasm虚拟机与合约运行

EOSIO源码分析-Wasm虚拟机与合约运行

什么是wasm虚拟机

在区块链系统的开发中,目前主流的虚拟机有两种:

在目前的区块链系统中,有的系统支持以太坊虚拟机,有的支持wasm虚拟机,有的系统同时支持这两种虚拟机。

一些系统有其他虚拟实现。 比如迅雷链,早期是用C++和Lua实现的,后来更新为wasm虚拟机。

Wasm 虚拟机是现代区块链系统开发中的首选虚拟机。 与其他虚拟机相比,具有更高的安全性和更好的执行效率。 同时,合约语言的选择也大大增加。 可以选择C++,也可以选择Rust,然后可以选择wasm支持的其他语言。

那么 Wasm 到底是什么? WebAssembly,简称 WASM,是一种可移植、体积小、加载速度快的编码格式,可以运行在网络浏览器中。 Wasm 被设计为 C/C++/Rust 等高级语言的平台编译目标。

简单来说,Wasm 是系统在区块链中运行智能合约的沙箱

更多关于Wasm的详细信息,请参考以下链接:

wasm官网

EOSIO中Wasm的初始化

在EOSIO中,Wasm的初始化是在wasm_interface类中完成的,核心代码如下

wasm_interface_impl(wasm_interface::vm_type vm, bool eosvmoc_tierup, const chainbase::database& d, const boost::filesystem::path data_dir, const eosvmoc::config& eosvmoc_config) : db(d), wasm_runtime_time(vm) {
#ifdef EOSIO_EOS_VM_RUNTIME_ENABLED
   if(vm == wasm_interface::vm_type::eos_vm)
      runtime_interface = std::make_unique<webassembly::eos_vm_runtime::eos_vm_runtime<eosio::vm::interpreter>>();
#endif
}

注意:在 C++ 中,Wasm 使用 wbat 开发库运行

在这段代码中,Wasm 的初始化是在构造函数中实现的。 在wasm_interface类中,除了实例化runtime_interface对象外,还建立了代码的缓存列表。 调用合约时,可以直接读取代码句柄。

当然,整个Wasm的初始化肯定不止于此。 EOSIO中最核心的初始化是虚拟机与宿主机之间API调用的初始化。 虚拟机必须通过宿主机提供的接口访问链上数据。 具体过程如下

// 主机提供给虚拟机访问合约action数据的接口,虚拟机就是通过read_action_data读取了action的二进制数据,然后在虚拟机中反序列化后,最终传递给具体的action执行函数
REGISTER_INTRINSICS(action_api,
   (read_action_data,       int(int, int)  )
   (action_data_size,       int()          )
   (current_receiver,       int64_t()      )
);
// 主机提供的console日志输出API
REGISTER_INTRINSICS(console_api,
   (prints,                void(int)      )
   (prints_l,              void(int, int) )
   (printi,                void(int64_t)  )
   (printui,               void(int64_t)  )
   (printi128,             void(int)      )
   (printui128,            void(int)      )
   (printsf,               void(float)    )
   (printdf,               void(double)   )
   (printqf,               void(int)      )
   (printn,                void(int64_t)  )
   (printhex,              void(int, int) )
);

在EOSIO中,除了以上两套接口外,还提供了以下系列接口

如果以后我们要对EOSIO进行二次开发,扩展自己的API,可以在这里添加我们的开发

那么他是如何初始化的呢?继续展开宏REGISTER_INTRINSICS,我们可以得到如下结果

#define REGISTER_INTRINSICS(CLS, MEMBERS)\
   BOOST_PP_SEQ_FOR_EACH(_REGISTER_INTRINSIC, CLS, _WRAPPED_SEQ(MEMBERS))
#define _REGISTER_INTRINSIC_EXPLICIT(CLS, MOD, METHOD, WASM_SIG, NAME, SIG)\
   _REGISTER_WAVM_INTRINSIC(CLS, MOD, METHOD, WASM_SIG, NAME, SIG)         \
   _REGISTER_WABT_INTRINSIC(CLS, MOD, METHOD, WASM_SIG, NAME, SIG)         \
   _REGISTER_EOS_VM_INTRINSIC(CLS, MOD, METHOD, WASM_SIG, NAME, SIG)       \
   _REGISTER_EOSVMOC_INTRINSIC(CLS, MOD, METHOD, WASM_SIG, NAME, SIG)
#define _REGISTER_WABT_INTRINSIC(CLS, MOD, METHOD, WASM_SIG, NAME, SIG)\
  static eosio::chain::webassembly::wabt_runtime::intrinsic_registrator _INTRINSIC_NAME(__wabt_intrinsic_fn, __COUNTER__) (\
     MOD,\
     NAME,\
     eosio::chain::webassembly::wabt_runtime::wabt_function_type_provider<WASM_SIG>::type(),\
     eosio::chain::webassembly::wabt_runtime::intrinsic_function_invoker_wrapper<SIG>::type::fn<&CLS::METHOD>()\
  );\

这里很明显的展示了生成了一个名为_INTRINSIC_NAME的静态类,并在这个类中注入了相应的函数函数回调。 intrinsic_registrator的结构如下

struct intrinsic_registrator {
   using intrinsic_fn = TypedValue(*)(wabt_apply_instance_vars&, const TypedValues&);
   struct intrinsic_func_info {
      FuncSignature sig;
      intrinsic_fn func;
   };
   static auto& get_map(){
      static map<string, map<string, intrinsic_func_info>> _map;
      return _map;
   };
   intrinsic_registrator(const char* mod, const char* name, const FuncSignature& sig, intrinsic_fn fn) {
      get_map()[string(mod)][string(name)] = intrinsic_func_info{sig, fn};
   }
};

注册的最终结果将宿主函数的相关信息存储到结构体变量_map中。

至此,在EOSIO中以太坊源码分析,Wasm虚拟机最重要的初始模块就完成了,就可以等待调用合约了。

函数合约调用

如前所述,函数合约最终会被转移到Wasm虚拟机中,入口如下:

control.get_wasm_interface().apply( receiver_account->code_hash, receiver_account->vm_type, receiver_account->vm_version, *this );

接下来我们进入相关函数,一步步看合约action最终是如何调用的

// 调用wasm_interface::apply函数
void wasm_interface::apply( const digest_type& code_hash, const uint8_t& vm_type, const uint8_t& vm_version, apply_context& context ) {
      // 函数get_instantiated_module根据code_hash,vm_type,vm_version获取正确的code,并且加载进wasm虚拟机中,在加载的过程中会对对应的code进行验证,验证其是否合法,最后成功加载完成之后,会将对应的句柄加入cache,方便下次调用
      // 获取对应的code的instance之后,调用apply函数
      my->get_instantiated_module(code_hash, vm_type, vm_version, context.trx_context)->apply(context);
   }

接下来我们进入apply函数继续细看

// 进入wabt_instantiated_module类的apply函数
void apply(apply_context& context) override {
    //reset mutable globals
    for(const auto& mg : _initial_globals)
       mg.first->typed_value = mg.second;
	// 注意context变量的保存,最终保存在静态变量static_wabt_vars
	// 想想前面宿主函数的形式,是不是也和context变量关联
    wabt_apply_instance_vars this_run_vars{nullptr, context};
    static_wabt_vars = &this_run_vars;
    //reset memory to inital size & copy back in initial data
    //这里主要初始化调用时内存信息的相关数据
    if(_env->GetMemoryCount()) {
       Memory* memory = this_run_vars.memory = _env->GetMemory(0);
       memory->page_limits = _initial_memory_configuration;
       memory->data.resize(_initial_memory_configuration.initial * WABT_PAGE_SIZE);
       memcpy(memory->data.data(), _initial_memory.data(), _initial_memory.size());
       memset(memory->data.data() + _initial_memory.size(), 0, memory->data.size() - _initial_memory.size());
    }
	// 传递最终调用的函数参数,三个参数都是uint64
	// 这里为什么参数类型都是uint64,是因为在传递中uint64是可以直接传递给wasm的,如果是字符串是需要通过内存拷贝进入的,具体参看action中data参数的传递,使用read_action_data函数完成
	// 因为name与uint64是可以相互转化的,所以可以这样使用
    _params[0].set_i64(context.get_receiver().to_uint64_t());
    _params[1].set_i64(context.get_action().account.to_uint64_t());
    _params[2].set_i64(context.get_action().name.to_uint64_t());
    ExecResult res = _executor.RunStartFunction(_instatiated_module);
    EOS_ASSERT( res.result == interp::Result::Ok, wasm_execution_error, "wabt start function failure (${s})", ("s", ResultToString(res.result)) );
	// 最终调用wasm合约导出函数apply
	// 这里产生一个疑问,我们在写合约时并没有看到apply函数,我们观察cdt的中关于eosio的库,也没有apply函数, 那么apply函数是怎么出现的,合约编译时又有哪些趣事呢,我们后面再讲
    res = _executor.RunExportByName(_instatiated_module, "apply", _params);
    EOS_ASSERT( res.result == interp::Result::Ok, wasm_execution_error, "wabt execution failure (${s})", ("s", ResultToString(res.result)) );
 }

至此,合约调用其实已经进入了wasm虚拟机,接下来需要仔细阅读wbat代码。

关于合约对宿主函数的调用

我们在分析宿主函数的初始化时,注意到宿主函数最终注册在一个map结构中,而宿主函数是apply_context类型的,所以合约对宿主函数的调用肯定与这些有关。最后以太坊源码分析,根据trace发现调用代码如下

template<>
struct intrinsic_invoker_impl<void_type, std::tuple<>> {
   using next_method_type        = void_type (*)(wabt_apply_instance_vars&, const TypedValues&, int);
   template<next_method_type Method>
   static TypedValue invoke(wabt_apply_instance_vars& vars, const TypedValues& args) {
      Method(vars, args, args.size() - 1);
      return TypedValue(Type::Void);
   }
   template<next_method_type Method>
   static const auto fn() {
      return invoke<Method>;
   }
};

这里定义了几个intrinsic_invoker_impl的实现,它们分别对应不同的参数和调用。 最后宿主函数的调用会进入apply_context类进行相应的业务处理。

总结

总的来说,EOSIO中合约的运行是比较透明的,它使用了wasm虚拟机,如果继续深入下去,我们需要进入wbat的源码才能看到,我们的目的是了解easm在eosio中的使用它,我们可以对其进行二次开发,进而开发出属于自己的区块链系统。

以上知识点,我们的核心可以归纳为以下几点:

扩展摘要

如果我们要对EOSIO进行二次开发,通过本文的理解,我们可以考虑做以下改动

我们也引入了新的问题

这些都是需要我们继续认真阅读CDT代码,了解wasm文件相关信息的答案。