Lab 1:PC,启动
Lab1:PC,启动
万物起源
开机自检
按下开机键之后,究竟发生了什么?
CPU 在电源稳定后,完成内部寄存器的初始化。然后,便要开始执行第一条指令。
第一条指令在哪里?是干什么的?
工欲善其事,必先利其器。计算机在开始正式工作前,也得先进行一番“自检”和初始化。负责这些的程序,称作 BIOS (Basic Input/Output System)。其存储在特定的 ROM 芯片中,确保断电也不会消失。
BIOS is a type of firmware used to provide runtime services for operating systems and programs and to perform hardware initialization during the booting process (power-on startup). BIOS 是在通电启动阶段执行硬件初始化,以及为操作系统提供运行时服务的固件。
加载系统
BIOS 识别了那么多固件,究竟怎么知道从哪里加载操作系统的代码呢?
答案很朴素——挨个试。BIOS 会遍历每一个磁盘,逐个尝试将该磁盘的主引导扇区加载到内存中的 0x7c00
处,然后查看末尾的两个字节是否为 0x55 0xaa
。如果找到了,那就跳转到 0x7c00
这个位置,然后执行刚刚加载进来的启动代码。计算机的控制权从此交给了主引导扇区。
主引导扇区(Master Boot Record, MBR)磁盘的 0 号柱面,0 号磁头,0 号扇区对应的扇区,有 512 字节的大小,末尾两个字节约定为
0x55 0xaa
要是 BIOS 遍历完了所有设备,发现都是不可启动的,那便会报错“找不到启动设备”。
主引导扇区中的代码是干啥的?
主引导扇区中的代码称作“加载程序”(Bootloader),它负责启动操作系统:
- 将操作系统的代码和数据从磁盘加载到内存中
- 跳转到操作系统的起始地址
至此,一切造物的工已经完毕,无疑之日已至
法度已立
实模式
从通电到运行 Bootloader,所有的指令都是直接交由 CPU 运行,也都是直接存储在了物理内存中。这就带来了两个问题:
- CPU 必须完整运行完一个程序,才会切换到下一个程序。
- 一个程序具有完全的访问物理内存的能力,甚至可以修改别的程序的代码。 不难发现,这时的计算机和早期计算机并无二致,没有虚拟化和并发的能力,所有的程序具有相同的执行权限。这种运行模式,被称为“实模式”。
8086 完全运行在实模式下,有 16 位的寄存器,16 位的数据总线,20 位的地址总线。
- 段寄存器:CS(Code Segment), DS(Data Segment), SS(Stack Segment), ES
- 状态和控制寄存器:FLAGS, IP
访存为直接访问物理内存,物理地址 = 段寄存器 << 4 + 偏移地址(IP)。因为 IP 是 16 位的,每个段最多可以存储
保护模式
为了解决实模式的问题,保护模式应运而生。但为了兼容性,实模式得以保留。x86 处理器采用了这样的机制:计算机启动时,CPU 工作在实模式,由 Bootloader 完成由实模式向保护模式的切换。
- 关闭中断
- 打开 A20 数据总线
- 加载
GDTR
- 设置
CR0
的 PE 位(第 0 位)为0x1
- 通过长跳转设置
CS
进入保护模式 - 初始化
DS, ES, GS, SS
为了实现更大规模、更灵活的寻址,保护模式下维护了一张“全局描述符表”,用来存放内存段的段首物理地址、段地址大小、段权限等信息,并通过段选择子来选择对应的段。

全局描述符表是谁负责创建?
Bootloader 负责将全局描述符表从磁盘上加载到内存中,并将 GDT 表的物理地址和表长界限赋值给 GDTR
寄存器。
设备交互
保护模式下,该如何与外设交互?
之前为外设保留的内存正是用来干这件事的。只需要在内存的特定区域写入数据,外设(例如显示设备)就会将写入的数据进行对应的处理。至于写入的格式是什么?那就是驱动程序的工作了。
如何读写磁盘?
虽然固态硬盘逐渐普及,很多来自机械硬盘的概念还是得到了保留,磁盘的从大到小被划分为:
- 硬盘:由多张平行堆叠的磁盘组成,每个磁盘配有一个磁头(或两个,有些硬盘的磁盘上下两面均可存储信息),所有磁头同步运动。
- 磁盘:类似光盘,由若干圈同心圆构成,一个同心圆就是一个磁道。
- 柱面:垂直方向上同样大小的同心圆构成的圆柱,可以被同步运动的磁头同时读取到。柱面是磁盘分区的最小单位。
- 磁道:单张磁盘上的一个同心圆。
- 扇区:构成磁道的若干条弧线,每个扇区中存储的数据大小一般为 512 字节。扇区是磁盘读写数据的最小单位。 因此,硬盘容量 = 柱面数(磁道数)* 磁头数* 扇区数 * 扇区数据大小(512 字节) 磁盘读写通过设置端口并获取端口返回实现:
void waitDisk(void) { // 在 0x1F7 端口等待磁盘响应
while((inByte(0x1F7) & 0xC0) != 0x40);
}
void readSect(void *dst, int offset) { // 读取磁盘对应扇区
int i;
waitDisk();
// 将需要读取的扇区地址分段存入指定端口
outByte(0x1F2, 1);
outByte(0x1F3, offset);
outByte(0x1F4, offset >> 8);
outByte(0x1F5, offset >> 16);
outByte(0x1F6, (offset >> 24) | 0xE0);
outByte(0x1F7, 0x20);
waitDisk();
// 等待读取完毕即可从 0x1F0 端口流式获取数据
for (i = 0; i < SECTSIZE / 4; i ++) {
((int *)dst)[i] = inLong(0x1F0);
}
}
实验程序
os.img
包含两个扇区:
- 主引导扇区中有一个引导程序,由
start.s
和boot.c
编译链接得到。start.s
在物理内存0x7c00
处接过 BIOS 的接力棒,承担从实模式到保护模式的转换,随即跳转至boot.c
中的bootMain(void)
函数处。boot.c
负责将app.s
编译得到的、位于第一扇区的程序加载到内存的0x8c00
处,并跳转运行。
- 第一扇区中有
app.s
编译得到的程序,会在屏幕中显示hello, world
,用以模拟操作系统。