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

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

IExecutable

    上一篇中提到,我们现在有CodeInstruction,代表单条指令,以及其两个子类,分别代表无分支基本块和本地码序列。另外,有class writer,class visitor,用于书写java字节码,生成java类。

在jpcsp中,定义了一个接口,IExecutable,也就是内部可执行类。

对于每一个mips函数,运用class writer和class visitor,为其生成一个IExecutable的子类,并生成(翻译出)exec方法。这样,调用该子类实例的exec方法,就可以运行该mips函数的翻译结果了。

IExecutable.java文件中的内容非常简单:

public interface IExecutable {

      publicint exec(int returnAddress, int alternativeReturnAddress, boolean isJump)throws Exception;

      publicvoid setExecutable(IExecutable e);

}

CodeBlock

对于每个mips函数,前面提到,是用IExecutable的子类来表示。在jpcsp中,还做了一次封装,将这个内部可执行类,作为CodeBlock类的数据成员出现。所以,实际上是用CodeBlock类来描述一个可执行函数。

来看这个类的数据成员:

该函数的地址范围

      privateint startAddress;

      privateint lowestAddress;

      privateint highestAddress;

 

CodeInstruction的列表,记录了该函数中的所有指令

      privateLinkedList<CodeInstruction> codeInstructions = newLinkedList<CodeInstruction>();//所有指令

 

该函数中的无分支基本块:

      privateLinkedList<SequenceCodeInstruction> sequenceCodeInstructions = newLinkedList<SequenceCodeInstruction>();//基本块

 

当前基本块,编译时刻使用:

      privateSequenceCodeInstruction currentSequence = null;

 

内部可执行类(编译的最终成果):

      privateIExecutable executable = null;

 

几个字符串,用于给内部可执行类的子类生成 类的名字:

      privatefinal static String objectInternalName = Type.getInternalName(Object.class);

      privatefinal static String[] interfacesForExecutable = new String[] {Type.getInternalName(IExecutable.class) };

      privatefinal static String[] exceptions = new String[] {Type.getInternalName(Exception.class) };

 

实例号,暂且无视:

      privateint instanceIndex;

 

CompilerClassLoader

    前述的ClassWriter和ClassVisitor是书写一个java类,然后还要借助一个加载器,加载这个类之后才可以在java虚拟机上使用这个类。

public class CompilerClassLoader extendsClassLoader

他是ClassLoader的子类,ClassLoader是出自java类库,我们这里主要是使用他的一个加载(定义)类的方法:

protected final Class<?>defineClass(String name, byte[] b, int off, int len)

 

CompilerClassLoader对这个函数进行了封装:

      publicClass<?> defineClass(String name, byte[] b) {

       return defineClass(name, b, 0, b.length);

      }

需要的参数是,类的名字,以及一个字节数组(buffer),就是要把ClassWriter的书写结果转换为字节数组,才可以引用这个方法:

compiledClass = loadExecutable(context,className, cw.toByteArray());

其中cw是ClassWriter的实例。

loadExecutable的核心语句:

return (Class<IExecutable>)context.getClassLoader().defineClass(className, bytes);

另外,这个加载器的子类额外添加了一个编译器作为数据成员。重载了findClass方法,在新的实现中,先调用原先的findClass方法,没找到的话,就调用编译器进行编译,然后返回编译的成果。

CompilerContext

顾名思义,是编译时刻的上下文。比如,当前正在编译的是哪条指令,当前正在编译的是哪个mips函数(CodeBlock),类似于这样的,编译时刻要用到的信息。

来看具体的数据成员,非常多,稍后解析编译过程时,将会看到这些数据成员的作用:

      内部可执行类的加载器

      privateCompilerClassLoader classLoader;

 

      当前正在编译的代码块(对应一个mips函数)

      privateCodeBlock codeBlock;

 

      扫描时刻用到,要跳过扫描的指令个数

      privateint numberInstructionsToBeSkipped;

 

      要跳过扫描的指令是否是一个延迟槽指令

      privateboolean skipDelaySlot;

 

      用于书写内部可执行类的exec方法

      privateMethodVisitor mv;

 

      当前正在编译的指令

      privateCodeInstruction codeInstruction;

 

      是否把通用寄存器存放在本地,加速目的

      privatestatic final boolean storeGprLocal = true;

 

      是否创建内存的本地副本,加速目的

      privatestatic final boolean storeMemoryIntLocal = false;

 

      一些常量:

      privatestatic final int LOCAL_RETURN_ADDRESS = 0;

      privatestatic final int LOCAL_ALTERVATIVE_RETURN_ADDRESS = 1;

   private static final int LOCAL_IS_JUMP = 2;

   private static final int LOCAL_GPR = 3;

   private static final int LOCAL_INSTRUCTION_COUNT = 4;

   private static final int LOCAL_MEMORY_INT = 5;

   private static final int LOCAL_TMP1 = 6;

   private static final int LOCAL_TMP2 = 7;

   private static final int LOCAL_TMP3 = 8;

   private static final int LOCAL_TMP4 = 9;

   private static final int LOCAL_TMP_VD0 = 10;

   private static final int LOCAL_TMP_VD1 = 11;

   private static final int LOCAL_TMP_VD2 = 12;

   private static final int LOCAL_MAX = 13;

   private static final int DEFAULT_MAX_STACK_SIZE = 11;

   private static final int SYSCALL_MAX_STACK_SIZE = 100;

   private static final int LOCAL_ERROR_POINTER = LOCAL_TMP3;

 

      是否使能指令计数。生成java字节码时,如果使能指令计数,要生成相应的计数代码

       private boolean enableIntructionCounting =false;

 

      第一遍扫描时使用,记录已经扫描过的指令

   public Set<Integer> analysedAddresses = newHashSet<Integer>();

      第一遍扫描时使用,记录等待扫描的指令

   public Stack<Integer> blocksToBeAnalysed = newStack<Integer>();

 

       

   private int currentInstructionCount;

 

      一些状态标记(控制相应代码的生成):

   private int preparedRegisterForStore = -1;

   private boolean memWritePrepared = false;

   private boolean hiloPrepared = false;

 

      一个函数中最大指令数(超过的话,要提取基本块,并将基本块视作单条指令)

   private int methodMaxInstructions;

 

      本地码管理器

   private NativeCodeManager nativeCodeManager;

 

      向量单元相关,尚未解析

   private final VfpuPfxSrcState vfpuPfxsState = new VfpuPfxSrcState();

   private final VfpuPfxSrcState vfpuPfxtState = new VfpuPfxSrcState();

   private final VfpuPfxDstState vfpuPfxdState = new VfpuPfxDstState();

   private Label interpretPfxLabel = null;

   private boolean pfxVdOverlap = false;

 

      一些字符串常量,生成内部可执行类时要用

   private static final String runtimeContextInternalName =Type.getInternalName(RuntimeContext.class);

   private static final String processorDescriptor =Type.getDescriptor(Processor.class);

   private static final String cpuDescriptor =Type.getDescriptor(CpuState.class);

   private static final String cpuInternalName =Type.getInternalName(CpuState.class);

   private static final String instructionsInternalName =Type.getInternalName(Instructions.class);

   private static final String instructionInternalName =Type.getInternalName(Instruction.class);

   private static final String instructionDescriptor =Type.getDescriptor(Instruction.class);

   private static final String sceKernalThreadInfoInternalName =Type.getInternalName(SceKernelThreadInfo.class);

   private static final String sceKernalThreadInfoDescriptor =Type.getDescriptor(SceKernelThreadInfo.class);

   private static final String stringDescriptor =Type.getDescriptor(String.class);

   private static final String memoryDescriptor =Type.getDescriptor(Memory.class);

   private static final String memoryInternalName =Type.getInternalName(Memory.class);

   private static final String profilerInternalName =Type.getInternalName(Profiler.class);

   private static final String vfpuValueDescriptor =Type.getDescriptor(VfpuValue.class);

   private static final String vfpuValueInternalName =Type.getInternalName(VfpuValue.class);

      public  static final String executableDescriptor =Type.getDescriptor(IExecutable.class);

      public  static final String executableInternalName =Type.getInternalName(IExecutable.class);

 

      快速系统调用

      privatestatic Set<Integer> fastSyscalls;

 

      实例号(每次复位之后实例号自增1):

      privateint instanceIndex;

 

      privateboolean preparedCall = false;

      privateNativeCodeSequence preparedCallNativeCodeBlock = null;

      privateint maxStackSize = DEFAULT_MAX_STACK_SIZE;

 

Compiler

    终于到了这个关键类,编译器。

   public class Compiler implements ICompiler

来看数据成员:

   一个日志,无视

   public static Logger log =Logger.getLogger("compiler");

 

   编译器实例,注意是static,也就是编译器只有一个实例

   private static Compiler instance;

 

   复位的次数(用作实例号)

   private static int resetCount = 0;

 

   内部可执行类加载器

   private CompilerClassLoader classLoader;

 

   编译耗费的时间

   public static CpuDurationStatisticscompileDuration = new CpuDurationStatistics("Compilation Time");

 

   配置信息,实际上是记录本地码的一个文件Compiler.xml

   private Document configuration;

   本地码管理器

   private NativeCodeManager nativeCodeManager;

 

   忽略非法的内存地址(加速目的)

   private boolean ignoreInvalidMemory = false;

 

   一个mips函数中最大的指令数(超出之后要识别基本块,并将基本块封装成单独的可执行函数,视作单条指令)

   public int defaultMethodMaxInstructions =3000;

 

提供了一个关键方法,来实现编译功能:

public IExecutablecompile(int address)

编译过程的实现

现在,我们有了CodeInstruction来代表mips指令,指令的序列构成了一个mips函数。我们有IExecutable,内部可执行类。用CodeBlock代表一个mips函数,其中包含了mips函数的指令序列,以及对应的IExecutable类。

编译的目标,就是从mips指令序列,生成IEexcutable的子类,及其exec方法。这样,要在这个psp模拟器上执行mips函数,只要去调用相应的IExecutable子类的exec方法。

他们的关系如图:

 

下面,尝试从Compiler.compile函数开始,来说明编译的实现过程与细节。注意,加黑的函数是主调用路径。

先描述一下主要流程:

   第一遍扫描,将mips指令序列转换为CodeInstruction,存放在codeBlock的codeInstructions链表中

   然后,识别并替换本地码序列

   如果指令数目过多,识别并替换无分支基本块

   为这个函数生成内部可执行类,逐条翻译codeBlock的codeInstructions,从而生成内部可执行类的exec方法。

   为所有无分支基本块生成内部可执行类,逐条翻译其codeInstructions,从而生成这个内部可执行类的exec方法。

   注意,主体函数的翻译过程中,无分支基本块的实现是调用了其对应内部可执行类的exec方法,可是无分支基本块对应内部可执行类是之后才生成的。

 

单个参数的comlile函数有两个,一个传入参数是类名,还有一个是地址。内部可执行类的类名生成逻辑中,确保类名包含地址,所以接收到类名时,是解析出地址,然后去调用以地址为参数的compile函数:

public IExecutablecompile(String name) {

        returncompile(CompilerContext.getClassAddress(name),CompilerContext.getClassInstanceIndex(name));

}

来看这个以地址为参数的compile

publicIExecutable compile(int address) {

       returncompile(address, getResetCount());

}

外加了一个实例号,这个实例号同样作为CompileContext的实例号,用途暂时无视。

追踪进去:

publicIExecutable compile(int address, int instanceIndex)

这个函数先检查了一下地址是否合法,不合法就报错:

   if (!Memory.isAddressGood(address)){}

然后,把模拟器的时钟暂停:

   Emulator.getClock().pause();

开始统计编译花费的时间:

   compileDuration.start();

实例了一个编译时刻上下文(传入参数是一个内部可执行类的加载器,以及一个实例号):

   CompilerContext context = newCompilerContext(classLoader, instanceIndex);

正式开始编译(尝试三次):

executable =analyse(context, address, false, instanceIndex);

compile函数至此就结束了。其他核心过程都在compile最后调用的analyse函数中。注意,编译时刻上下文的实例作为参数传入。

现在,来看analyse函数:

privateIExecutable analyse

(

   CompilerContext context,

   int startAddress,

   boolean recursive,

   intinstanceIndex

)throws ClassFormatError

首先,取得内存的实例。因为要取指令,指令在内存中:

   MemorySections memorySections =MemorySections.getInstance();

对于要编译的函数,其入口地址用内存的掩码掩掉高位,以便作为内存(数组)的索引:

   startAddress = startAddress &Memory.addressMask;

实例化一个CodeBlock

   CodeBlock codeBlock = newCodeBlock(startAddress, instanceIndex);

引入了一个栈(用于第一遍扫描,将mips指令逐条转换为CodeInstruction):

   Stack<Integer> pendingBlockAddresses =new Stack<Integer>();

第一遍扫描

扫描的成果,是将mips指令逐条转换为CodeInstruction,存放在codeBlock的condeInstructions链表中。

扫描算法

栈中存放待扫描的各个基本块的入口地址。初始时这个函数的入口地址入栈。

analysedAddresses是一个哈希表,记录已经扫描过的指令。

Xsb:这个栈是在RuntimeContext中定义,但是似乎只在这个扫描算法中被用到,应该可以用一个局部变量替换掉。

While(栈不空)

{

   栈顶元素出栈

   如果这个基本块已经扫描过(通过查找哈希表analysedAddresses来判定),栈顶元素打上isBranchTarget标记,下一个元素继续出栈。

   处理单个基本块的循环(条件是,pc<=当前基本块的结束地址)

   {//对于一个未扫描的基本块,顺序扫描基本块中的所有指令:

       取指,译码,生成相应codeInstruction并记录到codeBlock的codeInstructions链表中。记录进已经扫描过指令的哈希表中(analysedAddresses)。

       扫描到一个分支或跳转指令时,意味着一个基本块的结束,并且跳转的目标位置应该是一个新的基本块,将这个新的基本块首地址压栈。将基本块的结束地址置为延迟槽指令,这样可以确保延迟槽指令扫描完后,从扫描单个基本块的循环中跳出,去处理下一个基本块。

   }

}

简单来说,就是基本块的入口地址压栈,循环从栈顶取基本块入口地址,并扫描这个基本块。如果基本块入口地址已经被扫描过,说明这整个基本块都被扫描过了,直接从栈顶取下一个基本块即可。如果没有扫描过,就顺序扫描这个基本块。

如果扫描过程中遇到分支或跳转指令,意味着一个基本块的结束,并且,跳转的目标位置是一个新的基本块的入口,这个入口要入栈。

这里要注意,如果跳转的目标位置正好在一个延迟槽中,则该延迟槽指令被扫描过了,不代表以其为入口的整个基本块都扫描过,可能只是其前的分支或跳转指令被扫描过,导致该延迟槽也被扫描。所以判定一个基本块是否被扫描过的逻辑是:如果该入口被扫描过,且其后一条指令也被扫描过,才说明该基本块被扫描过。

/

至此,第一遍扫描完毕,所有mips指令被转换为codeInstruction,顺序存放在codeBlock的codeInstructions链表中。codeInstruction中包含的信息有:指令的地址,译码结果Instruction,mips指令的二进制编码,这条指令是否分支,分支的目标位置是哪里,这条指令是否是其他分支指令的目标位置。

这个扫描操作是在analyse函数中。在扫描操作之后,也是analyse函数的最后,调用了CodeBlock的getExecutable方法:

   IExecutable executable = codeBlock.getExecutable(context);

 

这个函数的核心语句只有一个:

   Class<IExecutable> classExecutable =compile(context);

 

也就是调用了CodeBlock.compile():

   privateClass<IExecutable> compile(CompilerContext context) throwsClassFormatError

传进来的参数只有一个,编译上下文。

 

对于编译上下文,将其当前编译的codeBlock置为this:

   context.setCodeBlock(this);

为将要构建的内部可执行类,造一个名字:

   String className = getInternalClassName();

这个函数深入进去看一下,实际内部可执行类的命名规则是:

   return "_S1_" + instanceIndex +"_" + Integer.toHexString(address).toUpperCase();

_S1_开头,然后是(编译器的)实例号,然后下划线,跟函数首地址。

这也就是jpcsp运行出错时,打印的提示信息,形如:

   at _S1_2_881A454.s(_S1_2_881A454.java:428)

就是报告出错的位置,是在某个内部可执行类中。

替换本地码和无分支基本块:

回到正题,这里调用了一个关键函数:

   prepare(context,context.getMethodMaxInstructions());

这个函数负责匹配本地码并替换,然后,如果指令数目过多,就识别并替换无分支基本块。

看他的实现代码。

扫描本地码序列,并替换

   scanNativeCodeSequences(context);

结合之前关于本地码管理器、CodeInstruction的子类的叙述,可以轻松看懂这个函数,此处不再赘述。特别指出的是,对于本地码序列之前要求做的操作,要填充额外的codeInstruction,并且他们的地址全部设置为本地码序列的首地址。也就是在最终的代码序列中,本地码和他之前的特别操作,地址全部一样。

Xsb:这里有潜在问题,因为用本地码替换之后,这一段被替换的代码序列变成了一个单独的指令,如果外部有指令要分支到原序列中的某条指令,会找不到目标。不过概率不大,因为本地码大多是库函数性质,入口相对单一,不会出现担忧的状况。

然后,判断当前函数的指令数是否过多,过多则要识别并替换无分支基本块

   if (codeInstructions.size() >methodMaxInstructions) {

            splitCodeSequences(context, methodMaxInstructions);

   }

splitCodeSequences函数首先生成无分支基本块:

   List<CodeSequence> codeSequences = new ArrayList<CodeSequence>();

   generateCodeSequences(codeSequences,methodMaxInstructions)

注意,无分支基本块的长度也要受到函数最大长度的限制。并且,在无分支基本块的生成阶段,只记录该基本块的地址范围,而不对基本块中包含的指令列表赋值。指令列表的赋值操作放在后面需要的时候才做,因为不是每个基本块最后都要被替换掉的,替换直到剩余部分的长度小于最大长度即可。

生成(识别)无分支基本块的算法

currentCodeSequence表示当前的基本块。

codeSequences是已经识别出来的基本块的列表。

codeInstructions是当前codeBlock的指令列表(按地址顺序存放,已经做过本地码替换)。

从codeInstructions列表中逐条取指令。

如果有FLAG_CANNOT_BE_SPLIT,表示当前指令是分支或跳转,也就是说该指令之前一条指令,是一个基本块的最后一条指令,那么该指令之前的部分已经构成一个完整的基本块,识别成功,将其(currentCodeSequence)加入到无分支基本块的列表中去,并将currentCodeSequence清空(当前是分支指令,不能计入无分支基本块,所以是清空操作,而不是以这个分支指令开启一个新的基本块)。

如果当前指令是某个分支的目标位置,则该指令是一个新的基本块的入口,并且,其前部分(currentCodeSequence)构成了一个完整的基本块,将其加入到codeInstructions列表中,并且,从当前位置重启一个新的基本块:

   currentCodeSequence = newCodeSequence(address);

如果是普通指令,不是分支,也不是分支的目标位置,则该指令属于当前基本块,将其加入当前基本块。实际只要设置一下当前基本块的结束地址即可:

   currentCodeSequence.setEndAddress(codeInstruction.getEndAddress());

这里需要处理基本块过长的情况。如果加上当前指令之后,序列过长,则该指令之前的部分currentCodeSequence加入到codeSequences中,然后该指令本身重启一个基本块。

Xsb:潜在的问题是,本地码之前额外加入的指令,其地址与代表本地码的那条指令地址相同,可能会导致错误。比如按照地址查找指令时,会有多个可能的返回值。也可能整个软件中都小心的避开这个问题。

///

回到splitCodeSequences函数。刚才提到,该函数调用了generateCodeSequences来生成基本块,但是生成的基本块只包含地址范围信息,而没有具体的指令列表。

然后,对基本块排序:

   Collections.sort(codeSequences);

注意,为了使得codeSequence对象可以排序,为其重载了compareTo方法,用基本块的长度比大小。

所以,排序之后长的基本块排在前面,短的在后面。

定义一个将要被替换的基本块的列表:

   List<CodeSequence> sequencesToBeSplit =new ArrayList<CodeSequence>();

从长到短,取下无分支基本块,并加入到sequencesToBeSplit列表,直到剩余部分的长度在限制范围内。

逐条指令,在sequencesToBeSplit中查找:

   CodeSequence codeSequence =findCodeSequence(codeInstruction, sequencesToBeSplit, currentCodeSequence);

如果找到,就把codeSequence封装成SequenceCodeInstruction,也就是CodeInstruction的子类:

   SequenceCodeInstructionsequenceCodeInstruction = new SequenceCodeInstruction(codeSequence);

并且,将其替换掉codeBlock中codeInstructions列表里的元素:

   lit.remove();

   lit.add(sequenceCodeInstruction);

注意,只在第一次匹配到某个基本块时做这样的替换,如果不是第一次,只是把原序列中的元素删除,并加入到codeSequence中。因为之前生成基本块时只是为其生成了地址范围信息,没有指令列表,这里正好填充指令列表。

这里还做了一个优化,从基本块查找当前指令时,先从上一次匹配成功的基本块查找,找不到时,才去其他基本块查找。因为是无分支基本块,所以第一条指令匹配之后,后面的一串指令应该都可以匹配成功。

Xsb:这里应该就会触发前述的bug,即本地码之前额外加入的指令,其地址与代表本地码序列的那个指令的地址是一样的,如果碰巧因为基本块过长,这两部分进入了不同的基本块,则其中某个基本块先被匹配之后,这几条地址相同的指令会进入同一个基本块,造成另一个基本块的首地址或末地址指令缺失。

/

至此,splitCodeSequences结束,他完成的任务是,识别并替换无分支基本块。

Prepare函数也结束,他首先匹配并替换本地码序列,然后如果指令太多,就识别并匹配本地码序列。

再回退一次,是CodeBlock.compile函数调用了prepare函数。下一步,使用ClassWriter、ClassVisitor和MethodVisitor,为这个codeBlock的codeInstructions序列生成一个内部可执行类,包括exec方法。ClassWriter相关内容参见源码解读13。

核心循环的位置(前述的CodeBlock.compile(CompilerContext context)函数调用了这个函数):

CodeBlock.java:

   private void compile(CompilerContext context,MethodVisitor mv, List<CodeInstruction> codeInstructions)

注意,MethodVisitor被作为参数传进来,因为要书写代码。循环内的核心语句只有一条:

   codeInstruction.compile(context, mv);

MethodVisitor继续被传递,用于书写exec方法的java字节码。

对于每种不同的指令,编译时的具体处理方法暂且不管,放到下一篇日志

现在只是编译了这个codeBlock主体代码,还有基本块没有编译。注意,基本块封装的指令,其编译方法的实现,是生成一个函数调用指令,可是相应的函数还没有编译出来。下一步就是为这些基本块生成内部可执行类,及其exec方法。

Xsb优化:实际上,应该只要对前述的sequencesToBeSplit进行编译即可,其余的基本块根本没有拆分出来,所以不用编译。可是jpcsp的源码中是对所有基本块都进行了编译,这里应该可以优化,实际效果有待于测试。

//

本章总结:

总的来说,对于一个mips函数,首先从内存中读取指令,解析为codeInstructions,按序存储在codeBlock中,然后对得到的codeInstructions,识别本地码并替换,如果指令数目过多,还要识别并替换基本块。

然后顺序编译codeInstructions,得到codeBlock的IExecutable成员对象。

最后,要逐个编译那些被抽取出的基本块。这是一个优化点,因为在jpcsp现有的源码中,是编译了所有的基本块,虽然那些没有抽取的基本块,其执行函数内容为空,但是小的基本块可能很多,对整体的编译性能影响将达到线性。

对于每一个指令,编译的实现细节将在下一篇文章中阐述。重点是分支和跳转指令的编译。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值