3.1. LoongArch指令特性
龙芯基础指令集LoongArch采用RISC风格,是一种具有典型精简指令集特征的指令系统架构。RISC强调“精简”:单条指令功能相对明确,CPU执行一条指令通常需要较少周期;指令编码不采用变长形式,而是使用固定长度,便于CPU快速译码,也符合“常用操作尽量快、少用操作保证正确”的设计思路;同时采用Load-Store结构,只有访存指令可以访问内存,其他指令一般只操作寄存器或立即数。这样的设计既便于硬件高效实现,也有利于结合流水线、多发射、乱序执行等技术提升处理效率。受这些优点影响,x86等非RISC架构也不断吸收RISC思想,例如把复杂指令预译码为多个简单操作,从而更好地利用流水线。典型RISC架构还包括MIPS、ARM、SPARC、PowerPC、RISC-V等。
本节从指令组成与分类、寄存器、指令长度、编码格式等方面,对龙芯基础指令集LoongArch进行介绍。
3.1.1. 指令组成和指令分类
第01章已经说明,计算机语言也遵循语法规范,计算机依靠这些规范理解指令并执行操作。机器语言是计算机能够识别的语言,而指令是机器语言中最小的识别单位。那么,一条机器指令如何组织?通常来说,指令由操作码和操作数组成。操作码规定指令要完成的功能,如加法、减法等;操作数则给出完成该功能所需的数据对象,如寄存器、常数或地址。下面是3条功能不同的龙芯汇编指令:
or $r4, $r0, $r0 # r4 = r0|r0
addi.d $r3, $r3, 16(0x10) # r3 = r3 + 16
jirl $r0, $r1, 0 # jump to address(r1 + 0)
其中,or、addi.d和jirl都是操作码,分别表示或运算、加法运算和跳转操作。以addi.d $r3,$r3,16(0x10)为例,指令名中的i表示立即数(Immediate),说明某个操作数是常数;.d表示该加法指令处理的是64位数据。
操作码之后的部分为操作数。例如,or $r4,$r0,$r0的操作数是寄存器r4和r0,功能是对寄存器r0的值进行或运算,并把结果写入寄存器r4。对于前面的加法指令,操作数则包括寄存器r3和立即数16。
龙芯基础指令集约有300条指令。按功能划分,可分为运算指令、访存指令、转移指令和特殊指令四类。运算指令用于完成加、减、乘、除、移位、逻辑与、逻辑或、逻辑非等操作,示例中的or和addi.d就属于这一类。访存指令负责在内存和寄存器之间搬运数据。转移指令用于改变程序执行流,其作用类似C语言中的if-else、switch、goto等语句,示例中的jirl属于转移指令。特殊指令主要服务于操作系统相关功能,例如系统调用、查询处理器特性、触发断点例外等。第03章将按功能类别对龙芯基础指令集进行详细说明。
3.1.2. 寄存器
寄存器是处理器内部最常用的存储资源。LoongArch中,大部分指令的操作数来自寄存器或立即数。现代指令系统通常会定义一定数量的通用寄存器和浮点寄存器,便于编译器进行指令调度和优化。龙芯指令系统提供32个整数通用寄存器(General-purpose Register,GR)和32个浮点寄存器(Floating-point Register,FR),编号分别为$r0~$r31和$f0~$f31,如图2-1和图2-2所示。
由图2-1可知,LA32架构下通用寄存器位宽为32位,LA64架构下通用寄存器位宽为64位。从指令集角度看,LA32可以看作LA64的子集。以加法指令为例,LA32只提供32位加法指令;LA64既支持32位加法,也支持64位加法。下面两条指令分别表示64位整数加法和32位整数加法。
add.d $r8, $r1, $r2 # 64位加法运算指令,仅适用于LA64
add.w $r8, $r1, $r2 # 32位加法运算指令,在LA32和LA64架构下都适用
在LA64架构中执行32位加法指令add.w r8, r1, r2时,虽然r1、r2和r8都是64位寄存器,但CPU只取r1和r2的低32位参与运算,然后将低32位结果符号扩展为64位,再写入r8。执行add.d r8, r1, r2时,参与运算的是r1和r2的完整64位,结果直接写回r8。
图2-2显示,浮点寄存器与整数通用寄存器不同:无论是LA32还是LA64架构,浮点寄存器位宽均为64位。只有当指令处理单精度浮点数或字整数(32位整型)时,数据才只使用浮点寄存器的[31:0]位,此时[63:32]位为无效值。在龙芯基础指令集中,浮点指令名可以直接体现数据类型。下面两条浮点加法指令中,.s表示单精度浮点加法,.d表示双精度浮点加法。
fadd.s $f3, $f1, $f2 # 单精度浮点数(32位)的加法运算 f3[31:0] = f1[31:0] + f2[31:0]
fadd.d $f3, $f1, $f2 # 双精度浮点数(64位)的加法运算 f3 = f1 + f2
浮点编程还会涉及条件标志寄存器(Condition Flag Register,CFR)和浮点控制状态寄存器(Floating-point Control and Status Register,FCSR)。前者保存浮点比较结果,后者记录浮点运算中的非法操作、除零、溢出等异常状态。第04章将进一步介绍这两类寄存器。
3.1.3. 指令长度和编码格式
指令定长是RISC架构的重要特征之一。龙芯基础指令集中,每条指令固定编码为32位。所谓指令编码,是指操作码和操作数在这32位中的排列方式及所占位数。图2-3给出了龙芯架构中9种典型编码格式。
从图2-3可以看到,龙芯架构的编码风格是操作码(opcode)位于高位,目的操作数(rd)位于低位,源操作数或立即数位于中间。不同格式中,操作码占用的位数并不相同。例如,两个操作数格式(2R-type)使用22位表示操作码,三个操作数格式(3R-type)使用17位表示操作码。寄存器字段均占5位,如图中的ra、rk、rj。立即数字段则可能占12位(I12)、14位(I14)、16位(I16)、21位(I21)或26位(I26)。
需要说明的是,图2-3并未覆盖龙芯架构的所有编码格式。少量指令采用不同于这9种典型格式的编码方式,但数量较少,差异也不大,不会明显增加开发负担。例如,系统调用指令syscall code、中断指令break code等只有1个操作数。在应用级基础整数指令中,非三操作数指令总数不到10条。龙芯指令集的大多数指令采用3个操作数格式(如3R-type、2RI8-type、2RI12-type等),少数指令采用2个操作数格式(2R-type、I26-type)或4个操作数格式(4R-type)。
每条指令都有唯一操作码。图2-4列出了龙芯基础指令集中部分指令的操作码值和编码规则,其他指令的码值可查阅龙芯架构参考手册末尾的指令码表。
以图2-4中的ADDI.D指令为例,它的操作码固定为0b0000001011。将汇编指令addi.d $r3,$r3,-16(0xff0)编码为机器指令,可得到如下二进制结果:
0b0000001011 000000010000 00011 00011
该二进制值对应的十六进制表示为0x02c04063。
3.1.4. 指令汇编助记格式
规范、清晰的汇编助记格式有助于开发人员理解指令含义,也能提高编写效率。龙芯指令的汇编助记格式主要由指令名和操作数组成。
指令名可分为“指令名前缀+指令名+指令名后缀”三部分。指令名前缀用于区分向量指令、整数指令和浮点指令:128位向量指令以字母“V”开头,256位向量指令以“XV”开头,浮点指令以“F”开头,其余无特殊前缀的通常为普通整数指令。指令名本身与其他架构类似,常使用能表达功能的英文单词或缩写,例如add表示加法,sub表示减法。指令名后缀用于说明操作对象类型。整数类型后缀包括.B、.H、.W、.D、.BU、.HU、.WU、.DU,分别表示字节、半字、字、双字、无符号字节、无符号半字、无符号字、无符号双字;浮点类型后缀包括.S和.D,分别表示单精度浮点数和双精度浮点数。
寄存器操作数的助记形式比较直接:rN表示通用寄存器,fN表示浮点寄存器,vN表示向量寄存器,xN表示扩展向量寄存器。这里的N为数字,表示第N+1号寄存器。LoongArch的通用寄存器、浮点寄存器和向量寄存器均各有32个,因此可分别表示为r0~r31、f0~f31、v0~v31、x0~x31。例如,r3表示整数通用寄存器组中的第4号寄存器。
掌握这些助记规则后,读者可以较容易理解下面几条汇编指令。
add.w $r8, $r1, $r2 # 32位整数加法运算
fadd.s $f8, $f1, $f2 # 单精度浮点数加法运算
vadd.b $v8, $v1, $v2 # 128位向量整数加法运算,数据类型为字节
xvadd.b $x8, $x1, $x2 # 256位向量整数加法运算,数据类型为字节
在龙芯自主指令集LoongArch中,带立即数的指令通常采用“指令名+i”的命名方式。例如:
addi.w $r8, $r1, 16 # 32位整数加法运算
Tip
龙芯自主指令集LoongArch不区分指令名大小写,即add.w和ADD.W都是合法且可被识别的写法。不过,建议统一使用小写。
3.1.5. 符号扩展
在龙芯指令集中,计算类指令使用立即数前,需要先进行符号扩展或无符号扩展(也称零扩展)。扩展结果可能是32位或64位,因此常见形式包括32位符号扩展、32位无符号扩展、64位符号扩展和64位无符号扩展。32位符号扩展是把n位立即数(n小于32)的高32-n位填充为该立即数最高位;32位无符号扩展则把高32-n位填充为0。64位符号扩展和64位无符号扩展规则类似,只是扩展目标位宽变为64位。在LA32架构下,立即数通常扩展为32位;在LA64架构下,n位立即数通常扩展为32位或64位。表2-1列出了两个16位立即数0x8000和0x1000分别符号扩展、零扩展到32位和64位后的结果。
16位立即数 |
0x8000 |
0x1000 |
符号扩展至32位 |
0xFFFF8000 |
0x00001000 |
零扩展至32位 |
0x00008000 |
0x00001000 |
符号扩展至64位 |
0xFFFFFFFFFFFF8000 |
0x0000000000001000 |
零扩展至64位 |
0x0000000000008000 |
0x0000000000001000 |
例如,32位加法指令addi.w rd, rj, si12会先把12位有符号立即数si12符号扩展到32位,再与rj相加。64位加法指令addi.d rd, rj, si12则把si12符号扩展到64位后参与计算。逻辑与指令andi rd, rj, ui12在LA32架构下会把12位无符号立即数ui12零扩展到32位,再进行逻辑与;在LA64架构下,则将ui12零扩展到64位后再参与运算。
在LA64架构中,32位操作数计算指令得到的结果通常也要先进行64位符号扩展,再写入目的寄存器。例如执行addi.w rd, rj, si12时,处理器先将si12符号扩展到32位,再与寄存器rj的低32位相加,最后把低32位结果符号扩展为64位并写入rd。
3.1.6. 寻址方式
寻址方式用于说明如何根据指令中的操作数得到要访问的地址。根据操作数格式不同,LoongArch支持5种寻址方式:寄存器寻址、立即数寻址、基址+立即数偏移寻址、基址+寄存器偏移寻址和相对寻址。表2-2列出了这些方式的格式和含义,其中符号#表示立即数,数组regs[]表示寄存器,数组mem[]表示存储器。
寻址方式 |
指令示例 |
格式说明 |
|---|---|---|
寄存器寻址 |
ADD R1,R1,R2 |
regs[R1] = regs[R1] + regs[R2] |
立即数寻址 |
ADD R1,R1,#2 |
regs[R1] = regs[R1] + 2 |
基址 + 立即数偏移寻址 |
LD R1,R2,#100 |
regs[R1] = mem[regs[R2] + 100] |
基址 + 寄存器偏移寻址 |
LD R1,R2,R3 |
regs[R1] = mem[regs[R2] + regs[R3]] |
相对寻址 |
BL #100 |
pc = PC + 100 |
寄存器寻址表示操作数保存在寄存器中。格式ADD R1,R1,R2的含义是将R1中的值与R2中的值相加,并把结果写回R1。寄存器寻址在LoongArch中应用广泛,算术运算、逻辑运算、移位运算、位操作等指令都经常使用这种方式。
立即数寻址表示操作数直接写在指令中,处理器取出指令时即可取得该操作数。格式ADD R1,R1,#2表示把R1中的值与立即数2相加,并把结果写回R1。LoongArch中的加法指令,逻辑与、或、异或指令,以及左移、右移等移位指令,都支持立即数寻址。由于LoongArch指令长度固定为32位,指令中可容纳的立即数大小会受到限制,常见长度为12位。
基址+立即数偏移寻址中,指令里的寄存器保存地址基址(可记为base_reg),立即数保存地址偏移(可记为offset_imm),最终访问地址为base_reg+offset_imm。示例LD R1,R2,#100表示从内存中读取一个数据到R1,内存地址由R2的值加立即数100得到。
基址+寄存器偏移寻址中,地址基址(base_reg)和地址偏移(offset_reg)分别保存在两个寄存器中,最终访问地址为base_reg+offset_reg。示例LD R1,R2,R3表示从指定内存地址读取数据到R1,该地址由寄存器R2和R3的值相加得到。LoongArch访存指令同时包含基址+立即数偏移和基址+寄存器偏移两类寻址方式。
相对寻址与基址+立即数偏移寻址相似,但它以程序计数器PC的当前值作为基址,以指令中的立即数作为偏移量,二者相加得到有效地址。示例BL #100表示跳转到PC+100对应的位置。相对寻址主要用于LoongArch的分支指令以及有条件或无条件跳转指令。