文章目录
指令格式
指令的定义
指令格式
零地址指令
- 某些堆栈型的计算机当中有可能会把操作数隐含在栈顶和次栈顶,然后计算的结果被我们压回栈顶,对于这种情况我们并不是不需要操作数,而是操作数会固定地隐含在某一个特定的位置
一地址指令
- 情况2:其中的一个操作数由地址码显式地指明,而另一个操作数隐式地隐含在ACC累加寄存器当中
- 由于运算结果是储存在ACC累加寄存器当中,所以存结果这个操作不需要访存
二地址指令
- 二地址指令会默认把操作结果存回目的操作数的所在的地址
三地址指令
- 三地址指令会显式地指明我们运算的结果要存放在哪个地址
四地址指令
- 四地址指令 = 三地址指令 + 下一条将要执行指令的地址
- 在四地址指令中,每次执行指令之后都会将PC的值修改为A4所指向的地址,即A4指明的是我们下一条应该执行的指令的地址,这样我们就可以让指令跳着执行而不是顺序地执行
- 指令总长度不变的情况下,地址码的数量越多,每个地址码的位数就越少,寻址能力就越差
指令—按地址码数目分类
指令—按指令长度分类
- 一台计算机的机器字长和存储字长都是固定不变的,但是一台计算机的指令字长是有可能发生改变的
指令—按操作码长度分类
指令—按操作类型分类
- 因为PC指明的是下一条指令的存放地址,所以要改变程序执行的顺序只需要改变PC的值即可,转移操作都是改变PC的值从而实现改变程序的执行顺序的
扩展操作码指令格式
- 这里的短码可以联系三地址指令的4位操作码,而长码可以类比二地址指令的8位操作码,也就是不允许短操作码和长操作码的前边部分的代码相同,二地址指令的操作码前面4位全为1,意味着三地址指令的操作码不能是1111
- 操作码越短意味着这个操作码的指令译码和分析的时间可以变短,电路设计起来更简单,使用频率高的指令,我们给它分配更短的操作码意味着我们可以有更大的概率使得我们操作码的译码变得更快,思想类似于哈夫曼编码
- 对于三地址指令,我们需要保留后边的12位用来表示3个地址,总共16位的指令就会只剩下开头的4位用来表示操作码,现在我们需要表示15条三地址指令,那么操作码的范围就是
0000
~1110
,总共有15种状态,会留下1111
作为扩展操作码 - 二地址指令要求有12条,首先对于二地址指令来说,它开头的4个bit一定是
1111
,由于我们只需要保留2个地址,每个地址是4位,因此我们只能用中间的4位来表示12种状态,分别对应12条二地址指令的操作码,可以取0到11,也就是0000
~1011
- 大于1011的数还剩下
1100
、1101
、1110
、1111
,我们发现剩余的几种状态最高位的2位都是全1的,那么可以得到如果开头的6位都是全1那么就超出了二地址指令的一个范围,现在我们要设计的是一地址指令,一地址指令只需要保留最后4位来表示地址,我们可以用剩下的6个bit来表示不同的一地址指令,我们需要6bit来分别表示0~61
这样的62种状态来表示一地址指令就可以,对应二进制就是000000
~111101
111110
这样的一个状态开始就超出了一地址指令所能表示的范围,所以当有11个bit全为1的时候已经超出了一地址指令操作码的一个范围,即1111 1111 111X
,最后我们需要有32条零地址指令,那我们剩下的5个bit刚好可以表示0~31
这个数字的范围,所以零地址指令的范围就是后面这5个bit分别是00000
~11111
这32种状态- 上面是另一种扩展操作码的例子,我们需要根据每一种地址指令需要有多少条,根据这样的条件来设计出合理的扩展操作码
- 那么在这种方式下CPU是怎样解析一条指令的呢?读入指令后CPU会判断前4位,如果前4位不是全1的状态就说明这是一条三地址指令,那么CPU会根据这4位操作码的指示按照三地址指令的规则去执行这一条指令;如果CPU检测到开始的4位是
1111
的话,那么CPU会检查后面的两位是不是11
,如果不是11
的话那就说明这是一条二地址指令,那么CPU就会根据前8位来判断出这是一条什么样的二地址指令;如果CPU检测到前面的6位是1111 11
,那么它还会继续检测后面跟着的5位是不是1111 1
,如果后面的5位跟着的不是1111 1
,那么意味着这是一条一地址指令,那么CPU就可以判断这是一条什么样的一地址指令,然后根据这条指令进行相应的操作;如果CPU发现前面的11位是1111 1111 111
,那么CPU就可以知道这是一条零地址指令,然后CPU根据这一整串的操作码信息来判断这条零地址指令需要做什么,然后CPU执行相应的操作就可以 - 我们的这种方式保证了操作码更短的指令不可能是长操作码指令的一个前缀,这样就不会产生歧义
指令寻址
顺序寻址
- 主存是按字编址的,这样CPU每取走一条指令过后只需要简单地让PC程序计数器的值加1,指向下一条指令就可以
- 每一条汇编指令和机器指令是一一对应的,同样也会有操作码和地址码,每一条指令占1个存储字
- 刚开始PC的值会指向这段程序的第一条指令,第一条指令存放在0这个地址,由于指令字长和存储字长都是2个字节,所以CPU只需要根据PC所指向的这个地址取出这一整个存储字的内容,那这个内容就是第一个指令,那这条指令被取出之后PC的值会自动加1指向下一条应该执行的指令
- 这条指令执行结束之后接下来CPU又会根据PC所指向的这个位置取出下一条指令,然后继续让PC加1,这就是这一系列的指令顺序执行的过程
- 如果现在主存是按字节编址,那么意味着每一条指令会占2个地址,第一条指令的前8个bit对应的地址是0,后8个bit对应的字节地址应该是1;第二条指令的前8个bit对应的地址是2,后8个bit对应的字节地址应该是3…所以如果改成这样的条件,那么我们的CPU每取一条指令之后,应该让PC的值加2
- 如果主存的编址方式发生了改变,那么我们对PC的相应处理也会发生改变
- 如果系统采用的是变长指令字结构,且主存是按照字节编址的,在上图中我们把不同的指令用不同的颜色区分开来,很明显不同的指令此时的指令字长都各不相同
- 刚开始PC的指向肯定是指向0这个地址,由于此时CPU无法确定这条指令到底占几个存储字,那在这种情况下CPU可以首先读入第一个字的内容,由于操作码被包含在了第一个字里面,所以CPU可以根据里边隐含的操作码来判断出这条指令到底是几地址的指令,这样的话就可以确定这条指令它总共占了多少个字节,那么对于上面的例子来说,CPU现在可以确定第一条指令现在占了4个字节,也就是2个存储字,因此CPU还会紧接着取入后面的两个字节,这样就得到了一个完整的指令,那在取指令结束之后CPU会把PC的值加上n,n指的是我们刚才取出的这条指令的总字节数,所以PC接下来就指向了4这个地址,也就是下一条指令的位置
跳跃寻址
- 可以根据转移类的指令来指出接下来应该执行的指令在什么位置
- 对于3这个地址所存储的JMP指令,CPU在取出这条指令之后同样会让PC的值加1,也就是指向4这个地址,但是CPU执行这条JMP指令的过程中发现这条指令是一条无条件转移指令,JMP也就是jump,跳转的意思,JMP后面跟着的数字表示的是下一条要执行的指令存放在什么位置,类似C语言的goto语句,所以执行这条语句后的效果就是让PC的值强行地变为7这个值,改变了程序的执行流,通过这种转移指令让CPU跳跃地找到了下一条指令的存放位置
数据寻址
- 我们在运行一条指令的时候,这条指令的地址码所指明的真实地址到底是什么,确定地址码的真实含义
指令寻址 v.s. 数据寻址
- 其实就是我们要如何正确地解读地址码的正确含义
直接寻址
间接寻址
- A1前面是个1,表示我们还需要继续往下寻址,以这个存储单元里面存储的数据作为地址去查找下一个存储单元
- 接下来的存储单元EA刚开始的1位是个0,表示这个存储单元所保存的这个地址就是最终的操作数的有效地址
- 多级间接寻址的形式类似于函数的调用过程,便于我们编址程序,方便完成子程序返回
寄存器寻址
- 我们的地址码不是指向某一个主存单元,而是指向了某一个寄存器,也就是寄存器的编号
寄存器间接寻址
隐含寻址
立即寻址
#
表示立即寻址特征
偏移寻址
基址寻址
- 基址寻址有什么作用呢?
- 基址寄存器的内容是由操作系统负责管理的,程序员无法修改
- 程序员可以用汇编语言来修改某个通用寄存器的值,但是如果某个通用寄存器被我们指定作为基址寄存器来使用的话,那么接下来这个通用寄存器里边的值我们不可以随便修改,必须由操作系统来负责管理了
变址寻址
- 变址寻址有什么作用?
- 对于这种方式,每一次循环我们都要对应一条指令,导致我们编程的极大不方便并且很不灵活,未来如果我们要增加循环的次数的话我们还需要在这条指令后面,再增加一些加法指令
基址&变址复合寻址
相对寻址
拓展:硬件如何实现数的“比较”
堆栈寻址
- 堆栈指针是存放在专门的一个寄存器中的,意味着我们的操作数不用显式地给出,其存放地址其实是隐含在SP这个寄存器当中的
(SP)+1→SP
或者(SP)-1→SP
都是由硬件帮我们自动完成的
- 软堆栈:从主存当中划分出一片区域,用这片主存区作为堆栈(
push
和pop
操作需要一次访存) - 硬堆栈:用寄存器实现(不用访存,速度快)
高级语言于机器级代码之间的对应
高级语言——汇编语言——机器语言
x86汇编语言指令基础
以mov指令为例
- 蓝色标识的表示的是一个寄存器
- 紫色表示的是一个立即数
- 绿色带中括号
[ ]
的表示的是一个内存地址,前面的一长串前缀表明我们此时要读写多少个字节,dword ptr
双字32bit、word ptr
单字16bit、byte ptr
字节8bit,其中ptr
是pointer
缩写,即指针,ptr
是固定格式写法,不能省略
x86架构CPU有哪些寄存器?
- 每一个寄存器都是以
e
开头的,e
表示的是extended
,表示这个寄存器的总长是32bit EAX EBX ECX EDX
这几个寄存器都是以X
结尾的,X
表示的是未知,也就是表明这几个寄存器是通用寄存器- EAX 是"累加器"(accumulator), 它是很多加法乘法指令的缺省寄存器;EBX 是"基地址"(base)寄存器, 在内存寻址时存放基地址;ECX 是计数器(counter), 是重复(REP)前缀指令和LOOP指令的内定计数器;EDX 则总是被用来放整数除法产生的余数
ESI EDI
这两个寄存器是以I
结尾,I是index的缩写,索引编号的意思,表示这两个寄存器是变址寄存器,S=Source D=Destination
,这两个寄存器通常用于处理线性表或者字符串- ESI/EDI分别叫做"源/目标索引寄存器"(source/destination index),因为在很多字符串操作指令中,ESI指向源串,而EDI指向目标串
- EBP:堆栈基指针Base Pointer,指向栈底;ESP:堆栈顶指针Stack Pointer,指向栈顶,这两个堆栈寄存器用于实现函数调用
- 如果我们只想使用32位通用寄存器的低16bit,我们可以把寄存器代号前面的e去掉,即
AX BX CX DX
- 但是下面的变址寄存器和堆栈寄存器就不能像上面的通用寄存器那样使用,只能按照固定的32bit来使用
- 除此之外,我们对4个32位通用寄存器的使用可以更加灵活,可以只使用8bit,如
AH AL BH BL CH CL DH DL
;H,high
,表示高位的8bit;L,low
,表示低位的8bit
更多例子
[寄存器]
,中括号里面有可能不是一个数字,而是一个寄存器的名字,这样的意思是括号内的寄存器里面保存的值所指向的这个地址,类似于寄存器间接寻址- 若未指明主存读写数据长度,也就是没有类似
dword ptr
、byte ptr
这种字符,则默认为32bit,也就是默认为dword ptr
常用的x86汇编指令
常见的算数运算指令
- 当操作数有d和s的时候,最终的运算结果会被放到d所指的那个位置
- 所以这也意味着d代表的操作数它只可能来自寄存器或者主存,不可能是一个常量
- imul中的i表示的是integer,有符号整数;mul表示无符号数乘
- 我们可以看到除操作中div或者idiv后面都只跟了一个操作数s,除法一定是被除数/除数,我们的s表示的是除数,而被除数会被提前存放到edx和eax两个寄存器当中,这里采用的是隐含寻址
edx:eax
,在进行运算之前需要把被除数进行位扩展,比如32位的被除数除以32位的除数,我们需要把这个被除数扩展为64位,用64位的被除数除以32位的除数,要存放64位的被除数就需要2个寄存器了,每个以e开头的寄存器都是32位,所以要存放64位的被除数,我们就需要把两个寄存器把它们连在一起使用,更高的32位存放在edx,更低的32位存放在eax- 除法的商会存入eax寄存器当中;余数会存入edx寄存器当中
<reg>
—寄存器<mem>
—内存<con>
—常量- 在x86汇编语言当中,不允许两个操作数都同时来自于主存,这样主要是为了保证每一条指令不要访问太多次主存,因为访问主存的次数越多这条指令的执行速度肯定也就越慢
常见的逻辑运算指令
- 当操作数有d和s的时候,最终的运算结果会被放到d所指的那个位置
- 所以这也意味着d代表的操作数它只可能来自寄存器或者主存,不可能是一个常量
- CPU的内部控制单元CU会依次执行这些指令,当它确定它所对应的功能之后,CU控制单元会给ALU算术逻辑单元发送与这条指令相应的一个控制信号,比如说是进行与运算,那么d和s两个操作数分别被送到ALU的两个输入端,现在控制单元CU又给ALU发来了和这个运算相关的这个控制信号,那么ALU就会把d和s相与的结果从输出端这边输出,那这就是执行一条指令的原理
其他指令
AT&T格式 v.s. Intel格式
- AT&T源操作数和目的操作数的位置刚好是相反的
- AT&T寄存器名之前必须加
%
- AT&T立即数之前必须加
$
- AT&T中的主存地址表示是用小括号
()
- AT&T指令后加b、w、l分别表示读写长度位byte、word、dword,未加默认读写长度为32bit
- AT&T看到小括号我们就知道是一个主存地址,那如果说我们在小括号这个主存地址基础上还需要再偏移若干个单位的话,我们需要把偏移量写在这个小括号的左边、前面这个位置
- AT&T中
偏移量(基址,变址,比例因子)
等同于Intel中[基址+变址*比例因子+偏移量]
基地址+变址*比例因子
可以找到我们想要访问的某一个数组元素,而一个数组元素里边可能会包含很多的变量,类似于结构体,那么我们到底要访问哪个变量哪几个字节就需要用加上偏移量的方式来指明
选择语句机器级表示
程序中的选择语句(分支结构)
无条件转移指令——jmp
- jmp,无条件转移指令,类似于C语言里的goto语句
- 程序员不可能知道自己的这条指令最终会被放到什么位置,所以如果只能使用上面的三种方法来实现jmp指令对程序员来说是很难受的
- 改进思路:用
NEXT:
“标号”锚定位置
- 无条件转移指令无法实现if-else逻辑
条件转移指令——jxxx
- 条件转移指令一般需要和cmp指令配合使用
选择语句机器级表示示例
扩展:cmp指令的底层原理
循环语句机器级表示
用条件转移指令实现循环
用loop指令实现循环
- Looptop标号,标记了循环的开始位置
- 中间做某些处理就是循环里边要做的事情,需要程序员自己填充
- 最后的地方是用到了loop指令,loop到Looptop,听起来像是一个绕口令(bushi,这一句指令所做的事情是首先会对我们用来作为循环计数器的ecx寄存器里边的值自动减1,然后判断ecx里边的值是否等于0,如果不等于0,那么就继续循环跳转回Looptop所标明的这个位置继续循环
- 在x86中ecx寄存器一般用于循环的计数器,loop指令就是默认指定对ecx进行ecx–的操作,因此如果需要使用loop指令那么肯定需要配合着使用ecx这个寄存器,用ecx里的值判断是否还需要继续进行循环
- loopnz:not zero
- loopz:zero
CISC和RISC
- 两种设计方向
- 注意看,这里我们的乘法指令也需要访问主存,去主存的对应单元里取数,但是在RISC中只有Load和Store指令可以访存,所以这里一定不是RISC,而是CISC
- 正也因为如此,RISC中程序员也许会长期占用很多个寄存器,所以RISC这种设计方式一定需要准备足够多的寄存器
- CISC难以用优化编译生成高效的目标代码程序,但是RISC可以采用优化的编译程序,生成较为高效的代码
- 这就好比C语言中,我们可以使用基础语法的同时C语言也给我们提供了很多的用得上用不上的库函数,我们当然可以通过一句代码调用一个库函数来完成某一个复杂的功能,但是如果这个库函数实现的不是特别好,执行效率不是特别高,那我们也没办法修改库函数来提高效率,因为CISC指令系统中的电路都是设计好的,没办法修改;而对于RISC中相当于我们只使用C语言的最基础的语法,当我们要实现某个复杂功能的时候,我们不可以通过直接调用某个库函数,但是我们用的是最基本的语法,所以我们完全可以自己通过优化自己的基础语句来优化我们的代码,让我们的代码执行起来更高效