本文尝试说明jpcsp中译码器单元的实现方式。
/
首先是对指令的一个抽象描述,Instruction类:
public static abstract class Instruction
/
java科普:
注意这是一个抽象类,不可以被实例化。只有在其某个子类中实现该抽象类中的所有抽象方法,才可对该子类实例化。而该子类的实例可以被赋值给这个抽象类的实例。
也就是说,抽象类实例化时,实例一定是某个实现了所有抽象方法的子类构造出的实例,而不是从该抽象类本身构造实例。
/
在这个指令抽象类中,提供了一些抽象方法和非抽象方法。抽象方法需要在子类中实现;非抽象方法,子类则不需要实现。
成员变量:
private int m_count = 0;//这条指令被执行的次数作计数
private int flags = 0;//关于这条指令的一些信息
flag中包含的信息有:是否分支指令,是否跳转指令,是否是一个基本块的入口 等。在这里提供一个感性的认识,不一一列出,具体可以参加源码。
提供的一个关键方法:
public abstract void interpret(Processor processor, int insn)
也就是这种类型的指令的实现方法,或者说,这种类型的指令打算如何从指令本身中提取自己需要的参数,然后使用这些参数要求处理器做出怎样的动作。
注意这是一个抽象方法,jpcsp采用的手法是,为每种类型的指令,定义Instruction的一个子类,在这个子类中实现这些抽象方法。每个子类实例化一个对象出来,以便译码操作时使用。
另外还提供了一些方法,关于指令的编译,反汇编,提取指令的flag信息,该指令被执行的次数统计 等。
/
以上说明了指令的抽象描述,现在看这个抽象类的子类。
首先比较容易找到的一个子类是:
public static abstract class STUB extends Instruction
注意这又是一个抽象类,并没有对指令的抽象类做实现。
stub的意思是存根,存根负责接收本地方法调用,并将它们委派给各自的具体实现对象。这里先提供一个语义模糊的描述,具体含义会在后文说明。
/
然后是对指令集中的每一条指令,定义Instruction的一个子类。
这个步骤在一个类中完成:
public class Instructions
现在举其中一个例子,比如加法指令:
public static final Instruction ADD = new Instruction(22) {
@Override
public final String name() { return "ADD"; }
@Override
public final String category() { return "MIPS I"; }
@Override
public void interpret(Processor processor, int insn) {
int rd = (insn>>11)&31;
int rt = (insn>>16)&31;
int rs = (insn>>21)&31;
// just ignore overflow exception as it is useless
processor.cpu.doADDU(rd, rs, rt);
}
@Override
public void compile(ICompilerContext context, int insn) {
if (!context.isRdRegister0()) {
if (context.isRsRegister0() && context.isRtRegister0()) {
context.storeRd(0);
} else {
context.prepareRdForStore();
if (context.isRsRegister0()) {
context.loadRt();
} else {
context.loadRs();
if (!context.isRtRegister0()) {
context.loadRt();
context.getMethodVisitor().visitInsn(Opcodes.IADD);
}
}
context.storeRd();
}
}
}
@Override
public String disasm(int address, int insn) {
int rd = (insn>>11)&31;
int rt = (insn>>16)&31;
int rs = (insn>>21)&31;
return Common.disasmRDRSRT("add", rd, rs, rt);
}
};
我们来看这段代码的结构:
public static final Instruction ADD = new Instruction(22) {
};
这是在定义抽象类 Instruction的一个实例,这个实例是其实现了抽象方法的子类的实例。{ ...};中就是子类的定义,其中实现了 Instruction中的所有抽象方法。
然后看他使用的构造函数,是Instruction(22),看构造函数的实现,是在 Instruction类中的非抽象方法,行为就是把这个实例放进了一个数组里。那个数组的用处暂时未知。
再看其中对抽象方法 interpret的实现:
@Override
public void interpret(Processor processor, int insn) {
int rd = (insn>>11)&31;
int rt = (insn>>16)&31;
int rs = (insn>>21)&31;
// just ignore overflow exception as it is useless
processor.cpu.doADDU(rd, rs, rt);
}
也就是说,从指令中提取了rd,rt,rs这三个寄存器号,作为参数,要求处理器做加法操作。关于cpu类的内部实现,见前一篇,jpcsp源码解读8.
/
至此,我们已经有了每一条指令的行为描述。
然后,将这些东西组织成一个译码器:
public class Decoder
实现的手法是,将这些指令的实例组织进一个数组:
public static final Instruction table_0[] = {
...
...
}
这样就可以用指令中的操作码作为索引,从数组中提取出相应的指令。
/
MIPS科普:
mips指令集中,每条指令32位,其中最高6位(31:26)是opcode,也就是操作码,决定这条指令的类型。比如,跳转指令。对于其中的某一种类型,比如r-type,寄存器类型,行为是两个寄存器中的内容做运算,结果存放到第三个寄存器中。寄存器号被编码在指令中(25:21,20:16,15:11)。具体做哪一种运算,取决于指令的最低6位(5:0)
/
回到译码器类,这个类中只有一个方法:
public static final Instruction instruction(int insn) {
return table_0[(insn >> 26) & 0x0000003f].instance(insn);
}
行为很简单,提取指令的最高6位,作为索引,取出数组中的相应元素,并调用该元素的instance方法,取得实例。
但是这里有一个问题,有些指令,根据最高6位只能确定指令的大类型(比如r-type),然后还要解析指令中的其他位,来确定具体是哪一条指令(是r-type中的add,还是sub)。
这里的手法是,对于最高6位就可以确定具体哪一条指令的,数组中放置该指令的实例作为元素(比如跳转指令);对于最高6位只能确定是某个大类的,数组中放置一个stub作为元素。这个stub元素的instance方法,会提取指令中相应的位(比如r-type位置的stub就提取指令中的最低6位),然后用提取出的这些位,去索引另一张表,从而确定具体是哪一条指令。
这也就是前文提到的stub的作用,作为存根,然后提取参数并分发任务。
这也解释了为什么译码方法中,取得数组元素之后还要调用元素的instance方法。对于已经查到的指令实例,instance方法直接返回this,也就是该元素本身;对于索引位置的元素为stub的情形,stub的instance方法进一步索引另一个表,并调用所得元素的instance方法,即可得到具体某一条指令的实例。
/
总结起来,就是每条指令有一个行为描述,将这些指令的行为描述组织为表格;从指令中提取出操作码,用操作码去索引表格,就可以得到指令的行为描述。
现在,通过译码单元,我们可以从二进制编码的指令得到该指令的行为描述。
下一篇,将说明模拟器中指令的执行过程。