jpcsp源码解读10:指令的执行

这次要说的是处理器类:

    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状态)

    


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值