2.4 指令系统组成

指令系统由若干条指令及其操作对象组成。每条指令都是对一个操作的描述,主要包括操作码和操作数。操作码规定指令功能,例如加减法;操作数指示操作对象,包含数据类型、访存地址、寻址方式等内容的定义。

2.4.1 地址空间

处理器可访问的地址空间包括寄存器空间和系统内存空间。寄存器空间包括通用寄存器、专用寄存器和控制寄存器。寄存器空间通过编码于指令中的寄存器号寻址,系统内存空间通过访存指令中的访存地址寻址。

通用寄存器是处理器中最常用的存储单元,一个处理器周期可以同时读取多条指令需要的多个寄存器值。现代指令系统都定义了一定数量的通用寄存器供编译器进行充分的指令调度。针对浮点运算,通常还定义了浮点通用寄存器。表2.1给出了部分常见指令集中整数通用寄存器的数量。

表2.1 不同指令集中整数通用寄存器的数量

LoongArch指令系统中定义了32个整数通用寄存器和32个浮点通用寄存器,其编号分别表示为$r0~$r31和$f0~$f31,其中读取$r0总是返回全0。

除了通用寄存器外,有的指令系统还会定义一些专用寄存器,仅用于某些专用指令或专用功能。如MIPS指令系统中定义的HI、LO寄存器就仅用于存放乘除法指令的运算结果。

控制寄存器用于控制指令执行的环境,比如是核心态还是用户态。其数量、功能和访问方式依据指令系统的定义各不相同。LoongArch指令系统中定义了一系列控制状态寄存器(Control Status Register,简称CSR),将在第3章介绍。

广义的系统内存空间包括IO空间和内存空间,不同指令集对系统内存空间的定义各不相同。X86指令集包含独立的IO空间和内存空间,对这两部分空间的访问需要使用不同的指令:内存空间使用一般的访存指令,IO空间使用专门的in/out指令。而MIPS、ARM、LoongArch等RISC指令集则通常不区分IO空间和内存空间,把它们都映射到同一个系统内存空间进行访问,使用相同的load/store指令。处理器对IO空间的访问不能经过Cache,因此在使用相同的load/store指令既访问IO空间又访问内存空间的情况下,就需要定义load/store指令访问地址的存储访问类型,用来决定该访问能否经过Cache。如MIPS指令集定义缓存一致性属性(Cache Coherency Attribute,简称CCA)Uncached和Cached分别用于IO空间和内存空间的访问,ARM AArch64指令定义内存属性(Memory Attribute)Device和Normal分别对应IO空间和内存空间的访问,LoongArch指令集定义存储访问类型(Memory Access Type,简称MAT)强序非缓存(Strongly-ordered UnCached,简称SUC)一致可缓存(Coherent Cached,简称CC)分别用于IO空间和内存空间的访问。存储访问类型通常根据访存地址范围来确定。如果采用页式地址映射方式,那么同一页内的地址定义为相同的存储访问类型,通常作为该页的一个属性信息记录在页表项中,如MIPS指令集中的页表项含有CCA域,LoongArch指令集中的页表项含有MAT域。如果采用段式地址映射方式,那么同一段内的地址定义为相同的存储访问类型。如MIPS32中规定虚地址空间的kseg1段(地址范围0xa0000000~0xbfffffff)的存储访问类型固定为Uncached,操作系统可以使用这段地址来访问IO空间。LoongArch指令集可以把直接地址映射窗口的存储访问类型配置为SUC,那么落在该地址窗口就可以访问IO空间。(有关LoongArch指令集中直接地址映射窗口的详细介绍请看第3章。)

根据指令使用数据的方式,指令系统可分为堆栈型、累加器型和寄存器型。寄存器型又可以进一步分为寄存器-寄存器型(Register-Register)和寄存器-存储器型(Register-Memory)。下面分别介绍各类型的特点。

1)堆栈型。堆栈型指令又称零地址指令,其操作数都在栈顶,在运算指令中不需要指定操作数,默认对栈顶数据进行运算并将结果压回栈顶。

2)累加器型。累加器型指令又称单地址指令,包含一个隐含操作数——累加器,另一个操作数在指令中指定,结果写回累加器中。

3)寄存器-存储器型。在这种类型的指令系统中,每个操作数都由指令显式指定,操作数为寄存器和内存单元。

4)寄存器-寄存器型。在这种类型的指令系统中,每个操作数也由指令显式指定,但除了访存指令外的其他指令的操作数都只能是寄存器。

表2.2给出了四种类型的指令系统中执行C=A+B的指令序列,其中A、B、C为不同的内存地址,R1、R2等为通用寄存器。

表2.2 四类指令系统的C=A+B指令序列

寄存器-寄存器型指令系统中的运算指令的操作数只能来自寄存器,不能来自存储器,所有的访存都必须显式通过load和store指令来完成,所以寄存器-寄存器型又被称为load-store型。

早期的计算机经常使用堆栈型和累加器型指令系统,主要目的是降低硬件实现的复杂度。除了X86还保留堆栈型和累加器型指令系统外,当今的指令系统主要是寄存器型,并且是寄存器-寄存器型。使用寄存器的优势在于,寄存器的访问速度快,便于编译器的调度优化,并可以充分利用局部性原理,大量的操作可以在寄存器中完成。此外,寄存器-寄存器型的另一个优势是寄存器之间的相关性容易判断,容易实现流水线、多发射和乱序执行等方法。

2.4.2 操作数

1.数据类型

计算机中常见的数据类型包括整数、实数、字符,数据长度包括1字节、2字节、4字节和8字节。X86指令集中还包括专门的十进制类型BCD。表2.3给出C语言整数类型与不同指令集中定义的名称和数据长度(以字节为单位)的关系。

表2.3 不同指令集整数类型的名称和数据长度

注:LA32和LA64分别是32位和64位LoongArch指令集。

实数类型在计算机中表示为浮点类型,包括单精度浮点数和双精度浮点数,单精度浮点数据长度为4字节,双精度浮点数据长度为8字节。

在指令中表达数据类型有两种方法。一种是由指令操作码来区分不同类型,例如加法指令包括定点加法指令、单精度浮点加法指令、双精度浮点加法指令。另一种是将不同类型的标记附在数据上,例如加法使用统一的操作码,用专门的标记来标明加法操作的数据类型。

2.访存地址

在执行访存指令时,必须考虑的问题是访存地址是否对齐和指令系统是否支持不对齐访问。所谓对齐访问是指对该数据的访问起始地址是其数据长度的整数倍,例如访问一个4字节数,其访存地址的低两位都应为0。对齐访问的硬件实现较为简单,若支持不对齐访问,硬件需要完成数据的拆分和拼合。但若只支持对齐访问,又会使指令系统丧失一些灵活性,例如串操作经常需要进行不对齐访问,只支持对齐访问会让串操作的软件实现变得较为复杂。以X86为代表的CISC指令集通常支持不对齐访问,RISC类指令集在早期发展过程中为了简化硬件设计只支持对齐访问,不对齐的地址访问将产生异常。近些年来伴随着工艺和设计水平的提升,越来越多的RISC类指令也开始支持不对齐访问以减轻软件优化的负担。

另一个与访存地址相关的问题是尾端(Endian)问题。不同的机器可能使用大尾端或小尾端,这带来了严重的数据兼容性问题。最高有效字节的地址较小的是大尾端,最低有效字节的地址较小的是小尾端。Motorola的68000系列和IBM的System系列指令系统采用大尾端,X86、VAX和LoongArch等指令系统采用小尾端,ARM、SPARC和MIPS等指令系统同时支持大小尾端。

3.寻址方式

寻址方式指如何在指令中表示要访问的内存地址。表2.4列出了计算机中常用的寻址方式,其中数组mem表示存储器,数组regs表示寄存器,mem[regs\]\]表示由寄存器Rn的值作为存储器地址所访问的存储器值。

表2.4 常用寻址方式介绍

除表2.4之外还可以列出很多其他寻址方式,但常用的寻址方式并不多。John L.Hennessy在其经典名著《计算机系统结构:量化研究方法(第二版)》中给出了如表2.5所示的数据,他在VAX计算机(VAX机的寻址方式比较丰富)上对SPEC CPU 1989中tex、spice和gcc这三个应用的寻址方式进行了统计。

表2.5 VAX计算机寻址方式统计

从表2.5可以看出,偏移量寻址、立即数寻址和寄存器间接寻址是最常用的寻址方式,而寄存器间接寻址相当于偏移量为0的偏移量寻址。因此,一个指令系统至少应支持寄存器寻址、立即数寻址和偏移量寻址。经典的RISC指令集,如MIPS和Alpha,主要支持上述三种寻址方式以兼顾硬件设计的简洁和寻址计算的高效。不过随着工艺和设计水平的提升,现代商用RISC类指令集也逐步增加所支持的寻址方式以进一步提升代码密度,如64位的LoongArch指令集(简称LA64)就在寄存器寻址、立即数寻址和偏移量寻址基础之上支持变址寻址方式。

2.4.3 指令操作和编码

现代指令系统中,指令的功能由指令的操作码决定。从功能上来看,指令可分为四大类:第一类为运算指令,包括加减乘除、移位、逻辑运算等;第二类为访存指令,负责对存储器的读写;第三类是转移指令,用于控制程序的流向;第四类是特殊指令,用于操作系统的特定用途。

在四类指令中,转移指令的行为较为特殊,值得详细介绍。转移指令包括条件转移、无条件转移、过程调用和过程返回等类型。转移条件和转移目标地址是转移指令的两个要素,两者的组合构成了不同的转移指令:条件转移要判断条件再决定是否转移,无条件转移则无须判断条件;相对转移是程序计数器(PC)加上一个偏移量作为转移目标地址,绝对转移则直接给出转移目标地址;直接转移的转移目标地址可直接由指令得到,间接转移的转移目标地址则需要由寄存器的内容得到。程序中的switch语句、函数指针、虚函数调用和过程返回都属于间接转移。由于取指译码时不知道目标地址,因此硬件结构设计时处理间接跳转比较麻烦。

转移指令有几个特点:第一,条件转移在转移指令中最常用;第二,条件转移通常只在转移指令附近进行跳转,偏移量一般不超过16位;第三,转移条件判定比较简单,通常只是两个数的比较。条件转移指令的条件判断通常有两种实现方式:采用专用标志位和直接比较寄存器。采用专用标志位方式时,通过比较指令或其他运算指令将条件判断结果写入专用标志寄存器中,条件转移指令仅根据专用标志寄存器中的判断结果决定是否跳转。采用直接比较寄存器方式时,条件转移指令直接对来自寄存器的数值进行比较,并根据比较结果决定是否进行跳转。X86和ARM等指令集采用专用标志位方式,RISC-V指令集则采用直接比较寄存器方式,MIPS和LoongArch指令集中的整数条件转移指令采用直接比较寄存器方式,而浮点条件转移指令则采用专用标志位方式。

指令编码就是操作数和操作码在整个指令码中的摆放方式。CISC指令系统的指令码长度可变,其编码也比较自由,可依据类似于赫夫曼(Huffman)编码的方式将操作码平均长度缩小。RISC指令系统的指令码长度固定,因此需要合理定义来保证各指令码能存放所需的操作码、寄存器号、立即数等元素。图2.7给出了LoongArch指令集的编码格式。

图2.7 LoongArch指令集的编码格式

如图2.7所示,32位的指令编码被划分为若干个区域,按照划分方式的不同共包含9种典型的编码格式,即3种不含立即数的格式2R、3R、4R和6种包含立即数的格式2RI8、2RI12、2RI14、2RI16、1RI21和I26。编码中的opcode域用于存放指令的操作码;rd、rj、rk和ra域用于存放寄存器号,通常rd表示目的操作数寄存器,而rj、rk、ra表示源操作数寄存器;Ixx域用于存放指令立即数,即立即数寻址方式下指令中给出的数。指令中的立即数不仅作为运算型指令的源操作数,也作为load/store指令中相对于基地址的地址偏移以及转移指令中转移目标的偏移量。