@wenshizhang
2018-10-15T10:08:13.000000Z
字数 5142
阅读 426
论文
启动概述
在最开始的时候,先声明一下。MBR实际上指的是硬盘的第一个扇区,其中存储了bootloader的stage1,为了和后面的stage2去分开,本文把bootloader stage1称为MBR。
BIOS是什么就不用介绍了,来简单说说MBR是什么。
计算机接电之后第一个启动的是BIOS,因为BIOS是存储在主板上的一个小程序,空间有限代码量少因此功能受限。要经过一步步的控制权转移,最后转移给功能更加强大的操作系统。
BIOS对系统做一些简单的初始化工作,然后把控制权交给MBR。MBR(Main Boot Record)存在于整个硬盘最开始的扇区。每个扇区都是512字节,在安装系统的时候创建,MBR引导扇区的内容是:
- 446字节的引导程序及参数
- 64字节的分区表(每个分区表项16字节,因此只能有4个主分区)
- 2字节的结束标志0x55和0xaa
0x55和0xaa是MBR结束标志,0x7c00是MBR被加载在内存中的位置。
BIOS结束自己的工作以后,需要把控制权交给MBR,过程是:BIOS找到MBR并且把MBR加载在内存中,跳转到该位置。
为了方便BIOS寻找,约定好0盘0道1扇区(第一个扇区)用来存储MBR。MBR内容占510个字节,剩余两个字节用作结束标志。BIOS不用遍历这个扇区,只要检测到有这个标志就认为这个扇区有可以运行的MBR存在,否则BIOS会报错。这两个结束标志是0x55和0xaa。
关于0x7c00,这是一个历史遗留问题。MBR的名称是主引导记录,MBR主要工作是:建立分区表、引导其他更复杂的程序。8086cpu要求0~30k位置用来存储中断向量表,DOS1.0要求的最小内存是32k,因此MBR必须存储在钱32k中,中断向量表长度不一定,MBR希望自己有足够的空间运行(MBR也是程序,程序运行需要用到栈,MBR预估自己需要用到512字节的自身存储和512字节的堆栈空间),同时尽可能多的留给向量表足够的空间。综上,MBR存储在32k的最后1k空间,也就是从0x7c00开始。
MBR的主要工作是定位bootloader,加载bootloader。
执行完了自己的工作,需要把控制权交出去。MBR遍历分区表中的4个分区,查找操作系统加载器,找到后把CPU交给加载器。MBR怎么知道加载器存在哪个分区呢?分区表项到 第一个字节就是活动分区标志,如果该分区存储了加载器,该标志被置为0x80,否则是0.MBR只需要遍历分区表就能找到控制权接力赛的下一棒选手应该是谁。这里为了方便MBR找到活动分区上的(因为程序跳转是需要指明跳转地址的),约定好加载器就存储在各分区的开始扇区,这个扇区被称为操作系统引导扇区也称为,这个OBR就是我们常见的x86平台上的grub或者arm平台上的uboot,这个程序负责的主要工作是加载kernel image 并解压缩。
下面的主角变成了bootloader,bootloader可能有些人会有点陌生,举一个例子:grub就是常见的X86平台上的bootloader。x86下bootloader工作在实模式下,
bootloader主要工作可分为4个:
你的内核一定是存在磁盘上的,问题是他在磁盘上的哪一个扇区。在确定这个问题之前,一切都是有可能的。可能是存在磁盘上,可能在缓冲区上等等。加载的时候需要加载什么,这也取决与kernel的设计。比如说Linux需要加载一个额外的文件/etc/initrd
,这个文件保存了初始化的过程。如果内核是模块化的,并且文件系统需要其他模块的支持,加载内核的时候就需要同时加载这些module。
一些内核要求额外的information,
下面举个例子,x86平台上的grub引导。
CRUB会根据需求显示一个可用的内核列表(定义在/etc/grub.con,以及/etc/grub/menu.lst和/etc/grub.conf的软连接)。你可以选中一个内核,并且可以用附加的内核参数改进它。另外,你还能通过shell终端命令行的方式手动控制整个启动过程。
Bootloader完成CPU、RAM、网卡等信息的初始化,建立内存的映射关系。在外存上找到kernel image,启动IO读入内存。一般kernel image使用zlib压缩为zImage或者bzImage格式,读取了image以后,在内核镜像的头部,有一个小型的routine,做一些简单的硬件设置,解压缩至高端内存(输出解压缩提示信息:Decompressing Linux...),加载initrd(注意:这个在后面会作为临时根文件系统存在)进内存,执行kernel通过./arch/i386/boot/head
。
Bootloader在把控制权交给kernel时候,会传递一些参数。这个参数可以是用户通过bootloader设置的(例如:grub的配置文件grub.conf),或者是bootloader自己检测到的硬件信息(例如:根设备标识、页面大小、内核需要的命令信息)。
内核先是设置系统的状态(此部分通过汇编程序head.S完成):查看CPU类型检查kernel是否支持此种类型,查找体系类型,初始化寄存器,创建页表,开启MMU,建立IDT、GDT、LDT,跳转到入口函数start_kernel(main.c中,是第一个C函数)处。
在main开始运行的时,PC处于一个实模式,CPU执行一些必要操作后跳转到保护模式。这里,有两个很重要的问题:中断和内存。在实模式下,处理器的中断向量表始终位于存储地址0(最开始的位置),而在保护模式下,中断向量表的位置存储在称为IDTR的CPU寄存器中。同是逻辑地址到线性地址转换实模式和保护模式是不一样的,保护模式需要一个称为GDTR的寄存器来加载内存的全局描述符表的地址。因此,go_to_protected_mode
调用setup_idt
和setup_gdt
来安装临时中断描述符表和全局描述符表。
现在我们准备好了跳转到保护模式了,调用protected_mode_jump
,这个函数设置CR0寄存器的PE位,打开A19地址线等等操作跳转到保护模式。注意此时分页还是被禁用的,因为暂时还不需要分页。现在,内存终于可以寻址到4GB,调用32位内核入口点startup_32
。
startup_32
调用decompress_kernel
解压缩真正的内核,并在显示器上打印信息Decompress Linux...。解压缩过的kernel此时会覆盖解压缩之前的内核镜像。解压缩结束以后,清楚BSS段设置最终的GDT和IDT,构建页表,打开分页初始化堆栈,最后跳转到start_kernel
。
start_kernel
执行各种初始化函数:初始化IRQ(interrupt handling),初始化时钟、CPU、内存、VFS、中断向量表、内核内存管理子系统。最后调用rest_init
,这几乎是start_kernel的全部工作,rest_init
创建一个内核线程,执行kernel_init
,rest_init
调用schedule
启动任务调度,调用cpu_idle
。cpu_idle
永远运行,只有当进程就绪列表中有就绪进程时候,cpu_idle才会被从CPU上拉下来,当没有进程时候,这个空闲进程又被啦起来执行。这是进程0.这个进程漫长的工作终于结束了,从BIOS、MBR、Bootloader最后到kernel。但是这里还不能认为kernel已经启动了。
进程0刚刚创建了一个内核线程执行kernel_init
,还记得吗?我们现在看看这个线程的运行。kernel_init
负责初始化启动剩下的CPU,从启动分析到现在,我们只有一个CPU在运行注意到了吗?这个CPU我们成为引导CPU,剩下的应用CPU初始化依然需要从实模式开始。最后kernel_init
调用init_post
,这个函数尝试执行一下用户进程:/sbin/init
、/etc/init
、/bin/init
和/bin/sh
,如果全部失败,产生kernel panic。如果上述程序存在,创建进程1运行(这个进程是所有用户进程的父进程)。init读取的第一个文件是/etc/inittab
,从这里init决定了我们的Linux操作系统的运行级别。Linux系统有7个运行级别(runlevel):
level 0:系统停机状态
level 1:单用户工作状态
level 2:多用户状态
level 3:完全多用户状态
level 4:保留
level 5:X11
level 6:reboot
Init从/etc/fstab
文件中查找分区表信息,用真正的文件系统替换initrd。Init然后启动默认运行级别的/etc/init.d目录中指定的所有服务或者脚本。这是所有服务由init逐个初始化的步骤。在这个过程中,一次一个服务由init启动,所有守护程序在后台运行,init继续管理它们。
至此,结束。
更加详细过程参见Linux启动概述。
根据上面传送门的分析,可以看出来,Linux启动过程基本就是小的boot loader加载一个大的boot loader,环环相扣争分夺秒交接控制权,这里就有一些疑惑:
BIOS是为什么出现的?为什么不是一开始就是从kernel开始执行?
BIOS是存储在主板上的依赖硬件的一段程序,每一个不同设计的主板都需要一个专用的BIOS,因此不太可能做成一个通用的标准的BIOS/OS。BIOS的作用是:
当PC开关按下以后,BIOS的第一个任务是加电自检(POST,Power On Self Test),识别和初始化硬件设备(例如:CPU,RAM,显卡设备,键盘,鼠标,硬盘,光驱,USB设备等等。注意鼠标只有UEFI识别,传统的BIOS是不能使用鼠标的)。
注意你可以在没有任何外部存储的情况下启动电脑,这是需要BIOS的原因之一。另一方面来说,BIOS提供了一个统一的接口,为程序使用每一个硬件资源提供了便利。
一旦BIOS加载了bootloader,计算机就完完全全被控制,BIOS退出舞台。
为什么需要MBR,为什么不是BIOS直接load bootloader?
因为BIOS约定好,直接跳转到硬盘的第一个扇区。去掉两字节的magic number,也就是说BIOS的下一棒选手只能有510字节,我们常见的bootloader(x86的grub或者是arm的u-boot)大大超过了这个限制。说道这里也有一个比较迷惑的一点,为什么要有这个限制呢?BIOS只会加载这一个扇区进来吗?为什么不是两个三个扇区,这样就不需要MBR,BIOS直接加载bootloader更节省时间不是吗。
为了解决上面的疑问,有必要提一下BIOS的背景。也就是说在什么情况下,出现了BIOS。BIOS最开始是为只有很小外存的8位PC编写的,他不能直接访问那么多的磁盘。因此我猜测,可能是BIOS最开始只能访问一个扇区,所以才有了MBR。
也是有这样的设计的,BIOS直接加载bootloader,也就是说one stage bootloader。这个bootloader也必须存放在MBR中,也就是说还是受446字节的限制。同时要想16位(实模式)的bootloader加载执行32位kernel(保护模式),并且限制在这么短的代码内,这是非常有难度的一件事,而且还有错误处理代码。因此bootloader要包括:查找加载32位的内核,执行内核和错误处理代码。
为什么需要bootloader,MBR不能直接load kernel吗?
MBR受本身size的影响(MBR全部占一个扇区,即512字节。其中还包括了64字节的分区表和两字节的magic number)。MBR程序只能占446字节,不能完成太复杂的功能。所以需要MBR先把控制权转给bootloader,bootloader负责加载kernel。
而且bootloader也可以允许在磁盘上安装多个操作系统,bootloader负责管理这些操作系统,以及根据用户选择加载这些系统。
和之前说过的一样,也有one stage bootloader的设计,只是因为空间关系,实现起来难度很大。Besides,也有混合bootloader。