引言:一段不到一秒的史诗

你按下电脑的电源按钮,听到风扇开始转动,看到屏幕上闪过一些白色的文字,然后操作系统缓缓升起。这一切发生在几秒之内,平淡无奇。但在这几秒里,你的计算机完成了一次从“混沌”到“有序”的奇迹般的蜕变。

如果我们把这短短几秒的时间拉长、放慢,你会看到这样一幅画面:

  • 第一毫秒:CPU从某个固定的内存地址醒来,它不知道自己是谁,也不知道该做什么。它像个初生的婴儿,只记得一个指令——“去那里”。
  • 第二毫秒:BIOS接管了控制,它开始四处巡视,检查内存、键盘、显卡……像一个严谨的管家,确认一切就绪。
  • 第三毫秒:BIOS找到了你的硬盘或软盘,翻出第一个扇区里的“咒语”——一段只有512字节的代码。它把这段代码小心翼翼地放到内存的一个角落,然后拍了拍它的肩膀:“去吧,接下来的路,你自己走。”
  • 第四毫秒:那段代码开始执行,它先把自己搬到更安全的地方,又召唤了两个同伴,最后三人联手,把CPU从古老的“实模式”带进了现代的“保护模式”。从此,计算机才能真正运行像Linux这样复杂的操作系统。

这就是引导启动程序的故事。而我们要讲的第一部分,就是故事发生之前的世界——那个计算机刚通电时的“原始状态”,以及为什么我们必须从“实模式”跳到“保护模式”。

为了让你能真正理解后续的汇编代码,我们这一章先不急着敲代码,而是把舞台的背景、规矩和角色全盘托出。相信我,当你理解了BIOS、实模式和保护模式,后面的bootsect.ssetup.shead.s对你来说就不再是天书,而是一部你已经读过剧本的精彩戏剧。

按下电源键

CPU硬复位
EIP=0xFFFFFFF0

BIOS接管

POST自检
初始化硬件

寻找启动设备

加载引导扇区
至0x7C00

跳转至0x7C00
执行bootsect.s

引导程序三阶段

进入保护模式

跳转至main()

上图的含义:整个启动过程就像一场精心编排的接力赛,从电源键按下到内核开始执行,每一步都环环相扣,缺一不可。

3.1 概述:引导过程的“接力赛”

3.1.1 从电源键到内核——宏观全景

我们先从宏观上看一下整个引导流程,把每一步的角色都认清楚。

第一步:CPU硬复位

当电源稳定供电后,主板上的复位电路会将CPU的RESET引脚拉低一段时间,然后释放。这个复位信号会让CPU内部的所有寄存器回到默认值。其中最关键的,是指令指针(EIP)被设置为0xFFFFFFF0(对于80386及以后的32位CPU)。

这个地址非常特殊。0xFFFFFFF0距离4GB地址空间的顶端只有16个字节。在大多数PC系统中,这个地址被映射到主板上的BIOS ROM芯片——一块即使断电也不会丢失内容的“固件存储器”。CPU从BIOS ROM中取出第一条指令,开始执行。

为什么不是0x00000000 因为物理内存(RAM)在刚上电时内容是不确定的(随机值),无法存放可执行的代码。而BIOS ROM是事先烧录好的,内容确定且可靠。所以CPU必须从ROM开始执行。

第二步:BIOS的“管家工作”

BIOS(基本输入输出系统)是一段固化的程序。在1991年的PC中,BIOS通常存储在主板上的EPROM或Flash芯片中。它负责在操作系统接管之前,完成所有硬件的初始化和基本检测。BIOS的主要工作包括:

  • 上电自检(POST,Power-On Self-Test):检查CPU、内存、键盘、显卡等关键硬件是否存在并正常工作。如果内存有坏块,屏幕会显示错误码或通过蜂鸣器发出特定的报警音(例如,连续短声可能表示内存故障)。这个过程在早期的286/386机器上可能需要十几秒。

  • 初始化硬件:为每个检测到的硬件分配基本的I/O端口、中断号、DMA通道等资源。例如,它会检测显卡类型并初始化显示模式,检测磁盘控制器并配置其I/O地址。

  • 建立中断向量表:在实模式下,BIOS会在物理内存0x00000x03FF处建立一个中断向量表(IVT)。这张表共有256个条目,每个条目占4个字节(2字节段地址+2字节偏移量),对应一个中断号。例如:

    • int 0x10(向量地址0x0040):视频服务
    • int 0x13(向量地址0x004C):磁盘服务
    • int 0x15(向量地址0x0054):系统服务(获取内存大小等)

    这些中断是引导程序在实模式下与硬件交互的唯一途径——bootsect.s使用int 0x13读取磁盘,int 0x10显示字符串;setup.s使用int 0x15获取内存大小,int 0x10获取光标位置。

  • 扫描扩展ROM:BIOS还会扫描0xC00000xDFFFF的地址范围,寻找显卡、网卡等扩展卡的ROM,并执行其中的初始化代码。

  • 寻找启动设备:BIOS按照CMOS中设置的启动顺序(在1991年通常是先A驱后C驱),依次尝试读取每个设备的第一个扇区(512字节)。如果某个扇区的最后两个字节是0xAA55(小端序,内存中为55 AA),BIOS就认为这个设备是可启动的。

第三步:加载引导扇区

找到可启动设备后,BIOS将该设备的第一个扇区(即引导扇区,Master Boot Record或Volume Boot Record)读取到物理内存地址0x7C00(段地址0x07C0,偏移0x0000)。然后,BIOS执行一条跳转指令,把CPU的控制权交给0x7C00处的代码。

为什么是0x7C00 这个地址并非随意选择。最早的IBM PC(1981年)只有64KB内存,DOS的引导扇区需要加载到32KB处(0x7C00 = 31,744,正好在32KB以下512字节)。这个传统一直延续下来,即使后来的PC拥有数MB内存,BIOS仍然将引导扇区加载到0x7C00。Linux 0.11遵循了这个约定。

至此,BIOS的使命完成,引导程序开始接管。BIOS提供的中断服务仍然可用,但CPU的控制流已经转移到了引导扇区的代码。

内存低端布局 0x00000~0x9FFFF

0x00000~0x003FF
中断向量表
256个中断向量

0x00400~0x004FF
BIOS数据区
硬件状态信息

0x00500~0x07BFF
空闲区域
约30KB可用

0x07C00~0x07DFF
引导扇区
bootsect.s(512B)

0x07E00~0x9FFFF
自由空间
约608KB

上图说明:BIOS将引导扇区精确地加载到0x7C00这个位置,这是IBM PC历史上的一个约定。它位于内存低端的32KB范围内,既足够低,又不会与中断向量表(0x00000x03FF)和BIOS数据区(0x04000x04FF)冲突。这个古老的决定,一直沿用到今天。

第四步:引导程序的三阶段

在Linux 0.11中,引导程序由三个汇编源文件组成,它们分工明确:

文件 大小 加载地址 运行模式 主要任务
bootsect.s 512字节 0x7C000x90000 实模式 自我搬迁,加载setup.ssystem模块
setup.s 约2KB 0x90200 实模式 → 保护模式 收集硬件参数,开启A20,建立GDT/IDT,进入保护模式
head.s 约6KB 0x00000 保护模式 重建GDT/IDT,检测FPU,开启分页,跳入main()

这三个程序就像接力赛的三棒选手:

  • 第一棒(bootsect.s):从BIOS手中接过“火种”,但它知道自己只有512字节,无法完成所有工作。于是它先把自己搬到更安全的位置(0x90000),然后加载第二棒和第三棒到预定位置。
  • 第二棒(setup.s):在实模式下完成最后的硬件侦察,然后执行一系列精密操作,将CPU从16位实模式切换到32位保护模式,把接力棒交给第三棒。
  • 第三棒(head.s):在32位世界中建立分页机制、重建系统表,最终通过一个精巧的栈技巧,跳入C语言世界。

阶段3:setup.s执行后

0x90000:硬件参数表

0x90200:setup.s(已完成使命)

0x00000:system模块(已搬移)

阶段2:bootsect.s执行后

0x90000:bootsect.s(已搬移)

0x90200:setup.s(已加载)

0x10000:system模块(已加载)

阶段1:BIOS加载后

0x07C00:bootsect.s

0x90200:空闲

0x10000:空闲

上图说明:引导程序三阶段中,各模块在内存中的位置不断变化。bootsect.s0x7C00搬到0x90000,为加载setup.s腾出空间;system模块从0x10000搬移到0x00000,为内核在保护模式下运行做好准备。

形象比喻:把整个计算机启动比作一个电影的拍摄过程。

  • BIOS:制片人,负责搭建场地、准备器材、选角(找到可启动设备)。
  • bootsect.s:场务,负责把主要演员(setup.ssystem模块)请到片场,并把自己挪到后台。
  • setup.s:导演,负责把场景从“黑白默片”升级到“彩色宽银幕”(从实模式切换到保护模式),并架好所有摄像机(GDT、IDT)。
  • head.s:摄影指导,负责调整最后的光圈和焦距(分页机制),然后喊“Action!”——把指挥棒交给main()函数,也就是主演。

3.1.2 为什么需要这么复杂的流程?

你可能想问:为什么不能直接把内核加载到内存,然后直接跳转执行?

答案是多重约束下的必然选择。

约束一:8086/80286兼容性

Linux 0.11设计于1991年,目标平台是Intel 80386处理器。但IBM PC/AT体系结构有严格的上电兼容要求:CPU上电时自动进入实模式,以兼容古老的8086/8088程序。BIOS本身也是16位实模式程序,依赖实模式下的中断向量表和1MB地址空间。如果CPU一启动就进入保护模式,BIOS根本无法运行。

所以,任何操作系统都必须从实模式开始,然后自行切换到保护模式。这就是“必须有人在实模式下完成模式切换”的根本原因。

约束二:引导扇区只有512字节

BIOS只读取引导设备的第一个扇区(512字节)。这512字节必须包含引导代码、磁盘参数、可能的错误信息字符串、分区表(如果是硬盘MBR)、以及结尾的引导签名0xAA55。512字节完全不足以容纳一个完整的内核(Linux 0.11编译后约120KB)。因此,引导扇区的代码必须极其精简,只负责加载更大的后续模块。

约束三:中断向量表占用低端内存

在实模式下,物理地址0x000000x003FF被中断向量表占用,0x004000x005FF被BIOS数据区占用。因此,bootsect.s不能把system模块直接加载到0x00000,只能先放到0x10000(64KB处)。等BIOS不再需要中断向量表之后(即进入保护模式之前),再由setup.ssystem0x10000搬到最终的家0x00000

约束四:1MB地址空间的限制

实模式下只有20根地址线(A0~A19),最大寻址1MB。但80386有32根地址线,可寻址4GB。为了兼容古老的8086程序(它们依赖地址回绕——访问1MB以上时自动回到0),IBM在主板上设计了一个逻辑门,可以屏蔽第21根地址线(A20)。上电时A20是关闭的,操作系统必须显式开启A20才能访问1MB以上的内存。这就是setup.s中那段向8042键盘控制器发送0xDF命令的由来。

这些约束叠加在一起,决定了引导过程不能是“一步到位”的,而必须是“分段接力”的。每一段代码都有明确的任务,完成后将控制权交给下一段,直到内核完全就绪。

约束1:CPU上电默认实模式
BIOS只能运行在实模式

结果:必须分段接力

约束2:引导扇区仅512字节
无法容纳完整内核

约束3:低端内存被占用
不能直接加载到0x00000

约束4:A20默认关闭
无法访问1MB以上内存

bootsect.s → setup.s → head.s
三阶段引导

上图说明:四大约束共同决定了引导流程必须是三段式的。每个约束都是一道“关卡”,引导程序必须逐一解决,才能让内核顺利启动。

3.2 实模式与保护模式:两座“城市”的对比

在深入代码之前,我们必须先理解CPU的两种运行模式。它们就像两座完全不同的城市:一座是古老、简单、但缺乏管制的“实模式城”,另一座是现代、复杂、但秩序井然的“保护模式城”。

保护模式特点

地址线:32位(A0~A31)

最大寻址:4GB

段寄存器存储:段选择子

内存保护:段级+页级

特权级:Ring 0~3

典型应用:现代操作系统

实模式特点

地址线:20位(A0~A19)

最大寻址:1MB

段寄存器存储:段基址

内存保护:无

特权级:无

典型应用:BIOS、DOS、引导初期

上图说明:实模式和保护模式的核心区别一目了然。从1MB到4GB,从无保护到有保护,从基址直接寻址到通过描述符表间接寻址——这是计算机体系结构的一次质的飞跃。

3.2.1 实模式:一座“违章建筑”横行的城市

3.2.1.1 实模式的起源与设计

实模式是Intel 8086/8088处理器(1978年发布)引入的一种运行模式。8086有20根地址线,理论上可以寻址2^20 = 1,048,576字节,即1MB的内存空间。在那个年代,1MB已经大得令人惊叹——当时许多计算机只有64KB甚至16KB内存。

寻址方式:段基址:偏移量

在实模式下,内存地址由两部分组成:

  • 段基址(Segment Base):存储在16位段寄存器中(CS、DS、SS、ES,386以后增加了FS和GS)。
  • 偏移量(Offset):存储在16位通用寄存器中(IP、SP、SI、DI、BX等)。

物理地址的计算公式是:

物理地址 = (段基址 << 4) + 偏移量

这里的“<< 4”表示左移4位,等价于乘以16。为什么是16?因为8086有20根地址线,但寄存器只有16位。将16位的段基址乘以16后,得到20位的段起始地址,再加上16位的偏移量,最终形成20位的物理地址。

例如:CS = 0x1234IP = 0x5678,则执行的物理地址为:

0x1234 × 16 + 0x5678 = 0x12340 + 0x5678 = 0x179B8

段基址
例:CS=0x1234

左移4位(×16)
0x1234 << 4 = 0x12340

偏移量
例:IP=0x5678

加法运算

物理地址
0x179B8
(20位有效)

上图说明:实模式下的地址转换非常直接——段基址左移4位后加上偏移量。这种“段基址:偏移量”的形式,在汇编代码中经常看到,例如mov ax, [0x1234]

段重叠:由于段基址左移4位,两个相邻的段基址之间相差16字节(而不是64KB)。因此,同一个物理地址可以由多个不同的“段基址:偏移量”组合来表示。例如,0x0000:0x7C000x07C0:0x0000都指向物理地址0x7C00。这种“段重叠”是实模式下特有的现象,也是许多底层编程技巧的源头。

3.2.1.2 实模式的“致命缺陷”:没有保护

在实模式下,所有程序都运行在同一个地址空间,没有任何内存保护机制。任何程序都可以:

  • 随意读写任何内存位置(包括BIOS数据区、中断向量表)
  • 执行任何CPU指令(包括特权指令)
  • 访问任何I/O端口

这意味着一个程序的bug(甚至恶意代码)可以轻易地覆盖中断向量表,导致整个系统崩溃。在现代操作系统中,这种“自由”是灾难性的。

但为什么我们还要用实模式?

因为兼容性。1978年到1990年代初,无数DOS程序在实模式下编写。这些程序假设了平坦的内存模型,假设可以随意访问任何内存位置。如果Intel让80386一启动就进入保护模式,所有这些老程序将无法运行。所以,Intel选择了上电时默认进入实模式,让操作系统或引导程序自行决定何时切换到保护模式。

3.2.1.3 实模式下的“中断向量表”

在实模式下,物理内存的0x00000x03FF(共1024字节)被BIOS用作中断向量表(IVT)。每个中断号占用4个字节(高2字节是段地址,低2字节是偏移量),总共支持256个中断(从int 0x00int 0xFF)。

当CPU执行int N指令(或硬件触发中断N)时:

  1. 将标志寄存器FLAGS压栈
  2. 清除IF(中断使能)和TF(陷阱)标志
  3. 将当前CS和IP压栈
  4. 从中断向量表读取第N个条目(地址 = N × 4)
  5. 将读取到的段地址和偏移量分别加载到CS和IP
  6. 从新的CS:IP处继续执行

BIOS在上电时会填充许多中断向量,为操作系统提供基本的硬件访问接口。引导程序正是通过调用这些BIOS中断来完成磁盘读取、屏幕显示、内存查询等操作的。

重要提示:进入保护模式后,中断向量表不再有效。保护模式下使用中断描述符表(IDT),它的格式完全不同。这就是为什么setup.s在进入保护模式前必须用cli关闭中断——在IDT建立之前,任何中断都会导致CPU崩溃。

3.2.2 保护模式:一座秩序井然的现代化都市

3.2.2.1 保护模式的设计目标

保护模式随Intel 80286(1982年)首次引入,在80386(1985年)中得到了极大的完善。80386的保护模式是一个32位的运行环境,其核心设计目标包括:

  1. 更大的寻址空间:32根地址线,理论上最大可寻址4GB物理内存。对于1991年的PC来说,这简直是天文数字——当时主流配置是2~4MB。

  2. 内存保护:每个内存段都有独立的访问权限(读、写、执行),并且分为4个特权级(Ring 0~3)。操作系统运行在最高特权级(Ring 0),用户程序运行在最低特权级(Ring 3)。任何越权访问都会触发CPU异常,由操作系统处理。

  3. 虚拟内存支持:通过可选的分页机制,允许每个进程拥有独立的地址空间。配合“缺页异常”处理,可以实现比物理内存更大的虚拟地址空间(交换到磁盘)。

  4. 多任务硬件支持:通过任务状态段(TSS),CPU可以在硬件层面保存和恢复任务的执行状态,实现快速任务切换。

3.2.2.2 寻址方式的根本变革:段选择子与描述符表

在保护模式下,段寄存器里存放的不再是段基址,而是一个段选择子(Segment Selector)。它像一个图书馆里的索引卡片,指向**描述符表(Descriptor Table)**中的某个条目。描述符表可以是全局的(GDT,Global Descriptor Table)或局部的(LDT,Local Descriptor Table)。

16位段选择子的位域布局

位15~3:Index
13位索引
指向描述符表中的某一项

位2:TI
1位表指示
0=GDT,1=LDT

位1~0:RPL
2位请求特权级
范围0~3

上图说明:段选择子是一个16位的“钥匙”,它告诉CPU去哪个描述符表(GDT或LDT)的哪个位置(Index)查找段信息,同时携带了请求者的特权级(RPL)。

段选择子的格式

名称 含义
15~3 Index(13位) 描述符表中的索引,最多8192项
2 TI(1位) 0=使用GDT,1=使用LDT
1~0 RPL(2位) 请求特权级0~3

段描述符的格式(8字节)

一个描述符定义了段的基址(32位)、限长(20位)、访问权限(类型、特权级等)。这个8字节结构包含了段的所有属性:

字节偏移 字段 含义
0~1 限长[15:0] 段限长低16位
2~3 基址[15:0] 段基址低16位
4 基址[23:16] 段基址中8位
5 访问权限字节 P、DPL、S、TYPE
6 标志位 G、D/B、AVL、限长[19:16]
7 基址[31:24] 段基址高8位

各关键位含义:

  • 基址(Base,32位):段在4GB线性地址空间中的起始位置。
  • 限长(Limit,20位):段的大小。配合G位,单位可以是1字节(最大1MB)或4KB(最大4GB)。
  • G(Granularity):0=限长以字节为单位,1=以4KB页为单位。
  • D/B:0=16位代码/数据,1=32位代码/数据。
  • P(Present):1=段存在于内存中。
  • **DPL(Descriptor Privilege Level,位1314)**:描述符特权级(03)。
  • TYPE(位8~11):段的类型——代码/数据、可读/可写、可执行等。

8字节描述符的结构化视图

字节0~1:限长[15:0]

字节2~3:基址[15:0]

字节4:基址[23:16]

字节5:P | DPL | S | TYPE
访问权限字节

字节6:G | D/B | AVL | 限长[19:16]
标志位

字节7:基址[31:24]

上图说明:段描述符的8个字节中,基址分散在三个位置(字节23、字节4、字节7),限长分散在两个位置(字节01、字节6的高4位)。这种分散布局是历史原因造成的(为了兼容80286),在代码中通常使用宏或结构体来组装。

地址转换流程

段选择子
例:CS=0x0008

提取字段
Index=1, TI=0, RPL=0

TI == 0 ?

通过GDTR定位GDT

通过LDTR定位LDT

从GDT中取出第Index个描述符

从描述符中提取32位段基址
例:基址=0x00000000

段基址 + 偏移量
= 线性地址

CR0.PG == 1 ?

线性地址直接作为物理地址

通过页目录+页表
转换为物理地址

上图说明:保护模式下的寻址不再直接使用段基址,而是通过“选择子→描述符表→描述符→基址”的间接路径。这虽然增加了复杂度,但换来了内存保护和更大的寻址空间。如果启用了分页(CR0.PG=1),线性地址还需要经过页表转换才能得到物理地址。

3.2.2.3 特权级与保护机制

保护模式引入了4个特权级(Ring 0~3),从高到低依次为:

特权级 名称 典型用途
Ring 0 最高权限 操作系统内核
Ring 1 高权限 通常未使用(预留给设备驱动)
Ring 2 中权限 通常未使用
Ring 3 最低权限 用户应用程序

Linux 0.11只使用了Ring 0和Ring 3。内核代码和数据运行在Ring 0,用户程序运行在Ring 3。这种隔离保证了用户程序的崩溃不会影响内核,恶意程序也无法直接访问内核内存。

特权级检查涉及三个关键值:

  • CPL(Current Privilege Level):当前正在执行的代码的特权级,存储在CS和SS段寄存器的低2位中。
  • DPL(Descriptor Privilege Level):描述符中指定的特权级,表示访问该段所需的最低权限。
  • RPL(Requested Privilege Level):段选择子中携带的请求特权级。

访问一个段时,CPU会检查:CPL ≤ DPLRPL ≤ DPL(数值越小,权限越高)。如果条件不满足,会触发“一般保护异常”(#GP,中断13)。

访问请求
包含CPL, RPL, 目标DPL

CPL ≤ DPL ?
(当前权限足够?)

触发一般保护异常
(#GP, 中断13)

RPL ≤ DPL ?
(请求权限足够?)

访问允许
执行内存操作

上图说明:CPU在访问一个段时,会检查当前特权级(CPL)和请求特权级(RPL)是否都不高于描述符特权级(DPL)。只有两个条件都满足时,访问才被允许。这种双重检查机制为操作系统提供了强大的保护能力。

3.2.2.4 分页机制:虚拟内存的基石

分页机制是80386保护模式的一个可选但极其重要的扩展。Linux 0.11在head.s中开启了分页,因为它需要:

  • 进程隔离:每个进程拥有独立的4GB虚拟地址空间。
  • 按需分配:只有在进程真正访问某个地址时,才为之分配物理页框。
  • 写时复制fork()系统调用时,父子进程共享同一个物理页,直到某方写入时才复制。

两级页表结构

80386使用二级页表。32位线性地址被分为三部分:

31              22  21              12  11               0
+------------------+------------------+------------------+
|   页目录索引(10位) |    页表索引(10位)  |    页内偏移(12位) |
+------------------+------------------+------------------+
  • 页目录(Page Directory):一张4KB的表,包含1024个页目录项(PDE),每个指向一个页表。页目录的物理地址存储在CR3寄存器中。
  • 页表(Page Table):同样4KB,包含1024个页表项(PTE),每个指向一个4KB的物理页框。
  • 页内偏移:12位,在4KB页内定位具体字节。

线性地址(32位)

高10位[31:22]
页目录索引

中10位[21:12]
页表索引

低12位[11:0]
页内偏移

CR3 → 页目录表

页目录项PDE
→ 页表物理地址

目标页表

页表项PTE
→ 物理页框号

物理页框号 << 12

物理地址

上图说明:线性地址通过两级查表(页目录+页表)最终映射到物理地址。页目录表和页表都存放在内存中,CR3寄存器保存页目录表的物理地址,每次进程切换时CR3会被更新。页目录和页表项的低12位用于存储标志位(如存在位P、读写位R/W、用户/超级位U/S等)。

恒等映射(Identity Mapping)

Linux 0.11在head.s中建立的是恒等映射——即线性地址X被映射到物理地址X。这大大简化了初始化过程:开启分页后,内核代码的地址没有发生任何变化,所有指针依然有效。后续的内存管理代码会在此基础上建立更复杂的映射关系。

3.2.3 为什么必须从实模式切换到保护模式?

3.2.3.1 兼容性的“原罪”

如前所述,CPU上电时默认进入实模式,这是为了兼容古老的BIOS和DOS程序。BIOS代码是16位的,运行在实模式下,依赖中断向量表和1MB地址空间。如果CPU一启动就进入保护模式,BIOS根本无法运行,计算机也就无法完成最基本的硬件初始化。

这就是“原罪”——为了兼容过去,我们必须在起步时选择落后的方式,然后自行切换到先进的方式。

3.2.3.2 保护模式带来的“超能力”

切换到保护模式后,Linux才能发挥它的全部潜力:

  • 4GB寻址:突破1MB限制,可以管理多达4GB的物理内存(实际受限于386的地址线数量——当时大部分386主板只支持16MB或更少,但理论上限是4GB)。
  • 内存保护:用户程序的崩溃不会拖垮内核。内存越界访问会触发异常,由内核捕获处理。
  • 多任务:硬件支持的进程隔离和快速切换。每个进程运行在自己独立的地址空间中。
  • 虚拟内存:每个进程看到的是连续的4GB虚拟地址空间,而物理内存可以是不连续的碎片。结合交换机制,程序可以使用比物理内存更大的地址空间。
3.2.3.3 模式切换的“前置条件”

要进入保护模式,必须完成以下准备:

开始

1. cli 关闭中断
防止切换过程中被中断干扰

2. 开启A20地址线
通过8042键盘控制器
out 0x64, 0xD1; out 0x60, 0xDF

3. 建立GDT
至少包含:空描述符
代码段描述符(32位,基址0)
数据段描述符(32位,基址0)

4. 建立IDT(可为空表)
限长=0,基址=0

5. lgdt 加载GDT基址/限长

6. lidt 加载IDT基址/限长

7. 设置CR0.PE=1
mov eax, cr0; or eax, 1; mov cr0, eax
或等价的 lmsw ax

8. 执行段间跳转 jmpi 0,8
清空指令预取队列
CS加载为选择子0x0008

进入保护模式
开始执行32位代码

上图说明:进入保护模式并非一步到位,必须按顺序完成这8个步骤。每一步都有其必要性:关闭中断是为了防止切换过程中被干扰;开启A20是为了访问1MB以上内存;GDT和IDT是保护模式下寻址和中断处理的基础;最后设置CR0.PE并执行段间跳转,CPU才正式切换模式。

  1. 关闭中断cli):在切换过程中,任何中断都可能导致不可预知的结果——因为中断向量表在保护模式下不再有效。
  2. 开启A20地址线:通过键盘控制器(8042)的P21引脚开启A20,让第21根地址线正常工作。
  3. 建立GDT:至少需要定义代码段和数据段的描述符。
  4. 建立IDT:至少需要加载一个空表,防止意外中断导致崩溃。
  5. 加载GDTR和IDTR:使用lgdtlidt指令。
  6. 设置CR0.PE = 1mov eax, cr0; or eax, 1; mov cr0, eax(或等价的lmsw ax)。
  7. 执行段间跳转jmpi 0, 8):清空指令预取队列,用保护模式规则重新取指。

3.3 结合实例:用Bochs观察启动过程

理论说完了,我们来看一个真实的实验。你可以用Bochs模拟器(一个开源的x86 PC模拟器)来观察整个引导过程,就像在显微镜下观察细胞分裂一样。

3.3.1 实验环境准备

Bochs是一款老牌的x86模拟器,它能够模拟从8086到Pentium的各种CPU,并且提供了强大的调试功能——单步执行、断点、内存查看、寄存器查看等。Linux 0.11的开发社区通常提供一个配置好的Bochs实验环境。

3.3.2 观察点1:CPU的初始状态

在Bochs调试器中,使用info cpur命令可以查看CPU寄存器的初始状态。你会看到:

  • eip = 0xFFFFFFF0
  • cs = 0xF000
  • 其他寄存器大多为0

这是因为CPU复位后,CS被设置为0xF000,EIP被设置为0xFFFFFFF0,组合起来正好指向BIOS ROM的入口。

3.3.3 观察点2:BIOS加载引导扇区

0x7C00处设置断点:pb 0x7c00(物理地址断点)。然后执行c(continue)。当CPU执行到0x7C00时,断点触发。此时可以:

  • u 0x7c00反汇编引导扇区的代码。
  • x /256bx 0x7c00查看引导扇区的十六进制内容。
  • 确认最后两个字节是0x55 0xAA

3.3.4 观察点3:模式切换的关键时刻

setup.s中,执行lmsw ax指令前,用info cpu查看CR0的值,PE位应为0。执行后(stepnext),再次查看,PE位变为1。

然后观察jmpi 0, 8执行前后CS寄存器的变化——执行前CS可能还是实模式下的值(如0x9020),执行后CS变为0x0008(选择子),其隐藏部分的基址变为0。

3.3.5 实验小结

通过Bochs的单步调试,你能亲眼目睹从实模式到保护模式的每一步变化。这比任何教科书都来得直观。如果你有兴趣,甚至可以修改bootsect.s中的消息字符串,重新编译并生成新的镜像,观察Bochs启动时显示你修改的内容——这会让你对引导过程有更深的掌控感。

3.4 小结:从“混沌”到“秩序”的第一课

我们这一章虽然没有写一行内核代码,但我们已经为后续的代码解析打下了坚实的基础。你现在应该清楚:

  • BIOS扮演了“启动总管”的角色。它负责上电自检(POST)、初始化硬件、建立中断向量表、扫描扩展ROM、寻找可启动设备,最后将引导扇区加载到0x7C00并跳转执行。

  • 实模式是CPU的“初始模式”。它只有1MB寻址空间,采用“段基址:偏移量”的寻址方式(物理地址 = 段基址 × 16 + 偏移量),没有任何内存保护。但它能运行BIOS和老DOS程序,是兼容性的基石。

  • 保护模式是操作系统所需的“现代模式”。它提供4GB寻址空间、通过GDT/LDT实现的段级保护、4个特权级的隔离机制、以及可选的分页机制(虚拟内存)。进入保护模式需要一系列准备:开启A20、建立GDT/IDT、设置CR0.PE=1、执行段间跳转。

  • 从实模式到保护模式的切换是Linux 0.11引导过程中最关键的环节之一,由setup.s负责完成。它必须在BIOS中断还可用的时候收集硬件参数,然后在精确的时序下完成模式切换。

  • 分页机制(恒等映射)是Linux 0.11内存管理的基石。head.s在物理内存0x00000x4FFF处建立页目录和四个页表,将前16MB线性地址一对一映射到物理地址,然后通过设置CR0.PG位开启分页。

BIOS加载
bootsect.s
至0x7C00

bootsect.s
自搬移+加载
setup.s和system

setup.s
收集硬件参数
切换到保护模式

head.s
建立分页机制
跳转至main()

init/main.c
内核初始化开始

上图说明:整个引导流程清晰明了——从BIOS到bootsect.s,再到setup.s,最后到head.s,每一棒都完整地传递给下一棒。每一棒的代码都是为特定的运行模式(实模式或保护模式)而设计的,模式切换是其中最惊险的环节。

现在,让我们按下那个电源键……故事刚刚开始。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐