译自:http://www.brokenthorn.com/Resources/OSDev7.html
第7 章:系统结构
by Mike, 2008
本系列文章旨在向您展示并说明如何从头开发一个操作系统。
欢迎!在之前的一章里,我们总算完成了引导加载器的工作!到目前为止:
我们详细的了解了FAT12 文件系统,并且了解了价值,解析,执行stage 2 的方法。
这章里会继续前面的工作。首先我们会仔细的看看x86 体系结构。这很重要,对于理解保护模式如何工作尤其重要。
我们会介绍计算机工作的每件事儿,我们要深入到比特一层。为了理解BIOS 在启动过程中是如何胜任工作的,你得记住你也可以启动其他的处理器。BIOS 仅仅处理注处理器,我们也可以做同样的事儿来支持多处理器。
包括以下内容:
就某些方面来说,这更像是一个计算机体系结构的教程。然而我们是在以操作系统开发的角度来看计算机体系结构。当然我们会涉及体系结构的方方面面。
理解这些会使我们更了解保护模式,在下一章里,我们会包括切换到保护模式的所有细节。
享受乐趣吧!
我们都听过这个术语。保护模式 (PMode) 是在 80286 及之后处理器提供的一个操作模式。保护模式主要用于提高系统的稳定性。
从前面的章节里你知道,实模式有大问题。首先,我们能在任何想要的地方写数据,这会覆盖代码或数据,这些代码或数据可能是软件端口或是处理器或是我们自己的。并且要做到这样的事情,我们有超过4,000 种不同的方法——包括直接的和间接的。
实模式没有内存保护 。所有的数据和代码被放置在单个的,为通用目的而存在的内存块中。
实模式,限制使用16 位寄存器,1MB 内存。
不支持硬件级,内存保护 和多任务 。
最重要的问题是,不存在向“环”这样的东西。所有的程序都在环0 级执行,所有的程序都有系统的绝对控制权。这意味着,如果你不够小心,在一个单任务环境里的,一条指令( 如cli/hlt ) 会使你的整个操作系统崩溃。
所有的这些,在我们讨论实模式时都提到过,保护模式解决了所有的这些问题。
保护模式:
我们在上一章中的汇编语言的环一节里提到,我们工作在环 0, 而通常的程序工作在环 3 ( 一般情况 ). 我们能够使用特殊的指令访问特殊的寄存器,而一般的程序不行。在这章里,我们将会使用 LGDT 指令,来完成一个“远跳”使用我们自己定义的段,和处理器控制寄存器。这在普通的程序中是不行的。
了解系统结构并制定处理器如何工作,有利于我们的工作。
x86 系列计算机遵循冯诺依曼体系结构。典型的冯诺依曼体系结构的计算机有 3 部分组成:
例如:
有些事情有注意。如你所知, CPU 重内存中取得数据和指令。内存控制器的工作是确定特定的RAM 芯片和其中的存储单元,所以,CPU 与内存控制器交流。
另外,注意"I/O 设备" 。他们也连接在系统总线上。所有的I/O 端口是内存位置的映射,这使得我们能够使用IN 和OUT 指令。
硬件设备使用系统总线访问内存。也允许我们当一些事情发生时通知设备。比如,如果向控制硬件设备控制器要读的位置写一字节,处理器可以通知设备“有数据在数据总线上”,这是通过控制总线在操作。这是软件与硬件交互的基础,我们会在后面详细的解释它,因为这是在保护模式中唯一的域硬件交互的方法,很重要。
我们先分开来介绍,然后,把他们结合起来,并且通过一个指令在硬件层之下的过程了解他们在一起是如何工作的。下面我们将讨论I/O 端口以及软件与硬件之间是如何交互的。
如果你有x86 的汇编经验,一些甚至是大部分对你来说是很熟悉的。但我们要介绍那些在大部分汇编语言教材里没有深入的内容,特别是环 0 的程序。
系统总线也称作前部总线,它在主板上连接 CPU 和北桥。
系统总线是数据总线、地址总线和控制总线的集合。总线上的一条电线表示1 比特 。使用电压来表示0 和1 ,基于标准晶体管- 晶体管逻辑(TTL )。我们不需要详细了解。TTL 是数字逻辑的部分,是计算机构造的基础。
如你所知,系统总线由3 种总线构成,我们分别介绍如下。
数据总线是传输数据的一系列电线。数据总线的宽度有 16 线 / 比特 , 32 线 / 比特 , 或 64 线 / 比特 . 注意电线和比特信号的直接单元关系。
这表明:32 位处理器有32 位的数据总线 。一次数据传输可以处理4 字节,我们可以注意我们程序中的数据大小,以其提高运行速度。
怎么做呢?对于1,2,4,8, 或16 比特的数据,处理器会在数据总线上扩展‘0 ’。对于大段的数据会切分(并扩展),使发送到数据总线上的数据符合总线宽度。发送与总线宽度一致的数据会更快,因为不需要额外的工作。
比如,我们有一个64 位的数据,而有32 位总线宽度,在第一个时钟周期里,只有前32 位数据被发送到内存控制器,第二个时钟周期才发送后32 位数据。注意:数据类型越大,需要更多的时钟周期!
通常的 "32 位处理器", "16 位处理器" 等,代表数据线的宽度。所以"32 位处理器" 有32 位数据总线。
当处理器或 I/O 设备需要访问内存是,它会在地址总线上放一个地址。我们也知道内存地址代表 内存中的一个位置。这很抽象。
" 内存地址" 是一个内存控制器 使用的数。内存控制器从总线中得到这个数,将它解释为一个内存位置。知道了每个RAM 芯片的大小内存控制器可以简单的访问一个特定的芯片及它的内部偏移。 从内存单元0 开始内存控制器将它解释为我们需要的偏移地址。
地址总线通过控制单元 (CU )连接处理器与I/O 控制器 。控制单元在处理器里面,我们在后面介绍。I/O 控制器 控制着硬件设备的接口,我们在后面介绍。
就像数据总线一样,每根电线代表一个比特信号 ,因为1 比特只有两个不同的值,所以CPU 可以访2^n 个不同的地址。 因此,地址总线的条数/ 位数决定CPU 可访问的最大内存。
8080 到80186 处理器都有20 条/ 比特地址总线。 80286 和80386 有24 条/ 比特, 80386+ 有32 条/ 比特。
所有的x86 系列都向老处理器兼容。这也是为什么启动时处在实模式。处理器限制通过20 条地址线访问1MB 内存——line 0 到line 19 。
对我们来说很重要, 因为这样的限制对我们依旧存在!我们需要使20 号地址线有效,才能让我们的操作系统访问高达4GB 的内存。 后文有更详细的解说。
我们把数据放到数据总线,使用地址总线确定内存地址,我们怎么知道如何处理数据呢?是读数据还是写数据呢?
控制总线是一系列表示设备要进行的工作的电线(比特)。比如:处理器设置READ 或WRITE 位使内存控制器知道他要在地址总线上指定的内存位置读到数据总线或是写入数据总线的数据。
控制总线也允许处理器通知设备。使设备引起注意,比如:我们要设备从perhaps 地址总线指定的内存位置读数据该怎么办呢?这要让设备知道我们希望它做的事。这在I/O 软件端口很重要。
当然,要知道系统总线并不直接与硬件设备相连。相反的它们连接到一个中心控制器——I/O 控制器 ,用于切换,并给设备发信号。
上面是系统总线的全部内容。它是处理器 ( 通过控制单元 (CU) ) 和 I/O 设备 ( 通过 I/O 控制器 ) 访问内存的通路。访问内存需要通过内存控制器,它的任务是确定要访问的内存芯片及内存芯片上的哪个存储单元。
" 控制器" 这个词 你可能听过很多,我在后面详细解释。
内存控制器是系统总线与物理内存直接的主要接口。
我们前面见过控制器,不是吗?控制器到底是什么?
控制器提供基本的已经控制功能。它也提供基本的软硬件接口。 这很重要。记得吗,在保护模式里,我们不能使用任何中断。在引导加载器里我们使用一系列中断来与硬件交流。而在保护模式使用这些中断会导致三重错误,我们怎么办呢?
我们要与硬件直接通信。我们要通过控制器,(在我们介绍了I/O 系统后,我们会详细介绍控制器是怎么工作的)。
内存控制器为软件提供了一种读写内存位置的方法。内存控制器也有刷新 RAM 芯片的责任,以保证数据不丢失。
内存控制器有一个多路选择器(Multiplexer ) 和一个信号分离器(Demultiplexer ) 用于选择特定的RAM 芯片,并且定位地址总线确定的地址。
DDR 控制器用于刷新 DDR SDRAM, 使用系统时钟脉冲来读写内存。
双通道控制器用在 DRAM 设备上,它由两组小的总线,可以同时读写两个不同的内存位置,这有助于加快 RAM 的访问速度。
内存控制器从地址总线接收地址。这很好,但我们怎么告诉内存控制器是读还是写内存呢?还有数据从哪里来的呢?当我们读内存时。处理器设置在控制总线上的 Read 位;同样,在写内存时,处理器设置在控制总线上的 Write 位。
处理器使用控制总线控制设备。
内存控制器使用的数据在数据总线上,使用的地址在地址总线上。
当读内存时,处理器将要读的内存的决定地址放到地址总线上。然后处理器设置读控制线。
内存控制器获得控制权。控制器使用多路选择器 将绝对地址转换为物理RAM 位置,并把数据放到数据总线上,然后将READ 位清0 ,设置READY 位。
现在处理器知道了数据在数据总线上。它复制这个数据,并执行剩余的指令……比如把数据保存到BX 里。
写内存类似。
首先,处理器将内存地址放到地址总线上。将要写的数据放到数据总线上。然后,设置控制总线上的WRITE 位。
内存控制器知道了要往地址总线确定的内存地址写在数据总线上的数据。完成后,内存控制器清空WRITE 位,设置控制总线的READY 位。
我们不直接和内存交流,我们间接的做,无论读还是写内存,我们都使用内存控制器。内存控制器是软件与 RAM 芯片之间的接口。
下面我们看看I/O 系统,等等!1337 多路选择器是什么样?它是 内存控制器中的物理线路。要了解它的工作方式,我们得知道一点数字逻辑电路 的知识。对我们来说复杂了,如果你想知道更多,Google 一下!
I/O 系统简单的表示 I/O 端口 。这个系统提供了软件和硬件控制器之间的接口。
仔细看看。
端口简单的提供软件与硬件设备直接的接口,有两种类型的端口:软件端口和硬件端口。
硬件端口提供两个物理设备之间的接口。这样的接口通常使用“槽”来连接设备,包括,不限于:串口,并口, PS/2 口 , 1394, 火线, USB 口等。
这些端口通常在机箱的边上、前面或是后面。
如果你想看看这样的接口,顺着一根连到你的电脑上的线,你就找到了。
一般的电器上,端口上的针承载的信号在不同的设备上有不同的含义。这些针就像系统总线一样代表比特!每根针1 比特。
一般将硬件端口分为两类,“公”的和“母”的。“公”的端口的针是露出来的,“母”的与它相反。硬件端口通过控制器访问。 后文有更详细的解说。
这对我们相当重要。软件端口是个数,它表示一个(或一种)硬件控制权。
你可能知道有些数代表同一个控制器。原因呢?内存映射I/O 。基本的想法是通过一个特定的内存地址来与硬件交流。端口号就代表这些地址。 这表明地址可以代表特定设备的一根寄存器,或是控制寄存器。
以后会仔细介绍。
x86 结构里,处理器使用特殊的内存位置来表示特定的东西。
比如:地址 0xA000:0 表示显卡的VRAM 起始地址。 在这个位置写数据,你直接改变了显存的内容,也就改变了屏幕上显示的内容。
其他的内存地址代表其他的一些东西——比如软驱控制器 (FDC) 的某个寄存器。
了解哪个地址是什么,是很关键的,也很重要。
一般的 x86 实模式内存映射 :
注意:也可能会将上面的设备映射到完全不同的内存区域。 BIOS POST 程序来完成上面的设备映射工作。
好,很好,因为这些地址代表不同的东西,读写这些特殊的地址会得到,或改变计算机的不同部分的状态。
比如,还记得我们关于INT 0x19 的讨论吗?我们说在0x0040:0x0072 写0x1234 会跳转到0xFFFF:0 ,实现计算机的热重启(Windows ctrl+alt+del) 。段:偏移寻址方式的0x0040:0x0072 转换为绝对地址是0x000000472 ,这是BIOS 数据区的一部分。
另一个例子是文本输出,往0x000B8000 写几个字节,我们就直接改变了字符模式的显存。因为在现实的时候不断刷新,这就改变了显示在屏幕上的字符,酷?
让我们回到端口映射,后面我们会经常查看这张表。
" 端口地址 " 是每个控制器监听的一个特殊的数。当启动的时候, ROM BIOS 为这些控制器设备分配一个不同的数。要知道 ROM BIOS 和 BIOS 相关,但是不同的软件。 ROM BIOS 是一个在 BIOS 芯片上的电子部件。它启动主处理器 , ,加载 BIOS 程序到 0xFFFF:0 ( 与上节的表比较一下 ) 。
ROM BIOS 把这些数分配给不同的控制器,这样控制器就有了一个区分自己的方法。这允许BIOS 设置中断向量表,可以使用一个特殊的数字与硬件交流。
当与I/O 控制器工作时,处理器使用相同的系统总线。处理器在地址总线上放一个特别的端口号, 就像读内存一样。同样会在控制总线READ 或WRITE 位,很酷,但有问题:处理器如何区分读写内存还是访问控制器呢?
处理器会设置控制总线上的另一位——I/O ACCESS 位。如果这一位为1 ,则I/O 控制器通过I/O 系统监视地址总线。如果地址总线上的数与分配给设备的数相对,设备则从数据总线接收数据,并处理它。 如果 这一位为1 内存控制器忽略所有请求。所以如果这个端口号未被分配,绝对不会有事发生,控制器不响应,内存控制器也忽视它。
让我们看看这些端口地址. 这很重要!这是在保护模式下唯一的与硬件交流的方法!
警告:这个表很大!
默认的 x86 端口地址分配 |
||||
地址范围 |
第 1 个 8 字节 |
第 2 个 8 字节 |
第 3 个 8 字节 |
第 4 个 8 字节 |
0x000-0x00F |
DMA 控制器,通道 0-3 |
|||
0x010-0x01F |
系统占用 |
|||
0x020-0x02F |
中断控制器 1 |
系统占用 |
||
0x030-0x03F |
系统占用 |
|||
0x040-0x04F |
系统时钟 |
系统占用 |
||
0x050-0x05F |
系统占用 |
|||
0x060-0x06F |
键盘 /PS2 鼠标 ( 端口 0x60) |
键盘 /PS2 鼠标 (0x64) |
系统占用 |
|
0x070-0x07F |
RTC/CMOS/NMI (0x70, 0x71) |
DMA 控制器,通道 0-3 |
||
0x080-0x08F |
DMA 页寄存器 0-2 (0x81 - 0x83) |
DMA 页寄存器 3 (0x87) |
DMA 页寄存器 4-6 (0x89-0x8B) |
DMA 页寄存器 7 (0x8F) |
0x090-0x09F |
系统占用 |
|||
0x0A0-0x0AF |
中断控制器 2 (0xA0-0xA1) |
系统占用 |
||
0x0B0-0x0BF |
系统占用 |
|||
0x0C0-0x0CF |
DMA 控制器 通道 4-7 (0x0C0-0x0DF), bytes 1-16 |
|||
0x0D0-0x0DF |
DMA 控制器 通道 4-7 (0x0C0-0x0DF), bytes 16-32 |
|||
0x0E0-0x0EF |
系统占用 |
|||
0x0F0-0x0FF |
浮点单元 (FPU/NPU/Mah Cop 处理器 ) |
|||
0x100-0x10F |
系统占用 |
|||
0x110-0x11F |
系统占用 |
|||
0x120-0x12F |
系统占用 |
|||
0x130-0x13F |
SCSI 主适配器 (0x130-0x14F), bytes 1-16 |
|||
0x140-0x14F |
SCSI 主适配器 (0x130-0x14F), bytes 17-32 |
SCSI 主适配器 (0x140-0x15F), bytes 1-16 |
||
0x150-0x15F |
SCSI 主适配器 (0x140-0x15F), bytes 17-32 |
|||
0x160-0x16F |
系统占用 |
第 4 IDE 控制器 , 主从 |
||
0x170-0x17F |
第 2 IDE 控制器 , 主设备 |
系统占用 |
||
0x180-0x18F |
系统占用 |
|||
0x190-0x19F |
系统占用 |
|||
0x1A0-0x1AF |
系统占用 |
|||
0x1B0-0x1BF |
系统占用 |
|||
0x1C0-0x1CF |
系统占用 |
|||
0x1D0-0x1DF |
系统占用 |
|||
0x1E0-0x1EF |
系统占用 |
第 3 IDE 控制器 , 主从 |
||
0x1F0-0x1FF |
主 IDE 控制器 , 主从 |
系统占用 |
||
0x200-0x20F |
游戏手柄端口 |
系统占用 |
||
0x210-0x21F |
系统占用 |
|||
0x220-0x22F |
||||
声卡 |
||||
Non-NE2000 网卡 |
系统占用 |
|||
0x230-0x23F |
SCSI 主适配器 (0x220-0x23F), bytes 17-32) |
|||
0x240-0x24F |
||||
声卡 |
||||
Non-NE2000 网卡 |
系统占用 |
|||
NE2000 网卡 (0x240-0x25F) Bytes 1-16 |
||||
0x250-0x25F |
NE2000 网卡 (0x240-0x25F) Bytes 17-32 |
|||
0x260-0x26F |
||||
声卡 |
||||
Non-NE2000 网卡 |
系统占用 |
|||
NE2000 网卡 (0x240-0x27F) Bytes 1-16 |
||||
0x270-0x27F |
||||
系统占用 |
即插即用系统设备 |
LPT2 – 2 号并口 |
||
系统占用 |
LPT3 – 3 号并口 ( 黑白系统 ) |
|||
NE2000 网卡 (0x260-0x27F) Bytes 17-32 |
||||
0x280-0x28F |
||||
声卡 |
||||
Non NE2000 网卡 |
系统占用 |
|||
NE2000 网卡 (0x280-0x29F) Bytes 1-16 |
||||
0x290-0x29F |
NE2000 网卡 (0x280-0x29F) Bytes 17-32 |
|||
0x2A0-0x2AF |
||||
Non NE2000 网卡 |
系统占用 |
|||
NE2000 网卡 (0x280-0x29F) Bytes 1-16 |
||||
0x2B0-0x2BF |
NE2000 网卡 (0x280-0x29F) Bytes 17-32 |
|||
0x2C0-0x2CF |
系统占用 |
|||
0x2D0-0x2DF |
系统占用 |
|||
0x2E0-0x2EF |
系统占用 |
COM4 – 4 号串口 |
||
0x2F0-0x2FF |
系统占用 |
COM2 - 2 号 串口 |
||
0x300-0x30F |
||||
声卡 / MIDI 端口 |
系统占用 |
|||
Non NE2000 网卡 |
系统占用 |
|||
NE2000 网卡 (0x300-0x31F) Bytes 1-16 |
||||
0x310-0x31F |
NE2000 网卡 (0x300-0x32F) Bytes 17-32 |
|||
0x320-0x32F |
||||
声卡 / MIDI 端口 (0x330, 0x331) |
系统占用 |
|||
NE2000 网卡 (0x300-0x31F) Bytes 17-32 |
||||
SCSI 主适配器 (0x330-0x34F) Bytes 1-16 |
||||
0x330-0x33F |
||||
声卡 / MIDI 端口 |
系统占用 |
|||
Non NE2000 网卡 |
系统占用 |
|||
NE2000 网卡 (0x300-0x31F) Bytes 1-16 |
||||
0x340-0x34F |
||||
SCSI 主适配器 (0x330-0x34F) Bytes 17-32 |
||||
SCSI 主适配器 (0x340-0x35F) Bytes 1-16 |
||||
Non NE2000 网卡 |
系统占用 |
|||
NE2000 网卡 (0x340-0x35F) Bytes 1-16 |
||||
0x350-0x35F |
||||
SCSI 主适配器 (0x340-0x35F) Bytes 17-32 |
||||
NE2000 网卡 (0x300-0x31F) Bytes 1-16 |
||||
0x360-0x36F |
||||
磁带加速卡 (0x360) |
系统占用 |
第 4 IDE 控制器 ( 从设备 )(0x36E-0x36F) |
||
Non NE2000 网卡 |
系统占用 |
|||
NE2000 网卡 (0x300-0x31F) Bytes 1-16 |
||||
0x370-0x37F |
||||
磁带加速卡 (0x370) |
第 2 IDE 控制器 ( 从设备 ) |
LPT1 – 1 号并口 ( 彩色系统 ) |
||
系统占用 |
LPT2 -2 号并口 ( 黑白系统 ) |
|||
NE2000 网卡 (0x360-0x37F) Bytes 1-16 |
||||
0x380-0x38F |
系统占用 |
声卡 (FM Synthesizer) |
系统占用 |
|
0x390-0x39F |
系统占用 |
|||
0x3A0-0x3AF |
系统占用 |
|||
0x3B0-0x3BF |
VGA/ 黑白显示器 |
LPT1 – 1 号并口 ( 黑白系统 ) |
||
0x3C0-0x3CF |
VGA/CGA 显示器 |
|||
0x3D0-0x3DF |
VGA/CGA 显示器 |
|||
0x3E0-0x3EF |
||||
磁带加速卡 (0x370) |
系统占用 |
COM3 – 3 号串口 |
||
系统占用 |
第 3 IDE 控制器 ( 从设备 )(0x3EE-0x3EF) |
|||
0x3F0-0x3FF |
||||
软盘控制器 |
COM1 – 1 号串口 |
|||
磁带加速卡 (0x3F0) |
主 IDE 控制器 ( 从设备 )(0x3F6-0x3F7) |
系统占用 |
这张表不完整,并且希望没什么错。我会随着更多设备的开发增加这张表。
所有这些内存访问被特定的控制器使用——如上表所示。端口地址的确切含义依赖于控制器。它可能代表一个控制寄存器,状态寄存器或是其他的什么东西。台不幸了。
强烈建议你打印一份上面的表格,当我们与硬件交流时,会频繁的参考上表。
我会更新它,如果我更新了,你需要再打印一份,确保它是最新的。
知道了这一切,我们一起来看。
X86 处理器有指令用于端口 I/O 。它们是 IN 和 OUT .
这些指令告诉处理器我们想要和设备交流,它们保证处理器与I/O 设备之间的控制线被正确设置。
看一个完整的例子,并试着从键盘控制器输入缓冲区读数。
看看我们上面的端口分配表,我们发现键盘控制器的端口地址在 0x60 到0x6F . 上表中显示的前8 个字节和第2 个8 字节( 从端口地址0x60 开始) 分别用于键盘 和PS/2 鼠标。后两个8 字节被系统占用,我们不管它。
键盘控制器映射到端口0x60 到端口 0x68 。酷,但对我们来说,这代表什么?这是设备标准,知道吗?
对键盘而言,端口0x60 是控制寄存器, 端口0x64 是状态寄存器。如果状态寄存器的第1 比特为1 ,则输入缓冲区有数据。 所以,如果我们将 控制寄存器设为READ ,我们就能把输入缓冲区的数据复制到什么地方。
WaitLoop:
in
al, 64h
;
取得状态寄存器的值
and al, 10b ; 测试状态寄存器的第1 位
jz WaitLoop ; 如果这位为0 ,缓冲区中没数据
in al, 60h ; 如果为1 从缓冲区( 端口0x60) 读数,并保存
是的,就是这儿,这正是硬件编程和设备驱动开发的基础。
IN 指令执行时,处理器将端口地址— 如0x64— 放到地址总线上,然后设置控制总线上的“I/O 设备”位,和READ 位。那个被ROM BIOS 分配的设备号为0x64 的设备——这里是键盘控制器的状态寄存器 ,知道要执行“读”操作(因为READ 位为1 ),所以它会从键盘寄存器的某个位置上将数据复制到数据总线,清掉控制总线上的READ 位I/O 设备位,并设置READY 位,现在处理器就从数据总线上得到要读的数据了。
OUT 指令相似。处理器将要写的数据放到数据总线 (0 扩展到数据总线宽度) 。 然后,设置控制总线的WRITE 和I/O 设备位。将端口地址——如0x60 ——复制到地址总线。因为“I/O 设备位”为1 ,这个信号告诉所有的控制器监视地址总线。如果地址总线上的数正好与其分配的数匹配,该设备处理这个数据。 我们的例子中是 键盘控制器。键盘控制器知道要执行“写”操作,因为控制执行的WRITE 位被置1 。它将数据总线上的值复制到它的控制寄存器中(那个寄存器被分配的端口地址是0x60 )。键盘控制器清掉WRITE 和I/O 设备位,并设置READY 位,处理器重新获得控制权。
端口映射和端口I/O 很重要,这是我们在保护模式下唯一的与硬件交流的方法。要知道如果我们没有编写中断处理代码,我们就不能使用中断。编写中断处理代码(如输入、输出)需要编写设备驱动,所有的这些都需要直接访问设备。如果你对这些没有信心,做些练习吧,有什么其他的问题,告诉我。
多数 80x86 指令可以被所有的程序使用。但是,有些指令只能被内核程序使用。因此有些指令我们的读者可能不熟悉。我们会大量的使用这些指令,理解他们很重要。
特权级 ( 环 0) 指令 |
||||
指令 |
描述 |
|||
LGDT |
加载 GDT 的地址到 GDTR |
|||
LLDT |
加载 LDT 的地址到 LDTR |
|||
LTR |
加载任务寄存器到 TR |
|||
MOV Control Register |
复制并保存控制寄存器中的数据 |
|||
LMSW |
加载新的机器状态字 |
|||
CLTS |
清空 CR0 控制寄存器任务切换标志 |
|||
MOV Debug Register |
复制并保存调试寄存器中的数据 |
|||
INVD |
使 Cache 无写回失效 |
|||
INVLPG |
是 TLB 实体失效 |
|||
WBINVD |
使 Cache 有写回失效 |
|||
HLT |
处理器停机 |
|||
RDMSR |
读模式描述寄存器 (MSR) |
|||
WRMSR |
写模式描述寄存器 (MSR) |
|||
RDPMC |
读性能监视计数器 |
|||
RDTSC |
读时间戳计数器 |
非内核模式的其他程序执行上面的任意一条指令都会产生一个一般性保护错误, 或者三重错误 .
不要担心你不了解上面的这些指令。我会在需要的时候解释他们。
X86 处理器有很多不同的寄存器 用于 保存当前状态。多数应用程序可访问的通用寄存器、段寄存器和 eflags 。其他寄存器只在向内核那样的环 0 程序有效。
X86 系列有下列寄存器:RAX (EAX(AX/AH/AL)), RBX (EBX(BX/BH/BL)), RCX (ECX(CX/CH/CL)), RDX (EDX(DX/DH/DL)), CS,SS,ES,DS,FS,GS, RSI (ESI (SI)), RDI (EDI (DI)), RBP (EBP (BP)). RSP (ESP (SP)), RIP (EIP (IP)), RFLAGS (EFLAGS (FLAGS)), DR0, DR1, DR2, DR3, DR4, DR5, DR6, DR7, TR1, TR2, TR3, TR4, TR5, TR6, TR7, CR0, CR1, CR2, CR3, CR4, CR8, ST, mm0, mm1, mm2, mm3, mm4, mm5, mm6, mm7, xmm0, xmm1, xmm2, xmm3, xmm4, xmm5, xmm6, xmm7, GDTR, LDTR, IDTR, MSR, TR . 所有这些寄存器都在处理器内部的一个称为寄存器文件 的内存区中。详细信息参考处理器体系结构 一节,其他的不在寄存器文件中的寄存器有:PC, IR, 向量寄存器和硬件寄存器。
这些寄存器中的大部分只在环0 程序有效。其中的大部分会在处理器的很多状态都有效。对于它们的错误设置很容易导致三重错误。其他情况可能会导致CPU 做出错误的动作 ( 多数情况下是因为TR4,TR5,TR6,TR7 的错误使用) 。
其他的一些寄存器是CPU 内部寄存器 ,不可用在通常情况下被访问。当对处理器本身编程的时候会用到它们。常见的如IR ,向量寄存器。
我们得仔细看看一些特殊的寄存器。
注意:把CPU 当作一个你要与之交流的普通设备。控制寄存器的概念 ( 和寄存器本身) 在我们与其它设备交流时很重要。
同样,请注意有效寄存器没有官方文档,所以可能有些寄存器没有列在上面。 如果 你知道,请告诉我 ,我会把它们加上的:) 。
这些 32 位寄存器的寄存器可以用于任何目的,同样这些寄存器也有着特殊的用途。
每个32 位寄存器可以分为两部分。高字 和低字 。高字是高16 位,低字是低16 位。
64 位处理器上,这些寄存器64 位宽,名字是RAX, RBX, RCX, RDX 。其低32 位是EAX 寄存器。
没有给高16 位分配特别的名字。但是低16 位有 , 这些名字后面跟一个'H' ( 低字的高8 位) 或是一个 'L' (低8 位)。
以RAX 为例:
+--- AH -------+--- AL ---+
|
|
|
+-------------------------------------------------------------+
|
|
|
| |
+-------------------------------------------------------------+
|
|
|
| +--------EAX 低32 位-----| -- 32 位处理器有效
|
|
|------------------ RAX 的64 位-------------------------------| -- 64 位处理器有效
这是什么意思? AH 和 AL 是 AX 的一部分,同样 AX 是 EAX 的一部分,因此,无论修改了上面的哪个名字代表的寄存器,都修改了同样的寄存器 - EAX.
这样,也就修改了64 位机上的RAX 。
上面的内容对BX,CX, 和DX 都一样。
通用目的寄存器可以在从环0 到环3 的任意程序中使用。因为这是最基础的汇编语言,我假设你已经知道它们是如何工作的了。
在实模式中,段寄存器用于记录当前的段地址,它们都是 16 位的。
记住:实模式下使用段:偏移的寻址方式。段地址保存在 段寄存器记住,像 BP, SP, 和 BX 用于保存偏移 地址 。
常见的用法如:DS:SI , 其中DS 存有段地址, SI 存有偏移地址。
段寄存器可以在从环0 到环3 的任意程序中使用。因为这是最基础的汇编语言,我假设你已经知道它们是如何工作的了。
x86 有一些寄存器用于辅助内存访问。
每个寄存器都保存一个 16 位的地址 ( 也可能使用偏移地址 ) 。
在32 位处理器上,这些寄存器是32 位的,它们的名字是ESI, EDI, EBP, 和 ESP .
在64 位处理器上,这些寄存器是64 位的,它们的名字是RSI, RDI, RBP, 和 RSP .
16 位寄存器是32 位寄存器的一个子集, 同样,32 位寄存器是64 位寄存器的一个子集,就像RAX 一样。
当特定的指令执行时,栈指针会自动的增加和减少特定的字节。这些指令包括push*, pop* 指令, ret/iret, call, syscall 等。
C 语言,实际上是大多数语言,经常使用栈,我们要保证将栈设定到一个合适的位置上,使得C 语言能够正常工作。另外,记住栈向下生长!
指令指针 (IP) 寄存器保存着当前正在执行的质量的偏移地址。记住:是偏移地址 , * 不是 * 绝对地址 !
指令指针(IP) 有时也称为程序计数器 (PC) 。
32 位计算机上,IP 是32 位的名字为 EIP .
64 位计算机上,IP 是64 位的名字为 RIP .
这是处理器内部的寄存器,不能以常规方法访问。它在处理器控制单元 (CU) 的指令 Cache 里 。它保存了将要被翻译为计处理器内部使用的微指令 的 当前指令。参看处理器体系结构 一节获取更多信息。
EFLAGS 寄存器是 x86 处理器的状态寄存器。它用于确定当前的状态。我们已经使用过很多次了。简单的例子如: jc, jnc, jb, jnb 指令
多数指令都影响EFLAGS 寄存器,这样我们就能产生条件了( 比如一个值是不是比另一个大?) 。
EFLAGS 是FLAGS 寄存器的扩展,RFLAGS 是EFLAGS 和FLAGS 的扩展 。如:
+---------- EFLAGS (32 位)-------+
|
|
|-- FLAGS (16 位)---+ |
|
|
|
==========================================
<
寄存器位
|
|
+------------------------- RFLAGS (64 位) --------------------------+
|
|
位 0 位 63
FLAGS 寄存器状态位 |
||
位 |
符号 |
描述 |
0 |
CF |
进位标志 – 状态位 |
1 |
保留 |
|
2 |
PF |
奇偶标志 |
3 |
保留 |
|
4 |
AF |
调整标志 - 状态位 |
5 |
保留 |
|
6 |
ZF |
零标志 - 状态位 |
7 |
SF |
符号标志 - 状态位 |
8 |
TF |
陷阱标志 ( 单步 ) – 系统标志 |
9 |
IF |
中断允许标志 – 系统标志 |
10 |
DF |
方向标志 – 控制标志 |
11 |
OF |
溢出标志 - 状态位 |
12-13 |
IOPL |
I/O 特权级 (286+) – 控制标志 |
14 |
NT |
嵌套任务标志 (286+) – 控制标志 |
15 |
保留 |
|
16 |
RF |
继续标志 (386+) - 控制标志 |
17 |
VM |
v8086 模式标志 (386+) - 控制标志 |
18 |
AC |
对其检查 (486SX+) - 控制标志 |
19 |
VIF |
虚拟中断标志 (Pentium+) - 控制标志 |
20 |
VIP |
虚拟中断 (Pentium+) - 控制标志 |
21 |
ID |
确认 (Pentium+) - 控制标志 |
22-31 |
保留 |
|
32-63 |
保留 |
IO 特权级(IOPL) 控制 特定指令执行需要的环级。比如: CLI, STI, IN 和OUT 指令在当前特权级与IOPL 相等或更大时才能执行。否则,处理器就会产生一个一般性保护错误 (GPF) 。
多数操作系统将IOPF 设为0 或1 。这表示只有内核级的软件才能执行这些指令. 这是一个很好的事情。毕竟如果所有的程序都可以使用CLI ,它会使得内核停止运行。
对于大多数的操作,我们只需要FLAGS 寄存器。注意RFLAGS 寄存器的或32 位是空的、不存在的,这只是为了好看些罢了,当然可能有速度上的考虑,但是多余的字节就被浪费掉了。
考虑到上面的列表有点大,我建议你打印一份,以备参考。
X86 系列有一些用于测试目的的寄存器。这些寄存器的大多数没有官方文档。在 x86 系列中,这些寄存器有 TR4,TR5,TR6,TR7 。
TR6 常用于命令测试 ,TR7 用于测试数据寄存器。可以使用MOV 指令访问。它们只在环0 有效,无论是保护模式还是实模式,任何其它企图都会导致一般性保护错误 (GPF) 或三重错误 。
这些寄存器用于程序调试。它们是: DR0,DR1,DR2,DR3,DR4,DR5,DR6,DR7 。与测试寄存器一样,它们可以用 MOV 指令访问,并且只能用在环 0 中。任何其它尝试都将导致一般性保护错误 (GPF) 和三重错误 .
寄存器 DR0, DR1, DR2, DR3 保存一个断点的绝对地址 。如果分页有效,这个地址会装换为据对地址。这些断点的执行条件定义在 DR7 中。
DR7 是一个 32 位寄存器,它使用位模式确定当前的调试任务,位模式为:
有两种方法使调试寄存器有效,全局 级的或是局部 级的。如果你有不同的任务 ( 比如分页 ) ,所有局部级的调试设置,只对这个任务有效,在任务切换是处理器自动的清空这些设置。全局级的,则不会这样。
上面的第0 到第7 位,如下表所示:
这个寄存器,用于决定当错误发生时调试器采取的动作。当处理器碰到一个可处理异常时,它会设置这个寄存器的低 4 位,并执行错误处理程序。
注意:调试状态寄存器DR6 ,不会自动清除,如果你想让程序继续运行,请先清空该寄存器!
这些特殊的控制寄存器有特定的处理器提供不同的功能,在别的处理器上可能不能使用。由于它们是系统级的寄存器,只有环 0 的程序可以访问。
应为这些寄存器随着处理器的不同, 这些寄存器可能会改变。
x86 有两个特殊的指令用于访问这个寄存器:
这个寄存器对于不同的处理器有很大差别。因此所以在使用它们之前先使用CPUID 指令。
为了访问这些寄存器, 需要传递一个代表你要访问的寄存器的地址。
这些年来,Intel 的一些MSR 不再是每个机器都不一样了,下面是x86 体系下共同的。
模式特定的寄存器 (MSRs) |
||
寄存器地址 |
寄存器名 |
IA-32 处理器系列 |
0x0 |
IA32_PS_MC_ADDR |
Pentium 处理器 |
0x1 |
IA32_PS_MC_TYPE |
Pentium 4 处理器 |
0x6 |
IA32_PS_MONITOR_FILTER_SIZE |
Pentium 处理器 |
0x10 |
IA32_TIME_STAMP_COUNTER |
Pentium 处理器 |
0x17 |
IA32_PLATFORM_ID |
P6 处理器 |
0x1B |
IA32_APIC_BASE |
P6 处理器 |
0x3A |
IA32_FEATURE_CONTROL |
Pentium 4 / 处理器 673 |
0x79 |
IA32_BIOS_UPDT_TRIG |
P6 处理器 |
0x8B |
IA32_BIOS_SIGN_ID |
P6 处理器 |
0x9B |
IA32_SMM_MONITOR_CTL |
Pentium 4 / 处理器 672 |
0xC1 |
IA32_PMC0 |
Intel Core Duo |
0xC2 |
IA32_PMC1 |
Intel Core Duo |
0xE7 |
IA32_MPERF |
Intel Core Duo |
0xE8 |
IA32_APERF |
Intel Core Duo |
0xFE |
IA32_MTRRCAP |
P6 处理器 |
0x174 |
IA32_SYSENTER_CS |
P6 处理器 |
0x175 |
IA32_SYSENTER_ESP |
P6 处理器 |
0x176 |
IA32_SYSENTER_IP |
P6 处理器 |
有更多的MSR 没有列在上表中。参看附录B Intel 开发手册 中的完整列表。
我不确定在我们的开发过程中是否会涉及MSR ,如果有必要,我会扩充这个列表的。
这个指令将 CX 指定的 MSR 复制到 EDX:EAX 中。
这个指令是特权级 指令,只能在环0( 内核层) 使用。当 非特权程序试图执行这条指令,或CS 中不是一个有效的MSR 地址时,会产生一个一般性保护错误, 或三重错误 。
这个指令不影响任何标志。
下面是使用这条指令的例子 ( 你会在这个教程的后面再见到它):
; 从IA32_SYSENTER_CS MSR 读数据
mov cx, 0x174 ; 寄存器 0x174: IA32_SYSENTER_CS
rdmsr ; 读入MSR
; 现在EDX:EAX 这个64 位寄存器的低32 位和高32 位
很酷,不是吗?
这个指令将保存在 EDX:EAX 中的 64 位数据保存到 CX 指定的 MSR 中。
这个指令是特权级 指令,只能在环0( 内核层) 使用。当 非特权程序试图执行这条指令,或CS 中不是一个有效的MSR 地址时,会产生一个一般性保护错误, 或三重错误 。
这个指令不影响任何标志。
这是使用它的例子:
; 写到IA32_SYSENTER_CS MSR
mov cx, 0x174 ; 寄存器 0x174: IA32_SYSENTER_CS
wrmsr ; 将EDX:EAX 写到MSR
这个对我们很重要。
控制寄存器允许我们改变处理器的动作,它们是:CR0, CR1, CR2, CR3, CR4 。
CR0 是主要的控制寄存器。 32 位定义如下:
呜,真多新东西呀!让我们看看 Bit 0 ——将系统置于保护模式 ,这意味着通过设置 CR0 寄存器的第 0 为,我们可以进入到保护模式。
例如:
mov ax, cr0 ; 取得CR0 的值
or ax, 1 ; 设置0 位——进入保护模式
mov cr0, ax ;0 位为1 ,我们在32 位模式了!
很简单 :)
如果你把上面的代码复制的你的引导加载器中,它很可能会导致一个三重错误。保护模式使用与实模式不一样的内存地址系统。同样,保护模式没有中断 。一个简单的时钟中断就会导致三重错误。同样的,因为我们使用不同的地址模式, CS 变得无效了。 我们需要更新CS 以执行32 位代码。此外,我们还没有设置内存映射的特权级。
我们会在后面详细介绍。
Intel 保留,未使用。
发生页错误的线性地址。如果发生了一个页错误, CR2 保存着那个试图访问的地址。
如果 CR0 的 PG 位置 1 ,最低的 20 位包含页目录的基地址寄存器 (PDBR) 。
在保护模式中用于控制操作,如 v8086 模式,开启 I/O 断点,页大小扩展和机器检测异常。
我不知道我们会不会使用这些标志。我决定在这里包括他们是出于完整性的考虑, 如果你不理解也没有关系。
通过对任务优先级寄存器 (TPR) 的读写访问。
X86 系列使用一些寄存器来保存每个段描述表的线性地址。后文有更详细的解说。
这些寄存器是:
我们会在下一节详细介绍这些寄存器。
尽管我们在这个系列里,你会注意到多数情况下术语“处理器”和“微控制器”是相似的。微控制器有寄存器,执行指令和处理器很像。 CPU 本身不过是一个特别的控制器芯片。
我们会在后面再次讨论引导的过程,只是从更底层来看罢了。这样就可以回答诸如:BIOS POST 到底是怎么开始的,又怎么执行POST ,启动主处理器,加载BIOS 的,一类的问题。我们已经介绍了是什么,还没介绍怎么做呢。
注意:这一节非常的技术化。如果你不理解,被担心,你不需要全部理解。我在这里写这些是出于完整性的考虑,我们会详细了解组成计算机系统和执行代码的主要部分。它们怎么执行我们给出的代码?为什么机器语言如此特殊?这些问题都会在这里回答。
当我们后面 学习内核及设备驱动开发时,你会发现学习理解计算机的基本硬件组成不仅仅是一个好的学习经历,有时也是理解控制器编程的必然要求。
为了解释的目的我们看看 Pentium III 处理器,我们先打开盖子,看看实际的组成:
处理器里有好多东西,不是吗?看看它多复杂。我们能从图上了解很多,我们先看看每个部件。