第03章 引导启动程序(0):从按下电源键到保护模式——揭秘BIOS、实模式与保护模式
引言:一段不到一秒的史诗
你按下电脑的电源按钮,听到风扇开始转动,看到屏幕上闪过一些白色的文字,然后操作系统缓缓升起。这一切发生在几秒之内,平淡无奇。但在这几秒里,你的计算机完成了一次从“混沌”到“有序”的奇迹般的蜕变。
如果我们把这短短几秒的时间拉长、放慢,你会看到这样一幅画面:
- 第一毫秒:CPU从某个固定的内存地址醒来,它不知道自己是谁,也不知道该做什么。它像个初生的婴儿,只记得一个指令——“去那里”。
- 第二毫秒:BIOS接管了控制,它开始四处巡视,检查内存、键盘、显卡……像一个严谨的管家,确认一切就绪。
- 第三毫秒:BIOS找到了你的硬盘或软盘,翻出第一个扇区里的“咒语”——一段只有512字节的代码。它把这段代码小心翼翼地放到内存的一个角落,然后拍了拍它的肩膀:“去吧,接下来的路,你自己走。”
- 第四毫秒:那段代码开始执行,它先把自己搬到更安全的地方,又召唤了两个同伴,最后三人联手,把CPU从古老的“实模式”带进了现代的“保护模式”。从此,计算机才能真正运行像Linux这样复杂的操作系统。
这就是引导启动程序的故事。而我们要讲的第一部分,就是故事发生之前的世界——那个计算机刚通电时的“原始状态”,以及为什么我们必须从“实模式”跳到“保护模式”。
为了让你能真正理解后续的汇编代码,我们这一章先不急着敲代码,而是把舞台的背景、规矩和角色全盘托出。相信我,当你理解了BIOS、实模式和保护模式,后面的bootsect.s、setup.s和head.s对你来说就不再是天书,而是一部你已经读过剧本的精彩戏剧。
上图的含义:整个启动过程就像一场精心编排的接力赛,从电源键按下到内核开始执行,每一步都环环相扣,缺一不可。
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会在物理内存
0x0000到0x03FF处建立一个中断向量表(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还会扫描
0xC0000到0xDFFFF的地址范围,寻找显卡、网卡等扩展卡的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的控制流已经转移到了引导扇区的代码。
上图说明:BIOS将引导扇区精确地加载到
0x7C00这个位置,这是IBM PC历史上的一个约定。它位于内存低端的32KB范围内,既足够低,又不会与中断向量表(0x00000x03FF)和BIOS数据区(0x04000x04FF)冲突。这个古老的决定,一直沿用到今天。
第四步:引导程序的三阶段
在Linux 0.11中,引导程序由三个汇编源文件组成,它们分工明确:
| 文件 | 大小 | 加载地址 | 运行模式 | 主要任务 |
|---|---|---|---|---|
bootsect.s |
512字节 | 0x7C00 → 0x90000 |
实模式 | 自我搬迁,加载setup.s和system模块 |
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语言世界。
上图说明:引导程序三阶段中,各模块在内存中的位置不断变化。
bootsect.s从0x7C00搬到0x90000,为加载setup.s腾出空间;system模块从0x10000搬移到0x00000,为内核在保护模式下运行做好准备。
形象比喻:把整个计算机启动比作一个电影的拍摄过程。
- BIOS:制片人,负责搭建场地、准备器材、选角(找到可启动设备)。
- bootsect.s:场务,负责把主要演员(
setup.s和system模块)请到片场,并把自己挪到后台。- 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)。因此,引导扇区的代码必须极其精简,只负责加载更大的后续模块。
约束三:中断向量表占用低端内存
在实模式下,物理地址0x00000到0x003FF被中断向量表占用,0x00400到0x005FF被BIOS数据区占用。因此,bootsect.s不能把system模块直接加载到0x00000,只能先放到0x10000(64KB处)。等BIOS不再需要中断向量表之后(即进入保护模式之前),再由setup.s把system从0x10000搬到最终的家0x00000。
约束四:1MB地址空间的限制
实模式下只有20根地址线(A0~A19),最大寻址1MB。但80386有32根地址线,可寻址4GB。为了兼容古老的8086程序(它们依赖地址回绕——访问1MB以上时自动回到0),IBM在主板上设计了一个逻辑门,可以屏蔽第21根地址线(A20)。上电时A20是关闭的,操作系统必须显式开启A20才能访问1MB以上的内存。这就是setup.s中那段向8042键盘控制器发送0xDF命令的由来。
这些约束叠加在一起,决定了引导过程不能是“一步到位”的,而必须是“分段接力”的。每一段代码都有明确的任务,完成后将控制权交给下一段,直到内核完全就绪。
上图说明:四大约束共同决定了引导流程必须是三段式的。每个约束都是一道“关卡”,引导程序必须逐一解决,才能让内核顺利启动。
3.2 实模式与保护模式:两座“城市”的对比
在深入代码之前,我们必须先理解CPU的两种运行模式。它们就像两座完全不同的城市:一座是古老、简单、但缺乏管制的“实模式城”,另一座是现代、复杂、但秩序井然的“保护模式城”。
上图说明:实模式和保护模式的核心区别一目了然。从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 = 0x1234,IP = 0x5678,则执行的物理地址为:
0x1234 × 16 + 0x5678 = 0x12340 + 0x5678 = 0x179B8
上图说明:实模式下的地址转换非常直接——段基址左移4位后加上偏移量。这种“段基址:偏移量”的形式,在汇编代码中经常看到,例如
mov ax, [0x1234]。
段重叠:由于段基址左移4位,两个相邻的段基址之间相差16字节(而不是64KB)。因此,同一个物理地址可以由多个不同的“段基址:偏移量”组合来表示。例如,
0x0000:0x7C00和0x07C0:0x0000都指向物理地址0x7C00。这种“段重叠”是实模式下特有的现象,也是许多底层编程技巧的源头。
3.2.1.2 实模式的“致命缺陷”:没有保护
在实模式下,所有程序都运行在同一个地址空间,没有任何内存保护机制。任何程序都可以:
- 随意读写任何内存位置(包括BIOS数据区、中断向量表)
- 执行任何CPU指令(包括特权指令)
- 访问任何I/O端口
这意味着一个程序的bug(甚至恶意代码)可以轻易地覆盖中断向量表,导致整个系统崩溃。在现代操作系统中,这种“自由”是灾难性的。
但为什么我们还要用实模式?
因为兼容性。1978年到1990年代初,无数DOS程序在实模式下编写。这些程序假设了平坦的内存模型,假设可以随意访问任何内存位置。如果Intel让80386一启动就进入保护模式,所有这些老程序将无法运行。所以,Intel选择了上电时默认进入实模式,让操作系统或引导程序自行决定何时切换到保护模式。
3.2.1.3 实模式下的“中断向量表”
在实模式下,物理内存的0x0000到0x03FF(共1024字节)被BIOS用作中断向量表(IVT)。每个中断号占用4个字节(高2字节是段地址,低2字节是偏移量),总共支持256个中断(从int 0x00到int 0xFF)。
当CPU执行int N指令(或硬件触发中断N)时:
- 将标志寄存器FLAGS压栈
- 清除IF(中断使能)和TF(陷阱)标志
- 将当前CS和IP压栈
- 从中断向量表读取第N个条目(地址 = N × 4)
- 将读取到的段地址和偏移量分别加载到CS和IP
- 从新的CS:IP处继续执行
BIOS在上电时会填充许多中断向量,为操作系统提供基本的硬件访问接口。引导程序正是通过调用这些BIOS中断来完成磁盘读取、屏幕显示、内存查询等操作的。
重要提示:进入保护模式后,中断向量表不再有效。保护模式下使用中断描述符表(IDT),它的格式完全不同。这就是为什么
setup.s在进入保护模式前必须用cli关闭中断——在IDT建立之前,任何中断都会导致CPU崩溃。
3.2.2 保护模式:一座秩序井然的现代化都市
3.2.2.1 保护模式的设计目标
保护模式随Intel 80286(1982年)首次引入,在80386(1985年)中得到了极大的完善。80386的保护模式是一个32位的运行环境,其核心设计目标包括:
-
更大的寻址空间:32根地址线,理论上最大可寻址4GB物理内存。对于1991年的PC来说,这简直是天文数字——当时主流配置是2~4MB。
-
内存保护:每个内存段都有独立的访问权限(读、写、执行),并且分为4个特权级(Ring 0~3)。操作系统运行在最高特权级(Ring 0),用户程序运行在最低特权级(Ring 3)。任何越权访问都会触发CPU异常,由操作系统处理。
-
虚拟内存支持:通过可选的分页机制,允许每个进程拥有独立的地址空间。配合“缺页异常”处理,可以实现比物理内存更大的虚拟地址空间(交换到磁盘)。
-
多任务硬件支持:通过任务状态段(TSS),CPU可以在硬件层面保存和恢复任务的执行状态,实现快速任务切换。
3.2.2.2 寻址方式的根本变革:段选择子与描述符表
在保护模式下,段寄存器里存放的不再是段基址,而是一个段选择子(Segment Selector)。它像一个图书馆里的索引卡片,指向**描述符表(Descriptor Table)**中的某个条目。描述符表可以是全局的(GDT,Global Descriptor Table)或局部的(LDT,Local Descriptor Table)。
上图说明:段选择子是一个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个字节中,基址分散在三个位置(字节23、字节4、字节7),限长分散在两个位置(字节01、字节6的高4位)。这种分散布局是历史原因造成的(为了兼容80286),在代码中通常使用宏或结构体来组装。
地址转换流程:
上图说明:保护模式下的寻址不再直接使用段基址,而是通过“选择子→描述符表→描述符→基址”的间接路径。这虽然增加了复杂度,但换来了内存保护和更大的寻址空间。如果启用了分页(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 ≤ DPL 且 RPL ≤ DPL(数值越小,权限越高)。如果条件不满足,会触发“一般保护异常”(#GP,中断13)。
上图说明: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页内定位具体字节。
上图说明:线性地址通过两级查表(页目录+页表)最终映射到物理地址。页目录表和页表都存放在内存中,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 模式切换的“前置条件”
要进入保护模式,必须完成以下准备:
上图说明:进入保护模式并非一步到位,必须按顺序完成这8个步骤。每一步都有其必要性:关闭中断是为了防止切换过程中被干扰;开启A20是为了访问1MB以上内存;GDT和IDT是保护模式下寻址和中断处理的基础;最后设置CR0.PE并执行段间跳转,CPU才正式切换模式。
- 关闭中断(
cli):在切换过程中,任何中断都可能导致不可预知的结果——因为中断向量表在保护模式下不再有效。 - 开启A20地址线:通过键盘控制器(8042)的P21引脚开启A20,让第21根地址线正常工作。
- 建立GDT:至少需要定义代码段和数据段的描述符。
- 建立IDT:至少需要加载一个空表,防止意外中断导致崩溃。
- 加载GDTR和IDTR:使用
lgdt和lidt指令。 - 设置CR0.PE = 1:
mov eax, cr0; or eax, 1; mov cr0, eax(或等价的lmsw ax)。 - 执行段间跳转(
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 cpu或r命令可以查看CPU寄存器的初始状态。你会看到:
eip = 0xFFFFFFF0cs = 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。执行后(step或next),再次查看,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在物理内存0x0000到0x4FFF处建立页目录和四个页表,将前16MB线性地址一对一映射到物理地址,然后通过设置CR0.PG位开启分页。
上图说明:整个引导流程清晰明了——从BIOS到
bootsect.s,再到setup.s,最后到head.s,每一棒都完整地传递给下一棒。每一棒的代码都是为特定的运行模式(实模式或保护模式)而设计的,模式切换是其中最惊险的环节。
现在,让我们按下那个电源键……故事刚刚开始。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)