注意,本文不区分 编译和翻译。在本文中,他们表示同一个意思。

首先回顾一下,之前已经说明了,我们有cpu状态,就是cpu中的各个寄存器,还有内存,以及更改这些寄存器的接口函数。另外,我们有译码器(Decoder),有指令分类(Instructions.java中各个Instruction的子类)。译码器通过索引数组的方式,可以确定一条指令属于哪个指令分类,也就是确定指令的功能。

在解释执行的情况下,processor类提供了step方法,指令的执行过程是:

取指:用pc寄存器的值读取内存

译码:用指令中的操作码索引数组,从而确定指令类型

执行:调用指令所属类型的解释方法(Instruction.interpret()),该解释方法调用cpu接口函数进行运算和更改寄存器状态

///

显然,这样的执行过程缓慢而低效。jpcsp中引入了一种高阶的优化技术,动态二进制翻译。

基本思想是,对于将要运行的函数(mips二进制码形式),为其构建一个java类,函数的功能对应到这个java类的一个成员函数。要运行这个函数,调用对应java类的成员函数即可。

将函数中的指令(mips二进制码形式)逐条翻译成java字节码,就可以生成这个成员函数。

///

预备知识之asm

要操作java字节码,要用到java中提供的一个字节码引擎库,叫做asm。这个引擎库提供的功能包括:创建一个类,为这个类添加成员变量,添加成员函数等。

使用实例见CodeBlock.java中的一个函数:

privateClass<IExecutable> compile(CompilerContext context) throwsClassFormatError

ClassWriter,见名知意,这个类可以写一个类出来。构建实例时需要一个flag参数,用于限制行为,具体含义没有细究。

ClassWritercw = new ClassWriter(computeFlag);

ClassVisitorcv = cw;

我们就把Visitor当成Writer,visit一个内容,就是写(生成)这个内容。

cv.visit(Opcodes.V1_6,Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER, className, null, objectInternalName,interfacesForExecutable);

创建了一个类,传递的参数包括要创建的类的名字等信息。

MethodVisitormv = cv.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC,context.getStaticExecMethodName(), context.getStaticExecMethodDesc(), null,exceptions);

为这个类添加一个函数,之后用MethodVisitor来书写这个函数。

mv.visitCode();

开始为这个函数书写代码(java字节码形式)。

mv.visitInsn(Opcodes.IAND);

类似于这样的语句,就是在书写java字节码指令,比如这个例子中是一条整形数据(int)的按位与(and)操作指令。

///

预备知识之java字节码

Java字节码是一种堆栈式的指令集,如果一条指令需要额外的参数,则需要先将这些参数压栈,然后这条指令的执行会消耗掉栈顶的参数,得到的计算结果被存放在栈顶。

Java字节码的指令集中,有常见的load,store指令,加载立即数的指令,跳转指令(goto),比较指令,条件跳转等。指令汇编码的第一个字母表示操作数类型:i开头表示该指令的操作数是整形,l表示该指令的操作数是long,等。

比如iadd,就是整形加法,会消耗栈顶两个整数,得到的结果放置在栈顶。

还有函数调用指令,invoke。Invoke static表示调用静态函数。该指令之前,应该用其他指令将参数预先放置在栈顶。该指令会消耗掉栈顶的参数,调用指令运行的结果,就是函数的返回值,会被放置在栈顶。

///

CodeInstruction

动态二进制翻译,需要一个辅助类,来表示一条指令,这个辅助类是CodeInstruction。也就是先把mips指令转换为这种中间表示,再在这种中间表示的基础上做编译,生成java字节码。

public class CodeInstruction

数据成员:

protected int address;                           //原始指令的地址

private int opcode;                                //原始指令本身

private Instruction insn;                        //原始指令的译码结果

private boolean isBranchTarget;         //是否某个跳转指令的目标位置(是否有某个分支指令会跳转到这里)

private int branchingTo;                //如果是分支指令,跳转到哪个目标位置

private boolean isBranching;           //是否分支指令

private Label label;                    //标号

可以看到,其中记录了一条指令的必要属性。标号的用途在于,如果是分支或跳转的目标位置,也就是有某条分支或跳转指令要跳转到这条指令时,则生成的java字节码就是一个java跳转指令,后面跟这个标号。

提供的接口方法:

一类是读写数据成员,比如:

publicint getAddress()

publicvoid setAddress(int address)

还有一类是从指令本身的编码中提取参数,比如这条指令要操作的寄存器号,指令中包含的16位立即数等:

publicint getRsRegisterIndex() {

return (opcode >> 21) & 0x1F;

}

然后,是最重要的一个,编译方法:

publicvoid compile(CompilerContext context, MethodVisitor mv)

这个方法为这条指令生成java字节码。实际是调用了insn.compile方法。也就是Instructions.java这个文件中定义的各个指令,其中每条指令有自己的compile方法。

放一个and指令的compile方法作为例子:

@Override

public void compile(ICompilerContextcontext, int insn) {

if(!context.isRdRegister0()) {

if(context.isRsRegister0() || context.isRtRegister0()) {

context.storeRd(0);

}else {

context.prepareRdForStore();

context.loadRs();

context.loadRt();

context.getMethodVisitor().visitInsn(Opcodes.IAND);

context.storeRd();

}

}

}

注意其中关键的几行,loadRs,loadRt,,深入进去可以看到是生成了实现相应功能的java字节码,此时rs和rt指定的寄存器中内容在栈顶,然后是一条iand指令:

context.getMethodVisitor().visitInsn(Opcodes.IAND);

也就是书写了一条整形数据的与操作指令(iand)。然后把结果store到rd指定的寄存器中。

总结起来,CodeInstruction这个类,其中包含了一条指令的所有信息,包括private Instruction insn,通过这个Instruction实例,可以得到该指令的compile方法,从而为这条指令生成java字节码。

///

CodeInstruction的子类

CodeInstruction有两个子类,一个代表本地码,一个代表无分支基本块。

在我们这个情境下,子类的意义在于,子类的对象可以伪装成父类的对象使用,但是重载了父类的几个方法。比较关键的,compile方法被重载。

这两个子类,都是对应一串CodeInstruction(也就是一个指令序列),但是要伪装成单个CodeInstruction(也就是单条执行)对象。原先compile方法是针对具体的一条指令,调用insn.compile。这里重载成,调用一个已有的函数,来实现这一串指令对应的功能(比如memory copy)。

先说本地码。前一篇中已经介绍过本地码序列NativeCodeSequence(相当于库函数),以及本地码序列的管理器NativeCodeManager。

这里对NativeCodeSequence作一次封装,包装成CodeInstruction的子类(NativeCodeSequence的对象作为这个子类的数据成员形式出现):

public class NativeCodeInstruction extendsCodeInstruction

其中数据成员只有两个:

private NativeCodeSequencenativeCodeSequence;

privateint flags = 0;

这样,这个本地码序列,就可以伪装成单个CodeInstruction对象,被放置进一个指令序列中。Compile方法被重载,行为改为,去调用本地码对应的类中的方法。比如,jpcsp.Allgrex.compiler.nativeCode这个包中的Memcmp.java:call方法。

然后是基本块。基本块的定义,见编译原理相关书籍。粗略来说,就是一串连续的指令,其入口唯一,就是该串中的第一条指令。分支指令是一个基本块的入口,分支指令的目标位置也是一个基本块的入口。

注意,我们这里对于基本块的利用,只是为了解决函数太长的问题,而不是其他高阶问题。当函数太长时,要识别其中的基本块,然后把一部分无分支指令的基本块提取出来,伪装成单个指令,为这个基本块单独编译功能函数。Compile方法被重载,在原始函数的翻译过程中,遇到这个基本块对应的指令,就去调用该基本块的功能函数。也就是说,用一个函数调用替换掉了原先完整的基本块,这样原始函数的长度可以得到控制,内容被分散到多个函数中去了。

来看基本块的代码实现(这个是基本块本身):

publicclass CodeSequence implementsComparable<CodeSequence>

{

private int startAddress;

private int endAddress;

private LinkedList<CodeInstruction>codeInstructions = new LinkedList<CodeInstruction>();

}

可以看到,其中有一个CodeInstruction的列表,也就是一串连续的指令。

然后,将他封装成CodeInstruction的子类:

publicclass SequenceCodeInstruction extends CodeInstruction

{

privateCodeSequence codeSequence;

}

继承自CodeInstruction,数据成员一个是CodeSequence对象。

注意,不论是本地码,还是基本块,都说到编译结果是去调用他们的功能函数。其中本地码的功能函数是模拟器中已经写好了的,对应类中的call函数,或者Compile.xml中指定的别的函数名。而对于基本块,其功能函数则是编译时刻生成的,函数名也需要生成。生成策略是,用该基本块的入口第一条指令的地址,作为一个特别的标识,这样就可以保证各个基本块对应的函数名互不相同。当然实际命名中还加入了一些其他字符,但是最特别的标识性内容,是这个基本块入口地址。

///

现在,我们有了三种类型的CodeInstruction:

一种是CodeInstruction本身,其对象对应一条具体的指令,其compile方法会为这条指令生成字节码

一种是本地码伪装成的CodeInstruction,其compile方法会生成函数调用的字节码,调用本地函数。

还有一种是基本块伪装成的CodeInstruction,其compile方法会生成函数调用的字节码,调用为该基本块单独编译生成的一个函数。注意其功能函数需要单独编译生成。

///

这样,动态二进制翻译的核心循环就很简单了:逐条取得CodeInstruction,调用其compile方法即可。

暂且收笔,更多实现细节,将在下一篇日志中呈现。

jpcsp源码解读13:动态二进制翻译1相关推荐

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

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

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

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

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

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

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

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

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

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

  6. jpcsp源码解读11:近期笔记

    最近阅读代码主要牵涉到两个问题,一个是动态二进制翻译,一个是进程管理. 两个问题都很棘手,代码量大,复杂度高.今天主要备份一下关键笔记. / 启动运行流程: 用户点击 运行 按钮 RunButtonA ...

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

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

  8. JPCSP源码解读15:动态二进制翻译3(翻译引擎最终章)

    今天,我们从CodeInstruction. compile(CompilerContextcontext, MethodVisitor mv)这个函数说起. 其中,CompilerContext是编 ...

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

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

最新文章

  1. 源码安装httpd2.4.4
  2. hdu 2516 FIB博弈模型
  3. 检查表单元素的值是否为空
  4. 经典C语言程序100例之七二
  5. 特稿 | OceanBase 连破纪录:蚂蚁技术人的砥砺前行
  6. redis入门demo
  7. jdk8 获取上一个月时间_JDK 10:FutureTask获取一个toString()
  8. Java Web开发技术教程入门-JavaBean组件与Servlet
  9. Spring笔记③--spring的命名空间
  10. android 代码 shape,Android Shape控件美化实现代码
  11. 浪潮云海OpenStack X版本技术贡献中国第一
  12. SAP License:实施ERP之后库存反而增加
  13. 一行命令配置深度学所需所有环境PyTorch, TensorFlow, CUDA, cuDNN, and NVIDIA Drivers.
  14. 用VC++封装自己的DLL动态链接库
  15. 数字 三位一节(逗号隔开)表示
  16. 如何构建“正确的”云平台存储
  17. AxureUX中后台管理信息系统通用原型方案
  18. libpng error处理方式
  19. 高通平台开发系列讲解(外设篇)BMI160基本配置
  20. mysql获取某个最大的值的一行数据_某一字段分组取最大(小)值所在行的数据

热门文章

  1. LTE系统内切换分析
  2. 颅内出血多久可以恢复?成都顾连康复治疗好吗
  3. minty_Brit666‘s python practice no.2
  4. Octavia 创建 Listener、Pool、Member、L7policy、L7 rule 与 Health Manager 的实现与分析
  5. 这代码谁写的,太可怕了!
  6. 32/64位Windows 7 下VC9.0编译boost::regex,带ICU
  7. 洛谷 P1774 最接近神的人(权值线段树+离散化)
  8. 内涵段子所有段子Spider
  9. 论计算机的维修策略论文,论计算机的维护维修策略.doc
  10. 多模态技术在淘宝主搜召回场景的探索