1.写在前面
人与人沟通需要说话,需要语言,我们中国人说的是汉语,那么与计算机进行沟通的话,我们需要什么语言呢?高级语言,低级语言,汇编语言,机器语言。今天我们要讲的就是与计算机进行沟通的语言,指令:计算机的语言。
2.概述
要控制计算机硬件,就必须用它的语言。计算机语言中的单词称为指令,其词汇称为指令系统。而我们主要所选的指令系统为RISC-V,2010年初由加州大学伯克利分销开发。指令系统主要是以下的两种,一种是MIPS是设计与20世纪80年代的指令系统典范。在许多方面,RISC-V都遵循类似的设计。还有一种是Intel x86起源于20世纪70年代,现在仍然支持PC以及后PC时代的云端。
但是计算机设计人员都有一个共同目标:找到一种让构建硬件和编译器容易,同时最大化新能且最小化成本和资源语言。一些常用的指令格式如下:
3.计算机硬件的操作
3.1两个数相加
add a,b,c
指示计算机将两个变量b和c相加并将其总和放入a中
两个被加到一起的数和一个放置总和的位置。要求每条指令恰好有三个操作数,不多不少,符合硬件简单设计的原则。
设计原则1:简单源于规整。
4.计算机硬件的操作数
与高级语言程序不同,算术指令的操作数会受到限制;它们必须取自寄存器,而寄存器数量有限并内建于硬件的特殊位置。
程序语言的变量和寄存器之间的一个主要区别是寄存器数量有限,在当前RISC-V等计算机上通常为32个。我们增加了限制,即RISC-V算术指令的三个操作数必须从32个64位寄存器中选择。这儿引出第二个设计原则
设计原则2:更少则更快。
简单来讲,数量过多的寄存器可能会增加时钟周期,因为电信号传输的距离越远,所花费的时间就越长。这种规则不是绝对的,在这种情况下,设计人员必须在程序对更多寄存器的渴求和设计人员认真对待。在这种情况下,设计人员必须在程序对更多寄存器的渴求和设计人员对缩短时钟周期的期望之间取得平衡。另外一个不使用超过32个寄存器的原因是指令格式的位数限制。
4.1存储器操作数
程序语言中,有一些只包含单个数据元素的简单变量,也有更复杂的数据机构–数组和结构体。那么计算机如何表示和访问这样庞大的数据结构呢?
那么我们将这些复杂的数据存储到内存中,但是计算都是在寄存器中,因此RISC-V必须包含内存和寄存器之间的传输数据的指令。这些指令称为数据传输指令。要访问内存中的字或者双字,指令必须提供内存地址。
将数据从内存复制到寄存器的数据传输指令通常称为载入指令(load)。载入指令的格式是操作名称后面紧跟数据待取的寄存器,然后是寄存器和用于访问内存的常量。指令的常量部分和第二个寄存器中的内容相加组成内存地址。
与之对应还有就是存储指令store,它从寄存器复制数据到内存。存储指令的格式类似载入指令的格式:操作名称,接着是要写回内存的寄存器,然后是基址寄存器,最后是选择数组元素的偏移量。
4.2常数或立即数操作数
程序经常会在一次操作中用到常数,只使用目前介绍过的指令,我们需要将常数从内存中取出才能使用。避免使用加载指令的一种方法是提供另一个版本的算术指令,它的其中的一个操作数是常数。这种带有一个常数操作数的快速加指令称为立即数加或addi
5.有符号数与无符号数
计算机中数字以一系列高低电信号的形式保存在计算机硬件中,因此他们被认为是基数为2的数。由于在计算机中所有的信息都由二进制数位或为表示。
至于二进制和八进制以及十进制、十六进制之间的进制转换,我在这就不做过多的介绍了,因为对于学编程的人,都是要了解,这儿就不做过多的赘述了。
我们要讲的就是有符号数和无符号数。有符号数就是最高位表示符号位(0 表示正数,1表示负数),无符号数就是最高位不表示符号位。
6.计算机中的指令表示
人们使用计算机指令的方式和计算机识别指令的方式是不同的。指令以一系列高低电平信号的形式保存在计算机中,并且以数字的形式表示。实际上,每条指令的各个部分都可以被视为一个单独的数,把这些数字并排拼到一起便形成了指令。
6.1将一条RISC-V的汇编指令翻译为一条机器指令
add x9,x20,x21
十进制表示为:
0 21 20 0 9 51
一条指令的每一段称为一个字段。第一、第四和第六个字段(0 0 和51)组合起来告诉RISV-V计算机该指令执行加法操作。第二个字段给出了作为加法运算的第二个源操作数的寄存器编号,第三个字段给出了加法运算的另一个源操作数,第五个字段存放要接收总和的寄存器编号。
该指令可以表示成如下的格式:
0000000 10101 10100 000 01001 0110011
这种指令的设计被称为指令格式。从位数可以看出,这个RISC-V指令只需要32位,刚好是一个字或一个双字的一半。按照"简单源于规整"的设计原则,RISC-V指令都是32位长。
为了把它和汇编语言区分开来,我们把指令的数字表示称作机器语言,把这样的指令序列称作机器码。
给RISC-V字段命名
具体的定义如下:
- opcode(操作码):指令的基本操作,这个缩写是它的惯用名称。
- rd:目的的操作数寄存器,用来存放操作结果。
- funct3:一个另外的操作码字段
- rs1:第一个源操作数寄存器。
- rs2:第二个源操作数寄存器。
- funct7:一个另外的操作码字段。
当指令需要比上面显示的更长的字段时就会出现问题。因此,在所有指令保持相同的需求和保持单一的指令格式的需求之间产生了矛盾,这个矛盾也将我们引向最终的设计原则。
设计原则3:优秀的设计需要适当的折中。
RISC-V设计人员选择的折中方案是报纸所有指令长度相同,对于不同的指令使用不同的指令格式。例如:上面的格式为R型(用于寄存器)。另一种指令格式的类型是I型,用于带一个常数的算术指令一级加载指令。I型的字段如下所示:
12位immediate字段为补码值,所以它可以表示从-211到211-1之间的整数。当I型格式用于加载指令时,immediate字段表示一个字节偏移量,所以加载双字指令可以取相对于基址寄存器rd中基地址偏移±2048字节。我们来分析一下如下的指令:
ld x9,64(x22)
这里,22(x22)存放在rs1字段中,64存放在immediate字段中,9(x9)存放在rd字段中。
我们还需要一个存储双字指令sd的指令格式,它需要两个源寄存器(用于基址和存储数据)和一个用于地址偏移量的immediate字段。S型的字段如下所示:
S型格式的12位immediate字段分成了两个字段,低5位和高7位。RISVC-V体系结构设计师选择这种设计是因为它能够在所有指令格式中保持rs1和rs2字段在相同的位置。保持尽可能相似的指令格式降低了硬件的复杂性。同样,opcode和funct3字段也总是报纸同样的大小并在同一位置。
指令格式通过操作码字段中的值来区分:每个格式在第一个字段(opcode)中被分配了一组不同的操作码值,以便硬件知道如何处理指令的其余部分。
注意:虽然RISC-V同时具有add和sub指令,但它并没有与addi相对应的subi指令。因为immediate字段表示的是二进制补码整数。所以addi可以用来做常数减法。
保持所有指令长度相同的需求与设置尽可能多的寄存器的需求相矛盾。任何增加的寄存器数量都会让指令格式的每个寄存器字段至少增加以为。鉴于这些约束和更少则更快的设计原则,如今的大多数的指令系统体系结构都是设置16或32个通用寄存器。
目前为止介绍的三种RISC—V指令格式是R型、I型和S型。R型格式有两个源寄存器操作数和一个目标寄存器操作数。I型格式用一个12位的immediate字段替换了一个源寄存器操作数。S型格式有两个源操作数和一个12位的immediate字段,但没有目标寄存器操作数。S型immediate字段分为两部分,最左边的字段是115位,最右边的字段是40位。
重点:当前计算机构建的基于两个关键的原则
- 指令由数字形式表示
- 程序和数据一样保存在存储器中来进行读写。
将指令作为数据的一个结果就是程序经常以二进制数据文件的形式来发布。
7.逻辑操作
尽管最初计算机只对整字进行操作,但人们很快发现,在一个字内对几个位构成的字段甚至是对单个位进行操作都是十分有用的。检查一个字中每个由8位组成的字符就是一个例子。随之而来的是,人们在编程语言和指令系统结构中添加了一些操作,用于简化打包或者拆包。这些指令被称为逻辑操作。
第一类操作称为移位。一个双字中的所有位都向左或向右移,用0填充空出来的位。这些移位指令使用I型格式。因为它不适用于对一个64位寄存器移动大于63位的操作,只有I型格式中12位的immediate字段中的低6位被实际使用。其余的6位被重新用作额外的操作吗字段,即funct6。
slli x11,x19,4
逻辑左移提供了另外一个好处。左移i位相当于乘以2^i。
还有一种类型的移位指令–算术右移。这个变体与srli相似,但它不是用零填充空出的左边的位,而是用原来的符号位来填充。
另外一个有用的操作是与(AND)。AND是按位操作的,只有当两个操作数的位都是1时,结果才是1。AND可以在源操作数的某些位为0时,将结果数的对应位设为0。这种和AND联合使用的源操作数的习惯上被称为掩码,因为掩码隐藏了某些位。
或(OR)如果任一操作数的位为1,则结果的对应位为1。
最后一个逻辑操作是按位取反(NOT)。NOT只有一个操作数,如果操作数中的某位为0.那么它将结果的对应位设为0,反之亦然。使用我们以前的符号。
最后就是XOR(异或),异或是两个操作数对应位相同时设0,不同时设为1,所以NOT等价于异或1111…111。
8用于决策的指令
计算机与简单计算器的区别在于它的决策能力。根据输入数据和计算中产生的值执行不同的指令。
beq rs1,rs2,L1
该指令表示如果寄存器rs1中值等于寄存器rs2中值,则转到标签为L1的语句执行。
bne rs1,rs2,L1
该指令表示如果寄存器rs1中的值不等于寄存器rs2中的值,则转到为标签为L1的语句执行。
这两条指令通常称为条件分支指令。
8.1循环
循环的实现方式就是,比较指令加上跳转的指令,就能实现对应的循环。
8.2边界检查的简便方法
将有符号数当做无符号数处理,给我们提供了一种低成本的方式检查是否0<=x<y,常用于检测数组下标是否越界。关键在于二进制补码表示中的负整数看起来像无符号表示中很大的数;因为最高有效位在有符号数中表示符号位,但在无符号中表示数的很大一部分。因此,无符号比较x<y在检测x是否小于y的同时,也检测了x是否为负数。
8.3case/switch语句
实现switch的最简单方法是通过一系列的条件测试,将switch语句转换成if-then-else语句。
有时,另一种更有效的方法是编码形成指令序列的地址表,称为分支地址表或分支表,程序只需要索引到表中,然后跳转到合适的指令序列。因此,分支表只是一个双字数组,其中包含与代码中的标签对应的地址。该程序将分支表的相对应条目加载 到寄存器中,然后需要使用寄存器中的地址进行跳转。于是提供了一种间接跳转指令,该指令对寄存器中指定的地址执行无条件跳转。jalr
9.计算机硬件对过程的支持
过程或函数是编程人员用于结构化编程的一种工具,两者均有助于提高程序的可理解性和代码的可重用行。过程允许程序员一次只专注于任务的一部分;参数可以传递数值并返回结果,因此用以充当过程和其余程序与数据之间的接口。
过程是用软件实现抽象的一种方式。程序必须遵循以下六个步骤:
- 将参数放在过程可以访问到的位置。
- 将控制转交给过程。
- 获取过程所需的存储资源。
- 执行所需的任务。
- 将结果值放在调用程序可以访问到的位置。
- 将控制返回到初始点,因为过程可以从程序中的多个点调用。
RISC-V软件为过程调用分配寄存器时遵循以下约定:
- x10~x17:八个参数寄存器,用于传递参数和返回值。
- x1: 一个返回地址寄存器,用于返回到起始点。
除了分配这些寄存器之外,RISC-V汇编语言还包含一个仅用于过程的指令:跳转到某个地址的同时将下一条指令的地址保存到目标寄存器rd。跳转-链接指令jal
指令中的链接部分表示指向调用点的地址或链接,以允许该过程返回到合适的地址。存储在寄存器x1中的这个链接被称为返回地址。返回地址是必需的,因为同一过程可能在程序的不同部分被调用。
正如所期望的那样,寄存器跳转-链接指令跳转到存储在寄存器X1中的地址。因此,调用程序或称为调用者将参数值翻入x10~x17中,并使用jal x1,x 跳转到过程x(有时称为被调用者)。被调用者执行计算,将结果放在相同的参数寄存器中,并使用jalr x0,0(x1)将控制返回给调用者。
在存储程序概念中,需要一个寄存器来保存当前执行指令地址。由于历史原因,这个寄存器总是被称为程序计数器。
9.1使用更多的寄存器
假设对于一个过程,编译器需要比8个参数寄存器更多的寄存器。由于在任务完成后必须掩盖踪迹,调用者所需的所有寄存器都必须恢复到调用该过程之前所存储的值。这种情况是需要将寄存器换出到存储器的一个例子。
换出寄存器的理想数据结构是栈(stack)—一种后进先出的队列。栈需要一个指向栈中最新分配地址的指针,以指示下一个过程应该放置换出寄存器的位置或寄存器旧值的存放位置。
在RISC-V中,栈指针是寄存器x2,也称为sp。栈指针按照每个被保存或恢复的寄存器按双字进行调整。
按照历史惯例,栈按照从高到低的地址顺序增长。这就意味着可以通过减栈指针将值压栈;通过增加栈指针缩小栈,从而弹出栈中的值。
RSIC-V软件将19个寄存器分成两组:
- x5x7以及x28x31:临时寄存器,在过程调用中不被调用者保存。
- x8x9以及x18x27:保存寄存器,在过程调用中必须被保存。
9.2嵌套过程
不调用其他过程的过程称为叶子过程。如果所有过程都是叶子过程,情况将会变得简单,但事实并非如此。
对于非叶子过程的解决方法是将其他所有必须保存的寄存器压栈,就像保存寄存器压栈一样。调用者将所有调用后还需要的参数寄存器(x10x17)或临时寄存器(x5x7和x28x31)压栈。被调用者将返回地址寄存器x1和被调用者使用的保存寄存器(x8x9和x18~x27)压栈。调整栈指针sp以计算机压栈寄存器的数量。返回时,从存储器中恢复寄存器并重新调整栈指针。
9.3在栈中为新数据分配空间
最后一点复杂性在于栈也用于存储过程的局部变量,但这些变量不适用于寄存器,栈中包含过程所保存的寄存器和局部变量的段称为过程帧和活动记录。
一些RISC-V编译器使用帧指针fp或者寄存器x8来指向过程帧的第一个双字。栈指针在过程中可能会发生改变,因此对存储器中局部变量的引用可能会有不同的偏移量,具体取决于它们在过程中的位置,从而使过程更难理解。帧指针在过程中为局部变量引用提供一个稳定的基址寄存器。注意,不管是否使用显式的帧指针,栈上都会显式一条活动记录。
9.4在堆中为新数据分配空间
栈从用户地址空间的高端开始并向下扩展。低端内存的第一部分是保留的,之后是RISC-V机器代码,通常称为代码段。在此之上是静态数据段用于存放常量和其他静态变量。虽然数组就有固定长度,且因此可与静态数据段很好地匹配,但像链表等数据结构往往会随生命周期增长或缩短。存放这类数据结构的段通常称为堆,它放在内存中。
如果参数超过8个,RISC-V约定将栈中额外的参数放在帧指针的上方。过程期望前8个参数在寄存器x10到X17中,其余参数在内存中,可通过帧指针寻址。
10.写在最后
由于计算机组成的第二章篇幅过长,所以这儿打算分多篇博客来书写。