文章目录
- 一、全局流程
- 二、从BIOS到GRUB
- 三、GRUB是如何启动的
- 四、详解vmlinuz文件结构
- 五、流程梳理-1
- 六、内核初始化从_start开始
- 七、流程梳理-2
- 参考资料
前言
本章节将讨论一下Linux如何去做初始化。
一、全局流程
在机器加电后,BIOS 会进行自检,然后由 BIOS 加载引导设备中引导扇区。在安装有 Linux 操作系统的情况下,在引导扇区里,通常是安装的 GRUB 的一小段程序(安装 windows 的情况则不同)。最后,GRUB 会加载 Linux 的内核映像 vmlinuz,如下图所示:
二、从BIOS到GRUB
内存在断电后就没法保存数据了,那 BIOS 又是如何启动的呢?硬件工程师设计 CPU 时,硬性地规定在加电的瞬间,强制将 CS 寄存器的值设置为 0XF000,IP 寄存器的值设置为 0XFFF0。
这样一来,CS:IP 就指向了 0XFFFF0 这个物理地址。在这个物理地址上连接了主板上的一块小的 ROM 芯片。这种芯片的访问机制和寻址方式和内存一样,只是它在断电时不会丢失数据,在常规下也不能往这里写入数据,它是一种只读内存,BIOS 程序就被固化在该 ROM 芯片里。
现在,CS:IP 指向了 0XFFFF0 这个位置,正是 BIOS 程序的入口地址。这意味着 BIOS 正式开始启动。
BIOS 一开始会初始化 CPU,接着检查并初始化内存,然后将自己的一部分复制到内存,最后跳转到内存中运行。BIOS 的下一步就是枚举本地设备进行初始化,并进行相关的检查,检查硬件是否损坏,这期间 BIOS 会调用其它设备上的固件程序,如显卡、网卡等设备上的固件程序。当设备初始化和检查步骤完成之后,BIOS 会在内存中建立中断表和中断服务程序,这是启动 Linux 至关重要的工作,因为 Linux 会用到它们。
具体是怎么操作的呢?BIOS 会从内存地址(0x00000)开始用 1KB 的内存空间(0x00000~0x003FF)构建中断表,在紧接着中断表的位置,用 256KB 的内存空间构建 BIOS 数据区(0x00400~0x004FF),并在 0x0e05b 的地址加载了 8KB 大小的与中断表对应的中断服务程序。中断表中有 256 个条目,每个条目占用 4 个字节,其中两个字节是 CS 寄存器的值,两个字节是 IP 寄存器的值。每个条目都指向一个具体的中断服务程序。
Linux 通常是从硬盘中启动的。硬盘上的第 1 个扇区(每个扇区 512 字节空间),被称为 MBR(主启动记录),其中包含有基本的 GRUB 启动程序和分区表,安装 GRUB 时会自动写入到这个扇区,当 MBR 被 BIOS 装载到 0x7c00 地址开始的内存空间中后,BIOS 就会将控制权转交给了 MBR。在当前的情况下,其实是交给了 GRUB。
到这里,BIOS 到 GRUB 的过程结束。
三、GRUB是如何启动的
GRUB 的加载分成了多个步骤,同时 GRUB 也分成了多个文件,其中有两个重要的文件 boot.img 和 core.img。
其中,boot.img 被 GRUB 的安装程序写入到硬盘的 MBR 中,同时在 boot.img 文件中的一个位置写入 core.img 文件占用的第一个扇区的扇区号。而 core.img 文件是由 GRUB 安装程序根据安装时环境信息,用其它 GRUB 的模块文件动态生成。如下图所示:
如果是从硬盘启动的话,core.img 中的第一个扇区的内容就是 diskboot.img 文件。diskboot.img 文件的作用是,读取 core.img 中剩余的部分到内存中。
由于这时 diskboot.img 文件还不识别文件系统,所以我们将 core.img 文件的全部位置,都用文件块列表的方式保存到 diskboot.img 文件中。这样就能确保 diskboot.img 文件找到 core.img 文件的剩余内容,最后将控制权交给 kernel.img 文件。
因为这时 core.img 文件中嵌入了足够多的功能模块,所以可以保证 GRUB 识别出硬盘分区上文件系统,能够访问 /boot/grub 目录,并且可以加载相关的配置文件和功能模块,来实现相关的功能,例如加载启动菜单、加载目标操作系统等。
正因为 GRUB2 大量使用了动态加载功能模块,这使得 core.img 文件的体积变得足够小。而 GRUB 的 core.img 文件一旦开始工作,就可以加载 Linux 系统的 vmlinuz 内核文件了。
四、详解vmlinuz文件结构
我们在 /boot 目录下会发现 vmlinuz 文件,其实它是由 Linux 编译生成的 bzImage 文件复制而来的。
生成 bzImage 文件需要三个依赖文件:setup.bin、vmlinux.bin,linux/arch/x86/boot/tools 目录下的 build。让我们挨个来分析一下。
其实,build 只是一个 HOSTOS(正在使用的 Linux)下的应用程序,它的作用就是将 setup.bin、vmlinux.bin 两个文件拼接成一个 bzImage 文件,如下图所示:
剩下的就是搞清楚 setup.bin、vmlinux.bin 这两个文件的的结构,先来看看 setup.bin 文件,setup.bin 文件是由 objcopy 命令根据 setup.elf 生成的。
setup.bin 文件正是由 /arch/x86/boot/ 目录下一系列对应的程序源代码文件编译链接产生,其中的 head.S 文件和 main.c 文件格外重要。
vmlinux.bin 文件依赖于 linux/arch/x86/boot/compressed/ 目录下的 vmlinux 目标
vmlinux 文件就是编译整个 Linux 内核源代码文件生成的,Linux 的代码分布在各个代码目录下,这些目录之下又存在目录,Linux 的 kbuild(内核编译)系统,会递归进入到每个目录,由该目录下的 Makefile 决定要编译哪些文件。
在编译完具体文件之后,就会在该目录下,把已经编译了的文件链接成一个该目录下的 built-in.o 文件,这个 built-in.o 文件也会与上层目录的 built-in.o 文件链接在一起。
再然后,层层目录返回到顶层目录,所有的 built-in.o 文件会链接生成一个 vmlinux 文件,这个 vmlinux 文件会通过前面的方法转换成 vmlinux.bin 文件。但是请注意,vmlinux.bin 文件它依然是 ELF 格式的文件。
最后,工具软件会压缩成 vmlinux.bin.gz 文件,这里我们以 gzip 方式压缩。
piggy.S 非常简单,使用汇编指令 incbin 将压缩的 vmlinux.bin.gz 毫无修改地包含进来。
除了包含了压缩的 vmlinux.bin.gz 内核映像文件外,piggy.S 中还定义了解压 vmlinux.bin.gz 时需要的各种信息,包括压缩内核映像的长度、解压后的长度等信息。
这些信息和 vmlinux.bin.gz 文件,它们一起生成了 piggy.o 文件,然后 piggy.o 文件和 ( v m l i n u x − o b j s − y ) (vmlinux-objs-y) (vmlinux−objs−y)(efi-obj-y) 中的目标文件一起链接生成,最终生成了 linux/arch/x86/boot/compressed 目录下的 vmlinux。
五、流程梳理-1
操作系统的启动分为两个阶段:引导boot和启动startup,本节主要还是boot过程:
BIOS->GRUG1->GRUB1.5->GRUB2->Linux内核【环境硬盘引导、MBR分区】
1、按电源键,系统加电
2、主板通电
CPU加电时,会默认设置[CS:IP]为[0XF000:0XFFF0],根据实模式下寻址规则,CPU指向0XFFFF0
这个地址正是BIOS启动程序位置,而BIOS访问方式与内存一致,所以CPU可以直接读取命令并执行
3、BIOS执行
3.1、BIOS首先执行POST自检,包括主板、内存、外设等,遇到问题则报警并停止引导
3.2、BIOS对设备执行简单的初始化工作
3.3、BIOS 会在内存中:
建立中断表(0x00000~0x003FF)
构建 BIOS 数据区(0x00400~0x004FF)
加载了中断服务程序(0x0e05b~0x1005A)
3.4、BIOS根据设备启动顺序,依次判断是否可以启动
比如先检查光驱能否启动
然后依次检查硬盘是否可以启动【硬盘分区的时候,设置为活动分区】
4、硬盘引导
4.1、先说下寻址方式,与扇区编号的事情
最传统的磁盘寻址方式为CHS,由三个参数决定读取哪个扇区:磁头(Heads)、柱面(Cylinder)、扇区(Sector)
磁头数【8位】,从0开始,最大255【微软DOS系统,只能用255个】,决定了读取哪个盘片的哪个面【一盘两面】
柱面数【10位】,从0开始,最大1023【决定了读取哪个磁道,磁道无论长短都会划分为相同扇区数】
扇区数【6位】,从1开始,最大数 63【CHS中扇区从1开始,而逻辑划分中扇区从0开始,经常会造成很多误解】
每个扇区为512字节
4.2、然后说下引导方式
BIOS在发现硬盘启动标志后,BIOS会引发INT 19H中断
这个操作,会将MBR【逻辑0扇区】,也就是磁盘CHS【磁头0,柱面0,扇区1】,读取到内存[0:7C00h],然后执行其代码【GRUB1阶段】,至此BIOS把主动权交给了GRUB1阶段代码
MBR扇区为512字节,扇区最后分区表至少需要66字节【64字节DPT+2字节引导标志】,所以这段代码最多只能有446字节,grub中对应的就是引导镜像boot.img
boot.img的任务就是,定位,并通过BIOS INT13中断读取1.5阶段代码,并运行
5、Grub1.5阶段
5.1、先说一下MBR GAP
据说微软DOS系统原因,第一个分区的起始逻辑扇区是63扇区,在MBR【0扇区】和分布表之间【63扇区】,存在62个空白扇区,共 31KB。
Grub1.5阶段代码就安装在这里。
5.2、上面提到,boot.img主要功能就是找到并加载Grub1.5阶段代码,并切换执行。
Grub1.5阶段代码是core.img,其主要功能就是加载文件系统驱动,挂载文件系统, 位加载并运行GRUB2阶段代码。
core.img包括多个映像和模块:
diskboot.img【1.5阶段引导程序】,存在于MBR GAP第一个扇区;【这里是硬盘启动的情况,如果是cd启动就会是cdboot.img】
lzma_decompress.img【解压程序】
kernel.img【grub核心代码】,会【压缩存放】
biosdisk.mod【磁盘驱动】、Part_msdos.mod【MBR分区支持】、Ext2.mod【EXT文件系统】等,会【压缩存放】
其实boot.img只加载了core.img的第一个扇区【存放diskboot.img】,然后控制权就交出去了,grub阶段1代码使命结束。
diskboot.img知道后续每个文件的位置,会继续通过BIOS中断读取扇区,加载余下的部分并转交控制权,包括:
加载lzma_decompress.img,从而可以解压被压缩的模块
加载kernel.img,并转交控制权给kernel.img
kernel.img的grub_main函数会调用grub_load_modules函数加载各个mod模块
加载各个mod后,grub就支持文件系统了,访问磁盘不需要再依靠BIOS的中断以扇区为单位读取了,终于可以使用文件系统了
6、GRUB2阶段
现在grub就能访问boot/grub及其子目录了
kernel.img接着调用grub_load_normal_mode加载normal模块
normal模块读取解析文件grub.cfg,查看有哪些命令,比如发现了linux、initrd这几个命令,需要linux模块
normal模块会根据command.lst,定位并加载用到的linux模块【一般在/boot/grub2/i386-pc目录】
当然,同时需要完成初始化显示、载入字体等工作
接下来Grub就会给咱们展示启动菜单了
7、选择启动菜单
7.1、引导协议
引导程序加载内核,前提是确定好数据交换方式,叫做引导协议,内核中引导协议相关部分的代码在arch/x86/boot/header.S中,内核会在这个文件中标明自己的对齐要求、是否可以重定位以及希望的加载地址等信息。同时也会预留空位,由引导加载程序在加载内核时填充,比如initramfs的加载位置和大小等信息。
引导加载程序和内核均为此定义了一个结构体linux_kernel_params,称为引导参数,用于参数设定。Grub会在把控制权移交给内核之前,填充好linux_kernel_params结构体。如果用户要通过grub向内核传递启动参数,即grub.cfg中linux后面的命令行参数。Grub也会把这部分信息关联到引导参数结构体中。
7.2、开始引导
Linux内核的相关文件位于/boot 目录下,文件名均带有前缀 vmlinuz。
咱们选择对应的菜单后,Grub会开始执行对应命令,定位、加载、初始化内核,并移交到内核继续执行。
调用linux模块中的linux命令,加载linux内核
调用linux模块中的initrd命令,填充initramfs信息,然后Grub会把控制权移交给内核。
内核此时开始执行,同时也就可以读取linux_kernel_params结构体的数据了
boot阶段结束,开始进入startup阶段。
六、内核初始化从_start开始
下面,我们先从 setup.bin 文件的入口 _start 开始,了解启动信息结构,接着由 16 位 main 函数切换 CPU 到保护模式,然后跳入 vmlinux.bin 文件中的 startup_32 函数重新加载段描述符。如果是 64 位的系统,就要进入 startup_64 函数,切换到 CPU 到长模式,最后调用 extract_kernel 函数解压 Linux 内核,并进入内核的 startup_64 函数,由此 Linux 内核开始运行。
vmlinux.bin.gz 文件则是由编译的 Linux 内核所生成的 elf 格式的 vmlinux 文件,去掉了文件的符号信息和重定位信息后,压缩得到的。
CPU 是无法识别压缩文件中的指令直接运行的,必须先进行解压后,然后解析 elf 格式的文件,把其中的指令段和数据段加载到指定的内存空间中,才能由 CPU 执行。
这就需要用到前面的 setup.bin 文件了,_start 正是 setup.bin 文件的入口。
GRUB 将 vmlinuz 的 setup.bin 部分读到内存地址 0x90000 处,然后跳转到 0x90200 开始执行,恰好跳过了前面 512 字节的 bootsector,从 _start 开始。
16 位的 main 函数,各种初始化后,会跳到0x100000,GRUB 会把 vmlinuz 中的 vmlinux.bin 部分,放在 1MB 开始的内存空间中。通过这一跳转,正式进入 vmlinux.bin 中。
startup_32 函数,重新加载段描述符,之后计算 vmlinux.bin 文件的编译生成的地址和实际加载地址的偏移,然后重新设置内核栈,检测 CPU 是否支持长模式,接着再次计算 vmlinux.bin 加载地址的偏移,来确定对其中 vmlinux.bin.gz 解压缩的地址。
如果 CPU 支持长模式的话,就要设置 64 位的全局描述表,开启 CPU 的 PAE 物理地址扩展特性。再设置最初的 MMU 页表,最后开启分页并进入长模式,跳转到 startup_64
startup_64 函数,初始化长模式下数据段寄存器,确定最终解压缩地址,然后拷贝压缩 vmlinux.bin 到该地址,跳转到 decompress_kernel 地址处,开始解压 vmlinux.bin.gz
kernel startup_64,这是内核中开始函数,最后就是调用x86_64_start_kernel 函数,然后就是start_kernel函数了
start_kernel 函数
各种初始化,start_kernel 函数执行完成,Linux 内核就具备了向应用程序提供一系列功能服务的能力。
最后执行了arch_call_rest_init函数,第一个进程
七、流程梳理-2
Grub在/boot目录下找到的linux内核,是bzImage格式
1、bzImage格式生成:
1.1、head_64.S+其他源文件->编译-> vmlinux【A】
1.2、objcopy工具拷贝【 拷贝时,删除了文件中“.comment”段,符号表和重定位表】->vmlinux.bin【A】
1.3、gzib压缩->vmlinux.bin.gz
1.4、piggy打包,附加解压信息->piggy.o->其他.o文件一起链接->vmlinux【B】
1.5、objcopy工具拷贝【 拷贝时,删除了文件中“.comment”段,符号表和重定位表】->vmlinux【B】
1.6、head.S +main.c+其他->setup.bin
1.7、setup.bin+vmlinux.bin【B】->bzImage合并->bzImage
2、GRUB加载bzImage文件
2.1、会将bzImage的setup.bin加载到内存地址0x90000 处
2.2、把vmlinuz中的vmlinux.bin部分,加载到1MB 开始的内存地址
3、GRUB会继续执行setup.bin代码,入口在header.S【arch/x86/boot/header.S】
GRUB会填充linux内核的一个setup_header结构,将内核启动需要的信息,写入到内核中对应位置,而且GRUB自身也维护了一个相似的结构。
Header.S文件中从start_of_setup开始,其实就是这个setup_header的结构。
此外, bootparam.h有这个结构的C语言定义,会从Header.S中把数据拷贝到结构体中,方便后续使用。
4、GRUB然后会跳转到 0x90200开始执行【恰好跳过了最开始512 字节的 bootsector】,正好是head.S的_start这个位置;
5、在head.S最后,调用main函数继续执行
6、main函数【 arch/x86/boot/main.c】【16 位实模式】
6.1、拷贝header.S中setup_header结构,到boot_params【arch\x86\include\uapi\asm\bootparam.h】
6.2、调用BIOS中断,进行初始化设置,包括console、堆、CPU模式、内存、键盘、APM、显卡模式等
6.3、调用go_to_protected_mode进入保护模式
7、 go_to_protected_mode函数【 arch/x86/boot/pm.c】
7.1、安装实模式切换钩子
7.2、启用1M以上内存
7.3、设置中断描述符表IDT
7.4、设置全局描述符表GDT
7.4、protected_mode_jump,跳转到boot_params.hdr.code32_start【保护模式下,长跳转,地址为 0x100000】
8、恰好是vmlinux.bin在内存中的位置,通过这一跳转,正式进入vmlinux.bin
9、startup_32【 arch/x86/boot/compressed/head64.S】
全局描述符GDT
加载段描述符
设置栈
检查CPU是否支持长模式
开启PAE
建立MMU【4级,4G】
开启长模式
段描述符和startup_64地址入栈
开启分页和保护模式
弹出段描述符和startup_64地址到CS:RIP中,进入长模式
10、 startup_64【 arch/x86/boot/compressed/head64.S】
初始化寄存器
初始化栈
调准给MMU级别
压缩内核移动到Buffer最后
调用.Lrelocated
11、.Lrelocated
申请内存
被解压数据开始地址
被解压数据长度
解压数据开始地址
解压后数据长度
调用 extract_kernel解压内核
12、extract_kernel解压内核【 arch/x86/boot/compressed/misc.c】
保存boot_params
解压内核
解析ELF,处理重定向, 把 vmlinux 中的指令段、数据段、BSS 段,根据 elf 中信息和要求放入特定的内存空间
返回了解压后内核地址,保存到%rax
13、返回到.Lrelocated继续执行
跳转到%rax【解压后内核地址】,继续执行
解压后的内核文件,入口函数为【arch/x86/kernel/head_64.S】
14、SYM_CODE_START_NOALIGN(startup_64)【arch/x86/kernel/head_64.S】
SMP 系统加电之后,总线仲裁机制会选出多个 CPU 中的一个 CPU,称为 BSP,也叫第一个 CPU。它负责让 BSP CPU 先启动,其它 CPU 则等待 BSP CPU 的唤醒。
第一个启动的 CPU,会跳转 secondary_startup_64 函数中 1 标号处,对于其它被唤醒的 CPU 则会直接执行 secondary_startup_64 函数。
15、secondary_startup_64 函数【arch/x86/kernel/head_64.S】
各类初始化工作,gdt、描述符等
跳转到initial_code,也就是x86_64_start_kernel
16、 x86_64_start_kernel【 arch/x86/kernel/head64.c】
各类初始化工作,清理bss段,清理页目录,复制引导信息等
调用x86_64_start_reservations
17、x86_64_start_reservations【 arch/x86/kernel/head64.c】
调用start_kernel();
18、start_kernel【 init/main.c】
各类初始化:ARCH、日志、陷阱门、内存、调度器、工作队列、RCU锁、Trace事件、IRQ中断、定时器、软中断、ACPI、fork、缓存、安全、pagecache、信号量、cpuset、cgroup等等
调用 arch_call_rest_init,调用到rest_init
19、rest_init【 init/main.c】
kernel_thread,调用_do_fork,创建了kernel_init进程,pid=1 . 是系统中所有其它用户进程的祖先
kernel_thread,调用_do_fork,创建了 kernel_thread进程,pid=2, 负责所有内核线程的调度和管理
【最后当前的进程, 会变成idle进程,pid=0】
20、kernel_init
根据内核启动参数,调用run_init_process,创建对应进程
调用try_to_run_init_process函数,尝试以 /sbin/init、/etc/init、/bin/init、/bin/sh 这些文件为可执行文件建立init进程,只要其中之一成功就可以
调用链如下:
try_to_run_init_process
run_init_process
kernel_execve
bprm_execve
exec_binprm
search_binary_handler-》依次尝试按各种可执行文件格式进行加载,而ELF的处理函数为 load_elf_binary
load_elf_binary
start_thread
start_thread_common,会将寄存器地址,设置为ELF启动地址
当从系统调用返回用户态时,init进程【1号进程】,就从ELF执行了
到此为止,系统的启动过程结束。
Linux启动流程:
思维导图:
参考资料
以上内容是我学习彭东老师的《操作系统实战45讲》后所进行的一个笔记记录,如有错误,还请各位大佬多多指教。
我主要参考了以下资料,十分感谢:
操作系统实战45讲——彭东老师
评论区大佬——neohope、艾恩凝等