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

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

作者: 蟹蟹宁 | 来源:发表于2021-08-17 22:29 被阅读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对象,我们看一下其构造函数:

    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的定义,发现其包含了两部分组成:

    • 导入的,即std::vector<Import<Type>> imports;
    • 自定义的,即std::vector<Definition> defs;

    对于导入的部分,其信息主要包括了类型、导入包的名字和导入导入项的名字,对于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字段
    • 这里需要注意,导入包名、导入项名其实应该是irModule的imports字段的信息,在实现山将这部分信息给到了template<typename Type> struct Import结构;
    • 其实根据wasm的定义,irModule的type字段中是不应该包含重复项的,但是显然这个过程是不能保证的,但是这个不重要;
    • 我们将外部接口函数放到了imports字段,但是显然链接过程是需要链接export字段的,这是因为能导出的函数,一定是自定义,而不能是导入的,这是合理的,我总不能把我导入的包再导出吧,因此外部接口是以导入项的形式存在的,显然这个导入项是不能从其他包导入的,所以导入包的名称是空,我们也不需要导入,因为外部接口函数的地址我们是知道的,就保存在functionImportBindings中;
    • 其实链接的过程,就是获取导入函数的地址的过程

    因此接下来我们要为每一个外部接口函数生成一个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. 填充导出段

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

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

    • 1. 将导入函数的地址写到指定的位置,这样在执行是就能找到对应的函数
    • 2. 同时提供一个接口,可以让链接器找到本实例导出函数的地址。

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

    • 1. 分配内存空间、初始化内存的数据
    • 2. 同上1
    • 3. 同上2

    在执行实例化函数之前,我们必须将导入项的内容准备好,对于函数而言,需要的就是导入函数的地址,对于内部实例,函数地址我们是直接写在
    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;
    }
    

    相关文章

      网友评论

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

          本文链接:https://www.haomeiwen.com/subject/cchibltx.html