本文分析Minix3的BootLoader。
* masterboot.s
该汇编文件中包含了主引导代码。这个代码一般被放在软盘、硬盘的第一个扇区。对于硬盘而言,这段代码
就是其主引导记录(MBR)。总体来讲,这段代码的主要工作如下:
1. 如果启动设备是一个硬盘,并且它有一个分区是活动分区,启动那个活动分区;
2. 否则,启动下一个软盘或者硬盘;
举例:一个可能的启动顺序是:
/dev/fd0 -- 尝试软盘0;
/dev/fd1 -- 尝试软盘1;
/dev/c0d0 -- 尝试硬盘0,其MBR找到并选择启动活动分区(主分区)2;
/dev/c0d0p2 -- 扩展分区的主启动记录,找到并选择活动扩展分区0;
/dev/c0d0p2s0 -- Minix启动快,读取并运行启动管理器/boot/boot;
想要理解这个文件中的代码,需要对磁盘的MBR有一定的了解(典型的MBR,包括本文件中的代码都如此)。
MBR是主引导记录(Master Boot Record)的缩写,它是一个“可分区存储设备”,例如硬盘,的第一个扇区。
MBR的大小是512Bytes。MBR的主要内容/工作包括:
1. 记录磁盘的分区表;
2. 负责启动OS;
3. 提供磁盘签名(用于标记磁盘类型);
MBR有一个签名,“AA55”,在little-endian上是"55 AA"。在IA32系统上的一个典型的用例是:
1. BIOS完成加电自检后,调用int 19。
2. int 19尝试读取软盘的启动扇区,并将其加载到内存0000:7c00处,然后跳转到0000:7c00执行;
3. 如果软盘上没有找到启动扇区,int 19会尝试将第一个硬盘的MBR读取到内存的0000:7c00,并跳转到
该地址执行;
4. MBR会找到第一个活动分区,并将那个分区的引导扇区读取到0000:7c00,跳转0000:7c00开始执行;
5. 活动分区的引导扇区负责继续启动操作系统,每个操作系统的引导扇区都可能不同;
详细的磁盘、引导信息,网上有很多说明,此处不再赘述。
从updateboot.sh脚本可以看出,/boot/boot是启动监视器,而bootblock则是minix的boot sector的代码。
* bootblock.s
这是minix的引导扇区,在MBR被执行后,最终会找到这个扇区,并把它加载到0000:0x7c00处执行。该文件
中的代码负责启动boot程序(boot被加载到地址为0x1000的内存位置),即Minix3的启动监视器。/boot/boot
本身在磁盘上的地址会被installboot工具patch到bootblock中,包括24位的起始扇区号和8位的长度(扇区数)。
由此看出,真正启动minix的是boot程序。从编译脚本看出,boot程序直接依赖的几个程序是boothead.s,
boot.[ch],bootimage.c,rawfs.[ch]。
bootblock执行的最后一步是启动boot程序,它执行完跳转后,实际上跳转到了boothead.s中的代码开始执行。
boothead.s最后会启动boot.c中的代码,入口是boot()函数。
* boothead.s
该文件包括两类内容:
1. 启动代码,这部分首先设置一些C变量,之后会调用boot.c中的boot()函数;
2. 底层处理函数定义,这部分定义了一些公用的函数,可供boot.c调用,主要处理一些低级的事务,例如
磁盘/tty/键盘IO,内存拷贝等。这些函数都是用BIOS实现。
启动代码的主要工作包括:
1. 根据编译器给的a.out的header设置寄存器状态(代码和数据仍然在0x1000附近);
2. 设置启动参数到C定义的变量,如_device、_rem_part、_cdbooted等;
3. 记录当前的vedio模式;
4. 保存代码段、数据段的绝对地址到C定义的变量_caddr、_daddr中;
5. 将boot程序运行所占用的总大小放入C定义的变量_runsize中;
6. 探测内存entries,及地址空间信息;
7. 调用_boot()函数,进入C代码。
* rawfs.[hc]
由于在BIOS模式下没有文件系统,这个模块用于提供简单的对(Raw Minix Filesyste)文件系统的访问操作。
使得boot.c可以读取文件系统中的启动映像文件和其它必须的文件。该模块唯一依赖的一个外部函数是
readblock(),用于读取文件系统的一个块。该函数由boot.c提供(后者由依赖与boothead.s)。
该模块几个主要的函数是:
1. r_super():读取超级块并初始化一些内部数据结构;
2. r_stat():获取指定的ino对应的文件的状态信息;
3. r_vir2abs():将一个文件内的块号转换为磁盘的物理块号;
4. r_lookup():将一个路径名转换成一个ino号;
5. r_readdir():读取一个指定目录的下一个目录项;
* boot.[hc]
该文件实现了minix3的boot monitor。它有两个入口:如果是在系统启动后执行的(及POSIX环境下),它的
入口是main()函数。而如果是一个系统正在启动时执行的,此时没有POSIX环境,则在BIOS环境下执行,入口是
boot()函数。(这两个入口不是同时可用的,在BIOS环境下,其运行在16位非保护模式下。每次编译只能生成
一个二进制文件,要么在BIOS下执行,要么在POSIX下执行)。
在POSIX下执行时,boot调用的外部函数,例如exit()等,有当前系统的标准库提供;而在BIOS下执行时,
则是由相应的boothead.s提供的。
该文件实现的是monitor,可以接收用户命令并作相应的事情。而加载和启动minix的代码在bootimage.c中,
后者有一个函数bootminix(),是入口。
下面,我们重点分析BIOS环境下的boot.c文件,当涉及到常量或其它定义时,可能谈到boot.h文件。文件头
不将EXTERN定义为空,可见,在boot.h中声明的那些全局变量都是实现在该文件中的。
boot()函数的工作分为三步:
1. 调用initialize()初始化一些全局的数据结构;
2. 调用get_parameters()从参数扇区获取启动参数;
3. 进入一个循环:
a. 如果当前命令不为空,调用execute()执行命令;
b. 调用monitor()等待并读取/分析下一个/组用户输入的命令;
由上可见,boot很重要的一个功能就是读取、分析并执行命令和命令序列。在进一步分析各个函数的主要
功能之前,我们先看看boot支持命令的方式。
** boot monitor command
所有支持的命令有resnames定义,包括一组枚举值和对应的命令的字符串表示。Command的基本元素是token,
由token结构体表示。
我们来大体看一下这几个函数。
** initialize()
1. 初始化mem[]表项:
它是由boothead.s探索出来的内存区段中类型为1的前三个构成的,应该就是程序可以使用的内存;
2. 将boot的代码、数据移动到低端内存(mem[0])的最后部分,如果移动的结果会跨越640K那个地址,
则移动到640K之前的最后部分,这是为minix内核的加载腾出空间;
3. 调用relocate()函数后,boot.c继续执行,但执行的是移动后的代码了;
4. 如果mem[1]的大小超过512K,那么,会保持boot monitor一直驻留内存,通过把它占用的地址空间
从mem[0]中去掉而做到,由此可见,mem数组是后续内核使用的内存映射表;
5. 去掉mem[0]前2K的空间;
6. 初始化bootdev结构体;
** get_parameters()
1. 设置全局的环境变量:
a. rootdev
b. ramimagedef
c. ramsize
d. processer
e. bus
f. video
g. chrome
h. memory
i. image
j. 等等;
2. 读取参数扇区,并分析,将参数扇区中的commands放入全局的cmds变量中,这些命令
会被boot直接执行,默认情况下,它们就是等待一段时间后启动minix。如果等待中有用户输入,
则取消minix的启动,进而执行用户的命令。
** execute()
执行当前cmds中的命令链。其中,最重要的一个命令是boot,它执行bootminix()方法,用于启动minix。
** monitor()
打印提示符,读取一行命令并分析。
至此,真正启动minix之前的流程都走完了。而bootminix()方法则会真正的加载minix的image并最终将
控制权交给真正的minix系统。
待续......