🚥计算机组成原理 系列文章导航🚥

  1. 计算机组成原理 第一章 计算机系统概述
  2. 计算机组成原理 第二章 数据的表示和运算
  3. 计算机组成原理 第三章 存储系统
  4. 计算机组成原理 第四章 指令系统
  5. 计算机组成原理 第五章 中央处理器
  6. 计算机组成原理 第六章 总线
  7. 计算机组成原理 第七章 输入/输出系统

一、指令集体系结构

机器指令(简称指令) 是指计算机执行某种操作的命令。一台计算机的所有指令的集合构成了该机的指令系统,也称指令集。指令系统是指令集体系结构(ISA) 中最核心的部分,ISA 完整定义了软件和硬件之间的接口。

ISA 规定的内容主要包括:

  • 指令格式,指令寻址方式,操作类型,以及每种操作对应的操作数的相应规定。

  • 操作数的类型,操作数寻址方式,以及是按大端方式还是按小端方式存放。

  • 程序可访问的寄存器编号、个数和位数,存储空间的大小和编码方式。

  • 指令执行过程的控制方式等,包括程序计数器、条件码定义等。

注意:一台计算机只能执行自己指令系统中的指令,不能执行其他系统指令,例如 X86 架构(Intel 系列 CPU)和 ARM 架构(智能手机)的计算机的二进制程序互不兼容。

二、指令的基本格式

指令字长是指一条指令所包含的二进制代码的位数。通常把长度等于机器字长的指令称为单字长指令,等于半个机器字长的称为半字长指令,等于两个机器字长长度的称为双字长指令。

注意:指令长度的不同会导致取指令时间开销的不同,单字长指令只需一次访存就能将完整指令取出;而双字长指令则需要访存两次,耗费两个存取周期。

1.按指令长度分类

  • 定长指令字结构:在一个指令系统中,若所有指令的长度都是相等的。定字长指令的执行速度快,控制简单。

  • 变长指令字结构:各种指令的长度随指令功能而异

因为主存一般是按字节编址的,所以指令字长通常为字节的整倍数。

2.按地址码数目分类

(1)零地址结构:只给出操作码 OP,没有显式地址

  1. 不需要操作数的指令。如空指令、停机指令、关中断指令等。

  2. 零地址的运算类指令仅用在堆栈计算机中。通常参与运算的两个操作数隐含地从栈顶和次栈顶弹出,送到计算机进行运算,运算结果再隐含地压入堆栈(例如数据结构中的后缀表达式,运算时操作数隐含在栈里)。

(2)一地址指令OP + A1

  1. 只有目标操作数的单操作指令。按 A1 地址读取操作数,进行 OP 操作后,结果存回原地址。如:OP(A1) -> A1(3 次访存,取指、读 A1、写 A1)。

  2. 隐含约定目的地址的双操作数指令。按指令地址 A1 可读取源操作数,指令可隐含约定另一个操作数由 ACC 提供,运算结果也存放在 ACC 中。如:(ACC)OP(A1) -> ACC(2 次访存,取指、读 A1)。

(3)二地址指令OP + A1(目的操作数)+ A2(源操作数)

指令含义:(A1)OP(A2) -> A1(4 次访存,取指、读 A1、读 A2,写 A1)

(4)三地址指令OP + A1 + A2 + A3(结果)

指令含义:(A1)OP(A2)->A3(4 次访存,取指、读 A1、读 A2、写 A3)

(5)四地址指令OP + A1 + A2 + A3(结果)+ A4(下址)

指令含义:(A1)OP(A2)->A3,A4 = 下一条将要执行的指令,执行指令后将 PC 的值修改为 A4 所指地址(4 次访存,取指、读 A1、读 A2、写 A3)。

3.按操作码长度分类

  • 定长操作码:指令系统中所有指令的操作码长度都相同。

  • 可变长操作码:指令系统中各指令的操作码长度可变。

最常见的变长操作码方式是扩展操作码,它使操作码的位数随地址数的减少而减少,下图为一种扩展操作码的方式:

image-20250310135316494

其中,零地址指令为 16 条,一、二、三地址指令为 15 条,因为需要留出 1111 作为扩展操作码使用。如果 4 位的基本操作码全部用于三地址,则有 16 条三地址指令。

除了这种安排方式之外,还有其他扩展方式,如 15 条三地址指令、12 条二进制指令、63 条一地址指令和 16 条零地址指令,共 106 条指令:

image-20250310140936824

设地址长度为 n 位,上一层留出 m 种状态,下一层可扩展出 m × 2^n 种状态。

在扩展时需要注意一下两点:

  • 不允许短码是长码的前缀,即短码操作码不能与长操作码的前面部分的代码相同。

  • 各指令的操作码一定不能重复。

一般对使用频率较高的指令分配较短的操作码,对使用频率较低的指令分配较长的操作码,从而尽可能减少指令译码和分析的时间。

4.按操作类型分类

(1)数据传送类

传送操作:通常有寄存器之间的传送(MOV)、从内存单元读取到 CPU 寄存器(LOAD)、从 CPU 寄存器写数据到内存单元(STORE)、进栈操作(PUSH)、出栈操作(POP)等。

(2)运算类

算术逻辑运算:这类指令主要有加(ADD)、减(SUB)、乘(MUL)、除(DIV)、加 1(INC)、减 1(DEC)、与(AND)、或(OR)、取反(NOT)、异或(XOR)等。

移位操作:算术移位、逻辑移位、循环移位等。

(3)程序控制类

转移指令:无条件转移(JMP)、条件转移(BRANCH)、调用(CALL)、返回(RET)、陷阱(TRAP)等。

注意:调用指令和转移指令的区别是,执行调用指令时必须保存下一条指令的地址(返回地址),当子程序执行结束时,根据返回地址返回到主程序继续执行;而转移指令则不返回执行。

(4)输入输出类

输入输出操作:用于完成 CPU 与外部设备交换数据或传送控制命令及信息状态。

三、指令的寻址方式

1.指令寻址

指令寻址的方式有两种:顺序寻址方式,跳跃寻址方式。

(1)顺序寻址

通过程序计数器 PC 加 “1” ,自动形成下一条指令的地址(这里的 1 指当前执行的这条指令的长度)。

注意:PC 自增的大小与编址方式、指令字长有关。现在计算机通常是按字节编址的,若指令字长为 16 位,则 PC 自增为(PC)+ 2;若为 32 位,则为 (PC)+ 4。

(2)跳跃寻址

跳跃是指由本条指令(转移类指令)给出下条指令地址的计算方式。跳跃的方式分为绝对转移(地址码直接指出转移目标地址)和相对转移(地址码指出转移目的地址相对于当前 PC 值的偏移量)。

因为 CPU 总是根据 PC 的内容去主存取指令,所以本质上还是对 PC 的值进行修改,下一条指令仍然通过 PC 给出。

2.数据寻址

数据寻址是指如何在指令中表示或计算出操作数的地址。为了区别不同的寻址方式,通常会在指令字中设置一个寻址特征字段(每个地址前都有一个)。

image-20250310150036633

指令中的地址码字段存储的是形式地址(A),形式地址结合寻址方式,可以计算出操作数在存储器中的真实地址,这种地址称为有效地址(EA)。

  • 若为立即寻址,形式地址的位数决定了操作数的范围。

  • 若为直接寻址,形式地址的位数决定了可寻址的范围。

  • 若为寄存器寻址,形式地址的位数决定了通用寄存器的最大数量。

  • 若为寄存器间接寻址,寄存器的位数决定了可寻址范围。

3.常见的数据寻址方式

假设指令字长 = 机器字长 = 存储字长,操作数为 3,一地址指令。

(1)直接寻址

直接寻址:指令字中的形式地址 A 就是操作数的真实地址 EA,即 EA = A。

image-20250310152218633

优点:简单,指令执行阶段仅访问一次主存,不需专门计算操作数的地址。

缺点:A 的位数决定了该指令操作数的寻址范围,操作数的地址不易修改。

(2)间接寻址

间接寻址:地址字段给出的形式地址不是操作数的真实地址,而是操作数有效地址所在的存储单元的地址,也就是操作数地址的地址,即 EA = (A)。

image-20250310153144418

此外可以进行多次间接寻址,如下图中的两次间接寻址:

image-20250310153611365

当进行多次间接寻址时,可用存储字的首位来表示间接寻址是否结束。如图中,当存储字首位为 “1” 时,标明还需继续访存寻址;为 “0” 时,标明该存储字即为 EA。因此,存储字首位不能作为 EA 的存储部分,寻址范围为 2^15(假设存储字长为 16) 。

优点:可扩大寻址范围(有效地址 EA 的位数大于形式地址 A 的位数);便于编制程序(用间接寻址可方便地完成子程序返回)。

缺点:指令在执行阶段要多次访存(一次间接寻址需两次)。

(3)寄存器寻址

寄存器寻址:在指令字中直接给出操作数所在的寄存器编号,即 EA = R_i,其操作数在由 R_i 所指的寄存器内。

image-20250310154827019

优点:指令在执行阶段不访问主存,只访问寄存器,执行速度快;寄存器数量远小于内存单元数,所以地址码位数少,指令字长短;支持向量/矩阵运算。

缺点:寄存器价格昂贵,CPU 的寄存器数量有限。

(4)寄存器间接寻址

寄存器间接寻址: 寄存器 R_i 中给出的不是一个操作数,而是操作数所在主存单元的地址,即 EA = (R_i)。

image-20250310155757401

特点:综合了间接寻址和寄存器寻址,既扩大了寻址范围,又减少了访存分次数(依旧需要访存,因为操作数在主存中)。

(5)隐含寻址

隐含寻址:不明显地给出操作数地址或变址寄存器和基址寄存器编号,而是由操作码隐含指出。

image-20250310160318459

优点:有利于缩短指令字长。

缺点:需增加存储操作数或隐含地址的硬件。

(6)立即寻址

立即寻址:地址字段指出的不是操作数的地址,而是操作数本身,也称立即数,采用补码表示。

image-20250310161941157

图中 # 表示立即寻址特征,A 就是操作数。

优点:指令在执行阶段不访存,指令执行速度最快。

缺点:A 的位数限制了立即数的范围。

(7)基址寻址

基址寻址:将 CPU 中基址寄存器(BR,base address register)的内容加上指令格式中的形式地址 A,而形成操作数的有效地址,即 EA = (BR) + A。

基址寄存器既可以采用专门寄存器,又可指定某个通用寄存器作为基址寄存器,如下图。

image-20250310183417241

使用通用寄存器时,要在指令中的一个专门字段里指明将哪个通用寄存器作为基址寄存器使用。该字段的位数取决于通用寄存器的数量。

基址寄存器是面向操作系统的,其内容由操作系统或管理程序确定,主要用于解决程序逻辑空间与存储器物理空间的无关性。采用通用寄存器作为基址寄存器时,可由用户决定哪个寄存器作为基址寄存器,但其内容仍由操作系统确定。

优点:可以扩大寻址范围(基址寄存器的位大于形式地址 A 的位数);用户不必考虑自己的程序存于主存的哪一空间区域,故有利于多道程序设计,并可以用于编址浮动程序(整个程序可以在内存里浮动)。

缺点:偏移量(形式地址 A)的位数较短。

(8)变址寻址

变址寻址:有效地址 EA 等于指令中的形式地址 A 与变址寄存器(IX,index register)的内容相加之和,即 EA = (IX) + A。其中 IX 可为变址寄存器(专用),也可用通用寄存器作为变址寄存器(与基址寻址同理)。

image-20250310190709508

变址寄存器是面向用户的,在程序执行过程中,变址寄存器的内容可由用户改变(IX 作为偏移量),形式地址 A 不变(作为基地址)。

在数组处理过程中,可设定 A 为数组的首地址,不断改变变址寄存器 IX 的内容,便可很容易形成数组中任一数据的地址,特别适合编制循环程序

image-20250310191655331

优点:可以扩大寻址范围(变址寄存器的位大于形式地址 A 的位数);适合编制循环程序;偏移量(变址寄存器 IX)的位数足以表示整个存储空间。

注意:基址寻址面向系统,主要用于为多道程序或数据分配存储空间;变址寻址面向用户,主要用于处理数组问题。

(9)相对寻址

相对寻址:把程序计数器 PC 的内容加上指令格式中的形式地址 A 而形成操作数的有效地址,即 EA = (PC) + A,其中 A 是相对于 PC 所指地址的位移量,可正可负补码表示。

注意:PC 在取指后立即加 1,因此 PC 当前值指向的是下一条指令。

image-20250310193122169

优点:操作数的地址不是固定的,它随 PC 值的变化而变化,且与指令地址之间总是相差一个固定的偏移量,因此便于程序浮动(一段代码在程序内部的浮动,而不是整段程序的浮动)。相对寻址广泛应用于转移指令

注意:基址寻址、变址寻址、相对寻址三种寻址方式统称为偏移寻址

(10)堆栈寻址

堆栈寻址:操作数存放在堆栈中,隐含使用堆栈指针(SP)作为操作数地址。

堆栈是存储器(或专用寄存器组)中一块特定的按后进先出(LIFO)原则管理的存储区,该存储区中被读/写单元的地址是用一个特定的寄存器给出的,该寄存器称为堆栈指针(SP)。

寄存器堆栈称为硬堆栈,成本较高,不适合做大容量堆栈。从主存中划出一块区域来做堆栈,这种堆栈叫软堆栈,成本低。硬堆栈在执行读/写时不需要访存,软堆栈每次执行读/写都需要进行一次访存。

在采用堆栈结构的计算机中,大部分指令表面上都表现为无操作数指令的形式,因为操作数地址都隐含使用了 SP。因此在读/写堆栈的前后都伴有自动完成对 SP 的加减操作。

四、程序的机器级代码表示

1.高级语言与机器级代码直接的对应

指令的作用:改变程序执行流;处理数据。

指令格式:操作码 + 地址码

操作码表示进行什么处理,地址码表示数据位置。数据可以在寄存器或主存或指令里。

以 mov 指令为例:

image-20250311144212191

  • 蓝色内容是寄存器的名字,表示寄存器中存储的数据。

  • 中括号表示内存地址(形式地址),h 说明是 16 进制。

  • 红色内容用于指明内存的读写长度,包括三种:dword ptr(双字,32 bit),word ptr(单字,16 bit),byte ptr(半字,8 bit)(x86 架构中一个字 16 比特)。

2.相关寄存器

x86 处理器中一共有 8 个 32 位的寄存器:

image-20250311150216432

为了向后兼容,四个通用寄存器的高两位字节和低两位字节都可以独立使用(另外四个不可以),例如 EAX 的低两位字节称为 AX,而 AX 的高低字节又可以分别作为两个八位寄存器,分别称为 AH 和 AL。

image-20250311150858747

3.常用的 x86 汇编指令

d 表示目的操作数,最终得到的结果要存放与 d 所指的位置,因此 d 不能是立即数;s 表示源操作数。

(1)算术运算

image-20250311152445712

i 表示 integer。

(2)逻辑运算

image-20250311154429845

(3)其他

用于实现分支结构、循环结构的指令:cmp、test、jmp、jxxx

用于实现函数调用的指令:push、pop、call、ret

用于实现数据转移的指令:mov

AT&T 格式和 Intel 格式指令的对比

image-20250311160031276

未给出读写长度均默认 32 bit。

4.选择语句的机器级表示

无条件转移指令jpm <地址> #PC 无条件转移至<地址>

地址可以是常数、寄存器或者主存地址。

jump 指令还可以跳转到标号(可以自定义标号的名字)的位置(类似于 goto 语句):

image-20250311181319694

注意:汇编语言写代码时,一般会以函数名作为标号,标注该函数指令的起始地址。例如如果存在一个函数 test(),可以通过 jmp test 跳转到该函数指令的起始地址。

条件转移指令

image-20250311162416774

条件转移指令需要对 和 cmp 指令搭配使用,cmp 的底层原理是对两个数作减法,根据 PSW 程序状态字寄存器的结果来进行判断。

5.循环语句的机器级表示

用条件转移指令实现循环

image-20250311190656932

用 loop 指令实现循环

image-20250311191616258

looptop 指令默认指定对 ecx 寄存器进行自减,因此不能使用其他寄存器作为循环计数器。

此外还有 loopnz 和 loopz:

  • loopnz —— 当 ecx != 0 && ZF == 0 时继续循环,即不为零时循环。

  • loopz —— 当 ecx != 0 && ZF == 1 时继续循环,即结果为零时循环。

6.函数调用

(1)call 和 ret 指令

函数的栈帧(Stack Frame):保存函数大括号内定义的局部变量、保存函数调用相关的信息。

计算机每运行一个函数,就会把这个函数的栈帧压栈,当该函数执行结束后进行退栈。

在 x86 处理器中,程序计数器 PC(Program Counter) 通常被称为 IP(Instruction Pointer)

image-20250311193505839

(2)如何访问栈帧

image-20250312134201186

栈由高地址向低地址方向增长,ebp 寄存器指向当前栈帧底部(高地址)的四字节,esp 寄存器指向当前栈帧顶部(低地址)的四字节。

push 和 pop 指令

  • push A // 先让 esp 减 4,再将 A 压入

  • pop B // 栈顶元素出栈写入 A,再让 esp 加 4

A 可以是立即数、寄存器、主存地址;B可以是寄存器、主存地址。

mov 指令

mov 指令可以结合 esp、ebp 指针访问栈帧里的数据;还可以用 sub/add 修改栈顶指针 esp 的值,例如:

1
2
3
4
5
6
sub esp, 12         #栈顶指针减 12
mov [esp + 8], eax #将 eax 的值复制到主存 [esp + 8]
mov [esp + 4], 678 #将 678 复制到主存 [esp + 4]
mov eax, [ebp + 8] #将主存 [ebp + 8] 的值复制到 eax
mov [esp], eax #将 eax 的值复制到主存 [esp]
add esp, 8 #栈顶指针加 8

(3)如何切换栈帧

函数调用时,切换栈帧过程如下:

image-20250312154251631

每个函数开头都要例行执行 push ebpmov ebp, esp 两条指令,因此每个函数的栈帧底部都存储着上一层函数的栈帧基址。

这两行代码也可以用 enter 指令代替,该指令是零地址指令。

函数返回时,切换栈帧过程如下:

image-20250312155058010 image-20250312155126726

注意此处的 pop 命令会将上一层函数栈帧基址赋值给 ebp,并将 esp +4。

move esp, ebppop ebp 这两条指令也可以用 leave 指令代替,该指令是零地址指令。

实现 leave 后,会执行 ret 指令结束函数,该指令的作用是从函数的栈帧顶部找到 IP 旧址,将其出栈并恢复 IP 寄存器。

(4)如何传递参数和返回值

image-20250312175436272

  • gcc 编译器将每个栈帧大小设置为 16B 的整数倍(当前函数的栈帧除外)。

  • 通常将局部变量集中存储在栈帧底部区域。

  • 通常将调用参数集中存储在栈帧顶部区域。

  • 栈帧最底部一定是上一层栈帧基址(ebp 旧值)。

  • 栈帧最顶部一定是返回地址(当前函数的栈帧除外)。

注意:越先定义的局部变量越靠近栈顶,这里可能有些反直觉,例如图中三个变量定义的先后顺序分别是 temp1,temp2,sum。

image-20250312175139526

在调用新的函数前,会先把需要用到的参数压栈(call 前的 mov 操作):

image-20250312181743126

这里参数的压栈过程中,用 eax 寄存器作为中转把 [ebp - 8] 的值赋给 [esp + 4],因为 mov 指令不支持两个操作数全为主存。

而当函数执行完成后,返回值会保存在 eax 寄存器中,上一层函数从 eax 寄存器中读取结果(故返回值只能有一个)。

发生函数调用时,如果调用了使用了部分寄存器,被调用者如果也使用了这些寄存器,可能会导致数据丢失。因此可以在调用前,把相关数据也压入栈(先压栈相关寄存器数据,后压栈调用参数)。

五、CISC 和 RISC

复杂指令集(CISC,Complex Instruction Set Computer) 的设计思路是一条指令完成一个复杂的基本功能,代表有 x86 架构,主要用于笔记本、台式本等。

  • 指令系统复杂度大,指令数目一般为 200 条以上。

  • 指令的长度不固定,指令格式多,寻址方式多。

  • 可以访存的指令不受限制。

  • 各种指令使用频率相差很大。

  • 各种指令执行时间相差很大,大多数指令需多个时钟周期才能完成。

  • 控制器大多数采用微程序控制,有些指令非常复杂,以至于无法采用硬连接控制。

  • 难以用优化编译生成高效的目标代码程序。

精简指令集(CISC,Reduced Instruction Set) 的设计思路是一条指令完成一个基本动作,多条指令组合完成一个复杂的基本功能,代表有 ARM 架构,主要用于手机、平板等。

  • 选取使用频率最高的一些简单指令,复杂指令的功能由简单指令的组合来实现。

  • 指令长度固定,指令格式种类少,寻址方式种类少。

  • 只有 LOAD/STORE(存数/取数)指令访存,其余指令的操作都在寄存器之间进行。

  • CPU 中通用寄存器的数量相当多。

  • 一定采用指令流水线技术,大部分指令在一个时钟周期内完成。

  • 以硬布线控制为主,不用或少用微程序控制。

  • 特别重视编译优化工作,以减少程序执行时间。

注意:从指令系统兼容性看,CISC 大多数能实现软件兼容,即高档机包含了低档机的全部指令,并可以加以扩充。但 RISC 简化了指令系统,指令条数少,格式也不同于老机器,因此大多数 RISC 机不能与老机器兼容。

CISC 和 RISC 的对比

对比项目 CISC RISC
指令系统 复杂,庞大 简单,精简
指令数目 一般大于 200 条 一般小于 100 条
指令字长 不固定 定长
可访存指令 不加限制 只有 LOAD/STORE 指令
各种指令执行时间 相差较大 绝大多数在一个周期内完成
各种指令使用频率 相差很大 都比较常用
通用寄存器数量 较少
目标代码 难以用优化编译生成高效的目标代码程序 采用优化的编译程序,生成代码较为高效
控制方式 绝大多数为微程序控制 绝大多数为组合逻辑控制
指令流水线 可以通过一定方式实现 必须实现