您现在的位置是:首页 >技术教程 >Wasmtime运行.wasm文件的流程解析网站首页技术教程

Wasmtime运行.wasm文件的流程解析

苦逼程序员233 2024-06-17 11:19:30
简介Wasmtime运行.wasm文件的流程解析

Wasmtime运行.wasm文件的流程

commands/run.rs中,通过execute(&self)执行wasmtime命令。在这个函数中,首先通过Store::new创建store,以Host::default()作为参数创建出Store<host>对象。然后调用populate_with_wasi函数。在populate_with_wasi中,首先调用wasmtime_wasi::add_to_linker函数将所有的WASI函数(wasi_snapshot_preview1::WasiSnapshotPreview1)添加到linker的map(HashMap<ImportKey, Definition>)中。其中,Definition包含在编译wasmtime阶段生成的HostFunc,每一个HostFunc对应一个WASI函数。编译wasmtime时会调用wiggle/generate/src/wasmtime.rs中的link_module函数,这个函数在编译wasmtime时动态生成snapshots::preview_1::add_wasi_snapshot_preview1_to_linker函数。wasmtime_wasi::add_to_linker会调用snapshots::preview_1::add_wasi_snapshot_preview1_to_linker函数。这个函数是将WASI添加到linker中的实际执行者。

模块编译

编译的入口点在 wasmtime::Module::from_binary API,也有一些其他入口用来处理从文本模式翻译成二进制模式,从文件系统加载等上层的细节。

编译过程可以简单地划分成几个阶段:

  1. 模块内的所有函数都被验证然后并行编译。这是在compile_functions中通过engine.run_maybe_parallel来实现的。每一个函数在这个阶段都是独立编译的,不存在过程间分析。Cranelift在每个函数上进行调用。

  2. 这里的编译结果被组织成一个 CompiledModuleInfo 结构,在finish_compile函数中生成。这个结构包含了模块信息(wasmtime_environ::Module),编译的JIT代码(存储为ELF镜像),以及其他杂项信息入平台如平台无关的unwinding 信息,每个函数的trap表(指明哪一个JIT指令可以 trap 以每个trap的含义),每个函数的地址映射(将JIT 地址映射到WASM偏移)以及调试信息(已解析的 WASM 模块的 DWARF 信息)。

  3. 最后,这个结构被包装在ModuleTranslation结构体的environ::Module结构中。此时,模块已经准备好被初始化了。

Trampolines

关于编译一个很重要的点是 Trampolines 的创建。Trampolines在这里指wasmtime为了进入WebAssembly代码所执行的代码段。Trampolines是由wasmtime/src/trampoline/func.rs中的create_function创建的,生成的trampoline类型是VMTrampoline。Wasmtime收集所有模块的导出函数,然后为export的函数创建trampoline。而create_function是在load_main_module加载wasm模块时调用的。

首先,load_main_module会调用linker.module,在linker.module中会调用linker的self.command函数。调用self.command会传入一个匿名函数,这个匿名函数是实际调用_start的入口。Trampolines需要和 wasmtime::Func::call API 配合使用,因此此匿名函数调用Func::new创建Func对象。在Func::new中也会传入一个匿名函数,这个匿名函数实际执行实例化过程,然后调用_start进入wasm代码。

另一个需要注意的点是 trampolines 此时并没有去重,每个编译后的模块包含了自己的trampolines集合,如果两个编译后的module拥有相同的类型,他们将会有 trampolines 的不同的拷贝。

类型驻留和 VMSharedSignatureIndex

关于编译过程另一个需要讨论的重要点是 VMSharedSignatureIndex 及其用法。call_indirect 操作码会对比函数的实际签名和指令的函数签名,如果签名不匹配则发生 trap。这在wasmtime内部是一个整数比较,比较发生在 VMSharedSignatureIndex 上。这里的Index是一个函数类型的 interned 表示。

VMSharedSignatureIndex 的驻留范围发生在 wasmtime::Engine 级别。模块被编译到 Engine 中,将 Module 插入到 Engine 中会将模块的所有类型分配 VMSharedSignatureIndex

模块的VMSharedSignatureIndex值是模块本地实例的(这个值可能会随着每次模块插入到不同的Engine而改变)。这些值在模块初始化的过程中被wasmtime使用,用于给每个导入函数之类的对象分配一个类型ID。

实例化模块

一旦一个模块编译完成,它就可以被实例化,然后调用exports出来的函数。实例化(crates/wasmtime/src/instance.rs) 可能看起来很复杂,执行实例化的入口是上面提到的在linker.module中调用self.command时传入的匿名函数,然后这个匿名函数调用Func::new时又传入了一个匿名函数。第二个匿名函数实际调用instance_pre.instantiate进行实例化。实例化的大致流程如下:

  1. 在完成实例化之前,每一个 wasmtime_environ::Module 都有一系列的initializers需要完成。然而,在 MVP 版wasm中,这只是加载所有的imports到index array,但是对于 module link,这可能会要求实例化其他的模块,处理别名等。在任何场景下,这一步的结果都是将所有的imports放入OwnedImports结构体中,OwnedImports中有所有的需要导入到wasm模块的值。需要注意的是,在这里 import 通常是实际状态的指针(ptr::NonNull)和import from的实例的VMOpaqueContext。变量instance_pre是在self.command中通过self.instantiate_pre创建的。在self.instantiate_pre中通过self._get_by_import将所有的imports转化为Definition结构体,然后传入InstancePre::new中。接着,将Definition中保存的HostFunc放入OwnedImports中。然后,instance_pre.instantiate调用Instance::new_started来创建实例。而Instance::new_started又会调用Instance::new_raw函数。Instance::new_raw这一步的结果是InstanceAllocationRequest, 最后InstanceAllocationRequest会被提交到配置好的按需或者池化的Instance allocator

  2. 通过InstanceAllocationRequest分配实例对应的InstanceHandle。如何分配取决于策略(按需分配策略采用malloc,polling策略采用slab分配器)。除了初始化InstanceHandle的字段,也初始化VMContext的所有字段(其分配和InstanceHandle是相邻的)。在crates/wasmtime/src/instance.rs中调用allocate分配InstanceAllocationRequest时,allocate会调用Instance::new_at函数。而Instance::new_at通过initialize_vmctx初始化与这个Instance相关的VMContext数据。

  3. 此时,InstanceHandle 存储在 Store 中,handle是无需返回的,handle的生命周期需要和 Store 一致。

关于实例化另一个值得一提的点是 Store 为所有实例化的 Module 维护了一个ModuleRegistry。这个ModuleRegistry存储在 StoreStoreInnerStoreOpaque结构体中,其目的是保留运行这个实例需要的items的强引用。这主要是指JIT code,也包括了VMSharedSignatureIndex 注册,函数地址的元数据等其他信息。大量的这些信息都存储在 GLOBAL_MODULES 映射中,GLOBAL_MODULES的类型是Lazy<RwLock<GlobalModuleRegistry>>,以便 trap 时可以访问到这些信息。

Traps

一旦实例被创建,wasm开始执行,大部分的事情都很标准化了。Trampolines用于进入wasm,而JIT代码正常运行以执行wasm模块。在上面提到的第二个匿名函数中,首先调用instance_pre.instantiate进行实例化,然后调用instance的export出来的_start来真正进入wasm代码的执行。通过调用Func::call函数,然后最终调用到call_unchecked_raw函数。在call_unchecked_raw中调用invoke_wasm_and_catch_traps来调用wasm代码。关于这个实现的一件重要的事情是traps。

Wasmtime使用 longjmpsetjmp 来实现trap。setjmp不能在rust中定义(即使是以unsafe的方式定义也不行,https://github.com/rust-lang/rfcs/issues/2625)。因此 setjmp/longjmpcrates/runtime/src/helpers.c中被调用。需要注意的是在rust中 longjmp 操作并不是安全的,因为它跳过了 stack-based 析构器。因此当call back回到rust时,在 setjmp 之后,一旦 wasm 被调用,我们需要在wasmtime中小心确保栈上没有重要的析构器。

有几个不同的trap来源:

  • 明确的traps:这发生在宿主调用返回一个trap的场景。例如,当调用 raise_user_trap 或者 raise_lib_trap 时,这两个都会立刻调用 longjmp 以返回到wasm的起始点。需要注意的是,就像调用 wasm 一样,这些调用的调用者需要小心的确保栈上没有任何析构器。
  • 信号:这是trap向量的主要部分。基本上我们使用段错误或者非法指令来在wasm中实现trap。当内存访问越界的时候会发生段错误,当执行到 unreachable 的时候会发生非法指令。在两种场景下,wasmtime都会安装一个与平台相关信号处理器来捕获信号,然后处理信号。注意 Wasmtime 只会尝试捕获 JIT code 中的信号,而不会捕获其他BUG。使用 longjmp 会退出信号处理函数并回到wasm调用侧。

执行入口

最后需要说明的一点是整个wasm代码的执行入口。在load_main_module中调用的linker.module函数的作用是在linker中定义 Module 的自动实例化。因此,linker.module可以说是整个实例化的过程。然后,在load_main_module中调用self.invoke_func来开始进入执行wasm代码的流程。self.invoke_func调用的func是上面提到的第二个匿名函数,通过invoke_func接口来执行func也会通过trampoline来调用。因此,这个func会首先执行一遍trampoline的流程,即通过invoke_wasm_and_catch_traps来调用。然后,真正的_start是在self.command的匿名函数调用Func::new时,在HostFunc::new_unchecked中由包装的匿名函数调用的func。在HostFunc::new_unchecked中通过crate::trampoline::create_function_start创建trampoline,然后在HostFunc::new_unchecked中创建的匿名函数中直接调用,这时会第二次经过trampoline进入wasm执行_start函数。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。