WAVM源码解析 —— WASI接口定义、内部实例初始化及实例链

2021-08-17  本文已影响0人  蟹蟹宁

前言

从前面两篇文章中,我们可以窥探,WAVM执行一个wasm程序,主要包括一下步骤:

  1. 加载wasm二进制文件到内存,解析生成 IR::Module
    在这一步主要是解析wasm的各个Segment,保存到我们自定义的数据结构IR::Module中

  2. 编译生成本地可执行的二进制代码
    这一步是实现JIT的关键,是区别于wasm解释器的地方,这样我们就可以通过wasm程序中各个函数的地址,来调用执行,同我们执行普通的C函数一样,这有点类似于加载动态链接库中的函数(见 dlsym(3))

  3. 生成内部实例
    这一步是实现外部函数接口的关键,比如wasi定义的接口,这些接口都将在内部实例中实现

  4. 链接
    链接的主要工作就是根据需要导入的Module和导入项的名字,在对应的实例中查找,以获取对应的内容,如导入的函数,需要获取其入口地址。
    wasi定义的接口,就需要在链接的过程中,从内部实例中获取对应接口函数的地址

  5. 实例化
    生成实例,其实生成内部实例的过程也是实例化,只不过内部实例不基于任何IR::Module,也无需链接任何内容
    所谓实例化,主要内容就是为内存段、表段等申请空间,记录所有函数(自定义的函数和导入的函数)的入口地址,然后将所有的信息记录到一个统一的数据结构中

  6. 执行
    根据提供的入口函数,比如"_start",通过实例化后的实例中的函数信息列表,找到其入口地址,然后调用

本文将重点关注上述3、4、5三个部分

生成内部实例

一、调用接口

Instance* Intrinsics::instantiateModule(
    Compartment* compartment,
    const std::initializer_list<const Intrinsics::Module*>& moduleRefs,
    std::string&& debugName)

Instance* wasi_snapshot_preview1
    = Intrinsics::instantiateModule(compartment,
                                    {WAVM_INTRINSIC_MODULE_REF(wasi),
                                     WAVM_INTRINSIC_MODULE_REF(wasiArgsEnvs),
                                     WAVM_INTRINSIC_MODULE_REF(wasiClocks),
                                     WAVM_INTRINSIC_MODULE_REF(wasiFile)},
                                    "wasi_snapshot_preview1");

函数,最重要的参数是moduleRefs,这是一个Intrinsics::Module类型的列表,因为内部实例不是基于wasm程序的,其只需要考虑导入导出段所关注的内容,因此定义了Intrinsics::Module类,其类似与IR::Module,但是仅仅包含Function、Global、Table、Memory等内容,其结构如下:

struct ModuleImpl
{
    HashMap<std::string, Intrinsics::Function*> functionMap;
    HashMap<std::string, Intrinsics::Table*> tableMap;
    HashMap<std::string, Intrinsics::Memory*> memoryMap;
    HashMap<std::string, Intrinsics::Global*> globalMap;
};
struct Module
{
    ModuleImpl* impl = nullptr;
    WAVM_API ~Module();
};

WAVM_INTRINSIC_MODULE_REF(wasi) 是一个宏,将其替换可得getIntrinsicModule_wasi(),查看其定义:

WAVM::Intrinsics::Module* getIntrinsicModule_wasi()
{
    static WAVM::Intrinsics::Module module;
    return &module;
}

也就是说,每一个WAVM_INTRINSIC_MODULE_REF()都会返回一个Intrinsics::Module对象,在WAVM中,定义了4个Intrinsics::Module对象,他们是按实现的功能类别分类的,但是实际上实现的都是wasi标准定义的接口。

那么如何初始化Intrinsics::Module对象呢?

二、Intrinsics::Module的构建

构建Intrinsics::Module,需要依赖于一个宏函数WAVM_DEFINE_INTRINSIC_FUNCTION,定义如下:

#define WAVM_DEFINE_INTRINSIC_FUNCTION(module, nameString, Result, cName, ...)                     \
    static Result cName(WAVM::Runtime::ContextRuntimeData* contextRuntimeData, ##__VA_ARGS__);     \
    static WAVM::Intrinsics::Function cName##Intrinsic(                                            \
        getIntrinsicModule_##module(),                                                             \
        nameString,                                                                                \
        (void*)&cName,                                                                             \
        WAVM::Intrinsics::inferIntrinsicFunctionType(&cName));                                     \
    static Result cName(WAVM::Runtime::ContextRuntimeData* contextRuntimeData, ##__VA_ARGS__)

WAVM_DEFINE_INTRINSIC_FUNCTION宏,定义了一个接口,同时将接口赋值给具体的Intrinsics::Module对象,我们以一个简单的sched_yield为例:

宏定义
WAVM_DEFINE_INTRINSIC_FUNCTION(wasi, 
                               "sched_yield",
                               __wasi_errno_return_t,
                               wasi_sched_yield)
{
    TRACE_SYSCALL("sched_yield", "()");
    Platform::yieldToAnotherThread();
    return TRACE_SYSCALL_RETURN(__WASI_ESUCCESS);
}
转化后
static __wasi_errno_return_t wasi_sched_yield(
    WAVM::Runtime::ContextRuntimeData* contextRuntimeData);
static WAVM::Intrinsics::Function wasi_sched_yieldIntrinsic(
    getIntrinsicModule_wasi(),
    "sched_yield",
    (void*)&wasi_sched_yield,
    WAVM::Intrinsics::inferIntrinsicFunctionType(&wasi_sched_yield));
static __wasi_errno_return_t wasi_sched_yield(WAVM::Runtime::ContextRuntimeData* contextRuntimeData)
{
    TRACE_SYSCALL("sched_yield", "()");
    Platform::yieldToAnotherThread();
    return TRACE_SYSCALL_RETURN(__WASI_ESUCCESS);
}

在这里,定一个三方面内容,分别是:

而这里最关键的就是第二部分,构建静态的Intrinsics::Function对象,我们看一下其构造函数:

Intrinsics::Function::Function(Intrinsics::Module* moduleRef,
                               const char* inName,
                               void* inNativeFunction,
                               FunctionType inType)
: name(inName), type(inType), nativeFunction(inNativeFunction)
{
    initializeModule(moduleRef);

    if(moduleRef->impl->functionMap.contains(name))
    { Errors::fatalf("Intrinsic function already registered: %s", name); }
    moduleRef->impl->functionMap.set(name, this);
}

可以看到,构造函数的一个重要作用就是,将自己赋值给Intrinsics::Module对象,换言之,每当使用WAVM_DEFINE_INTRINSIC_FUNCTION()宏定义一个接口函数,静态的Intrinsics::Function对象会自动调用构造函数,从而将自己赋值到Intrinsics::Module中。

三、Intrinsics::instantiateModule()执行

Instance* Intrinsics::instantiateModule(
    Compartment* compartment,
    const std::initializer_list<const Intrinsics::Module*>& moduleRefs,
    std::string&& debugName)
{。。。}

此函数,主要的步骤是:

1. 将moduleRefs转化为IR::Module
2. 编译上一步生成的IR::Module
3. 调用实例化接口函数,生成内部实例

整个过程是非常清晰的。

3.1 将moduleRefs转化为IR::Module

moduleRefs引用的是一个Intrinsics::Module列表,而Intrinsics::Module仅仅关注Function、Table、Memory、Globa四项内容,我们仅在这里分析最重要的Function部分。
首先我们看看Intrinsics::Function的数据结构:

struct Function
{
    WAVM_API Function(Intrinsics::Module* moduleRef,
                      const char* inName,
                      void* inNativeFunction,
                      IR::FunctionType type);

private:
    const char* name;
    IR::FunctionType type;
    void* nativeFunction;
};

主要包括,函数名、函数的签名、以及其函数指针,始终注意,所谓接口函数就是本地的CPP的函数,就是我们使用WAVM_DEFINE_INTRINSIC_FUNCTION()宏定义的,与之相互对的是wasm的内部函数,由WASM的类型段、函数段、代码段等组成,我们看一下WAVM给wasm的函数结构定义:

struct Module
    {
        ...
        IndexSpace<FunctionDef, IndexedFunctionType> functions;
        ...
    }
template<typename Definition, typename Type> struct IndexSpace
    {
        std::vector<Import<Type>> imports;
        std::vector<Definition> defs;
        ...
    }
template<typename Type> struct Import
    {
        Type type;
        std::string moduleName;
        std::string exportName;
    };
struct FunctionDef
    {
        // 函数类型的索引,继承自原WASM的函数段
        IndexedFunctionType type;
        // 函数局部变量的信息,也就是每个局部变量的值类型,继承自原WASM的代码段
        std::vector<ValueType> nonParameterLocalTypes;
        // 函数的字节码,继承自原WASM的代码段
        std::vector<U8> code;
        std::vector<std::vector<Uptr>> branchTables;
    };

在IR::Module中,定义了一个functions字段,来存储函数的信息,其类型为IndexSpace,我们查看IndexSpace的定义,发现其包含了两部分组成:

对于导入的部分,其信息主要包括了类型、导入包的名字和导入导入项的名字,对于Functiong而言,类型是IndexedFunctionType,这其实是wasm类型段的索引。

对于自定义的部分,其定义就是FunctionDef内容包括了函数类型、局部变量表以及具体的代码段的内容。

OK,来看一下具体的转化代码,依然只关注函数的内容:

for(const Intrinsics::Module* moduleRef : moduleRefs)
{
    if(moduleRef->impl)
    {
        for(const auto& pair : moduleRef->impl->functionMap)
        {
            functionImportBindings.emplace_back(pair.value->getNativeFunction());
            const Uptr typeIndex = irModule.types.size();
            const Uptr functionIndex = irModule.functions.size();
            irModule.types.push_back(pair.value->getType());
            irModule.functions.imports.push_back({{typeIndex}, "", pair.value->getName()});
            irModule.imports.push_back({ExternKind::function, functionIndex});
        }

过程是:

  1. 将接口函数的地址,写入到functionImportBindings中
  2. 将接口函数的类型,"依次"添加到irModule的type字段中,
  3. 将从第二步中获取的 {类型索引 、空包名“ ” 、函数名},写入irModule的functions字段的import
  4. 将函数索引,写入irModule的imports字段

因此接下来我们要为每一个外部接口函数生成一个wasm格式的thunks函数,然后将thunks导出。

3.2 创建thunks函数

主要过程如下;

for(Uptr functionImportIndex = 0; functionImportIndex < irModule.functions.imports.size();
    ++functionImportIndex)
{
    const FunctionImport& functionImport = irModule.functions.imports[functionImportIndex];
    const FunctionType intrinsicFunctionType = irModule.types[functionImport.type.index];
    const FunctionType wasmFunctionType(
        intrinsicFunctionType.results(), intrinsicFunctionType.params(), CallingConvention::wasm);

    const Uptr wasmFunctionTypeIndex = irModule.types.size();
    irModule.types.push_back(wasmFunctionType);

    // 下面操作是将加载参数和调用函数,封装到codeStream,可以认为是WASM的调用函数命令
    // WASM的函数调用,在调用函数的时候会先把参数放到操作数栈
    // call指令会先将参数加载到自己的局部变量表中,然后调用local_get将其放到操作数栈进行操作
    // 在这里,我们先执行了local_get,然后再进行call,然后将其放入codeStream中
    Serialization::ArrayOutputStream codeStream;
    OperatorEncoderStream opEncoder(codeStream);
    for(Uptr paramIndex = 0; paramIndex < intrinsicFunctionType.params().size(); ++paramIndex)
    {
        opEncoder.local_get({paramIndex});
    }
    opEncoder.call({functionImportIndex});
    opEncoder.end();
    // 将自定义的函数再写入functions.def和export中,其实函数的字节码为codeStream
    // 从这里可以看到,我们在WASM中用内部函数封装了一层外部的本地函数,内部函数所用就是执行call指令
    // 而前面的local_get就是为了将参数放到操作数栈
    const Uptr wasmFunctionIndex = irModule.functions.size();
    irModule.functions.defs.push_back({{wasmFunctionTypeIndex}, {}, codeStream.getBytes(), {}});
    irModule.exports.push_back(
        {functionImport.exportName, ExternKind::function, wasmFunctionIndex});
}
  1. 获取thunks函数的函数类型,填入到irModule.types
  2. 创建thunks函数,主要操作见注释
  3. 将thunks函数写入irModule.functions.defs,写入的内容包括了thunks函数的字节码,其与wasm的代码段的字节码是一样的
  4. 填充导出段

最后将执行实例化操作,创建出内部实例,内部实例和普通实例其实是相同的,区别在于导入段所需要内容的获取方式不同。

以函数为例,实例化的过程就是:

以Memory为例,实例化的过程就是:

在执行实例化函数之前,我们必须将导入项的内容准备好,对于函数而言,需要的就是导入函数的地址,对于内部实例,函数地址我们是直接写在
functionImportBindings中的,而对于普通实例,其导入函数的地址,需要通过链接器进行链接操作获取!那么链接器是如何工作的呢?

链接

说实话链接应该是最简单的了:

  1. 循环遍历自己的导入项
  2. 通过moduleNameToInstanceMap.get(moduleName);获取对应的项
  3. 如果没有找到,则记录未找到的导入包名、导入项以及导入类型等信息
  4. 如果不存在未找到的导入项,那么宣告链接成功

前面我们说过,实例化的一大作用就是为链接提供查询导出项的接口!

核心实现,实在很简单,就懒得解析了。

关于实例化的内容,见下一篇!

struct LinkResult
{
    struct MissingImport
    {
        std::string moduleName;
        std::string exportName;
        IR::ExternType type;
    };
    // 真正起作用的是resolvedImports,如果success的话,missingImports应该是空的,他描述的是没找到的导入项
    std::vector<MissingImport> missingImports;
    ImportBindings resolvedImports;
    bool success{false};
};

LinkResult Runtime::linkModule(const IR::Module& module, Resolver& resolver)
{
    LinkResult linkResult;
    for(const auto& kindIndex : module.imports)
    {
        switch(kindIndex.kind)
        {
        case ExternKind::function: {
            const auto& functionImport = module.functions.imports[kindIndex.index];
            linkImport(module,
                       functionImport,
                       module.types[functionImport.type.index],
                       resolver,
                       linkResult);
            break;
        case ExternKind::table:
        case ExternKind::memory:
        case ExternKind::global:
        default: WAVM_UNREACHABLE();
        }
        };
    }

    linkResult.success = linkResult.missingImports.size() == 0;
    return linkResult;
}

static void linkImport(const IR::Module& module,
                       const Import<Type>& import,
                       ResolvedType resolvedType,
                       Resolver& resolver,
                       LinkResult& linkResult)
{
    Object* importValue;
    if(resolver.resolve(import.moduleName, import.exportName, resolvedType, importValue))
    {
        linkResult.resolvedImports.push_back(importValue);
    }
    else
    {
        linkResult.missingImports.push_back({import.moduleName, import.exportName, resolvedType});
        linkResult.resolvedImports.push_back(nullptr);
    }
}

bool ProcessResolver::resolve(const std::string& moduleName,
                              const std::string& exportName,
                              ExternType type,
                              Object*& outObject)
{
    const auto& namedInstance = moduleNameToInstanceMap.get(moduleName);
    return namedInstance != nullptr;
}
上一篇下一篇

猜你喜欢

热点阅读