WAVM源码解析 —— 实例化
前言
在本系列的第三篇文章中,有一段关于实例化的论述:
以Function为例,实例化的过程就是:
- 将导入函数的地址写到指定的位置,这样在执行是就能找到对应的函数
- 同时提供一个接口,可以让链接器找到本实例导出函数的地址。
以Memory为例,实例化的过程就是:
- 分配内存空间、初始化内存的数据
- 将创建的内存地址写入到指定位置
- 提供一个接口,可以让链接器找到本实例导出的内存
这段话基本上就概括了实例化的工作,因为这部分的代码涉及了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的空间结构,形成类似于如下图的存储方式:
执行过程
- 创建Table、Memory等结构,并记录其地址
- 记录要导入的表、内存、函数等的地址信息记录
- 为每一个Funtion对象创建
FunctionMutableData
,为其申请空间并记录其地址
FunctionMutableData
中存放了其所在Function对象的地址,以便我们获取 - 利用前三步骤中获取的各类地址,来填充编译好的二进制代码,这个过程简直等同于运行一个可执行文件时,将可执行文件中各个符号(symbol)的地址替换为确定的物理内存地址
- 生成函数名和函数地址的映射,已供调用接口使用
- 生成导出项影射,以供链接过程使用,
- 填充数据段、元素段的内容到我们申请的内存、表等空间中
上述过程最难理解的就是第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));
其中jitTypes
、jitFunctionImports
、jitTables
、jitMemories
等都是要填充的地址,以及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);
}
}