第四章 指令系统
第一节 指令系统
「指令」概念
- 指令(机器指令)是指示计算机执行某种操作的命令。
- 指令集:一台计算机的所有指令的集合构成该机的指令系统。
- 指令系统:指令集体系结构(ISA)中最核心的部分。
- ISA 完整定义了软件和硬件之间的接口,是机器语言或汇编语言程序员所应熟悉的。
- ISA 规定的内容主要包括:
- 指令格式
- 数据类型及格式
- 操作数的存放方式
- 程序可访问的寄存器个数、位数和编号,
- 存储空间的大小和编址方式
- 寻址方式
- 指令执行过程的控制方式等。
「指令」基本格式
一条指令就是机器语言的一个语句,它是一组有意义的二进制代码。
一条指令通常包括两部分:
- 操作码字段
- 地址码字段
操作码
- 指出是算术加运算还是算术减运算,是程序转移还是返回操作。
地址码
- 给出被操作的信息(指令或数据)的地址,包括参加运算的一个或多个操作数所在的地址、运算结果的保存地址、程序的转移地址、被调用的子程序的入口地址等。
指令的长度:一条指令中所包含的二进制代码的位数。
指令字长:取决于操作码的长度、操作数地址码的长度和操作数地址的个数。
指令长度与机器字长没有固定的关系,它可以等于机器字长,也可以大于或小于机器字长。
- 单字长指令:指令长度等于机器字长的指令
- 半字长指令:指令长度等于半个机器字长的指令
- 双字长指令:指令长度等于两个机器字长的指令
定长指令字结构:在一个指令系统中,若所有指令的长度都是相等的
- 优点
- 执行速度快,控制简单。
- 优点
变长指令字结构:各种指令的长度随指令功能而异
指令字长多为字节的整数倍,因主存一般是按字节编址的
根据指令中操作数地址码的数目的不同,可将指令分成以下几种格式
「指令」零地址指令
- 只给出操作码 OP,没有显式地址。
- 两种可能:
- 不需要操作数的指令,如空操作指令、停机指令、关中断指令等。
- 零地址的运算类指令仅用在堆栈计算机中。
- 参与运算的两个操作数隐含地从栈顶和次栈顶弹出
- 送到运算器进行运算,运算结果再隐含地压入堆栈。
「指令」一地址指令
- 两种常见的形态,根据操作码的含义判定
- 只有目的操作数的单操作数指令,
- 按
地址读取操作数,进行 OP 操作后,结果存回原地址。 - 指令含义:
- 如操作码含义是加1、减1、求反、求补等。
- 按
- 隐含约定目的地址的双操作数指令
- 按指令地址
可读取源操作数,指令可隐含约定另一个操作数由ACC(累加器)提供,运算结果也将存放在 ACC中。 - 指令含义:
- 若指令字长为32位,操作码占8位,1个地址码字段占 24位,则指令操作数的直接寻址范围为
。
- 按指令地址
- 只有目的操作数的单操作数指令,
「指令」二地址指令
- 指令含义:
- 对于常用的算术和逻辑运算指令,往往要求使用两个操作数,需分别给出目的操作数
和源操作数 的地址 - 其中目的操作数地址还用于保存本次的运算结果。
- 若指令字长为32位,操作码占8位,两个地址码字段各占12位,则指令操作数的直接寻址范围为
「指令」三地址指令
- 指令含义:
- 若指令字长为32位,操作码占8位,3个地址码字段各占8位,则指令操作数的直接寻址范围为
。 - 若地址字段均为主存地址,则完成一条三地址需要4次访问存储器(取指令1次,取两个操作数2次,存放结果1次)。
「指令」四地址指令
- 指令含义:
, 下一条将要执行指令的地址。 - 若指令字长为32位,操作码占8位,4个地址码字段各占6位,则指令操作数的直接寻址范围为
。
「指令」定长操作码指令格式
- 定长操作码指令:在指令字的最高位部分分配固定的若干位(定长)表示操作码。
- 一般n位操作码字段的指令系统最大能够表示2”条指令。
- 优点
- 对于简化计算机硬件设计,提高指令译码和识别速度很有利
- 当计算机字长为32位或更长时,这是常规用法。
「指令」扩展操作码指令格式
可变长度操作码
- 全部指令的操作码字段的位数不固定,且分散地放在指令字的不同位置上。
- 优点
- 在指令字长有限的前提下仍保持比较丰富的指令种类
- 缺点
- 增加指令译码和分析的难度,使控制器的设计复杂化。
扩展操作码(最常见的变长操作码方法)
- 操作码的长度随地址码的减少而增加
- 不同地址数的指令可具有不同长度的操作码
- 从而在满足需要的前提下,有效地缩短指令字长。
设计扩展操作码指令格式,注意两点:
- 不允许短码是长码的前缀,即短操作码不能与长操作码的前面部分的代码相同。
- 各指令的操作码一定不能重复。
通常情况下
- 对使用频率较高的指令分配较短的操作码
- 对使用频率较低的指令分配较长的操作码
从而尽可能减少指令译码和分析的时间。
「指令」操作类型
- 数据传送
- 传送指令通常有寄存器之间的传送(MOV)、从内存单元读取数据到 CPU 寄存器(LOAD)、从 CPU 寄存器写数据到内存单元(STORE)等。
- 算术和逻辑运算
- 这类指令主要有加(ADD)、减(SUB)、比较(CMP)、乘(MUL)、除(DIV)、加1 (INC)、减1(DEC)、与(AND)、或(OR)、取反(NOT)、异或(XOR)等。
- 移位操作
- 移位指令主要有算术移位、逻辑移位、循环移位等。
- 转移操作
- 转移指令主要有无条件转移(JMP)、条件转移(BRANCH)、调用(CALL)、返回(RET)、陷阱(TRAP)等。
- 无条件转移指令在任何情况下都执行转移操作
- 条件转移指令仅在特定条件满足时才执行转移操作,转移条件一般是某个标志位的值,或几个标志位的组合。
- 调用指令和转移指令的区别
- 执行调用指令时必须保存下一条指令的地址(返回地址)
- 子程序执行结束时,根据返回地址返回到主程序继续执行
- 转移指令则不返回执行
- 输入输出操作
- 这类指令用于完成CPU与外部设备交换数据或传送控制命令及状态信息。
第二节 指令的寻址方式
- 寻址方式
- 是指寻找指令或操作数有效地址的方式,即确定本条指令的数据地址及下一条待执行指令的地址的方法。
- 寻址方式分为指令寻址和数据寻址两大类。
- 形式地址(A):指令中的地址码字段并不代表操作数的真实地址,这种地址称为形式地址(A)。
- 有效地址(EA):形式地址结合寻址方式,可以计算出操作数在存储器中的真实地址,这种地址称为有效地址(EA)。
- 注意
- (A)表示地址为A 的数值,A既可以是寄存器编号,也可以是内存地址。
- 对应的(A)就是寄存器中的数值,或相应内存单元的数值。
- 例如,EA=(A)意思是有效地址是地址 A 中的数值。
「寻址」指令寻址
- 指令寻址方式
- 顺序寻址
- 通过程序计数器PC加1(1个指令字长),自动形成下一条指令的地址。
- 跳跃寻址
- 通过转移类指令实现。
- 跳跃,指由本条指令给出下条指令地址的计算方式。
- 是否跳跃
- 可能受到状态寄存器和操作数的控制
- 跳跃的地址 分为
- 绝对地址(由标记符直接得到)
- 相对地址(相对于当前指令地址的偏移量)
- 跳跃的结果
- 是当前指令修改PC值,所以下一条指令仍然通过PC给出。
- 顺序寻址
「寻址」数据寻址
数据寻址方式
- 如何在指令中表示一个操作数的地址
- 如何用这种表示得到操作数或怎样计算出操作数的地址
- 数据寻址的方式较多,为区别各种方式,通常在指令字中设一个字段,用来指明属于哪种寻址方式
指令的格式如下所示
「常见寻址」隐含寻址
- 这种类型的指令不明显地给出操作数的地址,而在指令中隐含操作数的地址。
- 单地址的指令格式就不明显地在地址字段中指出第二操作数的地址,而规定累加器(ACC)作为第二操作数地址,指令格式明显指出的仅是第一操作数的地址。
- 因此,累加器(ACC) 对单地址指令格式来说是隐含寻址,如图4.2所示。
- 优点
- 有利于缩短指令字长
- 缺点
- 需增加存储操作数或隐含地址的硬件
「常见寻址」立即(数)寻址
- 这种类型的指令的地址字段指出的是操作数本身,又称立即数,采用补码表示。
- 图4.3所示为立即寻址示意图,图中#表示立即寻址特征,A 就是操作数。
- 优点
- 指令在执行阶段不访问主存,指令执行时间最短
- 缺点
- 是 A 的位数限制了立即数的范围
「常见寻址」直接寻址
- 指令字中的形式地址 A 是操作数的真实地址 EA,即EA=A,如图4.4所示。
- 优点
- 简单,指令在执行阶段仅访问一次主存,不需要专门计算操作数的地址
- 缺点
- A 的位数决定了该指令操作数的寻址范围,操作数的地址不易修改。
「常见寻址」间接寻址
间接寻址是相对于直接寻址而言的,
指令的地址字段给出的形式地址不是操作数的真正地址,而是操作数有效地址所在的存储单元的地址,也就是操作数地址的地址,即 EA=(A),如图4.5 所示。
间接寻址可以是一次间接寻址,还可以是多次间接寻址。
优点
- 是可扩大寻址范围(有效地址 EA 的位数大于形式地址 A 的位数)
- 便于编制程序(用间接寻址可方便地完成子程序返回)
缺点
- 指令在执行阶段要多次访存(一次间接寻址需两次访存,多次间接寻址需根据存储字的最高位确定访存次数)。
- 访问速度过慢,这种寻址方式并不常用。
一般问到扩大寻址范围时,通常指的是寄存器间接寻址。
在图4.5中,
- 主存字第一位为1时,表示取出的仍不是操作数的地址,即多次间址
- 主存字第一位为0时,表示取得的是操作数的地址。
「常见寻址」寄存器寻址
- 指在指令字中直接给出操作数所在的寄存器编号,即
,其操作数在由 所指的寄存器内,如图4.6所示。 - 优点
- 指令在执行阶段不访问主存,只访问寄存器
- 因寄存器数量较少,对应地址码长度较小,使得指令字短且因不用访存,所以执行速度快,支持向量/矩阵运算。
- 缺点
- 寄存器价格昂贵,计算机中的寄存器个数有限。
「常见寻址」寄存器间接寻址
- 指在寄存器
中给出的不是一个操作数,而是操作数所在主存单元的地址,即 ,如图4.7所示。 - 特点
- 与一般间接寻址相比速度更快,但指令的执行阶段需要访问主存(因为操作数在主存中)。
「常见寻址」相对寻址
把 PC 的内容加上指令格式中的形式地址 A 而形成操作数的有效地址
即
其中 A 是相对于当前 PC 值的位移量,可正可负,补码表示,如图4.8所示。
在图4.8中,A的位数决定操作数的寻址范围。
优点
- 操作数的地址不固定,随PC值的变化而变化
- 与指令地址之间总是相差一个固定值,因此便于程序浮动。
相对寻址广泛应用于转移指令。
注意
- 对于转移指令 JMP A,当CPU 从存储器中取出一字节时,会自动执行
。
- 对于转移指令 JMP A,当CPU 从存储器中取出一字节时,会自动执行
若转移指令的地址为X,且占2B,在取出该指令后,PC 的值会增 2,即(PC)=X+2,这样在执行完该指令后,会自动跳转到 X+2+A 的地址继续执行。
「常见寻址」基址寻址
- 指将 CPU 中基址寄存器(BR)的内容加上指令格式中的形式地址 A 而形成操作数的有效地址
- 即
。 - 其中基址寄存器
- 既可采用专用寄存器
- 又可采用通用寄存器,如图4.9所示。
- 基址寄存器是面向操作系统的
- 其内容由操作系统或管理程序确定
- 主要用于解决程序逻辑空间与存储器物理空间的无关性。
- 在程序执行过程中
- 基址寄存器的内容不变(作为基地址)
- 形式地址可变(作为偏移量)。
- 采用通用寄存器作为基址寄存器时,
- 可由用户决定哪个寄存器作为基址寄存器
- 但其内容仍由操作系统确定
- 优点
- 可扩大寻址范围(基址寄存器的位数大于形式地址 A 的位数)
- 用户不必考虑自己的程序存于主存的哪个空间区域,因此有利于多道程序设计,并可用于编制浮动程序,
- 缺点
- 但偏移量(形式地址 A)的位数较短
「常见寻址」变址寻址
- 指有效地址 EA 等于指令字中的形式地址 A 与变址寄存器 IX 的内容之和
- 即 EA = (IX) + A
- 其中 IX 为变址寄存器(专用),也可用通用寄存器作为变址寄存器。
- 图4.10所示为采用专用寄存器 IX 的变址寻址示意图。
- 变址寄存器面向用户
- 在程序执行过程中
- 变址寄存器的内容可由用户改变(作为偏移量)
- 形式地址A不变(作为基地址)。
- 在程序执行过程中
- 基址寻址面向系统
- 主要用于为多道程序或数据分配存储空间
- 在程序的执行过程中
- 基址寄存器的内容通常由操作系统或管理程序确定,值不可变
- 指令字中的A是可变的;
- 优点
- 可扩大寻址范围(变址寄存器的位数操作数大于形式地址 A 的位数)
- 在数组处理过程中
- 可设定A为数组的首地址,不断改变变址寄存器IX的内容,便可很容易形成数组中任意一个数据的地址,特别适合编制循环程序。
- 偏移图4.10 变址寻址量(变址寄存器 IX)的位数足以表示整个存储空间。
- 变址寻址与基址寻址的有效地址形成过程极为相似。但从本质有较大区别。
「常见寻址」堆栈寻址
堆栈
- 是存储器(或专用寄存器组)中一块特定的、按后进先出(LIFO)原则管理的存储区,该存储区中读/写单元的地址是用一个特定的寄存器给出的,该寄存器称为堆栈指针(SP)。
- 堆栈可分为硬堆栈与软堆栈两种。
硬堆栈
- 为寄存器堆栈。
- 寄存器堆栈的成本较高,不适合做大容量的堆栈
软堆栈
- 从主存中划出一段区域来做的堆栈。
在采用堆栈结构的计算机系统中
- 表面上都表现为无操作数指令的形式的大部分指令,操作数地址都隐含使用了 SP。
- 在读/写堆栈中的一个单元的前后都伴有自动完成对 SP 内容的增量或减量操作。
「常见寻址」寻址总结
总结寻址方式、有效地址及访存次数(不含取本条指令的访存)
第三节 程序的机器级代码表示
「汇编」相关寄存器
- x86 处理器中有8个32位的通用寄存器,各寄存器及说明如图4.11所示
- 为了向后兼容, EAX、EBX、ECX 和EDX 的高两位字节和低两位字节可以独立使用
- E为Extended,表示32位的寄存器
- EAX 的低两位字节称为AX
- AX 的高低字节又可分别作为两个8位寄存器
- 高8位 AH
- 低8位 AL
- AX 的高低字节又可分别作为两个8位寄存器
- 除 EBP 和 ESP 外,其他几个寄存器的用途是比较任意的。
「汇编」汇编指令格式
- AT&T 格式和 Intel 格式的主要区别
方面 | AT&T | Intel |
---|---|---|
指令 | 只能用小写字母 | 对大小写不敏感 |
操作数方向 | 第一个为源操作数 第二个为目的操作数 方向从左到右 合乎自然 | 第一个为目的操作数 第二个为源操作数 方向从右向左 |
是否前缀 | 寄存器需要加前缀“%” 立即数需要加前缀“$” | 寄存器和立即数 都不需要加前缀 |
内存寻址 | 使用( 和) | 使用[ 和] |
处理复杂寻址方式 | 内存操作数disp(base, index, scale) 分别表示 偏移量、基址寄存器、变址寄存器和比例因子 如 8(%edx, %eax, 2) 表示操作数为M[R[edx]+R[eax]*2+8] | 操作数为 [edx+eax*2+8] |
指定数据长度 | 操作码的后面紧跟一个字符,表明操作数大小,6 表示 byte(字节)、w 表示word(字)、l 表示long(双字) | 操作码后面显式地注明byte ptr 、word ptr 、dword ptr |
- 表4.2展示了两种格式的几条不同指令。
- mov 指令
- 用于在内存和寄存器之间或者寄存器之间移动数据
- lea 指令
- 用于将一个内存地址(而不是其所指的内容)加载到目的寄存器
「汇编」常用指令
汇编指令通常可以分为
- 数据传送指令
- 逻辑计算指令
- 控制流指令
下面为 Intel 格式一些重要的指令
- 用于操作数的标记,分别表示寄存器、内存和常数。
<reg>
:表示任意寄存器,若其后带有数字,则指定其位数<reg32>
表示 32 位寄存器(eax、ebx、ecx、edx、esi、edi、esp 或 cbp)<reg16>
表示 16 位寄存器(ax、 bx、cx 或 dx);<reg8>
表示 8 位寄存器(ah、al、bh、bl、ch、cl、dh、dl)。
<mem>
:表示内存地址- 如
[eax].[var + 4]
或dword ptr [eax + ebx]
。
- 如
<con>
:表示 8 位、16 位或 32 位常数。<con8>
表示 8 位常数<con16>
表示 16 位常数<con32>
表示 32 位常数。
x86中的指令机器码长度为1字节
对同一指令的不同用途有多种编码方式
比如 mov 指令就有28种机内编码
用于不同操作数类型或用于特定寄存器
例如
mov ax, <con16> # 机器码为 B8H
mov al, <con8> # 机器码为 BOH
mov <reg16>, <reg16>/<mem16> # 机器码为 89H
mov <reg8>/<mem8>, <reg8> # 机器码为 8AH
mov <reg16>/<mem16>, <reg16> # 机器码为 8BH
「数据传送」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字节中
「算术和逻辑运算」add/sub指令
- add 指令
- 两个操作数相加,相加的结果保存到第一个操作数中。
- sub指令
- 两个操作数相减,相减的结果保存到第一个操作数中。
它们的语法如下:
「算术和逻辑运算」inc/dec 指令
- inc、dec 指令分别表示将操作数自加1、自减1。
它们的语法如下:
「算术和逻辑运算」imul 指令
- 带符号整数乘法指令,有两种格式
- 两个操作数,两个操作数相乘,将结果保存在第一个操作数中,第一个操作数必须为寄存器。
- 三个操作数,将第二个和第三个操作数相乘,将结果保存在第一个操作数中,第一个操作数必须为寄存器。
其语法如下:
- 乘法操作结果可能溢出,则编译器置溢出标志 OF=1,以使 CPU 调出溢出异常处理程序。
「算术和逻辑运算」idiv 指令
- 带符号整数除法指令,它只有一个操作数,即除数
- 被除数 则为 edx:eax 中的内容(64位整数),
- 操作结果有两部分:
- 商:送到eax
- 余数:送到edx
其语法如下:
「算术和逻辑运算」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, OfH #将 eax 中的前28位全部置为0,最后4位保持不变
xor edx, edx #置 edx 中的内容为0
「算术和逻辑运算」not 指令
- 位翻转指令
- 将操作数中的每一位翻转,即
。
- 将操作数中的每一位翻转,即
其语法如下:
not <reg>
not <mem>
举例:
not byte ptr [var] #将 var 值指示的内存地址的一字节的所有位翻转
「算术和逻辑运算」neg指令
- 取负指令
其语法如下:
「算术和逻辑运算」shl/shr 指令
- 逻辑移位指令
- shl (shift left) 为逻辑左移
- shr (shift right) 为逻辑右移
- 第一个操作数表示被操作数
- 第二个操作数指示移位的位数
它们的语法如下:
「控制流」控制流指令
- x86 处理器维持着一个指示当前执行指令的指令指针(IP)
- 当一条指令执行后,此指针自动指向下一条指令。
- IP 寄存器不能直接操作,但可以用控制流指令更新。
- 通常用标签(label)指示程序中的指令地址,在 x86 汇编代码中,可在任何指令前加入标签。
mov esi, [ebp+8]
begin: xor ecx, ecx
mov eax, [esi]
begin
就是第二条指令的标签- 控制流指令通过标签就可以实现程序指令的跳转。
「控制流」jmp指令
- jmp 指令控制 IP 转移到 label 所指示的地址 (从 label 中取出指令执行)。
「控制流」jcondition 指令
- 条件转移指令
- 依据 CPU 状态字中的一系列条件状态转移。
- CPU状态字中包括指示最后一个算术运算结果是否为0,运算结果是否为负数等。
je <label> (jump when equal)
jne <label> (jump when not equal)
jz <label> (jump when last result was zero)
jg <label> (jump when greater than)
jge <label> (jump when greater than or equal to)
j1 <label> (jump when less than)
jle <label> (jump when less than or equal to)
举例:
cmp eax, ebx
jle done #如果 eax 的值小于或等于 ebx 值,跳转到done指示的指令 执行,否则执行下一条指令。
「控制流」cmp/test 指令
- cmp 指令
- 用于比较两个操作数的值
- test 指令
- 对两个操作数进行逐位与运算
- 这两类指令都不保存操作结果,仅根据运算结果设置 CPU 状态字中的条件码
- cmp 和 test 指令通常和 jcondition 指令搭配使用
其语法如下:
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 和 test 指令通常和jcondition 指令搭配使用
- 举例:
cmp dword ptr [var], 10
#将 var 指示的主存地址的4字节内容,与10比较
jne loop #如果相等则继续顺序执行:否则跳转到loop处执行
test eax, eax #测试 eax 是否为零
jz xxxx #为零则置标志ZF为1,转跳到xxxx处执行
「控制流」call/ret 指令
- 分别用于实现子程序(过程、函数等)的调用及返回。
语法如下:
cal1 <label>
ret
- call 指令
- 首先将当前执行指令地址入栈
- 然后无条件转移到由标签指示的指令
- 与其他简单的跳转指令不同,call指令保存调用之前的地址信息(当call 指令结束后,返回调用之前的地址)。
- ret 指令
- 实现子程序的返回机制
- ret 指令弹出栈中保存的指令地址,然后无条件转移到保存的指令地址执行。
- call 和 ret 是程序(函数)调用中最关键的两条指令。
「机器级表示」过程调用
- 上面的call/ret 指令主要用于过程调用,属于无条件转移指令。
- 假定过程P(调用者)调用过程Q(被调用者),过程调用的执行步骤如下:
- P将入口参数(实参)放在Q能访问到的地方。
- P将返回地址存到特定的地方,然后将控制转移到Q。
- Q保存P的现场(通用寄存器的内容),并为自己的非静态局部变量分配空间。
- 执行过程Q。
- Q恢复P的现场,将返回结果放到P能访问到的地方,并释放局部变量所占空间。
- Q取出返回地址,将控制转移到 P。
- 步骤2,是由 call 指令实现的
- 步骤6,通过 ret 指令返回到过程P
- 在上述步骤中,需要为入口参数、返回地址、过程P的现场、过程Q的局部变量、返回结果找到存放空间。
- 但用户可见寄存器数量有限,为此需要设置一个专门的存储区域来保存这些数据,这个存储区域就是栈。
- 寄存器 EAX、ECX 和EDX是调用者保存寄存器,其保存和恢复的任务由过程P负责,当P调用Q时,Q就可以直接使用这三个寄存器。
- 寄存器 EBX、ESI、EDI 是被调用者保存寄存器,Q必须先将它们的值保存在栈中才能使用它们,并在返回P之前先恢复它们的值。
- 一个栈由若干栈帧组成。
- 栈帧:每个过程的自己的栈区
- 帧指针寄存器 EBP 指示栈帧的起始位置(栈底)
- 栈指针寄存器 ESP 指示栈顶
- 栈从高地址向低地址增长
- 当前栈帧的范围在帧指针 EBP 和 ESP 指向的区域之间。
用一个简单的C语言程序来说明过程调用的机器级实现。
int add(int x, int y) {
return x + y;
}
int caller() {
int temp1 = 125;
int temp2 = 80;
int sum = add (templ, temp2);
return sum;
经GCC编译后,caller 过程对应的代码如下(#后面的文字是注释):
- GCC为保证数据的严格对齐而规定每个函数的栈帧大小必须是 16 字节的倍数。
Details
- 图 4.12 为 caller 栈帧的状态
- 假定 caller 被过程P调用
- 执行了第4行指令后 ESP 所指的位置如图中所示
- 看出GCC 为 caller 的参数分配了24 字节的空间
- 汇编代码显示,caller 中只使用了调用者保存寄存器 EAX,没有使用任何被调用者保存寄存器
- 因而在 caller 栈帧中无须保存除 EBP 外的任何寄存器的值
- caller 有三个局部变量 templ、temp2 和 sum,皆被分配在栈帧中
- 在用 call 指令调用add 函数之前,caller 先将入口参数从右向左依次将 temp2 和 temp1 的值(即80 和 125)保存到栈中
- 在执行 call 指令时再把返回地址压入栈中
- 此外,在最初进入 caller 时,还将 EBP 的值压入了栈,因此 caller 的栈帧中用到的空间占4+12+8+4=28字节。
- 但是,caller 的栈帧共有4+24+4=32字节,其中浪费了4字节的空间(未使用)
- 因为GCC为保证数据的严格对齐而规定每个函数的栈帧大小必须是 16 字节的倍数。
call 指令执行后
- add函数的返回参数存放到 EAX 中
- 因而在 call 指令后面的两条指令中
- 指令
movl %eax, -4(%ebp)
将 add 的结果存入 sum 变量的存储空间,该变量的地址为R[ebp]-4
- 指令
movl -4(%ebp), %eax
将 sum 变量的值作返回值送到寄存器 EAX中
- 指令
在执行ret指令之前,应将当前栈帧释放,并恢复旧 EBP 的值
- 上述第14行leave指令实现了这个功能,
- leave 指令功能相当于以下两条指令的功能:
movl %ebp, %esp
popl %ebp
执行完leave 指令后
ret 指令就可从 ESP 所指处取返回地址,以返回P执行
- 编译器也可通过 pop 指令和对 ESP 的内容做加法来进行退栈操作,而不一定要使用leave 指令。
add 过程经GCC编译并进行链接后,对应的代码如下所示:
8048469:55 push gebp
804846a:89 e5 mov gesp, %ebp
804846c:8b 45 0c mov 0xc(febp), %eax
804846f:8b 55 08 mov 0x8(%ebp), %edx
8048472:8d 04 02 lea (%edx, %eax, 1), %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]*1 = x + y
。
上述第6、7行指令序列是结束阶段的代码段
- 通过将 EBP 弹出栈帧来恢复 EBP 在 caller过程中的值,并在栈中退出add 过程的栈帧,使得执行到 ret 指令时栈顶中已经是返回地址
- 这里的返回地址应该是 caller 代码中第12 行指令
movl %eax, -4(%ebp)
的地址。
add 的栈帧中除了需要保存EBP,无须保留其他任何信息
- add 过程中没有用到任何被调用者保存寄存器,没有局部变量
- add 是一个被调用过程,并且不再调用其他过程,因而也没有入口参数和返回地址要保存
「机器级表示」选择语句
条件码(标志位)
- CF:进(借)位标志。
- 最近无符号整数加(减)运算后的进(借)位情况。
- 有进(借) 位时 CF=1;
- 否则 CF=0。
- ZF:零标志。
- 最近的操作的运算结果是否为0。
- 若结果为0 ZF=1;
- 否则 ZF=0。
- SF:符号标志。
- 最近的带符号数运算结果的符号。
- 若负 SF=1;
- 否则 SF=0。
- OF:溢出标志。
- 最近的带符号数运算结果是否溢出。
- 溢出 OF=1
- 否则 OF=0
「机器级表示」循环语句
- 循环结构中,通常使用条件转移指令来判断循环条件的结束
第四节 CISC和RISC的基本概念
「CISC」复杂指令系统计算机
- CISC 的主要特点:
- 指令系统复杂庞大,指令数目一般为200条以上。
- 指令的长度不固定,指令格式多,寻址方式多。
- 可以访存的指令不受限制。
- 各种指令使用频度相差很大。
- 各种指令执行时间相差很大,大多数指令需多个时钟周期才能完成。
- 控制器大多数采用微程序控制。
- 有些指令非常复杂,以至于无法采用硬连线控制。
- 难以用优化编译生成高效的目标代码程序。
「RISC」精简指令系统计算机
- RISC 的主要特点
- 选取使用频率最高的一些简单指令,复杂指令的功能由简单指令的组合实现。
- 指令长度固定,指令格式种类少,寻址方式种类少。
- 只有Load/Store(取数/存数)指令访存,其余指令的操作都在寄存器之间进行。
- CPU 中通用寄存器的数量相当多。
- RISC一定采用指令流水线技术,大部分指令在一个时钟周期内完成。
- 以硬布线控制为主,不用或少用微程序控制。
- 特别重视编译优化工作,以减少程序执行时间。
「CISC & RISC」比较
第五节 疑难点
什么是指令? 什么是指令系统? 为什么要引入指令系统?
- 指令
- 就是要计算机执行某种操作的命令。
- 计算机的指令系统
- 一台计算机中所有机器指令的集合
- 引入指令系统的原因
- 避免了用户与二进制代码直接接触,使得用户编写程序更为方便。
- 指令系统是表征一台计算机性能的重要因素
- 它的格式与功能
- 直接影响到机器的硬件结构
- 直接影响到系统软件
- 影响到机器的适用范围
指令分为哪些部分? 每部分有什么用处?
- 一条指令通常包括
- 操作码字段
- 操作码指出指令中该指令应该执行什么性质的操作和具有何种功能
- 它是识别指令、了解指令功能与区分操作数地址内容的组成和使用方法等的关键信息。
- 地址码字段
- 地址码用于给出被操作的信息(指令或数据)的地址
- 包括参加运算的一个或多个操作数所在的地址、运算结果的保存地址、程序的转移地址、被调用子程序的入口地址等。
- 操作码字段
对于一个指令系统来说,寻址方式多和少有什么影响?
- 多重寻址方式
- 优点:寻址方式的多样化能让用户编程更为方便
- 缺点
- 会造成CPU 结构的复杂化(详见下章)
- 不利于指令流水线的运行。
- 寻址方式太少
- 优点:能够提高 CPU 的效率
- 缺点:对于用户而言,少数几种寻址方式会使编程变得复杂,很难满足用户的需求。
简述各常见指令寻址方式的特点和适用情况
- 立即寻址:操作数获取便捷,通常用于给寄存器赋初值。
- 直接寻址:相对于立即寻址,缩短了指令长度。
- 间接寻址:扩大了寻址范围,便于编制程序,易于完成子程序返回。
- 寄存器寻址:指令字较短,指令执行速度较快。
- 寄存器间接:寻址扩大了寻址范围。
- 基址寻址:扩大了操作数寻址范围,适用于多道程序设计,常用于为程序或数据分配存储空间。
- 变址寻址:主要用于处理数组问题,适合编制循环程序。
- 相对寻址:用于控制程序的执行顺序、转移等。
基址寻址和变址寻址的区别
基址寻址 | 变址寻址 | |
---|---|---|
偏移地址 | 程序员操作 | 固定不变 |
基址寄存器 | 操作系统控制 执行过程动态调整 | 程序员操作 |
一个操作数在内存可能占多个单元,怎样在指令中给出操作数的地址?
- 现代计算机都采用字节编址方式
- 即一个内存单元只能存放一字节的信息
- 一个操作数(如char、int、float、double)可能是8位、16位、32位或64位等
- 因此可能占用1个、2个、4个或8个内存单元
- 也就是说,一个操作数可能有多个内存地址对应
- 有两种不同的地址指定方式
- 大端方式:指令中给出的地址是操作数最高有效字节(MSB) 所在的地址。
- 小端方式:指令中给出的地址是操作数最低有效字节(LSB) 所在的地址。
装入/存储(Load/Store)型指令有什么特点?
- 装入/存储型指令
- 规整型指令系统中的一种通用寄存器型指令风格。
- 在RISC指令系统中较为常见。
- 为了规整指令格式,使指令具有相同的长度
- 规定只有 Load 或 Store 指令才能访问内存。
- 而运算指令不能直接访问内存,只能从寄存器取数进行运算,运算的结果也只能送到寄存器。
- 因为寄存器编号较短,而主存地址位数较长,通过某种方式可使运算指令和访存指令的长度一致。
- 装入/存储型风格的指令系统的最大特点
- 指令格式规整,指令长度一致,一般为32 位。
- 由于只有 Load/Store 指令才能访问内存,程序中可能会包含许多装入指令和存储指令,与一般通用寄存器型指令风格相比,其程序长度会更长。