分析:x86 从加电到进入操作系统
实验要求
【题】请说明计算机从系统加电,引导扇区执行,内核加载并转到内核入口开始执行的过程,请比较 Haribote OS,XV6,uCore 三种自制系统的过程并体会它们的异同
开机跳线
给电源上电之后,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。
次引导加载程序
以 GRUB(多重操作系统启动管理器)为例,这个阶段的任务是加载 Linux 内核,存放在 /boot
目录。一旦次引导加载程序被加载到内存中后,便会显示 GRUB 的图形界面,在该界面中用户可以通过上下方向键选择需要加载的操作系统。
GRUB 会装载用户选择的操作系统,如果选择了 Linux,将会
跳转到 arch/x86/boot/header.S
中的_start 开始执行。
_start 处是一个手写的短跳指令,跳转到 start_of_setup 处执行。
start_of_setup 行为为:
- 重置磁盘控制器
- 设置 C 语言运行环境
- 保证%es == %ds
- 设置堆栈%ss:%sp 指向正确位置
- 调整%cs == %ds
- 将 BSS 段清零
- 检查签名
- 跳转到 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
void main(void)
{
/* First, copy the boot header into the "zeropage" */
copy_boot_params();
/* Initialize the early-boot console */
console_init();
if (cmdline_find_option_bool("debug"))
puts("early console in setup code\n");
/* End of heap check */
init_heap();
/* Make sure we have all the proper CPU support */
if (validate_cpu()) {
puts("Unable to boot - please use a kernel appropriate "
"for your CPU.\n");
die();
}
/* Tell the BIOS what CPU mode we intend to run in. */
set_bios_mode();
/* Detect memory layout */
detect_memory();
/* Set keyboard repeat rate (why?) and query the lock flags */
keyboard_init();
/* Query Intel SpeedStep (IST) information */
query_ist();
/* Query APM information */
#if defined(CONFIG_APM) || defined(CONFIG_APM_MODULE)
query_apm_bios();
#endif
/* Query EDD information */
#if defined(CONFIG_EDD) || defined(CONFIG_EDD_MODULE)
query_edd();
#endif
/* Set the video mode */
set_video();
/* Do the last things and invoke protected mode */
go_to_protected_mode();
}
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
寄存器中的 PG
和 PE
启用分页:
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 的位置,而是次引导加载程序的位置。
参考文献
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)
xv6 系统引导过程分析 | Zhengyu Zhang (freemandealer.github.io)
[系统启动流程 - deepin Wiki](https://wiki.deepin.org/wiki/ 系统启动流程)