WAVM源码解析 —— 实例化

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

前言

在本系列的第三篇文章中,有一段关于实例化的论述:

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

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

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

  1. 分配内存空间、初始化内存的数据
  2. 将创建的内存地址写入到指定位置
  3. 提供一个接口,可以让链接器找到本实例导出的内存

这段话基本上就概括了实例化的工作,因为这部分的代码涉及了llvm的很多东西,因此我们将着重理解上述过程是怎么实现的。

函数声明

Instance* Runtime::instantiateModuleInternal(Compartment* compartment,
                                             ModuleConstRefParam module,
                                             std::vector<FunctionImportBinding>&& functionImports,
                                             std::vector<Table*>&& tables,
                                             std::vector<Memory*>&& memories,
                                             std::vector<Global*>&& globals,
                                             std::vector<ExceptionType*>&& exceptionTypes,
                                             std::string&& moduleDebugName,
                                             ResourceQuotaRefParam resourceQuota)

中间5项,就囊括了Function、Table、Memory、Global等需要实例化的对象,其中对于内部实例,仅仅需要提供Function,即FunctionImportBinding的信息即可!

我们看一下FunctionImportBinding的定义:

struct FunctionImportBinding
{
    union
    {
        Function* wasmFunction;
        const void* nativeFunction;
    };
    FunctionImportBinding(Function* inWASMFunction) : wasmFunction(inWASMFunction) {}
    FunctionImportBinding(void* inNativeFunction) : nativeFunction(inNativeFunction) {}
};

只是一个联合体,对于内部函数,其函数地址是直接给出的,而对于普通实例,其函数地址目前是Runtime::Function*类型,这里需要知道,最终程序是一JIT模式运行的,也就是函数调用是遵循CPP的规则的,因此我们需要通过Runtime::Function*来获取其地址,这个地址方式应该是非常的类似于动态链接库的中函数获取。

我们看一下Runtime::Function的结构:

struct Function
{
    Object object;
    FunctionMutableData* mutableData;
    const Uptr instanceId;
    const IR::FunctionType::Encoding encodedType;
    const U8 code[1];
};

其中code字段是一个字节数组,内容正式函数的二进制代码,因此code就是函数的入口地址,但是奇怪的是这个数组仅仅有1个字节,这是为啥呢?

显然我们要先确定Function对象是由谁来创建的?

答案是实例化的过程中创建的,Function对象是用于导出的,因此显然应该是由实例化的函数创建。

那么为啥code[1]仅仅有一个字节呢?其实他利用了和linux内核中好象是PCB和栈类似的方法,也就是在将wasm编译成二进制代码的过程中,预留Function的空间结构,形成类似于如下图的存储方式:

执行过程

  1. 创建Table、Memory等结构,并记录其地址
  2. 记录要导入的表、内存、函数等的地址信息记录
  3. 为每一个Funtion对象创建FunctionMutableData,为其申请空间并记录其地址
    FunctionMutableData中存放了其所在Function对象的地址,以便我们获取
  4. 利用前三步骤中获取的各类地址,来填充编译好的二进制代码,这个过程简直等同于运行一个可执行文件时,将可执行文件中各个符号(symbol)的地址替换为确定的物理内存地址
  5. 生成函数名和函数地址的映射,已供调用接口使用
  6. 生成导出项影射,以供链接过程使用,
  7. 填充数据段、元素段的内容到我们申请的内存、表等空间中

上述过程最难理解的就是第4部分,其实现是调用了:

std::shared_ptr<LLVMJIT::Module> jitModule
    = LLVMJIT::loadModule(module->objectCode,
                          std::move(wavmIntrinsicsExportMap),
                          std::move(jitTypes),
                          std::move(jitFunctionImports),
                          std::move(jitTables),
                          std::move(jitMemories),
                          std::move(jitGlobals),
                          std::move(jitExceptionTypes),
                          {id},
                          reinterpret_cast<Uptr>(getOutOfBoundsElement()),
                          functionDefMutableDatas,
                          std::string(moduleDebugName));

其中jitTypesjitFunctionImportsjitTablesjitMemories等都是要填充的地址,以及functionDefMutableDatas,也是需要我们填充到我上面画得图的位置。

LLVMJIT::loadModule的主要工作就是将这些地址进行整理,融合到importedSymbolMap中,然后调用一下函数,创建一个LLVMJIT::Module对象

std::make_shared<Module>(objectFileBytes, importedSymbolMap, true, std::move(debugName));

LLVMJIT::Module中存放了函数名到函数对象的Map映射:

struct Module
{
    HashMap<std::string, Runtime::Function*> nameToFunctionMap;

最终在LLVMJIT::Module的构造函数中,将实现4、5步骤,我对这部分得代码进行了极大的简化,符号地址填充的部分我是根据代码的注释猜测的,应该错不了,而生成Function的代码中,使用了:

Runtime::Function* function
            = (Runtime::Function*)(loadedAddress - offsetof(Runtime::Function, code));

这也证实了我前面的图

Module::Module(const std::vector<U8>& objectBytes,
               const HashMap<std::string, Uptr>& importedSymbolMap,
               bool shouldLogMetrics,
               std::string&& inDebugName)
    : debugName(std::move(inDebugName))
    , memoryManager(new ModuleMemoryManager())
    , globalModuleState(GlobalModuleState::get())
{
    // 完成对符号(导入的函数,创建的内存、表,function->mutableData等等)的地址填充
    std::unique_ptr<llvm::object::ObjectFile> object;
    object = cantFail(llvm::object::ObjectFile::createObjectFile(llvm::MemoryBufferRef(
        llvm::StringRef((const char*)objectBytes.data(), objectBytes.size()), "memory")));
    struct SymbolResolver : llvm::JITSymbolResolver
    {
       ...
    };
    SymbolResolver symbolResolver(importedSymbolMap);
    llvm::RuntimeDyld loader(*memoryManager, symbolResolver);
    loader.setProcessAllSections(true);
    std::unique_ptr<llvm::RuntimeDyld::LoadedObjectInfo> loadedObject = loader.loadObject(*object);
    loader.finalizeWithMemoryManagerLocking();

    
    // 此循环用于解析loaded的object中的函数,算出其地址,并构建Runtime::Function结构
    for(std::pair<llvm::object::SymbolRef, U64> symbolSizePair :
        llvm::object::computeSymbolSizes(*object))
    {
        // 获取函数的地址
        llvm::object::SymbolRef symbol = symbolSizePair.first;
        llvm::Expected<U64> address = symbol.getAddress();
        Uptr loadedAddress = Uptr(*address);

        // 创建Function对象,配置nameToFunctionMap
        Runtime::Function* function
            = (Runtime::Function*)(loadedAddress - offsetof(Runtime::Function, code));// offsetof(Runtime::Function, code)==32
        nameToFunctionMap.addOrFail(std::string(*name), function);
        addressToFunctionMap.emplace(Uptr(loadedAddress + symbolSizePair.second), function);

        // 初始化mutableData
        function->mutableData->jitModule = this;
        function->mutableData->function = function;
        function->mutableData->numCodeBytes = Uptr(symbolSizePair.second);
        function->mutableData->offsetToOpIndexMap = std::move(offsetToOpIndexMap);
    }
}
上一篇下一篇

猜你喜欢

热点阅读