用MIC-1微指令实现IJVM

MIC-1的寄存器功能

之前陆陆续续讨论过大部分寄存器的作用,这里统一的明确和汇总一下:

1) MAR:32位内存地址寄存器。为read/write内存操作提供内存字地址。用于IJVM方法区以外的区域的内存地址,这些地址对应的数据都是1个字的长度;

2) MDR:32位内存数据寄存器。存放read从内存取回的32位数据、write要写入内存的32位数据;

3) PC:32位程序计数器。为fetch内存操作提供内存字节地址。通常每次fetch取的是IJVM方法区的IJVM指令的1个字节,并且通常是顺序读取的,即下次fetch的地址是PC+1;

4) MBR:8位内存缓冲寄存器。存放fetch操作从内存取回的8位数据。其作为输出时必然会按符号扩展无符号扩展中的一种方式,把32位数据送上B总线;

5) SP:32位寄存器,指向IJVM栈的栈顶的内存地址;

6) LV:32位寄存器,指向IJVM栈的顶部栈帧局部变量结构起始位置的内存地址;

7) CPP:32位寄存器,指向IJVM常量池的起始位置的内存地址;

8) TOS:32位寄存器,意为栈顶(TopOfStack),多数时候其等于SP指向的内存地址的,用于减少内存访问次数;因为内存访问慢于寄存器;

9) OPC:32位寄存器,无固定用途,用于存储中间变量等;

10) H:32位寄存器,作为ALU的唯一左输入

如果需要,可以把任何寄存器都作为中间变量寄存器,这时寄存器跟编程中的变量类似。

 

MIC-1的微汇编指令

1233df8a-e546-46fe-b9af-939b76022516

ce4a6f9f-aace-48db-8942-37e31a998cc0

8f95dcd6-d05e-4469-9bcc-fb64237e93ae

4cac44d2-017e-4223-abc3-1d8221d94af2

微体系结构同样也有类似于汇编语言的微汇编语言(MAL),并且有微汇编器用于编译。这里也为MIC-1引入MAL,在MIC-1的符号指令里,1条(行)里的内容表示1个微指令。其起到的作用和汇编语言一样,让人避免直接编写36位的MIC-1微指令,而是用助记符的方式描述1条微指令中的多个操作。

符号指令的基本语法类似于编程的“赋值运算”,比如“MDR=SP”和“MDR=H+SP”。无论是否包含“算术操作”,等号右边的项都通过B总线-ALU-C总线的路径到达左边,这里需注意几点:

1) ALU区分左右输入:比如“H=H-MDR”,由于ALU仅支持减数为左输入(H),所以这是非法指令。但对于加法“MDR=H+SP”和“MDR=SP+H”由于ALU的加法可交换,所以允许这种情况;

2) 考虑符号指令能否在单周期完成:比如“MDR=SP+MDR”,虽然其表意很明确,但由于ALU的唯一左输入是H寄存器,所以其不能在一个周期里完成,需要拆分为多个步骤实现,并且单单这以条指令也无法明确具体的拆分步骤方式,这也是非法指令

上图给出了更一般的对“赋值运算”语法的概况:

1) SOURCE:MDR、PC、MBR、MBRU(无符号MBR)、SP、LV、CPP、TOS、OPC;

2) DEST:MAR、MDR、PC、SP、LV、CPP、TOS、OPC、H;

任何“赋值运算”符号指令,只要SOURCE/DEST是中的任何一个,就是合法指令。另外由于MBR是可扩展的8位寄存器,当把MBR赋值给32位寄存器或跟32位寄存器做“计算”时难免有些歧义,所以规定这种情况下,用MBR表示符号扩展的MBR,用MBRU代表无符号扩展的MBR。并且其都允许再最后加上移位器提供的逻辑左移8位计算,可将这个计算计为“<<8”,比如“H=MBR <<8”。又由于C总线一个周期可写入多个寄存器,所以还有“SP=SP+1=MDR”的写法。

继续扩展上述语法,这里考虑关于N,Z的触发器的描述。符号指令“Z=TOS”表示让TOS从B总线直通ALU,从而更新Z的触发器。这种操作需1个时钟周期,在时钟上升沿完成Z的触发器的写入,而C总线不需要写入哪个寄存器。无论“N=TOS”还是”Z=TOS”,其对应微指令相同,因为N,Z总同时更新。

继续加入内存操作语法。这里把内存的read/write/fetch简写为rd/wr/fetch。这样在一个周期里让SP自增,并发出内存read就有”SP=SP+1; rd”,内存数据会在下个时钟周期(下个符号指令)到达。注意连续两条“MAR=SP; rd”和“MDR=H”是非法的,因为在第二条中,隐含了rd数据到达MDR这件事,而MIC-1的时序逻辑不允许在一个周期里C总线内存总线写同个寄存器。

继续加入流程控制(转移)语法。其引入三种包含goto关键字的语法:

1) 无条件转移:“goto LABEL”,其含义为跳转到某个符号指令处(微地址),等于规定当前符号指令对应微指令ADDR部分的值。这个语法隐含了JAMZ、JAMN、JMPC都为0的条件,否则的话就等于还可能发生别的跳转情况。由于所有微指令都有ADDR,所以每个符号指令都应该有goto关键字

2) 条件转移:比如“Z=TOS; if(Z) goto L1;else goto L2”,隐含条件是“JAMZ=1,JAMN=JMPC=0”,根据MIC-1电路,微指令的ADDR等于L1(对应微地址)且L1最高位为0。L2等于L1的最高位取反

3) 多路转移:“goto (MBR or value)”,隐含条件是“JAMZ=JAMN=0,JMPC=1”,其中value是ADDR,跳转地址为MBR与value(低八位)的按位或。写作“goto (MBR)”时意味ADDR/value为0,因为or的结果对MBR无影响,表示完全按MBR跳转。注意MBR是低八位,无论符号扩展无符号扩展都一样。

 

IJVM指令与MIC-1微指令的对照表

b58231d5-de7f-4aae-a00b-cb99828f0b0e

515fa7f2-4f9c-4226-8174-a0cfbb69b079

02d43a7f-d302-429e-bb29-dcac13d34960

a635c6c5-17e7-4f15-bfa6-4ddeb9275ebb

上图为IJVM在MIC-1下的实现,包含112条微指令,每条微指令都采用微汇编的指令符号来描述。横线隔开的是每个IJVM指令,左边是每个微指令的名称,右边是每个微指令。上图包含这样的默认约定,每个指令对应的微指令都按其书面格式那样自上而下的顺序执行,所以其符号指令部分在很多地方省略了goto部分,而实际的微地址不是顺序的,并且上图也没有给出具体的微地址。

上图的结构还提供了微指令实现指令的一种“范式”,因为每段的右边看起来像是程序,在“编写程序”时,一定要同时想着MIC-1的电路和时序逻辑。只有这样才能知道每行(每个时钟周期)中包含的操作是否合法,上下两行之间会不会互相影响,这种影响多见于内存操作,比如上图的IRETURN中空置了ireturn2的整个时钟周期,原因是第1个周期基于MAR发起了内存read,第2个周期数据会打入MDR,所以在第2个周期不能再让MDR经过B总线和ALU把数据送到MAR和SP,所以第3行被推迟到下个周期执行。

接下来分析微体系的整体运行框架、具体指令的实现、以及微地址的分布。

 

微程序的主循环

从宏观的工作流程上来看,MIC-1的工作流程就像一个“死循环”,其被称为主循环每个循环节的起点是标号为Main1的微指令,可看出Main1的作用是去IJVM方法区内存取下一条ISA指令,每当一个ISA指令执行完后,就要再次执行Main1取得下一条指令,其循环往复直到关机。

然后看具体的例子,假设IJVM方法区有连续3个NOP指令需要执行,并且开始前PC指向了第1个NOP的地址,MBR存放了第1个NOP的操作码。下面的每条表示每个周期:

1) 从Main1开始,首先PC自增为PC+1,并以PC+1发出fetch内存信号。此时MBR尚为第1个NOP,goto(MBR)提供了下个周期的微指令nop1;

2) 执行nop1,其仅包含goto操作,所以其只是提供下个周期的微指令Main1(注意该周期里,上个周期fetch的数据已在上升沿写入MBR);

3) 执行Main1,首先PC自增为PC+2,并以PC+2发出fetch内存信号。此时MBR尚为第2个NOP,goto(MBR)提供了下个周期的微指令nop1;

4) 执行nop1,其仅包含goto操作,所以其只是提供下个周期的微指令Main1(注意该周期里,上个周期fetch的数据已在上升沿写入MBR);

5) 执行Main1,首先PC自增为PC+3,并以PC+3发出fetch内存信号。此时MBR尚为第3个NOP,goto(MBR)提供了下个周期的微指令nop1;

6) 执行nop1,其仅包含goto操作,所以其只是提供下个周期的微指令Main1(注意该周期里,上个周期fetch的数据已在上升沿写入MBR);

7) 执行Main1,首先PC自增为PC+4,并以PC+4发出fetch内存信号。此时MBR为第3个NOP(最后)后面接着的某个指令,goto(MBR)提供了下个周期的新指令的首个微指令;

可以发现在开始(7)之前,整个状态类似于回到了开始(1)前的状态,并且对于NOP这样的简单指令,都需要2个周期。这里可“尝试”写出nop1的36位微指令,首先因为使用了goto Main1,所以其ADDR是Main1的微地址、JAM是全0。然后C和B也分别都是全0,因为没有数据写入寄存器,也没必要让数据上B总线。然后MEM全0因为该步骤没有内存操作。而ALU部分应该是随便选个合法的填进去就行。

对于Main1,其没有ISA指令直接对应,但其又要占用时钟周期,所以也占用了一行。由于Main1中包含了goto(MBR)的操作,那根据前面对多路转移微指令的讨论,Main1微指令的ADDR的所有位都必须为0,这时ADDR本身对“下一个微指令”完全没有影响,下个微指令完全由MBR(低八位)所决定

 

微地址的分布

根据上面的讨论可发现一些微地址的分布规律,即“ISA指令的操作码=首个微指令微地址”,因为MIC-1从内存取下一个指令(操作码部分)的动作都是由Main1完成的,而Main1包含goto(MBR)操作,所以Main1的ADDR为0,从而9位MPC的值等于8位MBR(低八位)。

再次借用一个例子强调一下,对于指令实现中的微指令,它们的微地址不是连续的,省略goto是为了书面上的可读性。这里看POP和DUP指令,其操作码分别为0x57和0x59,而它们在MIC-1分别需要3和2条微指令来实现,如果微地址是顺序的那必然会导致两个指令的冲突。

前面提到过实现IJVM的微指令共112条,0x000开头的微地址空间应该够用。那0x100开头的微地址的作用是什么?主要是用于条件转移微指令,因为根据MIC-1电路逻辑限制,转移的两路首个微地址不能都以0x000开头。另外特殊功能比较特殊的WIDE指令也使用了这部分微地址。可能是为了让0x100开头的微地址空间有更大利用率,把Main1作为该空间首个微指令,即Main1的微地址是0x100。

 

IJVM指令(无操作数的栈操作)

这里按书面顺序讨论几类指令。首先以IADD这样无操作数的基于栈的算术运算为例,对于功能步骤类似的ISUB、POP、SWAP等之后不再赘述。IADD会被分解为按次序的如下4个微指令:

1) 当前PC已指向该IADD操作码的地址,MBR已存储该操作码。开始循环节(Main1),PC指向下个地址并发起内存fetch,在周期的最后时间多路跳转至iadd1微指令;

2) 设置MAR为栈顶地址-1,并以此发起内存read,因为IADD会发生2次出栈和1次入栈,所以在该周期里预先维护了栈顶指针SP=SP-1。在周期的最后时间无条件跳转至(3)微指令;

3) 让TOS保存的栈顶内容(被加数)经过C总线写入H寄存器(注意这里默认了TOS是正确的)。另外在该周期里(2)的read结果已写入MDR(加数)。在周期的最后时间无条件跳转至(4)微指令;

4) 让MDR+H的写入MDR。紧接着以新的MDR发起write,即把压入栈。同时由于栈顶发生了变化,所以维护了TOS=MDR。在周期的最后时间无条件跳转至Main1微指令,下个ISA指令即将开始;

这4个步骤不但完成了IADD的目标,还保证了在指令结束时SP和TOS的正确性,这也是所有指令实现都要能保证的。这里注意一点,IADD的微指令在处理2次的出栈时,并没有真的删2次内存,其只是改变了SP的指向,原来的加数在IADD结束时都还留在内存里。这种技巧在计算机中经常被用到,可以节省操作。比如操作系统在删除机械硬盘上的文件时,通常也不会把文件所在扇区都擦写一遍。

 

IJVM指令(有操作数的内存区域操作)

0e32af34-83b8-48f1-9f9b-db9e74e91418

08bbe6e7-c662-4c89-9420-686e4f05b24d

首先是BIPUSH指令,其不同之处在于BIPUSH带有操作数,且操作数是数据本身(常量),并且只能是1个字节的长度。其BIPUSH的每个周期的执行步骤如下:

1) 开始循环节(Main1),多路跳转至bipush1微指令。注意该周期结束后PC指向BIPUSH操作码的下个字节,也就是BIPUSH的操作数,发起的fetch也是对这个操作数的;

2) 预先把栈顶指针SP指向新栈顶(将要入栈的操作数),并把新的SP指向的地址送入MAR(准备以此发起write)。注意该周期里,BIPUSH的操作数会被内存送回到MBR;

3) 预先让PC指向下个ISA指令的操作码(PC自增1),并以新PC发起fetch;

4) 将尚未被(3)更新的MBR(符号扩展的BIPUSH操作数)赋给TOS(新栈顶)和MDR,并以新MDR发起write。最后多路跳转到Main1。注意该周期里,下个指令的操作码会被内存送入MBR;

BIPUSH由于包含操作数,所以包含3次内存操作,其被紧凑的安排进4个微指令,尤其是(4)刚使用完MBR,MBR就被内存更新。这些微指令还包含一些操作,用于维护下个goto Main1的正确执行(前面提到的指令都不含操作数,所以上个goto Main1直接就能保证下个goto Main1的正确执行)。

ILOAD指令则类似于引用数据的BIPUSH,其操作数是无符号的索引/偏移量。其前两个周期是用索引计算出内存地址,并对该地址read。注意为了让索引能有更大的范围,其8位是无符号的,即计算内存地址时应使用MBR的无符号扩展(MBRU),不过就算是这样,索引的范围也仅是0-255。后面的周期所做的操作类似于BIPUSH的后几个周期。ILOAD和BIPUSH的一个不同点是MBR的扩展方式的不同。ISTORE指令与ILOAD类似,不同之处在于它可以使用TOS,所以也就不具体讨论了。

WIDE指令是JVM/IJVM用于扩大ILOAD/ISTORE索引范围的,其将索引扩充为16位无符号整数。WIDE格式如上图,其包含4个字节。这使得WIDE需要2次多路转移,所以对照表把WIDE拆为多个部分。注意WIDE第2个字节和ILOAD/ISTORE操作码相同,这需要使用0x100开头的微地址

微指令wide1/wide2用于启动Main1后的第2次多路转移,其包含“WIDE ILOAD”“WIDE ISTORE”的两路,分别跳转至wide_iload1和wide_istore1的微指令处。wide_iload1和iload1的微地址仅最高位不同(wide_istore1和istore1同理)。而wide1的微地址还是等于WIDE的操作码。这里注意一点,在执行wide2的周期里,上个周期fetch的数据也会进入MBR,但此时该周期即将结束,MIR的输入还没来得及改变就被打入了MIR。所以goto(MBR or 0x100)还是按写入前的MBR执行的。

带wide的微指令相比于不带wide的,会多几步操作,就是把16位索引分2次fetch,并将这2个无符号扩展的8位通过逻辑左移8位计算拼接还原成16位索引。比如对于WIDE ILOAD,其在执行完wide_iload4后就会无条件转移至iload3,执行和ILOAD一样的剩余步骤。WIDE ISTORE同理。

LDC_W指令的索引也是16位的,其功能和WIDE ILOAD类似,只是数据来自常量池。在JVM指令中也有一个8位索引的LDC指令对应,IJVM中则没有这个指令。IINC指令是ISTORE/WIDE以外唯一修改局部变量结构的指令,其将指定字(通过索引)加上常量。其有3个字节,依次为操作码、索引、常量。由于求和是算术计算,所以规定这里的常量是有符号8位整数。这里不继续分析这两个指令了。

 

IJVM指令(跳转控制)

第一个跳转的指令是GOTO,Main1在取到GOTO的操作码后,需要做的是继续把操作数从内存取出,然后根据操作数再一次从内存取出下个指令的操作码然后执行。简而言之就是改变PC。注意GOTO的操作数16位带符号偏移量,偏移量是基于该GOTO指令的操作码的地址计算的。可以发现这个偏移量的范围似乎有点小,只有64KB的范围。但JVM的GOTO与此完全相同,反对这个特性的人认为这是个严重缺陷,而支持者认为良好设计的程序不会需要更大的偏移量。

GOTO指令的实现并不复杂,要注意的点就是在从Main1进入goto1时,PC的地址已经是该GOTO的操作码的下一个字节的地址,所以此时PC-1才是操作码的地址,所以第一步先用临时寄存器OPC暂存PC-1。然后就是读取内存并拼接出16位带符号偏移量,注意到对于“拼接”这个操作,这16位无论带不带符号,步骤都是一样的。然后用ALU做OPC和这个16位带符号偏移量的加法,即可得到下个指令的操作数的地址,将其放入PC并以此从内存读出操作码写入MBR,最后跳转至Main1就开始了下个指令。

IFEQ/IFLT指令为先弹出栈顶,若其等于/小于0则跳转至偏移量,等于带条件的GOTO。其关键在于最后一条微指令中的T和F两路转移,T表示条件满足执行跳转的情况,这时沿用GOTO指令的跳转逻辑即可。F则是不满足条件不跳转的情况,此时需要将PC指针、MBR调整为下个指令的操作码,然后正常执行下个指令。IF_ICMPEQ指令类似,其通过补码减法是否为0来判断相等。

 

IJVM指令(调用与返回)

INVOKEVIRTUAL是最复杂的指令,含20多个微指令。这些微指令基本都能对应于之前分析过的该指令的各个步骤。该指令根据方法方法区的前4个字节信息做完初始化操作后,就会开始前面所讲的,我们熟悉的按PC+1顺序读内存顺序执行指令的过程。而IRETURN同理,其只有很少的微指令。

发表评论

您的电子邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部