CPU 的大致工作原理

CPU整体工作内容分为三大块:

  1. 读取指令
  2. 解码
  3. 执行(计算,读写内存,设置寄存器,跳转)

CPU 与设备通过电线连接,这些电线在计算机中称之为总线。
计算机中有专门链接 CPU 和其他芯片的导线,称为总线,逻辑上划分为:

  1. 地址总线:代表 CPU 的寻址能力,通过地址总线寻找内存
  2. 数据总线:代表 CPU 能够一次性读取数据的能力
  3. 控制总线:用于传输控制信号

指令的格式

指令由操作码操作数两部分组成
操作码说明计算机要执行哪种操作,如赋值、计算、跳转等,它是指令中不可缺少的组成部分。一般用一个唯一的助记符表示,对应着机器指令的一个二进制编码。
操作数是指令执行的参与者,即各种操作的对象,有些指令不需要操作数,通常的指令都有一个或两个操作数,也有个别指令有3个甚至4个操作数
操作数的类型有三种,分别是:

  1. 寄存器
  2. 内存地址(存储器)
  3. 数字

寄存器的概览及作用

CPU 内部有一些存储空间,可以存储一些代码运行时的临时消息,他们被称之为寄存器

数据寄存器

8086的16位通用寄存器是:
AX BX CX DX
SI DI BP SP
其中前4个数据寄存器还可以分成高8位和低8位两个独立的寄存器,8086的8位通用寄存器是:
AH BH CH DH
AL BL CL CL
对其中某8位的操作,并不影响另外对应8位的数据

变址寄存器

  • 变址寄存器通常用于存储器寻址时提供地址
    • SI 是源变址寄存器(Source)
    • DI 是目的变址寄存器(Destination)
  • 串操作类指令中,SI 和 DI 具有特别的功能

指针寄存器

  • 指针寄存器用于寻址内存堆栈内的数据
  • SP 为堆栈指针寄存器,指示栈顶的偏移地址
  • SP 不能再用于其他目的,具有专用目的
  • BP 为基址指针寄存器,表示数据在堆栈段中的基地址
  • SP 和 BP 寄存器与 SS 段寄存器联合使用以确定堆栈段中的存储单元地址

  • 栈(stack)是主存中一个特殊的区域,本质上不属于寄存器

  • 它采用先进后出 FILO(First In Last Out)或后进先出 LIFO (Last In Firdt Out)的原则进行存取操作,而不是随机存取操作方式
  • 堆栈通常由处理器自动维持。在8086中,由堆栈段寄存器 SS 和堆栈指针寄存器 SP 共同指示

指令指针寄存器

  • 指令指针寄存器 IP,它与代码段寄存器 CS 联用,永远存储着即将执行的指令的地址,每执行完一条指令,CPU 都会根据 CS :IP 找到下一条指令,同时 IP 被赋值为再下一条指令的地址。
  • 计算机通过 CS :IP 寄存器来控制指令序列的执行流程
  • IP 寄存器是一个专用寄存器,不能被直接赋值修改

标志寄存器

  • 标志(Flag)用于反映指令执行结果或控制指令执行形式
  • 8086处理器的各种标志形成了一个16位的标志寄存器 FLAGS

标志寄存器 flag 是一个16位的寄存器,其每一个位单独使用,就像16个指示灯一样,描述着指令的执行情况,亦或者控制着某些指令的执行。其每一位的情况如下:

15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
OF DF IF TF SF ZF AF PF CF
溢出 方向 中断 陷阱 符号 辅助 奇偶 进位
Debug
标志位状态

/
OV
/
NV
DN
/
UP
EI
/
DI
NG/
PL
ZR
/
NZ
AC
/
NA
PE
/
PO
CY
/
NC
  • 标志状态——用来记录程序运行结果的状态信息,许多指令的执行都将相应地设置它

CF ZF SF PF OF AF

  • 控制标志——可由程序根据需要用的指令设置,用于控制处理器执行指令的方式

DF IF TF
其中比较重要的是 CF OF ZF SF,需要重点记忆。

进位标志 CF

  • 进位标志 CF(Carry Flag),当运算结果的最高有效位有进位(加法)或借位(减法)时,进位标志置1,即 CF = 1;否则 CF = 0
  • 例子:
  • 3A + 7C = B6, 没有进位:CF = 0
  • AA + 7C = (1)26, 有进位:CF = 1

溢出标志 OF

  • 溢出标志OF(Overflow Flag),若算术运算的结果有溢出,则 OF = 1,否则 OF = 0
  • 什么是溢出:16位的范围是(+32767~-32768),如果运算结果超出这个范围。就产生了溢出,有溢出,说明有符号数的运算结果不正确
  • 例子:

3A + 7C = B6,产生溢出:OF = 1
AA + 7C = (1)26,没有溢出:OF = 0

  • 溢出和进位

溢出标志 OF 和进位标志 CF 是两个意义不同的标志。进位是针对无符号数而言,溢出是针对有符号数而言。在汇编指令运算时,大多数不区分有符号数还是无符号数,都先按照无符号去计算结果,然后根据无符号,有符号运算,设置相应标记位而已。

  • 例如:FF + 2 = (1)01
  • 按照无符号数来说,它进位了,会设置 CF 标记位
  • 按照有符号数来说,它没有溢出,所以不会设置 OF 标记位
  • 根据现象记住溢出与进位:
    • 按照有符号数运算,同号相加才会产生溢出,异号相加,不会溢出
    • 两个数相加,超过 FF,就会进位
    • 做减法时,前面的数比后面的数小,会借位。
  • 进位和溢出没有逻辑上的联系。

    零标志 ZF

  • 零标志 ZF(Zero Flag),若运算结果为0,则 ZF = 1;否则 ZF = 0(注意:ZF 为1表示的结果是0)

  • 例子:

    • 3A + 7C = B6,结果不是零:ZF = 0
    • 84 + 7C = (1)00,结果是零:ZF = 1

      符号标志

  • 符号标志 SF(Sign Flag),运算结果最高位为1,则 SF = 1;否则 SF = 0(有符号数据用最高有效位表示数据的符号,所以最高有效位就是符号标志的状态)

  • 例子:

    • 3A + 7C = B6,最高位D7 = 1:SF = 1
    • 84 + 7C = (1)00,最高位D7 = 0:SF = 0

      奇偶标志

  • 奇偶标志 PF(Parity Flag),当运算结果最低8位中“1”的个数为零或偶数时,PF = 1,否则 PF = 0(PF标志仅反应最低8位中“1”的个数是偶或奇,即使是进行16位字操作)

  • 例子:

    • 3A + 7C = B6 = 10110110B
    • 结果中有5个1,是奇数:PF = 0

      辅助进位标志

  • 辅助进位标志 AF(Auxiliary Carry Flag),运算时D3位(低半字节有进位或借位时,AF = 1,否则 AF = 0(这个标志主要由处理器内部使用,用于十进制算数运算调整指令中,用户一般不必关心)

  • 例子:

    • 3AH + 7CH = B6H,D3 有进位:AF = 1

      方向标志

  • 方向标志 DF(Direction Flag),用于串操作指令中,控制地址的变化方向:

    • 设置 DF = 0,存储器地址自动增加;
    • 设置 DF = 1,存储器地址自动减少。
  • 例如:
  • CLD 指令用于复位方向标志,执行后 DF = 0
  • STD 指令用于置位方向标志,执行后 DF = 1

    陷阱标志

  • 陷阱标志 TF(Trap Flag),用于控制处理器进入单步操作方式:

    • 设置 TF = 0,处理器正常工作;
    • 设置 TF = 1,处理器单步执行指令。
  • 单步执行指令: 处理器在每条指令执行结束时,便产生一个编号为1的内部中断,这种内部中断称为单步中断,所以 TF 也称为单步标志,利用单步中可对程序进行逐条指令的调试,这种逐条指令调试程序的方法就是单步调试

内存地址操作数

当操作数是一个内存地址的时候,如下表示:
MOV [65h], 12h
地址操作数,需要在地址外包裹上[],此条指令代表要往地址所在的内存中写入数据。
8086CPU 有20条地址线,最大可寻址空间为2的20次方 = 1MB,物理地址范围从00000H~FFFFFH。

物理地址和逻辑地址

每个物理存储单元都有一个唯一的20位编号,就是物理地址,从00000H~FFFFFH。那8086-CPU 的地址总线是20位,但是操作数最大只有16位。那么如何产生20位地址呢?
8086-CPU 采用了分段机制解决这个问题。
分段后,用户在编程时,采用逻辑地址,形式为:
段基地址 : 段内偏移地址
将逻辑地址中的段地址左移4位(相当于乘以16),加上偏移地址就得到20位物理地址,一个物理地址可以有多个逻辑地址,例如:
逻辑地址 1460:100、1380:F00
物理地址 14700 14700
物理地址的计算方法:
物理地址 = 段基地址 × 16D + 偏移地址 (段基地址是16位的)

段寄存器

使用内存通常提供的都是一个段内偏移地址。段寄存器中存储段基地址。不同类型的段内偏移和不同的段寄存器默认关联。

  • CS(代码段)指明代码段的起始位置
  • SS(堆栈段)指明堆栈段的起始位置
  • DS(数据段)指明数据段的起始位置
  • ES(附加段)指明附加段的起始位置 | 访问存储器的方式 | 默认 | 偏移地址 | 例子 | | —- | —- | —- | —- | | 取指令 | CS | IP | mov ax, [ip] | | 堆栈操作 | SS | SP | mov ax, [sp] | | 一般数据访问 | DS | 有效地址EA | mov ax, [15h]
    mov ax, [bx] | | BP 基址的寻址方式 | SS | 有效地址EA | mov ax, [bp+6h] | | 串操作指令的源操作数 | DS | SI | mov ax, [si] | | 串操作指令的目的操作数 | ES | DI | mov ax, [di] |

段超越

  • 修改默认使用的段寄存器称为-段超越
  • 没有段超越的指令实例:

MOV AX, [2000H] ; AX←DS:[2000H]; 从默认的 DS 数据段取出数据

  • 采用段超越前缀的指令实例:

MOV AX, ES : [2000H] ; AX←ES:[2000H]; 从指定的 ES 附加段取出数据

理解分段的误区

初次接触分段机制的时候,会有一个认识上的误区,认为内存被划分成了一个一个的段,每一个段有一个段基址和范围。
其实:8086-CPU 共有 20 位地址总线,能够表示的地址空间是 1MB,这块空间在物理上是连续的。为了方便管理,我们在逻辑上将其划分成多个段。
逻辑段与另一个逻辑段是可以覆盖或者重合的。
可以通过一些总结,更好的理解分段:

  • 8086对逻辑段要求:
    • 段地址低4位均为0
    • 每段最大不超过 64KB
  • 8086对逻辑段不要求:
    • 必须是 64KB
    • 各段之间完全分开(即可以重叠)
  • 1MB 空间最多能分成多少个段?
    每隔 16 个存储单元就可以开始一个段,
    所以 1MB 最多可以有:2的20次方 ÷ 16 = 2的16次方 = 64K个段
  • 1MB 空间最少能分成多少个段?
    每隔 64K 个存储单元开始一个段,
    所以 1MB 最少可以有:2的20次方 ÷ 2的16次方 = 16个段

    内存操作数的多种书写方式

    内存地址作为指令的操作数的时候,可以有多种书写形式,如下表示:
操作数形式 解释
mov ax, [0x1230] 从 ds : 1230 取出两个字节数据,存入 ax 中
mov ax, word ptr [0x1230] 从 ds : 1230 取出两个字节数据,存入 ax 中
mov al, [0x1230] 从 ds : 1230 取出一个字节数据,存入 al 中
mov al, byte ptr [0x1230] 从 ds : 1230 取出一个字节数据,存入 al 中
mov ax, ES : [0x1230] 从 es : 1230 取出两个字节数据,存入 ax 中
mov ax, word ptr CS : [0x1230] 从 cs : 1230 取出两个字节数据,存入 ax 中
mov ax, [bx] [si] 等价于 mov ax, [bx + si]
mov ax, 12[si] 等价于 mov ax, [si + 12]

x86 的分段机制

在 CPU 进化到 386 时,操作系统普遍采用的是 32 位下的保护模式,分段机制发生了很大的变化。暂且可以认为 32 位程序中,所有的段基地址都是 0,段内偏移范围是 0~4GB 。这也被称之为平坦模式

总结

  1. 汇编指令逐条被 CPU 直接执行
  2. cs :ip 指明了当前 CPU 将要执行的指令位置,执行完一条指令,ip 自动存储下一条指令的地址
  3. 一条汇编指令由操作码和操作数组成
  4. 操作数可以是寄存器,也可以是内存中的数据,也可以是数字
  5. 内存是分段管理的,当内存作为操作数的时候,寻址方式是:段 × 16D + 段内偏移
  6. 执行完指令,除了得到运算结果,还会在标志寄存器中保存相应的状态
  7. 指令的执行反过来还会受标志寄存器状态的影响