指令集体系(ISA)是规定处理器外在行为的一系列内容的统称,包括基本数据类型、指令、寄存器、寻址模式、存储体系、中断、异常以及外部I/O,规定了处理器的行为,是处理器的语言,是硅农和码农之间的桥梁,处理器的具体设计即ISA的硬件实现方式被称为微架构。本章依次介绍RISC指令集的存储器、计算、分支和杂项指令,最后总结了处理器各类中断与异常。
5.1. RISC概述
MIPS指令集,指令长度基本都是32位,可分为I(立即数)、J(跳转指令)、R(寄存器操作)三种基本类型。注意除常规指令外还存在存在MTCO和MFCO用来在协处理器的寄存器和处理器中的寄存器之间传递数据。要对前者进行操作需要将其复制到通用寄存器中,操作完毕后再复制回原来的处理器,此外还有调试和数字信号处理相关指令,以及用于8bit图像数据操作的单指令多数据(SIMD)指令,该指令可在一个周期完成4个8位数据的运算,通过这些特殊指令增加处理器对特殊任务的处理能力。
ARM指令集的指令长度基本也是32位(16位的thumb指令在处理器内部也会转化为标准的32位指令),采用load/store结构,处理器只能对寄存器操作,ARM借鉴了CISC的特点如一条指令中尽可能做更多任务,这是有别于MIPS指令集的一个显著特点,本节主要介绍ARM指令集。
ARM主要分为DP(data processing)、DT(data transfer)以及BR(branch),具体格式如图85。
1)Cond,ARM中每条指令都可以条件执行,这部分判断条件是否成立;
2)F,区分指令格式,如DP、DT、BR三种类型;
3)I,立即数标识,为0说明第二个操作数来自寄存器,否则是立即数;
4)Opcode,指令的基本类型;
5)S,set condition code,置1表示该指令操作会影响状态寄存器(CPSR)的值,通常在指令后加S表示这个功能;
6)Rn,表示存放第一个操作数的寄存器;
7)Rd,目的寄存器,存放指令运算结果;
8)Operand2,第二个操作数,可能来自寄存器,也可能是立即数。表示立即数时需要进行扩展,如图86,实际的imm=imm_8循环右移偶数位(rotate_imm X 2),因此实际上很多32位的立即数是不合法的,不能在指令中直接被编码,合法imm格式如图87。
![6f7bd26b82f142657a2f58b6812fa697.png](https://i-blog.csdnimg.cn/blog_migrate/37481187a4bb9a32d3ac74a8ccca8832.png)
![36ca41ba1f6e58e6fd482e1c4b2332cc.png](https://i-blog.csdnimg.cn/blog_migrate/b4925d08d1ac44c90833ddfcacf2b008.png)
但这样的方式会出现一个立即数可以由多种方式得到,所以ARM规定选择rotate_imm数值最小的编码方式使得imm_12和实际的立即数是一一对应的。码农不用关心什么样的立即数合法,编译器会将合法的立即数直接编码,而将不合法立即数放进文字池(程序存储器中位于程序区后面用于存放常数的一段空间),然后用pc相关的load指令获得该立即数,如图88。由于流水线原因,load指令在计算地址时使用的pc实际上已经是pc+8了。pc寄存器是ARM指令集中定义的一个通用寄存器,因此可在程序中直接使用。MIPS中通过lui指令将16位立即数放入寄存器高16位,通过addiu或ori将16位立即数放入刚才寄存器的低位依次得到32位立即数,由于ARM的做法需要访问TLB和cache等部件,因此MIPS的处理方法更加简洁高效。在寻址方式上,由于MIPS和ARM都是RISC处理器,所以区别不大,图89概括了该类处理器使用的寻址方式,图89(e)的寻址方式把pc和指令中立即数进行拼接而不是加法操作,这种寻址方式是MIPS特有的。
![3a1ecf0a5dd182e63d0b8d234275fe66.png](https://i-blog.csdnimg.cn/blog_migrate/83ecb2aae61ad2f28694478bf7c5c003.jpeg)
![88ee0452bc769ba29129952f2edcd598.png](https://i-blog.csdnimg.cn/blog_migrate/0f770740dc8099583b29a55998bac3fd.jpeg)
5.2. Load/store指令
1)Load指令
包括LB、LBU、LH、LHU、LW,所有load的存储器地址均为基址+offset,基址来自寄存器,offset来自立即数。
2)Store指令
包括SB、SH、SW,分别将寄存器的低8、低16、整个32位放入存储器,没有符号之分因为只用将寄存器指定内容放入存储器指定字节位置即可。
3)Load/store指令
需要注意大小端问题,一般意义上小端格式更符合正常思维,小端格式是把一个数据的低位字节放在存储器低位地址,而大端模式则把低位数据放高位地址。MIPS中load/store任务简单,而ARM中的相应指令由于借鉴了CISC和DSP(数字信号处理)指令集的一些设计思路进行了更多任务,如:
支持前/后变址(pre-indexed/post-indexed)的寻址方式,除了完成普通的加载存储操作外,还可改变load/store指令中地址寄存器的值。当指令执行完成后,可自动将存放之地的寄存器进行自加或自减,就可以对一片连续的地址空间进行操作,而不再需要加减法指令。尤其是对于DSP运算,需要的数据和系数均存放在存储器中,使用ARM的load指令就可以方便地取出来进行乘累加运算。对于MIPS为降低硬件复杂度坚守RISC的理念,加32位的编码空间没有容纳前/后变址功能的编码而没采用这种寻址方式。该寻址方式在普通处理器中容易实现性能占优,但在超标量处理器中会带来额外的麻烦,第6章会详细说明。
多寄存器传送指令LDM/STM,在一条指令中将存储器中一片连续地址数据放在多个寄存器中,节省指令存储空间,减少I-cache缺失率,不过在超标量处理器中,LDM/STM由于含有多个目的寄存器和源寄存器,很难直接处理,需要采取特殊措施,也会在第6章中介绍。
5.3. 计算指令
1)加减法
MIPS中,如果发生加法溢出,ADD指令会产生异常,结果不写入rd,进入异常处理程序,超标量处理器中代价会很大,ADDU发生溢出时不产生异常,结果写入rd。减法直接把减法取反后加1,就变成加法,如果发生溢出SUB产生异常,SUBU不产生异常。而在ARM中,可选择将结果状态(是否溢出,是否为零等信息)保存到状态寄存器CPSR中,后续指令可根据CPSR值进行带进位加法或根据CPSR值决定自身是否执行等,ARM的一个显著特点就是每条指令都可条件执行,因此需要4位条件码,所以通用寄存器就只有16个(多了地址位数增加编码空间不足),增加访问存储器次数。ARM中要实现带进位加法就非常容易可轻松实现更高位数的加法运算,实现{r2,r1}={r2,r1}+{r4,r3}如下:
![8349ce96f869cc95257419ac6d6dd26e.png](https://i-blog.csdnimg.cn/blog_migrate/e3e8df34e487c83814ccc3f3d99c9329.png)
ADCS会自动加上ADDS的进位值。
2)移位指令
MIPS中带V和不带V的(如SLL和SLLV)的移位指令,不带V的移位位数由立即数决定,带V的由相关寄存器的低5位决定。左移直接低位补0,右移分为逻辑和算术两种,逻辑右移高位用0补充,算术右移高位用符号位补充。ARM中没有专门的移位指令,因为ARM中大部分运算指令都可将操作数在运算之前进行移位,即一条指令完成移位和其他运算。
3)逻辑指令
完成与、或、非、异或等操作,MIPS和ARM中都有,逻辑运算配合立即数可完成很多任务如有选择屏蔽寄存器某些位,在配置寄存器时会经常使用,还有计算余数功能,比如计算R1/32,用ANDI R2,R1,0x0000_001F即可实现,但需要除数为2的整数次幂。
4)乘法指令
MIPS中,MUL计算低32位,MULT则低32位放入特殊寄存器Lo,高32放入Hi,如果要对乘法结果进行操作,需要将Lo和Hi结果通过专门的指令移到通用寄存器才能计算,并将计算完的结果再移回特殊寄存器(为什么不能放通用寄存器?),这两个寄存器也可被重命名。ARM中可直接在指令中指定两个通用寄存器存放乘法结果。
5)乘累加指令
乘累加指令是DSP中最基本的运算,MIPS中此类指令有MADD、MADDU、MSUB、MSUBU。MADD可将有符号乘法结果自动与Hi及Lo中寄存器相加,可表示为{Hi,Lo}={Hi,Lo}+{rs x rt}(进位问题如何解决?)。只有最后一次需要把两特殊寄存器值移入通用寄存器进行其他运算,因此执行效率也相对较高。ARM中仍然指定两个通用寄存器存放结果。
6)特殊计算指令
MIPS中有两条分别为CLZ和CLO,CLZ计算通用寄存器从最高位开始连续0的个数,实现这个功能并不直接,需要一些硬件后续会介绍。CLO计算连续1的个数,只需将寄存器取反使用CLZ的硬件就可实现CLO的功能。ARM中CLZ实现功能一样。
5.4. 分支指令
改变指令执行顺序的指令被称为分支指令,分为有条件执行和无条件执行两种。注意由于任何pc最低位都是00,所以16位立即数都会左移两位再与pc相加再加4即为目标地址(pc_relative),这样扩大了跳转范围,注意18位立即数为有符号数即可向前也可向后跳,MIPS中的分支首先进行条件判断再决定是否跳转而ARM中是判断CPSR状态寄存器的状态是否满足要求。使用方式如下:
BEQ LABEL1
这条指令在执行时直接读取CPSR寄存器的内容,判断zero标志位Z是否有效,有效则上一条指令满足相等的条件,则跳到LABEL1的地方执行。MIPS中,J和JAL中将26位立即数左移两位而后作为低位与延迟槽中高4位pc拼接,注意可访问整个对齐的256MB区域而不是-128MB到128MB,如图90。JALR使用一个寄存器的值作为跳转地址,以获得更广的跳转范围。MIPS中所有名称中带有“AL”的分支指令都用来实现子程序调用的call功能,将当前指令的下一条pc(有延迟槽则是再下一条指令即pc+8,注意保存后再判断条件)保存到R31(JALR可自行指定保存返回地址的寄存器,如果是ARM由于有专门的pc寄存器就不用保存pc),然后如果是条件跳转再根据条件是否成立决定是否跳转。
![fd049bdea2a5acac73574c4afaacf876.png](https://i-blog.csdnimg.cn/blog_migrate/b95efa8ceb904ff3e6786c3036116734.png)
这样只要执行JR $R31就相当于执行了return指令,这样只要取指阶段预测到带“AL”指令(相当于call指令,通过历史pc预测)就将下一条pc(有延迟槽则下下条)保存到RAS中,而取指阶段预测到该指令为JR $R31(通过历史pc预测)就认为是return指令从RAS中读出最新写入的值作为预测返回地址。注意JR作为一般跳转指令就不能使用R31作为目标寄存器。进入子程序时为避免程序嵌套的影响也需把R31内容手动压栈保存,子程序结束将R31内容出栈就可恢复它的值。MIPS除了分支其他指令都不支持条件执行,因此if-else语句只能靠分支指令执行,这样需要进行预测就存在失败风险,而ARM任何指令都可条件执行,if-else语句可编译成下面的指令:
![da777893f6f020c1dc967fa021ef36c5.png](https://i-blog.csdnimg.cn/blog_migrate/8f18c5babf7e5572fcf42b9988fdec71.png)
每执行一条指令就可以选择将指令结果状态写入CPSR,包括结果是否为0、正值还是负值以及是否发生溢出等,而后面的指令可通过CPSR寄存器中状态决定是否执行,这样避免了分支预测失败的风险。但这种优势并非绝对,当分支块很大,需要条件执行的个数很多,这样的优势就变为劣势,还会给寄存器重命名的过程带来麻烦,后文将进行详细介绍。
上述subs结果为0时CPSR寄存器的标志位就会被置1,addeq条件码为0000表示Z==0才可指令,subne条件码为0001表示Z不为0时执行。但由于条件码的存在编码资源下降通用寄存器个数减小至16个,意味着存储器访问增多,发生cache miss的风险就大,而实际中大部分指令也并不会条件执行,不符合RISC的风格。
5.5. 杂项指令
包括访问协处理器的指令、产生软件中断的指令以及调试相关的指令。MIPS中用来控制处理器执行情况的所有控制寄存器都放在第一个协处理器CP0中,需先将其读取到通用寄存器才能修改处理完再写回,这是RISC处理器的一个特征,ARM也如此。由于向协处理器写数据这个过程不可逆,一般是先将结果存到一个缓存中,待指令退休后再写入协处理器。但由于CP0中的寄存器控制着处理器状态,后续指令应在新的状态执行,因此在乱序处理器中需要在协处理器指令MTC0后使用隔离(barrier)指令保证后序指令不能先于该指令执行且在改变处理器状态后才执行。超标量处理器中隔离指令到达最后阶段,在退休时会抹掉后序所有指令重新从隔离指令后面的地址取指从而实现了前面所述功能(是否需要先暂停取指译码以节省功耗?)。MIPS中隔离指令是SYNC,ARM中是DMB、DSB和ISB。
MIPS中SYCALL指令用来产生异常,当该指令执行时会无条件产生一个异常,跳转到对应异常处理程序。SYSCALL的编码中包括了20位的code部分,可以被软件用来传递参数,在异常处理程序中需要使用load指令将该指令从程序存储器中读出才知道code内容。在超标量的MIPS处理器中,只有SYCALL退休才会产生异常,与普通异常产生方式一样。当处理器运行操作系统时,如linux不允许普通用户访问操作系统内核空间(异常处理程序就在该空间)、不能直接访问底层很多硬件如协处理器只能访问用户态空间。当用户程序使用特权模式做一些事情如加载页到物理内存,就需要SYSCALL指令,该指令执行时,处理器由用户模式变为内核模式,可以访问任何资源,执行完任务在异常处理程序中通过ERET指令返回SYSCALL之后的指令,同时处理转回用户模式。ERET指令会保证异常处理程序的指令都退休后才执行返回操作相当于实现隔离功能(隔离指令+return指令)。
MIPS中trap类指令也是用来产生异常,不过需要满足某些条件才产生异常,如TEQ指令当rs和rt相等时才会产生异常,而SYSCALL是无条件产生,这是它们唯一的区别。
5.6. 异常
除分支之外的很多情况可打断程序执行,这被称为异常,包括:
1)外部事件引发的中断,处理器在执行的任何阶段都可能接收到中断,也称为异步异常;
2)VA到PA转换引发的异常,如发生TLB miss或page fault miss,或程序访问受保护页产生的访问权限错误的异常;
3)指令自身引起的错误如未定义指令、用户态非法指令、整数运算溢出、访问存储器地址未对齐。很多处理器还支持数据完整性检查,如处理器对L2送来的数据进行奇偶校验或ECC校验,失败也会引发异常;
4)指令自身产生的异常如SYSCALL和trap,普通程序通过这两种指令调用操作系统的某些任务。
所有类型处理过程都类似,跳转到异常处理程序的入口地址,执行完返回,这样做的前提是处理器必须找到哪条指令发生的异常(精确异常)。大部分产生异常的指令处理完执行需重新执行该指令(如load引发的page fault),而SYSCALL/trap则需从下一条指令重新执行,因此需要区别对待返回地址。异常发生时需保存当前pc或下一条pc,RISC处理器中一般使用专用寄存器保存如MIPS中使用EPC寄存器专门保存该值,而对于CISC处理器一般使用堆栈保存(那个年代寄存器昂贵)。
异常处理程序开始时需要对涉及到的寄存器进行保存,所有处理器都会将该内容保存到堆栈。对于RISC,只能使用store指令完成该过程,堆栈指针可用一个通用寄存器模拟,比如MIPS使用R29作为堆栈指针,只能在异常处理程序中被使用。但如果其他程序要使用该寄存器就需要软件管理堆栈指针的增减。CISC中使用PUSH/POP指令访问堆栈,硬件自动增加减少专用寄存器中的栈指针,ARM中也采用了这两个指令以及专用堆栈指针寄存器,与x86类似。
对于异常的处理一定要放在流水线最后阶段处理保证异常之前的指令全部执行,而异常之后的指令全部不执行。注意每条指令的pc都会随流水线流动这样最后才能将异常pc或下一条pc保存到EPC(exception pc)寄存器中,而异常原因被记录在cause寄存器中供处理程序查询使用。由于异常发生频率小,因此虽然后面有很多指令被抹掉但对处理器性能影响较小。在乱序超标量处理器中,对异常处理会更复杂,详细处理方法后文会介绍。
注:本专栏所有内容均来自于本人对《超标量处理器设计》这本书学习笔记的总结整理与思考,专业术语的英文全称及解释均只在第一次出现时说明,后文不再说明,可读性较差,但适合从事CPU设计工作的专业人员逐字逐句精读,不喜请自行离开,没必要瞎喷。