计算机加电之后发生了什么?

开机跳线

给电源上电之后,ATX 电源将会进入待机状态。电源与主板以及一些耗电外设(如硬盘、光驱)连接,以为其供电。在电源的待机状态,会给 I/O 芯片、南桥和开机排针提供 +5V 电压。

在主板上,有一排针脚用于开机、重启、指示灯供电等,这些针脚与机箱相连。将其中的 PWRBTN#(电源)与 GND 连接到机箱开关,并按下开关,这两个针脚会发生短接。在这一瞬间,将产生 POWRBT_IN 信号。

I/O 芯片收到 POWRBT_IN 信号,送出 POWRBT_OUT 信号到南桥芯片。南桥送出 SLP3 信号给 I/O 芯片。I/O 收到 SLP3 之后送出 PSON 信号。电源的 PSON 变成低电平,于是开始输出 3.3V 5V 12V 等电压。电压稳定后,ATX 输出 PW-OK 信号。

其中 +VTT_CPU 送给 CPU,CPU 反馈 VTT_PWRGD 信号。VRM 收到 VTT_PWRGD,送出 VCORE。

南桥的电压和时钟正常后,发出 PLTRST#,PCIRST# 给各个设备。北桥收到 PLTRST# 后,经过一段延迟,送出 CPURST# 信号。CPU 收到 CPURST# 信号,开始执行第一条指令(此时 CS=0xffff0000, EIP=0xfff0,第一条指令位于 0xffff fff0,位于 BIOS 中),此时 CPU 位于实模式。

从 0xffff0,CPU 进行跳转,到 0xf000:e05b。之后 CPU Reset 通过北桥、南桥,寻找 BIOS,生成片选信号。开启 POST(开机自检)。

开机自检

POST

POST 程序位于 BIOS 中。BIOS 会检查 CPU 各项寄存器、计数器、中断控制器、DMA 控制器状态。

POST自检过程大致为:CPU-ROM-BIOS-System Clock-DMA-64KB RAM-IRQ-显卡等。

检测显卡以前的过程称过关键部件测试。如果关键部件有问题,计算机会处于挂起状态,习惯上称为核心故障。

另一类故障称为非关键性故障,检测完显卡后,计算机将对64KB以上内存、I/O 口、软硬盘驱动器、键盘、即插即用设备、CMOS设置等进行检测,并在屏幕上显示各种信息和出错报告。

在现代主板中,往往有 Quick Boot 功能,可以跳过自检。

初始化

针对 DRAM,芯片组,显卡和外围寄存器初始化和检查。

记录和存储设置

记录系统设置值,并存储在 Non-Volatile RAM 中(如 CMOS)。

常驻程序初始化

将中断服务程序等运行时程序放到内存的某个位置。

启动

BIOS 依次遍历设置中的启动设备。读取其第一扇区(设备最开始的 512 字节,MBR),载入内存,放在 0x0000~0x7c00,检查扇区最后两个字节是不是 55 aa,这两个字节是启动设备魔数,表示该设备可启动。

加载 MBR 的 Bootloader

由于空间只有 512 字节,能做的事情有限,一般用于拷贝 OS (或者用户程序,如次引导加载程序 GRUB)到内存,然后再跳转到 OS 的代码执行。这样的程序称为 Bootloader。

img

次引导加载程序

以 GRUB(多重操作系统启动管理器)为例,这个阶段的任务是加载 Linux 内核,存放在 /boot 目录。一旦次引导加载程序被加载到内存中后,便会显示GRUB的图形界面,在该界面中用户可以通过上下方向键选择需要加载的操作系统。

GRUB 会装载用户选择的操作系统,如果选择了 Linux,将会

跳转到 arch/x86/boot/header.S 中的_start开始执行。

_start处是一个手写的短跳指令,跳转到start_of_setup处执行。

start_of_setup行为为:

  1. 重置磁盘控制器

  2. 设置C语言运行环境

    • 保证%es == %ds

    • 设置堆栈%ss:%sp指向正确位置

    • 调整%cs == %ds

    • 将BSS段清零

  3. 检查签名

  4. 跳转到C代码main函数(此函数最后一步切换到保护模式)

内核

对于 Linux 系统:

(1)内核映像首先会检测系统中的硬件设备,包括内存、CPU、硬盘等,对这些设备进行初始化并配置。

(2)内核映像是经过压缩的,接下来它要对自身进行解压,同时加载必要的设备驱动。

(3)初始化与文件系统相关的虚拟设备,如 LVM 或者软件 RAID 等。

(4)装载根文件系统(/),把根文件系统挂载到根目录下。

(5)完成引导后,Linux内核会在其进程空间内加载 init 程序(/sbin/init,所有的进程都是由它所衍生),并把控制器交给 init 进程,由 init 进程继续完成接下来的系统引导工作。

当 init 进程获得控制权后,它首先会执行 /etc/rc.d/rc.sysinit 脚本,根据脚本中的代码配置环境变量、配置网络、启用Swap、检查并挂载文件系统、执行其他系统初始化所必须的步骤等。

Linux 如何切换到保护模式

通过 go_to_protected_mode 函数实现。

arch/x86/boot/main.c

 1void main(void)
 2{
 3	/* First, copy the boot header into the "zeropage" */
 4	copy_boot_params();
 5
 6	/* Initialize the early-boot console */
 7	console_init();
 8	if (cmdline_find_option_bool("debug"))
 9		puts("early console in setup code\n");
10
11	/* End of heap check */
12	init_heap();
13
14	/* Make sure we have all the proper CPU support */
15	if (validate_cpu()) {
16		puts("Unable to boot - please use a kernel appropriate "
17		     "for your CPU.\n");
18		die();
19	}
20
21	/* Tell the BIOS what CPU mode we intend to run in. */
22	set_bios_mode();
23
24	/* Detect memory layout */
25	detect_memory();
26
27	/* Set keyboard repeat rate (why?) and query the lock flags */
28	keyboard_init();
29
30	/* Query Intel SpeedStep (IST) information */
31	query_ist();
32
33	/* Query APM information */
34#if defined(CONFIG_APM) || defined(CONFIG_APM_MODULE)
35	query_apm_bios();
36#endif
37
38	/* Query EDD information */
39#if defined(CONFIG_EDD) || defined(CONFIG_EDD_MODULE)
40	query_edd();
41#endif
42
43	/* Set the video mode */
44	set_video();
45
46	/* Do the last things and invoke protected mode */
47	go_to_protected_mode();
48}

Linux 如何切换到 64 位模式

对应源码位于 arch/x86/kernel/head_64.S

首先我们需要设置 MSR 中的 EFER.LME 标记为 0xC0000080

    movl    $MSR_EFER, %ecx
    rdmsr
    btsl    $_EFER_LME, %eax
    wrmsr

在这里我们把 MSR_EFER 标记(在 arch/x86/include/uapi/asm/msr-index.h 中定义)放到 ecx 寄存器中,然后调用 rdmsr 指令读取 MSR 寄存器。在 rdmsr 执行之后,我们将会获得 edx:eax 中的结果值,其取决于 ecx 的值。我们通过 btsl 指令检查 EFER_LME 位,并且通过 wrmsr 指令将 eax 的数据写入 MSR 寄存器。

下一步我们将内核段代码地址入栈(我们在 GDT 中定义了),然后将 startup_64 的地址导入 eax

    pushl    $__KERNEL_CS
    leal    startup_64(%ebp), %eax

在这之后我们把这个地址入栈然后通过设置 cr0 寄存器中的 PGPE 启用分页:

    movl    $(X86_CR0_PG | X86_CR0_PE), %eax
    movl    %eax, %cr0

然后执行:

lret

指令。记住前一步我们已经将 startup_64 函数的地址入栈,在 lret 指令之后,CPU 取出了其地址跳转到那里。

这些步骤之后我们最后来到了64位模式

Windows 如何切换到 64 位模式

简言之,通过 X64_Start 函数实现。

Haribote OS,XV6,uCore

uCore

uCore 引导程序主体由 boot 文件夹下的 bootasm.S 和 bootasm.c 共同组成。

  • bootasm.S:负责从加电时默认的实模式切换到32位保护模式

  • bootmain.c:负责将kernel内核部分从磁盘中读出并载入内存

而 bootloader 会将 ucore 的 kernel 内核完整地加载至内存,并通过ELF文件头中指定的 entry 入口跳转至内核入口,即 /kern/init.c 中的 kern_init 函数。

XV6

XV6 从 entry.s 开始启动。xv6 的 bootloader 为一级加载,即直接加载内核到内存。相关文件:bootasm.S 、bootmain.c。前者完成切换保护模式的任务,同时初始化C语言运行时。后者完成读取磁盘扇区寻找并加载内核的过程。

Haribote OS

tyfkda/haribote: Haribote OS for Linux/gcc (github.com)

Bootloader 装载到 0x7c00。 OS 装载到 0x8000。Haribote OS 切换到保护模式是通过 asmhead.nas 的代码实现。不支持 64 位。

可以发现,uCore 和 XV6 都是采用简单的一级引导方式。而 Windows/Linux 等现代系统采用的是多级引导的方式,即 Bootloader 跳转到的并非 OS 的位置,而是次引导加载程序的位置。

参考文献

电脑主板开机流程 - 百度文库 (baidu.com)

X86下Linux的启动过程 - kp_liu - 博客园 (cnblogs.com)

视频模式初始化和转换到保护模式 · Linux ­Insides­中文 (gitbooks.io)

x86体系结构下Linux-2.6.26启动流程 (ustc.edu.cn)

32位程序下调用64位函数——进程32位模式与64位模式切换 - HsinTsao - 博客园 (cnblogs.com)

32位進程注入64位進程 - 菜鳥學院 (noobyard.com)

内核解压之后的首要步骤-Linux 内核揭密 (cntofu.com)

ucore操作系统学习(一) ucore lab1系统启动流程分析 - 小熊餐馆 - 博客园 (cnblogs.com)

3.9 XV6 启动过程 - 知乎 (zhihu.com)

xv6系统引导过程分析 | Zhengyu Zhang (freemandealer.github.io)

系统启动流程 - deepin Wiki

最近发布

要查看全部文章,请点击右上角“归档”,下面是最近发布的几篇日志