1. 说明
切确地说,Linux内核启动之前的过程并不属于Linux进程管理的部分,但是了解这部分内容对于从全局上学习操作系统也是很有帮助的,所以我把这部分内容也放在了Linux进程管理的分类里面,作为整个故事的起源,可以更好地帮助我梳理整个Linux进程启动到消亡的过程。
本篇笔记涉及到grub,BIOS等内容本身也是十分复杂和庞大的软件,但是不属于我准备研究的重点,所以对这些内容只是作个宽泛地说明,了解个大概就好,后续如果有机会再专门研究看看吧。
注:文中的启动流程基于Linux v5.12版本和x86体系结构的处理器。
2. 平台引导流程
从按下电源键开始,到Linux start_kernel,这么长的时间内其实大部分都是运行在BIOS和grub引导程序中的,真正涉及到Linux内核的部分很少,而前两者并不仅仅针对Linux系统,而是所有系统都可通用的。所以与其说是Linux引导流程,我觉得叫平台引导流程更贴切些。
2.1 总体流程
- 按下开关键
- 上电后RIP寄存器内容被设置为0xFFFF0,指向1MB内存的最后16bytes(此时系统进入实模式,最大寻址为1MB)
- BIOS启动运行
- 启动POST自检程序
- 读取CMOS Flash
- 查找操作系统的引导盘 读取MBR内的boot.S文件,并跳转
- GRUB引导程序启动
- stage1:boot.S(MBR) -> diskboot.S -> startup.S ->
grub_main() in main.c - grub_main() -> … -> grub_show_menu -> show_menu
-> grub_menu_execute_entry -> grub_parser_execute / grub_command_execute -> grub_linux_boot -> code32_start
- 进入Linux代码
- Linux/arch/x86/boot/header.S -> arch/x86/boot/main.c ->
arch/x86/boot/pm.c -> arch/x86/boot/pmjump.S - arch/x86/boot/compressed/head_32.S -> extract_kernel()
- arch/x86/kernel/head_32.S: initial_code -> i386_start_kernel ->
start_kernel() - init/main.c: start_kernel() -> 内核启动
- Linux/arch/x86/boot/header.S -> arch/x86/boot/main.c ->
2.2 RIP寄存器
RIP寄存器是Intel x86架构下提供的指令寄存器,用于存放指令地址和程序计数。全称为instruction pointer register。
这里RIP被设置为0xFFFF0,是由硬件线路固定的,表示CPU下一步从该地址获取指令运行,而这个地址对应的是BIOS ROM的起始地址,从这里开始,平台跳转到BIOS开始执行。
2.3 BIOS阶段
BIOS一般是一个固定在硬件主板上的ROM,由BIOS开发商开发的FW被烧录到该器件内。此时BIOS ROM上的内容会被映射到内存(0xFFFF0的地址)中,也就是说对CPU而言仍然是按照内存寻址的方式去获取指令。
这部分运行流程如下:
- 启动自检程序
- 寻找显示设备,这部分内容在地址0xC000中 寻找其他设备的ROMs,比如IDE/ATA 磁盘在0xC8000地址处。
- 点亮启动屏幕
- 初始化内存
- 初始化外设,比如COM和LPT端口等
- 检测并配置即插即用设备
- 在屏幕上显示系统基本配置信息
- 根据CMOS的引导顺序,搜索引导设备,这里主要是硬盘和硬盘分区
- 在CHS=001的位置上寻找MBR
- 将MBR的内容拷贝到0x7C00内存位置(该地址在boot.S文件中规定)
- 跳转到0x7C00,grub引导程序开始执行
流程图如下所示:
注:上述BIOS运行流程根据开发商和平台的不同,可能略有不同。
CMOS Flash
CMOS Flash 芯片存放着一些在BIOS运行阶段用到的数据信息,这些数据是可改的,比如BIOS Setup下的配置等。该芯片的内容本身是易失的,一旦掉电,数据将恢复默认值,BIOS的运行也将恢复到出厂时默认的流程。
通常硬件主板会有一个纽扣电池用于给CMOS供电,保证数据稳定,这也就是为什么有的时候再重启系统不能解决问题的时候,会有人建议扣掉纽扣电池,完全放电再试一下。
2.4 GRUB阶段
grub其实只是bootloader的一种,Linux系统通常使用LILO或者GRUB,RedHat从7.2版本开始就是用GRUB作为默认的启动引导程序。不同的发行版Linux有可能会使用不同的引导程序。
grub引导程序是独立于Linux Kernel的,所以这部分代码也不在Linux kernel的源代码内。但是会被包含在发行版的系统(.iso)内,所以当我们光盘或者USB安装完整操作系统时,是自带了grub程序的。
grub有几个重要的文件,stage1,stage2。stage1很小,通常是1024字节。stage1通常位于主引导扇区里面,对于硬盘就是MBR了,stage1的主要功能就是装载第二引导程序(stage2)。这第二引导装载程序实际上是引出更高级的功能, 以允许用户装载入一个特定的操作系统。在GRUB中,这步是让用户显示一个菜单或是输入命令。由于stage2很大,所以它一般位于文件系统之中(通常是boot所在的根分区)。
这部分运行流程如下:
stage1:
- BIOS拷贝MBR(stage1) boot.S到0x7C00,并跳转到给地址;
- 拷贝diskboot.S镜像到buffer GRUB_BOOT_MACHINE_BUFFER_SEG (0X7000),并跳转到该地址;
- 拷贝buffer到kernel_address GRUB_BOOT_MACHINE_KERNEL_ADDR(0x8000),并跳转到该地址;
- 拷贝startup.S镜像到地址0x8200,并跳转到该地址;
- 此时进入startup.S,调用EXT_C(grub_main),进入stage2
stage2:
- main.c
- grub_machine_init()
- grub_load_modules() -> grub_dl_load_core() -> grub_dl_call_init() -> mod -> init(mod)
- grub_register_core_commands()
grub_load_config() -> grub_parser_execute - grub_load_normal_mode() -> grub_dl_load(“normal”) -> grub_command_execute(“normal” ) -> cmd -> func(cmd, argc, argv) -> grub_cmd_normal()
- grub_enter_normal_mode() -> grub_normal_execute(config) -> read_config_file(config) / grub_show_menu()
- commands/boot.c
- GRUB_MOD_INIT(boot) -> grub_register_command(“boot”, …) -> grub_register_command_prio() -> grub_command_list -> grub_cmd_boot()
- GRUB_MOD_FINI(boot)
- GRUB_MOD_INIT(linux) -> grub_cmd_linux()
- grub_cmd_linux() -> grub_loader_set(grub_linux_boot, …) -> grub_loader_boot_func -> grub_loader_loaded=1 -> loaded = 1
- GRUB_MOD_FINI(boot)
- menu.c
- show_menu() -> run_menu() -> e = grub_menu_execute_entry -> grub_scripte_execute_sourcecode / grub_cmd_execute(“boot”, …)
- grub_scripte_execute_sourcecode(entry->sourcecode) -> parsed_script = grub_script_parse(line) -> grub_script_execute(parsed_script) -> grub_script_execute_cmd(script->cmd) -> cmd -> exec(cmd)
- grub_cmd_execute(“boot”, …) -> grub_cmd_boot() -> grub_loader_boot() -> grub_loader_boot_func() -> grub_linux_boot() -> state.eip = params -> code32_start -> grub_relocator32_boot()-> grub_relocator_prepare_relocs() -> grub_cpu_relocator_jumper() -> restart = rels0; -> asm volatile(“cli”) -> ((void () (void)) relst) () --> code32_start --> 0x100000
- 进入Linux Kernel 下的 header.S
其中stage1的流程图如下所示:
2.5 Linux引导阶段
grub stage2最后会根据平台的不同跳转到Linux系统的引导文件中,比如x86架构下文件在linux/arch/x86/boot目录下。
- header.S:
code32_start: .long 0x100000 # 0x100000 = default for big kernel
call main # Jump to C code (should not return)
- main.c: main(void)
copy_boot_params();
…
go_to_protected_mode()
- pm.c : go_to_protected_mode(void)
realmode_switch_hook()
…
protected_mode_jump(boot_params.hdr.code32_start, (u32)&boot_params + (ds() << 4));
- pmjump.S : protected_mode_jump
.byte 0x66, 0xea # ljmpl opcode
jmpl *%eax # Jump to the 32-bit entrypoint
- compressed/head_32.S:
call extract_kernel /* returns kernel location in %eax */
xorl %ebx, %ebx
jmp *%eax
- compressed/misc.c: extract_kernel()
__decompress(...)
- x86/kernel/head_32.S:
call *(initial_code)
SYM_DATA(initial_code, .long i386_start_kernel)
- x86/kernel/head32.c:
asmlinkage __visible void __init i386_start_kernel(void)
start_kernel();
至此,引导程序结束,Linux内核开始运行,start_kernel()函数是各架构通用的入口函数。