第四章 指令系统

目录
4.1 指令系统
4.1.1 指令集体系结构
4.1.2 指令的基本格式
1.零地址指令
2.一地址指令
3.二地址指令
4.三地址指令
5.四地址指令
4.1.3 定长操作码指令格式
4.1.4 扩展操作码指令格式
4.1.5 指令的操作类型
1.数据传送
2.算术和逻辑运算
3.移位操作
4.转移操作
5.输入输出操作
4.2 指令的寻址方式
4.2.1 指令寻址和数据寻址
1.指令寻址
2.数据寻址
4.2.3 常见的数据寻址方式
1.隐含寻址
2.立即(数)寻址
3.直接寻址
4.间接寻址
5.寄存器寻址
6.寄存器间接寻址
7.相对寻址
8.基址寻址
9.变址寻址
10.堆栈寻址
4.3 程序的机器级代码表示
4.3.1 常用汇编指令介绍
1.相关寄存器
2.汇编指令格式
3.常用指令
(1)数据传送指令
1)mov指令
2)push指令
3)pop指令
(2)算术和逻辑运算指令
1)add/sub指令
2)inc/dec指令
3)imul指令
4)idiv指令
5)and/or/xor指令
6)not指令
7)neg指令
8)sh/shr指令
(3)控制流指令
1)jmp指令
2)jcondition指令
3)cmp/test指令
4)call/ret指令
4.3.2 选择语句的机器级表示
4.3.3 循环语句的机器级表示
(1)do-while循环
(2)while循环
(3) for 循环
4.3.4 过程调用的机器级表示
4.4 CISC和RISC的基本概念
4.4.1 复杂指令系统计算机(CISC)
4.4.2 精简指令系统计算机(RISC)
4.4.3 CISC和RISC的比较
4.1 指令系统
4.1.1 指令集体系结构
命题追踪 指令集体系结构(ISA)规定的内容(2022) 机器指令(简称指令)是指示计算机执行某种操作的命令。
一台计算机的所有指令的集合构成该机的指令系统,也称指令集。
指令系统是指令集体系结构(ISA)中最核心的部分,ISA完整定义了软件和硬件之间的接口,是机器语言或汇编语言程序员所应熟悉的。
ISA规定的内容主要包括: 1)指令格式,指令寻址方式,操作类型,以及每种操作对应的操作数的相应规定。 2)操作数的类型,操作数寻址方式,以及是按大端方式还是按小端方式存放。 3)程序可访问的寄存器编号、个数和位数,存储空间的大小和编址方式。 4)指令执行过程的控制方式等,包括程序计数器、条件码定义等。
ISA规定了机器级程序的格式,机器语言或汇编语言程序员必须对机器的ISA非常熟悉。
不过,大多数程序员不会用汇编语言或机器语言编写程序,通常用高级语言(如C/C++/Java)编写程序,这样开发效率更高,也不易出错。
但是,高级语言抽象层太高,隐藏了许多机器级程序的细节,使得高级语言程序员不能很好地利用与机器结构相关的一些优化方法来提升程序的性能。
若程序员对ISA和底层硬件实现细节有充分的了解,则可以更好地编制高性能程序。
4.1.2 指令的基本格式
一条指令就是机器语言的一个语句,它是一组有意义的二进制代码。
一条指令通常包括操作码字段和地址码字段两部分:
其中,操作码指出该指令应执行什么操作以及具有何种功能。
操作码是识别指令、了解指令功能及区分操作数地址内容等的关键信息。
例如,指出是算术加运算还是算术减运算,是程序转移还是返回操作。
地址码给出被操作的信息(指令或数据)的地址,包括参加运算的一个或多个操作数的地址、运算结果的保存地址、程序的转移地址、被调用子程序的入口地址等。
指令字长是指一条指令所包含的二进制代码的位数,其取决于操作码的长度、地址码的长度和地址码的个数。
指令字长与机器字长没有固定的关系,它既可以等于机器字长,又可以大于或小于机器字长。
通常,把指令长度等于机器字长的指令称为单字长指令,指令长度等于半个机器字长的指令称为半字长指令,指令长度等于两个机器字长的指令称为双字长指令。
注意 指令长度的不同会导致取指令时间开销的不同,单字长指令只需访存1次就能将指令完整取出;
而双字长指令则需访存2次才能完整取出,耗费2个存取周期。
在一个指令系统中,若所有指令的长度都是相等的,则称为定长指令字结构。
定字长指令的执行速度快,控制简单。
若各种指令的长度随指令功能而异,则称为变长指令字结构。
然而,因为主存一般是按字节编址的,所以指令字长通常为字节的整数倍。
根据指令中操作数 地址码的数目的不同,可将指令分成以下几种格式。
1.零地址指令
只给出操作码OP,没有显式地址。
这种指令有两种可能: 1)不需要操作数的指令,如空操作指令、停机指令、关中断指令等。
2)零地址的运算类指令仅用在堆栈计算机中。
通常参与运算的两个操作数隐含地从栈顶和次栈顶弹出,送到运算器进行运算,运算结果再隐含地压入堆栈。
2.一地址指令
这种指令也有两种常见的形态,要根据操作码的含义确定究竟是哪一种。
1)只有目的操作数的单操作数指令,按地址读取操作数,进行OP操作后,结果存回原地址。 指令含义: 如操作码含义是加1、减1、求反、求补、移位等。 2)隐含约定目的地址的双操作数指令,按指令地址可读取源操作数,指令可隐含约定另一个操作数由ACC(累加器)提供,运算结果也将存放在ACC中。 指令含义:
命题追踪 地址位数与寻址范围的关系(2010、2021) 若指令字长为32位,操作码占8位,1个地址码字段占24位,则指令操作数的直接寻址范围为。
若地址码字段均为主存地址,则完成一条一地址指令需要3次访存(取指令1次,取操作数1次,存结果1次)。
3.二地址指令
指令含义: 对于常用的算术和逻辑运算指令,往往要求使用两个操作数,需分别给出目的操作数和源操作数的地址,其中目的操作数地址还用于保存本次的运算结果。
若指令字长为32位,操作码占8位,两个地址码字段各占12位,则每个操作数的直接寻址范围为。
若地址码字段均为主存地址,则完成条二地址指令需要4次访存(取指令1次,取两个操作数2次,存结果1次)。
4.三地址指令
指令含义:。 若指令字长为32位,操作码占8位,3个地址码字段各占8位,则每个操作数的直接寻址范围为。
若地址码字段均为主存地址,则完成一条三地址需要4次访问存储器(取指令1次,取两个操作数2次,存结果1次)。
5.四地址指令
指令含义:,A4=下一条将要执行指令的地址。
若指令字长为32位,操作码占8位,4个地址码字段各占6位,则每个操作数的直接寻址范围为。
若地址码字段均为主存地址,则完成一条四地址指令需要4次访存(取指令1次,取两个操作数2次,存结果1次)。
4.1.3 定长操作码指令格式
命题追踪 定长操作码的指令条数(2015) 定长操作码指令在指令字的最高位部分分配固定的若干位(定长)表示操作码。
一般n位操作码字段的指令系统最大能够表示条指令。
定长操作码对于简化计算机硬件设计,提高指令译码和识别速度很有利。
当计算机字长为32位或更长时,这是常规用法。
4.1.4 扩展操作码指令格式
命题追踪 扩展操作码的设计与分析(2017、2021、2022) 为了在指令字长有限的前提下仍保持比较丰富的指令种类,可采取可变长度操作码,即全部指令的操作码字段的位数不固定,且分散地放在指令字的不同位置上。
显然,这将增加指令译码和分析的难度,使控制器的设计复杂化。
最常见的变长操作码方法是扩展操作码,它使操作码的长度随地址码的减少而增加,不同地址数的指令可具有不同长度的操作码,从而在满足需要的前提下,有效地缩短指令字长。
图4.1所示即为一种扩展操作码的安排方式。
在图4.1中,指令字长为16位,其中4位为基本操作码字段OP,另有3个4位长的地址字段、和。
4位基本操作码若全部用于三地址指令,则有16条。
图4.1中所示的三地址指令为15条,1111留作扩展操作码之用;
二地址指令为15条,11111111留作扩展操作码之用;
一地址指令为15条,111111111111留作扩展操作码之用;
零地址指令为16条。
除这种安排外,还有其他多种扩展方法,如形成15条三地址指令、12条二地址指令、63条一地址指令和16条零地址指令,共106条指令,请读者自行分析。
在设计扩展操作码指令格式时,必须注意以下两点: 1)不允许短码是长码的前缀,即短操作码不能与长操作码的前面部分的代码相同。 2)各指令的操作码一定不能重复。
通常情况下,对使用频率较高的指令分配较短的操作码,对使用频率较低的指令分配较长的操作码,从而尽可能减少指令译码和分析的时间。
4.1.5 指令的操作类型
设计指令系统时必须考虑应提供哪些操作类型,指令操作类型按功能可分为以下几种。
1.数据传送
传送指令通常有寄存器之间的传送(MOV)、从内存单元读取数据到 CPU寄存器(LOAD)、从 CPU 寄存器写数据到内存单元(STORE)、进栈操作(PUSH)、出栈操作(POP)等。
2.算术和逻辑运算
这类指令主要有加(ADD)、减(SUB)、乘(MUL)、除(DIV)、加 1(INC)、减1(DEC)、与(AND)、或(OR)、取反(NOT)、异或(XOR)等。
3.移位操作
移位指令主要有算术移位、逻辑移位、循环移位等。
4.转移操作
命题追踪 转跳指令、调用和返回指令、条件转移指令的区分(2019) 转移指令主要有无条件转移(JMP)、条件转移(BRANCH)、调用(CALL)、返回(RET)、陷阱(TRAP)等。
无条件转移指令在任何情况下都执行转移操作,
而条件转移指令仅在特定条件满足时才执行转移操作,转移条件一般是某个标志位的值,或几个标志位的组合。
调用指令和转移指令的区别:
执行调用指令时必须保存下一条指令的地址(返回地址),当子程序执行结束时,根据返回地址返回到主程序 继续执行;
而转移指令则不返回执行。
5.输入输出操作
这类指令用于完成CPU与外部设备交换数据或传送控制命令及状态信息。
4.2 指令的寻址方式
寻址方式是指寻找指令或操作数 有效地址的方式,即确定本条指令的数据地址及下一条待执行指令的地址的方法。
寻址方式分为指令寻址和数据寻址两大类。
4.2.1 指令寻址和数据寻址
寻找下一条将要执行的 指令地址称为指令寻址;
寻找本条指令的数据地址称为数据寻址。
1.指令寻址
指令寻址方式有两种:
一种是顺序寻址方式,
另一种是跳跃寻址方式。
(1)顺序寻址
通过程序计数器PC加1(1条指令的长度),自动形成下一条指令的地址。 命题追踪 PC自增大小与编址方式、指令字长的关系(2013、2014、2019、2023) 注意 PC自增的大小与编址方式、指令字长有关。
现代计算机通常是按字节编址的,
若指令字长为16位,则PC自增为(PC)+2;
若指令字长为32位,则PC自增为(PC)+4。
(2)跳跃寻址
通过转移类指令实现。
跳跃是指由本条指令给出下条指令地址的计算方式。
而是否跳跃可能受到状态寄存器的控制,跳跃的方式分为绝对转移(地址码直接指出转移目标地址)和相对转移(地址码指出转移目的地址相对于当前PC值的偏移量),
CPU总是根据PC的内容去主存取指令的,因此转移指令执行的结果是修改PC值,下一条指令仍然通过PC给出。
2.数据寻址
命题追踪 指令格式中各字段的位数分析(2020) 数据寻址是指如何在指令中表示一个操作数的地址,或怎样计算出操作数的地址。
数据寻址的方式较多,为区别各种方式,通常在指令字中设置一个寻址特征字段,用来指明属于哪种寻址方式(其位数决定了寻址方式的种类),由此可得指令的格式如下所示: 命题追踪 指令格式中寻址特征字段的作用(2023) 指令中的地址码字段并不代表操作数的真实地址,这种地址称为形式地址(A)。
形式地址结合寻址方式,可以计算出操作数在存储器中的真实地址,这种地址称为有效地址(EA)。 ● 若为立即寻址,则形式地址的位数决定了操作数的范围。 ● 若为直接寻址,则形式地址的位数决定了可寻址的范围。 ● 若为寄存器寻址,则形式地址的位数决定了通用寄存器的最大数量。 ● 若为寄存器间接寻址,则寄存器的位数决定了可寻址的范围。
注意 (A)表示地址为A的数值,A既可以是寄存器编号,又可以是内存地址。
4.2.3 常见的数据寻址方式
1.隐含寻址
这种类型的指令不明显地给出操作数的地址,而是隐含操作数的地址。
例如,单地址的指令格式就隐含约定第二个操作数由累加器(ACC)提供,指令中只明显指出第一个操作数的地址。 因此,累加器(ACC)对单地址指令格式来说是隐含寻址,如图4.2所示。
优点是有利于缩短指令字长;
缺点是需增加存储操作数或隐含地址的硬件。
2.立即(数)寻址
命题追踪 立即寻址的概念(2023)
指令字中的地址字段指出的不是操作数的地址,而是操作数本身,也称立即数,采用补码表示。
图4.3所示为立即寻址示意图,图中#表示立即寻址特征,A就是操作数。 优点是指令在执行阶段不访存,指令执行速度最快;
缺点是A的位数限制了立即数的范围。
3.直接寻址
指令字中的形式地址A就是操作数的真实地址EA,即EA=A,如图4.4所示。
优点是简单,不需要专门计算操作数的地址,指令在执行阶段仅需访存一次;
缺点是A的位数限制了该指令操作数的寻址范围,操作数的地址不易修改。
4.间接寻址
间接寻址是相对于直接寻址而言的,指令的地址字段给出的不是操作数的真正地址,而是操作数有效地址所在主存单元的地址,也就是操作数地址的地址,即EA=(A),如图4.5所示。
优点是可扩大寻址范围(有效地址EA的位数大于形式地址A的位数),便于编制程序(用间接寻址可方便地完成子程序返回);
缺点是指令在执行阶段要多次访存(一次间接寻址需2次访存)。
由于执行速度较慢,一般为了扩大寻址范围时,通常采用寄存器间接寻址。
5.寄存器寻址
与直接寻址的原理一样,只是把访问主存改为访问寄存器,指令的地址字段给出的是操作数所在寄存器的编号,即,其操作数在由所指的寄存器内,如图4.6所示。
命题追踪 寄存器编号位数与寄存器数量的关系(2022、2024)
优点是指令在执行阶段不用访存,只访问寄存器,执行速度快;
寄存器数量远小于内存单元数,所以地址码位数较少,指令字长较短;
缺点是寄存器价格昂贵,CPU的寄存器数量有限。
6.寄存器间接寻址
命题追踪 寄存器间接寻址的取数操作(2010) 这种方式综合了间接寻址和寄存器寻址各自的特点,指令字中的R所指寄存器给出的不是一个操作数,而是操作数所在主存单元的地址,即,如图4.7所示。
相比间接寻址,这种方式既扩大了寻址范围,又减少了访存次数,在执行阶段仅需访存1次。
相比寄存器寻址,这种方式在执行阶段需要访存(因操作数在主存中)获得操作数。
7.相对寻址
命题追踪 相对寻址的偏移量或目标地址的计算(2009、2013、2014、2019、2023) 相对寻址是把PC的内容加上指令格式中的形式地址A而形成操作数的有效地址,即EA=(PC)+A,其中A是相对于当前PC值的偏移量,可正可负,补码表示,如图4.8所示。
在图4.8中,A的位数决定操作数的寻址范围。
优点是操作数的地址不是固定的,它随PC值的变化而变化,且与指令地址之间总是相差一个固定的偏移量,因此便于程序浮动。
相对寻址广泛应用于转移指令。
命题追踪 相对寻址转跳范围的计算(2010、2013、2014) 注意 对于转移指令JMP A,若指令的地址为X,且占2B,则在取出该指令后,PC的值会增2,即(PC)=X+2,这样在执行完该指令后,会自动跳转到X+2+A的地址继续执行。
8.基址寻址
命题追踪 基址寻址的EA的计算(2019)
基址寻址是指将基址寄存器(BR)的内容加上指令字中的形式地址A而形成操作数的有效地址,即EA=(BR)+A。
其中基址寄存器既可采用专用寄存器,又可指定某个通用寄存器作为基址寄存器,如图4.9所示。
基址寄存器是面向操作系统的,其内容由操作系统或管理程序确定,主要用于解决程序逻辑空间与存储器物理空间的无关性。
在程序执行过程中,基址寄存器的内容不变(作为基地址),形式地址可变(作为偏移量)。
采用通用寄存器作为基址寄存器时,可由用户决定哪个寄存器作为基址寄存器,但其内容仍由操作系统确定。
基址寻址的优点是可以扩大寻址范围(基址寄存器的位数大于形式地址A的位数);
用户不必考虑自己的程序存于主存的具体位置,因此有利于多道程序设计,并可用于编制浮动程序,
但偏移量(形式地址A)的位数较短。
9.变址寻址
命题追踪 变址寻址的EA的计算(2013、2024),先变址后间址方式的EA的计算(2016) 变址寻址是指将变址寄存器(IX)的内容加上指令字中的形式地址A而形成操作数的有效地址,即EA=(IX)+A,其中IX为变址寄存器(专用),也可用通用寄存器作为变址寄存器。
图4.10所示为采用专用寄存器IX的变址寻址示意图。
变址寄存器是面向用户的,在程序执行过程中,变址寄存器的内容可由用户改变(作为偏移量),形式地址A不变(作为基地址)。
命题追踪 变址寻址的适用场景(2017)
变址寻址的优点是可扩大寻址范围(变址寄存器的位数大于形式地址A的位数);
在数组处理过程中,可设定A为数组的首地址,不断改变变址寄存器IX的内容,便可很容易形成数组中任意一个数据的地址,特别适合编制循环程序。
偏移量(变址寄存器IX)的位数足以表示整个存储空间。
命题追踪 变址寻址访问数组的分析(2018、2024)
显然,变址寻址与基址寻址的有效地址形成过程极为相似。
但从本质上讲,两者有较大区别。
基址寻址面向系统,主要用于为多道程序或数据分配存储空间,因此基址寄存器的内容通常由操作系统或管理程序确定,在程序的执行过程中其值不可变,而指令字中的A是可变的。
变址寻址立足于用户,主要用于处理数组问题,在变址寻址中,变址寄存器的内容由用户设定,在程序执行过程中其值可变,而指令字中的A是不可变的。
命题追踪 偏移寻址的范畴(2011)
相对寻址、基址寻址和变址寻址三种寻址方式非常类似,都将某个寄存器的内容与一个形式地址相加而生成操作数的有效地址,通常把这三种寻址方式称为偏移寻址。
10.堆栈寻址
堆栈是存储器(或寄存器组)中一块特定的、按后进先出(LIFO)原则管理的存储区,该存储区中读/写单元的地址是用一个特定寄存器给出的,该寄存器称为堆栈指针(SP)。
堆栈可分为硬堆栈和软堆栈两种。
寄存器堆栈也称硬堆栈,硬堆栈的成本较高,不适合做大容量的堆栈。
而从主存中划出段区域来做堆栈是最合算且最常用的方法这种堆栈称为软堆栈。
在采用堆栈结构的计算机中,大部分指令表面上都表现为无操作数指令的形式,因为操作数地址都隐含使用了SP。
因此在读/写堆栈的前后都伴有自动完成对SP的加减操作。
下面简单总结寻址方式、有效地址及访存次数(不含取本条指令的访存),见表4.1。
4.3 程序的机器级代码表示
命题追踪 涉及汇编代码真题的年份(2012、2014、2015、2017、2019、2023、2024) 本节是2022年才新增的考点,但历年统考真题曾多次以综合题的形式考查过,难度较大,不少跨考生对此无从下手,相信通过本节的学习后,应能从容应对。
统考大纲没有指定具体指令集,但历年统考真题主要考查的是x86汇编指令,因此本节主要介绍x86汇编指令。
4.3.1 常用汇编指令介绍
1.相关寄存器
x86处理器中有8个32位的通用寄存器,主要寄存器及说明如图4.11所示。
为了向后兼容,EAX、EBX、ECX和EDX的高两位字节和低两位字节可以独立使用,E表示Extended,表示32位的寄存器。
例如,EAX的低两位字节称为AX,而AX的高低字节又可分别作为两个8位寄存器,分别称为AH和AL。
其中,X表示未知 I=Index S=Source D=Destination
除EBP和ESP外,其他几个寄存器的用法是比较灵活的。
2.汇编指令格式
使用不同的编程工具开发程序时,用到的汇编程序也不同,一般有两种不同的汇编格式:
AT&T格式和Intel格式(统考要求掌握的是Intel格式)。
它们的区别主要体现如下:
①AT&T格式的指令只能用小写字母,而Intel格式的指令对大小写不敏感。
②在AT&T格式中,第一个为源操作数,第二个为目的操作数,方向从左到右,合乎自然;
在Intel格式中,第一个为目的操作数,第二个为源操作数,方向从右向左。
③在AT&T格式中,寄存器需要加前缀“%”,立即数需要加前缀“$”;
在Intel格式中,寄存器和立即数都不需要加前缀。
④在内存寻址方面,AT&T格式使用“(”和“)”,而Intel格式使用“[”和“]”。
⑤在处理复杂寻址方式时,
例如AT&T格式的内存操作数“disp(base,index,scale)”分别表示偏移量、基址寄存器、变址寄存器和比例因子,
如“8(%edx,%eax,2)”表示操作数为M[R[edx]+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位。
表4.2所示为AT&T格式指令和Intel格式指令的对比。
其中,mov指令用于在内存和寄存器之间或者寄存器之间移动数据;
lea指令用于将一个内存地址(而不是其所指的内容)加载到目的寄存器。
两种汇编格式的相互转换并不复杂,但历年统考真题采用的均是Intel格式。
3.常用指令
汇编指令通常可分为数据传送指令、算术和逻辑运算指令和控制流指令,下面以Intel格式为例,介绍一些常用的指令。
以下用于操作数的标记分别表示寄存器、内存和常数。 ●
●
●
命题追踪 分析汇编指令对应的二进制代码(2010)
x86中的指令机器码长度为1字节,对同一指令的不同用途有多种编码方式,比如mov指令就有28种机内编码,用于不同操作数类型或用于特定寄存器,例如,
mov ax,
mov al,
mov
mov
mov
命题追踪 模仿写出简单语句的机器级指令(2012)
(1)数据传送指令
1)mov指令
将第二个操作数(寄存器的内容、内存中的内容或常数值)复制到第一个操作数寄存器或内存。 其语法如下:
mov
mov
mov
mov
mov
举例:
mov eax, ebx #将ebx值复制到eax
mov byte ptr [var],5 #将5保存到var值指示的内存地址的一字节中
双操作数指令的两个操作数不能都是内存,即mov指令不能用于直接从内存复制到内存,若需在内存之间复制,可先从内存复制到一个寄存器,再从这个寄存器复制到内存。
2)push指令
将操作数压入内存的栈,常用于函数调用。
ESP是栈顶,入栈前先将ESP值减4(栈增长方向与内存地址增长方向 相反),然后将操作数压入ESP指示的地址。 其语法如下:
push
push
push
举例(注意,栈中元素固定为 32位):
push eax #将 eax 值入栈
push [var] #将 var 值指示的内存地址的4字节值入栈
3)pop指令
与push指令相反,pop指令执行的是出栈工作,出栈前先将ESP指示的地址中的内容出栈,然后将ESP值加4。 其语法如下:
pop eax #弹出栈顶元素送到eax
pop [ebx] #弹出栈顶元素送到ebx值指示的内存地址的4字节中
(2)算术和逻辑运算指令
1)add/sub指令
add指令将两个操作数相加,相加的结果保存到第一个操作数中。
sub指令用于两个操作数相减,相减的结果保存到第一个操作数中。 它们的语法如下:
add
add
add
add
add
举例:
sub eax,10 #eax<-eax-10
add byte ptr [var], 10 #10与var值指示的内存地址的一字节值相加,并将结果保存在var值指示的内存地址的字节中
2)inc/dec指令
inc、dec指令分别表示将操作数自加1、自减1。 它们的语法如下:
inc
inc
举例:
dec eax #eax值自减1
inc dword ptr [var] #var值指示的内存地址的4字节值自加1
3)imul指令
有符号整数乘法指令,有两种格式:
①两个操作数,将两个操作数相乘,将结果保存在第一个操作数中,第一个操作数必须为寄存器;
②三个操作数,将第二个和第三个操作数相乘,将结果保存在第一个操作数中,第一个操作数必须为寄存器。 其语法如下:
imul
imul
imul
imul
举例:
imul eax,[var] #eax< eax * [var]
imul esi, edi,25 #esi< edi *25
乘法操作结果可能溢出,则编译器置溢出标志OF=1,以使CPU调出溢出异常处理程序。
4)idiv指令
有符号整数除法指令,它只有一个操作数,即除数,而被除数则为edx:eax中的内容(共64位),操作结果有两部分:商和余数,商送到eax,余数则送到edx。 其语法如下:
idiv
idiv
举例:
idiv ebx
idiv dword ptr [var]
5)and/or/xor指令
and、or、xor指令分别是逻辑与、逻辑或、逻辑异或操作指令,用于操作数的位操作,操作结果放在第一个操作数中。 它们的语法如下:
and
and
and
and
and
举例:
and eax, 0fH #将eax中的前28位全部置为0,最后4位保持不变
xor edx, edx #置 edx中的内容为 0
6)not指令
位翻转指令,将操作数中的每一位翻转,即0→1、1→0。 其语法如下:
not
not
举例:
not byte ptr [var] #将var值指示的内存地址的一字节的所有位翻转
7)neg指令
取负指令 其语法如下:
neg
neg
举例:
neg eax
#eax-eax
8)sh/shr指令
逻辑移位指令,shl为逻辑左移,shr为逻辑右移,第一个操作数表示被操作数,第二个操作数指示移位的位数。 它们的语法如下:
shl
shl
shl
shl
举例:
shl eax,1 #将eax值左移一位
shr ebx,c1 #将ebx值右移n位(n为cI中的值)
(3)控制流指令
x86处理器维持着一个指示当前执行指令的 指令指针(IP),当一条指令执行后,此指针自动指向下一条指令。
IP寄存器不能直接操作,但可以用控制流指令更新。
通常用标签(label)指示程序中的指令地址,在x86汇编代码中,可在任何指令前加入标签。
例如,
begin:
mov esi, [ebp+8]
xor ecx, ecx
mov eax, [esi]
这样就用begin指示了第二条指令,控制流指令通过标签就可以实现程序指令的跳转。
命题追踪 无条件转移指令的指令格式(2021)
1)jmp指令
jmp指令控制IP转移到label所指示的地址(从label中取出指令执行)。 其语法如下:
jmp
举例:
jmp begin #转跳到 begin标记的指令执行
命题追踪 条件转移指令与标志位的结合(2013)
2)jcondition指令
条件转移指令,依据CPU状态字中的一系列 条件状态 转移。
CPU状态字中包括指示最后一个算术运算结果是否为0,运算结果是否为负数等。
其语法如下:
je
jz
jne
jg
jge
ji
jle
举例:
cmp eax, ebx
jle done #若eax值<=ebx值,则跳转到done执行;否则执行下一条指令
3)cmp/test指令
cmp指令的功能相当于sub指令,用于比较两个操作数的值。
test指令的功能相当于and指令,对两个操作数进行逐位与运算。
与sub和and指令不同的是,这两类指令都不保存操作结果,仅根据运算结果设置CPU状态字中的条件码。 其语法如下:
cmp
cmp
cmp
cmp
cmp和test指令通常和 jcondition指令搭配使用,举例:
cmp dword ptr [var],10 #将var指示的主存地址的4字节内容,与10比较
jne loop #若相等则继续顺序执行;否则跳转到1oop处执行
test eax, eax #测试 eax是否为零
jz XXXX #为零则置标志ZF为1,转跳到XXXX处执行
命题追踪 call指令的功能(2019)
4)call/ret指令
分别用于实现子程序(过程、函数等)的调用及返回。 其语法如下:
call
ret
call指令将下一条指令的地址(返回地址)入栈,然后无条件转移到由标签指示的指令。
与其他简单的跳转指令不同,
calI指令保存该指令的下一条指令的地址(当call指令结束后,返回保存的地址)。
ret指令实现子程序的返回机制,ret指令弹出栈中保存的指令地址,然后无条件转移到保存的指令地址执行。
call和ret是程序(函数)调用中最关键的两条指令。
理解上述指令的语法和用途,可以更好地帮助读者解答相关题型。读者在上机调试C程序代码时,也可以尝试用编译器调试,以便更好地帮助理解机器指令的执行。
4.3.2 选择语句的机器级表示
常见的选择结构语句有if-then、if-then-else等。
编译器通过条件码(标志位)设置指令和各类转移指令来实现程序中的选择结构语句。
条件码描述了最近的算术或逻辑运算操作的属性,可以检测这些寄存器来执行条件分支指令,最常用的条件码有CF、ZF、SF和OF。
常见的算术逻辑运算指令(add,sub,imul,or,and,shl, inc,dec,not,sal等)会设置条件码,还有cmp和test指令只设置条件码而不改变任何其他寄存器。
之前介绍的jcondition条件转跳指令,就是根据条件码ZF和SF来实现转跳的。 if-else语句的通用形式如下:
if (test_expr)
then_statement
else
else_statement
这里的test_expr是一个整数表达式,它的取值为0(假),或为非0(真)。
两个分支语句(then_statement或else_statement)中只会执行一个。
这种通用形式可以被翻译成如下所示的goto语句形式:
t=test_expr;
if(!t)
goto false;
then_statement
goto done;
false:
else_statement
done:
对于下面的C语言函数:
int get_cont(int *pl,int *p2) {
if (p1>p2)
return *p2;
else
return *p1;
}
已知p1和p2对应的实参已被压入调用函数的栈帧,它们对应的存储地址分别为R[ebp]+8、R[ebp]+12(EBP指向当前栈帧底部),返回结果存放在EAX中。对应的汇编代码为
mov eax,dword ptr [ebp+8] #R[eax]<--M[R[ebp]+8], 即R[eax]=p1
mov edx,dword ptr [ebp+12] #R[edx]<--M[R[ebp]+12],即R[edx]=p2
cmp eax,edx #比较p1和p2,即根据p1-p2的结果置标志
jbe .L1 #若p1<=p2,则转标记L1处执行
mov eax,dword ptr [edx] #R[eax]<--M[R[edx]],即R[eax]=M[p2]
jmp .L2 #无条件跳转到标记L2执行
.Ll:
mov eax,dword ptr [eax] #R[eax]M[R[eax]],即R[eax]=M[pl]
.L2:
p1和p2是指针型参数,所以在32位机中的长度是dword ptr,比较指令cmp的两个操作数都应来自寄存器,因此应先将p1和p2对应的实参从栈中取到通用寄存器,比较指令执行后得到各个条件码,然后根据各条件码值的组合选择执行不同的指令,因此需要用到条件转移指令。
4.3.3 循环语句的机器级表示
命题追踪 循环语句的机器级代码分析(2014、2017、2019、2023)
常见的循环结构语句有while、for和do-while。
汇编中没有相应的指令存在,可以用条件测试和转跳组合起来实现循环的效果,大多数编译器将这三种循环结构都转换为do-while形式来产生机器代码。
在循环结构中,通常使用条件转移指令来判断循环条件的结束。
(1)do-while循环
do-while语句的通用形式如下:
do
body_statement
while(test_expr);
这种通用形式可以被翻译成如下所示的条件和goto语句:
loop:
body_statement
t=test_expr;
if(t)
goto loop;
也就是说,每次循环,程序会执行循环体内的语句,body_statement至少会执行一次,然后执行测试表达式。
若测试为真,则继续执行循环。
(2)while循环
while语句的通用形式如下:
while(test_expr)
body_statement
与do-while的不同之处在于,第一次执行body_statement之前,就会测试test_expr的值,循环有可能中止。
GCC通常会将其翻译成条件分支加do-while循环的方式。
用如下模板来表达这种方法,将通用的while循环格式翻译成do-while循环:
t=test_expr;
if(!t)
goto done;
do
body_statement
while(test_expr);
done:
相应地,进一步将它翻译成goto语句:
t=test_expr;
if(!t)
goto done;
loop:
body_statement
t=test_expr;
if(t)
goto loop;
done:
(3) for 循环
for循环的通用形式如下:
for(init_expr; test_expr; update_expr)
body_statement
这个for循环的行为与下面这段while循环代码的行为一样:
init expr;
while(test_expr) {
body_statement
update_expr;
}
进一步把它翻译成goto语句:
init_expr;
t=test_expr;
if(!t)
goto done;
loop:
body_statement
update_expr;
t=testXexpr;
if(t)
goto loop;
done:
下面是一个用for循环写的自然数求和的函数:
int nsum_for (int n) {
int i;
int result = 0;
for(i=1;i<=n;i++)
result +=i;
return result;
}
这段代码中的for循环的不同组成部分如下:
init_expr i=1
test_expr i<=n
update_expr i++
body_statement result +=i
通过替换前面给出的模板中的相应位置,很容易将for循环转换为while或do-while循环。
将这个函数翻译为goto语句代码后,不难得出其过程体的汇编代码:
mov ecx,dword ptr [ebp+8] #R[ecx]<--M[R[ebp]+8], 即R[ecx]=n
mov eax,0 #R[eax]<--0, 即 result=0
mov edx,1 #R[edx]<--1,即i=1
cmp edx,ecx #Compare R[edx]:R[ecx],即比较i:n
jg .L2 #Ifgreater,转跳到 L2 执行
.L1: #loop:
add eax,edx #R[eax]<--R[eax]+R[edx],即result +=i
add edx,1 #R[edx]<--R[edx]+1,即i++
cmp edx,ecx #比较R[edx]和R[ecx],即比较i:n
jle .L1 #If less or equal,转跳到Ll执行
.L2:
已知n对应的实参已被压入调用函数的栈帧,其对应的存储地址为R[ebp]+8,过程nsum_for中的局部变量i和result被分别分配到寄存器EDX和EAX中,返回参数在EAX中。
4.3.4 过程调用的机器级表示
前面提到的call/ret指令主要用于过程调用,它们都属于一种无条件转移指令。
假定过程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。
在上述步骤中,需要为入口参数、返回地址、过程P的现场、过程Q的局部变量、返回结果找到存放空间。
用户可见寄存器数量有限,调用者和被调用者需共享寄存器,若直接覆盖对方的寄存器,则会导致程序出错。
因此有如下规范:寄存器EAX、ECX和EDX是调用者保存寄存器,当P调用Q时,若Q需用到这些寄存器,则由P将这些寄存器的内容保存到栈中,并在返回后由P恢复它们的值。
寄存器EBX、ESI、EDI是被调用者保存寄存器,当P调用Q时,Q必须先将这些寄存器的内容保存在栈中才能使用它们,并在返回P之前先恢复它们的值。
每个过程都有自己的栈区,称为栈帧,因此,一个栈由若干栈帧组成,寄存器EBP指示栈帧的起始位置,寄存器ESP指示栈顶,栈从高地址向低地址增长。
过程执行时,ESP会随着数据的入栈而动态变化,而EBP固定不变。
当前栈帧的范围在EBP和ESP指向的区域之间。
下面用一个简单的C语言程序来说明过程调用的机器级实现。
int add(int x, int y) {
return x+y;
}
int caller() {
int templ=125;
int temp2=80;
int sum=add(templ,temp2);
return sum;
}
经GCC编译后,caller过程对应的代码如下:
caller:
push ebp
mov ebp,esp
sub esp,24
mov [ebp-12],125 #M[R[ebp]-12]<--125, 即templ=125
mov [ebp-8],80 #M[R[ebp]-8]<--80,即temp2=80
mov eax,dword ptr [ebp-8] #R[eax]<--M[R[ebp]-8],即R[eax]=temp2
mov [esp+4],eax #M[R[esp]+4]<--R[eax],即temp2入栈
mov eax,dword ptr [ebp-12] #R[eax]<--M[R[ebp]-12], 即R[eax]=temp1
mov [esp],eax #M[R[esp]]<--R[eax],即temp1入栈
call add #调用add,将返回值保存在eax中
mov [ebp-4],eax #M[R[ebp]-4]<--R[eax],即add返回值送sum
mov eax,dword ptr [ebp-4] #R[eax]-M[R[ebp]-4],即sum作为返回值
leave
ret
图 4.12 给出了 caller 和 add 的栈帧,假定 caller 被过程P调用。
执行第4行的指令后,ESF所指的位置如图中所示,可以看出 GCC为 caller 的参数分配了 24 字节的空间。
从汇编代码中可以看出,caller 中只使用了调用者保存寄存器EAX,没有使用任何被调用者保存寄存器,因此在caller 栈帧中无须保存除 EBP 外的任何寄存器的值;
caller 有三个局部变量 temp1、temp2 和 sum皆被分配在栈帧中;
在用 cal 指令调用 add 函数之前,caller 先将入口参数从右向左依次将 temp2和 temp1 的值(即 80 和 125)保存到栈中。
在执行 cal 指令时再把返回地址压入栈中。
此外,在最初进入 caller 时,还将 EBP 的值压入了栈,因此 caller 的栈帧中用到的空间占4+12+8+4=28字节。
但是,caller 的栈帧共有4+24+4=32 字节,其中浪费了4字节的空间(未使用)。
这是因为 GCC 为保证数据的严格对齐而规定每个函数的栈帧大小必须是 16 字节的倍数。
call指令执行后,add函数的返回参数存放在EAX中,因此call指令后面的两条指令中,指令“mov[ebp-4],eax”将add的结果存入sum变量的存储空间,该变量的地址为R[ebp]-4;
指令“moveax,dword ptr[ebp-4]”将sum变量的值作为返回值送到寄存器EAX中。
在执行ret指令之前,应将当前栈帧释放,并恢复旧EBP的值,上述第14行leave指令实现了这个功能,leave指令功能相当于以下两条指令的功能:
mov esp, ebp
pop ebp
其中,第一条指令使ESP指向当前EBP的位置,第二条指令执行后,EBP恢复为P中的旧值,并使ESP指向返回地址。
执行完leave指令后,ret指令就可从ESP所指处取返回地址,以返回P执行。
当然,编译器也可通过pop指令和对ESP的内容做加法来进行退栈操作,而不一定要使用leave指令。
add过程经GCC编译并进行链接后,对应的代码如下所示:
8048469:55 push ebp
804846a:89 e5 mov ebp,esp
804846c:8b 45 0c mov eax,dword ptr [ebp+l2]
804846f:8b 55 08 mov edx,dword ptr [ebp+8]
8048472:8d 04 02 lea eax,[edx+eax]
8048475:5d pop ebp
8048476:c3 ret
通常,一个过程对应的机器级代码都有三个部分:准备阶段、过程体和结束阶段。
上述第1、2行的指令构成准备阶段的代码段,这是最简单的准备阶段代码段,它通过将当前栈指针ESP传送到EBP来完成将EBP指向当前栈帧底部的任务,如图4.12所示,EBP指向add栈帧底部,从而可以方便地通过EBP获取入口参数。
这里add的入口参数x和y对应的值125和80)分别在地址为R[ebp]+8、R[ebp]+12的存储单元中。
上述第3、4、5行的指令序列是过程体的代码段,过程体结束时将返回值放在EAX中。
这里好像没有加法指令,实际上第5行lea指令执行的是加法运算R[edx]+R[eax]x+y。
上述第6、7行的指令序列是结束阶段的代码段,通过将EBP弹出栈帧来恢复EBP在caller过程中的值,并在栈中退出add过程的栈帧,使得执行到ret指令时栈顶中已经是返回地址。
这里的返回地址应该是caller代码中第 12行的指令“mov[ebp-4],eax”的地址。
add过程中没有用到任何被调用者保存寄存器,没有局部变量,此外,add是一个被调用过程,并且不再调用其他过程,因此也没有入口参数和返回地址要保存,因此,在add的栈帧中除了需要保存EBP,无须保留其他任何信息。
4.4 CISC和RISC的基本概念
指令系统朝两个截然不同的方向的发展:
一是增强原有指令的功能,设置更为复杂的新指令实现软件功能的硬化,这类机器称为复杂指令系统计算机(CISC),典型的有采用x86架构的计算机;
二是减少指令种类和简化指令功能,提高指令的执行速度,这类机器称为精简指令系统计算机(RISC),典型的有ARM、MIPS架构的计算机。
4.4.1 复杂指令系统计算机(CISC)
随着集成电路技术的发展,软件成本不断上升,促使人们在指令系统中增加更多、更复杂的指令,以适应不同的应用领域,这样就构成了CISC。
命题追踪 CISC 的特点(2017)
CISC的主要特点如下: 1)指令系统复杂庞大,指令数目一般为200条以上。 2)指令的长度不固定,指令格式多,寻址方式多。 3)可以访存的指令不受限制。 4)各种指令使用频度相差很大。 5)各种指令执行时间相差很大,大多数指令需多个时钟周期才能完成。 6)控制器大多数采用微程序控制。有些指令非常复杂,以至于无法采用 硬连线控制。 7)难以用优化编译生成高效的目标代码程序。
如此庞大的指令系统,对指令的设计提出了极高的要求,研制周期变得很长。
后来人们发现,一味地追求指令系统的复杂和完备程度不是提高性能的唯一途径。
对传统CISC指令系统的测试表明,各种指令的使用频率相差悬殊,大概只有20%的简单指令被反复使用,约占整个程序的80%;
而80%左右的复杂指令则很少使用,约占整个程序的20%。
从这一事实出发,人们开始用最常用的20%的简单指令,重组实现 不常用的80%的指令功能,RISC随之诞生。
4.4.2 精简指令系统计算机(RISC)
RISC的中心思想是要求指令系统简化,尽量使用寄存器-寄存器操作指令,指令格式力求一致。RISC的主要特点如下: 1)选取使用频率最高的一些简单指令,复杂指令的功能由简单指令的组合来实现。 2)指令长度固定,指令格式种类少,寻址方式种类少。 3)只有LOAD/STORE(取数/存数)指令访存,其余指令的操作都在寄存器之间进行。 4)CPU中通用寄存器的数量相当多。 5)一定采用指令流水线技术,大部分指令在一个时钟周期内完成。 6)以硬布线控制为主,不用或少用微程序控制。 7)特别重视编译优化工作,以减少程序执行时间。
值得注意的是,从指令系统兼容性看,CISC大多能实现软件兼容,即高档机包含了低档机的全部指令,并可加以扩充。
但RISC简化了指令系统,指令条数少,格式也不同于老机器,因此大多数RISC机不能与老机器兼容。
RISC具有更强的实用性,因此应该是未来处理器的发展方向。
但事实上,当今时代Intel几乎一统江湖,且早期很多软件都是根据CISC设计的,单纯的RISC将无法兼容。
此外,现代CISC结构的CPU已经融合了很多RISC的成分,其性能差距已经越来越小。
CISC可以提供更多的功能,这是程序设计所需要的。
4.4.3 CISC和RISC的比较
和CISC相比,RISC的优点主要体现在以下几点:
1)RISC更能充分利用VLSI(超大规模集成电路)芯片的面积。
CISC采用微程序控制,其控制存储器占CPU芯片面积的50%以上,而RISC采用组合逻辑控制,其硬布线逻辑只占CPU芯片面积的10%左右。
2)RISC更能提高运算速度。
RISC的指令数、寻址方式和指令格式 种类少,又设有多个通用寄存器,采用流水线技术,所以运算速度更快,大多数指令在一个时钟周期内完成。
3)RISC便于设计,可降低成本,提高可靠性。
RISC指令系统简单,因此机器设计周期短;
其逻辑简单,出错概率低,有错也易发现,因此可靠性高。
4)RISC有利于编译程序代码优化。
RISC指令类型少,寻址方式少,使编译程序容易选择更有效的指令和寻址方式,并适当地调整指令顺序,使得代码执行更高效化。 CISC和RISC的对比见表4.3。