emmoblin.github.com
分类: LINUX
2008-11-17 23:59:11
主要步骤如下:
(1) 通过'rootnoverify'命令设置GRUB的主设备指向一个扇区。
grub> rootnoverify (hd0,0)
(2) 通过'makeactive'命令来设置在扇区上的'active’标志位。
grub> makeactive
(3) 通过'chainloader'命令来加载引导程序。
grub> chainloader +1
'+1'表明GRUB需要从起始分区读一个扇区。
(4) 运行命令'boot'
GRUB引导操作系统的简要流程分析
3.2.1 从计算机启动到GRUB启动操作系统
(1) BIOS执行INT 0x19,加载MBR至0x7c00并跳转执行。如果你安装GRUB到MBR,GRUB的安装程序会把Stage1(512B)拷贝到MBR。视stage2的大小,安装程序会在 Stage1中嵌入Stage1.5或者Stage2的磁盘位置信息。
(2) Stage1开始执行,它在进行直接加载Stage1_5或者Stage2并跳转执行。不论是哪种情况,这一步的结果都是Stage2开始运行了。
(3) Stage2这个小型的操作系统终于开始正式运行了!它会把系统切入保护模式,设置好C运行环境(主要是BSS)。他会先找Config文件(就是我们的 Menulist),如果没有的话就执行一个Shell,等待我们输入命令。然后Grub的工作就是输入命令-解析命令-执行命令的循环,当然 Stage2本身是为加载其他操作系统而存在的,所以如果情况允许,在他执行Boot命令以后就会把控制权转交出去。
3.2.2 GRUB的主要启动模块
GRUB 包含如下几个启动模块:两个必须的场景文件,一个叫"Stage 1.5"的可选的 场景文件以及2个网络启动的映像文件。首先对他们有一个大致的了解。
Stage1
这是一个基本必须的用来启动GRUB的映像文件。通常,这个文件是被装载到MBR或者启动扇区所在的分区。由于PC的启动扇区的大小为512字节,所以这个映像文件编译以后也必须为512字节。
Stage1的全部的工作是从本地磁盘把Stage 2或者Stage 1.5装载进来。由于对 stage1大小的限制,它通过分程序表的形式来编码Stage 2或者Stage 1.5的位置,所以在stage1是不能识别任何文件系统的。
Stage2
这是GRUB的核心映像。它几乎做了除启动它本身以外的所有事情。通常,它被存放为某一种文件系统下,但并非是必须的。
e2fs_stage1_5
fat_stage1_5
ffs_stage1_5
jfs_stage1_5
minix_stage1_5
reiserfs_stage1_5
vstafs_stage1_5
xfs_stage1_5
这些文件被称为stage 1.5,它存在的目的是做为stage1与stage2之间的桥梁,也就是说,stage1载入stage1.5,然后stage1.5载入 stage2。
stage1与stage1.5之间的区别是,前者是不识别任何文件系统的但后者识别文件系统(例如 'e2fs_stage1_5' 识别 ext2fs)。所以你可以安全的移动stage2的位置,即使是在GRUB安装完以后。
nbgrub
这是一个网络启动的映像文件,被类似于以太网启动装载器所使用。它很类似于stage2,但它还要建立网络,然后通过网络来载入配置文件[7]。
pxegrub
这是另一个网络启动的映像文件。
除了格式以外,它和'nbgrub'是一致的。
4 STAGE1模块分析
Stage1模块是整个引导程序的引导模块,是从开机过渡到GRUB的第一个模块。 Stage1的代码文件,是源码目录下 Stage1/Stage1.S,汇编后便成了一个512字节的Img,被写在硬盘的0面0道第1扇区,作为硬盘的主引导扇区。
4.1 Stage1.h文件分析
在此文件中主要是定义了一些在Stage1.S文件中使用到的一些常量。
关于这些常量的分析如下:
/* 定义了grub的版本号,在stage1中可以识别他们.*/
#define COMPAT_VERSION_MAJOR 3
#define COMPAT_VERSION_MINOR 2
#define COMPAT_VERSION ((COMPAT_VERSION_MINOR << 8) \
| COMPAT_VERSION_MAJOR)
/* MBR最后两个字节的标志*/
#define STAGE1_SIGNATURE 0xaa55
/* BPB (BIOS 参数块BIOS Parameter Block)的结束标记的偏移,他含有对驱动器的低级参数的说明. */
#define STAGE1_BPBEND 0x3e
/* 主版本号的标记的偏移*/
#define STAGE1_VER_MAJ_OFFS 0x3e
/* Stage1启动驱动器的标记的偏移*/
#define STAGE1_BOOT_DRIVE 0x40
/* 强迫使用LBA方式的标记的偏移*/
#define STAGE1_FORCE_LBA 0x41
/* Stage2地址标记的偏移*/
#define STAGE1_STAGE2_ADDRESS 0x42
/* STAGE2扇区的标记的偏移*/
#define STAGE1_STAGE2_SECTOR 0x44
/* STAGE2_段的标记的偏移*/
#define STAGE1_STAGE2_SEGMENT 0x48
/* 使用Windows NT的魔术头标识的偏移*/
#define STAGE1_WINDOWS_NT_MAGIC 0x1b8
/* 分区表起始地址的标记的偏移*/
#define STAGE1_PARTSTART 0x1be
/* 分区表结束地址的标记的偏移*/
#define STAGE1_PARTEND 0x1fe
/* Stage1堆栈段的起始地址*/
#define STAGE1_STACKSEG 0x2000
/* 磁盘缓冲段。磁盘缓冲必须是32K长而且不能跨越64K的边界。*/
#define STAGE1_BUFFERSEG 0x7000
/* 驱动器参数的地址*/
#define STAGE1_DRP_ADDR 0x7f00
/* 驱动器参数的大小*/
#define STAGE1_DRP_SIZE 0x42
/*在BOIS中软盘的驱动器号标志*/
#define STAGE1_BIOS_HD_FLAG 0x80
4.2 Stage1.s文件分析
首先在这个文件的开始部分定义了一些宏。
#define ABS(x) (x-_start+0x7c00)
这个宏计算了直接地址。由于MBR是被加载到0x7c00的位置,所以通过计算可以直接得到x参数的直接地址。这样就可以不依赖于Linker程序。
#define MSG(x) movw $ABS(x), %si;
这个宏用于处理对字符串的载入和响应。
然后程序从_start程序入口开始执行,此入口在内存中的位置为CS:IP 0:0x7c00。 随后对一系列的变量进行了初始化。设置了起始的扇区、磁道和柱面,并设置了他们的起始位置。同时还设置了stage1的版本号。通过设置boot_drive变量,来设置从那个盘来载入stage2。如果此变量设置成0xff则从默认的启动驱动器中来载入stage2。然后指定stage2的起始地址是0x8000,起始段是0x800,起始的扇区号是1。也就是说stage2起始位置是被存放在0柱面,0磁道,第2扇区上的 [8]。
程序从real_start入口开始真正执行。首先设置了数据段以及堆栈段的偏移为0, 然后设置stage1的堆栈的起始地址为 STAGE1_STACKSEG即0x2000。随后打开中断。 然后检查是否设置了启动的磁盘。即boot_drive变量是否为0xff,如果非 0xff则 保存设置的磁盘号到dl寄存器中,并压入堆栈保存。同时在屏幕上显示GRUB字样。 然后检查此启动磁盘是否是软盘,如果是软盘则直接跳转到 CHS模式不用检测是否 支持LBA模式。然后检测所启动的磁盘是否支持LBA模式。接着程序分成两块,一块是LBA模式,一块是CHS模式。
LBA英文全名为Logical Block Addressing,中文名称为逻辑区块寻址[9]。LBA所指的是一种磁盘设备的寻址技术,它是利用逻辑映对的方式来指定磁盘驱动器的扇区,目前个人计算机所使用的传输接口中,增强型 IDE (Enhanced IDE) 和 SCSI 均使用逻辑区块寻址方式。传统的硬盘寻址技术是采取实体寻址(physical mapping、physical addressing)的方式,以磁盘上的实际结构,直接作为资料区块地址的结构。但由于初期在设计实体寻址方式时,硬盘容量只有5、10、20 MB等 等小容量机种,所以设计出来的最大的寻址能力,只能到1024个磁柱 (cylinder)、16个磁头(head)、63个扇区(sector)。以每个扇区(sector) 512字节(bytes)计算,实体寻址的方式最多只能使用512×63×1024×16= 528482304字节(528MB)的硬盘空间。但是由于磁性储存技术不断的提升,硬盘容 量大幅增加的情况之下,这样的限制让使用者必须将硬盘画分为多个区块,使用上非常的不方便。
因此硬件厂商研究出了LBA逻辑寻址方式,也就是计算机系统并没有将资料存放地点的相关记录,应对到硬盘上资料实际存放的位置。而是由IDE控制电路和 BIOS负责转换寻址(mapping)资料的记录位置表。经过转换后的记录方式,是将第1个磁柱上的第1条磁道的第1个扇区编号为0,第二个扇区编号为 1,以此类推……,假设1 条磁道有2000个扇区,那么第2000个扇区的编号就是1999。第2条磁道上的第1个扇 区就是2000,如此一直线性排列下去。以逻辑区块的方式来寻址的硬盘,最多可达 16383磁柱,最大磁头数为16个,每轨扇区有63区,扇区大小为512字节,所支持之硬盘空间为512× 63×16383×16=8455200768字节(8.4GB)[10]。
在ATA的接口规格中,定义了使用28位来寻址,因此计算出来,它可以支持到224× 512=137GB的容量。不过不幸的,BIOS并无法配合,它使用 24位来寻址(也就是 LBA模式)。所以根本之道,就是改变BIOS对中断13h的支持,因此后来的BIOS就设计了加强版的中断13h。一口气使用了 64位来对硬盘做寻址,因此可以支持到264× 512=9.4TB,相当于3万亿倍的8.4GB[11]。
如果是LBA模式下读取,首先对先前定义的磁盘的一些参数进行了定义,为以后调用INT 13做准备。使用INT13的0x42功能,把磁盘内容读到内存中。设置ah为0x42 为功能号,设置dl寄存器来设置磁盘号,si为记录磁盘一系列信息的地址偏移量, 磁盘信息中包括了要读入的柱面号、磁道号以及扇区号。程序然后调用BOIS INT 13中断将启动磁盘上的第二扇区上的内容读到内存中的STAGE1_BUFFERSEG处,在 Stage1.h中定义STAGE1_BUFFERSEG为 0x7000。即将第二扇区上的内容读到内存中 的0x7000处。读入成功的话跳转到COPY_BUFFER处,如果读取失败则尝试使用CHS模 式读入。
与LBA模式不同的是,调用BIOS INT 0x13中断中的0x2号功能,设置ah寄存器为 0x2,al为扇区数,cl的位6,位7和ch组合为磁道号,cl的0-5位为扇区号,dh为 磁头号, dl为驱动器号(其中0x80为硬盘,0x0为软驱)。es:bx为数据缓冲区的 地址。但所起的功能与前面提到的LBA模式是类似的,也是将第二扇区中的内容读 到内存中的0x7000处,作为缓存。然后跳转到COPY_BUFFER处。
最后调用COPY_BUFFER将刚刚读入的扇区转移到stage2_address。即转移到0x8000处。
4.3 Stage1模块功能综述
由于对Stage1文件容量的限制,所以Stgae1所做的工作相对来说比较有限。它首先被BOIS装载到内存中的0x7c00处,然后通过调用BOIS INT13中断,把启动驱动器中第二扇区上的内容读到内存中的0x7000处,然后通过调用COPY_BUFFER将其转移到了内存中0x8000的位置上。这个被读入的第二扇区上的内容,就是下面将要分析的Start.s功能模块。
5 START模块分析
从上一章节的分析中我们看到,Stage1的是完成了一个MBR所需要完成的任务,但 GRUB并没有直接就通过Stage1直接载入GRUB的内核,而是通过Stage1载入了另一个 模块到0x8000处。根据对源代码的分析,发现被载入的这个模块就是下面需要分析 的第二个模块,即Start.S模块。
5.1 Start.s 模块功能分析
在程序的开始部分,仍然是对程序定义了一些宏。
#ifdef STAGE1_5
# define ABS(x) (x-_start+0x2000)
#else
# define ABS(x) (x-_start+0x8000)
#endif
可以发现,如果定义了STAGE1_5则程序的起始地址是0x2000,而如果没有定义 STAGE1_5程序起始的地址正好是0x8000。所以我判断,在Stage1后载入内存的程序部分就是Start.s所编译以后的512字节的映象文件。关于STAGE1_5的部分暂时先不进行分析,这里暂且跳过。
宏 “#define MSG(x) movw $ABS(x), %si;”的作用是在屏幕上显示字符串。
接着就是程序的入口_start。由于是紧接着Stage1被载入内存中的,所以它的起始 地址就是0x8000,并且它仍然将使用Stage1模块留下来的寄存器以及变量等信息。 如果设置STAGE1_5变量则在屏幕上显示“Loading stage1.5”,如果没有设置这个变 量则显示“Loading stage2”。然后读入需要读入的扇区的数目。接着进入一个 bootloop的循环,如果需要读入的扇区不为0,则继续循环,直到当需要读入的扇 区数目为0时,循环结束。在这个循环中,使用与Stage1中的方法相同,判断了驱 动器磁盘所支持的读写模式,根据不同的磁盘所支持的不同模式,跳转到相应的部 分去读取磁盘上的扇区到内存中去。如果磁盘支持的是LBA模式则跳转到lba_mode 部分读取相应的扇区,如果磁盘不支持LBA模式,则跳转到 chs_mode部分,通过 CHS模式来读入把磁盘中的扇区读入到内存中。首先是把读到的扇区读到内存中的 0x7000处缓存起来,然后通过调用 copy_buffer子程序,把缓存中的内容复制到目 标地址,即0x8200开始的地方。与Stage1中的一样,在Start.s中也有一个记录地址的数据结构,不同的是在Stage1中只有一项,而Start.S记录的是一个地址的链表,称为Blocklist,该链表的结点都记录了一个连续 sectors的集合。
lastlist:
.word 0
.word 0
. = _start + 0x200 - BOOTSEC_LISTSIZE
/*加0x200是由于Start.s编译完以后也是一个512字节的映象文件。*/
/* 初始化了第一个数据列表*/
blocklist_default_start:
.long 2 /* 记录了从第3个扇区开始*/
blocklist_default_len:
/* 这个参数记录了需要读取多少个扇区 */
#ifdef STAGE1_5
.word 0 /* 如果设置了STAGE1_5标志,则不读入*/
#else
.word (STAGE2_SIZE + 511) >> 9 /*读入Stage2所占的所有扇区*/
#endif
blocklist_default_seg:
#ifdef STAGE1_5
.word 0x220 /*如果设置STAGE1_5则从0x220开始读入*/
#else
.word 0x820 /*如果没有设置STAGE1_5则从0x820开始读入*/
#endif
firstlist:
当把所有需要读入的扇区都读入以后,程序进入bootit子程序块。然后程序进行跳转,如果设置了STAGE1_5标志,则跳转到0x2200执行,如果没有设置STAGE1_5标志,则跳转到0x8200处继续执行。
5.2 Start 模块功能综述
通过对Start.s文件的分析,我们可以看到。Start模块主要是做了一件事情,就是把Stage2或者Stage1_5模块从磁盘装载到内存中。如果是直接装载Stage2的话,是装载在内存的0x8200处,如果装载Stage1_5的话,是装载在内存的0x2200处。
6 GRUB Kernel模块分析
由于我分析的是GRUB2的源代码,从GRUB2开始,从Start模块载入的是Grub的整个 kernel。从官方的说明可以看到,与Grub相比,最大的差异在于GRUB2将Stage1.5 以及Stage2的功能归并为GRUB2的kernel,并提高了压缩性能;编译生成的 kernel──core.img只有24KB左右,即使对于最普遍的CHS读写模式所支持的0面0道 的64个扇区(折合32K左右)而言,空间是足够放置GRUB2的kernel的,Grub的每个 Stage1.5都至少在11K左右,而stage2则为110K左右。
6.1 Asm.s 文件分析
在分析了Start模块以后,发现如果没有设置Stage1_5参数,那么系统已经把Grub kernel从磁盘完全装载到了起始地址为0x8200开始的内存中。于是我便在源代码中 寻找起始地址从0x8200开始执行的代码。发现Asm.s文件就是这样一个符合条件的 模块。
首先在这个文件的开始,仍然定义了这样一个宏:
#ifdef STAGE1_5
# define ABS(x) ((x) - EXT_C(main) + 0x2200)
#else
# define ABS(x) ((x) - EXT_C(main) + 0x8200)
#endif
从这个宏中可以看出,从Start模块以后从磁盘转载的应该就是这个文件编译以后的模块。程序的入口是EXT_C(main)。如果没有定义 STAGE1_5那么程序的起始地址 正是0x8200完全符合前面所做的分析。同时由于设置了.code16,整个程序开始仍然是工作在实模式下的。
接着分析ENTRY(main)这个函数。首先为了保证main这个函数如果是Stage2的话被装载在0x8200,如果是Stage1.5的话被装载在0x2200。然后程序执行了一个长跳转。ljmp $0, $ABS(codestart)。
在执行codestart代码之前,它对一些变量进行了初始化。设置了如版本号、 install_partition、saved_entryno、 stage2_id、force_lba和config_file等。 如果是Stgae1.5则config_file为 “/boot/grub/stage2”,如果是Stage2则为 “boot/grub/menu.lst”。
然后进入codestart代码,首先关中断,对断寄存器进行了一些初始化,然后设置了堆栈的起始地址为STACKOFF即(0x2000 - 0x10)。
接着程序调用了real_to_prot这样一个子功能模块。从实模式转换把程序转换到保护模式下。
分析ENTRY(real_to_prot)子功能,主要的转换步骤如下:
首先程序仍然在实模式下,关中断。接着载入了GDT表。然后通过.code32转到保护模式下,跳转到protcseg子功能下,重新装载所有的段寄存器。同时把返回的地址放到 STACKOFF中,然后获得保护模式下的堆栈地址,把STACKOFF压入堆栈中,进行保护。然后返回。
然后程序继续,清空了bss段,调用了init_bios_info函数,这个函数体是整个C语言代码的入口,是C main函数前的初始化代码。
在asm.s文件中,主要是一些汇编代码的函数块,没有C语言的代码,于是我在 share.h中找到了init_bios_info的函数定义,从而在 common.c中找到了 init_bios_info的代码。
同时在这个文件中还定义了非常多的汇编代码写的函数,这些函数将来会被C文件 调用。这里先对这些文件进行一下说明,如表6.1所示。
表6.1 asm.s文件中的汇编函数列表
函数名称 函数作用
stop() 调用prot_to_real子函数,从保护模式转换成实模式
hard_stop() 通过反复调用自身,形成一个死循环,起到一个暂停的作用
grub_reboot() 重新启动系统
grub_halt(int no_apm) 暂停系统,利用时钟计时,如果设置NO_APM将不使用时钟计时
track_int13(int drive) 追踪INT13来操作I/O的地址空间
set_int15_handler(void) 建立INT15的句柄
unset_int15_handler(void) 重新恢复INT15的句柄
set_int13_handler(map) 复制一块数据到驱动器并且建立INT13的句柄
chain_stage1(segment,offset,part_table_addr) 启动另一个stage1的载入程序
chain_stage2(segment, offset, second_sector) 启动另一个stage2的载入程序
real_to_prot () 实模式转换成保护模式
prot_to_rea l() 保护模式转换成实模式
int biosdisk_int13_extensions (int ah, int drive, void *dap) 调用IBM/MS 扩展INT13的功能。
int biosdisk_standard (int ah, int drive, int coff,int hoff, int soff,int nsec, int segment) 调用标准的INT13功能
int check_int13_extensions (int drive) 检查磁盘是否支持LBA模式
get_diskinfo_int13_extensions (int drive, void *drp) 从参数*drp返回磁盘 驱动的具体结构
int get_diskinfo_standard (int drive, unsigned long *cylinders,unsigned long *heads, unsigned long *sectors) 返回指定磁盘的柱面,磁头以及扇区信息
int get_diskinfo_floppy (int drive, unsigned long *cylinders,unsigned long *heads, unsigned long *sectors) 返回软盘的磁盘的柱面,磁头以及扇区信息
get_code_end() 返回代码末端的地址
get_memsize(i) 返回内存大小,如果I为0返回常规内存,I为1返回扩展内存
get_eisamemsize() 返回EISA的内存分布图
get_rom_config_table() 获得Rom配置表的线性地址
int get_vbe_controller_info (struct vbe_controller *controller_ptr) 获得 VBE控制器的信息
int get_vbe_mode_info (int mode_number, struct vbe_mode *mode_ptr) 获得 VBE模式信息
int set_vbe_mode (int mode_number) 设置VBE模式
linux_boot() 做一些危险的设置,然后跳转到Linxu安装的入口代码
multi_boot(int start, int mb_info) 这个函数启动一个核心使用多重启动的标 准方法
void console_putchar (int c) 通过这个函数在终端上显示字符
int console_getkey (void) 调用INT16从键盘上读取字符
int console_checkkey (void) 检查是否某个键被一直按下去
int console_getxy (void) 调用INT10获得光标的位置
void console_gotoxy(int x, int y) 调用INT10设置光标的位置
void console_cls (void) 调用INT10 清空屏幕
int console_setcursor (int on) 调用INT10设置光标的类型
getrtsecs() 如果第二个值能被读取,则返回这个值
currticks() 用Ticks为单位返回当前时间,一秒约为18-20个Ticks
6.2 Common.c 文件分析
在common.c文件中我找到了函数init_bios_info的实现的代码。分析发现,整个 init_bios_info文件主要是对 multiboot_info这个结构进行初始化以及填充。
整个结构体分析如下:
struct multiboot_info
{
/* 多重启动信息的版本号*/
unsigned long flags;
/* 可以使用的内存 */
unsigned long mem_lower;
unsigned long mem_upper;
/* 主分区 */
unsigned long boot_device;
/*核心的命令行*/
unsigned long cmdline;
/*启动模块的列表*/
unsigned long mods_count;
unsigned long mods_addr;
union
{
struct
{
/* (a.out) 核心标识表的信息 */
unsigned long tabsize;
unsigned long strsize;
unsigned long addr;
unsigned long pad;
}
a;
struct
{
/* (ELF) 核心标识表的信息*/
unsigned long num;
unsigned long size;
unsigned long addr;
unsigned long shndx;
}
e;
}
syms;
/* 内存分布图的缓存 */
unsigned long mmap_length;
unsigned long mmap_addr;
/* 驱动器信息缓存 */
unsigned long drives_length;
unsigned long drives_addr;
/* ROM 配置表 */
unsigned long config_table;
/* 启动装载器的名称 */
unsigned long boot_loader_name;
/* APM 表 */
unsigned long apm_table;
/* 视频 */
unsigned long vbe_control_info;
unsigned long vbe_mode_info;
unsigned short vbe_mode;
unsigned short vbe_interface_seg;
unsigned short vbe_interface_off;
unsigned short vbe_interface_len;
};
通过调用asm.s中的底层功能模块,对这个结构进行初始化以后,直接调用cmain函数。
6.3 Stage2.c 文件分析
通过查找,在Stage2.c中找到了cmain函数,这个应该就是Stage2这个小型操作系 统的入口了。然后程序就进入一个死循环,整个Stage2 就在这个死循环中运行。 接着调用reset()函数对stage2的内部变量进行初始化。通过open_preset_menu() 函数尝试打开已经设置好的菜单。如果用户没有设置好菜单,那么将返回0,如果 已经设置好了菜单则不返回0。如果没有成功打开菜单,那么将通过grub_open()函 数尝试打开config_file。Grub使用内部的文件格式来打开这样一个配置文件,如 果仍然打开失败,则跳出整个循环。如果打开成功,则根据打开的情况,即 is_preset变量的值的情况来判断是从预设菜单读入还是从配置文件读入命令。然 后通过把is_preset传入 get_line_from_config()函数,将命令读入cmline中。然 后通过find_command()函数查找有没有这条命令。
在Grub中,保存命令的格式是保存在一个builtin的结构体中的。这个结构体在 shared.h头文件中进行了定义。
struct builtin
{
/* 命令名称,重要,是搜索命令时的依据 */
char *name;
/* 命令函数,重要,是搜索匹配后调用的函数 */
int (*func) (char *, int);
/* 功能标识 */
int flags;
/* 简短帮助信息 */
char *short_doc;
/* 完整帮助信息 */
char *long_doc;
};
整个命令的表的定义如下
extern struct builtin *builtin_table[];
find_command()函数在cmdline.c中定义,它对整个builtin表进行遍历,然后比较 名称。如果在表中发现了这个命令,则返回指向当前builtin结构的指针。如果没 有发现这个命令则返回0同时返回一个errnum。如果成功的找到了一条指令,然后 通过调用在cmdline.c中的skip_to ()函数,获得当前builtin指针所指向结构的命 令的参数。然后通过(builtin->func) (arg, BUILTIN_MENU)直接调用此命令。最 后一直循环,直到没有命令可以取为止。
如果由于前面没有成功的打开预先配置的文件而跳出循环,则通过在cmdline.c文 件中定义的enter_cmdline()函数调用来启动命令行。在 enter_cmdline()函数 中,进入另一个循环等待接受命令。当通过get_cmdline()函数接收到命令以后, 仍然通过 find_command()函数调用来遍历builtin表,如果没有在表中找到输入的 指令则返回一个errnum= ERR_UNRECONGNIZED。如果成功找到了这条指令,同样首 先调用在cmdline.c中的skip_to ()函数,获得当前builtin指针所指向结构的命令 的参数。然后(builtin->func) (arg, BUILTIN_MENU)直接调用此命令。
如果成功的打开了菜单则跳转到run_menu()函数,这里是grub中整个menu用户界面 的主循环。首先有一个计时器grub_timout进行计时,如果grub_timeout<0或者没 有设置,那么就强行显示菜单。如果菜单没有显示,则在屏幕上显示“Press `ESC' to enter the menu...”,并进入一个死循环中,当用户按下ESC按键,则马上显示 菜单。如果超时,那么就直接进入第一个,也就时默认的那个启动项目。如果显示 菜单,则显示所有可以选择的入口。
不论是否显示菜单,最后程序都将跳转到boot_entry。首先程序先清空了屏幕,然 后把光标定于第一行的位置。然后再次进入一个循环。然后如果没有设置入口则通 过调用get_entry()函数来获取一个默认的入口。然后调用在cmdline中的 run_script()函数解释这个入口。 Run_script()函数对这个入口以后的指令脚 本,进行了解析。解析的方式仍然是利用find_command()函数调用。
6.4 GRUB部分指令说明
Grub中所有的预先设置的指令都是在builtins.c文件中实现的。比如启动一个 FreeBSD操作系统,可以输入以下的指令:
grub> root (hd0,a)
grub> kernel /boot/loader
grub> boot
6.4.1 Root指令
调用root指令的函数是在builtins.c中的root_func (char *arg, int flags)函 数。第一个参数指定了哪个磁盘驱动器,如hd0是指第一块硬盘。第二个参数是分 区号。然后在root_func()中它有调用了 real_root_func (char *arg, int attempt_mount)这个函数,并把参数arg传入real_root_func中并把attempt_mount 设置为1。如果传入的arg是空的,那么就直接使用默认的驱动器。然后调用 set_device()函数,从字符串中提取出驱动器号和分区号。测试如果所填写的驱动 器号以及分区号读写没有问题,那么就在变量saved_partition和saved_drive中保 存读取的这两个数据。然后返回。
这个函数主要的作用是为GRUB指定一个根分区。
6.4.2 Kernel指令
调用kernel指令的函数是在builtins.c文件中kernel_func (char *arg, int flags)函数。在这个函数中,首先进入一个循环,对传进来的参数进行解析。如果 “--type=TYPE”参数被设置了,根据传入的参数设置 suggested_type变量赋予不用 的操作系统的值。当没有别的参数被设置以后,则跳出循环。然后从参数中获得内 核的文件路径,赋值给 mb_cmdline变量,然后通过load_image()函数载入核心, 并且返回核心的类型。如果返回的核心类型是grub不支持得类型,即 kernel_type == KERNEL_TYPE_NONE返回1,成功则返回0。
这个函数主要的作用是,载入操作系统的核心。
6.4.3 Boot指令
调用boot指令的函数是在builtins.c文件中的boot_func(char *arg, int flags) 函数。如果被载入的核心类型不是未知的,那么调用unset_int15_handler()函 数,清除int15 handler。接着根据grub支持的不同的操作系统调用相应的启动程序。当启动的内核为BSD时调用bsd_boot ()函数,当启动的内核为LINUX时调用的函数时linux_boot()函数,当启动方式是链式启动方式时,调用chain_stage1()函 数,当启动方式是多重启动时,调用multi_boot()函数。
这个函数主要的作用是,根据不同的核心类型调用相应的启动函数。
6.5 GRUB Kernel分析总结
通过分析,这个核心模块主要的工作是完成了GRUB这个微型操作系统的从磁盘到内 存的装载和运行。在asm.s这个文件中提供了从汇编代码到C代码转换的接口,也是 从这里开始正式载入了GRUB这个微型操作系统,可以说是GRUB运行的一个入口。同 时,在asm.s文件中,对底层的方法用汇编语言进行了封装,方便在以后的C代码中 调用。然后经过对BIOS进行一些初始化以后,正式进入了GRUB的主程序,即在 stage2中的cmain入口。从此这个微型的操作系统开始正式运行。然后值得注意的 是buildin这个数据结构,这个结构就是GRUB所有支持命令的数据结构。结构包括 了一个用来识别的名字和一个用来调用的方法。GRUB通过接收外部输入的指令的方 式,来间接的启动和装载其他的操作系统。
7 总结
7.1 GRUB源代码分析总结
通过对整个源代码的分析,大致上整个GRUB启动到引导其他操作系统分为如下几个 步骤。
第一步 开机后,通过BIOS装载Stage1模块
第二步 通过Stage1模块装载Start模块
第三步 通过Start模块将整个GRUB的内核载入内存
第四步 通过GRUB的一个Shell的机制,作为一个小型的操作系统,来通过指令的方 式装载不同的其他操作系统。
整个过程中GRUB启动的内存映象图如图7.1所示。
总体分析下来,首先感觉到GRUB整个代码在编码方面是非常严谨的,特别是整个程 序的构架体现出了它灵活容易扩展的特性。主要体现在,它有别于普通的操作系统 引导程序,在BIOS启动时就直接去装载特定操作系统的模块或者内核,而是通过 BIOS的功能首先装载了一个属于自己的引导程序,也可以理解为 GRUB这个操作系 统的引导程序。也就是说在引导任何用户的操作系统之前GRUB首先引导的是它的本 身。这样为将来的扩展性打下了非常好的基础。
由于GRUB采用了类似SHELL的方式来解释并运行用户设计好的脚本或者接受用户输 入的指令,并且为用户提供了非常好的底层的方法接口,所以用户可以非常灵活的 组合指令来引导不同的操作系统,同时并不要求用户对底层的物理结构有非常高的 了解,只要能使用提供的指令就可以来操作配置多重启动的多个操作系统。并且, GRUB提供了非常好的人机交互界面,可以通过预先设置的指令,或者菜单来显示让 用户选择操作系统,相对来说就比较易用。
同时GRUB也提供了非常好的扩展性,这个也是由于GRUB特殊的结构保证的。首先 GRUB是一个开源的项目,它的源代码是向所有的用户和开发着公开的,这样无论是 用户的需求发生了变化,还是硬件的标准得到了提升,都能很快的在GRUB中得到实 现。其次,GRUB的指令是非常容易添加的,用户只要了解了 GRUB指令的格式,就 能非常容易的在GRUB原始指令的基础上添加属于自己的指令,这样的设计相当程度 上提高了程序的模块化,耦合度比较低。