功能和结构
CPU
组成结构
在介绍 CPU 的组成结构之前,首先回顾一下计算机的 冯诺依曼结构,其中运算器、控制器、存储器、输入、输出为计算机组成的五大部件,其中 CPU 涵盖的功能就包含运算器和控制器这两个。
CPU 的核心功能包含 运算和控制 这两大模块,这对应了计算机五大部件中的运算器和控制器。具体而言:运算功能(执行功能)是指 CPU 根据指令对数据进行算术或逻辑运算;控制功能则是指 CPU 通过指令协调计算机中各个部件的工作。
CPU 的结构简图如上图所示,CPU 由运算部件和控制部件组成。但是其实这两者并不能完全涵盖 CPU 的所有结构,因为这种分类方式忽略了 CPU 了内总线,所以一种更完善的 CPU 组件分类方法是将其分为 数据通路 和 控制器 这两部分:
- 数据通路(Datapath)是指令执行过程中,数据所经过的路径,包括路径中的部件。它是指令的 执行部件。
- 控制器(Control Unit)对指令进行解码,生成对应的控制信号,控制数据通路的动作。能够执行指令发出控制信号,是指令的 控制部件。
数据通路
CPU 的数据通路是指 CPU 在执行指令的过程中内部数据流动的路径,ALU、寄存器组、多路选择器都属于数据通路的一部分。数据通路描述了信息从哪里开始,中间经过哪些部件,最后被传送到哪里。数据通路由 控制部件 控制,控制部件根据每条指令功能的不同,生成对数据通路的控制信号。
数据通路的元件主要分为 组合逻辑元件 和 时序逻辑元件 两类:
组合逻辑元件组合逻辑元件(操作元件)指那些仅由组合逻辑电路组成的元件,其输出仅取决于当前的输入,而不受存储器或时钟信号的影响。输入和输出之间无反馈通路,信号是单向传输的。
数据通路中常用的组合逻辑元件有算数逻辑单元(ALU)、译码器、多路选择器、三态门等,如下图所示:
- 译码器:用于操作码或地址码译码,$n$ 位输入对应 $2^n$ 种不同组合,因此有 $2^n$ 种不同的输出,但是每次只有一个输出被使能。
- 多路选择器:有多个输入,输出与其中一个输入相同,需要用控制信号 Select 选择哪个输入作为输出。
- 三态门:可以被视为一种控制开关,由控制信号 EN 决定信号线的通断,当 EN=1 时,三态门被打开,输入信号等于输出信号;当 EN = 0 时,输出端呈现高阻态,数据通路被断开。
对于时序逻辑元件(状态元件),其任何时刻的输出不仅与该时刻的输入有关,还与该时刻以前的输入有关,并且 具备状态存储 功能。此外,时序电路必须在时钟节拍下工作。
各类寄存器和存储器,如通用寄存器组、程序计数器、状态/移位/暂存/锁存寄存器等,都属于时序逻辑元件。
控制器
控制器是计算机的指挥中心,它负责协调和控制计算机的所有操作。 控制器的功能和组成详见 该节。
8086 CPU
上文已经对 CPU 中的组件进行了分类,但是仅有抽象概念是不足够的,该节会提供一个非常简单的 CPU 实现来说明情况,这个 CPU 就是 8086。
8086 是 Intel 首款真正的 16 位处理器,也是现代 x86 处理器的开端,只要掌握了这个 CPU 的架构,就能搞定绝大多数考试过程中会遇到的问题。
结构
上文已经提到了 CPU 的组成结构 可以分为数据通路和控制器这两部分,8086 中的数据通路由指令队列、寄存器 和内总线组成。控制器中包含指令译码器、时序控制单元、标志控制逻辑和微指令控制逻辑。
下文会按照这个脉络对 8086 中的组件进行详细说明。
寄存器
从功能的角度进行区分,8086 CPU 的寄存器可以为分为 通用寄存器、段寄存器、指针寄存器、附加寄存器和标志寄存器 这五类:
- 通用寄存器:用于存储任意的地址或者数据。
AX
寄存器:累加器(Accumulator),用于执行算术和逻辑运算。BX
寄存器:基址寄存器(Base Register),通常用于存储内存地址。CX
寄存器:计数寄存器(Counter Register),用于循环计数和移位操作。DX
寄存器:数据寄存器(Data Register),用于输入/输出操作和大整数运算。
- 段寄存器:用于存储内存段的起始地址。
CS (Code Segment)
寄存器:代码段寄存器,存储指向代码段的地址。DS (Data Segment)
寄存器:数据段寄存器,存储指向数据段的地址。ES (Extra Segment)
寄存器:附加数据段寄存器,通常用于数据访问。SS (Stack Segment)
寄存器:堆栈段寄存器,存储指向堆栈段的地址。
- 变址寄存器:用于支持变址寻址模式,和数组和指针操作相关。
SI
寄存器:源变址寄存器,通常用于数据传送操作。DI
寄存器:目的变址寄存器,也通常用于数据传送操作。
- 指针寄存器:
SP (Stack Pointer)
寄存器:堆栈指针寄存器,指向函数栈的顶部。BP (Base Pointer)
寄存器:堆栈基址寄存器,指向函数栈的底部。IP
寄存器:指令指针寄存器,存储当前执行指令的偏移地址。
- 标志寄存器:
FLAGS
寄存器:标志寄存器,存储有关条件和状态的信息,例如进位、零标志、溢出等。
- 特殊寄存器
IR (Instruction Register)
寄存器:暂存读取的指令。T
暂存器:暂存 ALU 一端的输入。
哪些寄存器是汇编程序员可见的?
有在 8086 仿真器上编写过汇编程序的人会知道,可见的寄存器就是你编写汇编程序时可以直接通过指令进行控制的寄存器。
可见的寄存器包括:通用寄存器、段寄存器、标志寄存器(Flags)和指令指针寄存器(IP)。
当然更为方便地是记住哪些寄存器是不可见的:MAR、MDR、IR。
段寄存器
段寄存器指向程序调用时的 内存结构中不同段的起始位置:
- CS 指向代码段(.text)的起始位置
- SS 指向栈段(User Stack)的起始位置
- DS 指向数据段(.data)的起始位置
- ES 指向附加段(Extra Segment)的起始位置
对于运行在 8086 上的程序而言,其内存结构相比 现代进程内存空间 更加简单,但其中的逻辑结构是类似的:
从逻辑上来说,程序的执行需要四个段:
- 代码段:存储编译后程序指令的地方
- 数据段:存储有全局数据的地方
- 栈段:函数嵌套调用的发生场所
- 额外段:提供一些灵活性,供程序员发挥
不同的段寄存器与不同的段相关联,指向相关段的起始地址。
指针寄存器
对于有些段来说,仅仅知道其起始地址是不足够的,在程序执行过程中,需要一些额外的寄存器来实现我们需要的操作,指针寄存器主要操作 栈段 和 代码段。
栈指针寄存器栈指针寄存器包含 BP、SP 这两个,在进一步了解这两个寄存器之前,请复习一下 函数调用时的内存结构。
函数在执行过程中需要保存的数据与栈类似,具有先进后出的特点。
函数栈从高地址向低地址增长,嵌套调用的函数所对应的函数栈 在栈段上不断堆叠。如果我们在 main()
主函数中嵌套调用 f(g(h(1)))
,那么该程序对应的栈段对应的逻辑结构如下所示:
----------------- ← SS
main 的函数栈
-----------------
f 的函数栈
-----------------
g 的函数栈
----------------- ← BP (指向函数 h 的栈底)
上一个函数的 BP
函数参数
局部变量 ← SP(指向函数 h 的栈顶)
其中 BP 指向最后一个调用的函数栈的底部,SP 指向最后一个调用的函数栈的顶部,通过 BP 和 SP 我们保存了最后一个函数栈帧的栈底(开始位置)和栈顶(结束位置)。
指令指针寄存器在 8086 中,指令指针寄存器指的就是 IP(Instruction Pointer)寄存器。
指令在代码段(.text 段)中是从低地址向高地址增长的,这种增长方式也符合 程序计数器(PC 或 IP)的增长逻辑:在 指令执行的取指阶段,控制单元在完成取指后会控制 IP = IP + 指令长度,这样 IP 就指向了 下一条待执行指令的地址。
-----------------
代码段高地址
-----------------
后续指令 ← 取指后 IP 指向该位置
-----------------
当前指令 ← IP 当前位置
-----------------
前一指令
-----------------
代码段低地址
变址寄存器
变址寄存器主要用于实现变址寻址模式,方便对数组、字符串等数据结构中的 第 i 个元素 进行操作。8086 中的主要变址寄存器是 SI(源变址寄存器)和 DI(目的变址寄存器),下文分别从 数组和字符串操作 两个例子说明一下:
数组操作
以下汇编代码段通过一个循环操作实现了对于数组中的前五个元素进行操作。注意在 8086 中,我们是在 CX 中保存 loop 的轮数,每次 LOOP 被调用后 CX 的值自动被减一。
; 假设 DS 指向数据段并且数组从数据段开始
MOV DS, addr ; 将数组的起始地址保存到 DS 中
MOV CX, 5 ; CX 用作循环计数器,假设数组有 5 个元素
MOV SI, 0 ; SI 作为索引寄存器,初始化为 0
NEXT_ELEMENT:
MOV AX, [SI] ; 从数组当前元素读取到 AX
; 对 AX 中的数据进行处理
ADD SI, 2 ; 移动到下一个元素(假设每个元素 2 字节)
LOOP NEXT_ELEMENT ; 循环直到 CX 为 0
字符串操作
以下汇编代码将一个字符串的前 length 个字符复制到另一个字符串中。
; 假设 DS 和 ES 已经分别指向源和目的数据段
MOV SI, OFFSET source ; SI 指向源字符串的起始位置
MOV DI, OFFSET dest ; DI 指向目的字符串的起始位置
CLD ; 清除方向标志位,确保字符串操作从低地址到高地址
MOV CX, length ; CX 初始化为字符串的长度
REP MOVSB ; 复制 CX 个字节从 DS:SI 到 ES:DI
标志寄存器
标志寄存器(Flags)用于存储处理器在 执行指令过程中产生的各种状态和条件,这些状态可以进一步被 控制器 所用,控制后续指令执行的行为。
标志寄存器中的不同位用于标记某个特殊的状态,8086 中的标志寄存器是一个 16 位的寄存器,其中 9 个标志位被使用,其他 7 个标志位没有含义。
这些标志位在逻辑上可以被分为 条件标志(conditional flags)和 控制标志(control flags)两种:
条件标志条件标志用于标记指令执行后的结果状态,用于影响程序的控制流,条件标志包含以下几种:
- OF (Overflow flag):溢出标志。
- 当有符号整数运算的结果太大而无法适应目标寄存器时,OF 标志会设置为 1,表示发生了溢出。
- SF (Sign flag):符号标志。
- 根据操作结果的符号位来设置,如果结果为负数,则 SF 被设置为 1,否则为 0。
- ZF (Zero flag):零标志。
- 当操作结果为零时,ZF 标志被设置为 1,否则为 0。
- AF (Auxiliary carry flag):辅助进位标志。
- 通常用于 BCD(二进制编码十进制)算术运算,指示低四位的进位。
- PF (Parity flag):奇偶校验标志。
- 根据结果中二进制位 1 的个数是奇数还是偶数,设置 PF 标志。奇数个 1 则 PF 为 1,偶数个 1 则 PF 为 0。
- CF (Carry flag):进位标志。
- 当无符号整数运算的结果超出了目标寄存器的位数,CF 标志被设置为 1,表示发生了进位。
条件标志不需要 程序员 手动通过指令设置,当算数和逻辑指令(Add 和 AND 等)和 比较指令(CMP)被执行时,相关的标志位会被自动设置。
OF 标志位是如何被设置的?
对于两个 n 位数字的计算指令,只需要判断最高位是否有进位即可,如果有进位,OF 被设置为 1,否则被设置 0。
这是一种可行的方法,但在考试中常考察的是另一种方式,假设我们进行 $A$ 和 $B$ 的计算加法计算,两个数字都包含 $n$ 位,那么其计算结果可能包含以下几种情况:
- $A$ 和 $B$ 中一个为负数,一个为正数,那么 $A+B$ 一定不会溢出。
- $A$ 和 $B$ 中任意一个为 0,那么 $A+B$ 一定不会溢出。
- $A$ 和 $B$ 都是正数
- $A+B$ 的结果为负数,加法溢出
- $A+B$ 的结果为正数,加法无溢出
- $A$ 和 $B$ 都是负数
- $A+B$ 的结果为正数,加法溢出
- $A+B$ 的结果为负数,加法无溢出
令 $C=A+B$,则可以通过如下公式计算 OF:
$$\text{OF} = A_{n-1} \cdot B_{n-1} \cdot \overline{C_{n-1}} + \overline{A_{n-1}} \cdot \overline{B_{n-1}} \cdot C_{n-1}$$
对于减法指令,可以将其转化为等价的加法指令,进行同样逻辑的判断。
条件标志通常用于控制 条件跳转指令 的执行。条件跳转指令会检查条件寄存器中的标志位,根据标志位的状态决定是否将程序计数器(PC)修改为目标地址,从而实现程序流程的跳转。
控制标志控制标志并不会被通常的运算指令自动修改,而是通过专门的指令来进行设置或清除。对于以下字段,了解即可:
- IF (Interrupt flag)
- 控制中断处理
- 当 IF 被设置为 1 时,CPU 允许中断请求。如果 IF 为 0,CPU 将禁止所有中断请求,无论是外部硬件中断还是软件中断。
- TF (Trap flag)
- 控制单步执行。
- 当 TF 被设置为 1 时,CPU 将进入单步执行模式。在单步执行模式下,每执行一条指令后,CPU 将引发一个单步中断,允许程序员逐条调试程序。
- DF (Direction flag)
- 字符串操作的标志位。
- 当 DF 被设置为 1 时,字符串操作(如 MOVS、LODS、STOS)在内存中向高地址方向移动。这通常用于从高地址向低地址扫描字符串。当 DF 被清除为 0 时,字符串操作在内存中向低地址方向移动。这通常用于从低地址向高地址扫描字符串。
特殊寄存器
暂存器每次 ALU 操作都需要两个操作数,但是数据总线是 共享的,同一时刻只能传输一个操作数。所以在 ALU 会按照以下方式进行计算:
- 先读取第一个操作数,暂存在临时寄存器 T 中;
- 然后再读取第二个操作数;
- 最后,ALU 从 T 和第二个操作数一起执行运算。
所以在 只有一个内部数据总线 的设计里,为了配合 ALU 的两个输入,暂存器被设置用于存储前一个时钟周期的数据。
指令寄存器当 CPU 从内存或指令队列中取出一条指令时,这条指令会被送入 IR(指令寄存器),暂存在这里,供后续的译码和执行阶段使用。
控制单元(CU)可以对 IR 中的指令进行译码,生成一系列控制信号。