这次要说的是处理器类:
public class Processor
主要的成员变量:
public CpuState cpu = new CpuState();
public static final jpcsp.Memory memory = jpcsp.Memory.getInstance();
public ParameterReader parameterReader;
///
其中只有cpu状态处于核心地位。memory在parameterReader实例化时用到,是为了在虚拟机中实现固件功能(系统调用)时使用,因为系统调用函数需要从cpu状态和内存单元中取得用户进程提供的参数。这方面内容将在以后关于系统调用实现的专题中详细说明。
///
另外还有几个配置选项:
public static boolean ENABLE_STEP_TRACE = false; //单步追踪
public static boolean ENABLE_INSN_EXECUTE_COUNT = false; //对每种指令的执行次数作计数
private final static boolean ENABLE_INSN_CACHE = false; //指令cache使能
其中单步追踪只是对每一步记录日志,暂时无视。
///
在这里还实现了指令cache:
static class CacheLine { //cache行中包含 有效位,地址,还有数据(指令cache中的数据就是指令)
//这里存了完整的地址,而不是地址的高位。用于地址匹配,以确定是否cache命中
boolean valid;
int address;
int opcode; //是二进制形式的mips指令
Common.Instruction insn; //译码出来的指令实例
}
/*
* jpcsp虚拟机中实现了指令cache
*/
private final int INSN_CACHE_SIZE = 0x1000; //指令cache的尺寸
private final int INSN_CACHE_MASK = INSN_CACHE_SIZE - 1; //减1就得到掩码
private CacheLine[] insnCache; //指令cache实例,是cache行的数组
private long insnCacheHits, insnCacheMisses, insnCount; //关于指令cache命中与失效的统计信息
这个指令cache采用直接相连,指令的地址低位作为索引查找到cache行,然后用指令的地址和cache行中存放的指令的地址作比较,来判定是否命中。
///
来看这个类提供的一个核心方法:
public void step() {
interpret();
}
step,就是前进一步。直接调用了interpret()方法:
public void interpret() {
if (ENABLE_STEP_TRACE)
StepLogger.append(cpu);
if (ENABLE_INSN_CACHE) {
CacheLine line = fetchDecodedInstruction();
line.insn.interpret(this, line.opcode);
if (ENABLE_INSN_EXECUTE_COUNT)//对每一种指令分别计数
line.insn.increaseCount();
} else {
int opcode = cpu.fetchOpcode();//没有使能指令cache,则调用cpu本身的预取,动作应当包括从内存中取指,以及pc、npc值分别加4
Common.Instruction insn = Decoder.instruction(opcode); //对指令进行译码
insn.interpret(this, opcode); //执行这条指令
if (ENABLE_INSN_EXECUTE_COUNT)
insn.increaseCount();
}
}
这是函数的完整源码。如果使能单步调试,就记录一下日志。如果使能指令cache,就从cache中预取指令,否则直接调用cpu本身的预取方法。然后对预取到的指令译码,最后调用这个译码得到的指令实例的interpret方法(对于每种不同指令,其interpret方法不同,会从指令本身提取参数,并根据这些参数调用CpuState的相应方法,改变cpu状态;具体见上一篇,关于指令的抽象描述)。之后更新指令执行次数的统计信息。
也就是说,一共三步,预取,译码,执行。
其中稍微复杂的是分支或者跳转指令的执行,因为带延迟槽。
///
来看其中的预取操作:
private CacheLine fetchDecodedInstruction() { //npc也加了4,表示带预取
CacheLine line = insnCache[cpu.pc & INSN_CACHE_MASK]; //取地址地位,到cache中索引
if (!line.valid || line.address != cpu.pc) { //从这里可以看出,指令cache使用直接相连的结构
line.valid = true; //没有命中
line.address = cpu.pc; //用此次想要取得的值来重新填充这个cache行
line.opcode = memory.read32(cpu.pc); //从内存中取得指令
line.insn = Decoder.instruction(line.opcode); //对指令进行译码
//译码意味着确定这条指令的类型,或者说,取得这个指令的实现动作
//执行的时候,调用此处取得的实现动作,并把指令本身以及当前cpu作为参数穿给这个动作
//这个动作会从指令中提取参数,然后要求cpu执行指定的操作
insnCacheMisses++; //cache缺失计数
}
else insnCacheHits++; //cache命中计数
insnCount++; //总的指令数
cpu.pc = cpu.npc = cpu.pc + 4; //pc值向前加1
//很恶劣的一个语句,猜想java应该是从左向右计算,先把npc赋值给pc,也就是pc被加4。
//然后,把得到的这个新的pc再加4,得到新的npc,赋值给cpu.npc
return line;
}
如果命中,就直接返回那个cache行。如果没命中,就从内存中读取指令,用读取到的指令填充cache行并返回。
注意预取操作应当更新cpu状态,将pc和npc都加4。所以前述的 interpret()中,从cache行预取之前或之后都没有做pc加4的操作,因为在这个预取操作中做了;如果没有使能cache,是调用了CpuState的预取操作,其中也包含了pc的更新动作。
///
现在来看分支指令的处理:
首先,如果外部调用step,会触发上述流程,预取,译码,执行。
注意,预取的时候已经包括了pc和npc的加4操作。也就是说,如果当前预取到的指令是分支或跳转指令,那么预取之后,pc已经指向延迟槽中的指令。
这时进入到分支指令的 interpret:
public static final Instruction BEQ = new Instruction(73, FLAGS_BRANCH_INSTRUCTION | FLAG_ENDS_BLOCK) {
。。。
@Override
public void interpret(Processor processor, int insn) {
int imm16 = (insn>>0)&65535;
int rt = (insn>>16)&31;
int rs = (insn>>21)&31;
if (processor.cpu.doBEQ(rs, rt, (int)(short)imm16))
processor.interpretDelayslot();
}
。。。
};
其中调用了processor.cpu.doBEQ,返回值应该是分支是否发生,如果分支发生,则调用processor.interpretDelayslot(),去处理延迟槽中的指令。
先看一下doBEQ做了什么:
public boolean doBEQ(int rs, int rt, int simm16) {
npc = (gpr[rs] == gpr[rt]) ? branchTarget(pc, simm16) : (pc + 4);
if (npc == pc - 4 && rs == rt) {
Processor.log.info("Pausing emulator - branch to self (death loop)");
Emulator.PauseEmuWithStatus(Emulator.EMU_STATUS_JUMPSELF);
}
return true;
}
注意,他只更改了npc的值,而没有更改pc的值。pc的值此时就是指向延迟槽。并且不论分支是否发生,都返回true,认为分支成功。也就是说,分支指令的interpret中,interpretDelayslot一定会执行。
interpretDelayslot的行为和processor.interpret行为类似,区别在于:
其中取指令的操作没有调用预取函数,使用了别的取指函数,这些函数不包含npc加4的动作,只有pc加4的操作(无意义),
在执行步骤之后,调用了cpu.nextPc(),这个方法是将npc赋值给pc,然后将npc置为pc+4
总结来说,通常指令的过程是:预取(pc和npc都加4),译码,执行;分支或跳转指令的过程是:预取(pc和npc都加4),译码,执行(npc更新为跳转的目标位置,对延迟槽中指令做 取指(注意不是预取),译码,执行,用npc覆盖pc)
///
本篇总结,Processor类的step方法可以执行一条指令。指令的执行流程是 预取(根据pc从内存取指令并更新pc npc),译码(用Decoder类实现,解析见上一篇文章),执行(指令的interpret方法去调用CpuState的方法,更改cpu状态)