现代的多用户多进程操作系统,需要 MMU 才能达到每个用户进程都拥有自己独立的地址空间的目标。使用 MMU,操作系统划分出一段地址区域,在这块地址区域中, 每个进程看到的内容都不一定一样。例如 Windows 操作系统将地址范围 4M-2G 划分为用户地址空间,进程 A 在地址 0X400000(4M)映射了可执行文件,进程 B 同样在地址 0X400000(4M)映射了可执行文件,如果 A 进程读地址 0X400000,读到的是 A 的可执行文件映射到 RAM 的内容,而进程 B 读取地址 0X400000 时,读到的则是 B 的可执行文件映射到 RAM 的内容。这就是 MMU 在当中进行地址转换所起的作用。
A segment selector is a 16-bit identifier for a segment . It does not point directly to the segment, but instead points to the segment descriptor that defines the segment.
这段时间学操作系统,好奇计算机是怎么从通电到成功加载操作系统的,看了一些文章顺便做下总结。
第 0、1 小节介绍了一些地址和寄存器的基本概念,后面介绍了 80386 从通电后,怎么把操作系统加载到内存中来运行的过程。
0. 几个地址的概念
先来理解这几个地址的概念:物理地址、虚拟地址(线性地址)、逻辑地址。
任何时候,计算机上都存在一个程序能够产生的地址集合,我们称之为地址范围。这个范围的大小由 CPU 的位数决定,例如一个 32 位的 CPU,它的地址范围是
0~0xFFFFFFFF
(4G), 而对于一个 64 位的 CPU,它的地址范围为0~0xFFFFFFFFFFFFFFFF
(64T)。 这个范围就是程序能够产生的地址范围,我们把这个地址范围称为虚拟地址空间,该空间中的某一个地址我们称之为虚拟地址。与虚拟地址空间和虚拟地址相对应的则是物理地址空间和物理地址,大多数时候我们的系统所具备的物理地址空间只是虚拟地址空间的一个子集。这里有一个虚拟内存的概念,虚拟内存(virtual memory)是对整个内存(不要和机器上插那条对上号)的抽像描述。它是相对于物理内存来讲的,能直接理解成 “不直实的”,“假的” 内存。现代操作系统都提供了一种内存管理的抽像,即虚拟内存(virtual memory)。进程使用虚拟内存中的地址,由操作系统协助相关硬件,把它 “转换” 成真正的物理地址。这个“转换”,是所有问题讨论的关键。 有了这样的抽像,一个程序就能使用比真实物理地址大得多的地址空间(拆东墙,补西墙,银行也是这样子做的),甚至多个进程能使用相同的地址。这不奇怪,因为转换后的物理地址并非相同的。
CPU 将一个逻辑地址转换为物理地址,需要进行两步:
这样做两次转换,的确是非常麻烦而且没有必要的,因为直接可以把线性地址抽象给进程。之所以这样冗余,Intel 完全是为了兼容而已(Intel 为了兼容,将远古时代的段式内存管理方式保留了下来,x86 体系的处理器刚开始时只有 20 根地址线,寻址寄存器是 16 位。我们知道 16 位的寄存器可以访问 64K 的地址空间,如果程序要想访问大于 64K 的内存,就需要把内存分段,每段 64K,用段地址+偏移量的方式来访问,这样使 20 根地址线全用上,最大的寻址空间就可以到 1M 字节,这在当时已经是非常大的内存空间了。
现代的多用户多进程操作系统,需要 MMU 才能达到每个用户进程都拥有自己独立的地址空间的目标。使用 MMU,操作系统划分出一段地址区域,在这块地址区域中, 每个进程看到的内容都不一定一样。例如 Windows 操作系统将地址范围 4M-2G 划分为用户地址空间,进程 A 在地址 0X400000(4M)映射了可执行文件,进程 B 同样在地址 0X400000(4M)映射了可执行文件,如果 A 进程读地址 0X400000,读到的是 A 的可执行文件映射到 RAM 的内容,而进程 B 读取地址 0X400000 时,读到的则是 B 的可执行文件映射到 RAM 的内容。这就是 MMU 在当中进行地址转换所起的作用。
1. X86 寄存器说明
32 位 CPU 所含有的寄存器有:
1.1 数据寄存器
数据寄存器主要用来保存操作数和运算结果等信息,从而节省读取操作数所需占用总线和访问存储器的时间。
32 位 CPU 有 4 个 32 位的通用寄存器 EAX、EBX、ECX 和 EDX。对低 16 位数据的存取,不会影响高 16 位的数据。这些低 16 位寄存器分别命名为:AX、BX、CX 和 DX,它和先前的 16 位 CPU 中的寄存器相一致。
4 个 16 位寄存器又可分割成 8 个独立的 8 位寄存器(AX:AH-AL、BX:BH-BL、CX:CH-CL、DX:DH-DL),每个寄存器都有自己的名称,可独立存取。程序员可利用数据寄存器的这种“可分可合”的特性,灵活地处理字/字节的信息。
在 16 位 CPU 中,AX、BX、CX 和 DX 不能作为基址和变址寄存器来存放存储单元的地址,但在 32 位 CPU 中,其 32 位寄存器 EAX、EBX、ECX 和 EDX 不仅可传送数据、暂存数据保存算术逻辑运算结果,而且可作为指针寄存器,所以,这些 32 位寄存器更具有通用性。
1.2 变址寄存器
32 位 CPU 有 2 个 32 位通用寄存器 ESI 和 EDI。其低 16 位对应先前 16 位 CPU 中的 SI 和 DI,对低 16 位数据的存取,不影响高 16 位的数据。
变址寄存器不可分割成 8 位寄存器。作为通用寄存器,也可存储算术逻辑运算的操作数和运算结果。
它们可作一般的存储器指针使用。在字符串操作指令的执行过程中,对它们有特定的要求,而且还具有特殊的功能。
1.3 指针寄存器
32 位 CPU 有 2 个 32 位通用寄存器 EBP 和 ESP。其低 16 位对应先前 16 位 CPU 中的 BP 和 SP,对低 16 位数据的存取,不影响高 16 位的数据。
指针寄存器不可分割成 8 位寄存器。作为通用寄存器,也可存储算术逻辑运算的操作数和运算结果。
它们主要用于访问堆栈内的存储单元,并且规定:
1.4 段寄存器
在 32 位 CPU 中,有 6 个 16 位的段寄存器,所以,在此环境下开发的程序最多可同时访问 6 个段。
段寄存器是根据内存分段的管理模式而设置的。内存单元的物理地址是由段寄存器的值和一个偏移量组合而成 的,这样可用两个较少位数的值组合成一个可访问较大物理空间的内存地址。
CPU 内部的段寄存器为:
尽管只有 6 个段寄存器,但程序可以把同一个段寄存器用于不同地目的,这 6 个段寄存器中的 3 个有专门的用途:
其它 3 个段寄存器用作一般用途,可以指向任意的数据段。
1.4.1 实模式与保护模式简述
32 位 CPU 有两个不同的工作模式:实模式和保护模式。实模式和保护模式都是 CPU 的工作模式,而 CPU 的工作模式是指 CPU 的寻址方式、寄存器大小等用来反应 CPU 在该环境下如何工作的概念。
在这两种模式下,段寄存器的作用是不同的。有关规定简单描述如下:
1.4.2 实模式原理
实模式出现于早期 8086 和 8088 CPU 时期。当时由于 CPU 的性能有限,一共只有 20 位地址线 A19 ~ A0,寻址 1MB 的存储空间,其物理地址范围为
00000H ~ FFFFFH
(即 2^20 = 1048576 Byte,所以地址空间只有 1MB)。由于复位后首先从地址高端的 FFFFFH 开始执行指令,所以将地址高端设置位 ROM 空间,而低端作为 RAM 空间。80386 的实模式是为了与 8086 处理器兼容而设置的,在实模式下,80386 处理器就相当于一个快速的 8086 处理器。80386 处理器被复位或加电的时候以实模式启动,这时候处理器中的各寄存器以实模式的初始值工作。
此时 80386 的 32 位地址线只使用了低 20 位,即可访问 1MB 的物理地址空间。在实模式下,80386 处理器不能对内存进行分页机制的管理,所以指令寻址的地址就是内存中实际的物理地址。在实模式下,所有的段都是可以读、写和执行的。实模式下 80386 不支持优先级,所有的指令相当于工作在特权级(即优先级 0),所以它可以执行所有特权指令,包括读写控制寄存器 CR0 等。这实际上使得在实模式下不太可能设计一个有保护能力的操作系统。
它有 8 个 16 位的通用寄存器,以及 4 个 16 位的段寄存器。所以为了能够通过这些 16 位的寄存器去构成 20 位的主存地址,必须采取一种特殊的方式。当某个指令想要访问某个内存地址时,它通常需要用下面的这种格式来表示:
其中第一个字段是段基址,它的值是由段寄存器提供的(一般来说,段寄存器有 6 种,上面有介绍)。
第二个字段是段内偏移量,代表要访问的这个内存地址距离这个段基址的偏移。它的值就是由通用寄存器来提供的,所以也是 16 位。那么两个 16 位的值如何组合成一个 20 位的地址呢?CPU 采用的方式是把段寄存器所提供的段基址先向左移4位。这样就变成了一个 20 位的值,然后再与段偏移量相加。即:
所以假设段寄存器中的值是
0xff00
,段偏移量为0x0110
。则这个地址对应的真实物理地址是:0xff00<<4 + 0x0110 = 0xff110
。由上面的介绍可见,实模式的“实”更多地体现在其地址是真实的物理地址。
1.4.3 保护模式原理
随着 CPU 的发展,CPU 地址线的个数也从原来的 20 根变为现在的 32 根,可以访问的内存空间也从 1MB 变为现在4GB,寄存器的位数也变为 32 位。所以实模式下的内存地址计算方式就已经不再适合了,因此就引入了现在的保护模式,实现更大空间的、更灵活也更安全的内存访问。
简单地说,通过保护模式,可以把虚拟地址空间映射到不同的物理地址空间,且在超出预设的空间范围会报错(一种保护机制的体现),且可以保证处于低特权级的代码无法访问高特权级的数据(另外一种保护机制的体现)。
在保护模式下,80386 的 32 条地址线全部有效,但是内存寻址方式还是得兼容老办法,即(段基址:段偏移量)的表示方式,这使得其:
保护模式下的偏移值和实模式下是一样的,就是变成了 32 位而已。段值仍旧是存放在原来 16 位的段寄存器中,但是这些段寄存器存放的却不再是段基址了。之前说过实模式下寻址方式不安全,在保护模式下需要加一些限制,而这些限制不是一个寄存器能够容纳的,于是把这些关于内存段的限制信息放在一个叫做全局描述符表(GDT:Global Descriptor Table)的结构里。全局描述符表中含有一个个表项,每一个表项称为段描述符。在保护模式下,段寄存器存放的便是相当于一个数组索引的东西,即段选择子(Segment Selector),通过这个索引,可以找到对应的表项。
在保护模式下,每个内存段就是一个段描述符。段描述符存放了段基址(Base)、段界限(Limit)、内存段类型属性(比如是数据段还是代码段,注意一个段描述符只能用来定义一个内存段)等许多属性,具体信息见下图:
其中,段界限表示段边界的扩张最值,即最大扩展多少或最小扩展多少,用 20 位来表示,它的单位可以是字节,也可以是 4KB,这是由 G 位决定的(G 为 1 时表示单位为 4KB)。
此外, 扩充的存储器分段管理机制和可选的存储器分页管理机制,有如下好处:
1.4.4 延伸知识:段选择子、特权级细节
这一小节会对段选择子及特权级的细节进行总结。
特权级
CPL、RPL 与 DPL
段选择子的存储布局
段选择子(Segment Selector)
它是这样定义的:
也就是前面说的,段选择子是一个 16 位的段标识符,它不直接指向段,而是指向 GDT 中定义该段的段描述符。此外还需要理解:
也就是说:
CS 段寄存器指向的是 CPU 中当前运行的指令,所以 CS 的 RPL 位称为当前特权级 CPL。所以得出结论:
CS.RPL 的值 = CPL
的值。1.5 指令指针寄存器
32 位 CPU 把指令指针扩展到 32 位,并记作 EIP,EIP 的低 16 位与先前 16 位 CPU 中的 IP 作用相同。
在实模式下,由于每个段的最大范围为 64K,所以,EIP 中的高 16 位肯定都为 0,此时,相当于只用其低 16 位的 IP 来反映程序中指令的执行次序。
1.6 标志寄存器
以下是运算结果标志位。
1.6.1 进位标志 CF(CarryFlag)
进位标志 CF 主要用来反映运算是否产生进位或借位。如果运算结果的最高位产生了一个进位或借位,那么,其值为 1,否则其值为 0。
使用该标志位的情况有:多字(字节)数的加减运算,无符号数的大小比较运算,移位操作,字(字节)之间移位,专门改变 CF 值的指令等。
1.6.2 奇偶标志 PF(ParityFlag)
奇偶标志 PF 用于反映运算结果中“1”的个数的奇偶性。如果“1”的个数为偶数,则 PF 的值为 1,否则其值为 0。
利用 PF 可进行奇偶校验检查,或产生奇偶校验位。在数据传送过程中,为了提供传送的可靠性,如果采用奇偶校验的方法,就可使用该标志位。
1.6.3 辅助进位标志 AF(AuxiliaryCarryFlag)
在发生下列情况时,辅助进位标志 AF 的值被置为 1,否则其值为 0:
1.6.4 零标志 ZF(ZeroFlag)
零标志 ZF 用来反映运算结果是否为 0。如果运算结果为 0,则其值为 1,否则其值为 0。在判断运算结果是否为 0 时,可使用此标志位。
16.5 符号标志 SF(SignFlag)
符号标志 SF 用来反映运算结果的符号位,它与运算结果的最高位相同。在微机系统中,有符号数采用补码表示法,所以,SF 也就反映运算结果的正负号。运算结果为正数时,SF 的值为 0,否则其值为 1。
1.6.6 溢出标志 OF(OverflowFlag)
溢出标志 OF 用于反映有符号数加减运算所得结果是否溢出。如果运算结果超过当前运算位数所能表示的范围,则称为溢出,OF 的值被置为 1,否则,OF 的值被清为 0。
对以上 6 个运算结果标志位,在一般编程情况下,标志位 CF、ZF、SF 和 OF 的使用频率较高,而标志位 PF 和 AF 的使用频率较低。
2. X86 平台启动过程
2.1 读取第一条指令
在计算机通电后,寄存器在初始状态下会有一个缺省值。
CS(16 位的段寄存器的基址) 和 EIP(段内偏移) 结合在一起形成了启动后的第一条地址,CPU 会从该地址获取指令来执行。
上面有介绍,X86 为了向下兼容 8086,刚启动时是处于 16 位的实模式,会按照实模式的寻址方式来寻址:
所以第一条地址的实际值如下:
2.2 BIOS 所做的工作
BIOS 主要做一些硬件的初始化工作,完成一些关键硬件的自检,随后:
2.3 Bootloader 所做的工作
Bootloader 所做的工作,简单说就是进入保护模式,加载操作系统到内存,如下:
这样 Bootloader 就完成了操作系统的加载工作。
当然这还没完,仅仅是把操作系统加载到内存中,接下来终于要轮到操作系统登场了。
在总结完后看到条 2017 年的新闻:Intel 已经决心在 2020 年之前,彻底淘汰 PC BIOS,全面向 UEFI 固件过渡,而这也意味着一个时代的终结。
继任者叫 UEFI。
3. 参考