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

什么是wasm虚拟机

在区块链系统开中,目前主流的虚拟机有两大类

  • 以太坊虚拟机:开发语言是特殊定制的solidity
  • wasm虚拟机:开发语言可以是C++,Rest等,通俗讲就是可以编译成wasm目标格式的语言都可以

在目前的区块链系统中,有的系统支持以太坊虚拟久,有些支持wasm虚拟机,也有系统两种虚拟机都支持的。
有些系统还有另外的虚拟实现,如迅雷链早期就是用过c++与lua实现过,后面更新成wasm虚拟机了。

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

那么Wasm到底是什么呢?WebAssembly简称WASM,它是可以运行在Web浏览器中的一种可移植、体积小、加载快的编码格式,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对象,同时还建立了code的缓存列表,在合约调用时,可以直接读取code句柄。

当然整个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中,除了上面的两组接口,还提供了以下一系列接口

  • compiler_builtins:内置操作运算API
  • privileged_api,producer_api:生产控制相关高级API
  • transaction_context:交易运行超时相关API
  • database_api:数据库操作相关API,我们在合约开发中实际调用最多的API,合约通过这一类API访问chainbase数据库
  • crypto_api:加密安全相关API
  • context_free_system_api:断言相关API,在合约中断言失败,会中断合约的运行
  • softfloat_api:浮点运算相关的API,因为区块链运行与不同的平台,如ubuntu,Mac等,所以在wasm中运行的合约特别是服点运算这一块,必须要精度一致

如后续我们要在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类型,那么合约对宿主函数的调用,肯定和这些相关。最后根据跟踪我们发现,调用代码如下

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的源代码去看,我们的目的是了解wasm在eosio中的使用,我们可以在上面进行二次开发,进而可以开发属于我们自己的区块链系统。

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

  • wasm在初始化时,最核心的是初始化宿主函数,也就是提供合约调用链相关的API
  • 合约最终的调用入口是合约导出函数apply
  • 在链上apply_context,wasm_interface是和wasm虚拟机打交道最深的两个类,一个提供初始化接口,一个负责具体的业务逻辑
  • apply函数的参数是三个uint64类型的数值,在合约中会和name类互转
  • action中data的数值是通过read_action_data宿主函数传递的,而后在合约中反序列化为具体的参数
扩展总结

如果我们要对EOSIO进行二次开发,通过这篇文章的理解,我们可以思考如下的改动

  • 增加属于我们的宿主函数
  • 如果我们在熟悉CDT源代码,改变action中data的序列化规则
  • 在CDT中开发封装属于我们自己的合约库
  • 甚至模拟以太坊的合约事件机制,为eosio增加我们自己的事件

同时我们也引入了新的问题

  • 在wasm格式的文件中,导出函数是个啥?
  • 在eosio中,apply导出函数是怎么出现的

这些是需要我们继续细读CDT的代码,并且了解wasm文件相关信息才能解答的。

猜你喜欢

转载自blog.csdn.net/whg1016/article/details/128692000