ADD R,M
:将寄存器R中的数和一个存储器M中的数相加,然后存到这个寄存器R中
LOAD R,M
:把存储器M中的内容,加载到寄存器R中
STORE M,R
:把寄存器R中的数存入到存储器M中
JMP L
:无条件转向L处
CPU是从内存中,按照地址依次(本质是计数器累计取值)取出指令开始执行的,如果想改变取指令的位置,就需要用到JMP L
(即跳转jump),当CPU执行该指令后,就会转移到L所指向的存储器单元中去取下一条指令来执行
注:M和L为存储器地址,R为通用寄存器编号
每条指令等长,均为两个字节
第一个字节的高四位是操作码,如:(目前只提供四条指令,最多可扩展至16条)
LOAD:0000
、ADD:0001
、STORE:0010
、JMP:0011
第一个字节的第四位是寄存器号,如:(目前只提供4个寄存器,最多可扩展到16个)
R0:0000
、R1:0001
、R2:0010
、R3:0011
第二个字节是存储单元的地址,有八个二进制位,最大可以使用256个字节的存储器
0001 0010 | 0000 1001
:ADD R2,[9]
在下图中,分别依次执行:
R3<-M5、R3<-R3+M6、M7<-R3、M18<-jump
即实现了M7=M5+M6的功能,并跳转到M18的指令
一开始PC
上记录着即将执行的下一条指令的地址,开始执行时,PC
上的地址会通过MAR
传送到地址总线上,并且由控制总线给出读的控制信息;存储器返回指令信息到MDR
上,再通过CPU内部总线传送到IR
上,经过指令译码,将之翻译成不同的命令执行
这里有一个问题是,为什么PC
寄存器中一开始的地址是这个呢?这也是一直困扰我的一个问题,只有拿到备忘录PC
中第一条指令地址信息,接下来才能够通过计数的方式继续读取新的指令,而存储器中的JMP
指令提供了指令的地址跳转方式(即使指令不一定要连续存放)
实际上PC
寄存器的初值,是进行指令系统体系结构设计时,必须要约定的一个内容,CPU在启动或复位完成后,第一条指令需要从哪里取出,这也是软硬件双方一开始必须要协商好的事情,至于这个地址到底应该是什么,并没有明确的规则,但通常情况下,我们会约定为这个体系结构所能访问存储单元的最小地址,也就是0,或者是接近最高地址的地方
关于内存寻址能力跟CPU位宽的关系,实际上是没有关系
CPU的寻址能力与它的地址总线位宽有关,而我们通常说的CPU位宽指的是数据总线位宽,它和地址总线位宽半毛钱关系也没有,自然也与寻址能力无关
注意到,每一个地址对应的数据都是8位,即一个Byte字节,地址总线位宽实际上指的是地址总线的位数,比如说地址总线是32位,那么它的位宽为32位,相当于有 2 32 2^{32} 232个地址,但是,寻址能力指的是这 2 32 2^{32} 232个地址的数据量大小,由于一个地址对应一个Byte,因此32位地址总线对应 2 32 2^{32} 232Byte即4GB的寻址能力
而CPU位宽,也就是我们通常所说的32位操作系统、64位操作系统中的那个32位和64位,它指的是一个时钟周期内CPU能处理的二进制位数,可以被理解为通用寄存器的位数,如8086 CPU是16位的,可以一次处理2个字节(16个bit),80386 CPU是32位,能一次处理4个字节,目前的CPU基本上64位的了,一次能处理8个字节
我们的Windows操作系统也分为32位和64位,主要是针对上面CPU的位宽做了些优化,比如32位的CPU就不能用64位的Windows(因为CPU一次只能处理32bit,而操作系统给你的指令是要处理64bit),但64位的CPU就可以运行32位的Windows,也能运行64位Windows
在8086这个16位CPU上,它的地址总线位宽是20位,正好能寻址1MB,80286它的PAE是24位,在Pentium II(32位CPU)时这个PAE变成了36位,可以支持64GB的寻址;64位CPU出现之后,其地址总线位宽一般采用的是36位或者40位,它们寻址的物理地址空间为64GB或者1T
那地址总线和数据总线有什么关系?可以这么理解,地址总线用来定位,数据总线用来传输,也就是当CPU需要从内存读取数据或向内存写入数据时,它使用地址总线来指定其需要访问的存储器块的物理地址,然后通过数据总线发送数据
所以说,CPU的位宽和寻址能力是没有关系的,16位CPU的地址总线位宽可以是20位,32位CPU的地址总线可以是36位,64位CPU的地址总线位宽可以是40位。所以你下次一定不要说32位的CPU只能寻址2^32(4GB)了,大错特错
那操作系统的位宽和寻址能力有什么关系吗,这个其实还是有的。我们在使用计算机时,操纵的其实是逻辑地址,32位操作系统的逻辑地址寻址范围只有2^32=4GB
所以,不管你用什么样的CPU,它最多也只支持4GB的内存容量,但这是操作系统的锅,并不是说32位CPU只能寻址4GB空间
8086是一款16位的CPU,内部的通用寄存器为16位,可以处理16位和8位的数据
对外有16根数据线和20根地址线,可寻址的内存空间为1MB( 2 20 2^{20} 220Byte)
CPU发送到存储器的地址为物理地址,物理地址的形成采用了“段加偏移”的方式
采用实模式,即实地址模式,BIOS芯片的指令就是运行在实模式下的
是80x86系列中第一款32位微处理器
支持32位算术逻辑运算,提供32位通用寄存器
同时地址总线也扩展到了32位,可寻址4GB的内存空间
改进了“保护模式”,增加了“虚拟8086模式”以兼容8086模式
主要工作模式为保护模式(Protected Mode即pmode):支持多任务、设置特权级、特权指令的执行、访问权限检查、可以访问4GB物理存储空间、引入了虚拟存储器概念
x86扩展到64位的第一款微处理器
可以访问高于4GB的存储器
最重要的是它可以兼容32位x86程序,且不降低性能
x86-64支持原有的32位x86的运行模式,并将之命名为传统模式(Legacy mode)
新增的运行模式被称为长模式(Long mode),又分为两个子模式,64位模式(64-bit mode)和兼容模式(Compatibility mode),在兼容模式下,原有的x86程序不需要重新编译就可以高效运行,这也是x86-64得以成功的关键
8086是16位系统,因此指令指针寄存器IP(Instruction Pointer)的寻址能力为 2 16 = 64 K B 2^{16}=64KB 216=64KB,而8086对外有20位地址线,寻址范围是 2 20 = 1 M B 2^{20}=1MB 220=1MB,
我们在编写程序时给出的地址是偏移地址,即偏移量,是16位的;而段寄存器中存放了段基值,也是16位的;这一对地址就被成为逻辑地址
其中段基值在运算时首先被左移四位,然后和偏移量相加,这样就得到了一个20位的物理地址,这也就是从逻辑地址生成物理地址的过程:物理地址=段基值*16+偏移量
MOV AX, [3000H]
在段加偏移的模式下,操作数是默认存放在DS即数据段寄存器所指向的段中,假设事先已经在DS段寄存器中存入了2000H,那么实际物理地址是2000H*16+3000H=23000H
存储器中,OP是操作码,而30H和00H是操作数(一个地址存一个Byte,因此3000H这个16位数需要2个Byte即两个地址来存放),CPU会取出DS寄存器中的内容,并将其左移四位后与偏移量相加,从而得到23000H这个物理地址,然后用这个地址去访问存储器,因为我们的目标寄存器AX是16位的,所以从存储器中取出两个字节,并依照高地址放在高字节,低地址放在低字节的原则存放在AX当中
MIPS是精简指令系统的代表,采用了与X86相反的设计理念,并引领了精简指令系统的潮流
RISC(Reduced Instruction Set Computer):精简指令系统计算机,如mips
CISC(Complex Instruction Set Computer):复杂指令系统计算机,如x86
MIPS(Microprocessor without Interlocked Piped Stages):流水线不会互锁的微处理器
流水线是现代微处理器为提高性能而采用的一项技术,而流水线中的互锁则是导致其性能降低的一个非常重要的因素
因此MIPS主要的关注点是:减少指令的类型,降低指令的复杂度;其基本原则是A simpler CPU is a faster CPU
固定指令长度(32-bit,即1 word),注:x86中一个word是16-bit,MIPS中一个word是32-bit
指令长度固定简化了从存储器取指令的工作,不用像x86CPU那样需要判断每条指令的长度
简单的寻址模式,简化了CPU访问存储器读取操作数的控制逻辑
指令数量少,指令功能单一(一条指令只完成一个操作,而x86是一条指令完成许多操作),可以简化指令的执行过程
只有Load和Store指令可以访问存储器,不支持x86指令中让算术指令访问存储器的操作,如ADD AX,[3000H]
但是,这些特点使得MIPS指令进行直接编程变得非常困难,因此想要有高效率的MIPS程序,必须要有优秀的编译器的支持
以加法指令为例:add a,b,c
-> a = b + c
,同理还有算术运算、逻辑运算、移位等指令
可以发现,MIPS指令的操作都非常简洁统一,且这些指令的操作数都不可以是存储器操作数,即不能直接对存储器里的内容进行操作
要访问存储器,就必须要使用专门的访存指令,假设A是一个100字(word)的数组,首地址在寄存器$19中,变量h对应寄存器$18,临时数据存放在寄存器$8,我们要实现的是A[10]=h+A[3]
注意:MIPS一个word是32bit即4Byte,因此每两个地址之间相差4Byte
lw $8,12($19) # t0=A[3]
add $8,$18,$8 # t0=h+A[3]
sw $8,40($19) # A[10]=h+A[3]
MIPS有编号从0-31共32个通用寄存器,每个通用寄存器都是32位宽(4Byte)
在编写汇编程序时,我们可以用数字,也可以用名称来表示这些寄存器,下面几条指令与注释内容等价:
lw $t0,12($s3) # lw $8,12($19)
、add $t0,$s2,$t0 # add $8,$18,$8
、sw $t0,40($s3) # sw $8,40($19)
值得注意的是,$t0-$t7
、$s0-$s7
这些寄存器都是我们常用的,s一般用于存放变量,t一般用于存放临时结果
分为R型(Register,寄存器型)、I型(Immediate,立即数型)、J型(Jump,无条件转移型)
R型指令格式包含6个域:2个6-bit域,可表示0~63的数;4个5-bit域,可表示0~31的数
用于指定指令的类型,对于所有R型指令,改域的值均为0
但这并不说明R型指令只有一种,还需要用funct域来更为精确地指定指令的类型
所以对R型指令,实际上一共有12个bit的操作码,那为什么不将opcode域和funct域合并成一个12-bit的域呢?这样不是更直观明了吗?
Source Register,通常用于指定第一个源操作数所在的寄存器编号
Target Register,通常用于指定第二个源操作数所在的寄存器编号
Destination Register,通常用于指定目的操作数所在的寄存器编号,也就是保存运算结果的地方
5个bit的域可以表示0-31的数,正好对应MIPS体系结构中的32个通用寄存器
shift amount,用于指定移位指令进行移位操作的位数,5bit域可以表示0-31个移位位数;对于32bit的数,更多的移位没有实际意义;对于非移位指令,该域设为0
add $8,$9,$10
查指令表可知:
opcode=0,funct=32,shamt=0(非移位指令)
根据指令操作数可知:
rd=8(目的操作数),rs=9(第一个源操作数),rt=10(第二个源操作数)
然后把各个域的数值转化成二进制数,填写到对应位置,即可将该汇编指令转化成二进制机器码:
000000(opcode)01001(rs)01010(rt)01000(rd)00000(shamt)100000(funct)
R型指令只有一个5bit域也就是移位域来表示立即数,范围为0-31;而常用的立即数远大于这个范围,因此需要新的指令格式,即I型指令
用于指定指令类型(没有funct域),所以不同的I型指令,opcode域是不同的
指定第一个源操作数所在的寄存器编号
指定用于目的操作数(保存运算结果)的寄存器编号,对于某些指令,指定第二个源操作数所在的寄存器编号
I型指令与R型指令不同,他只有两个寄存器数域,剩下的16位被整合成了一个完整的域immediate,可以存放16位立即数,表示 2 16 2^{16} 216个不同的数值
对于一般的访存指令,我们需要用一个寄存器加上一个立即数来指示一个内存单元,如lw rt,imm(rs)
;这个立即数就是访存地址的偏移量,16位立即数可以访问正负32K的空间,通常可以满足访问地址偏移量的需求
对于运算指令,如addi rt,rs,imm
;虽然无法满足全部需求,但可以满足大多数情况下的需求,这一点上就体现出x86的CISC指令系统的优势,对x86指令来说,如果想用更大宽度的立即数,它可以很容易扩展,因为它的指令本来就没有限制长度,但是MIPS指令总长度就是32位,再加上各个寄存器位域的使用,所以I型指令最多只能使用16位立即数
addi $21,$22,-50 # $21=$22+(-50)
addi可以让一个源操作数为立即数(这里的立即数指存储器地址),而add指令的操作数必须都是寄存器
查指令编码表可知:
opcode=8
分析指令可知:
rs=22(源操作数寄存器编号)、rt=21(目的操作数寄存器编号)、immediate=-50(立即数)
将这些数转化为二进制,可得二进制机器码:
001000(opcode)10110(rs)10101(rt)1111111111001110(immediate)
Branch,分支指令是用于改变控制流的指令,其实就相当于x86中的转移指令
在MIPS中,分支指令也分为条件分支和非条件分支
Conditional Branch
条件分支:根据比较的结果改变控制流
两条指令:branch if equal(beq);
、branch if not equal(bne)
Unconditional Branch
非条件分支:无条件地改变控制流
一条指令:jump(j)
beq rs,rt,imm # opcode=4
bne rs,rt,imm # opcode=5
格式:
beq reg1,reg2,L1
if (value in reg1)==(value in reg2)
goto L1
以beq
指令为例,共有三个操作数,前两个是寄存器操作数,第三个是存储器地址,即立即数,CPU会判断第一个寄存器第一个寄存器中的数和第二个寄存器中的数是否相等,如果相等就跳转到L1所指向的寄存器单元取出下一条指令,否则,顺序执行beq
之后的那条指令
注:这里的条件分支与x86转移指令有很大不同,MIPS没有标志位,而是在一条指令中既进行了比较又完成了转移,MIPS的全称,就是为了减少指令流水线的互锁,也就是要尽量避免不同指令之间的相互影响,而标志位这件事很明显就是前一条指令运行的结果,可能会对后面的某条指令产生影响,这是MIPS指令设计时要尽量避免的
举例:
//c语言代码
if(i == j)
f = g + h;
else
f = g - h;
s3和s4中保存了i和j这两个变量,如果它们内容相同,会转移到True:后的语句,执行加分指令,也就对应于f=g+h;如果它们不等,则会顺序地执行下一条指令,也就是减法指令,对应于f=g-h,执行完之后会跳过这条加法指令,然后进入后面的代码Fin
#MIPS汇编语言代码
beq $s3,$s4,True # branch i==j
sub $s0,$s1,$s2 # f=g-h(false)
j Fin # goto Fin
True: add $s0,$s1,$s2 # f=g+h(true)
Fin:...
从条件分支指令的格式可以看出,目标地址只能使用16bit的位移量,如何充分发挥16bit的作用?
以当前PC(在MIPS中,指向下一条指令地址的寄存器叫PC,类似于x86中的IP寄存器)为基准,16bit位移量可以表示 ± 2 15 ±2^{15} ±215bytes
MIPS的指令长度固定为32-bit(word),每条指令的位置一定会在四个字节对齐的地方,地址的最低两位肯定为0,因此16-bit位移量可以表示 ± 2 17 ±2^{17} ±217Bytes= ± 2 15 ±2^{15} ±215words (目标地址的范围扩大四倍,可以达到±128KB)
此时目标地址的计算方法:(这里的数字均指代Byte)
PC = PC + 4 = next instruction
PC = (PC + 4) + (immediate * 4)
这里的(+4)和(*4)是怎么来的呢?这就涉及到了内存模型,我们知道,一个地址始终对应着8bit即一个Byte,而如果这是一条指令的话(MIPS的一条指令是32bit即4Byte),那么相邻的四个地址(四个Byte)必然是属于这个指令的,由于我们每次拿到的PC都是这个指令的首地址,也就是4个Byte中的第一个Byte的地址,因此在他后面的第一条指令最起码也应该是4个地址之后了,因此下一条指令的首地址:next instruction = PC = PC + 4
而我们也得知了,每四个地址可以被看作是一个指令组,因此为了尽可能地利用immediate的16个bit去寻找更多的内存地址,我们使每一个bit都指代一个指令组即4Bytes,这就是这里的immediate*4的由来
条件分支语句,有两个寄存器用于比较条件,如果我们不需要判断条件,就可以想办法扩大目标地址的范围,当然理想条件下是直接使用32位地址,但还是因为MIPS指令长度固定为32位,而每条指令至少需要有一个opcode域指示它的指令类型(占用6个bit),那我们把剩下的26个bit全部用于目标地址,这就是J型指令
考虑到之前提到的一个指令组=4Bytes的情况,J型指令的目标地址计算方法为:当前pc加四之后,取最高的四位(考虑溢出),再加上J型指令编码中的26位,然后在末尾添上两个0(这里把第一条指令的地址默认为...0000
,那么四个字节过后的第二个字节的地址为...0100
,以此类推,每一个指令首地址的最后两位必然是0)
New PC = {(PC + 4)[31..28], address, 00}
此时虽然目标地址的范围还不能达到4G空间,但比之前的条件分支指令已经扩大了很多
J型指令的目标地址范围: ± 2 28 B y t e s ( ± 256 M B ) ±2^{28}Bytes(±256MB) ±228Bytes(±256MB)
如何到达更远的目标地址?可以2次调用j指令以扩大范围,但是有些不方便
可以使用jr
指令:jr rs
,将要转移的目标地址放到寄存器(32bit)中,这样就可以使用32位的目标地址了,但是这样的指令无法用J型指令来实现,但是可以用R指令来实现,只用占用其中一个寄存器位域,然后新增一种funct的编码就可以了
假设变量和寄存器对应关系如下:
f->$s0
、g->$s1
、h->$s2
、i->$s3
、j->$s4
C语言代码如下:
if (i == j)
f = g + h;
else
f = g - h;
asm汇编代码如下:
bne $s3,$s4,Else
add $s0,$s1,$s2
j Exit
Else: sub $s0,$s1,$s2
Exit:...