最近一直在专注的学习一样技术,主要就是Linux/Unix系统的引导过程,从最基本的机器加电一直到最终系统能够正确的使用为止,这个过程中涉及到了相当多的技术,以及各种技术的推陈出新,都在这个重要的过程中得以体现。我之所以对这部分进行了分析,主要是工作上面需要这方面的知识,同时觉得很有意思,所以广泛的深入研究了一下。
好了,废话不多说了,开始来点干货吧,本文并非全部原创,参考了一些文章,具体请查看“参考”部分。
深入Linux/Unix系统引导过程
Linux/Unix系统的引导过程包含了很多阶段,但是对于一个标准的系统的引导,大致的阶段是类似的,不同的平台会有一些不同之处(x86平台下主要使用LILO、SYSLINUX或是grub,SPARC平台下主要使用OBP作为loader)。在接下来的章节中,我将从系统加电开始直至Linux/Unix内核被正式加载运行。
1 概述
首先从高层的架构分析一下Linux/Unix引导流程,下面的图作为一种更加形象的说明,这样你可以从上次梗概的了解整个流程,为下面深入的分析打下基础。
从这张图中,总结一下大概的流程,分别是:系统启动、阶段1引导、阶段2引导、内核加载和用户态的初始化。在系统启动阶段主要用的技术包括BIOS(最新的发展为EFI),阶段1引导主要用的技术包括Master Boot Record(最近的发展为GPT),阶段2引导主要用的技术包括LILO、GRUB、SYSLINUX、GRUB2(x86平台下)和OBP(SPARC平台下)。这张图属于比较古老的一种介绍,在目前的技术发展下,每个阶段都有了一些变化。
这里。
2.2 EFI启动
与传统MBR相比,GPT采用了不同的分区方式。
对于传统MBR,其结构主要如下:
上图即对上文中所述的很形象的说明,在图中看到MBR被分成三个部分,分别是:Bootloader、分别表以及Magic Number。其中Bootloader部分为stage1中被执行的起始部分,程序在这里被作为GRUB程序执行,详细的关于GRUB的内容将再下面章节中进行详细阐述。
相反,对于EFI系统中所采用的GPT分区方式,则采用了不同于MBR分区方式的形式,从下图中可以发现:
如上图所示,GPT分区表主要包括:保护MBR、首要GPT头、首要GPT、备用GPT、备用GPT头和磁盘数据区。保护MBR与正常的MBR区别不大,主要是分区表上的不同,在保护MBR中只要一个表示为0xEE的分区,以此来表示这块硬盘使用GPT分区表。首要GPT头包含了众多信息,具体内容如下:
偏移 |
字节长度 |
说明 |
0x00 |
8 |
签名,固定为ASCII码“EFI PART”,16进制表示0x5452415020494645。 |
0x08 |
4 |
版本号,目前的版本为V1.0,16进制表示0x00010000。 |
0x0C |
4 |
分区表头的大小(单位是字节,通常是92字节,即5C 00 00 00) |
0x10 |
4 |
GPT头中字节的CRC32校验 |
0x14 |
4 |
固定值00 00 00 00 |
0x18 |
8 |
当前LBA(这个分区表头的位置) |
0x20 |
8 |
备份LBA(另一个分区表头的位置) |
0x28 |
8 |
第一个可用于分区的LBA(主分区表的最后一个LBA + 1) |
0x30 |
8 |
最后一个可用于分区的LBA(备份分区表的最后一个LBA−1) |
0x38 |
16 |
磁盘GUID |
0x48 |
8 |
分区表项的起始LBA |
0x50 |
4 |
分区表项的数量 |
0x54 |
4 |
一个分区表项的大小(通常是128) |
0x58 |
4 |
分区表CRC32校验 |
0x5C |
420 |
保留,剩余的字节必须是0(对于512字节LBA的硬盘即是420个字节) |
分区表头定义了硬盘的可用空间以及组成分区表的项的大小和数量。分区表头还记录了这块硬盘的GUID,记录了分区表头本身的位置和大小(位置总是在LBA1)以及备份分区表头和分区表的位置和大小(在硬盘的最后)。它还存储着它本身和分区表的CRC32校验。固件、引导程序和操作系统在启动时可以根据这个校验值来判断分区表是否有错误,如果出错了,可以使用软件从硬盘最后的备份GPT分区表恢复整个分区表,如果备份GPT也校验错误,那么磁盘将不可用,系统拒绝启动。
接下来主要是128个分区表项,GPT分区表使用简单而直接的方式表示分区。一个分区表项的前16字节是分区类型GUID。例如,EFI系统分区的GUID类型是{C12A7328-F81F-11D2-BA4B-00A0C93EC93B} 。接下来的16字节是该分区的唯一的GUID(这个指的是该分区本身,而之前的GUID指的是该分区的类型)。在接下来是分区其实和末尾的64位LBA编号,以及分区的名字和属性。具体结构如下表:
偏移 |
字节长度 |
说明 |
0x00 |
16 |
分区类型GUID |
0x10 |
16 |
唯一的分区GUID |
0x20 |
8 |
开始LBA |
0x28 |
8 |
结束LBA |
0x30 |
8 |
分区属性 |
0x38 |
72 |
分区名称(Unicode码) |
3 阶段1的引导(GRUB中stage1过程)
接下来开始真正的引导过程了,主要说明GRUP的引导。总体上GRUB更像是一个mini os,只不过这个mini os的作用只是加载其他的操作系统,在GRUB中包括stage1、stage1.5(可选)和stage2,其中stage1和stage1.5属于boot loader,stage2属于mini os的内核部分。GRUB中stage1过程主要位于MBR的前446字节中(对于支持GPT分区的磁盘,同样有最开始的512字节作为保护MBR,保护MBR与正常的MBR区别不大,主要是分区表上的不同,在保护MBR中只要一个表示为0xEE的分区,以此来表示这块硬盘使用GPT分区表,不能识别GPT硬盘的操作系统通常会识别出一个未知类型的分区,并且拒绝对硬盘进行操作),之后的64字节为硬盘的分区表,最后两个字节为MBR结束标志位(0xAA55)。
stage1部分占用了446字节,其代码文件是源码目录下stage1/stage1.S文件,汇编后生成一个512字节的boot.img,被写在硬盘的0面0道1扇区中,作为硬盘的MBR。stage1的工作很简单,就是加载0面0道2扇区上的512字节到0x8000,然后跳转到0x8000执行。
在0面0道2扇区上的512字节内容为stage1/start.S文件汇编后生成。该扇区上的内容的作用是加载stage1.5或是stage2过程,并将控制权转交。
至于代码,这里就不进行分析了,感兴趣的读者可以去查看源码,不过对于GRUB和GRUB2,其stage1的源码位置不尽相同。
4 阶段2的引导(GRUB中stage1.5或stage2过程)
在start过程将控制权转交后,接下来就是GRUB的核心过程了。该过程之所以区分stage1.5和stage2,主要原因是GRUB和GRUB2的区别。在GRUB2中,将stage1.5过程集成到了stage2的过程中,所以stage1.5过程仅仅是针对GRUB的。下面将会分别介绍两种GRUB版本的两种过程。
4.1 GRUB中stage1.5过程
Stage1.5过程很无辜,它的作用很单一,但是非常关键。它的主要功用就是构造一个boot分区系统对应的文件系统,这样可以通过文件系统的路径(/boot/grub/)寻找stage2过程需要的core.img,进而加载到内存中开始执行。
Stage1.5存在于0面0道3扇区开始的地方,并一直延续十几k字节的区域,具体的大小与相应的文件系统的大小有关(文中涉及到了0面0道1-3+x扇区,这部分扇区为保留扇区,BIOS不会放置任何数据。正因为如此如果转换到GPT分区形式,系统将不能被正确引导,如上文所示,MBR后面的扇区都被其他内容所占据)。Stage1.5过程被构建成多种不同类型,但是功能类似,下面简单介绍一下基本的stage1.5过程的文件系统。e2fs_stage1_5(针对ext2fs,可引导ext2和ext3文件系统)、fat_stage1_5(针对fat文件系统,可引导fat32和fat16)、ffs_stage1_5、jfs_stage1_5、minix_stage1_5、reiserfs_stage1_5、vstafs_stage1_5和xfs_stage1_5,这些文件被称为stage1.5过程,这些文件每个至少都在11k以上。除此之外还有两个比较特殊的文件,分别为nbgrub和pxegrub,这两个文件主要是在网络引导时使用,只是格式不同而已,他们很类似与stage2,只是需要建立网络来获取配置文件。
由于stage1.5过程中会涉及到多个文件系统对应的文件,因此本文中主要以ext2fs为例进行说明,其他文件系统与此类似,可以同样进行分析理解。
对于ext2fs文件系统,用于生成该文件系统的stage1.5过程文件(e2fs_stage1_5)的代码为stage2/fsys_ext2fs.c文件。
在stage2/filesys.h文件中定义了每个文件系统对外的接口,用于上层调用,作为stage2过程寻找核心代码使用,文件系统一般被定义的接口主要就是三个函数,分别是mount、read和dir函数。对应ext2fs,其定义的函数为:
#ifdef FSYS_EXT2FS
#define FSYS_EXT2FS_NUM 1
int ext2fs_mount (void);
int ext2fs_read (char *buf, int len);
int ext2fs_dir (char *dirname);
#else
#define FSYS_EXT2FS_NUM 0
#endif
针对ext2fs有如上的函数名称,每个函数将具体在stage2/fsys_ext2fs.c文件中被定义,这里面没有包含任何的写的过程,对于bootloader而言仅仅读就可以完成任务了,没必要对其系统进行写操作。其中ext2fs_mount函数用于检查文件系统类型,并将superblock读入到内存中;ext2fs_read函数和ext2fs_dir函数用于对文件系统具体的操作。在stage2/fsys_ext2fs.c文件中除了需要对这三个函数的定义之外,还需要文件系统的属性的数据结构(superblock、inode和group结构,这些结构最初被定义在include/linux/ext2_fs.h文件中),通过这些数据结构描述一个文件系统。
如果读者有兴趣可以试着创建新的文件系统的支持,可以参照目前存在的一些文件系统的模板(实例)编写。
4.2 GRUB中stage2过程
GRUB中的核心过程也就是stage2过程了,该过程主要是在文件系统建立以后选择合适的操作系统进行加载并转交控制权,达到最后引导操作系统的目标。由于GRUB属于multi boot loader,因此在引导的时候要进行选择,选择哪种操作系统来运行。在GRUB内部主要包括两种方式,首先是从menu.list中读取显示到屏幕让用户选择,其次是通过grub-shell中定义的命令手动进行启动。本文将在后面介绍这两种方式如何运行,接下来先介绍一下stage2的具体的执行过程。
在上面一节中介绍过,stage1.5过程中将boot分区的文件系统加载了,之后又做了一件事情,就是将控制权转交给stage2,而stage2入口的地方就是stage2/asm.S文件。Stage2/asm.S文件属于汇编代码,主要作用是初始化C语言的运行环境,为下面执行C语言的函数做好准备,在准备好之后,将执行init_bios_info(stage2/common.c)函数。init_bios_info函数的作用是执行一些底层的函数,然后跳转到cmain执行,cmain函数位于stage2/stage2.c文件中。cmain函数内部进行一个死循环,在循环内部首先加载配置文件,显示给用户,在这同时循环一个内层循环,在内层循环中,获取配置文件中的命令,并解析执行。过程中如果没有可用的配置文件,那么进入命令行模式(enter_cmdline函数),如果找到可用的menu,那么开始执行menu的对应的内容(run_menu函数)。
对于enter_cmdline(stage2/stage2.c)函数,将调用find_command(stage2/cmdline.c),进而执行相应命令的函数。
对于run_menu(stage2/stage2.c)函数,将调用stage2/cmdline.c文件中的run_script函数,进而调用find_command,执行相应命令的函数。
这两种方式虽然经过了不同的过程,对用户输入的行为进行分析和处理,最终调用的函数为find_command,在该函数中顺序循环比较“输入”的命令是否与系统内部定义的相同,如果相同转到执行该函数。在这个比较的过程中包含了一个全局的数据结构为struct builtin(stage2/shared.h),由该数据结构组成了一个table类型(stage2/builtins.c),将命令与相对应的builtin结构对应一起并进行串联。下面描述一下builtin结构的定义:
struct builtin
{
/* 命令名称,重要,是搜索命令时的依据*/
char *name;
/* 命令函数,重要,是搜索匹配后调用的函数*/
int (*func) (char *, int);
/* 功能标示,一般未用到. */
int flags;
/* 简短帮助信息*/
char *short_doc;
/* 完整帮助信息*/
char *long_doc;
};
struct builtin *builtin_table[];
有兴趣的读者可以对上面的内容进行扩展,形成自己的命令,主要在stage2/builtins.c文件中按照预定的格式更新,并添加到builtin_table中即可。
在上面打开配置文件的过程中,主要是通过一些文件操作函数(被定义在stage2/disk_io.c中)完成。这些文件操作函数主要包括:grub_open、grub_read、grub_seek和grub_close等,这些函数属于grub对外的上层接口,具体的函数内部将调用前文中提到的boot分区对应的文件系统的相应的函数完成,这个过程主要是通过回调函数来完成。该过程整体思路类似于面向对象过程,通过对象操作具体的函数。关于用C语言实现面向对象的编程思路,可以参考一本书——《Object-oriented Programming with ANSI-C》。
4.3 GRUB中stage2过程
未完待续。
5 加载内核
未完待续。
6 Linux/Unix系统初始化
未完待续。
本文永久更新链接地址:http://www.linuxidc.com/Linux/2014-10/108032.htm