深入剖析RISC-V指令:类型、编解码与工作原理
什么是指令
本篇文章将以RISC-V 32位处理器为例,详细为大家介绍什么是指令、指令的类型、指令的编解码,揭示计算机工作的秘密。
我们在《程序之下:计算机的三层抽象与性能优化的底层逻辑》中提到过,想要让计算机能够执行目标任务,就必须用计算的话,告诉它该如何执行。我们所写的所有高级语言都会被转成计算机能够理解的语言–指令(Instruction)。
指令是01字符串,它告诉计算机该执行什么操作(operation),以及操作的对象(operands)。
指令的类型
在开始之前,请聪明的你思考一个问题:假如你是一个机器人,你应该抽象出那些特质,才能够让你表现得和真实的人类一样?
在这里我给出我的个人看法:信息获取(眼睛、鼻子、耳朵、嘴巴)、信息处理与决策(大脑)、执行行动(四肢)。这个问题,其实很有启发性,因为**计算的工作,其实也可以抽象成一些列行为,这些行为的排列组合可以构成可表述的任意复杂的任务。**在这里我特别想强调可表述,这是因为一旦人类的特质,如创新、幽默、感性、情绪等可以用数学的公式表达。这就意味着,可以创建出具有人类的特质的机器人,那么那个时候或许离通用人工智能就不远了。
计算机的行为可以分为五大类,相应地也就对应了不同的指令类型,它们分别对应为:计算(Arithmetic)、数据搬运(Data Transfer)、逻辑运算(Logical)、移位(Shift)、决策(Conditional branch、 Unconditional branch)
下图为常见的RISC-V 汇编语言,知道了汇编语言其实也就知道了指令,不过在告诉你汇编与指令之间的转换, 即指令的编解码之前,咱还需要在知识的海洋里泡澡。
所有运算里,加法或许是最简单的的运算了,它们是对寄存器直接操作,从寄存器里面读取值,然后存放到寄存器里。
add x5, x6, x7 //x5= x6 + x7 ,先将寄存器x6位置的所存值与寄存器x7位置的值相加,然后在存入到寄存器x5
sub x19, x5, x6 //x19 = x5-x6 ,先将寄存器x5位置的所存值与寄存器x7位置的值相减,然后在存入到寄存器x19
编译器的工作是将程序变量与对应的寄存器相关联,以下面赋值的语句位列
f = (g+h)-(i+j)
我们假设分配给 f, g, h, i, j对应的寄存器分别为x19,x20,x21,x22,x23;x5,x6为两个临时变量寄存器。 那么编译后的RISC-V代码对应为
add x5, x20,x21 // x5 = g+h
add x6, x22, x23 //x6 =i+j
sub x19, x5,x6 //f=(g+h)-(i+j)
当然,除了基本的加减法运算,数据搬运型指令其实也非常重要,它们负责寄存器和内存之间数据的交换与传递。有两类常用的指令,分别对应为 ld(用于载入) 和 sd(用于存储)。它们将内存对应位置的数据载入到特定寄存器 或者 将寄存器的值存储在内存特定位置
假设变量 h存放在寄存器x21中,数组A的基址存放在寄存器x22中,那么
A[12] = h+A[8]
的汇编语言如下
ld x9, 64(x22) // 将A[8]处对应的载入到临时寄存器x9上
add x9, x9 x21 // x9 =h+A[8]
sd x9, 96(x22) //将h+A[8]的值载入到A[12]
提醒: 对内存的处理时(想想数组),常常用基地址(首地址)+偏移量 的方法来存放或者载入数据
数组中对应的一个偏移量,对应一个字节的数据(8位),所以A[8]–>A[0]+8x8–> ld x9, 64(x22)
到这里,你已经登堂入室了,可以进入下一个部分:指令编解码。
指令编解码
生活中常常会出现这样一个场景:人群中,你听见有人叫你的名字,你立刻四处张望,试图确定是谁。那么这里就有一个很有意思的话题了:别人叫你的名字,你却答应了。这是否意味着,你的名字等于你呢?换言之,你等于你的名字。
从哲学的角度,或许会扯到一大堆符号学让人红温的东西。简单来说,你的名字是对你的编码,,它是你的标签,每当有人使用这个标签的时候,你下意识地认为是你。我们也可以举一个程序员都懂的东西,你在赋予变量的值的时候,你实际上是在对变量贴上了一个标签。
指令其实也是一样,它是将字符串拆分成不同有效字段,不同有效字段被赋予不同的含义。
下图就是RISC-V R型指令的格式, 该指主要用于对寄存器经行操作
-
opcode :操作码,用于表示指令操作和指令格式
-
rd : 目的操作数寄存器,用来存放操作结果
-
funct3 :辅助操作码,用于辅助识别操作类型
-
rs1 :第一个源操作寄存器
-
rs2 : 第二个源操作寄存器
-
funct7 : 另外一个操作码字段
说明
-
7 + 5 +5+3+5+7 =32; 对应了32bit处理器
-
2^5 =32; 源操作寄存器以及目的操作寄存器的数据来去方向覆盖到32个寄存器
-
操作码,funct3,funct7 一起决定了指令的类型和操作方式, 而 rs1,rs2, rd 则表示操作的对象
这里举一个列子,演示从汇编到机器码的过程
add x9,x21,x9
- 指令为 add, 所以 funct7 = 0000000, funct3=000 两者一起表示操作类型; opcode=0110011(表示指令类型)
- 目的操作寄存器为 x9, 也就是第9个寄存器, rd= (9)10 = (01001)2
- 同理 rs1=(21)10=(10101)2, rs2= (9)10 = (01001)2
- 汇编转换成机器语言为: 0000000_01001_10101_000_01001_0110011(此处使用下划线为了方便阅读,也可以将32位二进制转为8位16进制)
针对不同的操作,需求不一样,因而指令的类型也不一样,对于立即数加法(add)和装载(ld),其格式如下
再举一个列子
数组A的首地址存在x10中,
x9 = A[30] 对应的机器码为?
- 对应的汇编语言为 ld x9, 240(x10)
- I型指令,opcode=0000011
- 载入数据, funct3=011
- 原操作寄存器x10, rs1=(10)10=(01010)2,目的操寄存器作x9, rd=(9)10=(01001)2
- 偏移地址 (240)10=(000011110000)2
- 所以机器码为 : 000011110000_01010_011_01001_0000011(此处使用下划线为了方便阅读,也可以将32位二进制转为8位16进制)
指令告诉了计算机执行什么操作,以及操作的对象,相信你看了这两个例子深有感触。
下面图为常见的指令类型以及对应的编码
n.a 表示 not applicable,这意味着对于某些指令或操作码,这些字段或值并不相关或不需要填写