您的位置:首页 > 其它

JPCSP源码解读16:HLE与模块装载过程

2012-04-16 15:48 344 查看
之前说过,jpcsp中使用了HLE技术,用本地码实现了系统软件的功能。
HLE,代表的单词是high level emulation,高层仿真。也就是说,模拟的了上层的操作系统,而不仅仅是下层的mips架构的机器。下层的机器提供的服务是执行二进制指令,而上层的操作系统封装出了更多功能,这些功能强大而实用。
为了说清楚jpcsp中hle的实现机制,需要从psp中程序的加载与运行说起。
//////////////////////////////////////////////

elf

首先,有一种文件格式叫做elf,executable and linkable file,可执行可链接文件。也就是一种可执行文件,类似于windows下的exe后缀文件。
在elf的文件头中,包含了一些关键信息,比如,这个程序有哪些段(比如代码段,数据段等),各个段应该被加载到内存中的什么位置,这个程序引用了哪些库函数等。
//////////////////////////////////////////////

prx

然后,psp中使用的可执行文件,叫做prx,他是elf的一种变体。将一个可执行文件中包含的程序,称为模块。
在prx的文件头部,包含了两种特别的信息,import和export,导入与导出。导入,就是该程序引用的外部函数,比如某个系统函数。导出,就是该程序提供的服务函数,供其他模块引用。也就是说,一个模块导入的函数,应该是由另一个模块导出的。
//////////////////////////////////////////////

nid和stub(存根)

来看具体的表示。
对于导出,一个数组记录所有导出函数的nid,另一个数组记录相应nid对应函数的地址。
对于导入,一个数组记录所有导入函数的nid,另一个数组以两个字(也就是两条指令)为单位,放置存根。一个nid就对应一个存根。
导入与导出信息都在prx文件的头部。
这里有两个概念,nid和存根。
nid,就是导入的那个函数的一个标识。用那个函数的名字作为输入,以sha-1算法(哈希算法),得到一个32位数。也就是说,nid是函数名的哈希值。基于这个哈希算法,使得不同函数名的哈希值,在很大概率上会不一样。所以,psp中实际上以这个哈希值作为函数的标识来使用。
这样的好处是,函数的标识由不定长的函数名,变成了定长的整数;而且,函数的地址变得可以变动,只要函数名不变,nid就不变,就有可能找到这个函数。当其他模块需要引用这个函数,不要直接引用函数的地址(因为这个地址不确定),而是引用这个函数的nid。
存根,就是两条mips指令,形如:
j addrOfThisNid
nop
对于当前模块,如果其中某处要调用特定nid对应的函数(比如func),只要jal到nid对应的存根处即可,存根处是j指令(j addrOfFunc),经过这个二次跳转,就到达了nid对应函数(func)的入口。func末尾的jr $ra,正好可以返回到之前的jal指令延迟槽之后,而不是返回到存根之后(因为存根处是j指令)。
问题是,要导入某个nid的这个模块,其在编译时刻并不知道该nid对应函数在内存中的地址,所以编译时刻,该模块中的存根是这样:
jr $ra
nop
也就是没有实现需要的功能,就直接返回了。
并且,直到这个模块被加载进内存之前,prx文件中的存根部分都是这样的返回语句。
当模块被加载进内存,加载器首先将其导出函数在系统中做记录,于是系统中有了该模块导出的nid以及相应函数的地址。实际上,系统中记录了所有已加载模块的nid和相应函数的地址。
然后,加载器去处理模块的导入函数。对导入的每个nid,系统查询已经装载的模块导出的nid列表,如果查询到,就用之前记录的该nid对应地址去填充导入位置的存根,修改后的形式如前述:
j addrOfThisNid
nop
///////////////////////////////////////////////////

nid转为系统调用

psp的系统固件被存放在flash0只读存储器中,系统加电启动后,这里的一部分固件模块(当然也是prx格式)首先被加载进内存。
jpcsp作为模拟器,不可能用本地码实现所有nid对应的功能,因为有用户自定义的nid,其对应的函数功能是任意的。
但是系统固件模块导出的nid,其对应的函数是用来提供系统服务,功能确定,所以他们可以用本地码实现,以提高模拟效率。
所以对于一个模块导入的nid,系统首先查找用户模块导出的nid,如果找到,就将存根改为j指令。如果找不到,就查找系统固件导出的nid。
注意,是先查找用户模块导出的nid,这样就使得用户有可能将某个系统固件导出的nid,重定向到自己的实现函数中。
如果找到系统固件导出的nid,说明是在引用系统功能,此时将存根改为:
jr $ra
syscall syscallCodeOfThisNid
注意,延迟槽指令是在跳转生效之前被执行。
这里有个问题,存根处放的是系统调用号,可是我们要的是nid对应的函数。所以,实际上系统调用号和nid有一个对应关系。在jpcsp中,是为每个系统模块导出的nid,随便分配了系统调用号。分配策略是,从0x4000开始,然后每次分配一个系统调用号之后加1。
这个系统调用号的分配器是HLEModuleManger这个class的一个成员变量:
private int syscallCodeAllocator;
在HLEModuleManger.Initialise函数中初始化:
syscallCodeAllocator = 0x4000;
初始值为0x4000,这是因为,psp的固件本身系统调用号是从0x2000开始,这里取了一个相距较远的值,避免冲突。
在HLEModuleManger.getSyscallFromNid函数中为nid分配系统调用号,然后增加1:
code = syscallCodeAllocator;
syscallCodeAllocator++;
反向追踪HLEModuleManger.getSyscallFromNid这个函数,会发现只有系统模块导出的nid会分配系统调用号。
///////////////////////////////////////////////////////

模块装载过程

现在来看jpcsp中,完整的模块装载过程。
入口是Emulator.java:
public SceModule load(String pspfilename,ByteBuffer f, boolean fromSyscall)
调用路径:
load
àmodule= jpcsp.Loader.getInstance().LoadModule(pspfilename, f,MemoryMap.START_USERSPACE + 0x4000, false);
à LoadSPRX(f, module, baseAddress, analyzeOnly)
à LoadPSP(f.slice(), module, baseAddress,analyzeOnly);
à LoadELF(psp.decrypt(f), module, baseAddress,analyzeOnly);
à LoadELFImports(module);
LoadELFExports(module);
ProcessUnresolvedImports();
其中,LoadELFImports将模块的所有导入nid都列入unresolvedImports,是一个列表,表示未处理的导入(后面会有地方处理这些导入):
module.unresolvedImports.add(deferredStub);
并且,存根写入一个syscall指令,系统调用号置为无效值0xfffff:
int instruction = // syscall <code>
((jpcsp.AllegrexOpcodes.SPECIAL & 0x3f)<< 26) |
(jpcsp.AllegrexOpcodes.SYSCALL &0x3f) |
((0xfffff & 0x000fffff) <<6);
mem.write32(importAddress + 4, instruction);
注意这里实际是改写成了这样的形式:
jr $ra
syscall 0xfffff
然后,LoadELFExports,在系统中记录该模块的所有导出,每条记录包括模块的名字,以及导出的nid,还有对应函数的地址:
nidMapper.addModuleNid(moduleName, nid,exportAddress);
最后,ProcessUnresolvedImports,处理所有处于未处理状态的导入:
先查询是否用户导出的nid
exportAddress =nidMapper.moduleNidToAddress(moduleName, nid);
如果不为-1,表示查询成功,改写存根为跳转指令:
if (exportAddress != -1)
{
int instruction = // j<jumpAddress>
((jpcsp.AllegrexOpcodes.J & 0x3f) << 26)
| ((exportAddress >>> 2) & 0x03ffffff);

mem.write32(importAddress,instruction);
mem.write32(importAddress + 4, 0);//nop

}
写的指令是:
j exportAddress
nop
如果查询到的地址是-1,但是nid是0,表示该nid应当被忽略。
最后,如果查询到的地址是-1,nid又不是0,应该是一个系统功能,生成syscall指令:
int code = nidMapper.nidToSyscall(nid);
if (code != -1)
{
int instruction = // syscall<code>
((jpcsp.AllegrexOpcodes.SPECIAL & 0x3f)<< 26)
| (jpcsp.AllegrexOpcodes.SYSCALL & 0x3f)
| ((code & 0x000fffff) << 6);

mem.write32(importAddress + 4, instruction);

}
///////////////////////////////////////////////////////

系统调用指令的翻译

(Instructions.java):
public static final Instruction SYSCALL = newInstruction(15) {
@Override
public void compile(ICompilerContextcontext, int insn) {
context.compileSyscall();
}
}
系统调用指令的翻译,是回调了编译上下文的compileSyscall(CompileContext.java):
public void compileSyscall() {
visitSyscall(codeInstruction.getOpcode());
}
然后从指令中提取系统调用号,根据系统调用号找到对应函数,并生成代码,去调用目标函数。注意,这里是编译时刻。

本章总结

对于系统导出的nid,其nid和系统调用号,以及对应的本地码实现的函数,有一一对应关系。
模块被装载进内存时,导入nid对应的存根被更改为跳转指令(该nid由某个用户模块导出),或者改为系统调用指令(该nid由系统模块导出),并填入对应的系统调用号
当jpcsp的编译引擎编译syscall指令时,根据系统调用号,可以找到对应的(HLE)函数,并生成调用该函数的代码。这样模拟器上的用户程序就可以通过系统调用来使用本地码实现的HLE函数了。
nidà(模块装载时)系统调用号à(二进制翻译时)HLE函数
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: