以下笔记 来自阅读: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指令编码的示例:
