⚠ 转载请注明出处:作者:ZobinHuang,更新日期:July 19 2021
本作品由 ZobinHuang 采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可,在进行使用或分享前请查看权限要求。若发现侵权行为,会采取法律手段维护作者正当合法权益,谢谢配合。
目录
有特定需要的内容直接跳转到相关章节查看即可。
Section 1. 计算机部件:介绍了基本的计算机部件原理
1.1 CPU:介绍了汇编程序员看到的处理器上的基本资源;
1.2 RAM:介绍了内存的访问原理;
1.3 BIOS ROM:介绍了 BIOS ROM 的功能作用;
1.4 外设:HDD 机械硬盘:介绍了 HDD 的结构和编址;
1.5 外设:SSD 固态硬盘:TODO
Section 2. 器件互联:介绍了 CPU 与其它计算机部件的互联;
Section 3. 地址空间:介绍了 CPU 是如何给其它互联资源编址的:
3.1 Memory-mapped I/O:介绍了基于内存映射的编址方式;
3.2 Port-mapped I/O:介绍了基于端口映射的编址方式;
Section 4. 本系列博客所使用的 CPU 模型:介绍了本系列博客在实模式部分使用的 Intel 8086 CPU 的资源情况
1. 计算机部件
当我们分析汇编语言的时候,我们实际上做的事情就是分析指令和数据在计算机中的流动。因此,我们必须对计算机底层的组成原理有一个感性的认识。在本小节,我们对底层硬件进行简单的描述,以满足我们分析汇编程序。
首先我们先约定:当我们提到 指令 的时候,我们指的是指导 CPU 进行特定算术/逻辑操作的机器码;当我们提到 数据 的时候,我们指的是用于特定算术/逻辑操作的算数。
1.1 CPU
Firstly, 是我们的主角 CPU。关于微处理器内部的结构,我们在 数字逻辑设计 & 计算机架构 中有过详细的介绍,这里我们可以简单理解,CPU 中可以分为两个部分:计算单元 和 寄存器组。我们所编写的汇编指令在被编译器编译为机器码之后,就能被计算单元所执行。在不同架构的 CPU 中,它们的计算单元认识的指令集是不同的,因此我们必须基于我们的 CPU 架构来编写和分析汇编代码,并且使用相应平台下的编译器将汇编代码转化为机器码,这样才能够使 CPU 正确地运行起来。指令执行的结果,可以被存放在 CPU 中的各个寄存器中。CPU 中的各个寄存器有各种各样的作用,我们在后面的文章将会重点分析它们。
1.2 RAM
其次是 RAM,也就是内存。内存是一种 按字节寻址 的存储器,我们把一个字节的内存空间称为一个 内存单元。从汇编程序员的角度出发简单地理解,CPU 为了读写内存单元,它会把要操作的内存单元的地址送上 地址总线,通过在 控制总线 上发送对应的控制信号,就能够通过 数据总线 来收发数据。内存是一种读写速率很快的存储器。因此在计算机系统中,我们都是使用内存来存储指令和数据,CPU 通过读取内存中存储的内容来运行。
1.3 BIOS ROM
在系统主板上,有一块 ROM 芯片,存放着一套程序,这套程序有一个令人熟知的名字:BIOS (Basic Input/Output System),这套程序包括了:
- 硬件系统的检测和初始化程序
- 外部中断和内部中断的中断例程
- 用于对硬件设备进行 I/O 操作的中断例程
- 其它和硬件系统相关的中断例程
在以 Intel 8086 为处理器的系统中,ROM 占据着整个内存空间顶端的 64KB,物理地址范围是 0xF0000~0xFFFFF。8086 加电或者复位时,CS=0xFFFF,IP=0x0000,所以,它取的第一条指令位于物理地址 0xFFFF0,如果你不熟悉这样的计算,没关系,我们在下一篇文章就会讲解 CPU 计算内存地址的方法,你只要明白的是,CPU 上电后读取的第一条指令,正好位于 ROM 中,那里固化了开机时需要执行的指令。0xFFFF0 处存储的是一条转移指令,如果你不熟悉转移指令,它会通过改变 CS 和 IP 的内容,使处理器从 ROM 中的较低地址处开始取指令执行,这些指令将进行硬件的诊断、检测和初始化等工作。在完成使命后,ROM-BIOS 将读取硬盘主引导扇区(MBR)的内容,将 MBR 加载到内存地址 0x0000:0x7c00 处(也就是物理地址 0x07C00)。MBR 的功能是继续从硬盘的其他部分读取更多的内容加以执行,因此它的内容通常和具体的操作系统有关系。我们下面马上就会介绍 MBR。
1.4 外设:HDD 机械硬盘
如上图所示,磁盘实际上由多个 磁盘面 (platter) 组成。在每个 磁盘面中由从里到外的若干条 磁道 (track) 组成。根据硬盘的规格不同,磁道数可以从几百到成千上万不等。最外圈的磁道是0号磁道,向圆心增长依次为1磁道, 2磁道, ...。磁盘的数据存放就是从最外圈开始的。每个磁道可以存储数 Kb 的数据。在每条 磁道 中,又可以分为若干个 扇区 (sector)。现在每个扇区可存储 512 Bytes 数据已经成了业界的约定。我们又把位于不同 磁盘面 的具有相同编号 (i.e. 位置相同) 的 磁道 称为 柱面 (cylinder)。
需要注意的是,磁盘读写数据是按 柱面 进行的,磁头读写数据时首先在同一柱面内从 0 磁头开始进行操作,依次向下在同一柱面的不同盘面(即磁头上)进行操作,只有在同一 柱面 所有的磁头全部读写完毕后磁头才转移到下一 柱面。因为选取磁头只需通过磁盘控制器电子切换即可,而选取柱面则必须通过机械切换。数据的读写是按柱面进行的,而不是按盘面进行,所以把数据存到同一个柱面是很有价值的。
由于使用 "磁盘面+磁道+扇区" 的定位方法十分麻烦,因此业界推出了 LBA(Logic Block Address) 的磁盘扇区编址方式:把磁盘上所有的扇区从 0 开始编号。最早的逻辑扇区编址方法是 LBA28,使用 28 个比特来表示逻辑扇区号,从逻辑扇区 0x0000000 到 0xFFFFFFF,共可以表示 `2^28` =268435456 个扇区。每个扇区有 512 字节,所以 LBA28 可以管理 128 GB 的硬盘。 硬盘技术发展得非常快,最新的硬盘已经达到几百个吉字节的容量,LBA28 已经落后了。在这 种情况下,业界又共同推出了 LBA48,采用 48 个比特来表示逻辑扇区号。如此一来,就可以管理 131072 TB 的硬盘容量了。
在硬盘的众多扇区中,有一个特别的扇区,它位于 0 面 0 道 1 扇区,被称为 主引导扇区 (MBR),里面存储了一些指令。当 CPU 开始运行主引导扇区中的指令的时候,CPU 就能够从磁盘中读取出操作系统的代码,所以它相当于起到了一个引导的作用。我们在上面分析 BIOS ROM 的时候已经有过简单的讲解。
我们在后面会描述 CPU 如何与机械硬盘进行交互。
1.5 外设:SSD 固态硬盘
TODO
2. 器件互联
基于上面第一章对计算器部件的阐述,我们在本节讨论这些部件在计算机系统上是如何互联的。
上图所示的是传统的计算机部件互联架构。首先,在主板上会有两个重要的部件:北桥(North Bridge) 和 南桥(South Bridge),统称为 芯片组(Chipset)。北桥主要用于连接 CPU 与 内存,PCIe 设备等高速器件之间的通信,并且也会通过内部总线与南桥通信;南桥主要用于负责与各种较慢速 I/O 接口 (e.g. USB, IDE/STATA) 之间进行通信。对于 CPU 来说,与它直接相连接的是北桥,也就是说,对于 CPU 发起的对其它部件的访问,最终都会由北桥来进行处理。
我们现在还没有讨论 CPU 是如何访问各种互联资源的,我们把这一部分放在下一个小节进行分析。
3. 地址空间
在本小节中我们将讨论 CPU 是如何访问系统资源的,包括了自身寄存器、内存、ROM、以及磁盘等 I/O 设备。
CPU 访问互联设备资源,本质上是在访问分布在各个地方的存储器,包括系统内存,系统 BIOS ROM,I/O 设备芯片寄存器/板上 RAM 等等。因此,在访问之前,系统需要为这些可以被访问的存储器进行统一编址,这样,CPU 才能够访问到正确的位置。我们按照CPU 对 I/O 设备上可访问资源编址方式的不同,将编址方式分为:Memory-mapped I/O (MMIO) 和 Port-mapped I/O (PMIO)。下面进行介绍。
3.1 Memory-mapped I/O
在 MMIO 下,如上图所示,系统内存,BIOS ROM,I/O 设备寄存器/RAM 都被映射到了一个统一的内存地址空间上。当 CPU 发起对某一个地址的访问的时候,由于该地址在整个地址空间内唯一确定了一个存储器单元的位置,它就能够在整个互联系统中找到这个存储器进行读写。这个存储器可能在系统内存中,也可能在 BIOS ROM 中,也可能在 I/O 设备上。因此,对于 MMIO 的寻址方式,CPU 只需要使用一套指令,就能够发起对互联系统所有存储器单元的访问,这与我们在下面要看到的 PMIO 的方式有所不同。
MMIO 是现在大部分互联系统采用的内存编址方式。
3.2 Port-mapped I/O
在早期的 Intel CPU 中,由于只有 20 条地址线,只能支持 `2^20` = 1M Bytes 的系统地址空间,系统内存本来就是紧缺的资源,如果还使用 MMIO 的方式,只会使系统内存部分的地址空间被严重耗尽。因此早期的 Intel CPU 使用了 PMIO 的方式。在 PMIO 方式下,I/O 设备上的寄存器被称作端口,CPU 将这些端口独立出来,在系统内存空间之外独立编址,如上图所示。这样一来,系统内存空间又变为在当时看来比较充裕的 1M Bytes。在这样的设计下,CPU 访问系统内存和访问 I/O 设备存储器使用的就是不同的指令,因为只有区分了指令,CPU 在运行指令的时候才能区分要向哪个地址空间发起读写访问。
在如今的 64 位系统下,我们的地址线早已变成了 64 根,理论上来说能支持 `2^64` = 16384 PB(PebiByte)。我们的地址空间已经变得和太阳一样取之不尽用之不竭了,因此 PMIO 的设计也就慢慢被淘汰了。
然而,在汇编语言的学习中,由于我们在很多书中看到的是使用 16-bits 的 Intel 8086 CPU 作为运行汇编语言的处理器,因此在讲述操作 I/O 设备的时候仍然使用的是 PMIO 的方式。只能说,这是为了让初学者更好地理解 CPU 访问外围设备的一种工作方式,因为它较为简单。读者在学习的时候在心里面清楚我们实际中更多使用的是 MMIO 的方式就可以了。
4. 本系列博客所使用的 CPU 模型
在实模式部分的系列博客中,我们与市面上大部分教材一样,选用 Intel 8086 CPU 作为我们分析的对象。Intel 8086 CPU 是一个 16-bits 的 CPU,这代表了它内部的寄存器都是 16-bits 的,计算单元的单次计算能支持的最大位数也是 16 bits。按理来说它的寻址空间应该也是 16-bits,但是为了支持 `2^20` = 1M Bytes 的内存空间,它有 20 条地址线,我们在后面会分析它是如何生成地址总线上的 20-bits 地址的。对于 I/O 设备的访问,它使用的是 PMIO 的方式。
另外,由于我们在后面的程序中会经常使用 8086 CPU 在屏幕上输出内容,作为我们验证我们程序运行结果的判断依据,因此在这里我们也介绍一些 8086 CPU 输出内容的原理。8086 CPU 将内存的 0xB8000~ 0xBFFFF 共 32KB 的空间作为显存。这 32KB 可以被分为 8 页,CPU 默认显示第 1 页的内容,也即显示低 4KB 的页中的内容。在这 4KB 中,是一个 80(Width)x25(Height) 的彩色字符输出区域,每个字符占用 2 个字节,1 个字节用于描述显示内容,1 个字节用于描述字符的属性 (颜色,背景颜色等)。也就是说这 4KB = 80 x 25 x 2 Bytes,其中低地址(偶数字节)存放字符的 ASCII 码,高地址(奇数字节)存放字符的属性。
同时,Intel 的 字节序 是低端字节序(小端),即低字节在低地址端,高字节在高地址端,如此递归。比如:
1
2
3;NASM Assemly
mov dword [0x00], 0x7c0001ff
mov dword [0x04], 0x00409800
上面这两条指令用于向内存中连续的 8 个单元中写入数据,一次写入一个 "双字"(double word),即 2 字节。写入后,低双字将在低地址端,高双字将在高地址端;低字将在低地址端,高字将在高地址端;低字节将在低地址端,高字节将在高地址端,最终形成:
| 地址 | 内容 |
|---|---|
| +0x07 | 00 |
| +0x06 | 40 |
| +0x05 | 98 |
| +0x04 | 00 |
| +0x03 | 7c |
| +0x02 | 00 |
| +0x01 | 01 |
| +0x00 | ff |
