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

之前说过,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函数

 



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值