Appearance

Lab 1:PC,启动

byml2025-02-28OS

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 位 CPU)

8086 完全运行在实模式下,有 16 位的寄存器,16 位的数据总线,20 位的地址总线。

  • 段寄存器:CS(Code Segment), DS(Data Segment), SS(Stack Segment), ES
  • 状态和控制寄存器:FLAGS, IP

访存为直接访问物理内存,物理地址 = 段寄存器 << 4 + 偏移地址(IP)。因为 IP 是 16 位的,每个段最多可以存储 216=64 KB 的数据。

保护模式

为了解决实模式的问题,保护模式应运而生。但为了兼容性,实模式得以保留。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.sboot.c 编译链接得到。
    • start.s 在物理内存 0x7c00 处接过 BIOS 的接力棒,承担从实模式到保护模式的转换,随即跳转至 boot.c 中的 bootMain(void) 函数处。
    • boot.c 负责将 app.s 编译得到的、位于第一扇区的程序加载到内存的 0x8c00 处,并跳转运行。
  • 第一扇区中有 app.s 编译得到的程序,会在屏幕中显示 hello, world,用以模拟操作系统。