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现有的源码中,是编译了所有的基本块,虽然那些没有抽取的基本块,其执行函数内容为空,但是小的基本块可能很多,对整体的编译性能影响将达到线性。

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

JPCSP源码解读14:动态二进制翻译2相关推荐

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

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

  2. jpcsp源码解读13:动态二进制翻译1

    注意,本文不区分 编译和翻译.在本文中,他们表示同一个意思. 首先回顾一下,之前已经说明了,我们有cpu状态,就是cpu中的各个寄存器,还有内存,以及更改这些寄存器的接口函数.另外,我们有译码器(De ...

  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. CentOS7.2基于LAMP搭建WordPress,并自定义Logo和名称
  2. [03]常用正则表达式
  3. android 圆滑曲线,如何使用贝塞尔曲线在一组点上绘制平滑线?
  4. 图解如何在DC上添加自定义属性类
  5. Vue 新手学习笔记:vue-element-admin 之安装,配置及入门开发
  6. 24-[模块]-re
  7. SpringBoot整合Dubbo案例
  8. C语言标准输入输出stdio.h
  9. Python给指定QQ好友自动发送信息和图片
  10. 一文读懂锂电池叠片、卷绕工艺区别!
  11. 干货 | 人工智能应用落地的关键成功要素
  12. python检测刀具_科研一角|Python语言在人工智能加工中心机器人方面的应用
  13. 中国网络视频前景 表面云淡风轻实在暗潮汹涌
  14. CityEngine+Python自动化建模实现【系列文章之四】
  15. AD9的pcb 里面怎样才能从TOP层视图换成从BOTTOM层网上面看,相当于把板子翻过来看
  16. dubbo优点是什么dubbo有哪些缺点
  17. 基于OpenCV的鱼眼相机畸变矫正(含代码)
  18. IFS系统成本资料来源
  19. 论文的一般写作流程注意事项及如何用Word进行科研绘图 ?(三线图,模型结构图,折线图,曲线图)
  20. vue-pdf实现放大、缩小

热门文章

  1. Linux驱动3:驱动模块加载与卸载
  2. SEO讲座之关键字布局、密度、相关性及转换(转)
  3. 如何在漫画阅读器中离线阅读Webcomics
  4. android编程权威指南(第2版)的PhotoGallery项目的练习
  5. win7招不到网络计算机,分享:一招了却G41 Win7下的遗憾
  6. SMT贴片加工的主要流程解析?
  7. 【转】寻找最好的笔记软件:海选篇 (v1.0)
  8. 天梯赛蓝桥杯2022.4.23有感
  9. snapchat阅后即焚实现分析
  10. 【华中科技大学软件学院】19级停止招生??