注:内容参考王道2024考研复习指导
指令系统
指令集体系结构
机器指令(简称指令)是指示计算机执行某种操作的命令。一台计算机的所有指令的集合构成该机的指令系统,也称指令集。指令系统是指令集体系结构(ISA)中最核心的部分,ISA完整定义了软件和硬件之间的接口。
ISA规定的内容主要包括:
1)指令格式,指令寻址方式,操作类型,以及每种操作对应的操作数的相应规定。
2)操作数的类型,操作数寻址方式,以及是按大端方式还是按小端方式存放。
3)程序可访问的寄存器编号、个数和位数,存储空间的大小和编址方式。
4)指令执行过程的控制方式等,包括程序计数器、条件码定义等。
指令格式
指令
又称机器指令,是指示计算机执行某种操作的命令,是计算机运行的最小功能单位。
一台计算机的所有指令的集合构成该机的指令系统,也称为指令集。
指令格式
一条指令就是机器语言的一个语句,是一组有意义的二进制代码。
一条指令通常包括操作码和地址码字段两部分。
指令-按地址码数目分类
指令-按指令长度分类
定长指令字结构:指令系统中所有指令的长度都相等。
变长指令字结构:指令系统中各种指令的长度不等。
关于字不同字长的知识:
指令字长:一条指令的总长度,一般取字节的整数倍(可能会变)。
机器字长:CPU进行一次整数运算所能处理的二进制数据的位数(通常和ALU直接相关)。
存储字长:一个存储单元中的二进制代码位数(通常和ALU直接相关)。
根据指令长度是机器字长的多少倍,可以分为半字长指令、单字长指令、双字长指令。
注:指令字长会影响取指令所需时间。如:机器字长=存储字长=16bit,则取一条双字长指令需要两次访存。
指令-按操作码长度分类
定长操作码:指令系统中所有指令的操作码长度都相同。
可变长操作码:指令系统中各指令的操作码长度可变。
定长指令字结构+可变长操作码 → \rightarrow →扩展操作码指令格式(不同地址数的指令使用不同长度的操作码)。
指令-按操作类型分类
扩展操作码
指令字长为16位,每个地址码占4位:前4位为基本操作码字段OP,另有3个4位长的地址字段A1、A2和A3。
4位基本操作码若全部用于三地址指令,则有16条。但至少须将1111留作扩展操作码之用,即三地址指令为15条。
1111 1111留作扩展操作码之用,二地址指令为15条;
1111 1111 1111留作扩展操作码之用,一地址指令为15条;零地址指令为16条。
在设计扩展操作码指令格式时,必须注意以下两点:
- 不允许短码是长码的前缀,即短操作码不能与长操作码的前
面部分的代码相同。 - 各指令的操作码一定不能重复。
对使用频率较高的指令,分配较短的操作码;对使用频率较低的指令,分配较长的操作码,从而尽可能减少指令译码和分析的时间。
举例:
说明:地址长度为n,上一层留出m种状态,下一层可扩展出 m ∗ 2 n m*2^n m∗2n种状态。
指令的寻址方式
寻址范围和寻址空间
寻址空间一般指的是CPU对于内存寻址的能力。通俗地说,就是能最多用到多少内存的一个问题,即能够寻址的最大容量。
寻址范围仅仅是一个数字范围,不带有单位,而寻址范围的大小很明显是一个数,指寻址区间的大小。
注:M为数量单位。1024=1K,1024K=1M。MB指容量大小。1024B=1KB,1024KB=1MB。 如寻址范围为1M,寻址空间为1MB。
指令寻址
指令寻址,下一条欲执行指令的地址: ( P C ) + 1 → P C (PC)+1 \rightarrow PC (PC)+1→PC(始终由程序计数器PC给出)。
注:每次取指令之后,PC一定会自动+1,指向下一条应该执行的指令(即当前取完指令1,未执行,PC已经指向指令2)。
现假设该系统采用定长指令字结构,且指令字长=存储字长=16bit=2B,若主存按字编址则PC=PC+1;若主存按字节编址则PC=PC+2。
再该系统采用变长指令字结构,主存按字节编址,读入一个字,根据操作码判断这条指令的总字节数n,则PC=PC+n
顺序寻址
( P C ) + 1 → P C (PC) + 1 \rightarrow PC (PC)+1→PC,此处的“1”理解为一个指令字长,实际值会因指令长度、编址方式而不同。
跳跃寻址
由转移指令给出。
跳跃的方式分为绝对转移(地址码直接指出转移目标地址)和相对转移(地址码指出转移目的地址相对于当前PC值的偏移量),由于CPU总是根据PC的内容去主存取指令的,因此转移指令执行的结果是修改PC值,下一条指令仍然通过PC给出。
数据寻址
数据寻址,确定本条指令的地址码指明的真实地址。在指令字中设置一个寻址特征字段,用来指明属于哪种寻址方式(其位数决定了寻址方式的种类)。
直接寻址
指令字中的形式地址A就是操作数的真实地址EA,即EA=A。
一条指令的执行:取指令访存1次,执行指令访存1次,暂不考虑存结果,共访存2次。
优点:简单,指令执行阶段仅访问一次主存,不需专门计算操作数的地址。
缺点:A的位数决定了该指令操作数的寻址范围,操作数的地址不易修改。
间接寻址
指令的地址字段给出的形式地址不是操作数的真正地址,而是操作数有效地址所在的存储单元的地址,也就是操作数地址的地址,即EA=(A)。
优点:可扩大寻址范围(有效地址EA的位数大于形式地址A的位数),便于编制程序(用间接寻址可以方便地完成子程序返回)。
缺点:指令在执行阶段要多次访存(一次间址需两次访存,多次寻址需根据存储字的最高位确定几次访存)。
寄存器寻址
在指令字中直接给出操作数所在的寄存器编号,即EA =Ri,其操作数在由Ri所指的寄存器内。
一条指令的执行:取指令访存1次,执行指令访存0次,暂不考虑存结果,共访存1次。
优点:指令在执行阶段不访问主存,只访问寄存器,指令字短且执行速度快,支持向量/矩阵运算。
缺点:寄存器价格昂贵,计算机中寄存器个数有限。
寄存器间接寻址
寄存器Ri中给出的不是一个操作数,而是操作数所在主存单元的地址,即EA=(Ri)。
一条指令的执行:取指令访存1次,执行指令访存1次,暂不考虑存结果,共访存2次。
特点:与一般间接寻址相比速度更快,但指令的执行阶段需要访问主存(因为操作数在主存中)。
隐含寻址
不是明显给出操作数的地址,而是在指令中隐含着操作数的地址。
例如,单地址的指令格式就隐含约定第二个操作数由累加器(ACC)提供。
优点:有利于缩短指令字长,简化地址结构。
缺点:需增加存储操作数或隐含地址的硬件。
立即寻址
形式地址A就是操作数本身,又称为立即数,一般采用补码形式。#表示立即寻址特征。
一条指令的执行:取指令访存1次,执行指令访存0次,暂不考虑存结果,共访存1次。
优点:指令执行阶段不访问主存,指令执行时间最短。
缺点:A的位数限制了立即数的范围,如A的位数为n,且立即数采用补码时,可表示的数据范围为 − 2 n − 1 ∼ 2 n − 1 − 1 -2^{n-1} \sim 2^{n-1}-1 −2n−1∼2n−1−1。
偏移寻址
三种偏移寻址的区别在于偏移的”起点“不一样。
基址寻址
以程序的起始存放地址作为“起点”,将CPU中基址寄存器(BR)的内容加上指令格式中的形式地址A,而形成操作数的有效地址,即EA=(BR)+A。
拓展:程序运行前,CPU将BR的值修改为该程序的起始地址(存在操作系统PCB中)。
注:基址寄存器是面向操作系统的,其内容由操作系统或管理程序确定。在程序执行过程中,基址寄存器的内容不变(作为基地址),形式地址可变(作为偏移量);当采用通用寄存器作为基址寄存器时,可由用户决定哪个寄存器作为基址寄存器,但其内容仍由操作系统确定。
优点:可扩大寻址范围(基址寄存器的位数大于形式地址A的位数);用户不必考虑自己的程序存于主存的哪一空间区域,故有利于多道程序设计,以及可用于编制浮动程序(整个程序在内存里边的浮动),方便实现多道程序并发运行。
变址寻址
程序员自己决定从哪里作为“起点”,有效地址EA等于指令字中的形式地址A与变址寄存器IX的内容相加之和,即EA= (IX)+A,其中IX可为变址寄存器(专用),也可用通用寄存器作为变址寄存器。
注:变址寄存器是面向用户的,在程序执行过程中,变址寄存器的内容可由用户改变(IX作为偏移量),形式地址A不变(作为基地址),这是基址寻址与变址寻址的区别。
优点:在数组处理过程中,可设定A为数组的首地址,不断改变变址寄存器IX的内容,便可很容易形成数组中任一数据的地址,特别适合编制循环程序。偏移量的位数足以表示整个存储空间。
基址&变址复合寻址
先基址后变址寻址:EA=(IX)+(BR)+A
多种寻址方式复合使用,可理解为复合函数。
相对寻址
以程序计数器PC所指地址作为“起点“,把程序计数器PC的内容加上指令格式中的形式地址A而形成操作数的有效地址,即EA=(PC)+A,其中A是相对于PC所指地址的位移量,可正可负,补码表示。
优点:操作数的地址不是固定的,它随着PC值的变化而变化,并且与指令地址之间总是相差一个固定值,因此便于程序浮动(一段代码在程序内部的浮动)。相对寻址广泛应用于转移指令。
堆栈寻址
操作数存放在堆栈中,隐含使用堆栈指针(SP)作为操作数地址。
堆栈是存储器(或专用寄存器组)中一块特定的按“后进先出(LIFO)”原则管理的存储区,该存储区中被读/写单元的地址是用一个特定的寄存器给出的,该寄存器称为堆栈指针(SP)。
注:寄存器堆栈也称硬堆栈,硬堆栈的成本较高,不适合做大容量的堆栈。而从主存中划出一段区域来做堆栈是最合算且最常用的方法,这种堆栈称为软堆栈。
程序的机器代码表示
高级语言与机器级代码之间的对应
考试要求:题目给出某段简单程序的C语言、汇编语言、机器语言表示。能结合C语言看懂汇编语言的关键语句(看懂常见指令、选择结构、循环结构、函数调用)
汇编语言、机器语言一一对应,要能结合汇编语言分析机器语言指令的格式、寻址方式。
x86汇编语言指令基础
mov指令为例
mov指令功能:将源操作数s复制到目的操作数d所指的位置
- mov eax, ebx #将寄存器ebx的值复制到寄存器eax
- mov eax, 5 #将立即数5复制到寄存器eax
- mov eax, dword ptr [af996h] #将内存地址af996h所指的32bit值复制到寄存器eax
- mov byte ptr [af996h], 5 #将立即数5复制到内存地址af996h所指的一字节中
内存读写长度:dword ptr——双字,32bit,word ptr——单字,16bit,byte ptr——字节,8bit
若未指明主存读写长度,默认32 bit。
x86架构CPU中的寄存器
每个寄存器都是32bit。
- 通用寄存器:eax、ebx、ecx、edx(ax表示eax通用寄存器使用低16bit,al表示eax通用寄存器的第8位bit,ah表示eax的低16位bit的高8位bit)
- 变址寄存器:esi、edi
- 堆栈寄存器:ebp、esp
变址寄存器可用于线性表、字符串的处理,堆栈寄存器用于实现函数调用。
注:寄存器是由触发器构成的,一个触发器能够存储一位二进制码,所以把n个触发器的时钟端口连接起来就能构成一个存储n位二进制码的寄存器。
常用的x86汇编指令
注:双操作数指令的两个操作数不允许同为内存地址。
算数运算指令
逻辑运算指令
其他指令
数据传送指令
mov 指令
mov 指令将第二个操作数(寄存器、内存中或常数),复制到第一个操作数(寄存器、内存),但不能用于直接从内存复制到内存。
mov <reg>/<mem>, <reg>/<mem>/<con>
mov eax, ebx;
mov byte ptr [var], 5;
push 指令
push 指令将操作数压入内存的栈,常用于函数调用。栈中元素固定为 32 位。
push <reg32>/<mem>/<con32>
push eax;
push [var];//将 var 指示的内存地址的4字节值压入栈
pop 指令
pop 指令执行出栈工作,出栈前先将 ESP 指示的地址种内容取出栈,然后将 ESP 值加 4。
pop edi;//弹出栈顶元素送到 edi
pop [ebx];//弹出栈顶元素送到 ebx 值指示的内存地址的 4 字节中
算术和逻辑运算指令
add/sub 指令
add 指令将两个操作数相加,相加的结果保存到第一个操作数中。
sub 指令用于两个操作数相减,相减的结果保存到第一个操作数中。
add/sub <reg>/<mem>, <reg>/<mem>/<con>
sub eax, 10;//eax-10->eax
add byte ptr [var], 10;//10与var值指示的内存地址的一字节值相加,并将结果保存在与 var 值指示的内存地址的字节中
inc/dec 指令
inc、dec 指令分别表示操作数自加 1、自减 1。
inc/dec <reg>
inc/dec <mem>
dec eax;//eax 值自减 1
inc dword ptr [var];//var 值指示的内存地址的 4 字节值自加 1
imul 指令
带符号整数乘法指令,有两种格式:
- 两个操作数,将两个操作数相乘,并将结果保存在第一个操作数中,第一个操作数必须为寄存器。
- 三个操作数,将第二个和第三个操作数相乘,并将结果保存在第一个操作数中,第一个操作数必须为寄存器。
imul <reg32>,<reg32>/<mem>
imul <reg32>,<reg32>/<mem>,<con>
imul eax,[var];//eax*[var]->eax
imul esi,edi,25;//edi*25->esi
idiv 指令
带符号整数除法指令,只有一个操作数,即除数,而被除数则为 edx:eax 中的内容,64 位整数;操作结果分为两部分:商和余数,商送到 eax,余数送到 edx。
idiv <reg32>
idiv <mem>
idiv ebx
idiv dword ptr [var]
and/or/xor 指令
逻辑与、逻辑或、逻辑异或操作指令,用于操作数的位操作,操作结果放在第一个操作数中。
and <reg>/<mem>, <reg>/<mem>/<con>
and eax, ofH;//将eax中的前28位全部置零,最后4位不变
xor edx, edx;//置edx中的内容为0
not 指令
位翻转指令,将操作数的每一位翻转,即0⟶1,1⟶0。
not <reg>/<mem>
not byte ptr [var];//将 var 值指示的内存地址的一字节的所有位翻转。
neg 指令
取负指令。
neg <reg>/<mem>
neg eax;
shl/shr 指令
逻辑位移指令,shr 为逻辑右移,shl 表示逻辑左移,第一个操作数表示被操作数,第二个操作数指示移位的位数;语法格式如下:
shl <reg>/<mem>, <con8>/<cl>
shl eax, 1;//将eax的值左移1位相当乘于2
shr ebx, cl;//将ebx值右移n位n为cl 中的值,相当于除于2^n
控制流指令
X86 处理器维持这一个指示当前执行指令的指令指针(IP),当执行一条指令后,此指针自动指向下一条指令。IP 寄存器不能直接操作,但可以用控制流指令更新。通常用标签(label)指示程序中的指令地址,在 X86 汇编代码中,可在任何指令前加入标签,例如:
mov esi, [edp+8]
begin: xor ecx, ecx
mov eax, [esi]
AT&T格式和Intel格式
函数调用机器级表示
函数的栈帧(Stack Frame),保存函数大括号内定义的局部变量、保存函数调用相关的信息。
call、ret指令
call指令的作用:
- 将IP旧值压栈保存(保持在当前函数的栈帧顶部);
- 设置IP新值,无条件转移至被调用函数的第一条指令。
ret指令作用:
- 从函数的栈帧顶部找到IP旧值,将其出栈并恢复IP寄存器(x86处理器中程序计数器PC通常被称为IP)。
访问栈帧
函数调用栈在内存中的位置
栈底为高地址,栈顶为地址值,图示通常栈底在上,栈顶在下。
标记栈帧范围
通过堆栈基指针ebp和堆栈顶指针esp分别指向当前栈帧的底部和顶部。
对栈帧内数据的访问,都是基于ebp、esp进行的。
注:x86系统中,默认以4字节为栈的操作单位。
访问栈帧数据
- push、pop指令实现入栈、出栈操作。
push 🐶//先让esp-4,再将🐶压入栈
pop 🐶//栈顶元素出栈,并写入🐶,再让esp+4
栈底为高地址,栈顶为低地址。要想入栈,即指针地址变低,即-4;要想出栈,即指针地址变高,即+4。
- 可以用mov指令,结合esp、ebp指针访问栈帧数据。可以用减法/加法指令,即sub/add修改栈顶指针esp的值。
切换栈帧
当执行到caller函数的call指令时,IP指向下一条mov指令。具体执行流如下:
- IP旧值(即下一条mov指令地址)压入当前函数(caller)栈帧顶部(esp-4),同时设置IP新值(指向add函数的push指令),无条件转移至被调函数(add)的第一条指令。
- 执行add函数的push指令,将ebp(堆栈基地址)压入(esp-4)当前函数(add)的栈底部(IP自动加“1”)。
- 执行add函数的mov指令,将esp的值移动到ebp中,即将栈底指针指向此时的栈顶指针(此时esp指向add函数栈底,ebp未移动时指向caller函数栈底)。
注:指令enter等同于(push ebp;mov ebp,esp
)两条指令。指令leave等同于(mov esp,ebp;pop ebp
)两条指令。
- add函数指令顺序执行…
- 执行到add函数的leave指令(此时,ebp指向add函数栈底,esp指向add函数栈顶,栈底保存着caller函数的栈底地址),将ebp的值移动到esp中(转移后,add函数的整个栈帧清除,只剩下栈底4字节)。
- leave指令还会将栈顶的值出栈(此时add函数的栈顶值为caller函数的栈底),赋值给ebp(esp+4,回到caller函数栈帧,且指向栈顶;ebp指向caller函数的栈底)。
- add函数指向ret指令,将栈顶(即esp指向地址,已经是cller函数的栈帧了)的IP旧值出栈,恢复IP寄存器。
栈帧内容及如何传参
一个栈帧的内容
GCC编译器为保证数据的严格对齐而规定的每个函数的栈帧大小必须是16字节的倍数(当前函数的栈帧除外),因此栈帧内可能出现空闲未使用的区域。
- 通常将局部变量集中存储在栈帧底部区域(越后定义的局部变量越靠近栈底,即越在上方)
- 通常将调用参数集中存储在栈帧顶部区域,从右往左依次保存(第一个调用参数最靠近栈顶)。
- 栈帧最底部一定是上一层栈帧基址(ebp旧值)。
- 栈帧最顶部一定是返回地址(当前函数的栈帧除外)。
汇编代码示例
- caller函数前两行的push和mov是进入函数的操作,将上个函数的栈底位置压栈,同时让栈底指针指向这个位置。
- sub指令使得esp的值-24,即esp向下(低地址)移动24字节的大小,为caller函数开辟24字节大小的栈帧。
- 4-5行的mov指令表示局部变量的存放,每个局部变量是int型,占四个字节,有三个局部变量,最先定义的局部变量在最下面(靠近栈顶),即[esp-12]存放125(temp1)这个数据,第三个局部变量sum在[esp-4]的位置,未初始化。
- 6-9行的mov指令,进行函数调用的传参准备,函数需要传入两个参数x,y。先将[ebp-8](temp2)的值给eax寄存器,再从eax中转移到[esp+4](esp的上一行)作为参数y。再将[ebp-12](temp1)的值给eax寄存器,再从eax中转移到esp的位置作为参数x。
此时IP指向call指令,函数的栈帧如下图所示:
需要进行函数调用,即切换栈帧,具体内容在切换栈帧中有详细描述。
- add函数将[ebp+12](x)和[ebp+8](y)分别移动到寄存器,进行加法运算,最后将结果存储eax寄存器。
(注:此时的ebp=esp(未给add分配更多的栈帧空间),而esp经过切换栈帧,压入了IP旧值和caller栈底地址,esp-8)
- 执行add函数的leave和ret指令。
- 回到caller函数,11行的maov指令将add函数保存在eax的计算结果,赋值给ebp-4(sum)中。
- 12行mov指令将sum值转移到eax中,准备返回值。
总结
注:寄存器EAX、ECX和EDX是调用者保存寄存器,当P调用Q时,若Q需用到这些寄存器,则由P将这些寄存器的内容保存到栈中,并在返回后由P恢复它们的值。若保存,则紧挨着调用参数上方存储。
寄存器EBX、ESI、EDI是被调用者保存寄存器,当P调用Q时,Q必须先将这些寄存器的内容保存在栈中才能使用它们,并在返回P之前先恢复它们的值。
选择语句机器级表示
无条件转移指令——jmp
jmp <地址> #PC无条件转移至<地址>。
此处的地址可以是由常数给出,可以来自于寄存器,可已来自于主存。
同时,jmp NEXT #<地址>可以用“标号”(标签)锚定。
标号特征,有冒号,名字可以自己取。
条件转移指令——jxxx
指令 | 含义 |
---|---|
cmp a,b | #比较a和b两个数 |
je <地址> | #jump when equal,若a==b则跳转 |
jne <地址> | #jump when not equal,若a!=b则跳转 |
jg <地址> | #jump when greater than,若a>b则跳转 |
jge <地址> | #jump when greater than or equal to,若a>=b则跳转 |
jl <地址> | #jump when less than,若a<b则跳转 |
jle <地址> | #jump when less than or equal to,若a<=b则跳转 |
jbe <地址> | #jump when below or equal to,和jle一样的效果 |
主要依靠上述指令完成条件转移,使用cmp进行比较,通过比较结果进行跳转。
具体操作如下:
cmp eax,ebx//比较寄存器eax和edx里的值
jg NEXT//若eax>ebx,则跳转到NEXT
test eax,eax//测试eax是否为0
jz xxxx//为零则标志位ZF=1,跳转到xxxx执行
注:条件转移指令根据相应标志位进行条件判断
示例
注意使用不同的条件判断,代码结果会不一样。
如果按照C语言代码使用汇编语言条件指令,则紧跟着条件指令的代码部分为C语言代码中的else部分,而if代码部分则在标号之后。
如果使用C语言逻辑判断的反,使用汇编指令,则代码结构跟C语言代码相同。
循环语句机器级表示
常见的循环结构语句由while、for、do-while
,汇编中没有相应的指令存在,使用条件测试和跳转组合结合起来实现循环效果,大多数编译器将这三种循环结构都转化为do-while
形式产生机器代码。
用条件转移指令实现循环
用loop指令实现循环
loop Looptop=(dec ecx;cmp ecx,0;jne Looptop)
理论上,能用loop指令实现的功能一定能用条件转移指令实现,使用loop指令可能会使代码更清晰简洁。
补充:loopx指令——如loopnz, loopz;loopnz——当ecx!=0 && ZF==0时,继续循环;loopz——当ecx!=0 && ZF==1时,继续循环。
CISC和RISC
两种设计方向