指令集架构
以下笔记 来自阅读:Hennessy J L, Patterson D A.
Computer Architecture: A Quantitative Approach 6th Edition. 2019.
指令集架构指代程序可见的指令集,也是软件和硬件的分界线。下面以实际的例子说明指令集架构的具体7个方面:
-
指令集架构的分类:几乎所有今天的ISA都被归类为通用寄存器架构,其操作数不是寄存器就是内存位置。80x86有16个通用寄存器、16个保存浮点数据的寄存器;RISC-V有32个通用和浮点寄存器;
从通用寄存器架构向下划分,可以再分为两类。一类是 register-memory
ISA ,比如80x86,可以在很多指令中直接访问内存;一类是
load-store ISA,比如 ARMv8 和 RISC-V
,只能通过 load 和 store
指令访问内存。所有从1985年之后发布的ISA都是 load-store
ISA。下表是 RISC-V ISA的寄存器示例:
寄存器 | 名称 | 用途 |
---|---|---|
x0 | zero | 常量 '0' |
x1 | ra | 返回地址 |
x2 | sp | 栈指针(指向栈下一个空闲区域) |
x3 | gp | 全局指针(指向全局变量区域) |
x4 | tp | 线程指针(指向线程局部变量区域) |
x5-x7 | t0-t2 | 临时存储 |
x8 | s0/fp | 保存的寄存器/栈帧指针(指向栈帧基址) |
x9 | s1 | 保存的寄存器 |
x10-x11 | a0-a1 | 函数参数/返回值 |
x12-x17 | a2-a7 | 函数参数 |
x18-x27 | s2-s11 | 保存的寄存器 |
x28-x31 | t3-t6 | 临时存储 |
f0-f7 | ft0-ft7 | 浮点 临时存储 |
f8-f9 | fs0-fs1 | 浮点 保存的寄存器 |
f10-f11 | fa0-fa1 | 浮点 函数参数/返回值 |
f12-f17 | fa2-fa7 | 浮点 函数参数 |
f18-f27 | fs2-fs11 | 浮点 保存的寄存器 |
f28-f31 | ft8-ft11 | 浮点 临时存储 |
- 内存寻址:基本上所有的台式机和服务器,包括 80x86 、ARMv8 和 RSIC-V ,使用字节编址来访问内存操作数。某些架构,比如 ARMv8, 要求访存对象必须对齐。而 80x86 和 RISC-V 不需要对齐,但是如果对齐,访问速度会更快。
- 寻址模式:寻址模式描述了寄存器操作数、立即数(常量)操作数和内存对象的地址。RISC-V 寻址包括:寄存器、立即数(常量)和位移(displacement, 寄存器的值加上一个常量的偏移构成了内存地址)。80x86支持上述三种模式,另外加上三种位移的变种:1)无寄存器(绝对地址) 2)两寄存器(基地址+索引+位移)3)两寄存器(基地址+索引*操作数大小+位移)。
- 操作数类型和大小:和大多数ISA一样,80x86、ARMv8和RISC-V支持8比特、16比特、32比特、64比特的操作数大小,也支持IEEE 754 32位(单精度)和64位(双精度)浮点数。80x86 也支持80比特的浮点数(扩展双精度)。
- 操作: 操作一般可分为:数据传输、算术逻辑、控制和浮点几类。下表是RISC-V的数据传输和算术指令示例:
指令类型/操作码 | 指令含义 |
---|---|
数据传输 | 在寄存器和内存,或者整数寄存器和浮点寄存器、特殊寄存器间搬移数据。只有内存地址是由12比特的偏移加上通用寄存器的值构成 |
lb, lbu, sb | 加载字节(byte 8比特)、加载无符号字节、存储字节 |
lh, lhu, sh | 加载半字(half word 16比特)、加载无符号半字、存储半字 |
lw, lhw, sw | 加载字(word 32比特)、加载无符号字、存储字 |
ld, sd | 加载双字(double word 64比特)、存储双字 |
flw, fld, fsw, fsd | 记载单精度浮点,加载双精度浮点、存储单精度浮点、存储双精度浮点 |
fmv.S.X, fmv.D.X, fmv.X.S, fmv.X.D | 从整数寄存器拷贝到浮点寄存器,或从浮点寄存器拷贝到整数寄存器。S代表单精度,D代表双精度 |
csrrw, csrrwi, csrrs, csrrsi, csrrc, csrrci | 读取计数器并写入状态寄存器,计数器包括:时钟周期数、时间、退役指令数量等。 |
算术/逻辑 | 通用寄存器数据上的整型或逻辑数据运算 |
add, addi, addw, addiw | 加, 加立即数(12比特), 有符号加32比特并扩展至64比特, 加立即数(32比特) |
sub, subw | 减, 减32比特 |
mul, mulw, mulh, mulhsu, mulhu | 乘, 乘32比特, 乘上半部(16比特), 无符号乘有符号上半部, 无符号乘上半部 |
div, divu, rem, remu | 除, 无符号除, 余数, 无符号余数 |
divw, divuw, remw, remuw | 除余低32位, 有符号扩展 |
and, andi | 与, 与立即数 |
or, ori, xor, xori | 或, 或立即数, 异或, 异或立即数 |
lui | 加载立即数到寄存器上半部 (31-12比特位)并进行符号扩展 |
auipc | 加立即数的上半部 (31-12位,余下低12位为0) 到PC; 和JALR 配合使用转移控制到任何32位地址 |
sll, slli, srl, srli, sra, srai | 移位; 逻辑左移, 逻辑右移; 使用立即数或者变量 |
sllw, slliw, srlw, srliw, sraw, sraiw | 移位低32位, 有符号扩展 |
slt, slti, sltu, sltiu | 小于情况下设置; 小于情况下设置; 有符号和无符号 |
- 控制流指令:实际上所有的ISA,都支持有条件分支、无条件的跳转、函数调用和返回。80x86、ARM、RISC-V 均使用PC相对寻址,分支地址是通过PC加上地址字段构成的。但也有也许的不同,RISC-V 有条件分支(BE,BNE,etc)是检查寄存器的内容,而80x86和ARM检查的是算术或者逻辑运算副作用设置的比特位。ARM和RISC-V将返回地址放到寄存器中,而80x86调用(CALLF)将返回地址放到内存栈里。下表是RISC-V的控制指令示例:
指令类型/操作码 | 指令含义 |
---|---|
控制 | 有条件分支和跳转;PC相对寻址或者通过寄存器 |
beq, bne, blt, bge, bltu, bgeu | 通用寄存器相等/不相等/小于/大于或等于时分支;有符号或无符号 |
jal, jalr | 跳转并链接;保存返回地址,目标相对PC寻址或者使用寄存器;如果x0是目标寄存器,则按照跳转来运行 |
ecall | 向执行环境(一般为OS)下发请求 |
ebreak | 调试软件用来将控制交回调试环境 |
fence, fence.i | 线程间同步用以保证内存访问的顺序;同步指令及store使用的数据到指令内存 |
- 编码ISA: 在编码时有两种选择:定长和变长。所有的ARMv8和RISC-V指令的长度都是32比特,这样的定义方便了指令的解码过程。80x86是变长编码,指令长度1到18个字节。变长指令相对于定长指令可以占用更少的内存空间,因此为80x86编译的程序一般比RISC-V编译后的程序要小。指的注意的是,我们前述提到的ISA在设计上的选择都将影响指令在二进制形式下的编码方式。例如,由于在一个指令中,寄存器字段和寻址模式字段都可能反复出现,因此,寄存器数量和寻址模式都对指令大小有着重要的影响。(ARMv8和RISC-V后面都增加了支持变长的扩展:Thumb-2 和 RV64IC,支持16比特和32比特指令混合使用。通过这种方式降低程序大小。通过这种压缩方式编译的程序要比80x86要小)下图是RISC-V指令编码的示例: