前言
由于解密固件方面的逆向需要,所以需要了解linux系统加载机制。花了我好几天来来去去缕清关系和学习,学的时候还是慎用语言大模型不然容易被它绕进去。。。
本文默认读者学习过操作系统等课堂上能学到的知识。。。不然太多了写不完啦
尝试使用通俗的语言+各个地方的截图+常见疑惑,结合来描述,写得不够详细的话请见谅,有不对的地方请一定要指点出来呀谢谢!
从CPU加电到用户态,大概流程如下(省略很多细节不过没关系,看这个图可以有个大概印象,不然刚开始接触那么多概念挺乱的记不住)
画了一张图,希望能帮到您进行理解~
第零站:内核镜像bzImage文件结构
下图展现了bzImage里有什么
其就像一个“多级推进火箭”,在内核加载的过程中,逐渐把“有效载荷”(最右侧的vmlinux)解压到内存之中
bzImage第一层:setup.bin
这部分是不进行压缩的,特别的是,此部分运行于386实模式。
其专注于最基本的硬件探测和初始化。
历史
在历史上(Linux版本<=2.4,那时候还在用软盘),直接由这一部分对内核进行内核的信息收集(如显示信息,内存信息等,通过BIOS的变量boot_params获取),以及把内核加载进内存里。
现今
但后来人们制定了32位启动协议(32-bit boot protocol),bootloader出现了,setup.bin以上职能都被取代了,其本身退化成辅助bootloader加载内核到内存里的部分。
那么,为什么不直接把setup.bin移除掉呢,这不是更省地方吗?
没有它却不太行,因为bootloader有些信息是必须要镜像来告诉它的,比如:内核是否是可重定位的,内核建议的加载地址,等等。
而setup.bin在加载前未被压缩,天然的适合作为这样一个镜像与bootloader之间的“传话筒”。而且还要照顾到不能使用32位启动协议的场合,
于是setup.bin便被保留下来了。
bzImage第二层:uncompressed部分
这部分是不进行压缩的,与前者不一样,此部分运行于保护模式(或者64位长模式)。
其专注于解压vmlinux内核镜像,以及内核重定位(如果内核可以重定位的话)
问题:为什么其不和setup.bin合并呢
有以下原因:
1.在实模式下,CPU 使用 20 位地址线,因此可寻址的内存空间最大只有 1MB (2^20 字节),0x00000 ~ 0xFFFFF 。由于在实模式下直接执行内核的初始化逻辑与内核最终运行的保护模式(或者64位长模式)有较大差异,可能会遇到一些兼容性问题
2.为了setup.bin的代码更为简洁,专注于单项功能,这样好维护
bzImage第三层:vmlinux.bin.gz(不一定是gzip也可以是别的)
顾名思义,使用了gzip压缩技术(或者lzma也可以,gzip压缩速度比较快)的部分,被uncompressed部分解压之后就是有效载荷vmlinux了
问题:为什么一定要压缩呢,直接bin不是更加直接吗
首先这算半个历史遗留问题,在Linux版本<=2.4的时候内核镜像必须能够放入软盘里,而且当时加载内核的全过程都是在实模式下的,所以当时内核镜像必须经过压缩以小于1MB
但是其实这样做也有好处所以保留了下来:gzip解压时间远远快于IO导入更大的镜像的耗时(因为是在CPU上执行解压的过程),是有利的
bzImage最内层,有效载荷:vmlinux(.bin)
这是Linux 内核的完整可执行映像。
其主要负责以下功能:
1.内存管理初始化:
- 在解压缩自身之后,vmlinux 会建立起完整的内存管理机制。
- 包括设置页表、初始化内存分配器等关键工作,为后续内核运行做好准备。
2.内核子系统初始化:
- vmlinux 会启动各种内核子系统的初始化过程。
- 如进程管理、设备驱动、文件系统等核心功能模块的初始化。
3.内核main函数执行:
- vmlinux 最终会执行内核的 main() 函数。
- 这是内核启动的最后一步,内核从此进入正式的运行状态。
那么,让我们按下电源键吧。
第一站:在CPU加电的那一刻
CPU进行了加电自检(Power-on self-test,POST),不需要太过了解,因为这属于硬件知识,只需要知道它做了这些事情:
1.初始化CPU内部寄存器和缓存
2.检查CPU功能是否正常
3.确定CPU的工作模式和性能参数
4.为操作系统启动做好基础设置
CPU被设计为只能运行内存里面的东西,RAM内存在断电时会丢失所有数据
也正是因为启动时RAM里面啥都没有,硬件工程师设计CPU时,硬性地规定了一个初始值沿用至今,
在加电的瞬间,强制将 CS 寄存器的值设置为 0XF0000,IP 寄存器的值设置为 0XFFFF0,CS 为代码段寄存器,而 IP 为指令指针寄存器
这样计算机CS:IP 就是0xFFFF0 (CS*10H+IP),cs左移4个二进制位
在这两个地址上,便开始了一切的初始化。。。
第二站α路线:BIOS+MBR引导
BIOS(Basic Input/Output System,基本输入输出系统)
CS:IP 指向了 0XFFFF0 这个位置,这正是 BIOS 程序的入口地址。
bootsect启动过程
在曾经的操作系统里面,bootsect是完成初始化任务的主要角色,后面才是grub
bootsect模块(bootsect.s编译而成,存放在主引导扇区MBR,0磁道,0磁头,第1个扇区)
实际上BIOS的启动过程中,首先是去到入口点,如下所示,其会首先执行bootsect.s,而bootsect.s首要的任务就是把自身从0x07c00开始的数据复制到0x90000物理地址
可以看到,这段x86汇编的主要功能就是设置寄存器指向原来的bootsect,以及复制数据,最后再次设置寄存器以备新的bootsect位置被使用
从图上理解可能更为直观
相信很多人和我一样都会有疑惑,为什么需要复制到这里,岂不是多此一举?
当然不是的,这是因为以下两个主要原因:
1.原本的bootsect前后都有东西,空间很小,复制出来好进行更为复杂的操作
2.根据linux系统的设计,留在0x7c00以及附近的地址是危险的,会被后面加载的system模块覆盖,当然这个设计还是保守了,放心紧接着的后面会详细讲清的
然后就是setup.s登场了
setup模块(setup.s编译而成,存放在MBR第2个到第4个扇区)
bootsect 执行完后,会跳转到内存地址 0x90200 处继续执行。
setup模块做了以下四件事情:
1.利用 BIOS 的 0x13 功能从硬盘读取当前引导设备的几何信息,ax的高8位ah表示扇区,ax的低8位al表示需要读取的扇区数量
2.利用 BIOS中断0x10功能号ah=0x13扫描字符串内容,并显示"Loading system..."字样
3.之后将硬盘上 setup 代码之后的 system 代码使用cmp加载到内存 0x10000 地址处。
4.接下来根据所保存的根文件系统设备号,以及从引导扇区获取的每个扇区的类型和大小(是 1.44M A 盘吗?)等信息,最终跳转到 setup 程序的入口(0x90200)执行 setup 程序。
之后,内存布局变成的这样子
请注意,他们的内存空间不是是连续的,这个《Linux内核完全注释》里的图有误导之嫌疑,
在 Linux 0.11 版本中, system 代码占用了 240 个扇区。而在此之后的 2630 多个扇区都未被使用。这些未使用的空闲扇区可以用来存储一个基本的文件系统,从而可以支持从引导扇区启动系统的能力。
5.setup正式开始执行
setup程序的主要任务是利用BIOS中断去读取诸多系统需要的参数,并且将这些数据保存到bootsect的地方,直接覆盖掉已经使用完的bootsect
获取的参数如下,看起来bootsect原来的位置确实够放了,这段代码比较长,都是获取参数的,此处限于篇幅不进行展示,了解一下获取的是什么就可以了
6.加载system模块(并进入保护模式(或者64位长模式))
setup程序将system代码加载到内存0x10000-0x8ffff的范围内(曾经人们认为这足以容纳整个系统代码,最大长度不会超过0x80000(512k),也即其末端不会超过内存地址0x10000+0x80000=0x90000)
所以当时的人们认为,只需要让bootsect的地址放到0x90000,那么把system 模块加载到0x00000 开始的地方便不会波及bootsect和setup。
但是这显然对于现代的操作系统来说太小了,甚至整个实模式的空间加起来都不够system用的,所以后期还是要让system进入保护模式(或者64位长模式),这样寻址空间就大了,足够让system加载进内存并执行了
。。。
这里有些内容太太偏底层了,可以了解一下A20地址线、中断描述符号表IDT、全局描述符号表GDT
A20地址线:
最后,内存布局变成了这样:
是的,在刚刚完成初始化以后,数据段和代码段描述符指针都直接指向0x00000,也就是system模块的起始点
而setup的最后一条指令是jmp 0,8,此处8为段选择符,0是偏移量,在这里指临时全局描述符表gdt,于是跳转指向了0x00000
system模块(head.s以及各种内核程序编译而成,存放在第6扇区以后)
head.s,顾名思义,是system的head。其功能比较单一,首先是加载各个数据段寄存器,再重新设置中断描述符表idt,一共256项,每个描述符项占8字节....分页其实是基础知识问题,此处不再赘述
设置中断+全局描述符的方式原来是封装好的:
实际上长这样,有兴趣可以看看,不想看其实也没有啥关系。。。
后面还有设置页表的代码,比较复杂,有兴趣可以看看《Linux内核完全注释》,
总之,我们知道了head.s的主要任务就ok,那就是在进入保护模式(或者64位长模式)的那一刻,对idt、gdt、页表等进行了初始化,以让后面的内核模块顺利使用保护模式(或者64位长模式)
此时的内存布局如图所示:
此时的DS、CS、ESI,以及全局、局部描述符表寄存器的关系如图所示,注意一个LDT对应一个任务(进程),一个GDT最多可以有64个LDT
从bootsect来看,BIOS需要做什么?
我们可以从一个系统正常运行的需求,反过来看BIOS需要做什么
最需要的东西:CPU、内存
一个系统,没有CPU或者内存,啥都干不了
BIOS首先被CPU执行起来,然后它帮助CPU首先进行一些初始化,然后就是对内存进行初始化,BIOS把一部分的自身复制到内存里,最后就是跳到内存中执行。之后就能正常使用CPU和内存这两个最核心的东西,来做更多事情了。
第二需要的东西:其余的设备
这时候BIOS就开始进行其余设备的枚举了,并且进行简单的检查,看看它们有没有坏掉,一切正常的话,然后就是设备的初始化了。
第三需要的东西,设备一切准备就绪,建立内核与用户态的桥梁
中断表和中断服务程序是为中断服务的,而中断,则是内核与用户态的桥梁。想启动linux系统,必须建立内核与用户态的随时切换机制。
1 2 3 4 5 6 7 | 0x00000 - 0x003ff :中断向量表
0x00400 - 0x004ff :BIOS数据区
0x00500 - 0x07bff :自由内存区
0x07c00 - 0x07dff :引导程序加载区
0x07e00 - 0x9ffff :自由内存区
0xa0000 - 0xbffff :显示内存区
0xc0000 - 0xfffff :BIOS中断处理程序区
|
BIOS中断表(注意与中断描述符表IDT区分,虽然确实差不多)
至今约定俗成的是,256个中断的中断表(实际上就是按顺序存放的偏移地址+段地址)必须放在0x00000~0x003FF这1KB的空间之中,放不满的地方就空着
中断表中有 256 个条目,每个条目占用 4 个字节,其中两个字节是 CS 寄存器(图中的段地址)的值,两个字节是 IP 寄存器(图中的偏移地址)的值。每个条目都指向一个具体的中断服务程序。
BIOS中断服务程序(注意与中断服务程序区分)
在紧接着中断表的位置,用 256KB 的内存空间构建 BIOS 数据区(0x00400~0x004FF),并在 0x0e05b 的地址加载了 8KB 大小的与中断表对应的中断服务程序。
为了启动外部储存器中的程序,BIOS 会搜索可引导的设备,搜索的顺序是由 CMOS 中的设置信息决定的(这也是我们平时讲的,所谓的在 BIOS 中设置的启动设备顺序)。一个是软驱,一个是光驱,一个是硬盘上,还可以是网络上的设备甚至是一个 usb 接口的 U 盘,都可以作为一个启动设备。
Linux 通常是从硬盘中启动的。硬盘上的第 1 个扇区(每个扇区 512 字节空间),被称为** MBR(主启动记录)**,其中包含有基本的 GRUB 启动程序和分区表,安装 GRUB 时会自动写入到这个扇区,当 MBR 被 BIOS 装载到 0x7c00 地址开始的内存空间中后,BIOS 就会将控制权转交给了 MBR。在当前的情况下,其实是交给了 GRUB。
到这里,BIOS 到 GRUB 的过程结束。
第二站β/γ路线:BIOS(或UEFI)+GRUB引导
Here we go again...学完前面的bootsect,其实这块就比较轻松了,因为我们只需要关注linux内核做了什么,其它的话逆向上遇不到,想看似乎也很少详细分析,就不浪费太多时间啦
UEFI统一可扩展固件接口(Unified Extensible Firmware Interface,UEFI)
UEFI 的出现就是为了代替 BIOS,BIOS已经是很多年前的产物了,一些设计显得跟不上时代了。
但是好像很多主要还是bios吧,看了一下自己的vps,确实是bios的而非uefi的。
主要是看看/boot/下的文件,以及有没有/efi/这个文件夹。对UEFI挂载点感兴趣的童鞋可以查阅https://wiki.archlinuxcn.org/wiki/EFI_%E7%B3%BB%E7%BB%9F%E5%88%86%E5%8C%BA
linux内核并不负责uefi的实现,其使用"EFISTUB"与UEFI进行通信。
可以参考Arch Linux这部分的一些细节来加深理解:https://wiki.archlinuxcn.org/wiki/Arch_%E7%9A%84%E5%90%AF%E5%8A%A8%E6%B5%81%E7%A8%8B
"UEFI 不从主引导记录(MBR)中启动任何引导代码(无论其是否存在),相反,UEFI 的启动过程依赖非易失性随机访问存储器(NVRAM)中的引导条目。"
这句话相当重要,这解答了我的一个疑惑:前面看完bootsect那些机制,怎么就没见到NVRAM的身影呢
UEFI流程
首先是Security阶段、硬件初始化阶段(PEI),在CPU加电自检后,UEFI 初始化引导所需的硬件(硬盘、键盘控制器等等),不停留在实模式,而直接进入64位长模式;
问题来了,那32位系统岂不是用不了UEFI? 比如32位操作系统在UEFI固件上运行,但UEFI固件是64位的:
此时32位系统会在兼容模式下运行。在兼容模式下,UEFI固件会模拟传统的BIOS环境,以便32位操作系统能够引导和运行。
UEFI从文件系统(FAT格式)读取\efi\boot\bootx64.efi文件,启动这个程序,
紧接着进入驱动加载阶段阶段(DXE)、启动设备选择阶段(BDS)读取 NVRAM 中的引导项,以决定要启动哪一个 EFI 应用程序,以及从哪启动(哪一个硬盘和哪一个分区),这是通过读取全局唯一标识分区表(GUID Partition Table,GUID 分区表,GPT)来实现的,UEFI 固件会扫描系统中的各个分区,并根据预定义的 Partition Type GUID 识别 EFI 系统分区(EFI System Partition,ESP)。ESP 包含了 UEFI 启动所需的关键文件。比如这样:
注意GUID是几乎“全球唯一”的,其“全局”是指全地球哦,为了标识某样东西而存在,比如swap分区就是0657FD6D-A4AB-43C4-84E5-0933C84B4F4F
最后进入操作系统预加载阶段(TSL)、操作系统加载阶段(RT) ,UEFI会为操作系统的加载做好准备,然后UEFI会把控制权交给操作系统,操作系统开始加载
如果在启动过程中出现了严重的错误,UEFI会提供错误处理和恢复机制,进入错误处理阶段(AL阶段)
注意UEFI为了保证兼容MBR启动,其仍然存储了一份传统的MBR,用来防止不支持GPT的硬盘管理工具错误识别并破坏硬盘中的数据,这个MBR也叫做保护MBR。
UEFI与BIOS异同对比
正是UEFI使用GPT磁盘的原因,其通过ESP分区来管理启动流程。这个分区里包含了可启动的 EFI 文件。
UEFI 固件甚至可以先启动某个 EFI 程序,由它再去启动其他的 EFI 程序。在这个过程中,还可以加载各种驱动程序和其他必要的组件,为操作系统的启动做好准备。这种灵活的启动机制,为 UEFI 系统带来了更强大的功能和扩展性。
BIOS的加载,如前文所述,则是环环相扣的,是线性的。
UEFI使用 64 位逻辑块地址(logical block address),可寻址的最大硬盘空间是 2 ZiB。MBR 只支持 2 TiB 的最大硬盘。
UEFi在磁盘末尾储存头部和分区表,有助于恢复分区表,而且其采用了CRC32 校验码。
UEFI的99%是用C语言编写的,更像软件。而BIOS则是用汇编语言写的,更接近硬件。
此外,由于BIOS是固定的,缺乏文档,完全基于经验和晦涩约定的一个事实标准,这就导致了其扩展性非常不好。例如,BIOS不能直接支持新的硬件设备,每当需要支持新的硬件设备时,都必须更新BIOS。而且,由于缺乏标准,不同的BIOS可能会有不同的实现方式,这就使得硬件制造商需要为不同的BIOS提供不同的驱动程序,增加了开发的复杂性。
GRUB(GRand Unified Bootloader)
这是目前最主流的引导方式,GNU GRUB 和GRUB是GRand Unified Bootloader的缩写,它是一个多重操作系统启动管理器。用来引导不同系统,如windows,linux。
其实就是启动时可以上下选择启动的系统的那个东西,这就是Grub引导
目前 GRUB 分成 GRUB legacy 和 GRUB 2。版本号是 0.9x 以及之前的版本都称为 GRUB Legacy ,从 1.x 开始的就称为 GRUB 2,如无特别说明皆指GRUB2
GRUB 在每次启动的时候加载配置文件 /boot/grub/grub.cfg
(这里initrd可以换成initramfs)
其它的部分比较熟悉,但是stage1和2是新的东西,这是什么?
Stage 1
这部分的代码存放于MBR(图没有画错,注意 程序存放于MBR和由MBR引导的区别)。
实际上这部分的主要任务是加载Stage 2,并根据配置文件显示引导菜单。
Stage 2
该程序存储在文件系统中,以模块化的方式加载。
配置加载+显示选择:
其主要任务首先是读取GRUB的配置文件,通常是grub.cfg,其中包含了引导菜单的信息。
详情见https://blog.csdn.net/Luckiers/article/details/113387209
1 2 3 4 5 | menuentry "Ubuntu" {
set root = (hd0, 1 )
linux / vmlinuz root = / dev / sda1
initrd / initrd.img
}
|
根据配置文件的指示,显示引导菜单供用户选择。
用户选择启动的系统后:
加载选定的操作系统内核+初始化RAM磁盘镜像(initrd)或者initramfs到内存。
将控制权移交给选定的操作系统内核,完成引导过程。
从镜像文件角度看stage1、stage2
具体作用可以看http://bbs.c3.wuyou.net/forum.php?mod=viewthread&tid=422869
https://hugh712.gitbooks.io/grub/content/image-files.html
stage1(上图左边蓝色):
如果启动设备是硬盘,即从硬盘启动时,core.img中的第一个扇区的内容就是diskboot.img。
如果启动设备是光驱(cd-rom),即从光驱启动时,core.img中的第一个扇区的的内容就是cdboo.img。
如果是从网络的PXE环境启动,core.img中的第一个扇区的内容就是pxeboot.img。
如果是用LILO开机,lnxboot.img可以被放置在core image的前方,这样可以使核心映像看起来与 Linux 内核很像,这样 LILO 就可以使用 "image=sector" 的方式来引导。
这四个文件的作用是一样的,即,读取 core.img 中剩余的部分到内存中。由于此时还不识别文件系统,所以将core.img的全部位置以block列表的方式编码,使得它们四个能够找到剩余的内容。
stage2:将控制权移交给选定的操作系统内核,完成引导过程
最后将控制权交给 kernel.img 文件。
与bootsect区别
其工作原理和 bootsect 有一些不同:
1.GRUB 的第一阶段代码存放在 MBR 扇区,第二阶段代码存放在磁盘的其他扇区。
2.GRUB 提供了更加灵活的引导配置,可以引导多种操作系统。
3.GRUB 支持从文件系统中加载内核,而不需要像 bootsect 那样直接从磁盘扇区加载。
第三站:文件系统初始化
终于到了这一步,离用户能够使用操作系统仅差一步之遥了
回顾一下前文做了什么
目前仍然处于内核态,但已经把操作系统内核加载到内存里,准备好随时执行了。下一步的目标就是让系统准备就绪,进入用户态,以供用户正常使用。
但是此时操作系统没有任何的文件系统,这可让用户怎么用呀,
所以接下来的一个任务就是文件系统的初始化。而要实现文件系统的初始化,涉及到硬件驱动的加载,那么难题就来了:
1.厂商并没有统一标准,故硬件驱动五花八门,要想实现对硬件的兼容,把各种驱动编译到linux内核里面不现实。
2.这是一个先有鸡还是先有蛋的问题,文件系统的加载需要硬件驱动支持,而硬件驱动的正确识别需要文件系统里面的模块。。。
所以这样一个专门存放硬件设备的驱动、加载驱动的程序、运行环境的分区,便应运而生了。
刚开始,它叫做initrd:
旧:Linux内核采用initrd("initial ramdisk",初始内存盘,基于ramdisk)
曾经(指Linux内核2.4版本以前,现在都6.7.12了)使用的是initrd,基于ramdisk,
ramdisk是基于块设备的内存文件系统,可在特殊块设备( /dev/ram ) 中使用,如果是/dev/ram0,则将initrd映像挂载为 root。
ramdisk需要预先分配固定大小的内存空间,
其在这里的主要特点是支持随机读写(区别于字符设备只能顺序读写),而且需要文件系统驱动才能启动
一旦初始根文件系统启动,内核将执行 /linuxrc ,作为其第一个进程;
当它退出时,内核假定真正的根文件系统已经挂载,并执行 /sbin/init 以开始正常的用户空间引导过程。
安装initrd
稍微了解一下做了什么,怎么做的,就行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | dd if = / dev / zero of = initrd bs = 300k count = 1
mke2fs - F - m0 initrd
mount - t ext2 - o loop initrd / mnt
mkdir / mnt / dev
mknod / mnt / dev / console c 5 1
chroot / mnt / sbin / init
umount / mnt
gzip - 9 initrd
initrd
root = / dev / ram0 rw
LOADLIN <kernel> initrd = <disk_image>
image = / bzImage
initrd = / boot / initrd.gz
append = "root=/dev/ram0 rw"
/ sbin / lilo
|
init
1.挂载新的根文件系统。
2.将其转变为根文件系统。
根更改是通过 pivot_root 系统调用完成的,pivot_root 是一个系统调用,用于在 Linux 中更改系统的根文件系统。它允许将一个已经挂载的文件系统(通常是一个临时的根文件系统,比如 initrd 或 initramfs)替换为另一个文件系统,而不需要重新启动计算机。
它将当前根移动到新根下的目录,并将新根放在其位置。在调用 pivot_root 之前,旧根目录必须存在。例如:
注意:pivot_root 的实现细节可能会随着时间而改变。
3.删除对旧(initrd)根文件系统的所有访问。
现在,init 进程仍然可以通过其可执行文件、共享库、标准输入/输出/错误及其当前根目录访问旧根目录。所有这些引用都通过以下命令删除:
其中,what-follows 是新根下的程序,例如,/sbin/init。如果新的根文件系统将与 udev 一起使用并且没有有效的 /dev 目录,则必须在调用 chroot 之前初始化 udev 以提供 /dev/console。
4.卸载 initrd 文件系统并取消分配 RAM 磁盘。
现在,可以卸载 initrd 并释放 RAM 磁盘分配的内存:
至此,initrd的流程结束
新:Linux内核采用initramfs("initial RAM filesystem",初始内存文件系统,基于ramfs(也可以称作tmpfs))
但是ramdisk的缺点是固定大小、需要自己实现缓存机制,需要文件系统驱动,甚至内核访问initrd时还需要经过缓存机制来操作(因为其被视为块设备来使用的缘故),而initrd本来就在内存里面,这样反而非常低效率
于是在 Linux 2.6 及更新的版本中,内核引入了基于 ramfs 的 initramfs (初始 RAM 文件系统)机制。主要的思想就是将cache当作一个文件系统直接挂载使用。cache本身就是缓存,不需要自己实现缓存机制,而且不需要经过又一层缓存机制。
ramfs 是一种基于页面的内存文件系统,不需要预先分配固定大小,可以动态增长。而且由于基于内存的特性,其不需要文件系统驱动,非常方便。
根据静态编译到其中的算法,内核可以解压使用gzip、bzip2、LZMA、XZ、LZO、LZ4和zstd压缩的 initrd/initramfs 映像,兼容性非常出色。
然后,内核执行 /init (在 rootfs 中)作为第一个进程。
这张图展现了rootfs挂载阶段的函数调用:
VFS是啥:rootfs为VFS提供了根目录,而rootfs又作为第一个文件系统挂载到VFS
这张图展现了initramfs解压到rootfs阶段的函数调用:
为什么设计成不删除rootfs呢,明明这是中间过程?
其实是因为rootfs此时已经成为虚拟系统目录树的总根,已经离不开它了,而且根本没必要删掉,因为其基于内存文件系统ramfs,删掉其中文件即可释放其空间
ramdisk vs ramfs
initrd vs initramfs直观图
参考
《深度探索Linux操作系统-系统构建和原理解析》好是好,但书是真有点难干啃,可以先看各种博客看个大概,有宏观的认识才比较好理解。
推荐看《Linux内核完全注释》,其对“麻雀虽小,五脏俱全”的v0.11版本(2万行代码左右,但在加载操作系统方面跟现今2024年的linux版本,差别不大)进行细节上的分析,非常适合刚刚学习的人们;
其具有比较透彻的原理解析、更为详细的图例、更适合从对内核一无所知开始初学的,当属《Linux内核完全注释》,其它的总是因为描述不够详细,导致有时候使得刚开始学习的人陷入概念之间的混乱;
只是可惜这本书出来的时候可能grub引导并未流行,所以作者主要还是着眼于老版的bootsect而非后来更流行的grub
https://github.com/torvalds/linux/blob/96fca68c4fbf77a8185eb10f7557e23352732ea2/init/main.c#L1468
推荐看ArchLinux的官方wiki(非常好教程,爱来自cn),讲得非常通俗易懂,可惜详细程度不足,可以用来快速了解全貌
https://wiki.archlinuxcn.org/wiki/Arch_%E7%9A%84%E5%90%AF%E5%8A%A8%E6%B5%81%E7%A8%8B#
内存加载时的布局,看图的话推荐
https://juejin.cn/post/6910028853339488264#heading-0
https://blog.csdn.net/LIJIWEI0611/article/details/125952005
https://www.cnblogs.com/Sna1lGo/p/15752638.html
https://juejin.cn/post/6844904121946865678(推荐)
https://github.com/Yuandong-Chen/Linux-0.11(推荐,Linux内核完全注释)
https://baijiahao.baidu.com/s?id=1773305137022311302&wfr=spider&for=pc
https://blog.csdn.net/Luckiers/article/details/113387209
http://bbs.c3.wuyou.net/forum.php?mod=viewthread&tid=422869
https://hugh712.gitbooks.io/grub/content/image-files.html
https://www.kernel.org/doc/html/latest/search.html(Linux内核手册,可以搜索,推荐)
https://www.cnblogs.com/sztom/p/11120364.html#204(initramfs)
https://blog.csdn.net/weixin_43269452/article/details/131169725(initramfs)
https://www.cnblogs.com/smartjourneys/p/9566974.html(initramfs)
在系统底层机制方面Chatgpt/Claude3会经常胡说八道。遇到好多次,次次都让我怀疑理解错误了,幸好找到资料来反驳它。。。谨慎听取,小心求证