指令系统
指令(机器指令)是指示计算机执行某种操作的 命令。一台计算机中所有指令的集合构成指令系统,或称指令集。
指令基本格式
指令是一串而二进制代码,通常包括 操作码字段 和 地址码字段。
根据长度可以分为半字长指令、单字长指令或双字长指令。
根据操作数地址码的数量可以分为零地址指令、一地址指令、二地址指令和三地址指令等。
这些指令可能会有某些 隐含操作,如对于零地址指令其将两个操作数从栈顶弹出再将结果入栈等。
若在一个指令系统中所有操作码的长度相同,则成为 定长指令,其执行识别 速度快,控制简单。
扩展操作码指令格式
为了在字长有限的情况下增加指令种类,可以采用 可变长度操作码,但会增加指令译码和分析的难度。
最常见的变长操作码方法就是 扩展操作码,具体方法为短码占用一定的前缀,而长码的前缀不能和短码重复,这样译码时可以根据前缀判断是短码还是长码。
一般对使用频率高的指令分配短的操作码,使用频率低的指令分配长的操作码,后面会有分配方法。
指令的操作类型
按照功能可以分为一下几类:
- 数据传送,通常有寄存器间的传送(MOV)、内存读取到寄存器(LOAD)、寄存器写入内存(STORE)等;
- 算术和逻辑运算,加(ADD)、减(SUB)、乘(MUL)、除(DIV)、比较(CMP)、加一(INC)、减一(DEC)、与(AND)、或(OR)、非(NOT)、异或(XOR)等;
- 移位操作,算术移位、逻辑移位、循环移位等;
- 转移操作,无条件转移(JMP)、条件转移(BRANCH)、调用(CALL)、返回(RET)、陷阱(TRAP)等;
- 输入输出操作,用于完成 CPU 和外部设备之间的交换数据或传送控制命令及状态信息。
指令寻址方式
指令中地址码字段并不是操作数的真实地址,这种地址被称为 形式地址(A),通过寻址才能找到其在存储器中的真实地址,被称为 有效地址(EA).
(A) 表示地址为 A 的数值,即该地址中所存放的数据内容。
指令寻址
指令寻址有两种方式:
- 顺序寻址,即程序计数器加一,自动形成下一条指令的地址;
- 跳跃寻址,通过转移类指令实现,跳跃地址分为 绝对地址(由标记符直接得到) 和 相对地址(相对于当前指令的偏移量),跳跃的结果是当前指令修改 PC 值,因此下条指令还是由程序计数器给出。
数据寻址
隐含寻址
默认操作数的地址为特定的寄存器,如累加寄存器 ACC,优点是有利于缩短指令字长,缺点是需要增加存储操作数或隐含地址的硬件。
立即(数)寻址
地址码表示的不是操作数地址而是 操作数本身。
直接寻址
,地址码就是有效地址,优点是简单,指令执行阶段仅访问主存一次,缺点是寻址范围有限,取决于 的位数。
间接寻址
,有效地址是形式地址的中所存的数,可以有多次间接寻址(需要根据主存内容最高位来定,若是 1 表示仍然需要再进行间接寻址,直到最高位为 0)。
间接寻址的优点是可以扩大寻址范围,但是要多次访问主存,由于访问速度满,不太常用,一般提及扩大寻址范围 常用 寄存器间接寻址。
寄存器寻址
操作数直接存放在寄存器中,优点是执行阶段无需访存,速度快,支持向量/矩阵运算,且指令字短,但是寄存器价格昂贵,数量少。
寄存器间接寻址
此时寄存器中存放的是操作数在主存当中的地址,需要一次访问。
相对寻址
,以程序计数器 PC 的内容为基准,加上形式地址得到最终的有效地址,广泛应用于 转移指令。
相对寻址中形式地址使用补码表示,可正可负。
基址寻址
CPU 中基址寄存器(BR)的内容加上形式地址形成有效地址,,其中基质寄存器可以是专用寄存器也可以是通用寄存器。
基址寄存器是面向操作系统的,其内容由操作系统或管理程序确定,主要用于解决程序逻辑空间与存储器物理空间的无关性。程序执行过程中,基址寄存器 中的内容作为基地址 保持不变,形式地址可变,当使用通用寄存器作为基址寄存器时,用户可以指定哪个通用寄存器作为基址寄存器,但是其内容仍然由操作系统决定。
优点:寻址范围大,用户不必考虑程序存于主存的哪个位置,有利于多道程序设计,可用于编制浮动程序(即程序可以从主存中的一个区域移动到其他区域)。
缺点: 偏移量的位数较短。
变址寻址
CPU 中变址寄存器(IX)的内容加上形式地址形成有效地址,,同样可以用专用寄存器和通用寄存器。
变址寄存器是面向用户的,程序执行过程中,变址寄存器内容可变,形式地址保持不变(充当基地址)。
可以应用于数组处理的过程,如将基地址设为数组起点,变址寄存器存放偏移量用于索引。
优点:寻址范围大,方便处理数组问题。
堆栈寻址
堆栈是存储器(或专用寄存器组)中一块特定的、按后进先出原则管理的存储区,其中读写单元的地址由堆栈寄存器(SP)给出。
堆栈可以分为硬堆栈(寄存器堆栈)与软堆栈(主存堆栈),前者成本高,容量小,后者更划算和常用。
采用堆栈结构的计算机中,大部分指令都表现为无操作数指令的形式,因为其往往隐含使用了 SP.
程序的机器级代码表示
相关寄存器
x86 处理器中有 8 个 32 位通用寄存器,如下表:
| 31-24 | 23-16 | 15-8 | 7-0 |
|---|---|---|---|
| AH | AL | ||
| BH | BL | ||
| CH | CL | ||
| DH | DL | ||
| ESI | ESI | ESI | ESI |
| EDI | EDI | EDI | EDI |
| EBP | EBP | EBP | EBP |
| ESP | ESP | ESP | ESP |
按照行的顺序进行解释:
- 累加器 AX,占据 16 位,并且可以分为高位 AH 和低位 AL,整个 32 位被称为 EAX,这里的 E 表示拓展 Extended 的意思,高 16 位和低 16 位也可以分开使用,下同;
- 基地址寄存器;
- 计数寄存器;
- 数据寄存器;
- 变址寄存器,长度 32 为,下同;
- 变址寄存器;
- 堆栈基指针,和堆栈相关的指针一般不能随意使用,而其他寄存器都是可以的,下同;
- 堆栈顶指针。
汇编指令格式
一般有两种汇编格式:AT&T 格式和 Intel 格式,主要区别如下:
- AT&T 的指令只能使用小写字母,而 Intel 指令对大小写不敏感;
- AT&T 中第一个为源操作数,第二个为目的操作数,Intel 则相反;
- AT&T 中寄存器前面需要加
%,立即数前需要加$,Intel 中则都不需要; - AT&T 使用
(和)进行寻址,Intel 中使用[和]; - 在处理复杂寻址时,AT&T 中的内存操作数为
disp(base,index,sacle),分别表示偏移量、基址寄存器、变址寄存器和比例因子,如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.
由于 或 位体系结构都有 位扩展而来,因此字指的是 位。
常用指令
汇编指令可以分为 数据传送指令、逻辑计算指令 和 控制流指令。
下面介绍用于操作数的标记:
<reg>,表示寄存器,后面跟数字表示其位数,下同;<mem>,表示内存地址;<con>,表示常数;
下面以 Intel 格式为例进行介绍。
数据传送指令
| 名称 | 格式 | 功能 | 备注 |
|---|---|---|---|
| mov | mov <reg>|<mem> <reg>|<mem>|<con> | 将第二个操作数复制到第一个操作数 | 不能直接用于从内存复制到内存 |
| push | push <reg32>|<mem>|<con32> | 将操作数压入内存的栈中,常用于函数调用。 | 栈元素固定为 32 位 |
| pop | pop <reg> | [<var>] | 将栈顶弹出送入寄存器或指定的内存地址中 |
架构中,栈是从高地址向低地址增长的,栈顶 的增长方向与栈实际增长方向相反,若入栈, 应该减去 ( 位)(字节)。
算术和逻辑运算指令
| 名称 | 格式 | 功能 | 备注 |
|---|---|---|---|
| add/sub | add/sub <reg>|<mem> <reg>|<mem>|<con> | 将两个操作数相加/减,结果存入第一个操作数中,减法为第一个操作数减第二个 | 同样不能直接对内存中的两个操作数进行运算 |
| inc/dec | inc <reg>|<mem> | 对操作数自增/减 | |
| imul | imul <reg32> <reg32>``<mem> | 带符号整数乘法,将两个操作数相乘,结果存在第一个操作数 | 第一个操作数必须为寄存器,乘法结果溢出则 OF=1 |
imul <reg32> <reg32>|<mem> <con> | 将后面两个操作数相乘存入第一个 | 第三个操作数必须为常数? | |
| idiv | idiv <reg32>|<mem> | 带符号整数除法,只有一个操作数作为除数,被除数在 edx: eax 中,商被送入 eax, 余数被送入 edx | 被除数是 64 位整数 |
| and/or/xor | and/or/xor <reg>|<mem> <reg>|<mem>|<con> | 位操作,结果送入第一个操作数 | 同样不能直接对两个内存中的操作数运算 |
| not | not <reg>|<mem> | 按位取反 | |
| neg | neg <reg>|<mem> | 取负 | |
| shl/shr | shl/shr <reg>|<mem> <cl>|<con8> | 逻辑左/右移,第二个操作数表示移位的位数 |
控制流指令
| 名称 | 格式 | 功能 | 备注 |
|---|---|---|---|
| jmp | jmp <label> | 控制 IP 跳转到指定的 label 所指示的地址 | |
| jcondition | jcondition <label> | 条件转移指令,根据 CPU 状态字的一系列状态转移 | |
| je | 相等时跳转 | ||
| jne | 不相等时跳转 | ||
| jz | 上个运算结果为 0 时跳转 | ||
| jg | 大于时跳转 | ||
| jge | 大于等于时跳转 | ||
| jl | 小于时跳转 | ||
| jle | 小于等于时跳转 | ||
| cmp/test | cmp/test <reg>|<mem> <reg>|<mem>|<con> | cmp 用于比较两个操作数的值,test 用于两数进行逐位运算 | 不保留操作结果,仅根据运算结果设置 CPU 状态字中的条件码 ,常和 jcomdition 搭配使用 |
| call | call <label> | 调用子程序(过程、函数等)执行 | |
| ret | ret | 子程序返回 |
过程调用的机器级表示
和 指令主要用于过程调用,属于一种无条件转移指令,过程调用可以认为是高级语言中的函数调用。
过程转移指令的执行步骤如下所示,其中调用者为 ,被调用过程为 :
- 将 入口参数(实参)放在 能访问到的地方;
- 将 返回地址 保存到特定的地址,然后将控制转移到 (call 指令);
- 保存 的现场(通用寄存器的内容),同时为 中的非静态局部变量分配空间;
- 执行 ;
- 恢复 的现场,将返回结果放到 能访问的地方,同时释放局部变量;
- 取出返回地址,将控制交换 (ret 指令)。
上述步骤中所需要的入口参数、返回地址、现场、局部变量等,都存储在一个专门的区域中—— 栈。
、 和 是调用者保存寄存器,其保存和恢复的过程由 控制, 调用 时, 可以直接使用这些寄存器,同样,、、 是被调用者保存寄存器, 必须先将它们的值保存到栈中才能使用,并在返回前恢复。
每一个过程所占据的栈区被称为 栈帧,帧指针寄存器 指示栈帧的起始位置(栈底),栈指针寄存器 指示栈顶,栈从高地址向低地址增长,因此入栈是 --.
中为了保证数据严格对齐规定了,每个函数栈帧的大小必须是 字节。
这里所说的栈从高地址从低地址增长主要原因到底是什么,并没有找到具体明确的解释,说法很多,可以归于两类:
- 由于数组的分配的是从低地址向高地址(大端小端不考虑了吗?),而栈从高地址向低地址,可以方便的找到数据的指针;
- 是人为规定的,堆是从低地址到高地址(二者建立在共享栈上?),两者可以提高利用率;某些汇编可以认为规定方向。
选择语句的机器级表示
条件码(标志位):标志位寄存器描述了最近的算术或逻辑运算的属性,可以通过这些状态进行条件跳转
| 标志位 | 作用 | 详细 |
|---|---|---|
| CF | 进(借)标志 | 最近无符号帧数加(减)运算后的进(借)位情况,有则为 1 |
| ZF | 零标志 | 最近操作的匀速啊结果时不时为 0, ,为 0 则为 1 |
| SF | 符号标志 | 最近的带符号数运算的结果的符号,为负则为 1 |
| OF | 溢出标志 | 最近的带符号数运算结果是否溢出,溢出则为 1 |
也有两个只设置条件码而不改变其他任何寄存器 的指令—— 和 ,分别和 和 的行为一致,但是只改变标志位,不会更新到寄存器中。
结合前文提到的 控制流指令 ,即可根据标志位实现对应的跳转。
循环语句的机器级表示
循环同样可以通过对应的跳转指令实现,不再赘述。