在上一章我们主要讲解了CPU的外部通讯,即CPU是如何通过总线、逻辑存储器与外部设备的物理存储器进行数据、指令传递的。本章我们主要讲解CPU(8086CPU)的内部通讯及其工作原理。
CPU概述
一个典型的CPU,是由运算器、控制器、寄存器等器件组成的,这些器件是通过CPU内部总线相连的。CPU内部总线和外部总线有什么区别?CPU内部总线实现了CPU内部各个器件之间的联结;CPU外部总线实现了CPU与主板及其上所有其它设备器件的联结。
寄存器概述
对CPU来讲,它也有存储临时数据的需求,寄存器就是用于CPU内部数据存储的器件。8086CPU有14个寄存器,其中通用的寄存器有8个。
什么是通用寄存器?
8086CPU所有的寄存器都是16位的,即可以存放两个字节。其中 AX、BX、CX、DX 用来存放一般性数据,故被称为通用寄存器。下面以 AX 寄存器为例,来看一下寄存器的逻辑结构。
一个16位的寄存器可以存储一个16位的数据,即两个字节。比如十进制数字18(二进制表示为 10010),在AX寄存器中的存储方式如下图示:
再比如十进制数字20000(二进制表示为 0100111000100000)在AX寄存器中的存储方式如下图示:
那么一个16位的寄存器所能存储数据的最大值是多少呢?答案是 2的16次方再减1。那么一个8位的寄存器所能存储数据的最大值又是多少呢?答案是 2的8次方再减1。
关于CPU的兼容性(寄存器的兼容性设计)
在8086上一代的CPU中寄存器都是8位的,为了保证向上兼容,8086CPU把上述四个16位的通用寄存器分别划分成了两个独立的8位寄存器来使用。于是,在8086CPU中,AX 寄存器被分成了 AH 和 AL,BX 寄存器被分成了 BH 和 BL,CX 寄存器被分成了 CH 和 CL,DX 寄存器被分成了 DH 和 DL (其中 H 表示高位寄存器,L 表示低位寄存器)。做了兼容设计和处理之后,8086CPU通用寄存器的逻辑结构如下图示(以 AX 寄存器为例):
当要兼容处理8086上一代的CPU时,只用使用低位寄存器,高位寄存器都填入0即可。以 AX 寄存器为例,只使用 AL 低位寄存器,AH 高位寄存器都写入0。
AX寄存器的低8位构成了AL寄存器,AX寄存器的高8位构成了AH寄存器。AH和AL都是可以独立使用的8位寄存器。示例如下:
上图示,当把AX看成一个独立的寄存器,它存储的数据是十进制数字2000;当把AX兼容划分成两个独立寄存器时,AH寄存器存储的数据是十进制数字78,AL寄存器存储的数据是十进制数字32。
关于十六进制
我们知道一个存储单元可以存放8位数据,那么寄存器中可以存放 8N 位数据。于是说,计算机中的数据大多都是由 8N 位数据来构成的。比如十六进制的数字,可以直观地看出它是由几个8位数据构成的,因为对十六进制数字来讲,每两位数就对应一个 8 位(即2的3次方)。所以,我们通常习惯使用十六进制的数字,这样能更加直观和方便地看出它在内存中占据多少位的空间,这便是十六进制数字比二进制、十进制数字更大的优势。
88DDH
// 直观地可以看出,这个数字占据两个字节空间,即16位数据。H,表示十六进制。
字 在寄存器中的存储
一个字可以存储在一个16位的寄存器中,这个字的高位字节(高8位)和低位字节(低8位)分别存储在这个寄存器的高位寄存器和低位寄存器中。一个字所占空间的大小就是2字节(16位)。
汇编指令初体验 汇编指令是不区分大小写的!!
汇编例题1:
汇编例题1 题解,经过最后一步运算 ax = ax + bx,得到的结果是 1044CH,但由于 AX 寄存器只有16位,因此最终 AX 寄存器中的存储结果是 044CH。
汇编例题2:
汇编例题2 题解,经最后一步运算 al = al + 93H,即 C5H + 93H = 158H,但是 AL 寄存器是从 AX 寄存器中独立出来的低位寄存器,AH 寄存器中只能是 00H,且 AL 只有8位,因此 AX 寄存器中最终的存储结果是 0058H。(如果最后一步的指令改成 add ax, 93H,则 AX 中的最终结果就是 0158H)
上述两个例题中,我们都遇到了寄存器位数小于存储结果数据的位数问题,看上去是丢失了运算结果的最高进制位。但事实上,CPU并不会真正地丢失这个运算结果的最高进制位,而是使用了其它寄存器来存储这个看上去被丢失了最高进制位,这个知识点将在后续课程中继续探讨。
什么是物理地址?
所有的存储单元构成了一个线性的存储空间。CPU访问这些存储单元时,要给出存储单元的唯一地址,我们把这个唯一的地址称为物理地址。
16位结构的CPU有哪些特征?
概括地讲,16位结构描述了CPU具有以下几个方面的特征:1)CPU中的运算器一次最多可以处理16位的数据;2)CPU中寄存器的最大宽度是16位;3)CPU中寄存器和运算器之间的通路是16位的。
地址加法器
8086CPU的外部地址总线宽度是20,则8086CPU外部寻址能力约1M。但是,8086CPU的内部是16位结构,则8086CPU内部只能表现出约64K的寻址能力。那么问题来了,8086CPU是如何用内部16位的数据转换成外部20位的物理地址呢?
8086CPU采用一种在CPU内部使用两个16位地址合成的方法来形成一个20位的物理地址,从而实现了CPU内外物理地址位数相等。
解决方案如下图示:
8086CPU在读写CPU外部存储器中数据时,到底发生了哪些事?
1)8086CPU首先提供两个16位的地址,一个被称为段地址,另一个被称为偏移地址。
2)然后通过CPU内部总线,把段地址和偏移地址发送至地址加法器。
3)地址加法器,将这两个16位地址合成为一个20位的物理地址,并发送给输入输出控制电路。
4)输入输出电路把这个20位的物理地址发送至CPU外部的地址总线。
5)通过CPU外部地址总线,将这个20位的物理地址传递至指定的存储器。
地址加法器,是怎样对这两个16位地址进行合成运算的呢?
合成公式: 20位的物理地址 = 16位的段地址 * 16 + 16位的偏移地址。
16位的段地址 * 16,本质上就是把这个16位的二进制数向左移动4位,即得到一个20位的二进制数。原理是,一个二进制数向左移动1位,相当于该二进制数乘以2;一个二进制数向左移动 N 位,相当于这个二进制数乘以 2 的 N 次方;所以,一个二进制数乘以16,即向左移动4位。如下图示:
深入理解“段”与“段地址”的概念
事实上,存储器并没有分段,段的划分来自于CPU,由于8086CPU使用“段地址*16 + 偏移地址 = 20位物理地址”来计算出存储单元的物理地址,使得我们误认为存储器是分段来管理的,事实上存储器并没有被分段。“段”只是CPU在合成20位物理地址时的一个概念而已。另需注意,完全相等的物理地址,可以来自于不同的段地址和偏移地址,示例如下:
现实中,在编程时我们可以根据实际需要,把若干连续地址的存储单元看作成一个“段”,用“段地址16”来定位这个“段”的起始地址(基础地址),用“偏移地址”来定位这个“段”中的某个存储单元。基于这样的理论,我们可以进一步得到如下两个结论:
1)由于“段地址16”是16的倍数,所以一个“段”的起始地址也一定是16的倍数。
2)由于“偏移地址”是16位的,16位的偏移地址的寻址能力是64K,所以一个“段”的长度最大为64K。
关于 物理地址 的小结
1)CPU访问存储单元时,必须向存储器提供存储单元的物理地址。8086CPU是通过地址加法器来生成最终的物理地址。
2)根据实际工作需要,我们可以把连接地址、起始地址是16倍数的一组存储单元定义为一个“段”。
3)在8086CPU机器中,存储单元的物理地址由段地址和偏移地址共同决定。比如 21F60H 这一物理地址,我们可以将其理解为“物理地址为 2000H : 1F60H 的存储单元”,还可以理解成“2000H 段中的 1F60H 存储单元”。
什么是段寄存器?什么是 CS:IP 寄存器?
在CPU中,用于存储“段地址”的寄存器,就叫做段寄存器。段寄存器,就是用来提供“段地址”的。8086CPU中,有4个段寄存器,分别是 CS、DS、SS、ES。当8086CPU要访问存储器时,就是由这4个段寄存器来提供存储单元的段地址的。
CS 和 IP 是 8086CPU中最为关键的两个寄存器,它们指示着CPU当前将要读取的指令所在的地址。CS 是代码段寄存器,IP是指令指针寄存器。
那么CPU是如何读取和执行程序指令的呢?CPU的工作流程是怎样的呢?
1)第一步,从 CS:IP 指向的存储单元读取指令,读取的指令进入到指令缓冲器。
2)第二步,更新 IP 指令指针寄存器的值,IP = IP + 所读取指令的长度,从而让 IP 指针指向下一条指令。
3)第三步,执行指令缓冲器中的指令。然后转至第一步,如此循环这个过程。
8086CPU工作过程的简要描述:在8086CPU通电启动或者复位后(即CPU刚开始工作时),CS 代码段寄存器 和 IP 指令指针寄存器会被设置为 CS = FFFFH, IP = 0000H。也就是说,当8086CPU机器刚启动时,CPU从内存的 FFFF0H 这个物理地址处开始读取指令并执行。FFFF0H 这一物理地址处的指令是 8086CPU机器开机时执行的第一条指令。
在任何时候,CPU将CS 代码段寄存器 和 IP 指令指针寄存器中的数据当作成指令的段地址和偏移地址,并使用它们合成为指令的物理地址,进而从存储器中读取该条指令代码并执行。
如果说,存储器中的一条信息曾被CPU执行过的话,那么该条信息所在的存储单元必定被 CS:IP 指向过。
程序员如何才能控制 CPU 的运行?如何修改 CS:IP 寄存器中的数据内容?
在CPU中,程序员唯一能够用指令执行读写操作的部件只有寄存器。程序员可以通过操作和修改寄存器中的数据内容,进而实现对CPU的控制。
CPU从何处执行指令,是由 CS:IP 寄存器中的数据内容来决定的。程序员可以通过修改 CS:IP 寄存器中的数据内容来控制CPU执行目标任务。那我们该如何修改 CS:IP 寄存器中的数据内容呢?
使用“jmp 段地址 : 偏移地址”命令,可以同时修改 CS:IP 寄存器中的数据内容,从而实现对CPU的控制,以读取存储器中的目标指令。其中,jmp 命令中“段地址”用于修改 CS 代码段寄存器中的内容,jmp 命令中“偏移地址”用于修改 IP 指令指针寄存器中的内容。示例如下:
jmp 2AE3:3
// 修改 CS:IP = 2AE3:3
// 读取 2AE33H 这一物理地址的存储单元中的内容
jmp 3:0B16
// 修改 CS:IP = 3:0B16
// 读取 00B46H 这一物理地址的存储单元中的内容
注意,不能用 mov 指令来修改 CS、IP 寄存器的值,8086CPU没有提供这样的功能。
如何只修改 IP 指令指针寄存器中的数据内容呢?
使用“jmp 寄存器X”命令,可以只修改 IP 指令指针寄存器中的数据内容。该指令的意思是,把 IP 指令指针寄存器中的值修改成 寄存器X 中的值。
jmp ax // 等价于 mov ip, ax,即把 ax 中的值赋值给 ip 寄存器
jmp bx // 等价于 mov ip, bx,即把 bx 中的值赋值给 ip 寄存器
汇编例题3:
汇编例题3 解析:重点在于理解 mov 命令和 jmp 命令的功能。mov 命令用于修改某个通用寄存器的值,而 jmp 命令用于修改 CS:IP 或者是 仅修改 IP 寄存器的值。并且要知道,当 CS:IP 发生变化,CPU 将要读取并执行的指令所在物理地址就变成了 CS*16 + IP 这一物理地址。所以该例题的最终答案如下:
mov ax, 6622H
jmp 1000H:3H // 把 cs 代码段寄存器的值修改成 1000H,同时把 ip 指令指针寄存器的值修改成 3H
mov ax, 0000H
mov bx, ax
jmp bx // 把 bx 寄存器中的值赋值给 ip 指令指针寄存器
mov ax, 0123H
... 循环 第3行代码 至 第6行代码 ... 这一个死循环
什么是代码段?
对8086CPU机器,在编程时,我们可以根据需要,将一组连续的存储单元定义为一个“段”。将长度为N(N 不大于 64KB)的一组代码存放到一组地址连接且起始地址为16倍数的存储单元中,那么这一组用于存放代码的“段”,就被称为是“代码段”。
如何才能让存储器中的代码段被CPU执行呢?
将一段存储空间当作代码段,这仅仅是我们在编程时的一种安排。CPU并不会由于这种安排就自动地执行我们的代码。那么到底是什么才能决定代码的执行呢?答案是 CS:IP 寄存器,CPU只会读取并执行CS:IP所指向的存储单元中的代码指令。即让 CS:IP 指向代码段中第一行代码所在的存储单元即可。
CS寄存器存放着指令的段地址,IP寄存器存放着指令的偏移地址。在8086CPU机器中,在任意时刻,CPU都会读取 CS:IP 指向的内容并将其当作指令来执行。jmp 转移指令,可以修改 CS:IP 或者仅修改 IP 寄存器中的内容,从而控制CPU对程序代码的执行顺序。
实 验 实验目标,用机器指令和汇编指令编程,查看和操作CPU、内存。
Debug是DOS(Disk Operating System,磁盘操作系统)、windows提供的实模式(8086方式)程序的调试工具。使用它,可以查看CPU各种寄存器中的内容、内存的情况和在机器码级跟踪程序的运行。在 DOS 的 debug 环境,所有数字都默认是 16进制的。
DOSBox官网下载
DosBox的安装、debug.exe 配置
r // 查看寄存器的内容
r ax // 修改 ax 通用寄存器的内容
r cs // 修改 cs 代码段寄存器的内容
r ip // 修改 ip 指令指针寄存器的内容
d // 查看内存中的内容
d 138c:0100 // 查看某段内存中的内容
u // 把内存中的机器码转化成对应的汇编指令
u 138c:0100 // 把指定内存段的机器码转化成对应的汇编指令
t // 执行一条机器指令
a // 以汇编指令的格式,向内存中写入一条机器指令
e // 改写内存中的内容
a 2000:0 // 从地址2000:0000处,向内存中写入指令
mov ax, 1 // 把 ax 寄存器更改为 1
add ax, ax // 让 ax = ax + ax
jmp 3 // 修改 ip 寄存器为 3
r cs // 修改 cs 段寄存器为 2000
2000
r ip // 修改ip 指令指针寄存器为 0
0
u 2000:0 // 查看 2000:0 处的内存中指令代码
t // 读取执行当前 cs:ip 指向的存储单元中的指令
本章完!!!