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现有的源码中,是编译了所有的基本块,虽然那些没有抽取的基本块,其执行函数内容为空,但是小的基本块可能很多,对整体的编译性能影响将达到线性。
对于每一个指令,编译的实现细节将在下一篇文章中阐述。重点是分支和跳转指令的编译。
JPCSP源码解读14:动态二进制翻译2相关推荐
- jpcsp源码解读9:指令的抽象描述与指令的译码
本文尝试说明jpcsp中译码器单元的实现方式. / 首先是对指令的一个抽象描述,Instruction类: public static abstract class Instruction / jav ...
- jpcsp源码解读13:动态二进制翻译1
注意,本文不区分 编译和翻译.在本文中,他们表示同一个意思. 首先回顾一下,之前已经说明了,我们有cpu状态,就是cpu中的各个寄存器,还有内存,以及更改这些寄存器的接口函数.另外,我们有译码器(De ...
- jpcsp源码解读12:本地码管理器与Compiler.xml
jpcsp这个模拟器的优化手段实在让人汗颜. 之前说过,他把系统调用功能全部用本地码实现了,也就是在软件需要的时候,调用java语言的实现,而不是跳转到内存中相应位置去解释执行,或者对系统调用代码做动 ...
- jpcsp源码解读之一:源码的获取与编译,以及psp详尽硬件信息文档
是我心血来潮的想法,要解读一下psp模拟器的源码,并添加详尽的中文注释.这个博客则成为文档. 本文面向java语言零基础的程序员,因为我本人的java基础就是零. 水平所限,疏漏错误之处欢迎指正.也欢 ...
- jpcsp源码解读6:PSF文件
当你运行了模拟器,通过模拟器菜单选择并加载一个umd镜像,模拟器就用这个umd镜像实例化一个UmdIsoReader(见上一篇,源码解读5). 通过这个UmdIsoReader,从光盘提取的第一个文件 ...
- jpcsp源码解读11:近期笔记
最近阅读代码主要牵涉到两个问题,一个是动态二进制翻译,一个是进程管理. 两个问题都很棘手,代码量大,复杂度高.今天主要备份一下关键笔记. / 启动运行流程: 用户点击 运行 按钮 RunButtonA ...
- jpcsp源码解读5:umd光盘镜像(.iso)
这次的状况稍显复杂. 首先说一下umd光盘镜像文件的内部组织方式.注意,这些内容全部是从源码解读而来,而不是来自关于这种文件格式的标准文档. java科普之文件操作: fileReader = new ...
- JPCSP源码解读15:动态二进制翻译3(翻译引擎最终章)
今天,我们从CodeInstruction. compile(CompilerContextcontext, MethodVisitor mv)这个函数说起. 其中,CompilerContext是编 ...
- jpcsp源码解读之二:main函数与jpcsp的初始化流程
虽然这个软件是用java语言编写,面向对象,可是总要有个开始的入口,这里关心的就是,main函数在哪里. 似乎java中也可以没有main函数,也可能是我的错误认识.暂且不管,jpcsp中是有main ...
最新文章
- CentOS7.2基于LAMP搭建WordPress,并自定义Logo和名称
- [03]常用正则表达式
- android 圆滑曲线,如何使用贝塞尔曲线在一组点上绘制平滑线?
- 图解如何在DC上添加自定义属性类
- Vue 新手学习笔记:vue-element-admin 之安装,配置及入门开发
- 24-[模块]-re
- SpringBoot整合Dubbo案例
- C语言标准输入输出stdio.h
- Python给指定QQ好友自动发送信息和图片
- 一文读懂锂电池叠片、卷绕工艺区别!
- 干货 | 人工智能应用落地的关键成功要素
- python检测刀具_科研一角|Python语言在人工智能加工中心机器人方面的应用
- 中国网络视频前景 表面云淡风轻实在暗潮汹涌
- CityEngine+Python自动化建模实现【系列文章之四】
- AD9的pcb 里面怎样才能从TOP层视图换成从BOTTOM层网上面看,相当于把板子翻过来看
- dubbo优点是什么dubbo有哪些缺点
- 基于OpenCV的鱼眼相机畸变矫正(含代码)
- IFS系统成本资料来源
- 论文的一般写作流程注意事项及如何用Word进行科研绘图 ?(三线图,模型结构图,折线图,曲线图)
- vue-pdf实现放大、缩小