【汇编语言】02-寄存器
一个典型的 CPU 由运算器、控制器、寄存器等器件组成,这些器件靠内部总线相连。前一篇博客说的总线,相对于 CPU 内部来说是外部总线。内部总线实现 CPU 内部各个器件之间的联系,外部总线实现 CPU 和主板上其他器件的联系。
简而言之,在 CPU 中:
-
- 运算器进行信息处理;
- 寄存器进行信息存储;
- 控制器控制各种器件工作;
- 内部总线连接 CPU 内部的各种器件,在其间进行数据传送。
对于汇编程序员而言,CPU 中的主要部件是寄存器。程序员可以用指令读写寄存器,并通过改变各种寄存器中的内容来实现对 CPU 的控制。
不同的 CPU 中,寄存器的个数、结构都不尽相同。8086 CPU 有 14 个寄存器,分别是:AX、BX、CX、DX、SI、DI、SP、BP、IP、CS、SS、DS、ES、PSW。我们先列举在这里,后面逐渐详细说明。
1. 通用寄存器
8086 CPU 的所有寄存器都是 16 位的,可以存放两个字节。AX、BX、CX、DX 这 4 个寄存器通常用来存放一般性数据,称之为通用寄存器。
我们以 AX 为例,来看看寄存器的逻辑结构:
16 位寄存器的逻辑结构
一个 16 位的寄存器可以存放 16 位的数据,其最大值能达到 216 - 1。数据在寄存器中存放情况示例如下图:
16 位数据在寄存器中的存放情况示例
8086 CPU 的前代产品,使用的都是 8 位寄存器。为了保证兼容,使得基于上一代 CPU 编写的程序稍加修改就能在 8086 上运行,8086 的 AX、BX、CX、DX 这 4 个通用寄存器,都可以分成两个独立的 8 位寄存器使用。如下图所示:
16 位寄存器拆分为两个 8 位寄存器示意
-
- AX 可以分为 AH 和 AL;
- BX 可以分为 BH 和 BL;
- CX 可以分为 CH 和 CL;
- DX 可以分为 DH 和 DL。
以 AX 为例,来分析 8086 的 16 位寄存器分为两个 8 位寄存器的情况。AX 的低 8 位构成 AL 寄存器,高 8 位构成 AH 寄存器,AH 和 AL 是可以独立使用的寄存器。8 位寄存器能存储的数据最大值是 28 - 1。如下图所示:
16 位寄存器及所分成的两个 8 位寄存器的数据存储情况
2. 字在寄存器中的存储
处于兼容性考虑,8086 CPU 可以一次性处理两种尺寸的数据:
-
- 字节(byte):一个字节由 8 个 bit 组成,可以存在 8 位寄存器中;
- 字(word):一个字由两个字节组成,分别称为高位字节和低位字节。一个字可以存放在一个 16 位寄存器中,高低字节自然也就是放在高 8 位寄存器和低 8 位寄存器中。
字的组成示意
3. 几条汇编指令
我们先给出以下几条指令,注意汇编指令和寄存器名称不区分大小写:
汇编指令 | 控制 CPU 完成的操作 | 高级语言的语法描述 |
mov ax,18 | 将18送入寄存器AX | AX=18 |
mov ah,78 | 将78送入寄存器AH | AH=78 |
add ax,8 | 将寄存器AX中的数值加上8 | AX=AX+8 |
mov ax,bx | 将寄存器BX中的数据送入寄存器AX | AX=BX |
add ax,bx | 将AX和BX中的数值相加,结果存在寄存器AX中 | AX=AX+BX |
接下来,我们假设 AX 和 BX 中的初始数据都是 0000H,看看执行指令后,寄存器中的数据改变。
程序段中的指令 | 指令执行后AX中的数据 | 指令执行后BX中的数据 |
mov ax,4E20H | 4E20H | 0000H |
add ax,1406H | 6226H | 0000H |
mov bx,2000H | 6226H | 2000H |
add ax,bx | 8226H | 2000H |
mov bx,ax | 8226H | 8226H |
add ax,bx | 044CH | 8226H |
执行完上述表格的最后一条指令后,ax + bx 的结果实际为 1044CH。但 ax 是 16 位寄存器,只能存放 4 位十六进制的数据,所以最高位的 1 被舍弃,结果为 044CH。
我们再看一组指令(仍然假设 AX 和 BX 中的初始数据都是 0000H):
程序段中的指令 | 指令执行后AX中的数据 | 指令执行后BX中的数据 |
mov ax,001AH | 001AH | 0000H |
mov bx,0026H | 001AH | 0026H |
add al,bl | 0040H | 0026H |
add ah,bl | 2640H | 0026H |
add bh,al | 2640H | 4026H |
mov ah,0 | 0040H | 4026H |
add al,85H | 00C5H | 4026H |
add al,93H | 0058H | 4026H |
程序最后一条指令执行后,运算结果为 158H,但 al 是作为一个独立的 8 位寄存器来使用的,和 ah 没有关系,只能存放两位十六进制的数据。所以最高位的 1 丢失,ax 中的数据为 0058H。要注意,这里说的丢失是指进位制不能在 8 位寄存器中保存,但 CPU 并不是真的丢弃这个进位值。这一点后续会详细讨论。
在进行数据传送或运算时,指令的两个操作对象的位数应当是一致的。以下这几种指令是错误用法,务必注意:
1 mov ax,bl 2 mov bh,ax 3 mov al,2000 4 add al,100H
4. 物理地址
CPU 访问内存单元时,要给出内存单元的地址。所有的内存单元构成的存储空间是一个一维线性空间,每个内存单元在这个空间中都有唯一的地址,即物理地址。
CPU 通过地址总线送入存储器的,必须是一个内存单元的物理地址。CPU 向地址总线上发出物理地址之前,必须先在内部形成这个物理地址。不同的 CPU 有不同的物理地址形成方式。
5. 16 位结构的 CPU
前文提到,8086 是 8 位机,也可以说,8086 是 16 位结构的 CPU。
简而言之,16 位结构(也表达为 16 位机、字长为 16 位)描述了一个 CPU 具有以下几方面的特性:
-
- 运算器一次最多可以处理 16 位的数据;
- 寄存器的最大宽度为 16 位;
- 寄存器和运算器之间的通路为 16 位。
8806 内部能够一次性处理、传输、暂时存储的信息的最大长度是 16 位的。
6. 8086 CPU 给出物理地址的方法
8086 CPU 有 20 位地址总线,寻址能力达到 1MB。但 8086 CPU 是 16 位结构,如果不经处理,只能发出 16 位地址。因此,8086 CPU 在内部合成两个 16 位地址来形成 20 位物理地址。
8086 CPU 相关部件逻辑结构示意
如上图所示,当 8086 CPU 要读写内存时:
-
- CPU 中的相关部件提供两个 16 位地址——段地址和偏移地址;
- 将段地址和偏移地址通过内部总线送入地址加法器;
- 地址加法器将两个 16 位地址合成为一个 20 位的物理地址;
- 地址加法器通过内部总线将 20 位物理地址送入输入输出控制电路;
- 输入输出控制电路将 20 位物理地址送上地址总线;
- 20 位物理地址被地址总线传送到存储器。
地址加法器采用如下公式,将段地址和偏移地址合成物理地址:
物理地址 = 基础地址(段地址 X 16) + 偏移地址
为了更好地理解地址加法器的工作原理,再附上一张示意图:
地址加法器工作原理示意
所谓“段地址 X 16”,在计算机学科中有个更常用的描述——左移 4 位。这是因为计算机中的所有信息都是以二进制的形式存储的,从二进制的角度来理解,左移一位,数值就会 X 2。
7. 段地址 X 16 + 偏移地址 = 物理地址的本质
本质含义是:CPU 在访问内存时,用一个基础地址(段地址 X 16)和一个相对于基础地质的偏移地址相加,给出内存单元的物理地址。
这种寻址功能是“基础地址+偏移地址=物理地址”这种寻址模式的一种具体实现方案。
8. 段的概念
“段地址”这种说法可能会使人误认为内存被划分成了一个一个的段,每一个段有一个段地址。我们不应这样来理解。
内存并没有分段,段的划分来自 CPU。8086 CPU 这种“基础地址 + 偏移地址 = 物理地址”的方式使得我们能用分段的方式来管理内存。如下图所示,我们可以认为:地址 10000H~100FFH 的内存单元组成一个段,该段的起始地址是 10000H,段地址是 1000H,大小为 100H。我们也可以认为地址 10000H~1007FH、10080H~100FFH 的内存单元组成两个段,基础地址分别为 10000H、10080H,段地址为 1000H、1008H,大小均为 80H。
内存分段示意
在编程时可以根据需要,将若干地址连续的内存单元看作一个段,用段地址 X 16 定位段的起始地址(基础地址),用偏移地址定位段中的内存单元。因为段的起始地址是段地址 X 16,所以段的起始地址一定是 16 的倍数。偏移地址为 16 位,寻址能力是 64KB,所以一个段的长度最大为 64KB。
CPU 可以用不同的段地址和偏移地址形成同一个物理地址。譬如 CPU 要访问 21F60H 单元,则其给出的段地址 SA 和偏移地址 EA 满足 SA X 16 + EA = 21F60H 即可。在 8086 CPU 中,存储单元的地址用两个元素来描述,即段地址和偏移地址。但我们通常不会说“数据在 21F60H 内存单元中”,而是用以下两种说法来表示:
-
- 数据存在内存 2000:1F60 单元中;
- 数据存在内存的 2000H 段中的 1F60H 单元中。
9、 段寄存器
8086 CPU 在访问内存时要由相关部件提供内存的段地址和偏移地址,送入地址加法器合成物理地址。段地址在 8086 CPU 的段寄存器中存放,8086 CPU 有四个段寄存器:CS、DS、SS、ES。当 8086 CPU 要访问内存时,由这四个段寄存器提供内存单元的段地址。
10. CS 和 IP
CS 和 IP 是 8086 CPU 中最关键的两个寄存器,它们指示了 CPU 当前要读取指令的地址。CS 为代码段寄存器,IP 为指令指针寄存器。在任意时刻,设 CS 中的内容为 M,IP 中的内容为 N,则 8086 CPU 将从内存 MX16+N 单元开始,读取一条指令并执行。也可以这样表示:8086 机中,任意时刻,CPU 将 CS:IP 指向的内容当作指令执行。
8086 PC 读取和执行指令的相关部件
上图展示了 8086 CPU 读取、执行指令的工作原理,说明如下:
-
- 8086 CPU 当前状态:CS 中的内容为 2000H,IP 中的内容为 0000H;
- 内存 20000H~20009H 单元存放着可执行的机器码;
- 内存 20000H~20009H 单元中存放的机器码对应的汇编指令如下:
- 此处需补图
通过以上图解,可知 8086 CPU 工作过程简述如下:
-
- 从 CS:IP 指向的内存单元读取指令,读取的指令进入指令缓冲器;
- IP=IP+所读取指令的长度,从而指向下一条指令;
- 执行指令。
- 转到步骤 1,重复以上过程。
在 8086 CPU 上电启动或复位后,CS 和 IP 被设置为 CS=FFFFH,IP=0000H,即 CPU 从内存 FFFF0H 单元中读取指令执行,FFFF0H 单元中的指令是 8086 CPU 开机后执行的第一条指令。
前文提到,在内存中,指令和数据没有任何区别,都是二进制信息。CPU 在工作时把有些信息看作指令,有些信息看作数据。那么 CPU 根据什么将内存中的信息看作指令?CPU 将 CS:IP 指向的内存单元中的内容看作指令。换句话说,如果内存中的一段信息曾被 CPU 执行过的话,它所在的内存单元必然被 CS:IP 指向过。
11. 修改 CS、IP 的指令
在 CPU 中,程序员能用指令读写的部件只有寄存器,程序员通过改变寄存器中的内容实现对 CPU 的控制。CPU 从何处执行指令是由 CS、IP 中的内容决定的,程序员可以通过改变 CS、IP 中的内容来控制 CPU 执行目标指令。
前文提到,8086 CPU 中大多数寄存器的值都可以用 mov 来改变,mov 指令也被称为传送指令。但 mov 指令不能用于设置 CS、IP 的值,8086 CPU 为 CS、IP 提供了另外的指令来改变它们的值,此类指令统称为转移指令。
若想同时修改 CS、IP 的内容,可用形如”jmp 段地址:偏移地址“的指令完成。例如:
1 jmp 2AE3:3 # 执行后 CS=2AE3H,IP=0003H,CPU将从2AE33H处读取指令 2 jmp 3:0B16 # 执行后 CS=0003H,IP=0B16H,CPU将从00B46H处读取指令
若想仅修改 IP 的内容,可用形如”jmp 某一合法寄存器“的指令来完成。例如:
1 jmp ax # 将ax寄存器中的值送入IP
12. 代码段
前面讲到,对于 8086 PC 机,可以根据需要,将一组内存单元定义为一个段。我们可以将长度为 N(N ≤ 64)的一组代码,存在一组地址连续、起始地址为 16 的倍数的内存单元中。因此,我们可以认为这段内存是用来存放代码的,从而定义了一个代码段。
譬如我们有一段长度为 10 字节的指令,存放在 123B0H~123B9H 的一组内存单元中。我们可以认为 123B0H~123B9H 这段内存就是用来存放代码的,是一个代码段。其段地址为 123BH,长度为 10 字节。
将一段内存当作代码段,只是我们编程时的一种安排。CPU 只认被 CS:IP 指向的内存单元中的内容为指令,并不会自动执行我们定义在代码段中的指令。要让 CPU 执行我们放在代码段中的指令,必须要将 CS:IP 指向代码段中的第一条指令的首地址。