【计算机组成原理】第四章:指令系统
本篇笔记课程来源:王道计算机考研 计算机组成原理
【计算机组成原理】第四章:指令系统
- 一、指令系统基础
- 1. 指令格式
- 2. 扩展操作码
- 二、寻址
- 1. 指令寻址
- 2. 数据寻址
- 三、x86 指令基础
- 1. x86 汇编语言指令说明
- 2. 常用的 x86 汇编指令
- 3. AT&T 汇编语言格式
- 四、语句的机器级表示
- 1. 选择语句
- 2. 循环语句
- 五、函数调用的机器级表示
- 1. 小结
- 2. Call 和 Ret 指令
- 3. 访问栈帧
- 4. 切换栈帧
- 5. 传递参数和返回值
- 六、CISC 和 RISC
- 1. 基本概念
- 2. 对比
一、指令系统基础
1. 指令格式
- 指令:又称机器指令。是指示计算机执行某种操作的命令,是计算机运行的最小功能单位。
- 一台计算机的所有指令的集合构成该机的指令系统,也称为指令集。
- 一台计算机只能执行自己指令系统中的指令,不能执行其他系统的指令。如:x86 架构、ARM 架构
- 一条指令就是机器语言的一个语句,它是一组有意义的二进制代码。通常要包括操作码字段和地址码字段两部分:
- 操作码(OP)
- 指明指令应该执行什么性质的操作和具有何种功能
- 操作码是识别指令、了解指令功能和区分操作数地址内容的组成和使用方法等的关键信息。如:停机中断、算术逻辑运算、程序转移等
- 地址码(A)
- 指明对谁进行操作,地址码可能有 0 ~ 4 个
- nnn 位地址码的直接寻址范围是 2n2^n2n,即如果指令总长度不变,则地址码数量越多,寻址能力越差
- 操作码(OP)
- 按地址码数目分类:根据地址码数目不同,可以将指令分为
- 零地址指令:
- 不需要操作数,如空操作、停机、关中断等指令
- 堆栈计算机,两个操作数隐含存放在栈顶或次栈顶,计算结果压回栈顶,如后缀表达式
- 一地址指令:
- 只需要单操作数,如 +1、-1、取反、求补等。
- 指令含义:
OP(A₁)→A₁
- 完成一条指令需要 3 次访存:取指 → 读 A1 → 写 A1
- 指令含义:
- 需要两个操作数,但其中一个操作数隐含在某个寄存器(如隐含在 ACC)
- 指令含义:
(ACC)OP(A₁)→ACC
- 完成一条指令需要 2 次访存:取指 → 读 A1
- 指令含义:
A1 指某个主存地址,(A1)表示 A1 所指向的地址中的内容
- 只需要单操作数,如 +1、-1、取反、求补等。
- 二地址指令:OP | A1(目标操作数) | A2(源操作数)
- 常用于需要两个操作数的算术运算、逻辑运算相关指令
- 指令含义:
(A₁)OP(A₂)→A₁
- 完成一条指令需要 4 次访存:取指 → 读 A1 → 读 A2 → 写 A1
- 三地址指令:OP | A1 | A2 | A3(结果)
- 常用于需要两个操作数的算术运算、逻辑运算相关指令,并将结果写回 A3
- 指令含义:
(A₁)OP(A₂)→A₃
- 完成一条指令需要 4 次访存:取指 → 读 A1 → 读 A2 → 写 A3
- 四地址指令:OP | A1 | A2 | A3(结果)| A4(下一条要执行指令的地址)
- 指令含义同三地址指令
- 访存次数和顺序同三地址指令
- 执行指令后,将 PC 的值修改为 A4 所指的地址
- 零地址指令:
- 指令字长:
- 表示一条指令的总长度(可能会变)
- 机器字长:CPU 进行依次整数运算所能处理的二进制数据的位数(通常和 ALU 直接相关)
- 存储字长:一个存储单元中的二进制代码位数(通常和 MDR 位数相同)
- 半字长指令、单字长指令、双字长指令,是说指令字长是机器字长的多少倍
- 指令字长会影响取指令所需时间。如:机器字长 = 存储字长 = 16 bit,则取一条双字节指令需要 2 次访存
- 按指令长度分类:
- 定长指令字结构:指令系统中所有指令的长度都相等
- 变长指令字结构:指令系统中各种指令的长度不等
- 操作码位数反映系统中最多可以支持多少种指令
- 对于 nnn 位操作码可支持 2n2^n2n 种指令
- 按操作码长度分类:
- 定长操作码:指令系统中所有指令的操作码长度都相同
- nnn 位操作码字段的指令系统最大能够表示 2n2^n2n 条指令
- 优点:简化计算机硬件设计,提高指令译码和识别速度
- 缺点:指令数量增加时会占用更多固定位,留给表示操作数地址的位数受限,灵活度较低
- 可变长操作码:全部指令的操作码字段位数不固定,且分散地放在指令字的不同位置上
- 最常见的可变长操作码方法是扩展操作码,使操作码的长度随地址码的减少而增加,不同地址数的指令可以具有不同长度的操作码,从而在满足需要的前提下,有效地缩短指令长度
- 优点:在指令长度有限的前提下仍保持比较丰富的指令种类
- 缺点:增加了指令译码和分析的难度,使控制器的设计复杂化
- 扩展操作码指令格式:定长指令字结构 + 可变长操作码
- 定长操作码:指令系统中所有指令的操作码长度都相同
- 按操作类型分类:
- 数据传送类:CPU 与主存之间的数据传送
- LOAD:把存储器(源)中的数据放到寄存器(目的)中
- STORE:把寄存器(源)中的数据放到存储器(目的)中
- 运算类:算术逻辑操作、移位操作
- 算术操作:加减乘除、增 1、减 1、求补、浮点运算、十进制运算等
- 逻辑操作:与或非、异或、位操作、位测试、位清除、位求反等
- 移位操作:算术移位、逻辑移位、循环移位(带进位和不带进位)等
- 程序控制类:改变程序执行流
- 转移操作:
- 无条件转移:JMP
- 条件转义:JZ 结果为 0、JO 结果溢出、JC 结果有进位
- 调用 CALL、返回 RETURN
- 陷入(TRAP)
- 转移操作:
- 输入输出类:进行 CPU 和 I/O 设备之间的数据传送
- CPU 寄存器与 IO 端口(IO 接口中的寄存器)之间的数据传送
- 数据传送类:CPU 与主存之间的数据传送
2. 扩展操作码
- 扩展操作码指令格式:
- 不同地址数指令使用不同长度的操作码
- 定长指令字结构 + 可变长操作码
- 注意两点:
- 不允许短码是长码的前缀,即短操作码不能与长操作码的前面部分的代码相同
- 各指令的操作码一定不能重复
- 设地址长度为 nnn,上一层留出 mmm 种状态,下一层可扩展出 m×2nm \times 2^nm×2n 种状态。
- 设指令字长固定为 16 位,地址长度为 4 位,设计一套指令系统满足:
- 设指令字长固定为 16 位,地址长度为 4 位,设计一套指令系统满足:
- 通常情况下,对使用频率较高的指令,分配较短的操作码;对使用频率较低的指令,分配较长的操作码,从而尽可能减少指令译码和分析的时间。
二、寻址
1. 指令寻址
- 每一条指令的执行都分为 “取指令”、“执行指令” 两个阶段
- 指令寻址:确定下一条欲执行指令的存放地址
- 不管如何寻址,每次取指令结束后,程序计数器 PC 始终指向下一条指令的地址
- 在 x86 处理器中,程序计数器 PC(Program Counter)通常被称为 IP(指令指针,Instruction Pointer)
- 顺序寻址
(PC) + n → PC
:nnn 表示指令字长,指令字长会因指令长度、编址方式而不同- 若系统采用定长指令字(设指令字长 = 存储字长 = 16 bit)结构,主存按字编址,则 PC 每次
+1
- 指令系统同上,但主存按字节编址,则 PC 每次
+2
- 若系统采用变长指令字结构,主存按字节编址,CPU 会先读入一个字,根据操作码判断这条指令的总字数 nnn,然后 PC
+n
- 若系统采用定长指令字(设指令字长 = 存储字长 = 16 bit)结构,主存按字编址,则 PC 每次
- 跳跃寻址
- 由转移指令指出
- 如无条件转移 JMP,将 PC 中的内容更变为指令所指向的地址
2. 数据寻址
- 数据寻址:确定本条指令的地址码指明的真实地址
- 在每一条指令中通过寻址特征,表示用什么方式寻址。
- 根据形式地址(A)求出操作数的真实地址,称为有效地址(EA,Effective Address)
- 每一个形式地址都会配上寻址特征(一对一)
- 有 10 种数据寻址方式,因此可用 4 bit 表示寻址特征:隐含寻址、立即寻址、直接寻址、间接寻址、寄存器寻址、寄存器间接寻址、偏移寻址(相对寻址、基址寻址、变址寻址)、堆栈寻址
- 小结:
序号 寻址方式 有效地址 执行指令访存次数 优点 缺点 1 直接寻址 EA = A 1 简单,不需专门计算操作数的地址 操作数的地址不易修改,灵活性较差 2 一次间接寻址 EA = (A) 2 可扩大寻址范围、便于编制程序 指令在执行阶段要多次访存 3 寄存器寻址 EA = Ri 0 指令执行阶段只访问寄存器;指令字短、执行速度快;支持向量 / 矩阵运算 寄存器价格昂贵,计算机中寄存器个数有限 4 寄存器间接一次寻址 EA = (Ri) 1 比一般间接寻址相比速度更快 与寄存器寻址相比指令的执行阶段需要访问主存 5 隐含寻址 程序指定 0 有利于缩短指令字长 需增加存储操作数或隐含地址的硬件 6 立即寻址 A 即是操作数 0 指令执行阶段不访问主存,指令执行时间最短 A 的位数限制了立即数的范围 7 基址寻址 EA = (BR) + A 1 可扩大寻址范围,有利于多道程序设计,可用于编制浮动程序(内存里浮动) \ 8 变址寻址 EA = (IX) + A 1 便于处理数组问题,适合编制循环程序 \ 9 相对寻址 EA = (PC) + A 1 广泛用于转移指令,便于程序浮动(程序内部浮动) \ 10 堆栈寻址 入栈 / 出栈时 EA 的确定方式不同 硬堆栈不访存,软堆栈访存 1 次 硬堆栈速度快,软堆栈成本低 软堆栈速度慢、硬堆栈成本高
前情提要:
- 假设指令字长 = 机器字长 = 存储字长
- 假设操作数为 3
- 以一地址指令为例
- A 表示地址(类似指针)、(A)表示数据(类似指针指向的值)
- 直接寻址
- 指令字中的形式地址 A 就是操作数的真实地址 EA,即 EA = A
- 访存次数:2 次 —— 取指令 1 次、执行指令 1 次
- 优点:简单,指令执行阶段仅访问一次主存,不需专门计算操作数的地址
- 缺点:A 的位数决定了该指令操作数的寻址范围,操作数的地址不易修改,灵活性较差
- 间接寻址
- 指令的地址字段给出的形式地址不是操作数的真正地址,而是操作数有效地址所在的存储单元的地址,也就是操作数地址的地址。即 EA = (A)
- 访存次数:至少 3 次 —— 取指令 1 次、执行指令至少 2 次
- 优点:可扩大寻址范围(有效地址 EA 的位数大于形式地址 A 的位数)、便于编制程序(方便地完成子程序返回)
- 缺点:指令在执行阶段要多次访存(一次间接寻址需两次访存,多次寻址需根据存储字的最高位确定几次访存)
- 寄存器寻址
- CPU 内部有很多寄存器,每个寄存器有自己的编号,记为 RiR_iRi
- 在指令字中直接给出操作数所在的寄存器编号,即 EA = Ri,其操作数在 Ri 所指的寄存器内。
- 访存次数:1 次 —— 取指令 1 次、执行指令 0 次
- 优点:指令在执行阶段不访问主存,只访问寄存器;指令字短且执行速度快;支持向量 / 矩阵运算
- 缺点:寄存器价格昂贵,计算机中寄存器个数有限
- 寄存器间接寻址
- 寄存器 Ri 中给出的不是一个操作数,而是操作数所在主存单元的地址,即 EA = (Ri)
- 访存次数:2 次 —— 取指令 1 次、执行指令 1 次
- 特点:比一般间接寻址相比速度更快,但指令的执行阶段需要访问主存(因为操作数在主存中)
- 隐含寻址
- 不是明显地给出操作数的地址,而是在指令中隐含着操作数的地址
- 优点:有利于缩短指令字长
- 缺点:需增加存储操作数或隐含地址的硬件
- 立即寻址
- 形式地址 A 就是操作数本身,又称为立即数,一般采用补码形式。
#
表示立即寻址特征,如:LOAD # 666
- 访存次数:1 次 —— 取指令 1 次、执行指令 0 次
- 优点:指令执行阶段不访问主存,指令执行时间最短
- 缺点:A 的位数限制了立即数的范围。若 A 的位数为 nnn,则立即数采用补码时的表示范围是 −2n−1-2^{n-1}−2n−1 ~ 2n−1−12^{n-1}-12n−1−1
- 形式地址 A 就是操作数本身,又称为立即数,一般采用补码形式。
以某个地址作为起点形式地址视为 “偏移量”,以下 3 种寻址方式都属于偏移寻址。
- 基址寻址
- 以程序的起始存放地址作为 “起点”
- 将 CPU 中 基址寄存器(BR,Base Address Register)的内容加上指令格式中的形式地址 A,而形成操作数的有效地址,即 EA = (BR) + A
- 程序运行前,CPU 将 BR 的值修改为该程序的起始地址(存在操作系统 PCB 中)
- BR 是面向操作系统的,其内容由操作系统或管理程序确定。在程序运行过程中,BR 的内容不变(作为基地址),形式地址可变(作为偏移量)
- 如果不采用专门的 BR,而是将通用寄存器作为基址寄存器使用,则根据通用寄存器总数判断地址所需位数。如:8 个寄存器用 3 bit 表示
- 若采用通用寄存器作为基址寄存器,可由用户决定哪个寄存器作为基址寄存器,但其内容仍由操作系统确定。
- 优点:可扩大寻址范围(寄存寄存器的位数大于形式地址 A 的位数);用户不必考虑自己的程序存于主存的哪一空间区域,因此有利于多道程序设计,以及可用于编制浮动程序(整个程序在内存里的浮动)
- 变址寻址
- 程序员自己决定从哪里作为 “起点”
- 有效地址 EA 等于指令字中的形式地址 A 与 变址寄存器(IX,Index Register)的内容相加之和,即 EA = (IA) + A
- IX 可为专用变址寄存器,也可用通用寄存器作为变址寄存器
- 变址寄存器是面向用户的,在程序执行过程中,变址寄存器的内容可由用户改变(IX 作为偏移量),形式地址 A 不变(作为基地址),正好与基址寻址相反
- 优点:在数组处理过程中,可设定 A 为数组的首地址,不断改变变址寄存器 IX 的内容,便可很容易形成数组中任一数据的地址,特别适合编制循环程序
- 实际应用中往往需要多种寻址方式复合使用(可理解为复合函数):
- 先基址后变址寻址:EA = (IX) + ((BR) + A)
- 相对寻址
- 以程序计数器 PC 所指地址作为 “起点”
- 把程序计数器 PC 的内容加上指令格式中的形式地址 A 而形成操作数的有效地址,即 EA = (PC) + A,其中 A 是相对于 PC(下一条指令地址)所指地址的偏移量,可正可负,补码表示。
- 优点:操作数的地址不是固定的,它随着 PC 值得变化而变化,并且与指令地址之间总是相差一个固定值,因此便于程序浮动(一段代码在程序内部的浮动),相对地址广泛用于转移指令
- 堆栈寻址
- 堆栈是存储器(或专用寄存器组)中一块特定的按 “先进先出(FIFO)” 原则管理的存储区,该存储区中被读 / 写单元的地址是用一个特定的寄存器给出的,该寄存器称为堆栈指针(SP,Stack Pointer)
- 操作数存放在堆栈中,隐含是用堆栈指针 SP 作为操作数地址。
- 堆栈分为硬堆栈、软堆栈
- 硬堆栈:用专门的寄存器实现堆栈,不用访存,速度快,成本高
- 软堆栈:在主存中划分一片区域作为堆栈,速度慢,成本低
- 记栈顶单元为 MspM_{sp}Msp,YYY 是寄存器
- 出栈指令 POP、入栈指令 PUSH
- 如果栈顶在小地址方向:
- 出栈
PUSH ACC
:(Msp) → ACC; (SP) + 1 → SP; - 入栈
PUSH Y
: (SP) -1 → SP; (Y) → Msp;
- 出栈
- 如果栈顶在大地址方向:
- 出栈
PUSH ACC
:(Msp) → ACC; (SP) - 1 → SP; - 入栈
PUSH Y
: (SP) +1 → SP; (Y) → Msp;
- 出栈
三、x86 指令基础
1. x86 汇编语言指令说明
- 已知高级语言可通过编译器(编译程序)编译成汇编语言,汇编语言通过汇编器(汇编程序)翻译为与之对等的机器语言
- 一条高级语言代码可编译成多条汇编语言(一对多)
- 一条汇编语言可翻译成一条机器语言(一对一)
- 汇编语言和机器语言都属于机器级代码
前情提要:
mov
指令将源操作数复制到目的操作数所指的位置
- 记源操作数为 s\text{s}s(source),目的操作数 d\text{d}d(destination)。如:
mov d,s
- 指明内存的读写长度:
dword ptr
:double word pointer,双字,32 bitword ptr
:单字,16 bitbyte ptr
:字节,8 bit
- 指明内存地址:
- 使用
[]
表示一个地址,内存地址通常用十六进制表示,结尾通常会写 h,比如[af996h]
- 地址前通常还会指明此次要读 / 写的数据长度。比如:
move byte ptr [af996h], 5
- 使用
- 在 x86 架构中,寄存器名称以 E(Extended,扩展)开头时,其长度通常为 32 位。x86 架构 CPU 的常见寄存器:
- 通用寄存器:以 X 结尾,表未知。
- 扩展(E = Extended = 32 bit)通用寄存器:EAX、EBX、ECX、EDX
- 基础通用寄存器(使用低 16 bit):AX、BC、CX、DX
- 只使用基础的高 8 位:AH、BH、CH、DH
- 只使用基础的低 8 位:AL、BL、CL、DL
- 变址寄存器:用于线性表、字符串的处理,以 I 结尾,表变址 Index
- ESI(Source)、EDI(Destination)
- 堆栈寄存器:用于实现函数调用,以 P 结尾,表指针 Pointer。
- EBP:堆栈基指针,Extended Base Pointer
- ESP:堆栈顶指针,Extended Stack Pointer
- 通用寄存器:以 X 结尾,表未知。
汇编指令 | 功能说明 | 源操作数寻址方式 | 目的操作数寻址方式 |
---|---|---|---|
mov eax, dword ptr [ebx] | 将 ebx 所指主存地址的 32bit 复制到 eax 寄存器中 | 寄存器间接寻址 | 寄存器寻址 |
mov dword ptr [ebx], eax | 将 eax 的内容复制到 ebx 所指主存地址的 32bit | 寄存器寻址 | 寄存器间接寻址 |
mov eax, byte ptr [ebx] | 将 ebx 所指的主存地址的 8bit 复制到 eax | 寄存器间接寻址 | 寄存器寻址 |
mov eax, [ebx] | 若未指明主存读写长度,默认 32 bit,与第一条指令等价 | 寄存器间接寻址 | 寄存器寻址 |
mov [af996h], eax | 将 eax 的内容复制到 af996h 所指的地址(未指明长度默认 32bit ) | 寄存器寻址 | 直接寻址 |
mov eax, dword ptr [ebx+8] | 将 ebx+8 所指主存地址的 32bit 复制到 eax 寄存器中 | 相对寻址 | 寄存器寻址 |
mov eax, dword ptr [af996-12h] | 将 af996-12h 所指主存地址的 32bit 复制到 eax 寄存器中 | 直接寻址 | 寄存器寻址 |
2. 常用的 x86 汇编指令
- 单词缩写:
- <reg>:寄存器 register
- <mem>:内存 memory
- <con>:常数 constant
- 规定:
- 目的操作数 d 不能是常量,只能是寄存器或主存单元
- 源操作数 s 可以是常量、寄存器、主存单元
- x86 中不允许两个操作数同时来自主存
- 算术运算指令
功能 英文 汇编指令 注释 加 add add d,s 计算 d + s,结果存入 d 减 subtract sub d,s 计算 d - s,结果存入 d 乘 multiply mul d,s 无符号数 d * s,乘积存入 d imul d,s 有符号数 d * s,乘积存入 d 除 divide div s 无符号数除法 edx:eax/s,商存入 eax,余数存入 edx idiv s 有符号数除法 edx:eax/s,商存入 eax,余数存入 edx 取负数 negative neg d 将 d 取负数,结果存入 d 自增++ increase inc d 将 d ++,结果存入 d 自减– decrease dec d 将 d - -,结果存入 d - 除法运算默认被除数已存入
edx:eax
寄存器,两个寄存器合并拓展为 64 bit 再进行除法运算(看第二章的除法器) - 乘法和除法的
i
可理解为 integer —— 带符号的整数
- 除法运算默认被除数已存入
- 逻辑运算指令
功能 英文 汇编指令 注释 与 and and d,s 将 d、s 逐位相与,结果放回d 或 or or d,s 将 d、s 逐位相或,结果放回d 非 not not d 将 d 逐位取反,结果放回d 异或 exclusive or xor d,s 将 d、s 逐位异或,结果放回d 左移 shift left shl d,s 将d逻辑左移s位,结果放回d(通常s是常量) 右移 shift right shr d,s 将d逻辑右移s位,结果放回d(通常s是常量) - 其他指令
- 用于实现分支结构、循环结构的指令:cmp、test、jmp、jxxx
- 用于实现函数调用的指令:push、pop、call、ret
- 用于实现数据转移的指令:mov
3. AT&T 汇编语言格式
- x86 架构由 intel 首创,在 x86 架构下汇编语言有两大主流语法体系,分别是 AT&T 格式 和 intel 格式。
- AT&T 格式 和 intel 格式对比
AT&T 格式 Intel 格式 常用于 Unix、Linux Windows 目的操作数d、源操作数s op s, d
注:源操作数在左,目的操作数在右op d, s
注:源操作数在右,目的操作数在左寄存器的表示 mov %ebx, %eax
注:寄存器名之前必须加"%"mov eax, ebx
注:直接写寄存器名即可立即数的表示 mov $985, %eax
注:立即数之前必须加“$”mov eax, 985
注:直接写数字即可主存地址的表示 mov %eax, (af996h)
注:用“小括号”mov [af996h], eax
注:用“中括号”读写长度的表示 movb $5, (af996h)
movw $5, (af996h)
movl $5, (af996h)
addb $4, (af996h)
注:指令后加 b、w、l 分别表示读写长度为 byte、word、dwordmov byte ptr [af996h], 5
mov word ptr [af996h], 5
mov dword ptr [af996h], 5
add byte ptr [af996h], 4
注:在主存地址前说明读写长度byte、word、dword主存地址偏移量的表示 movl -8(%ebx), %eax
注:偏移量(基址)
movl 4(%ebx, %ecx, 32), %eax
注:偏移量(基址,变址,比例因子)mov eax, [ebx - 8]
注:[基址+偏移量]
mov eax, [ebx + ecx*32 + 4]
注:[基址+变址*比例因子+偏移量]
四、语句的机器级表示
1. 选择语句
- 选择语句即分支结构,分支结构可能改变程序的执行流
- 无条件转移指令
jmp
- 格式:
jmp <地址>
- 作用:PC 无条件转移至 <地址>,类似 C 语言中的 goto 语句,但无法实现
if...else...
这种复杂功能 - 地址可来自常量
jmp 996
、寄存器jmp eax
、主存单元jmp [999h]
- 地址还可以用 “标号” 锚定,标号要有冒号,名字自己取。如:
jmp NEXT
、jmp iFulling
- 格式:
- 比较指令
cmp
- 格式:
cmp a,b
(compare,比较) - a,ba,ba,b 可能来自常数、寄存器、主存单元
- 比较过程跳转 第二章:数据的表示和运算(下)—— 数的比较
cmp eax,ebx # 比较寄存器 eax 和 ebx 中的值 jg NEXT # 若 eax > ebx,则跳转到 NEXT:
- 格式:
- 条件转移指令
j...
- 条件转移指令通常要和 cmp 指令一起使用。如上
指令 英文(完整含义) 注释 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 则跳转
- 条件转移指令通常要和 cmp 指令一起使用。如上
- if 语句转汇编
写法一:比较用大于,但是 if 和 else 的逻辑会反过来if (a > b) {c = a; } else {c = b; }
写法二:比较反着来,if 和 else 的顺序就会一致mov eax,7 ; 假设变量 a = 7,存入 eax mov ebx,6 ; 假设变量 b = 6,存入 ebx cmp eax,ebx ; 比较变量 a 和 b jg NEXT ; 若 a > b,转移到 NEXT: mov ecx,ebx ; 假设用 ecx 存储变量 c,令c=b jmp END ; 无条件转移到 END: NEXT: mov ecx,eax ; 假设用 ecx 存储变量c,令c=a END:
mov eax,7 ; 假设变量 a = 7,存入 eax mov ebx,6 ; 假设变量 b = 6,存入 ebx cmp eax,ebx ; 比较变量 a 和 b jle NEXT ; 若 a ≤ b,转移到 NEXT: mov ecx,eax ; 假设用 ecx 存储变量 c,令c=a jmp END ; 无条件转移到 END: NEXT: mov ecx,ebx ; 假设用 ecx 存储变量 c,令c=b END:
2. 循环语句
- loop 指令
- ecx 专门作为循环的计数器,loop 指令只能用 ecx 进行循环
- 会对 ecx 寄存器里的值自减,然后判断 ecx 的值是否等于零,如果不等于零继续循环
loop Looptop
等价于dec ecx cmp ecx,0 jne Looptop
- loop 指令可能会使代码更清晰简洁,理论上,能用 loop 指令实现的功能一定能用条件转移指令实现
- loop 扩充指令
loopxx
。如 loopnz、loopz- loopnz:当
ecx != 0 && ZF == 0
时,继续循环 - loopz:当
ecx != 0 && ZF == 1
时,继续循环
- loopnz:当
- 循环语句转 loop 指令
loop 指令for (int i = 500; i > 0; i--) {做某些处理; } // 循环 500 轮
mov ecx, 500 ; 用 ecx 作为循环计数器 Looptop: ; 循环的开始 ... 做某些处理 ... loop Looptop ; ecx--,若 ecx != 0,跳转到 Looptop
- 条件转移指令实现循环需要 4 个部分构成:
- 循环前的初始化
- 是否直接跳过循环?
- 循环主体
- 是否继续循环?
- 循环语句转条件转移指令
条件转移指令// 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
# 1、循环前的初始化 mov eax,0 ; 用 eax 保存 result,初值为0 mov edx,1 ; 用 edx 保存 i,初始值为 1 # 2、是否跳出循环 cmp edx,100 ; 比较 i 和 100 jg L2 ; 若 i > 100,转跳到 L2 执行 L1: ; 循环主体 # 3、循环主体 add eax,edx ; 实现 result += i inc edx ; inc 自增指令,实现 i++ # 4、是否继续循环 cmp edx,100 ; 比较 i 和 100 jle L1 ; 若 i <= 100,转跳到 L1 执行 L2: ; 跳出循环主体
五、函数调用的机器级表示
前情提要:
- 程序计数器 PC 通常也被称为指令指针寄存器 IP
1. 小结
- 本节内容较多,先做一个小结,后面分点笔记
- 栈帧内部可能包含的内容(自底向上):
层次 内容 是否存在 备注 底部 上一层栈帧基址 一定存在 用于恢复上一层函数的栈帧 二层 若干个局部变量 不一定存在 有些函数可能不定义局部变量 三层 未使用区域 不一定存在 如果其他部分刚好是 16 B 整数倍,则不会剩下 “零头” 四层 部分寄存器值 不一定存在 如果这些寄存器值不是运算的中间结果,则可以不保存 五层 若干个调用参数 不一定存在 有些函数调用不需要传参数 顶部 IP(返回地址) 一定存在(发生调用时) 单反调用其他函数,就必须记录返回地址 - 函数调用的机器级表示分析框架:
2. Call 和 Ret 指令
- 操作系统在内存中开辟一片区域用作函数调用栈,执行函数时,用于存储函数的区域称为函数的栈帧(Stack Frame)
- 函数的栈帧:一个栈帧对应一层函数,用于保存函数大括号内定义的局部变量、保存函数调用相关的信息
- x86 系统中,默认以 4 字节 为栈的操作单位
- 函数调用指令:
call <函数名>
- 通常用函数名作为函数起始地址的 <标号>
- 作用:将 IP 旧值 压栈保存(保存在函数的栈帧顶部,效果相当于
push IP
);设置 IP 新值,无条件转移至被调用函数的第一条指令(效果相当于jmp <标号>
)
- 函数返回指令:
ret
- 作用:从函数的栈帧顶部找到 IP 旧值,将其出栈并恢复 IP 寄存器
- 函数调用转汇编
intel 格式int caller() {int temp1 = 125;int temp2 = 80;int sum = add(temp1, temp2);return sum; }int add(int x, int y) {return x + y; }
; caller 函数实现 caller:push ebp ; 保存旧的ebp(栈帧基址)mov ebp, esp ; 新的ebp = 旧的espsub esp, 24 ; 分配24字节栈空间(局部变量等)mov dword [ebp-12], 125 ; [ebp-12] = 125 (对应C代码 temp1=125)mov dword [ebp-8], 80 ; [ebp-8] = 80 (对应C代码 temp2=80)mov eax, [ebp-8] ; eax = [ebp-8](temp2的值)mov [esp+4], eax ; esp+4 = temp2(准备add的第二个参数)mov eax, [ebp-12] ; eax = [ebp-12](temp1的值)mov [esp], eax ; esp = temp1(准备add的第一个参数)call add ; 调用add函数,返回值存在eaxmov [ebp-4], eax ; 保存add返回值到[ebp-4]mov eax, [ebp-4] ; 准备返回值(eax作为返回寄存器)leave ; 等价于 mov esp, ebp / pop ebpret ; 返回caller的调用者; add 函数实现 add:push ebp ; 保存旧的ebpmov ebp, esp ; 新的ebp = 旧的espmov eax, [ebp+12] ; eax = [ebp+12](第一个参数,temp1)mov edx, [ebp+8] ; edx = [ebp+8] (第二个参数,temp2)add eax, edx ; eax = temp1 + temp2(计算结果)leave ; 恢复栈帧ret ; 返回caller,eax携带返回值
3. 访问栈帧
- 高地址为栈底,低地址为栈顶。因此计组中通常栈底在上,栈顶在下(与数据结构区别开来)
- 标记栈帧范围寄存器:
- ebp:堆栈基地址,Base Pointer。指向当前栈帧的 “底部”
- esp:堆栈顶地址,Stack Pointer。指向当前栈帧的 “顶部”
- 访问栈帧数据方式一:
push
、pop
指令push 🐂
:入栈- 🐂 可以是立即数、寄存器、主存地址
- 已知 x86 默认以 4 字节为单位,先让 esp - 4,再将 🐂 压入栈顶
pop 🐎
:出栈- 🐎 可以是寄存器、主存地址
- 栈顶元素出栈,写入 🐎,再让 esp + 4
- 如
push eax ; 将寄存器 eax 的值压栈 push 985 ; 将立即数 985 压栈 push dword [ebp+8] ; 将主存地址 [ebp+8] 里的数据压栈pop eax ; 栈顶元素出栈,写入寄存器 eax pop dword [ebp+8] ; 栈顶元素出栈,写入主存地址 [ebp+8]
- 访问栈帧数据方式二:
mov
指令结合 esp、ebp 指针mov
指令 +sub
减指令、add
加指令修改栈顶指针 esp 的值
- 如
sub esp, 12 ; 栈顶指针 -12 mov [esp+8], eax ; 将 eax 的值复制到主存 [esp+8] mov dword [esp+4], 985 ; 将 985 复制到主存 [esp+4] mov eax, [ebp+8] ; 将主存 [ebp+8] 的值复制到 eax mov [esp], eax ; 将 eax 的值复制到主存 [esp] add esp, 8 ; 栈顶指针 +8
4. 切换栈帧
- 当发生函数调用时,需要修改 ebp 和 esp,让他们指向新的函数栈帧。当函数执行完毕后,需要让 ebp 和 esp 重新指回上一层函数栈帧
- 已知执行
call
指令后,IP 旧值压入栈顶,IP 新值指向被调用函数的第一条指令 - 函数被调用时,每个函数开头例行执行指令:
; 将栈帧切换为被调用函数栈帧 push ebp ; 保存上一层函数的栈帧基址(ebp 旧值) mov ebp,esp ; 设置当前函数的栈帧基址(ebp 新值)
- 由上面代码可知:每个函数栈帧底部一定存放上一层函数的栈帧基址(利于函数结束时返回)
- 以上两行代码等价于
enter
指令(零地址指令)
- 函数返回时,每个函数结尾例行执行:
; 将栈帧切换为上一个函数栈帧 mov esp,ebp ; 让 esp 指向当前栈帧的底部 pop ebp ; 将 esp 所指元素出栈,写入寄存器 ebp
- 已知
pop
指令会让出栈元素写入寄存器,且 esp + 4,此计可谓是一箭双雕 - 以上两行代码等价于
leave
指令(零地址指令)
- 已知
ret
指令:- 从函数的栈帧顶部(执行
call
时压入的)找到 IP 旧值,将其出栈并恢复 IP 寄存器(即让程序执行流回到call
指令后面)
- 从函数的栈帧顶部(执行
5. 传递参数和返回值
已知:
- 栈帧最底部一定是上一层栈帧基址(ebp 旧值)
- 栈帧最顶部一定是返回地址(当前函数的栈帧除外)
- gcc 编译器将每个栈帧大小设置为 16 B 的整数倍(当前函数的栈帧除外),因此栈帧内可能出现空闲未使用的区域
- 通常将局部变量集中存储在栈帧底部区域
- C 语言中越靠前定义的局部变量越靠近栈顶,
- 如
[ebp - 4]
、[ebp - 8]
等通常是访问本函数局部变量
- 通常将调用参数集中存储再栈帧顶部区域
- C 语言中越靠前的参数越靠近栈顶。
- 如
[ebp + 8]
、[ebp + 12]
等通常是访问上一层函数传过来的参数 [ebp + 4]
是上一层函数的返回地址(IP 旧值)
- 返回值:
- 在
ret
指令前,将函数返回值写入 eax 寄存器 - eax 是通用寄存器,存在数据丢失风险,因此可以把部分寄存器值保存在栈帧中防止丢失
- 在
- 依旧是这块代码:
int caller() {int temp1 = 125;int temp2 = 80;int sum = add(temp1, temp2);return sum; }int add(int x, int y) {return x + y; }
六、CISC 和 RISC
- CISC 和 RISC 是指令系统的两种设计方向
1. 基本概念
- 指令系统的 80-20 规律:典型程序中 80% 的语句仅仅使用处理机中 20% 的指令
- CISC:复杂指令集计算机,Complex Instruction Set Computer
- 设计思路:一条指令完成一个复杂的基本功能,一条指令可以由一个专门的电路完成
- 代表:x86 架构
- 主要用于:笔记本、台式机等
- 有的复杂指令用纯硬件实现很困难,于是采用 “存储程序” 的设计思想,由一个比较通用的电路配合存储部件完成一条指令,于是就有了微指令
- RISC:精简指令集计算机,Reduced Instruction Set Computer
- 设计思想:一条指令完成一个基本 “动作”;多条指令组合完成一个复杂的基本功能
- 代表:arm 架构
- 主要用于:手机、平板等
2. 对比
对比项目 | CISC | RISC |
---|---|---|
指令系统 | 复杂,庞大 | 简单,精简 |
指令数目 | 一般大于200条 | 一般小于100条 |
指令字长 | 不固定 | 定长 |
可访存指令 | 不加限制(如乘法指令可访存) | 只有 Load / Store 指令(乘法指令不能访存) |
各种指令执行时间 | 相差较大 | 绝大多数在一个周期内完成 |
各种指令使用频度 | 相差很大(80-20 规律) | 都比较常用 |
通用寄存器数量 | 较少 | 多 |
目标代码 | 难以用优化编译生成高效的目标代码程序 | 采用优化的编译程序,生成代码较为高效 |
控制方式 | 绝大多数为微程序控制(效率更低) | 绝大多数为组合逻辑控制(效率更高) |
指令流水线 | 可以通过一定方式实现 | 必须实现 |