主页 > 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文件相关信息的答案。