从头开始编写操作系统(8) 第7章:系统结构

来源:互联网 发布:热血霸业神奇网络 编辑:程序博客网 时间:2024/06/01 23:41

译自:http://www.brokenthorn.com/Resources/OSDev7.html

7章:系统结构
by Mike, 2008

本系列文章旨在向您展示并说明如何从头开发一个操作系统。

介绍

欢迎!在之前的一章里,我们总算完成了引导加载器的工作!到目前为止:

我们详细的了解了FAT12文件系统,并且了解了价值,解析,执行stage2的方法。

这章里会继续前面的工作。首先我们会仔细的看看x86体系结构。这很重要,对于理解保护模式如何工作尤其重要。

我们会介绍计算机工作的每件事儿,我们要深入到比特一层。为了理解BIOS在启动过程中是如何胜任工作的,你得记住你也可以启动其他的处理器。BIOS仅仅处理注处理器,我们也可以做同样的事儿来支持多处理器。

包括以下内容:

  • 80x86寄存器
  • 系统组织
  • 系统总线
  • 实模式内存映射
  • 指令如何执行
  • 软件端口

就某些方面来说,这更像是一个计算机体系结构的教程。然而我们是在以操作系统开发的角度来看计算机体系结构。当然我们会涉及体系结构的方方面面。

理解这些会使我们更了解保护模式,在下一章里,我们会包括切换到保护模式的所有细节。

享受乐趣吧!

保护模式的世界

我们都听过这个术语。保护模式 (PMode) 是在80286及之后处理器提供的一个操作模式。保护模式主要用于提高系统的稳定性。

从前面的章节里你知道,实模式有大问题。首先,我们能在任何想要的地方写数据,这会覆盖代码或数据,这些代码或数据可能是软件端口或是处理器或是我们自己的。并且要做到这样的事情,我们有超过4,000种不同的方法——包括直接的和间接的。

实模式没有内存保护。所有的数据和代码被放置在单个的,为通用目的而存在的内存块中。

实模式,限制使用16位寄存器,1MB内存。

不支持硬件级,内存保护多任务

最重要的问题是,不存在向“环”这样的东西。所有的程序都在环0级执行,所有的程序都有系统的绝对控制权。这意味着,如果你不够小心,在一个单任务环境里的,一条指令(cli/hlt)会使你的整个操作系统崩溃。

所有的这些,在我们讨论实模式时都提到过,保护模式解决了所有的这些问题。

保护模式:

  • 有内存保护
  • 虚拟存储器任务状态切换(TSS)的硬件机制
  • 硬件级支持中断编程及执行
  • 4中操作模式: 0, 1, 2, 3
  • 访问32位寄存器
  • 访问高达4GB内存

我们在上一章中的汇编语言的环一节里提到,我们工作在 0,通常的程序工作在环 3 (一般情况). 我们能够使用特殊的指令访问特殊的寄存器,而一般的程序不行。在这章里,我们将会使用LGDT 指令,来完成一个“远跳”使用我们自己定义的段,和处理器控制寄存器。这在普通的程序中是不行的

了解系统结构并制定处理器如何工作,有利于我们的工作。

系统结构

x86系列计算机遵循冯诺依曼体系结构。典型的冯诺依曼体系结构的计算机有3部分组成:

  • 中央处理器(CPU)
  • 内存
  • 输入/输出 (IO)

例如:

有些事情有注意。如你所知, CPU重内存中取得数据和指令。内存控制器的工作是确定特定的RAM芯片和其中的存储单元,所以,CPU与内存控制器交流。

另外,注意"I/O设备"。他们也连接在系统总线上。所有的I/O端口是内存位置的映射,这使得我们能够使用INOUT指令

硬件设备使用系统总线访问内存。也允许我们当一些事情发生时通知设备。比如,如果向控制硬件设备控制器要读的位置写一字节,处理器可以通知设备“有数据在数据总线上”,这是通过控制总线在操作。这是软件与硬件交互的基础,我们会在后面详细的解释它,因为这是在保护模式中唯一的域硬件交互的方法,很重要。

我们先分开来介绍,然后,把他们结合起来,并且通过一个指令在硬件层之下的过程了解他们在一起是如何工作的。下面我们将讨论I/O端口以及软件与硬件之间是如何交互的。

如果你有x86的汇编经验,一些甚至是大部分对你来说是很熟悉的。但我们要介绍那些在大部分汇编语言教材里没有深入的内容,特别是环 0的程序。

系统总线

系统总线也称作前部总线,它在主板上连接CPU和北桥。

系统总线是数据总线、地址总线和控制总线的集合。总线上的一条电线表示1比特。使用电压来表示01,基于标准晶体管-晶体管逻辑(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可访问的最大内存。

808080186处理器都有20/比特地址总线。802868038624/比特, 80386+32/比特。

所有的x86系列都向老处理器兼容。这也是为什么启动时处在实模式。处理器限制通过20条地址线访问1MB内存——line0line 19

对我们来说很重要, 因为这样的限制对我们依旧存在!我们需要使20号地址线有效,才能让我们的操作系统访问高达4GB的内存。 后文有更详细的解说。

控制总线

我们把数据放到数据总线,使用地址总线确定内存地址,我们怎么知道如何处理数据呢?是读数据还是写数据呢?

控制总线是一系列表示设备要进行的工作的电线(比特)。比如:处理器设置READWRITE位使内存控制器知道他要在地址总线上指定的内存位置读到数据总线或是写入数据总线的数据。

控制总线也允许处理器通知设备。使设备引起注意,比如:我们要设备从perhaps地址总线指定的内存位置读数据该怎么办呢?这要让设备知道我们希望它做的事。这在I/O 软件端口很重要。

当然,要知道系统总线并不直接与硬件设备相连。相反的它们连接到一个中心控制器——I/O控制器,用于切换,并给设备发信号。

完了

上面是系统总线的全部内容。它是处理器 (通过控制单元 (CU))I/O设备(通过I/O控制器) 访问内存的通路。访问内存需要通过内存控制器,它的任务是确定要访问的内存芯片及内存芯片上的哪个存储单元。

"控制器"这个词你可能听过很多,我在后面详细解释。

内存控制器

内存控制器是系统总线与物理内存直接的主要接口。

我们前面见过控制器,不是吗?控制器到底是什么?

控制器

控制器提供基本的已经控制功能。它也提供基本的软硬件接口。这很重要。记得吗,在保护模式里,我们不能使用任何中断。在引导加载器里我们使用一系列中断来与硬件交流。而在保护模式使用这些中断会导致三重错误,我们怎么办呢?

我们要与硬件直接通信。我们要通过控制器,(在我们介绍了I/O系统后,我们会详细介绍控制器是怎么工作的)。

内存控制器

内存控制器为软件提供了一种读写内存位置的方法。内存控制器也有刷新RAM芯片的责任,以保证数据不丢失。

内存控制器有一个多路选择器(Multiplexer和一个信号分离器(Demultiplexer 用于选择特定的RAM芯片,并且定位地址总线确定的地址。

 双数据率(DDR)控制器

DDR控制器用于刷新DDR SDRAM, 使用系统时钟脉冲来读写内存。

双通道控制器

双通道控制器用在DRAM设备上,它由两组小的总线,可以同时读写两个不同的内存位置,这有助于加快RAM的访问速度。

内存控制器总结

内存控制器从地址总线接收地址。这很好,但我们怎么告诉内存控制器是读还是写内存呢?还有数据从哪里来的呢?当我们读内存时。处理器设置在控制总线上的Read位;同样,在写内存时,处理器设置在控制总线上的Write位。

处理器使用控制总线控制设备。

内存控制器使用的数据在数据总线上,使用的地址在地址总线上。

读内存

当读内存时,处理器将要读的内存的决定地址放到地址总线上。然后处理器设置读控制线。

内存控制器获得控制权。控制器使用多路选择器将绝对地址转换为物理RAM位置,并把数据放到数据总线上,然后将READ位清0,设置READY位。

现在处理器知道了数据在数据总线上。它复制这个数据,并执行剩余的指令……比如把数据保存到BX里。

写内存

写内存类似。

首先,处理器将内存地址放到地址总线上。将要写的数据放到数据总线上。然后,设置控制总线上的WRITE位。

内存控制器知道了要往地址总线确定的内存地址写在数据总线上的数据。完成后,内存控制器清空WRITE位,设置控制总线的READY位。

总结

我们不直接和内存交流,我们间接的做,无论读还是写内存,我们都使用内存控制器。内存控制器是软件与RAM芯片之间的接口。

下面我们看看I/O系统,等等!1337 多路选择器是什么样?它是内存控制器中的物理线路。要了解它的工作方式,我们得知道一点数字逻辑电路的知识。对我们来说复杂了,如果你想知道更多,Google一下!

I/O 系统

I/O系统简单的表示I/O端口这个系统提供了软件和硬件控制器之间的接口

仔细看看。

端口

端口简单的提供软件与硬件设备直接的接口,有两种类型的端口:软件端口和硬件端口。

硬件端口

硬件端口提供两个物理设备之间的接口。这样的接口通常使用“槽”来连接设备,包括,不限于:串口,并口,PS/2, 1394, 火线,USB口等。

这些端口通常在机箱的边上、前面或是后面。

如果你想看看这样的接口,顺着一根连到你的电脑上的线,你就找到了。

一般的电器上,端口上的针承载的信号在不同的设备上有不同的含义。这些针就像系统总线一样代表比特!每根针1比特。

一般将硬件端口分为两类,“公”的和“母”的。“公”的端口的针是露出来的,“母”的与它相反。硬件端口通过控制器访问。 后文有更详细的解说。

软件端口

这对我们相当重要。软件端口是个数,它表示一个(或一种)硬件控制权。

你可能知道有些数代表同一个控制器。原因呢?内存映射I/O。基本的想法是通过一个特定的内存地址来与硬件交流。端口号就代表这些地址。这表明地址可以代表特定设备的一根寄存器,或是控制寄存器。

以后会仔细介绍。

内存映射

x86结构里,处理器使用特殊的内存位置来表示特定的东西。

比如:地址 0xA000:0 表示显卡的VRAM起始地址。在这个位置写数据,你直接改变了显存的内容,也就改变了屏幕上显示的内容。

其他的内存地址代表其他的一些东西——比如软驱控制器 (FDC)的某个寄存器。

了解哪个地址是什么,是很关键的,也很重要。

x86 实模式内存映射

一般的x86 实模式内存映射:

  • 0x00000000 - 0x000003FF实模式中断向量表
  • 0x00000400 - 0x000004FF – BIOS数据区
  • 0x00000500 - 0x00007BFF未使用
  • 0x00007C00 - 0x00007DFF引导加载器
  • 0x00007E00 - 0x0009FFFF未使用
  • 0x000A0000 - 0x000BFFFF显存(VRAM)
  • 0x000B0000 - 0x000B7777单色显存
  • 0x000B8000 - 0x000BFFFF彩色显存
  • 0x000C0000 - 0x000C7FFF显存ROM BIOS
  • 0x000C8000 - 0x000EFFFF - BIOS Shadow Area
  • 0x000F0000 - 0x000FFFFF系统BIOS

注意:也可能会将上面的设备映射到完全不同的内存区域。BIOS POST程序来完成上面的设备映射工作。

好,很好,因为这些地址代表不同的东西,读写这些特殊的地址会得到,或改变计算机的不同部分的状态。

比如,还记得我们关于INT 0x19的讨论吗?我们说在0x0040:0x00720x1234 会跳转到0xFFFF:0,实现计算机的热重启(Windows ctrl+alt+del)。段:偏移寻址方式的0x0040:0x0072转换为绝对地址是0x000000472,这是BIOS数据区的一部分。

另一个例子是文本输出,往0x000B8000写几个字节,我们就直接改变了字符模式的显存。因为在现实的时候不断刷新,这就改变了显示在屏幕上的字符,酷?

让我们回到端口映射,后面我们会经常查看这张表。

端口映射 内存映射I/O

"端口地址"是每个控制器监听的一个特殊的数。当启动的时候,ROM BIOS为这些控制器设备分配一个不同的数。要知道ROM BIOSBIOS相关,但是不同的软件。ROM BIOS是一个在BIOS芯片上的电子部件。它启动主处理器,,加载BIOS程序到0xFFFF:0 (与上节的表比较一下)

ROM BIOS把这些数分配给不同的控制器,这样控制器就有了一个区分自己的方法。这允许BIOS设置中断向量表,可以使用一个特殊的数字与硬件交流。

当与I/O控制器工作时,处理器使用相同的系统总线。处理器在地址总线上放一个特别的端口号,就像读内存一样。同样会在控制总线READWRITE位,很酷,但有问题:处理器如何区分读写内存还是访问控制器呢?

处理器会设置控制总线上的另一位——I/O ACCESS位。如果这一位为1,则I/O控制器通过I/O 系统监视地址总线。如果地址总线上的数与分配给设备的数相对,设备则从数据总线接收数据,并处理它。如果这一位为1内存控制器忽略所有请求。所以如果这个端口号未被分配,绝对不会有事发生,控制器不响应,内存控制器也忽视它。

让我们看看这些端口地址. 这很重要!这是在保护模式下唯一的与硬件交流的方法!

警告:这个表很大!

默认的x86端口地址分配

地址范围

18字节

28字节

38字节

48字节

0x000-0x00F

DMA控制器,通道0-3

0x010-0x01F

系统占用

0x020-0x02F

中断控制器 1

系统占用

0x030-0x03F

系统占用

0x040-0x04F

系统时钟

系统占用

0x050-0x05F

系统占用

0x060-0x06F

键盘/PS2鼠标 (端口 0x60)
扬声器(0x61)

键盘/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)

系统占用

这张表不完整,并且希望没什么错。我会随着更多设备的开发增加这张表。

所有这些内存访问被特定的控制器使用——如上表所示。端口地址的确切含义依赖于控制器。它可能代表一个控制寄存器,状态寄存器或是其他的什么东西。台不幸了。

强烈建议你打印一份上面的表格,当我们与硬件交流时,会频繁的参考上表。

我会更新它,如果我更新了,你需要再打印一份,确保它是最新的。

知道了这一切,我们一起来看。

INOUT指令

X86处理器有指令用于端口I/O。它们是INOUT.

这些指令告诉处理器我们想要和设备交流,它们保证处理器与I/O设备之间的控制线被正确设置。

看一个完整的例子,并试着从键盘控制器输入缓冲区读数。

看看我们上面的端口分配表,我们发现键盘控制器的端口地址在 0x600x6F. 上表中显示的前8个字节和第28字节(从端口地址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),所以它会从键盘寄存器的某个位置上将数据复制到数据总线,清掉控制总线上的READI/O设备位,并设置READY位,现在处理器就从数据总线上得到要读的数据了。

OUT指令相似。处理器将要写的数据放到数据总线 (0扩展到数据总线宽度)。 然后,设置控制总线的WRITEI/O设备位。将端口地址——如0x60——复制到地址总线。因为“I/O设备位”为1,这个信号告诉所有的控制器监视地址总线。如果地址总线上的数正好与其分配的数匹配,该设备处理这个数据。我们的例子中是键盘控制器。键盘控制器知道要执行“写”操作,因为控制执行的WRITE位被置1。它将数据总线上的值复制到它的控制寄存器中(那个寄存器被分配的端口地址是0x60)。键盘控制器清掉WRITEI/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

读时间戳计数器

非内核模式的其他程序执行上面的任意一条指令都会产生一个一般性保护错误或者三重错误.

不要担心你不了解上面的这些指令。我会在需要的时候解释他们。

80x86 寄存器

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位寄存器的寄存器可以用于任何目的,同样这些寄存器也有着特殊的用途。

  • EAX – 累加寄存器。主要用途:数学计算
  • EBX – 基地址 寄存器。主要用途:作为直接内存访问的基地址.
  • ECX – 计数寄存器。主要用途:循环计数
  • EDX – 数据寄存器。主要用途:存数,是的,就是这样:)

每个32位寄存器可以分为两部分。高字低字。高字是高16位,低字是低16位。

64位处理器上,这些寄存器64位宽,名字是RAX, RBX, RCX, RDX。其低32位是EAX 寄存器。

没有给高16位分配特别的名字。但是低16位有 这些名字后面跟一个'H' (低字的高8)或是一个 'L'(低8位)。

RAX为例:

                                                 +--- AH -------+--- AL ---+
                                                 |                    |               |
        +-------------------------------------------------------------+
        |                    |                   |                    |               |
        +-------------------------------------------------------------+
        |                                        |                                     |
        |                                          +--------EAX 32-----| -- 32位处理器有效
        |                                                                              |
        |------------------ RAX64-------------------------------| -- 64位处理器有效

这是什么意思?AHAL AX的一部分,同样AXEAX的一部分,因此,无论修改了上面的哪个名字代表的寄存器,都修改了同样的寄存器 - EAX.

这样,也就修改了64位机上的RAX

上面的内容对BX,CX,DX都一样。

通用目的寄存器可以在从环0到环3的任意程序中使用。因为这是最基础的汇编语言,我假设你已经知道它们是如何工作的了。

段寄存器

在实模式中,段寄存器用于记录当前的段地址,它们都是16位的。

  • CS – 代码段寄存器
  • DS – 地址段寄存器
  • ES – 附加段寄存器
  • SS – 堆栈段寄存器
  • FS – 远程段寄存器
  • GS – 通用寄存器

记住:实模式下使用段:偏移的寻址方式。段地址保存在段寄存器记住,像BP, SP,BX用于保存偏移地址

常见的用法如:DS:SI,其中DS存有段地址, SI存有偏移地址。

段寄存器可以在从环0到环3的任意程序中使用。因为这是最基础的汇编语言,我假设你已经知道它们是如何工作的了。

索引寄存器

x86有一些寄存器用于辅助内存访问。

  • SI – 源地址索引
  • DI – 目的地址索引
  • BP – 基指针
  • SP – 栈指针

每个寄存器都保存一个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位计算机上,IP32位的名字为EIP.

64位计算机上,IP64位的名字为RIP.

指令寄存器

这是处理器内部的寄存器,不能以常规方法访问。它在处理器控制单元(CU)指令Cache。它保存了将要被翻译为计处理器内部使用的指令当前指令。参看处理器体系结构一节获取更多信息。

EFlags 标志寄存器

EFLAGS寄存器是x86处理器的状态寄存器。它用于确定当前的状态。我们已经使用过很多次了。简单的例子如:jc, jnc, jb, jnb指令

多数指令都影响EFLAGS寄存器,这样我们就能产生条件了(比如一个值是不是比另一个大?)

EFLAGSFLAGS寄存器的扩展,RFLAGSEFLAGSFLAGS的扩展。如:

 +---------- 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, INOUT指令在当前特权级与IOPL相等或更大时才能执行。否则,处理器就会产生一个一般性保护错误 (GPF)

多数操作系统将IOPF设为01。这表示只有内核级的软件才能执行这些指令. 这是一个很好的事情。毕竟如果所有的程序都可以使用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位寄存器,它使用位模式确定当前的调试任务,位模式为:

  • Bit 0...7使调试寄存器有效(详见后文)
  • Bit 8...14 - ?
  • Bit 15...23当断点被触发是,每2位代表一个单独的调试寄存器。其值可以是下面的一个:
    • 00执行时中断
    • 01写数据时中断
    • 10 – IO读写时中断。当前你没有硬件支持
    • 11数据读写时中断
  • Bit 24...31定义监视的内存大小,每2位代表一个单独的调试寄存器。其值可以是下面的一个:
    • 00 – 1字节
    • 01 - 2字节
    • 10 - 8字节
    • 11 - 4字节

有两种方法使调试寄存器有效,全局级的或是局部级的。如果你有不同的任务(比如分页),所有局部级的调试设置,只对这个任务有效,在任务切换是处理器自动的清空这些设置。全局级的,则不会这样。

上面的第0到第7位,如下表所示:

  • Bit 0: 开启局部 DR0 寄存器
  • Bit 1: 开启全局 DR0 寄存器
  • Bit 2: 开启局部 DR1 寄存器
  • Bit 3: 开启全局 DR1 寄存器
  • Bit 4: 开启局部 DR2 寄存器
  • Bit 5: 开启全局 DR2 寄存器
  • Bit 6: 开启局部 DR3 寄存器
  • Bit 7: 开启全局 DR3 寄存器

调试状态寄存器

这个寄存器,用于决定当错误发生时调试器采取的动作。当处理器碰到一个可处理异常时,它会设置这个寄存器的低4位,并执行错误处理程序。

注意:调试状态寄存器DR6,不会自动清除,如果你想让程序继续运行,请先清空该寄存器!

模式特定的寄存器

这些特殊的控制寄存器有特定的处理器提供不同的功能,在别的处理器上可能不能使用。由于它们是系统级的寄存器,只有环0的程序可以访问。

应为这些寄存器随着处理器的不同,这些寄存器可能会改变。

x86有两个特殊的指令用于访问这个寄存器:

  • RDMSRMSR
  • WRMSR写向MSR

这个寄存器对于不同的处理器有很大差别。因此所以在使用它们之前先使用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,如果有必要,我会扩充这个列表的。

RDMSR 指令

这个指令将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

很酷,不是吗?

WRMSR 指令

这个指令将保存在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 控制寄存器

CR0是主要的控制寄存器。32位定义如下:

  • Bit 0 (PE) : 将系统置于保护模式
  • Bit 1 (MP) : 监视协处理器标志,它控制WAIT指令的执行。
  • Bit 2 (EM) : 仿真标志。当该位被设置,协处理器指令会产生一个异常
  • Bit 3 (TS) :任务切换标志,处理器由一个任务切换到另一个任务时,该位被置1
  • Bit 4 (ET) : 扩展类型标志,它告诉我们,安装的是何种类型的协处理器。
    • 0 – 安装的是80287
    • 1 – 安装的是80387
  • Bit 5 (NE): 数值错误
    • 0 – 标准错误报告有效
    • 1 - x87 FPU内部错误报告有效
  • its 6-15 : 不使用
  • Bit 16 (WP): 写保护
  • Bit 17: 不使用
  • Bit 18 (AM): 对齐标志
    • 0 – 对齐检查无效
    • 1 – 对齐检查有效(要求环3EFLAGSAC标志置1)
  • Bits 19-28: 不使用
  • Bit 29 (NW): Not Write-Through
  • Bit 30 (CD): 禁用Cache
  • Bit 31 (PG) :内存分页有效
    • 0 – 无效
    • 1 – 有效,并使用CR3 寄存器

呜,真多新东西呀!让我们看看Bit 0——将系统置于保护模式,这意味着通过设置CR0寄存器的第0为,我们可以进入到保护模式。

例如:

               mov     ax, cr0        ;取得CR0的值
               or      ax, 1          ;设置0位——进入保护模式
               mov     cr0, ax        ;0位为1,我们在32位模式了!
 

很简单:)

如果你把上面的代码复制的你的引导加载器中,它很可能会导致一个三重错误。保护模式使用与实模式不一样的内存地址系统。同样,保护模式没有中断。一个简单的时钟中断就会导致三重错误。同样的,因为我们使用不同的地址模式,CS变得无效了。我们需要更新CS以执行32位代码。此外,我们还没有设置内存映射的特权级。

我们会在后面详细介绍。

CR1 控制寄存器

Intel保留,未使用。

CR2 控制寄存器

发生页错误的线性地址。如果发生了一个页错误,CR2保存着那个试图访问的地址。

CR3 控制寄存器

如果CR0PG位置1,最低的20位包含页目录的基地址寄存器(PDBR)

CR4 控制寄存器

在保护模式中用于控制操作,如v8086模式,开启I/O断点,页大小扩展和机器检测异常。

我不知道我们会不会使用这些标志。我决定在这里包括他们是出于完整性的考虑,如果你不理解也没有关系。

  • Bit 0 (VME) : 开启虚拟8086模式扩展
  • Bit 1 (PVI) : 开启保护模式虚拟中断
  • Bit 2 (TSD) : 开启时间戳
    • 0 - RDTSC 指令可用于任意特权级
    • 1 - RDTSC 指令只用于环0
  • Bit 3 (DE) : 开启调试扩展
  • Bit 4 (PSE) : 页大小扩展
    • 0 – 页大小为4KB
    • 1 – 页大小为4MB. PAE开启是,页大小为2MB.
  • Bit 5 (PAE) : 无论地址扩展
  • Bits 6 (MCE) : 机器检测异常
  • Bits 7 (PGE) : 分页全局有效
  • Bits 8 (PCE) : 性能监视计数器开启
    • 0 - RDPMC指令可用于任意特权级
    • 1 - RDPMC指令只用于环0
  • Bits 9 (OSFXSR) : FXSAVEFXSTOR指令(SSE)的操作系统支持
  • Bits 10 (OSXMMEXCPT) : 对无标记的SIMD FPU异常的操作系统支持
  • Bits 11-12 : 不使用
  • Bits 13 (VMXE) : VMX开启

CR8 控制寄存器

通过对任务优先级寄存器(TPR)的读写访问。

保护模式段寄存器

X86系列使用一些寄存器来保存每个段描述表的线性地址。后文有更详细的解说。

这些寄存器是:

  • GDTR - 全局描述表寄存器
  • IDTR - 中断描述表寄存器
  • GDTR - 局部描述表寄存器
  • TR – 任务寄存器

我们会在下一节详细介绍这些寄存器。

处理器体系结构

尽管我们在这个系列里,你会注意到多数情况下术语“处理器”和“微控制器”是相似的。微控制器有寄存器,执行指令和处理器很像。CPU本身不过是一个特别的控制器芯片。

我们会在后面再次讨论引导的过程,只是从更底层来看罢了。这样就可以回答诸如:BIOS POST到底是怎么开始的,又怎么执行POST,启动主处理器,加载BIOS的,一类的问题。我们已经介绍了是什么,还没介绍怎么做呢。

注意:这一节非常的技术化。如果你不理解,被担心,你不需要全部理解。我在这里写这些是出于完整性的考虑,我们会详细了解组成计算机系统和执行代码的主要部分。它们怎么执行我们给出的代码?为什么机器语言如此特殊?这些问题都会在这里回答。

当我们后面学习内核及设备驱动开发时,你会发现学习理解计算机的基本硬件组成不仅仅是一个好的学习经历,有时也是理解控制器编程的必然要求。

解剖处理器

为了解释的目的我们看看Pentium III处理器,我们先打开盖子,看看实际的组成:

处理器里有好多东西,不是吗?看看它多复杂。我们能从图上了解很多,我们先看看每个部件。

  • L2: 2级缓存
  • CLK: 时钟
  • PIC: 可编程中断控制器
  • EBL: 前部总线逻辑
  • BBL: 后部总线逻辑
  • IEU: 整数处理单元
  • FEU:浮点处理单元
  • MOB: 内存顺序缓存
  • MIU / MMU: 内存接口单元/内存管理单元
  • DCU: 数据缓存单元
  • IFU: 取指令单元
  • ID: 指令解码器
  • ROB: 逆序缓冲区
  • MS: 微指令序列
  • BTB: 分支目标缓冲区
  • BAC: 分支分配缓存区
  • RAT: 寄存器换名表
  • SIMD: 浮点数打包
  • DTLB: 数据TLB
  • RS: 保留站
  • PMH: 缺页控制权
  • PFU: 预取单元
  • TAP: 测试访问端口

我计划扩充这有一节。

指令如何执行

好的,还记得IP寄存器保存有当前执行的指令的偏移地址,而CS 保存段地址吗?

当指令执行时,处理器到底发生了什么?

首先要计算要读取指令的绝对地址。在段:偏移模式下,绝对地址= 段地址*16+偏移地址。或者说:绝对地址 = CS*16 + IP

处理器将这个地址复制到地址总线上。要知道地址就是一系列电信号,每个电信号代表1比特。这个位模式表示下一条指令的绝对地址的二进制表示。

此后,处理器设置"读内存"(把这一位置1)。这告诉内存控制器我们要从内存读数据。

内存控制器获得控制权。内存控制器从地址总线获得数据,并计算它在RAM芯片上的实际位置。内存控制器更新这个位置,并把它放在到数据总线上。这是因为在控制总线上的"读内存"位被置1

内存控制器重置控制,这样处理器就知道了读内存是让我完成了。处理器从数据总线取值,并使用其内的数字逻辑电路"执行"它。这个“值”是机器指令的二进制表示,被一系列的电子脉冲编码。

比如:指令mov ax, 0x4c00 0xB8004C会被当道数据总线上。0xB8004C是操作码(OPCode)。每条指令都有一条对应的opcode。对于i86体系,上面的指令会变成opcode 0xB8004C。我们可以把它变换为二进制形式,这样我们就能看看他们在电线上的情况。其中1代表高,0代表低:

101110000000000001001100

处理器根据构建在其内部的数字逻辑电路和一系列离散指令来解释。这些指令告诉处理器怎样处理这些比特。所有是x86处理器会将这个位模式当作我们的mov ax, 0x4c00指令.

指令变得越来越复杂,多数新的处理器实际上是根据其内部的指令级。对处理器来说这不新鲜——许多微控制器使用其内部的指令集以减少电路的复杂度。通常的这些是宏指令微指令

宏指令是一个处理器用于将指令解码为微指令的抽象指令集。宏指令通常被电子工程师使用特殊的宏语言开发并保存在控制器内部的ROM芯片上,使用宏汇编器编译。宏汇编器将宏语言汇编成更低级的语言——那些被控制器使用的语言:微指令。

微指令是电子工程师开发的非常低级的语言。微指令被控制器或处理器用来几秒指令——如0xB8004C (mov ax,0x4c00) 指令.

使用处理器的算术逻辑单元(ALU) CPU就得到了数——0x4C00。并将它复制到AX (简单的位复制)

这个例子展示了它们是如何一起协同工作的。CPU如何使用系统总线,内存控制器如何估价控制总线解码内存位置的。

这是一个重要概念,软件端口在模拟工作方式下依赖于内存控制器。

保护模式理论

为什么我们会讨论体系结构呢?因为在保护模式下,不能使用中断。而没中断、没系统调用、没标准库,啥都没有。一切都得靠我们自己。没有任何的帮助,一个错误就会使我们的系统崩溃,如果不小心,甚至会损坏硬件,不只是软盘,还包括硬盘,外部(和内部)设备等。

理解系统结构对我们理解一切有很大的帮助,起码可以使你少犯错误。同样这也给你一种对硬件编程的直接指导——这是我们能做的一切。

你可能会想:等等,你承诺的C内核在哪儿?好的,要知道C在一定程度上是低级语言。通过行内汇编,你可以创建一个硬件接口层。C就像C++一样只产生那些直接被x86处理器直接执行的代码。记住:这里没有标准库,尽管你是在使用高级语言在编程,你仍然在一个非常底层的环境下工作

当我们完成这些,我们就开始内核的编写。

总结

我从没写过这样的教程。它包含了大量的信息,为了更好的理解也包含了少量代码,这很难写,你知道吗?

我希望我做的够好。我希望包含内存映射,端口映射,x86端口地址,所有的x86寄存器,x86内存映射,系统体系结构,IN/OUT 指令和它们的执行步骤——一步一步的。我们也看了硬件编程的基本步骤——我们会在后面做很多。

下一章,我们会做个改变——欢迎来到32位的世界! 我们会详细了解GDT ——我们需要它来做这个切换。我也会警告你们每一步会发生的常见错误。如我先前所说的,在保护模式里,一点小错误就会是你的系统崩溃。

来吧 :)

下次见

原创粉丝点击