系统启动——Grub篇(二)

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语言代码的入口,是Cmain函数前的初始化代码。
在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原始指令的基础上添加属于自己的指令,这样的设计相当程度上提高了程序的模块化,耦合度比较低。 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值