更新于 

指令集体系

指令集体系结构

不同的处理器会有不同的指令集,但是无论是哪一种,最终要解决的问题就是兼容性问题,即可以使得一个软件在不同的系统上运行,我们在学习了OS后知道操作系统实际上就是一个服务软件,用来协调硬件系统和上层软件之间的合作运行。而指令集来充当一个服务的翻译官,使得不同的软件可以在可兼容的硬件系统中将运行的指令翻译为硬件系统可以识别的命令供处理器运行。因此一个软件可以运行在装配有不同intel处理器的个人电脑上,也可以同时运行在装配有不同ARM处理器的安卓手机上,实际上就是指令即完成的可兼容功能。因此通过指令集的可兼容功能,不同类型,不同品牌的硬件产品只要使用相同的指令集,就可以进行组合使用。

思考:操作系统和指令集体系结构谁更靠近底层?

我们前面讲到了操作系统是本质上一个服务软件,因此他如果想在不同的硬件系统中进行运行,也需要可兼容性,因此需要指令集体系结构来实现翻译功能,因此指令集体系结构更靠近底层。

如上图是一个计算机不同层次的模块,可以看到指令集体系结构更靠近底层,因此他会向os提供服务。并且指令集体系结构属于硬件模块层次。

定义:什么是指令集体系结构?

指令集体系结构(ISA,也称为指令系统),是对处理器硬件细节的抽象描述,即设计规范,他定义了处理器能够做什么,也是对系统级程序员所能看到的处理器的属性。

指令集体系结构之所以能够定义处理器能够做什么,是因为他为处理器提供最基础的命令,处理器只能组合使用指令集体系结构提供的指令来实现软件需要的功能。比如指令集体系结构提供了add功能,那么处理器才能识别并完成add功能。并且要注意指令体系结构不仅仅局限于指令功能的编码,他还包括一些其他的硬件机制。

下面这些功能也都属于指令集体系结构中:

软件就是使用指令集体系结构给出的规则恰当的使用硬件来完成功能。

拓展:ISA中的“五朵金花”

五大主流生产指令集体系结构的厂商,x86常用于桌面,arm,power常用于移动端设备,mips常出现于通信系统中。

其中MIPS指令集(无内部互锁流水级处理器)是最经典的RISC处理器,由斯坦福大学校长Hennessy领导的小组在1981年开始设计,MIPS的理念就是使用相对简单的指令,结合优秀的编译器以及应用流水线技术执行指令,从而使用更少的晶体管生产处更快的处理器。我们后面的实验就是要设计实现一个简单的MIPS指令集体系结构。

思考:指令集系统结构为什么很重要?

通过层次图我们可以看到指令集体系结构起到了协调硬件与软件之间协调兼容的作用,只有硬件与软件使用同一套指令集体系结构,两者才能合作工作形成一个计算机。因此指令集体系结构是计算机产业的枢纽,连接着软件与硬件行业。

一个指令集体系结构并无好坏之分,但是明显被更多地区,行业广泛接受支持的指令集体系结构是更具有影响力的,当前最主流的计算机体系结构就是x86指令集体系结构,但是这并不能说明x86在功能上就一定是最好的isa,只是被更多的行业产商所接受。所以在设计软、硬件时生产商需要参考x86指令集体系结构来实现其在其他使用x86指令集体系结构应用的兼容性,所以指令集体系结构还是计算机软硬件的重要标准,当isa被更多人普遍接受,那么会被更多人所参考,也就逐渐形成了重要的行业标准。

并且指令系统决定了系统的性能和实现复杂性,例如RISC,CISC提供的指令复杂度不同,那么系统工作时的性能也会受到影响,再比如32/63位,媒体向量,向量指令等不同的ISA会采用不同的应用方式,也就决定了系统工作性能的高低。

微体系结构

微体系结构是指令集体系结构的一种具体硬件实现,如指令的数据通路结构,计算单元的电路结构(加法器等),存储器体系(寄存器文件、主存的结构)等等。

我们可以看到x86进一步拆分出了许多微体系结构,不同的微体系结构模块有不同的功能。

汇编语言

汇编语言就是从机器易于识别的机器语言到人能易于理解的高级语言的一个过渡语言,他能够使用标注符号(助记符)将高级语言简化成贴近机器语言的形式,更便于我们人来阅读理解机器语言的功能。

其中汇编语言包括汇编指令,伪指令(标签)、宏指令等。而机器语言仅仅是01编码。汇编语言可以将高级语言的复杂功能拆分简化,使得汇编指令和机器指令一一对应,然后汇编指令的不同搭配即可实现高级语言的功能。这里我们主要学习MIPS汇编指令,也就是学习MIPS指令集体系结构中的指令。

我们以加减法指令为例,学习一下汇编语言得基本格式:

1
助记符 目的操作数 源操作数

助记符是用来区分不同的汇编指令的符号,目的操作数是计算输出数据最终的去向,源操作数是输入数据的来源。在MIPS指令集系统结构中设计的准则是

  1. 指令格式前后一致
  2. 操作数格式一致
  3. 易于在硬件编码和处理

因此MIPS仅仅包含了非常简单常用的指令,使得硬件编码和指令指令变得简单,短小和快速。但是同样对于复杂的指令操作就只能使用许多简单的指令搭配组合去实现,而intel的x86则引入了更加复杂功能强大的指令,这也导致了x86指令集体系结构的硬件阶码和指令执行速度慢于MIPS。因此MIPS为RISC(精简指令),而intel的x86为CISC(复杂指令)。

在汇编操作中,以下三个形式可能是操作数:

  1. 寄存器(存储操作数)
  2. 存储器(存储操作数)
  3. 常数(立即数,自身就是操作数)

MIPS32位寄存器

MIPS定义了32个32位的寄存器组成的寄存器文件,我们学习过OS和机组原理后知道寄存器的访问速度是要快于内存的,但是存储容量确是有限的,因此我们要尽可能的使得寄存器能够存储更多的数据。MIPS中的寄存器操作的数据宽度为32位数据,因此MIPS又被称为32位体系,这体现了“越小的设计越快"的设计准则。

通用寄存器(General Purpose Register)

32个寄存器,并且每一个寄存器的操作数据长度为32位,寄存器文件/寄存器堆是一个32×32位的通用寄存器组成的,这32个通用寄存器都是被程序员可见的寄存器。

特殊寄存器

特殊寄存器一般被定义来实现一些特殊的存储功能,如用于存储乘/除法结果的寄存器HI和LO,这些特殊寄存器程序员是可见的,但是用于存储指令地址的PC(程序计数器,功能是存储下一条要被cpu进行处理的指令的地址的寄存器),这种寄存器程序员是不可见的。

系统控制状态寄存器

例如CP0协处理中的寄存器,一般也是程序员不可见的,他是用来存储记录当前系统状态操作数的寄存器,他能决定系统是否处于目态,很明显为了安全,是不允许程序员看见并修改的。

思考:在汇编语言中我们如何区分识别当前使用的是哪一个寄存器?

为了对我们使用的寄存器加以区分,我们对每一个不同的寄存器都设置了唯一的寄存器编号,如下图:

一定要注意寄存器的助记符是要在数字编号前加上一个$符号的!例如$0代表的就是0号寄存器

我们观察上表可以看出并不是所有的寄存器都可以随意存储操作数的,有一些寄存器有专有用途,如:

  • $0总是表示立即数0
  • $0-$7为保存寄存器,用于保存变量
  • $0-$9为临时寄存器,用于存储大型计算中的中间值
  • 当然了大部分还是通用寄存器,在我们学习过程中,可以默认为处了$0寄存器,其他寄存器都是可以任意使用来充当通用寄存器的

下面我们最后看几个包含寄存器的汇编指令:

MIPS存储器

存储器可以存储更多的数据,并且访问时间更长,但是访问的时间开销也更大,因此常用的数据常放在寄存器中,而不常用的大量数据会存储到存储器中,当某个存储器的数据被使用后会放到寄存器中以便接下来一段时间可能会经常使用(局部性原理的体现)。MIPS中存储器和寄存器的综合搭配使用的机制实际上和页面调度算法中tlb和内存的工作机制是类似的。

对于MIPS而言,存储器的地址为32位,一个存储字的长度也是32位。注意在MIPS中只采用按字节编址存储器,每一个字节有一个单独的地址,但是我们可以按字节、半字和字的方式进行寻址。

按字编址和寻址

按字编址时,一个字对应一个地址,如上图此时是一个词(字)对应着一个地址,而不是,他相较于按字节编址,一个地址对应着更多的数据(四个字节)。

读存储器的指令称为加载指令(load)指令,他一次性加载一个字的指令助记符为lw(load a word)。例如:

1
lw $s0,5($t1)

上面是一个读存储器的指令,作用是把某个地址对应的字数据加载到s0寄存器,后面的5($t1)实际上返还的是访存的地址。计算方式为:

访存地址=基地址($t1寄存器中的值)+偏移(5)

1.

注意$t1是一个通用寄存器,只是这里刚好基地址存储在t1寄存器,实际上基地址可以存储在任意一个寄存器中 2. 一个字对应四个字节 ,

写存储器的指令称为存储指令(store指令),他一次性存储一个字的指令助记符为sw(store a word)。例如:

1
sw $s0,5($t1)

同样的访存地址还是($t1+5),但是此时的作用是将t0寄存器中的字写入到这个主存地址中。因此lw和sw时一个对立的指令,两者搭配使用,完成读/写存储器的功能。

按字节编址

此时一个字节对应着一个地址,因此一个字会分成4个字节存储到4个存储器的地址单元中。此时根据一次性读/写1,2,4个字节可以分类成一下几种指令,他们都是读/写存储器的指令,但是一次性读/写对应的数据字节大小不同:

  • 读/写1个字节:lb(load a byte),sb(store a byte)
  • 读/写半字(2个字节):lh(load half byte),sh(store half byte)
  • 读/写字(4个字节):lw(load a word),sw(store a word)
info

, 一个字(32bit=4byte)为4字节,字地址按4递增,字节地址按1递增

因此,如果我们想在按字编址的存储器中读一个字节只能先取出一个字,在拆分处相应的需要的字节数据,而在一个按字节编址的存储器中如果我们想要读一个字的数据,那么应该一次性读4个字节的信息,因此访存地址应为4的倍数,即地址最后两位均为0,否则会出错。

假设我们现在想从存储器地址4处,加载一个字到寄存器$3中,那么MIPS汇编代码如下:

1
2
3
4
#基地址为0,因此$0永远存储的是常数0,请区分$s0和$0寄存器
#因此偏移4个单位对应的访存地址就是000000004
#又因为使用的是lw按字读,因此一次性读4个字节
lw $s3,4($0)

因此最终$3寄存器存储的数据时0xF2F1AC07。

写按字节编址存储器也是类似的,假设我们现在想要将$t7寄存器中的值写入到存储器地址44处,那么MIPS汇编代码是:

1
sw $t7,44($0)

总之,我们要会区分按字和按字节编/寻址的操作,同时我们要注意具体的指令如何操作,取决于存储器的编址方式,我们学习的是使用按字节编址的MIPS存储器,因此大部分操作使用都是按字节读/写操作。

大端和小端

在按字节编址的存储器中,根据一个字中的字节的存储顺序将存储器的组织方式为两种:大端和小端。

  • 大端:一个字中,最高有效字节存储在低地址
  • 小端:一个字中,最高有效字节存储在高地址

两种组织方式,字地址都是相同的,只是字中的字节存储的地址是不同的。大端/小端由ISA确定,对于MIPS而言,两者都可以。

一定要注意,大端存储并不是指大的数存储到低地址,而是高有效字节的数据存储在低地址。小端存储并不是指小的数存储到低地址,而是低有效字节的数据存储在低地址。

我们以下面的例子来具体区分一下大端和小端存储的区别:

因此我们发现对于一个数00112233H,大端存储就是正常的高位数从左到右存储,因此高位有效数存储到了低字节地址处。而小端存储反而相反,高位有效数存储到了高字节地址处,也就造成了实际上一个字的数拆分成4个字节后是从右到左存储的,因此读出来的数要逆序一下才是真正的数值。

例题

假设$s0中的初始值为0x23456789,对于大端和小端组织形式,下面程序执行后,$s0的值是多少?

1
2
sw $s0,0($0)
lb $s0,1($0)

对于大端存储:首先将0x23456789存储到了地址0处,并且存储的顺序从低字节地址到高字节地址为23 45 67 89,因此再lb取1处的一个字节的数据时得到的是45

而对于小端存储:首先将0x23456789存储到了地址0处,并且存储的顺序从低字节地址到高字节地址为89 67 45 23,因此再lb取1处的一个字节的数据时得到是67

思考:如果上面的代码改写为lw $s0,1($0)可以吗?

不可以,因此lw一次取一个字也就是四个字节,又因为从0开始存储第一个字,因此地址一定是4的倍数才行,但是1这里对应的并不是一个字的地址起点,因此是不可行的。

思考:如何用一段C程序,来判断运行机器采用的是大端存储还是小端存储?

实际上我们只需要先将一个四字节的信息存储到四字节的参量中,然后取出一个字的信息即可判断,如下:

1
2
3
4
5
6
bool big_little_endian(){
int i=0x23456789;
char *c=(char*)&i;
//返还true就是大端存储,返还false就是小端存储
return (*c==0x23);
}

操作数–立即数

立即数既不来自寄存器,也不来自存储器,而是直接来自指令,他通常使用16位二进制补码来表示,直接嵌入在汇编指令中使用。

如上图所示,4和-12就是立即数,他们可以直接在指令中嵌入使用,而不是通过寄存器,存储器等方式进行取操作后再使用。

机器语言

在MIPS指令集中我们可以将指令根据32位指令代码区域划分规则的不同而分类:

  1. 寄存器类型指令(R型指令)
  2. 立即数类型指令(I型指令)
  3. 跳转类型指令(J型指令)

寄存器类型指令(R型指令)

MIPS指令集是32位的,因此每一个指令都使用32位代码,其中按照上面的规则进行划分,每一个区域都有特定的功能:

  • op字段:操作码,通常为全0(在nemu实验中opcode_table中的各种指令名的名称大多相似)
  • func字段与op字段一起决定指令的功能(在nemu实验中opcode_table中相同地址地址的指令虽然前缀相似,但是后面有些许不同的后缀,实际上就是func,两者共同决定一个指令类型)
  • rs字段和rt字段是寄存器编号,表示两个源操作来自于哪两个寄存器
  • rd字段用来表示目的寄存器的编号
  • sa(shamt)字段只在移位指令中使用,表示移位位数,对于其他R-型指令sa字段全为0

上面的指令由两个源操作加上一个目的操作数,并且所有的操作数都来源与寄存器,因此称为寄存器类型指令(Register command),也就是R-型指令。下面是一个寄存器类型指令的例子:

他们的指令划分(下表中的数据都使用十进制真值来表示)如下:

我们一定要注意rs,rt,rd各对应的是谁,在mips指令add和sub等寄存器指令,书写时的规则是

1
2
3
#助记符 目的操作数 源操作数1 源操作数2 
add $s0 $s1 $s2
sub $t0 $t3 $t5

和划分的区域略有顺序的不同。因此在机器中的代码存储如下;

然后在使用16进制代码表示整个的32位指令,因此一个指令是由8为十六进制数表示。

立即数类型指令(I型指令)

划分规则:

  • op字段表示操作码
  • rs字段为寄存器编号,表示一个源操作数来自于寄存器
  • imm字段是一个16位立即数,表示另一个操作数,需要扩展为32位再使用
  • rt字段表示目的寄存器的编号,用于存放指令运行结果

我们发现此时的指令所有的操作数中有一个是立即数,因此称为立即数类指令,要注意此时的目的操作数夹在两个源操作数之间,但是在书写指令时,仍然为操作数在最前面:

1
2
3
4
5
6
#助记符	目的操作数	源操作数1	源操作数2(立即数)
addi rt, rs imm
lw rt, imm
sw rt, imm
lw rt, rs
sw rt, rs

最终再机器码中还要将十进制真值数改用二进制数表示,然后最终的汇编代码再将32位的二进制码转换为8位16进制码来表示:

跳转型指令(J型指令)

  • op字段表示操作码,用于确定指定的类型

  • instr_index用于产生跳转的目的地址,但是我们知道一个应该是32位,因此我们需要对instr_index进行一定的处理;

    (PC+4)31:28instr_index0(PC+4)_{31:28}||instr\_index||0

    实际上就是使用instr_index处理PC+4这个数想办法使其表示不同的32位地址数。

    注意上面的||不是取或的意思,而是地址拼接的意思,我们是将下一条地址的31:28这4位和instr_index26位拼接再在低两位拼接两个0形成跳转地址。并且后面拼接两个0实际上就是<<2的操作,因此后面我们学习到跳转指令的数据通路时会用移位操作实现。

总结

一定要注意无论是哪种指令都是对一个32为二进制码进行划分,然后指令使用8位16进制码来简单表示,同时要注意上面的这种划分规则只是MIPS指令集的规则,对于intel的x86等并不适用。指令可以根据opcode和后面的低16位的划分规则来进行判别类型。