1. System Boots Up

第一课 Sector and MBR


计算机的启动需要操作系统的支持,而操作系统通常放在非易失的辅存中。因此,我们在学习系统启动之前,先来了解一下最常见的辅存——磁盘的构成。BIOS的启动顺序如下:系统会先检查软盘驱动器,然后硬盘驱动器。之后是光盘驱动器、USB设备、最后是网络(PXE)。

1.1 Memory Architecture

在计算机中,计算机的存储结构通常是分层设计的。通过这些不同的层次使得计算机的存储同时具有高速度和大容量的特征。计算机的分层结构通常由以下几部分组成:

  1. 寄存器(Register) : 以字节为单位。速度最快,存储容量最小。也是最接近CPU的存储结构,是CPU内部的一部分。
  2. 高速缓存(Cache) : 以KB-MB为单位。速度次之,位于寄存器和主存之间的存储结构。和寄存器一样基于SRAM技术。
  3. 主存(Main Memory) : 主存通常以GB为计量单位。是计算机CPU可以直接读取的存储结构。计算机的取指令、译码、执行、写回就是建立在主存之上的,是不可或缺的存储结构。
  4. 辅助存储器(Auxiliary Memory) : 如硬盘驱动器(HDD)和固态驱动器(SSD),它们的容量通常从几百GB到几TB不等。在辅助存储器中存储的数据是非易失的(Non-volatile)。

memory hierarchy.png

1.2 Hard Disk Storage

本系列,我们主要讨论从磁盘启动的方式。

1.2.1 Physical Structure of a Disk

磁盘读写数据的方式非常简单。一个磁盘由多个盘片组成,数据存放在盘片 (Platter) 表面的一圈圈同心圆上,这些同心圆称为磁道 (Track)。半径相同的磁道组成的圆柱被称为柱面 (Cylinder)。然而,单独使用磁道来存储信息并不实际,因为每个磁道的信息量太大。因此,我们需要进一步划分磁道。

我们将磁道按一定角度划分成若干扇形,这些扇形与磁道的相交面称为扇区 (Sector/Disk block)。为了管理的方便,有时候我们还会将多个扇区组合在一起形成一个簇 (Cluster),扇区一遍是最小的数据存储单元。一般而言,扇区的大小为512字节。

早期的磁盘每个磁道的扇区数量都是一样的,这就意味着半径越小的磁道位密度越大。而现在扇区不再按以前的方式划分了,现在磁盘的位密度都是一样大的,半径越大的磁道存储的数据越多。这就意味着外面磁道的扇区数要大于里面磁道的扇区数。

Pasted image 20241214172755.jpg

1.2.2 Logical Structure of a Disk

磁盘逻辑结构有两种:CHS模式LBA模式。CHS(Cylinder-Head-Sector)是早期的硬盘数据寻址方式,直接反应硬盘的物理结构。数据的位置由柱面、磁头和扇区这三个参数确定。例如,数据存储在某个具体的柱面上,该柱面由特定的磁头访问,而数据具体位于该磁道上的某个扇区内。

另一种逻辑结构是LBA(Logical Block Addressing)。相比CHS,LBA可以很好地应对HDD和SSD这两种存储介质。LBA方式下,硬盘被视为一个连续的逻辑块序列,每个块包含固定数量的字节。每个逻辑块都有一个唯一的编。当系统需要访问硬盘上的数据时,它只需指定数据块的LBA编号即可。

CHS的磁头号和柱面号都是从0开始,而扇区号从1开始。LBA模式下,第1个扇区又是从0开始。它们有以下的对应法则:其中 表示磁盘中磁头的数量, 表示每个磁道上扇区的数量。

1.3 Master Boot Record

主引导记录是硬盘最开始的那个扇区(是1号还是0号取决于寻址方式),MBR 对于整个 bootstrap 过程十分重要。其中有如下需要注意的点:

  1. MBR是这个特殊扇区的名字。
  2. MBR中包含了一段小的代码,被称为启动代码(bootloader)。这段代码负责引导过程中加载操作系统。当计算机启动时,BIOS(基本输入输出系统)会首先读取并执行MBR中的启动代码。
  3. MBR的魔数(magic number):在MBR的末尾,有一个特定的值(通常是 0x55AA)作为有效性标志。这个标志用来表示该MBR是有效的,可以被BIOS识别。
  4. 一般地,MBR占用1个扇区512字节,即使里面的代码不足512字节,也要用0进行填充。

第二课 BIOS and Booting


2.1 Basic Input Output System

BIOS 是计算机系统中非常基础且关键的部件,它是一个固件(firmware),嵌入在计算机主板的一个芯片中。一旦计算机通电(或按下 reset 复位),CPU的PC寄存器就会写入 BIOS 中起始地址,即CPU执行的第一条指令来自 BIOS。BIOS 指令的地址是预定义的,有硬件电路完成,再计算机初始化的过程中就会把这个预定义的地址交给 PC 寄存器。

2.2 Legacy Boot and UEFI

我们将传统的计算机启动方式和现代计算机启动方式区别开来。上述提到的 BIOS 属于传统 bootstrap 的范畴。而现代操作系统大多使用 UEFI。

2.2.1 Legacy Boot

Legacy Boot 使用 BIOS 作为固件接口,其启动流程包括加电、执行 BIOS 指令、电源自检 (POST)、加载主引导记录 (MBR),然后加载 Bootloader,将操作系统内核加载到内存中,并最终初始化系统完成启动。这种方式依赖于MBR 分区表,而 MBR 对磁盘容量的支持较为有限(最大为 2TB),且分区数量较少(最多 4 个主分区)。BIOS 详细的启动流程如下:

  • 加电:当计算机通电后,电源供应器(PSU)开始工作,向主板和其他组件提供电力。
  • BIOS启动:CPU从预定义的地址开始执行第一条指令,这条指令位于BIOS(基本输入输出系统)中。BIOS是存储在主板上的固件。
  • 电源自检(POST):BIOS执行电源自检(Power-On Self Test),检查硬件设备(如内存、键盘、显示器等)是否正常工作。如果检测到错误,BIOS会发出错误信号(如蜂鸣声)或显示错误信息。
  • 加载MBR:BIOS完成自检后,会寻找启动设备(如硬盘、SSD、光盘等)。找到启动设备后,BIOS会将主引导记录(MBR)加载到内存的 0x7C00H 处。MBR是硬盘的第一个扇区,包含启动加载程序(Bootloader)和分区表。
  • 执行Bootloader:MBR中的Bootloader代码接管控制权。Bootloader的任务是加载操作系统内核。它会读取硬盘上的操作系统内核文件,并将其加载到内存中。
  • 内核初始化:操作系统内核被加载到内存后,开始执行。内核会初始化操作系统的各个子系统(如内存管理、进程管理、文件系统等),并启动系统服务和驱动程序。
  • 启动完成:内核初始化完成后,操作系统进入用户模式,显示登录界面或桌面,系统启动过程结束。

booting process.png

2.2.2 UEFI

UEFI(统一可扩展固件接口)是现代操作系统的主流选择。UEFI 的启动流程和BIOS很相似,相当于 BIOS 的 plus pro 版本。UEFI 改进了 BIOS 的功能,还扩展了其启动流程,提供了更丰富的启动界面功能,包括鼠标和键盘操作,甚至还支持启动时的网络连接。

UEFI 采用 GUID 分区表 (GPT),大幅提高了对磁盘容量(支持超过 2TB)和分区数量(最多 128 个或更多)的支持。此外,UEFI 还引入了安全启动机制,可校验引导加载器和操作系统的数字签名,增强系统的安全性,防止恶意软件的篡改。

相比 BIOS,UEFI 最重要的升级是支持 GUID 分区表,而 BIOS 支持的是 MBR。

第三课 DIY a Bootloader


3.1 Real Mode

3.2 bootloader

第四课 GRand Unified Bootloader


GRUB 是一个开源的多引导加载器(Multiboot),是计算机启动时运行的第一个软件程序。GRUB 常用于多操作系统环境中,它负责选择加载某个操作系统内核并将控制权转交给内核,内核随后初始化操作系统的其余部分。

GRUB(Grand Unified Bootloader)的历史始于1995年,最初旨在支持GNU Hurd操作系统。它由Erich Boleyn开始开发,为了解决PC启动方法的不兼容性问题,他设计了多引导规范。1999年,Gordon Matzigkeit 和 Yoshinori K. Okuji 将 GRUB 官方纳入 GNU 项目。

4.1 GRUB Legacy

GRUB 2 是一个重要的后续版本,提供了更多的功能和改进,尽管带上了版本号,但大家通常还是简称为 GRUB。为了区别,将老的 GRUB 称为 GRUB legacy,版本号停留在2005年的 v0.97。相比 GRUB 2,GRUB legacy 缺乏对 UEFI 的支持。

4.2 GRUB Interface

在GRUB中,你可以选择不同的操作系统。GRUB界面是字符模式,一般长这个样子:

Pasted image 20240406183909.png

我们通过阅读 GRUB legacy 源码片段,再深入了解一下 bootloader 的工作原理。

  1. 假设:启动盘中安装了Linux,GRUB被安装在启动盘的最前面几个扇区。
  2. 电脑加电后,BIOS会找到MBR,将这个扇区的可执行指令加载到内存起始地址0x7C00处,这是一个事先约定好的地址。至此,GRUB的 stage1加载完毕,CPU开始执行0x7C00处的指令。
/*stage1.s*/
	jmp after _BPB /*第一条指令*/
	...
	after _BPB:
	boot_drive_check:
		...
		testb $0x80, %dl /*通过测试dl寄存器是否为0x80来判断是否从硬盘启动*/
		...
		/*check if LBA is supported*/
		mov $0x41, %ah
		movw $0x55aa, %bx
		int $0x13
		/*
		BIOS中断号:0x13
		功能号:0x41
		参数:0x55aa
		功能:查询扩展的磁盘访问功能
		返回结果:如果成功且bx寄存器返回相同魔数0x55aa,则说明支持LBA
		*/
		...
	lba_mode:
	/*为启动盘加载stage2代码做准备工作,比如从磁盘起始扇区、扇区数等*/
		movl 0x10(%si), %ecx
		movw $ABS(disk_address_packet), %si
		movb $1, -1(%si)
		movl $ABS(stage2_sector), %ebx
		movw $0x0010, (%si)
		movw $1, %2(%si)
		movl %ebx, 8(%si)
		movw $STAGE1_BUFFERSEG, 6(%si)
		xorl %eax, %eax
		movw %ax, 4(%si)
		movl %eax, 12(%si)
		
		movb $0x42, %ah
		int $0x13
		...
		/*
		BIOS中断号:0x13
		功能号:0x42
		参数:%dl = 驱动器编号
			 %si = offset of disk address packet
			 (DiskAddressPacket这个结构包含了要读取的扇区数、内存中缓冲区地址以及起始扇区号)
		返回结果:读取成功%al寄存器设置为0x0,否则为错误代码
		*/
		
	stage2_address:
		.word 0x8000
		/*boot stage2*/
		jmp *(stage2_address)
  1. stage2 的代码数据加载在内存的0x8000,打开stage2部分的代码发现绝大部分是用C语言而非汇编,我们可以理解成stage1阶段已经设置了指令执行的基本环境,并且因为第二阶段的功能更为复杂,使用高级语言编写会降低开发难度。
    Pasted image 20240827160217.png

4.3 0x7C00

MBR(主引导记录)位于内存地址 0x7C00 后的 512 字节,而不是从 0x0000 到 0x01FF 的原因是历史和兼容性的考虑。在早期的 IBM PC 中,使用的 8088 处理器在启动时会将MBR加载到 0x7C00 这个地址。这是因为 8088 处理器需要在内存的前面部分(0x0000~0x03FF)保留用于中断向量表。此外,操作系统需要尽可能多的连续内存空间,所以MBR被放置在内存的尾部,即 0x7C00 地址,这样可以为操作系统留出更多的内存空间。为了兼容 8088,后续的 CPU 继续使用这个地址。

简单来说,计算机启动时,BIOS 会检查硬件,然后根据指定的顺序检查引导设备的第一个扇区(即MBR),并将其加载到内存地址 0x7C00 。MBR随后将控制权交给操作系统。这个过程确保了操作系统能够获得足够的内存空间,并且与早期的硬件保持兼容。为什么主引导记录的内存地址是0x7C00?

第五课 Inturrupt Vector Table, IVT*


中断向量表(IVT)和BIOS的INT中断调用之间有直接的联系。在实模式下,BIOS利用IVT来处理中断请求。IVT是一个位于内存低地址的表,通常从0x00000开始,它包含了256个中断向量,每个向量指向一个中断服务例程(ISR)的地址。当中断发生时,CPU会使用中断号来索引IVT并跳转到相应的ISR执行中断处理。

BIOS提供了一系列预定义的中断服务例程,这些例程可以通过软件中断调用(如INT 0x13用于磁盘操作,INT 0x10用于视频服务等)来访问。这些中断服务例程是实模式下与硬件交互的基本方法,允许操作系统和其他程序在不直接操作硬件的情况下执行诸如读取磁盘、显示字符等操作。

在保护模式下,中断描述符表(IDT)取代了IVT,但BIOS的INT调用仍然可以在实模式下使用。在系统启动时,BIOS会设置IVT,并在需要时响应中断请求,直到操作系统接管并可能设置自己的中断处理机制。

第六课 BIOS Memory Layout


实模式下内存低1MB的地址空间都有什么?

Pasted image 20240924013452.png
[Memory Map (x86) - 实模式内存映射](https://wiki.osdev.org/Memory_Map_(x86)

第七课 MBR/GPT*


磁盘分区表:硬盘上存储分区信息的一个结构,它告诉计算机磁盘上有那些分区以及每个分区的起始和结束位置。这个表对操作系统来说非常重要,因为它用来读取、识别和管理磁盘上的数据。有几种不同的分区表类型,常见的包括MBR(主引导记录)和 GPT(GUID分区表)。

7.1 MBR

MBR 位于硬盘的最开始部分,它包含了引导代码和分区表。MBR 的大小通常为 512 字节,其中的bootloader占据446字节,分区表占据 64 字节,最后两字节是魔数 0x55AA 。

  • 分区表有4个表项,每个表项16字节。那么最多支持多少个分区?最多4个主分区:分别是C盘、D盘、E盘、F盘。
  • 但是一般可能会有3个主分区,1个扩展分区。这里请注意,主分区可以是4个、3个、2个或1个,但扩展分区在MBR下最多只能有1个。
  • 逻辑分区是包含在扩展分区里面,逻辑分区的数量不受限制。

0212511f66bbc3e3312972413e91a87c.webp

每个分区表中包含了一个分区的属性数据,主要有:

  1. 启动指示器(1字节)、起始磁头(1字节)、起始扇区和柱面(2字节)、分区类型(1字节)、结束磁头(1字节)、结束扇区和柱面(2字节)起始逻辑扇区(4字节)、分区内扇区总数(4字节)
  2. 启动指示器中 0x80 表示活动分区
  3. 分区类型标识有:ext4/fat/ntfs...
  4. 高亮部分表示CHS模式寻址
  5. 斜体加粗部分表示LBA模式寻址
  6. 这种分区表的属性(逻辑扇区用4字节表示)决定每个分区最大容量为:

7.2 GUID

与 MBR 相比,GPT 是一个更现代的分区方案,它支持大于 2TB 的磁盘,并且可以创建多达128个分区。GPT 位于磁盘的开始和结束部分,提供冗余,以防主 GPT 损坏。GPT 使用全局唯一标识符(GUID)来标识分区,这意味着每个分区的标识符在全世界范围内都是唯一的。大多数现代操作系统(例如 Windows 10、Linux、macOS )都支持 GPT。但是,使用 GPT 可能需要 UEFI 固件,而不是传统的 BIOS 。

  • 保护MBR:位于磁盘的最开始,即第一个扇区(LBA 0)。保护MBR的目的是使不支持GPT的旧系统识别C磁盘,避免这些系统意外修改GPT磁盘。保护MBR占用1个扇区。如果系统支持GPT,则会忽略这个扇区。
  • GPT头部:磁盘的第二个扇区(LBA 1)。包含有关GPT分区表的元数据,如其位置、大小和CRC校验码。GPT头部通常也只占用1个扇区。
  • GPT分区表:跟在GPT头部之后,这是一系列分区条目,每个条目128字节。默认情况下,GPT为分区条目数组预留了128个条目,每个条目128字节,因此默认情况下分区表占用 16KB 。对于 512 字节扇区的磁盘,这将是 32 个扇区。

Pasted image 20240724122510.jpg
GPT 每个分区条目 128 字节包含以下字段:分区类型 GUID (16字节)、起唯一分区 GUID (16字节)、起始 LBA (8字节)、结束 LBA (8字节)、属性标志(8字节)、分区名称(72字节)。这种分区表属性决定了 每个分区最大容量:。此外,在磁盘的最后面还会有一组GPT的备份。

第x课 Linux Boots-Up


x.1 init vs. systemd

现阶段,引导程序加载操作系统内核,内核启动的时候,会有两种比较常见的系统启动方式:

  1. 传统的 System V init 系统,也常称为 SysVinit
  2. 新式的 systemd,即 system deamon 的缩写

x.1.1 init

init 是一个守护进程(Deamon),开机运行,关机结束。init 进程是进程号为1的进程(pid = 1),所有系统上运行的其他进程都是 init 进程所 fork() 出来的。若当 init 进程无法运行,则系统就不会允许有其他任何程序运行,这种状态也被称为 "system panic" 。

一般我们常把 init 和 SysVinit 所联系起来。但实际上根据不同的 linux distribution ,init进程可以是SysVinit、Upstart或Systemd。

x.1.2 systemd

systemd 是一种系统和服务管理器,旨在成为 Linux 系统的 init 系统的替代品。它提供了并行化启动服务、按需启动守护进程、按需挂载文件系统、快照和恢复系统状态等功能。

相比init必须一个进程接着一个串行的系统启动,systemd支持多线程并行的启动方式,提高系统的启动速度。除此之外,systemd使用依赖关系图来确定服务的启动顺序,增强了系统的稳定性,但也提高了复杂性。

x.2 Linux Boots-Up in Old Days

Pasted image 20240820022709.png

早期Linux系统开机的几个阶段:

  1. BIOS开机自检到加载内核阶段

    • 这个阶段的内容在本章已经介绍过。
    • Linux内核保存在/boot目录下。
  2. 启动 init 进程

    • 运行/sbin/init,这是 linux 系统启动运行的第一个进程,PID 为1,又叫超级进程根进程
    • init 进程负责产生其他所有的用户进程,所有的进程都被挂载到这个进程下,如果 init 进程退出,那么所有进程都会被强制杀死(即系统关机)。如果一个进程的父进程先于子进程退出,子进程就会变成孤儿进程,转而挂载到 init 进程下。
  3. init 进程读取/etc/inittab文件

    • 根据文件中设置的启动层级和执行项来启动对应的程序,inittab 文件头的部分如下:
    # Default runlevel. The runlevels used by RHS are:
    #   0 - halt (Do NOT set initdefault to this)
    #   1 - Single user mode
    #   2 - Multiuser, without NFS (The same as 3, if you do not have networking)
    #   3 - Full multiuser mode
    #   4 - unused
    #   5 - X11
    #   6 - reboot (Do NOT set initdefault to this)
    # 
    id:5:initdefault:
    
    • 0-6的文件中说明了不同的启动层级,以及设定的启动层级。3和5较为常见。
      • 0为关机;
      • 1为单用户模式(维护模式);
      • 2为无网络的多用户模式;
      • 3为普通用户登陆的字符终端模式;
      • 5为带图形界面的用户终端模式;
      • 6为重启。
        其中我们看到,真正有用的地方只有id:5:initdefault:,表示 init 要启动的进程会完成操作系统5级的功能。
  4. 执行系统初始化脚本/etc/rc.d/rc.sysinit)。

    • 这个初始化脚本是每个系统级别都要运行进行初始化。
    • 对linux系统一些必备的东西进行初始化,比如时钟,键盘,磁盘,文件系统等。其中/etc/inittab每一行代表一个执行项。
    • 执行项的格式如:id:runlevels:action:process
      id:表示该执行项的id,为符合命名规则的标识符
      runlevels:符合执行条件的运行层级,为0-6数字的组合
      action:启动进程的方式,有initdefault, wait, respawn等选项
      process:表示需要执行的进程
  5. 执行启动层级对应的脚本/etc/rc*.d)。

    • 根据第3步中所选中不同的系统层级进行的专门的初始化,上面的例子中,/etc/inittab文件中记录的运行级别5。所以init进程执行/etc/rc5.d进程,进行5运行级别必要的操作。
    • 在早期,如果有系统级的进程想要设置为开机启动,就将启动该进程的命令放在该文件中。后面有了service系统,就不需要手动更改配置文件了。
  6. 启动终端

    • rc 执行完毕后,系统环境已经设置完成,各种服务进程也已经启动。init开始启动终端程序。不同的运行级别启动不同类型的终端。然后执行 rc.local 文件。
    1:2345:respawn:/sbin/mingetty tty1
    2:2345:respawn:/sbin/mingetty tty2
    3:2345:respawn:/sbin/mingetty tty3
    4:2345:respawn:/sbin/mingetty tty4
    5:2345:respawn:/sbin/mingetty tty5
    6:2345:respawn:/sbin/mingetty tty6
    
    • 早期如果有什么用户级进程需要在系统启动时自动启动,就添加到rc.local文件中。之后有了service系统,就不需要手动更改配置文件了。

x.3 System Boots-Up using Systemd

在有些时候,我们用ps -ef查看系统上运行所有进程的信息时,我们会发现进程号为1的进程并不是systemd。这时我们打印一下/sbin/init的进程信息,会发现/sbin/init只是一个软链接文件,实际上指向的还是/lib/systemd/systemd文件。

du@DVM:/$ ps -ef | more
UID          PID    PPID  C STIME TTY          TIME CMD
root           1       0  0 16:19 ?        00:00:03 /sbin/init auto noprompt splash
du@DVM:/$ ls -l /sbin/init
lrwxrwxrwx 1 root root 20 1122  2023 /sbin/init -> /lib/systemd/systemd