4 指令系统
4.1 指令系统
**指令(机器指令)**是指示计算机执行某种操作的命令。
指令系统:一台计算机的所有指令的集合构成该机的指令系统,也称指令集。指令系统是指令集体系结构(ISA)中最核心的部分,ISA完整定义了软件和硬件之间的接口,是机器语言或汇编语言程序员所应熟悉的。
ISA规定的内容主要包括:指令格式,数据类型及格式,操作数的存放方式,程序可访问的寄存器个数、位数和编号,存储空间的大小和编址方式,寻址方式,指令执行过程的控制方式等。
4.1.1 指令的基本格式
一条指令就是机器语言的一个语句,它是一组有意义的二进制代码。一条指令通常包括操作码字段和地址码字段两部分:
-
操作码:操作码指出指令中该指令应该执行什么性质的操作以及具有何种功能。
- 操作码是识别指令、了解指令功能及区分操作数地址内容的组成和使用方法等的关键信息。
- 例如,指出是算术加运算还是算术减运算,是程序转移还是返回操作。
-
地址码:地址码给出被操作的信息(指令或数据)的地址。包括参加运算的一个或多个操作数所在的地址、运算结果的保存地址、程序的转移地址、被调用的子程序的入口地址等。
-
指令的长度:指一条指令中所包含的二进制代码的位数。
- 指令字长取决于操作码的长度、操作数地址码的长度和操作数地址的个数。
- 指令长度与机器字长没有固定的关系,它可以等于机器字长,也可以大于或小于机器字长。
- 通常,把指令长度等于机器字长的指令称为单字长指令,
- 指令长度等于半个机器字长的指令称为半字长指令,
- 指令长度等于两个机器字长的指令称为双字长指令。
-
定长指令字结构:在一个指令系统中,若所有指令的长度都是相等的,则称为定长指令字结构。
- 定字长指令的执行速度快,控制简单。
-
变长指令字结构:若各种指令的长度随指令功能而异,则称为变长指令字结构。然而,因为主存一般是按字节编址的,所以指令字长多为字节的整数倍。
根据指令中操作数地址码的数目的不同,可将指令分成以下几种格式。
-
零地址指令:只给出操作码OP,没有显式地址。
- 1)不需要操作数的指令,如空操作指令、停机指令、关中断指令等。
- 2)零地址的运算类指令仅用在堆栈计算机中。通常参与运算的两个操作数隐含地从栈顶和次栈顶弹出,送到运算器进行运算,运算结果再隐含地压入堆栈。
-
一地址指令
-
1)只有目的操作数的单操作数指令,按A1地址读取操作数,进行OP操作后,结果存回原地址。
- 指令含义:OP(A1)→A1
- 如加1、减1、取反、求补等。
-
2)隐含约定目的地址的双操作数指令,按指令地址A1可读取源操作数,指令可隐含约定另一个操作数由ACC(累加器)提供,运算结果也将存放在ACC中。
- 指令含义:(ACC)OP(A1)→ACC
- 若指令字长为32位,操作码占8位,1个地址码字段占24位,则指令操作数的直接寻址范围为224= 16M
注:A1指某个主存地址,(A1)表示A1所指向的地址中的内容
-
-
二地址指令
- 对于常用的算术和逻辑运算指令,往往要求使用两个操作数,需分别给出目的操作数和源操作数的地址,其中目的操作数地址还用于保存本次的运算结果。
- 指令含义:(A1)OP(A2)→A1
- 完成一条指令需要访存4次,取指→读A1→读A2→写A1
-
三地址指令
- 常用于需要两个操作数的算术运算、逻辑运算相关指令
- 指令含义:(A1)OP(A2)→A3
- 完成一条指令需要访存4次,取指→读A1→读A2→写A3
-
四地址指令
- 执行指令后,将PC的值修改位A4所指地址
- 指令含义:(A1)OP(A2)→A3,A4=下一条将要执行指令的地址。
- 完成一条指令需要访存4次,取指→读A1→读A2→写A3
n位地址码的直接寻址范围=2n,若指令总长度固定不变,则地址码数量越多,寻址能力越差
4.1.2 定长操作码指令格式
定长操作码指令在指令字的最高位部分分配固定的若干位(定长)表示操作码。
一般位操作码字段的指令系统最大能够表示2n条指令。定长操作码对于简化计算机硬件设计,提高指令译码和识别速度很有利。当计算机字长为32位或更长时,这是常规用法。
4.1.3 扩展操作码指令格式
为了在指令字长有限的前提下仍保持比较丰富的指令种类,可采取可变长度操作码,即全部指令的操作码字段的位数不固定,且分散地放在指令字的不同位置上。显然,这将增加指令译码和分析的难度,使控制器的设计复杂化。
-
设计方案
-
1)不允许短码是长码的前缀,即短操作码不能与长操作码的前面部分的代码和同。
-
2)各指令的操作码一定不能重复。
通常情况下,对使用频率较高的指令,分配较短的操作码;对使用频率较低的指令,分配较长的操作码,从而尽可能减少指令译码和分析的时间。
设地址长度为n,上一层留出m种状态,下一层可扩展出m×2n种状态。
-
-
对比
- 定长操作码
- 优:定长操作码对于简化计算机硬件设计,提高指令译码和识别速度很有利;
- 缺:指令数量增加时会占用更多固定位,留给表示操作数地址的位数受限。
- 扩展操作码
- 优:在指令字长有限的前提下仍保持比较丰富的指令种类;
- 缺:增加了指令译码和分析的难度,使控制器的设计复杂化。
- 定长操作码
4.1.4 指令的操作类型
设计指令系统时必须考虑应提供哪些操作类型,指令操作类型按功能可分为以下几种。
-
数据传送
- MOV:传送指令通常有寄存器之间的传送
- LOAD:从内存单元读取数据到CPU寄存器
- STORE:从CPU寄存器写数据到内存单元
-
算术和逻辑运算
- 算术:加、减、乘、除、增1、减1、求补、浮点运算、十进制运算
- 逻辑:与、或、非、异或、位操作、位测试、位清除、位求反
-
移位操作
- 算术移位、逻辑移位、循环移位(带进位和不带进位)
-
转移操作
-
无条件转移(JMP):无条件转移指令在任何情况下都执行转移操作
-
条件转移(JZ):结果为0;(JO):结果溢出;(JC):结果有进位
-
调用(CALL)和返回(RETURN)
调用指令和转移指令的区别:执行调用指令时必须保存下一条指令的地址(返回地址),当子程序执行结束时,根据返回地址返回到主程序继续执行;而转移指令则不返回执行。
-
陷阱(Trap)与陷阱指令
-
-
输入输出操作
- 成CPU与IO端口交换数据或传送控制命令及状态信息
4.2 指令的寻址方式
寻址方式是指寻找指令或操作数有效地址的方式,即确定本条指令的数据地址及下一条待执行指令的地址的方法。寻址方式分为指令寻址和数据寻址两大类。
4.2.1 指令寻址和数据寻址
寻址方式分为指令寻址和数据寻址两大类。寻找下一条将要执行的指令地址称为指令寻址;寻找本条指令的数据地址称为数据寻址。
-
指令寻址
指令寻址方式有两种:一种是顺序寻址方式,另一种是跳跃寻址方式。
- 顺序寻址:通过程序计数器PC加1(1个指令字长),自动形成下一条指令的地址。
- 跳跃寻址:通过转移类指令实现。执行转移类指令导致的PC值改变,跳跃的地址分为绝对地址(由标记符直接得到)和相对地址(相对于当前指令地址的偏移量)。
-
数据导址
数据寻址是指如何在指令中表示一个操作数的地址,如何用这种表示得到操作数或怎样计算出操作数的地址。
指令格式:
4.2.2 常见的数据寻址方式
-
隐含寻址
不是明显地给出操作数的地址,而是在指令中隐含着操作数的地址。
例,单地址指令不指出第二操作数的地址,而规定累加器(ACC)作为第二操作数地址,指令格式明显指出的仅是第一操作数的地址。累加器(ACC)对单地址指令格式是隐含地址。
-
优点:有利于缩短指令字长。
-
缺点:需增加存储操作数或隐含地址的硬件。
-
-
立即(数)寻址
形式地址A就是操作数本身,又称为立即数,一般采用补码形式。图中#表示立即寻址特征。
-
执行:取指令访存1次,执行指令访存0次
-
优点:执行指令阶段不访问主存,执行指令时间最短
-
缺点:A的位数限制了立即寻址的范围。如A的位数为n,且立即数采用补码时,可表示-2n-1~2n-1-1
-
-
直接寻址
指令字中的形式地址 A是操作数的真实地址EA,即EA=A。
- 执行:取指令访存1次、执行指令访存1次,暂不考虑存结果,共访存2次
- 优点:简单,指令执行阶段仅访问一次主存,不需专门计算操作数的地址。
- 缺点:A的位数决定了该指令操作数的寻址范围,操作数的地址不易修改
-
间接导址
指令的地址字段给出的形式地址不是操作数的真正地址,而是操作数有效地址所在的存储单元的地址,也就是操作数地址的地址,即EA=(A)。
主存字第一位为1时,表示取出的不是操作数地址,即多次间接寻址
主存字第一位为0时,表示取得的是操作所的地址
- 执行:取指令访存1次,执行指令访存2次,暂不考虑存结果,共访存3次,
- 优点:可扩大寻址范围(有效地址EA的位数大于形式地址A的位数)。便于编制程序(用间接寻址可以方便地完成子程序返回)。
- 缺点:指令在执行阶段要多次访存(一次间址需两次访存,多次寻址需根据存储字的最高位确定几次访存)。
-
寄存器寻址
在指令字中直接给出操作数所在的寄存器编号,即EA=Ri,其操作数在由Ri所指的寄存器内。
- 执行:取指令访存1次,执行指令访存0次,暂不考虑存结果,共访存1次
- 优点:指令在执行阶段不访问主存,只访问寄存器,指令字短且执行速度快,支持向量/矩阵运算。
- 缺点:寄存器价格昂贵,计算机中寄存器个数有限
-
寄存器间接寻址
寄存器Ri中给出的不是一个操作数,而是操作数所在主存单元的地址,即EA=(Ri)
- 执行:取指令访存1次,执行指令访存1次,暂不考虑存结果,共访存2次
- 特点:与一般间接寻址相比速度更快,但指令的执行阶段需要访问主存(因为操作数在主存中)。
-
相对寻址
把程序计数器PC的内容加上指令格式中的形式地址A而形成操作数的有效地址,即EA=(PC)+A,其中A是相对于PC所指地址的位移量,可正可负,补码表示。
当CPU从存储器中取出一字节时,会自动执行(PC)+1→PC。
当前指令存放地址=1000,若当前指令字长=2B,则PC+2;若当前指令字长=4B,则PC+4。因此取出当前指令后PC可能为1002 or 1004
- 优点:这段代码在程序内浮动时不用更改跳转指令的地址码。相对寻址广泛应用于转移指令。
-
基址寻址
将CPU中基址寄存器(BR)的内容加上指令格式中的形式地址A,而形成操作数的有效地址,即EA=(BR)+A
注:基址寄存器是面向操作系统的,其内容由操作系统或管理程序确定。在程序执行过程中,基址寄存器的内容不变(作为基地址),形式地址可变(作为偏移量)。
采用通用寄存器作为基址寄存器时,可由用户决定哪个寄存器作为基址寄存器,但其内容仍由操作系统确定。
- 优点:可扩大寻址范围(基址寄存器的位数大于形式地址A的位数);用户不必考虑自己的程序存于主存的哪个空间区域,因此有利于多道程序设计,并可用于编制浮动程序,但偏移量的位数较短。
-
变址寻址
有效地址EA等于指令字中的形式地址A与变址寄存器IX的内容相加之和,即EA=(IX)+A,其中Ix可为变址寄存器(专用),也可用通用寄存器作为变址寄存器
注:变址寄存器是面向用户的,在程序执行过程中,变址寄存器的内容可由用户改变(IX作为偏移量),形式地址A不变(作为基地址)
- 优点:在数组处理过程中,可设定A为数组的首地址,不断改变变址寄存器IX的内容,便可很容易形成数组中任一数据的地址,特别适合编制循环程序。
基址寻址和变址寻址区别
基址寻址面向系统,主要用于为多道程序或数据分配存储空间,因此基址寄存器的内容通常由操作系统或管理程序确定,在程序的执行过程中其值不可变,而指令字中的A是可变的;
变址寻址立足于用户,主要用于处理数组问题,在变址寻址中,变址寄存器的内容由用户设定,在程序执行过程中其值可变,而指令字中的A是不可变的。 -
堆栈寻址
操作数存放在堆栈中,隐含使用堆栈指针(SP)作为操作数地址。
堆栈是存储器(或专用寄存器组)中一块特定的按“后进先出(LIFO)”原则管理的存储区,该存储区中被读/写单元的地址是用一个特定的寄存器给出的,该寄存器称为堆栈指针(SP)。
寄存器堆栈又称硬堆栈。寄存器堆伐的成本较高,不适合做大容量的堆栈;而从主存中划出一段区域来做堆栈是最合算且最常用的方法,这种堆栈称为软堆栈。
堆栈可用于函数调用时保存咨前函数的相关信息,堆栈寻址大多数都是无操作数,因为操作数地址隐含使用SP,如POP和PUSH指令。
- 访存次数:硬堆栈不访存,软堆栈访存1次
下面简单总结寻址方式、有效地址及访存次数(不含取本条指令的访存)
寻址方式 | 有效地址 | 访存次数 |
---|---|---|
隐含寻址 | 程序指定 | 0 |
立即寻址 | A即是操作数 | 0 |
直接寻址 | EA=A | 1 |
一次间接寻址 | EA=(A) | 2 |
寄存器寻址 | EA=Ri | 0 |
寄存器间接一次寻址 | EA=(Ri) | 1 |
相对寻址 | EA=(PC)+A | 1 |
基址寻址 | EA=(BR)+A | 1 |
变址寻址 | EA=(IX)+A | 1 |
4.3 程序的机器级代码表示
4.3.1 常用汇编指令介绍
-
相关寄存器
x86处理器中有8个32位的通用寄存器,为了向后兼容,EAX、EBX、ECX和EDX的高两位字节和低两位字节可以独立使用,E为Extended,表示32位。
除EBP和ESP外,其他几个寄存器的用途是比较任意的。
-
汇编指令格式
使用不同的编程工具开发程序时,用到的汇编程序也不同,一般有两种不同的汇编格式:AT&T格式和Intel格式。
-
AT&T格式的指令只能用小写字母,而Intel格式的指令对大小写不敏感
-
在AT&T格式中,第一个为源操作数,第二个为目的操作数,方向从左到右,合乎自然;在Intel格式中,第一个为目的操作数,第二个为源操作数,方向从右向左
-
在AT&T格式中,寄存器需要加前缀“%”,立即数需要加前缀“$”;在Intel格式中,寄存器和立即数都不需要加前缀。
-
在内存寻址方面,AT&T格式使用“(”和“)“、而tel格式使用“[”和“]”。
-
在处理复杂寻址方式时,例如AT&T格式的内存操作数“disp(base,idex,scae)”分别表示偏移量、基址寄存器、变址寄存器和比例因子,如“8(%edx,%eax,2)”表示操作数为M[R[ed]+R[eax]*2+8],其对应的Intel格式的操作数为“[edx+eax*2+8]”
-
在指定数据长度方面,AT&T格式指令操作码的后面紧跟一个字符,表明操作数大小,"b"表示byte(字节)、“w”表示word(字)或“l”表示long(双字)。Intel格式也有类似的语法,它在操作码后面显式地注明byte ptr、word ptr或dword ptr。
由于32或64位体系结构都是由16位扩展而来的,因此用word (字)表示16位
下表展示两种格式的几条不同指令。
-
-
常用指令
汇编指令通常可以分为数据传送指令、逻辑计算指令和控制流指令,下面以Intel格式为例,介绍一些重要的指令。以下用于操作数的标记分别表示寄存器、内存和常数。
-
数据传送指令
-
mov 指令
将第二个操作数 (寄存器的内容、内存中的内容或常数值) 复制到第一个操作数(寄存器或内存)。但不能用于直接从内存复制到内存。
其语法如下:
mov <reg>,<reg> mov <reg>,<mem> mov <mem>,<reg> mov <reg>,<con> mov <mem>,<con>
举例:
mov eax,ebx #将ebx值复制到eax mov byte ptr [var],5 #将5保存到var值指示的内存地址的一字节中
-
push指令
将操作数压入内存的栈,常用于函数调用。ESP是栈顶,压栈前先将ESP值减4(栈增长方向与内存地址增长方向相反),然后将操作数压入ESP指示的地址。
其语法如下:
push <reg32> push <mem> push <con32>
举例(注意,栈中元素固定32位):
push eax #将eax值压栈 push [var] #将var值指示的内存地址的4字节值压栈
-
pop指令
与push指令相反,pop指令执行的是出栈工作,出栈前先将ESP指示的地址中的内容出栈,然后将ESP值加4。
其语法如下:
pop edi #弹出栈顶元素送到edi pop [ebx] #弹出栈顶元素送到ebx值指示的内存地址的4字节中
-
-
常见算数运算指令
除法运算中,s为除数,被除数被提取安排到edx:eax中
-
add/sub指令:add指令将两个操作数相加,相加的结果保存到第一个操作数中。sub指令用于两个操作数相减,相减的结果保存到第一个操作数中。
其语法如下:
add <reg>,<reg> / sub <reg>,<reg> add <reg>,<mem> / sub <reg>,<mem> add <mem>,<reg> / sub <mem>,<reg> add <reg>,<con> / sub <reg>,<con> add <mem>,<con> / sub <mem>,<con>
举例:
sub eax,10 #eax ← eax-10 add byte ptr [var],10 #10与var值指示的内存地址的一字节值相加,并将结果保存在原位置
-
inc/dec指令:inc、dec指令分别表示将操作数自加1、自减1。
其语法如下:
inc <reg> / dec <reg> inc <mem> / dec <mem>
举例:
dec eax #eax值自减1 inc dword ptr [var] #var值指示的内存地址的4字节值自加1
-
imul指令。带符号整数乘法指令,有两种格式:
- ①两个操作数,将两个操作数相乘,将结果保存在第一个操作数中,第一个操作数必须为寄存器;
- ②三个操作数,将第二个和第三个操作数相乘,将结果保存在第一个操作数中,第一个操作数必须为寄存器。
imul <reg32>,<reg32> imul <reg32>,<mem> imul <reg32>,<reg32>,<con> imul <reg32>,<mem>,<con>
举例:
imul eax,[var] #eax ← eax * [var] imul esi,edi,25 #esi ← edi * 25
-
idiv指令:带符号整数除法指令,它只有一个操作数,即除数,而被除数则为edx:eax中的内容(64位整数),操作结果有两部分:商和余数,商送到eax,余数则送到edx。
其语法如下:
idiv <reg32> idiv <mem>
举例:
idiv ebx idiv dword ptr [var]
-
-
常见逻辑运算指令
-
and/or/xor指令。and、or、xor指令分别是逻辑与、逻辑或、逻辑异或操作指令,用于操作数的位操作,操作结果放在第一个操作数中。
语法如下:
and <reg>,<reg> / or <reg>,<reg> / xor <reg>,<reg> and <reg>,<mem> / or <reg>,<mem> / xor <reg>,<mem> and <mem>,<reg> / or <mem>,<reg> / xor <mem>,<reg> and <reg>,<con> / or <reg>,<con> / xor <reg>,<con> and <mem>,<con> / or <mem>,<con> / xor <mem>,<con>
举例:
and eax,0fh #将eax中的前28位全部置为0,最后4位保持不变 xor edx,edx #置edx中的内容为0
-
not指令。位翻转指令,将操作数中的每一位翻转,即0→1、1→0。
语法如下:
not <reg> not <mem>
举例:
not byte ptr [var] #将var值指示的内存地址的一字节的所有位翻转
-
neg指令。取负指令。
语法如下:
neg <reg> neg <mem>
举例:
neg eax #eax ← -eax
-
shl/shr 指令。逻辑移位指令,shl为逻辑左移,shr 为逻辑右移,第一个操作数表示被操
作数,第二个操作数指示移位的位数。语法如下:
shl <reg>,<con8> / shr <reg>,<con8> shl <mem>,<con8> / shr <mem>,<con8> shl <reg>,<cl> / shr <reg>,<cl> shl <mem>,<cl> / shr <mem>,<cl>
举例:
shl eax,1 #将eax值左移1位 shr ebx,cl #将ebx值右移位(n为c1中的值)
-
-
控制流指令
x86处理器维持着一个指示当前执行指令的指令指针(IP),当一条指令执行后,此指针自动指向下一条指令。IP寄存器不能直接操作,但可以用控制流指令更新。
通常用**标签(label)**指示程序中的指令地址,在x86汇编代码中,可在任何指令前加入标签。例如,
mov esi,[ebp+8] Begin: xor ecx,ecx mov eax,[esi]
这样就用Begin指示了第二条指令,控制流指令通过标签就可以实现程序指的跳转。
-
jmp 指令。jmp 指令控制IP转移到label所指示的地址(从 label 中取出指令执行)。
语法如下:
jmp <label>
举例:
jmp begin #转跳到begin标记的指令执行
-
jcondition指令。条件转移指令,依据CPU状态字中的一系列条件状态转移。CPU状态字中包括指示最后一个算术运算结果是否为0,运算结果是否为负数等。根据条件码ZF和SF来实现转跳。
语法如下:
je <label> #equal,若a==b则跳转,ZF==1 ? jne <label> #not equal,若a!=b则跳转,ZF==0 ? jg <label> #greater than,若a>b则跳转,ZF==0 && SF==OF ? jge <label> #greater than or equal to,若a>=b则跳转,SF==OF ? jl <label> #less than,若a<b则跳转,SF!=OF ? jle <label> #less than or equal to,若a<=b则跳转,SF!=OF||ZF==1
举例:
cmp eax, ebx #比较寄存器eax和ebx里的值 jg NEXT #若 eax> ebx,则跳转到 NEXT:
-
cmp/test指令。cmp指令用于比较两个操作数的值(同sub),test指令对两个操作数进行逐位与运算,这两类指令都不保存操作结果,仅根据运算结果设置CPU状态字中的条件码。
语法如下:
cmp <reg>,<reg> / test <reg>,<reg> cmp <reg>,<mem> / test <reg>,<mem> cmp <mem>,<reg> / test <mem>,<reg> cmp <reg>,<con> / test <reg>,<con>
注意,不能同时将内存,常数为地址码
cmp <mem>,<con> / test <mem>,<con>
举例:
cmp dword ptr [var],10 #将var指示的主存地址的4字节内容,与10比较 jne loop #如果相等则继续顺序执行;否则跳转到10p处执行 test eax,eax #测试eax是否为零 jz xxxx #为零则置标志ZF为1,转跳到xxxx处执行
-
call/ret指令。分别用于实现子程序(过程、函数等)的调用及返回。
语法如下:
call <label> ret
call指令首先将当前执行指令地址入栈,然后无条件转移到由标签指示的指令。与其他简单的跳转指令不同,call指令保存调用之前的地址信息(当call指令结束后,返回调用之前的地址)。
ret指令实现子程序的返回机制,ret指令弹出栈中保存的指令地址,然后无条件转移到保存的指令地址执行。cal和ret是程序(函数)调用中最关键的两条指令。
-
-
4.3.2 选择语句的机器级表示
常见的选择结构语句有if-then、if-then-else、case(或switch)等。编译器通过条件码(标志位)设置指令和各类转移指令来实现程序中的选择结构语句。
-
条件码(标志位)
除了整数寄存器,CPU还维护一组条件码(标志位)寄存器,它们描述了最近的算术或逻辑运算操作的属性。可以检测这些寄存器来执行条件分支指令,最常用的条件码如下:
- CF:进(借)位标志。最近无符号整数加(减)运算后的进(借)位情况。有进(借)位时,CF=1;否则CF=0。
- ZF:零标志。最近的操作的运算结果是否为0。若结果为0,ZF=1;否则ZF=0。
- SF:符号标志。最近的带符号数运算结果的符号。若为负,SF=1;否则SF=0。
- OF:溢出标志。最近的带符号数运算结果是否溢出。若溢出,OF=1;否则OF=0。
可见,OF和SF对无符号数运算来说没有意义,而CF对带符号数运算来说没有意义。
-
if语句
if-else 语句的通用形式如下:
if(a>b){ c=a; } else{ c=b; }
对应汇编代码如下:
mov eax,7 #假设变量a=7,存入eax mov ebx,6 #假设变量b=6,存入ebx cmp eax,ebx #比较变量a和b jg NEXT #若a>b,转移到NEXT: move ecx,ebx #假设用ecx存储变量c,令c=b ——> else 部分的逻辑 jmp END #无条件转移到END: NEXT: mov ecx,eax #假设用ecx存储变量c,令c=a ——> if 部分的逻辑 END:
此时if和else的顺序与原代码相反,可以通过对跳转条件取反(jg→jle),使其顺序与原代码相同:
mov eax,7 #假设变量a=7,存入eax mov ebx,6 #假设变量b=6,存入ebx cmp eax,ebx #比较变量a和b jle NEXT #若a≤b,转移到NEXT: move ecx,eax #假设用ecx存储变量c,令c=a ——> if 部分的逻辑 jmp END #无条件转移到END: NEXT: mov ecx,ebx #假设用ecx存储变量c,令c=b ——> else 部分的逻辑 END:
4.3.3 循环语句的机器级表示
常见的循环结构语句有while、for和do-while。汇编中没有相应的指令存在,可以用条件测试和转跳组合起来实现循环的效果,大多数编译器将这三种循环结构都转换为d0-while形式来产生机器代码。在循环结构中,通常使用条件转移指令来判断循环条件的结束。
-
for循环
int result = 0; for(int i=1;i<=100;i++){ result+=i; }//求1+2+3+...+100
-
while循环
int i = 1; int result = 0; while(i<=100){ result +=i; i++; }//求1+2+3+...+100
-
汇编指令翻译循环
mov eax,0 #用 eax保存 result,初值为0 mov edx,1 #用 edx保存 i,初始值为1 cmp edx,100 #比较 i和100 jg L2 #若i>100,转跳到 L2 执行 L1: #循环主体 add eax,edx #实现 result +=i inc edx #inc 自增指令,实现 i++ cmp edx,100 #比较i和100 jle L1 #若 i<=100,转跳到 L1 执行 L2: #跳出循环主体
用条件转移指令实现循环,需要4个部分构成:
- ①循环前的初始化——1、2句
- ②是否直接跳过循环?——3、4句
- ③循环主体——6、7句
- @是否继续循环?——8、9句
-
用loop指令实现循环
C语言形式:
for(int i=500;i>0;i--){ 做某些处理 }//循环500轮
汇编形式
mov ecx 500 #用ecx作为循环计数器 Looptop: #循环的开始 ... 做某些处理 ... loop Looptop #ecx--,若ecx!=0,跳转到Looptop
loop指令默认每轮对ecx进行减小操作,只能用ecx存轮次数。loop指令等价于:
dec ecx cmp ecx,0 jne Looptop
使用loop指令可能会使代码更清晰简洁,与跳转区分开来。
补充:loopx指令一-如loopnz,loopz
loopz——当ecx!=0&&ZF==0时,继续循环
loopnz——当ecx!=0&&ZF==1时,继续循环
4.3.4 过程调用的机器级表示
上面提到的call/ret指令主要用于过程调用,它们都属于一种无条件转移指令。
-
高级语言的函数调用
函数的栈帧(Stack Frame):保存函数大括号内定义的局部变量、保存函数调用相关的信息
假定过程P(调用者)调用过程Q(被调用者),过程调用的执行步骤如下:
- 1)P将入口参数(实参)放在Q能访问到的地方。
- 2)P将返回地址存到特定的地方,然后将控制转移到Q。
- 3)Q保存P的现场(通用寄存器的内容),并为自己的非静态局部变量分配空间。
- 4)执行过程Q。
- 5)Q恢复P的现场,将返回结果放到P能访问到的地方,并释放局部变量所占空间。
- 6)Q取出返回地址,将控制转移到P。
步骤2是由call指令实现的,步骤6由ret指令返回到过程P。
-
如何访问栈帧
栈的位置:
-
栈寄存器
- ebp:指向当前栈的“底部"
- esp:指向当前栈的“顶部"
-
利用push、pop的入栈出栈操作(push、pop默认以4字节为单位)
- Push操作:先让esp减4,再将数据压入
- Pop操作:栈顶元素写入寄存器或主存地址,再让esp加4
-
利用mov的入栈出栈操作
mov指令,结合esp、ebp 指针访问栈数据
注:可以用减法/加法指;即sub/add修改找顶指针esp的值。
-
-
切换栈帧
-
call指令
- ①将IP旧值压栈保存 (保存在函数的栈帧顶部)
- ②设置IP新值、无条件转移至被调用函数的第一条指令
-
return指令
从函数的栈顶部找到IP旧值,将其出栈并恢复IP寄存器
-
enter指令(更新ebp)
等价于push ebp + move ebp,esp;保存上一层函数的栈帧基地址;0地址指令
-
leave指令(更新esp)
等价于mov esp,ebp + pop ebp;让esp指向当前栈帧底部
-
-
栈内内容
- gcc编译器将每个栈帧大小设置为16B的整数倍(当前函数的除外),因此栈帧内可能出现空闲未使用的区域。
- 通常将局部变量集中存储在栈帧底部区域
- 通常将调用参数集中存储在栈帧顶部区域
- 栈帧最底部一定是上一层栈基址(ebp旧值)
- 栈帧最顶部一定是返回地址(当前函数的栈帧除外)
-
函数调用时栈的变化
4.4 CISC和RISC的基本概念
4.4.1 复杂指令系统计算机(CISC)
-
设计思路:一条指令完成一个复杂的基本功能。
-
代表:x86架构,主要用于笔记本、台式机等
-
特点:
- 1)指令系统复杂庞大,指令数目一般为200条以上。
- 2)指令的长度不固定,指令格式多,寻址方式多。
- 3)可以访存的指令不受限制。
- 4)各种指令使用频度相差很大。
- 5)各种指令执行时间相差很大,大多数指令需多个时钟周期才能完成。
- 6)控制器大多数采用微程序控制。有些指令非常复杂,以至于无法采用硬连线控制。
- 7)难以用优化编译生成高效的目标代码程序。
-
问题
80-20规律:典型程序中80%的语句仅仅使用处理机中20%的指令
4.4.2 精简指令系统计算机(RISC)
- 设计思路:一条指令完成一个基本“动作”,多条指令组合完成一个复杂的基本功能。
- 代表:ARM架构、MIPS架构,主要用于手机、平板等
- 特点:
- 1)选取使用频率最高的一些简单指令,复杂指令的功能由简单指令的组合来实现。
- 2)指令长度固定,指令格式种类少,寻址方式种类少。
- 3)只有Load/Store(取数/存数)指令访存,其余指令的操作都在寄存器之间进行。
- 4)CPU中通用寄存器的数量相当多。
- 5)RISC一定采用指令流水线技术,大部分指令在一个时钟周期内完成。
- 6)以硬布线控制为主,不用或少用微程序控制。
- 7)特别重视编译优化工作,以减少程序执行时间。
4.4.3 CISC和RISC的比较
类别 | CISC | RISC |
---|---|---|
指令系统 | 复杂,庞大 | 简单,精简 |
指令数目 | 一般大于200条 | 般小于100条 |
指令字长 | 不固定 | 定长 |
可访存指令 | 不加限制 | 只有Load/store指令 |
各种指令执行时间 | 相差较大 | 绝大多数在一个周期内完成 |
各种指令使用频度 | 相差很大 | 都比较常用 |
通用寄存器数量 | 较少 | 多 |
目标代码 | 难以用优化编译生成高效的目标代码程序 | 采用优化的编译程序,生成代码较为高效 |
控制方式 | 绝大多数为微程序控制 | 绝大多数为组合逻辑控制 |
指令流水线 | 可以通过一定方式实现 | 必须实现 |