您现在的位置是:首页 >技术教程 >Wasmtime运行.wasm文件的流程解析网站首页技术教程
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,也有一些其他入口用来处理从文本模式翻译成二进制模式,从文件系统加载等上层的细节。
编译过程可以简单地划分成几个阶段:
-
模块内的所有函数都被验证然后并行编译。这是在
compile_functions
中通过engine.run_maybe_parallel
来实现的。每一个函数在这个阶段都是独立编译的,不存在过程间分析。Cranelift在每个函数上进行调用。 -
这里的编译结果被组织成一个
CompiledModuleInfo
结构,在finish_compile
函数中生成。这个结构包含了模块信息(wasmtime_environ::Module
),编译的JIT代码(存储为ELF镜像),以及其他杂项信息入平台如平台无关的unwinding 信息,每个函数的trap表(指明哪一个JIT指令可以 trap 以每个trap的含义),每个函数的地址映射(将JIT 地址映射到WASM偏移)以及调试信息(已解析的 WASM 模块的 DWARF 信息)。 -
最后,这个结构被包装在
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
进行实例化。实例化的大致流程如下:
-
在完成实例化之前,每一个
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
。 -
通过
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
数据。 -
此时,
InstanceHandle
存储在Store
中,handle是无需返回的,handle的生命周期需要和Store
一致。
关于实例化另一个值得一提的点是 Store
为所有实例化的 Module
维护了一个ModuleRegistry
。这个ModuleRegistry
存储在 Store
的StoreInner
的StoreOpaque
结构体中,其目的是保留运行这个实例需要的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使用 longjmp
和 setjmp
来实现trap。setjmp
不能在rust中定义(即使是以unsafe的方式定义也不行,https://github.com/rust-lang/rfcs/issues/2625)。因此 setjmp/longjmp
在crates/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
函数。