Chapter 2. Instruction: Language of Machine¶
约 1952 个字 248 行代码 1 张图片 预计阅读时间 13 分钟
精简指令集
前面我们讨论过,计算机的 Performance 受每条指令执行的时间影响,而不同的 CPU 可能使用不同的指令架构集(ISA)。
RSIC-V 是 UC Berkeley 大学设计的第五代RISC芯片,其特点是开源、架构简单、模块化设计,是本课程所要讲解的 ISA。
寄存器¶
RISC-V 一共有 32 个寄存器,每个寄存器宽度均为 64 位,命名从 x0
到 x31
不同于8086,RISC-V中 1 word = 32 bit
,所以寄存器大小为 doubleword
x0
保持常数0
,常用于函数返回时作为无意义地址的接收方:jalr x0, 0(x1)
x0
的读写是无效的,处理器并不会去读写它的值
x1
用来保存返回地址,即return address
。- 在调用函数前,使用
jal x1, FUNC_ADDR
保存PC当前指令的下一条指令地址,即PC+4
。 - 调用的函数返回时,使用
jalr x0, 0(x1)
返回调用指令的下一条指令继续执行。
- 在调用函数前,使用
x2
即栈指针,始终指向栈顶元素。栈从高地址向低地址增长。x3
指向全局变量和静态数据区的起始地址,称为global pointer
,用于访问和管理全局变量。x4
是线程寄存器,指向当前线程的局部寄存器,即线程私有的数据区域。x5-x7
以及x28-x31
是临时寄存器,通常用于存储计算的中间结果或临时变量,它们是调用者保存的寄存器。x8-x9
以及x18-x27
也可以用来存储临时数据,不过它们是被调用者保存的寄存器,即被调用函数在开头和结尾分别需要入栈和出栈以恢复它们的值。x8
一般也用作帧指针frame pointer
,指向栈的底端
x10-x17
是参数寄存器,函数调用的前八个参数会存储在这些寄存器中,但如果参数超过 8 个就需要放到栈中传递(如果参数为 8 bytes,sp+8
是第九个参数,sp+16
是第十个参数...)。同时,过程的结果也会放到这些寄存器上,对于C语言这种只有一个返回值的语言,可能只会用到x10
。
Preserved on call
是否保证调用前后这些寄存器的值不变。如果为 yes
,则被调用函数开头结尾分别要将这些寄存器入栈出栈以恢复它们的值;如果为 no
,则需要主函数上自行入栈出栈恢复值。
过程调用
RISC-V 使用 jal x1, ProcedureAddress
来调用子程序,使用 jalr x0, 0(x1)
返回母程序。在程序调用中,RISC-V 必须使用额外的指令在调用前将调用者保存的寄存器压入内存栈,在调用后将这些寄存器数据弹出内存栈,从而保证这些数据的不变性。
指令格式¶
不同于 8086 无定长的机器码,RISC-V 的指令都是 32 bit ,且有固定格式的。
RISC-V 指令格式如下:
opcode
: 指 operation 的编码,大部分时候需要和funct3
和funct7
一起决定指令的种类。rd
: 即 Destination Register ,目标寄存器。funct3
: 3 bit 的 function code,相当于 additional opcode。rs1
: 即 First Source Register ,第一个源寄存器。rs2
: 即 Second Source Register ,第二个源寄存器。funct7
: 7 bit 的 function code,相当于 additional opcode。i
: 立即数,需要注意是有符号数,例如i[11:0]
的范围是 \(-2^{11}\) ~ \(2^{11}-1\)
可以观察到 I-Type 有两个条目,下面那个条目只对应 slli
,srli
,srai
,因为立即数移位操作并不可能对一个 64 位寄存器进行大于 63 位的移位操作,因此 immediate 中只有后 6 位能实际用上,因此前六位可以用来当作额外的操作码字段。
UJ 格式无条件跳转指令 jal
和 SB 格式条件跳转指令 branch
注意到,这两个格式指令的立即数没有最低位,这是因为地址偏移量必须是 2 的整数倍,即最低位默认看作 0
- Offset of
jal
: \((-2^{20}, 2^{20}-2)\) - Offset of
branch
: \((-2^{12} , 2^{12}-2)\)
考察对 IS 理解的题目
假设有一指令 rpt x29, loop
的效果为 x29
自减 1
,若 x29>0
,则跳转到 loop
。如果该指令在 RISC-V 指令集内,它应该是什么格式的。
答案其实是 U-type ,因为它只用到一个 rd 寄存器和一个立即数地址。
RISC-V 使用 Branch
系列指令进行跳转,举一例演示其应用:
同时,也可以使用无条件跳转指令 jal
和 jalr
(Jump And Link Register) 来达成C语言 switch 语句的效果:
与8086不同,RISC-V中跳转指令的立即数取值并不是目标地址减去下一条指令的地址
(21-22 Final) 如何在不适用额外寄存器的情况下交换 x10
和 x11
的值
杂项¶
大数加载¶
由于受指令宽度限制,立即数宽度不能超过 12bit,但是这个大小限制并不能涵盖计算机的常规工作范围。为此,RISC-V 通过组合 addi
和 lui
(Load Upper Immediate) 指令来实现存储 32bit 大小立即数:
lui
指令的作用为读取一个 20bit 的立即数,存储进寄存器低 32 位的高 20 位,左侧 32 位全部填充 bit 31(类似符号扩充),右侧 12 位全部填充 0,其指令格式为:
若 addi
所加载的 12bit 立即数的最高位为 0,那么很明显符合逻辑:
但是由于 RISC-V 的立即数都视为有符号数,若 addi
加载的数最高位为 1 ,该立即数会被视为负数,并对其进行 sign extension(即用 1 填充至 64bit),这种情况相当于额外增加了 0xFFFFF000
,为了补偿这个错误,需要加上 0x00001000
抵消,即对 lui
的立即数加一。例如,加载 976 * 212 + 2304 = 4000000:
在 Compiler Explorer 中用 RISC-V(32bit) gcc 编译下列代码:
可以看到 a = 4000000
对应的汇编代码为:
(23-24 Final) 将 0x12345678ABCDEF
加载到寄存器 x10
中
寻址¶
采用小端寻址 little endian 。
同时,RISC-V没有要求要字节对齐
嵌套调用¶
斐波那契数列:
当调用函数时,使用栈来存储函数返回地址、传递的参数(且 x10
还充当 return 值)。例如,将下列递归计算斐波那契数列的函数转换成 RISC-V 汇编语言:
完全优化版本:
无优化,但是易于理解的版本:
阶乘:
From Minjoker
读取和存储时都要注意位宽
对基址为 a0
的 int
型数组,取第 i
个元素,需要使用 load word
指令:lw rd, i*4(a0)
像我们递归使用存 64bit 寄存器的值,就使用 load doubleword
互相调用,存在两个ra:
更严格的来讲,对于过程开头开辟栈空间部分:
- 需要保留的寄存器
- Saved Registers:
x8-x9
,x18-x27
(s0,s1...) - Return Address:
x1
(ra) - Stack Point:
x2
(sp) - Frame Point:
x8
(tp) - Stack Above:
sp
之上的数据
- Saved Registers:
- 不需要保留的寄存器
- Argument Registers:
x10-x17
(a0,a1,...) - Temporary Registers:
x5-x7
,x28-x31
(t0,t1,...) - Stack Below:
sp
之下的数据
- Argument Registers:
(23-24 Final) 使用了没教过的 rem 和 div 指令???