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

JPCSP源码解读16:HLE与模块装载过程相关推荐

  1. jpcsp源码解读7:HLE

    作为一个模拟器,也就是一个虚拟机,所作的事情就是解释执行运行于该机器上的软件代码,包括系统软件和用户态的应用软件. 基于这个想法,系统软件应该是被加载到内存中的某些位置,当应用软件使用系统调用时,就跳 ...

  2. jpcsp源码解读9:指令的抽象描述与指令的译码

    本文尝试说明jpcsp中译码器单元的实现方式. / 首先是对指令的一个抽象描述,Instruction类: public static abstract class Instruction / jav ...

  3. JPCSP源码解读14:动态二进制翻译2

    JPCSP源码解读14:动态二进制翻译2 IExecutable 上一篇中提到,我们现在有CodeInstruction,代表单条指令,以及其两个子类,分别代表无分支基本块和本地码序列.另外,有cla ...

  4. jpcsp源码解读之一:源码的获取与编译,以及psp详尽硬件信息文档

    是我心血来潮的想法,要解读一下psp模拟器的源码,并添加详尽的中文注释.这个博客则成为文档. 本文面向java语言零基础的程序员,因为我本人的java基础就是零. 水平所限,疏漏错误之处欢迎指正.也欢 ...

  5. jpcsp源码解读5:umd光盘镜像(.iso)

    这次的状况稍显复杂. 首先说一下umd光盘镜像文件的内部组织方式.注意,这些内容全部是从源码解读而来,而不是来自关于这种文件格式的标准文档. java科普之文件操作: fileReader = new ...

  6. jpcsp源码解读6:PSF文件

    当你运行了模拟器,通过模拟器菜单选择并加载一个umd镜像,模拟器就用这个umd镜像实例化一个UmdIsoReader(见上一篇,源码解读5). 通过这个UmdIsoReader,从光盘提取的第一个文件 ...

  7. jpcsp源码解读之二:main函数与jpcsp的初始化流程

    虽然这个软件是用java语言编写,面向对象,可是总要有个开始的入口,这里关心的就是,main函数在哪里. 似乎java中也可以没有main函数,也可能是我的错误认识.暂且不管,jpcsp中是有main ...

  8. jpcsp源码解读10:指令的执行

    这次要说的是处理器类: public class Processor 主要的成员变量: public CpuState cpu = new CpuState(); public static fina ...

  9. jpcsp源码解读12:本地码管理器与Compiler.xml

    jpcsp这个模拟器的优化手段实在让人汗颜. 之前说过,他把系统调用功能全部用本地码实现了,也就是在软件需要的时候,调用java语言的实现,而不是跳转到内存中相应位置去解释执行,或者对系统调用代码做动 ...

最新文章

  1. Linux下编译运行C程序
  2. 使用mac pro电脑当tomcat端口被占用怎么解决?
  3. html jquery 不能自动完成,在jQuery UI自动完成中使用HTML
  4. 物联网避坑 3 大指南!
  5. zynq7020安装linux图形界面,ZYNQ开发板的NFS文件系统安装 - ZYNQ7020学习
  6. 事务的隔离级别 数据库
  7. html页面的盒子边框怎么做的,HTML+CSS入门 用三层盒子结构实现多边框详解
  8. vue2.0中组建里面套用组件_vue19 组建 Vue.extend component、组件模版、动态组件 的实例代码...
  9. JAVA 中的 CAS
  10. Andorid Binder进程间通信---Binder本地对象,实体对象,引用对象,代理对象的引用计数...
  11. DNS、NS、DDNS
  12. 微软同步工具之synctoy
  13. Skype for business 界面自动弹出打开
  14. JAVA8实战 -- Lamdba表达式
  15. 2020年营收仅62.5万,九城凭什么砸1亿美金购买矿机?
  16. 水文预报 求各个年份的纳什效率系数
  17. 块储存、对象存储、文件存储的区别和联系
  18. 自动驾驶仿真工具之AirSim简介
  19. 严冬欲御寒增强体质 首当养肾
  20. centos编译Aegisub

热门文章

  1. 压电陶瓷超声波换能器设计
  2. HMS Core Discovery第17期直播预告|音随我动,秒变音色造型师
  3. 【数据可视化】数据可视化七大发展趋势
  4. [模板] 快速沃尔什变换
  5. 台式计算机噪声,台式电脑主机突然发出很大的噪音怎么办呢
  6. 【Vue】Vue重写教室管理系统的前端网页V1(前后端分离)--20221222
  7. matlab中prbs怎么用,答题 | PRBS码到底是啥玩意?
  8. Matplotlib将image和图表的尺寸对齐
  9. 超详细!手把手带你轻松用 MMSegmentation 跑语义分割数据集
  10. 数据可视化总结2021.05.17