计算机是如何启动的?

一、boot的含义

在英语中,启动的英文单词是boot,这与它原本的意思-靴子似乎有很大区别,其实这来源于一句谚语:

1
"pull oneself up by one's bootstraps"

这句谚语的意思是通过鞋带把自己拉起来,这是一个不可能的事情,而计算机启动也如同这个一样是一件很复杂的事,首先我们都知道,计算机的运行离不开程序,但程序是要在计算机启动之后才能够运行的,但是计算机的启动又需要一个程序把硬盘中的操作系统加载到

内存[^1]中,这是一个相互矛盾的过程。

那么在RAM里什么都没有的时候,是哪个程序来完成对操作系统加载的任务呢,答案是Legacy BIOS(Basic Input/Output System) UEFI,前者是传统的启动程序,现代计算机大都以后者为主。

二、通过BIOS实现计算机启动

2.1 BIOS初始化硬件并加载主引导扇区

就如前面所说,在计算机启动之前是无法运行程序的,那是基于软件角度考虑的,既然软件走不通,那就只能走硬件方向了。在计算机通电之后,CPU的复位电路将其初始化为实模式(16位)[^2],并从固定地址0xFFFF0(传统BIOS的入口点)开始执行第一条指令。

BIOS启动后,首先会进行POST(Power-On Self-Test),也就是硬件自检,计算机会检验硬件(如内存,硬盘等)是否能正常工作。

2.2 BIOS读取MBR(主引导记录)

首先我们要明白一点,BIOS没有能力把整个操作系统加载到内存去,BIOS只能够把放在MBR中的引导记录加载到内存执行,而具体怎么加载操作系统,则由各个操作系统的生产厂商所制定。也就是说,BIOS只加载并执行引导代码,而引导代码自行加载整个操作系统。

BIOS会读取磁盘的前512字节,也就是MBR,如果这512个字节的最后两个字节是0x55和0xAA,表明这个设备可以用于启动;如果不是,表明设备不能用于启动。

1
2
3
4
分区表
1-446字节:BIOS控制权移交给的第一阶段引导程序。
447-510字节:分区表(Partition table)(将硬盘分为多个分区,最多只有4个主分区,每个分区16字节)。
511-512字节:主引导记录签名(0x55和0xAA)。

2.3硬盘启动

这时,计算机的控制权就要转交给硬盘的某个分区了,这里又分成三种情况。

2.3.1 卷引导记录

第一阶段引导程序会定位并加载Volume boot record,缩写为VBR),VBR通常包含第二阶段引导程序,第二阶段引导程序会负责在磁盘上找到操作系统内核,进而加载操作系统。

2.3.2 扩展分区和逻辑分区

要引导的操作系统位于逻辑分区 里。随着硬盘越来越大,4个分区已经不够了,需要更多的分区。但是分区表只有4项,因此规定有且只有一个分区可以被定义成“扩展分区”(Extended Partition)。所谓“扩展分区”,就是指这个区里面又分为好多区。这种分区里面又有分区就叫做“逻辑分区”(Logical Partition)。扩展分区包含一个或多个逻辑分区。
计算机首先读取扩展分区的第一个扇区(即 EBR),这里包含一张 64 字节的分区表,但最多只能记录 2 个逻辑分区的信息。

若存在更多逻辑分区,计算机会继续读取第二个逻辑分区的第一个扇区(其内部也包含 EBR),从该分区表中找到第三个逻辑分区的位置。

依此类推,后续每个逻辑分区的 EBR 都会指引下一个逻辑分区的位置,直到某个逻辑分区的 EBR 中仅记录自身信息(即不再指向其他分区),整个逻辑分区链才结束。

2.3.3启动管理器

计算机读取“主引导记录”前面446字节的机器码后,不再把控制权交给某一个分区,而是运行事先安排好的“启动管理器”程序(比如 GRUB(用于Linux系统) 或 Windows Boot Manager (用于Windows系统),这意味着第二部分引导程序启动了。

至此,系统引导结束,操作系统内核登场。

2.4 加载操作系统内核(以Linux0.11为例)

对于Linux0.11操作系统而言,计算机将分3批次逐次加载操作系统的内核代码,第一批由BIOS 加载 MBR(含 bootsect.s)至 0x07C00,第二批,第三批在bootsect的指挥下,分别把其后的4个扇区^3和240个扇区加载到内存。

2.4.1 bootsect.s

1.首先bootsect要把自身从0x07C00的位置复制到0x90000这个位置。

2.下面bootsect会将setup程序加载到内存中,这个操作通过BIOS提供的int 0x13中断向量^4所指向的中断服务程序^5来完成。

3.加载system模块,system模块就是内核模块,包含库模块lib、内存管理模块mm、内核模块kernelmain.chead.s程序。

2.4.2 setup.s

2.4.2.1 将system模块移动至内存起始位置
1
2
3
4
5
6
7
8
9
10
11
12
do_move:
mov es,ax ! destination segment
add ax,#0x1000
cmp ax,#0x9000
jz end_move
mov ds,ax ! source segment
sub di,di
sub si,si
mov cx,#0x8000
rep
movsw
jmp do_move

这段程序的作用是将位于0x10000的内核程序移动至起始位置0x00000处(原BIOS中断响应表和中断响应程序存放处),这样做达到一个‘’破旧立新“的效果,为建立32位的操作系统做准备。

2.4.2.2 建立IDT(中断描述符表)^6和GDT(全局描述符表)^7

GDT是保护模式[^8]下管理段描述符的数据结构,对操作系统自身的运行以及管理调度进程有着重大意义。此时由于内核尚未真正运行起来,所以还没有进程,所以现在创建的GDT第一项为空,第二项为内核代码段描述符,第三项为内核数据段描述符其余为空。

IDT虽然已经设置,但由于中断服务程序已被关闭,实际为一张空表。

2.4.2.3 开启A20地址线

打开A20,意味着CPU可以进行32位寻址,最大寻址空间为4GB。

值得注意注意的是:A20地址线并不是打开保护模式的关键,只是在保护模式下,不打开A20地址线,你将无法访问到所有的内存。

2.4.3 head.s

2.4.3.1 初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pg_dir:
.globl startup_32
startup_32:
movl $0x10,%eax
mov %ax,%ds
mov %ax,%es
mov %ax,%fs
mov %ax,%gs
lss stack_start,%esp
call setup_idt
call setup_gdt
movl $0x10,%eax # reload all the segment registers
mov %ax,%ds # after changing gdt. CS was already
mov %ax,%es # reloaded in 'setup_gdt'
mov %ax,%fs
mov %ax,%gs
lss stack_start,%esp
xorl %eax,%eax
1: incl %eax # check that A20 really IS enabled
movl %eax,0x000000 # loop forever if it isn't
cmpl %eax,0x100000
je 1b

pg_dir标识内核分页机制完成后的内核起始位置,也就是物理内存的起始位置,head程序马上要在这里建立页目录表,为分页机制做准备,这一点非常重要,是内核能够掌握用户进程的基础之一。

1
2
3
mov	ax,#0x0001	! protected mode (PE) bit
lmsw ax ! This is it!
jmpi 0,8 ! jmp offset 0 of segment 8 (cs)

setup程序通过上述代码将CPU工作方式设为保护模式,在保护模式下,一个最重要的特征就是要根据GDT决定后续执行哪里的程序。

2.4.3.2 建立新的IDT

以下是中断描述符的组成

image-20250731214307154

1
2
3
4
5
Offset:对应中断服务程序的段内偏移地址。
Selector:所在段选择符。
DPL:描述符特权级。
P:段存在标志。
TYPE:段描述符类型。

创建IDT表是重建保护模式下中断服务体系的开始,程序先让所有的中断描述符默认指向ignore_int这个位置(将来main函数里面还要让中断描述符对应具体的中断服务程序),之后还要对IDT寄存器的值进行设置,这种初始化操作,可以防止无意中覆盖代码或数据引起的逻辑混乱,以及对开发中的误操作给出及时的提示。

2.4.3.3 建立新的GDT

为什么要废除原来的GDT而重新设计一套GDT呢?因为原来GDT所在的位置是设计代码时在setup.s里面设置的数据,将来这个setup.s 所在的内存位置会在设计缓冲区时被覆盖掉,如果不改变位置,将来GDT的内容就会被缓冲区覆盖掉。

2.4.3.4 开启分页

head程序先要将页目录表和4个页表放在物理内存的起始位置,如下图:

image-20250731221501848

head程序设置完页目录表后,Linux0.11在保护模式下支持的最大寻址地址为0xFFFFFF(16MB),此处将第4个页表(由pg3指向的位置)的最后一个页表项(pg3+4902指向的位置)指向寻址范围的最后一个页面,即0xFFF000开始的4KB字节大小的内存空间。

然后从高地址向低地址方向填写4个页表,依次指向内存从高地址向低地址方向的各个页面,下图为首次设置页表:

2.png

head.s 程序执行结束后,其内存如下:

png

2.5 加载内核流程总结

image-20250731223314332

三、通过UEFI实现计算机启动

BIOS最初是为运行在8086处理器的IBM PC设计的,地址空间局限在1MB, UEFI(统一可扩展固件接口)是 BIOS 的替代方案,具有更强大的功能(如支持更大硬盘、图形化界面、安全启动等)。

与传统的BIOS不同,UEFI不依赖于BIOS的引导代码,UEFI从GPT磁盘上的ESP(EFI System Partition) 查找EFI application(通
常是bootloader) 。UEFI根据boot entries,选择EFI application(比如 GRUB(用于Linux系统) 或 Windows Boot Manager (用于
Windows系统)),并启动这个程序加载操作系统内核。

如果想进一步了解UEFI,推荐阅读这篇文章

四、参考

https://www.ruanyifeng.com/blog/2013/02/booting.html

Linux内核启动(1,0.11版本)启动BIOS与加载内核_linux添加内核启动标签-CSDN博客

Linux内核启动(2,0.11版本)内核启动前的苦力活与内核启动-CSDN博客

(x86)电脑是怎么开机的?_哔哩哔哩_bilibili

[^1]: 准确来讲是RAM(Random Access Memory):随机读取存储器,内存就是一种常见的RAM

[^2]: 实模式(RealMode)是Intel80286和之后的80x86兼容CPU的操作模式(应该包括 8086)。实模式的特性是一个20位的存储器地址空间(2^20=1048576,即1MB的存储器 可被寻址),可以直接软件访问BIOS以及周边硬件,没有硬件支持的分页机制和实时多任务概念。从80286开始,所有的80x86CPU的开机状态都是实模式;8086等早期的CPU只有 一种操作模式,类似于实模式。实模式类似 “无门禁的房间”,只能访问 1MB 内存,且程序可直接操作硬件(风险高),就像早期计算机 “裸奔” 状态。

[^8]: 类似 “带门禁的大楼”,支持 32 位 / 64 位寻址(可访问更大内存),通过权限管理限制程序对硬件的直接操作(更安全),现代操作系统均运行在此模式。