第2章 指令:计算机的语言
2.1 引言
计算机语言中的单词称为指令,其词汇表称为指令系统(instruction set)。
指令系统:被一个给定体系结构所理解的命令词汇表。
本书所选指令系统为RISC-V,2010年由加州大学伯克利分校开发。
两个流行的指令系统:
1.MIPS设计与1980年的指令系统典范。
2.Intel x86起源于20世纪70年代,现在仍支持PC以及后PC云端。
存储程序概念:指令与多种类型的数据不加区别地存储在存储器中并因此易于更改,因此产生了存储程序计算机。
RISC-V 操作数 | ||
名称 | 示例 | 注解 |
32个寄存器 | x0~x31 | 快速定位数据,在RISC-V中,只对寄存器中的数据执行算术运算,寄存器x0总是等于0. |
2^30个存储字 | Memory[0],Memory[4],...,Memory[4294967292] | 只能被数据传输指令访问。RISC-V使用字节寻址,因此顺序字访问相差4,存储器保存数据结构、数组和换出的寄存器内容。 |
RISC-V 汇编语言 | ||||
类别 | 指令 | 示例 | 含义 | 注解 |
算术运算 | 加 | |||
减 | ||||
立即数加 | ||||
2.2 计算机硬件的操作
每台计算机必须能够实现算术运算,RISC-V汇编语言的符号 add a,b,c,指示计算机将两个变量b和c相加并将其总和放入a.
每个算术指令只执行一个操作,并且必须总是只有三个变量。加入b,c,d,e的和放入变量a中,指令序列将变成四个相加:
add a, b, c
add a, a, d
add a, a, e
硬件简单设计原则:操作数数量可变的硬件比固定数量的硬件更复杂。
设计原则1:简单源于规整。
例题:将两条C赋值语句编译成RISC-V
a = b + c;
d = a - e;
编译器将C语言转换成RISC-V汇编指令
add a, b, c
sub d, a, e
例题:将一条复杂的C赋值语句编译成RISC-V
f = (g + h) - (i + j);
编译器将C语言转换成RISC-V汇编指令
add t0, g, h
add t1, i, j
sub f, t0, t1
为了提高可移植性,Java最初被设计为依赖于软件的解释器。这个解释器的指令系统称为Java字节码(bytecode)。为了使性能接近于等效的C程序,现在的Java系统通常会将Java字节码编译为像RISC-V这样的机器指令,因此这样的Java编译器通常称为即时(立即,当下,立刻)编译器(JIT)。
2.3 计算机硬件的操作数
算术指令的操作数会收到限制,它们必须取自寄存器,而寄存器数量有限并内建于硬件的特殊位置。
寄存器是硬件设计中的基本元素,对程序员可见,将其视为计算机构建的"砖块"。
RISC-V体系中,寄存器大小为32位。
字:计算机中的一种基本访问单元,通常是32位一组,对应于RISC-V体系结构中单个寄存器的位宽。
双字:计算机中另一种基本访问单元,通常是64位一组。
当前RISC-V等计算机上的寄存器通常为32个,算术指令的三个操作数必须从32个32位寄存器中选择。
设计原则2:更少则更快。
寄存器编号:x0~x31
将案例 f = (g + h) - (i + j); 中的变量f、g、h、i、j分别分配给x19、x20、x21、x22、x23,编译后代码,
两个临时变量用x5和x6代替:
add x5, x20, x21
add x6, x22, x23
sub x19, x5, x6
2.3.1 存储器操作数
更复杂的数据结构,数组和结构体,包含比寄存器数量更多的数据元素,可以将其保存在内存中。
内存只是一个大型一维数组,其地址作为数组下标,从0开始,每个元素1Byte也就是8bit。
寄存器一次处理一个字也就是32位的数据,一次处理4个内存单元。
数据传输指令:在内存和寄存器之间传送数据的命令。
地址:用于描述内存数组中特定数据元素位置的值。
载入指令(load):将数据从内存复制到寄存器的数据传输指令通常称为载入指令。
取字的指令:lw
例题:当操作数在内存中时,编译C赋值语句
假设A是一个由100个字组成的数组,并且编译器和之前一样将寄存器x20和x21分别分配给变量g和h。我们还假设数组的起始地址或基址
存放在寄存器x22中,编译这个C赋值语句:
g = h + A[8]
答案:虽然赋值语句只有一个操作,但其中一个操作在内存中,我们必须将A[8]传送到一个寄存器。该数组元素的地址是数组A的基址(x22中)加上元素序号8的和。数据应该放在一个临时寄存器中以便下一条指令使用。
ld x9, 8(x22)//获取A[8]到x9中
下一条指令可以对x9操作,因为A[8]已存放在寄存器x9中。该指令必须将h(x21)和A[8](x9)相加,并将该和放入与g相对应的寄存器x20中:
add x20, x21, x9;//g = h + A[8]
存放基址寄存器x22被称为基址寄存器,而数据传输指令的常数8称为偏移量。
所有体系结构都是按单个字节寻址的,因此字地址与4个字节地址之一相匹配,连续字的地址相差4.
大端:最左边作为字地址。
小端:最右边作为字地址,RISC-V属于这小端。
为了上面代码获得正确的字节地址,加到基址寄存器x22的偏移量必须是8*4,以便取值地址将选择A[8]而不是A[8/4]。
存储指令(store):与载入指令相反,它从寄存器复制数据到内存,sw表示存储字。
对齐限制:数据在内存中要与自然边界对齐的要求,MIPS要求字的起始地址必须是4的倍数。
由于加载和存储指令中的地址是二进制数,我们可以看到为什么作为主存的DRAM以二进制而不是十进制表示容量大小。
也就是说以2的30次,40次方,而不是10的9次,12次方。
例题:使用load和store编译生成指令
A[12] = h + A[8];//h放入x21,A的基址放在x22
//汇编如下
lw x9, 32(x22)//将A[8]值载入x9
add x9, x21, x9//x9 = h + A[8]
//A[12]偏移量是48
sw x9, 48(x22) //将结果保存在A[12]中
硬件/软件接口
许多程序有着比计算机中寄存器数量更多的变量,所以,编译器会尽量把最常用的变量存放在寄存器中,剩下的存放在内存中,使用load和store在寄存器和内存之间传输变量,将不常用的变量存放到内存的过程称为寄存器换出。
寄存器与内存相比,快200倍(50ns与0.25ns),能效提高10000倍(1000pJ与0.1pJ),巨大差异导致缓存的出现。
2.3.2 常数或立即数操作
立即数加:其中一个操作数是常数,写为 addi x22, x22, 4 //x22=x22+4
RV64:64位宽的寄存器,是RISC-V的变体。
32位到64位地址变化让编译器编写者不得不考虑C语言中数据类型的大小。
操作系统 | 指针 | int | long int | long long int |
Microsoft Windows | 64位 | 32位 | 32位 | 64位 |
Linux和大部分Unix | 64位 | 32位 | 64位 | 64位 |
2.4 有符号数与无符号数
二进制数位:也称作位,以2为基数表示,或0或1,作为信息的基本单位。
推广到任意基数,第i个数位d的值是 d x 基的i次方,其中i从0开始从右向左递增。
1011 = 11
32位宽
最低有效位:字中上图中最右边的位,第0位。
最高有效位:字中最左边的位。
无法符号数,最大可以表示42亿多的数。
有符号数如下:
原码:计算机程序可以计算正数和负数,增加单独符号位来表示正负,有缺点,被放弃了。
二进制补码:前导0表示正数,前导1表示负数。
正数:0~2的31次-1(0~1000...0000),负数:-2的31次到-1(1000...0001到1111...1111)
-2的31次方没有正数。
无符号字节载入:lbu将字节视为无符号数,用0拓展填充寄存器最左位,C几乎总是使用字节来表示字符而不是将字节视为有符号的短整数。
字节载入:lb载入带符号整数。
二进制补码求反:将1转为0,将0转为1,结果加1.
符号拓展:将n位表示的二进制转换为一个多余n位表示的数,先取位数更少的书的最高位(符号位),将其复制填充位数更多的数的新位,原来的非符号位被复制到新字的右侧。
2.5 计算机中的指令表示
指令以一系列高低电平信号的形式保存在计算机中,并且以数字的形式表示。每条指令的各个部分都可以被视为一个单独的数,把这些数字并排拼到一起便形成了指令。32个寄存器也只是用0到31这些数来表示。
例题:将一条RISC-V汇编指令翻译为一条机器指令
add x9, x20, x21
十进制表示为:
0 | 21 | 20 | 0 | 9 | 51 |
一条指令的每一段称为一个字段,第1,4,6(0,0,51)组合起来告诉计算机该指令执行加法操作。
第2个字段给出加法运算的第2个源操作数寄存器21号,第3个字段给出另一个源操作数寄存器x20,第5个将相加之和存放在x9。
0000000 | 10101 | 10100 | 000 | 01001 | 0110011 |
7位 | 5位 | 5位 | 3位 | 5位 | 7位 |
总数32位。
指令格式:由二进制数字字段组成的指令表示形式。
机器语言:用于计算机系统内通信的二进制表示。
这样的指令序列称作机器码。
十六进制:以16为基数的数字,由于二进制字串冗长,而且几乎所有计算机数据大小都是4的倍数,所以每4位替换一位十六进制。
十六进制-二进制转换表
十六进制 | 二进制 | 十六进制 | 二进制 |
0 | 0000 | 8 | 1000 |
1 | 0001 | 9 | 1001 |
2 | 0010 | a | 1010 |
3 | 0011 | b | 1011 |
4 | 0100 | c | 1100 |
5 | 0101 | d | 1101 |
6 | 0110 | e | 1110 |
7 | 0111 | f | 1111 |
经常处理不同进制,十进制下标10,二进制下标2,十六进制下标16。
C和Java用0xnnnn来表示十六进制。
RISC-V 字段
给字段命名使其更易于讨论:
funct7 | rs2 | rs1 | funct3 | rd | opcode |
7位 | 5位 | 5位 | 3位 | 5位 | 7位 |
RISC-V指令中每个字段名称的含义:
- opcode(操作码):指令的基本操作,这个缩写是它的惯用名称。
- rd:目的操作数寄存器,用来存放操作结果。
- funct3:一个另外的操作码字段。
- rs1:第一个源操作数寄存器。
- rs2:第二个源操作数寄存器。
- funct7:一个另外的操作码字段。