LA32R中的用户态整型指令
LA32R的机器语言与汇编语言表达
这一节我们讨论几个与指令的机器语言和汇编语言两种表达形式相关的问题。
我们常见的指令表达形式有两种:一种是给CPU硬件“看”的,我们称之为机器语言;另一种是给编程人员看的,即所谓汇编语言。例如,LA32R中功能行为是“1号寄存器的值加上2号寄存器的值结果写入3号寄存器”这样一条指令,其机器语言表达是一个32位的比特串00000000000100000000100000100011,其可以被汇编器直接识别的汇编语言表达是add.w $r3, $r1, $r2或者add.w $sp, $ra, $tp。两种汇编语言表达的差异主要体现在寄存器操作数,第一种是从指令集手册规范直接得到的最基础表达形式,第二种则涉及到ABI中定义的寄存器规范。
LA32R的机器语言表达
机器语言中的指令直观上就是一个二进制比特串,进一步对于RISC指令集来说,构成每个指令的的比特串都是固定长度。LA32R指令集采用RISC设计理念,其指令长度固定为32位。这32位的比特串,哪些用来对应操作类型(如加减还是移位),哪些用来寻址操作对象(如哪个寄存器或者内存中的哪个地址),需要有个规则,这就是我们通常讲的指令编码格式。LA32R指令手册的表1-1给出了该指令集的典型编码格式。正如手册中提到的,这9种是典型编码格式,并不是全部编码格式。同MIPS和RISC-V指令集的编码格式相比,LA32R的种类要更多,格式上好像也有差异,对已经熟悉MIPS或RISC-V教学和设计的人来说可能有些不习惯。针对这方面问题,下面提供一些我们的观点供参考。
在教学方面,LA32R具有大多数RISC指令集指令编码的设计特征,体现在三个方面:一是定长以简化取指访存行为和译码电路定位指令边界的逻辑复杂度,二是操作码采用层次化变长编码以适应不用数目操作数的情况,三是操作数位置固定从而使得译码过程中操作码解析和操作数提取可以并行处理。这三方面特征,定长和操作数位置固定是比较容易理解的,层次化变长编码可能还是会让人困惑:MIPS或者RISC-V的指令编码有opcode、func域,这是层次化,但没看出变长;LA32R中的opcode是有好几个长度(变长),但没看出层次化。其实,一套指令集中指令操作码主要职责就是能够唯一的标识出这条指令,又由于操作数信息占据的位置有长有短,因此要在指令码整体长度固定的情况下定义尽可能多的指令,操作码很自然的会采用前缀编码方式来提升其编码效率。又因为增减一个操作数会一次性增减几个比特(如一个寄存器操作数占5个比特),所以采用前缀编码形式的操作码又呈现出一种层次化的形态。
在设计方面,真正会关注指令码细节的无外乎CPU结构设计人员和汇编器开发人员。其中汇编器中识别、生成指令码的工作是软件自动进行的,可以认为还是机器在看,所以按照汇编器指令编码模板的格式,将LA32R指令手册附录B中的指令码信息填入即可。可能会因为指令编码格式感到困难的可能是一些直接用Verilog等传统HDL语言开发CPU译码部件设计的读者,因为原先MIPS或RISC-V指令集将操作码信息层次切分到opcode、funct等域中,会使得设计者很自然地在写译码逻辑时按照这种层次化风格来译码。那么LA23R这种情况怎么办呢?其实,设计者还是可以自己定义一个切分的层次,再按照层次化风格来译码。譬如chiplab项目中IP/myCPU/目录下的OpenLA500处理器核的译码逻辑(id_stage.v)就是这种风格。不过,如果对于生成电路质量不是要求那么高且对于现代EDA综合工具提取公共子表达优化有充分的信心,直接写出如下风格的代码也未尝不可:
最后再探讨一个阅读使用习惯的问题,即有些人问LA32R的用户手册中为什么不像其它指令集手册一样,将每条指令的指令码和它的功能表述逐条放在一起。对此我们的考虑是,指令码作为主要给机器看的指令表达,通常设计开发人员并不需要查看。目前我们能想到的典型应用场景,主要是前面提到的CPU译码部件开发和汇编器开发工作,此外就是人工翻译指令码或者从指令码解析指令功能这种极特殊的场景。以我们的实践经验来看,这些场景下都是所有指令的编码集中在一处比分散在手册各处更利于使用。
LA32R的汇编语言表达
本小节并不展开介绍LA32R汇编语言,只是讨论初学者在进行LA32R汇编开发时容易产生困惑的寄存器表达问题和伪指令问题。
寄存器号前是否要加$?
假设当前要表达一个将第12号和第13号通用寄存器值相加结果写入第14号寄存器的操作,已知这需要使用add.w指令,因为指令手册中add.w写的指令格式是add.w rd, rj, rk,所以直观上汇编代码应该写成add.w r14, r12, r13,但是这样写的代码编译是会报错的。目前LA的工具链规定了指令中通用寄存器号的r前面必须添加$符号,即本例中的指令应该写成add.w $r14, $r12, $r13。由于我们进行汇编或反汇编都离不开工具链,所以汇编器对寄存器操作数书写形式的要求就成了一种事实上的规范。类似的情况对于浮点指令也存在,即可以通过编译的代码中浮点寄存器要写成$f##而不是f##。
根据ABI寄存器使用约定的寄存器别名前是否要加$?
LoongArch的ABI中定义了寄存器使用约定,目前大多数读者对这部分内容的了解是来自于《计算机体系结构基础(第3版)》中的内容。例如,书中4.1.1小节表4-1中指出不用保存的暂存器是r12 ~ r20,它们的别名是t0 ~ t8。不过,一些读者实际看到的汇编源程序中却既有写成t##又有写成$t##,然后自己写程序的时候,写成t##有时候编译不报错有时候又报错。那么到底哪种写法是正确的呢?其实LoongArch工具链原生支持的只有$t##这种写法。之所以有的地方能写成t##而编译通过,实际上是因为这些代码中重新定义了一批形如t##的宏。例如,chiplab项目中software/func/目录下的汇编测试代码包含了regdef.h这个头文件,其中包含了如下的宏定义:
#define zero $r0
#define ra $r1
#define tp $r2
#define sp $r3
#define a0 $r4
#define a1 $r5
#define a2 $r6
#define a3 $r7
#define a4 $r8
#define a5 $r9
#define a6 $r10
#define a7 $r11
#define v0 $r10
#define v1 $r11
#define t0 $r12
#define t1 $r13
#define t2 $r14
#define t3 $r15
#define t4 $r16
#define t5 $r17
#define t6 $r18
#define t7 $r19
#define t8 $r20
#define x $r21
#define fp $r22
#define s0 $r23
#define s1 $r24
#define s2 $r25
#define s3 $r26
#define s4 $r27
#define s5 $r28
#define s6 $r29
#define s7 $r30
#define s8 $r31
汇编程序中出现的指令在指令手册中查不到是怎么回事?
初学者可能会在一些LA32R的汇编程序中看到诸如move、li.w、jr、la这样的指令,但是翻遍指令集手册却看不到这些指令的定义。其实这些指令是LA32R的汇编器支持的伪指令。之所以称其“伪”是因为它们并不“真”的需要在CPU上专门实现,它们其实是一些固定了某些操作数表达形式的指令(序列)。例如,伪指令move $r1 $r2实际上会被汇编器自动翻译成or $r1, $r2, $r0,伪指令jr $ra会被汇编器自动翻译成jirl $r0, $ra, 0。汇编器定义伪指令是为了在不增加硬件实现开销的情况下丰富汇编编程可用的指令,提升汇编开发的效率。本手册第1部分后面将会结合具体的应用场景陆续介绍LA32R汇编开发时常用的伪指令,同时在本手册第2部分将列出目前LA32R汇编器支持的所有伪指令供大家查阅。
这里再和熟悉MIPS汇编的读者探讨一下,为什么我们用“伪指令”而不是MIPS体系下的“宏指令”来称呼上面这些指令。主要还是MIPS的ABI中预留了一个通用寄存器at专供汇编器使用,有了这个寄存器存储临时中间变量,汇编器就可以构造出更复杂的指令序列,其对应的宏指令确实功能更丰富,也配得上macro这个词。相比之下,LA中的伪指令尽管也有对应两三条真实指令序列的,但大多数只对应一条,某种程度上更像是一个“别名”,因此用pseudo这个词更合适一点。