第五章 OSKIT的启动及系统初始化
5.1 概述
OSKIT的初始化分为两个阶段,第一阶段是由称为Multiboot的装入程序先初始化CPU,检测系统内存等系统环境,将所得的结果存入一个结构中,并将该结构的地址传递给核心中的主函数,然后主函数根据这些参数完成系统的硬件初始化,构造出整个系统的运行环境,最后再初始化系统运行所需的软件数据结构,完成核心的初始化。
5.2 核心装入程序-Multiboot
5.2.1 Multiboot简介
Multiboot是为在x86PC上运行的32位操作系统所制定的装入规范。它所制定的原因是每个操作系统都需要做装入的工作,现在各个操作系统都要自己编写代码来完成这项工作。Multiboot实际上希望成为操作系统与装入程序的标准接口,那样,只要操作系统在编写时遵循这个规范,那么它就可以使用遵循Multiboot规范编写的出来的装入程序完成核心的装入工作,这样便可以节省大量的时间和人力,也避免了重复劳动。
5.2.2 Multiboot规范
Multiboot规范详细定义了操作系统/装入程序的接口中三个方面的内容,分别是?
?
* 装入程序所要装入的操作系统的核心映象格式
* 操作系统启动时机器所处的状态
* 装入程序传递给操作系统信息的格式
核心映象格式:操作系统格式分为两种,一种是a.out,另一种是elf。首先讨论a.out格式。由于各种操作系统中a.out格式是有区别的,例如linux中的ZMAGIC格式与mach中的ZMAGIC格式很难区分。因此,Multiboot规范指定了一个称为Multiboot_header的结构,存放在可执行文件的开始处,来获得不同的映象格式信息。而且这个结构必须完全包含在可执行文件的开始8192个字节处,这使得Multiboot装入程序能够发现a.out格式文件的文本段,而不必事先知道a.out中的详细变量。Multiboot_header的结构如下:
0 magic: 0x1BADB002 (必需)
4 flags (必需)
8 checksum (必需)
8 header_addr
12 load_addr
16 load_end_addr (只有标志中的第16位被设置才有效)
20 bss_end_addr
24 entry_addr
Multiboot_header中的magic是用来标识该结构的,其值必须是十六进制的0x1BADB002。Flag标志是用来标识操作系统要求装入程序提供的系统信息。其中,0到15位用来标识装入程序必须提供的信息,如果由于某种原因,装入程序不能提供这些信息,装入程序将不能完成系统的装入工作。而第16位到31位是可选的信息,如果装入程序不能得到这些信息,它将忽略这些位,继续装入工作。下面对这个标志中的各位进行解释。
如果标志中第0位被设置,则表明所有随操作系统装入的模块必须在4k的范围内。如果位1被设置,则表明multiboot_info结构中的内存部分有效。如果位16被设置,则表明multiboot_header中第8到24偏移量中的数据是有效的。由于第16位被设置而有效的地址信息都是物理地址。
Header_addr:存储着multiboot_header结构开始地址。
Load_addr:存储着正文段的开始物理地址,header_addr-load_addr得出的偏移量表明装入程序应该由核心映象文件中的什么位置开始装载操作系统。
Load_end_addr:存储着数据段的结束物理地址,(Load_end_addr - Load_addr)表明需要装载多少数据,在a.out格式的核心映象中,正文段和数据段是相接的。
Bss_end_addr:存储着bss段的结束物理地址,初始化程序将这段区域置为0,并保留它所占用的内存空间,防止其中放置了启动模块以及其它与操作系统相关的数据。
Entry:存储着操作系统的开始地址,装入程序应该跳转到该地址开始操作系统的执行。
Checksum是一个32位的无符号值,该值与multiboot_header中的其它必需项(flag,magic)相加时,应该得到一个32位的无符号0。
当32位操作系统开始运行时,机器应该处于以下状态:
·CS必需是一个32位的可读可执行的代码段,其偏移量为0,而且其最大可达到0xffffffff;
·DS,ES,FS,GS,SS必需是32位的可读可写数据段,其偏移量为0,而且其最大可达到0xffffffff;
·20根地址线必须可用于形成pc中标准的32位存储器地址;
·CPU中的页功能必须被关闭;
·CPU中的可中断标志将被置为不可中断;
·EAX寄存器中必须存放着magic值:0x2BADB002,这个值表明操作系统是由一个与multiboot相兼容的装入程序来完成装入的;
·EBX寄存器中必须存放着multiboot_info结构的32位物理地址,这个结构是由装入程序提供给操作系统的;
CPU中所有其它的寄存器和标志位都没有定义,这之中尤其需要注意的是:
·ESP,当需要自己的堆栈时,32位的操作系统必须立即创建一个;
·GDTR ,即使段寄存器如上面所说的那样设置,GDTR有可能是无效的,因此操作系统有可能无法读取任何段寄存器的值,直到操作系统设置好自己的GDTR;
·IDTR,操作系统必须禁止中断,直到它设置好自己的IDTR;
其它的机器状态应该处于"正常态",就像是由bios初始化后一样。即使装入程序在创建32位环境时改变了PIC的值,它必须使PIC的值还原为通常的BIOS/DOS值。
当操作系统开始运行时,EBX寄存器中存放着"multiboot_info"结构的物理地址,通过这个结构,装入程序向操作系统传递一些关键信息。操作系统可以使用或忽略该结构中的任何部分。Multiboot_info结构及其子结构可以由装入程序放置在内存中的任意位置(除了预留给核心和启动模块的内存空间)。操作系统负责不覆盖这个结构所占用的内存,直到它已经使用完该结构。
Multiboot_info结构的格式如下所示:
0 flags (必须)
4 mem_lower (标志位0设置时有效)
8 mem_upper (标志位0设置时有效)
12 boot_device (标志位1设置时有效)
16 cmdline (标志位2设置时有效)
20 mods_count (标志位3设置时有效)
24 mods_addr (标志位3设置时有效)
28 - 40 syms (标志位4或5设置时有效)
44 mmap_length (标志位6设置时有效)
48 mmap_addr (标志位6设置时有效)
如上所示,flags标识结构中的各个部分是否有效。Flags中所有未定义的位由装入程序置为0,而操作系统不理解的位将被忽略。Flags同时也起到对multiboot进行版本控制的作用,为将来multiboot_info结构的扩充留下余地。
如果flags中的位0被设置,则mem_lower和mem_upper将有效。Mem_lower和mem_upper分别以kb表示系统中低端和高端的内存数量。低端内存由地址0开始,高端内存由地址1兆开始。低端内存的最大值为640kb,高端内存的值是高端第一个内存空洞的地址减去1兆。这里,multiboot规范并不保证高端内存的返回值。
如果flags中的位1被设置,则boot_device将有效。Boot_device表明装入程序是从哪个bios磁盘设备装载操作系统。如果操作系统不是由bios磁盘设备装入,则该位将被清0。操作系统可以根据boot_device的值来确定它的根设备,但这不是必须的。
Boot_device区由四个子区组成,每个子区1字节,如下所示:
Driver|part1|part2|part3
Driver部分表明能被bios的INT13中断接口所能获得的磁盘设备。例如,0x00代表第一个软盘,0x80代表第一快硬盘。剩下的3个字节标识启动分区。
如果flags中的位2被设置,命令行启动参数将有效,cmdline部分包含着传递给核心的命令行参数所存放的物理地址。命令行采用C风格的null终结字符串格式。
如果flags中的位3被设置,mods区将告诉核心那些启动模块和核心映象一起被装入,以及这些模块的位置。Mods_count部分表明模块的数量。Mods_addr表明第一个模块结构的物理地址,每一个模块结构的格式如下:
0 mod_start 模块开始地址
4 mod_end 模块结束地址
8 string 模块标识字符串
12 reserve 保留
值得注意的是,位4和位5是互斥的。如果位4被设置,则multiboot_info中从第28字节处开始的4个部分有效。这4个部分为:
28 tabsize
32 strsize
36 addr
40 reserve
这4部分表明a.out核心映象格式中标志表的存放位置。其中,addr表明存放a.out核心映象格式中nlist结构数组大小的物理地址,tabsize与a.out格式中symbol部分的size参数相等,strsize与a.out格式中string部分的size参数相等。值得注意的是,即使位4被设置,tabsize也可以是0,表明没有symbol
如果位5被设置,则multiboot_info中从第28字节处开始的4个部分有效。这4个部分为:
28 num
32 size
36 addr
40 shndx
这些部分表明在elf格式的核心映象中,header表的位置以及表中各项的大小,表项的数量,以及作为索引的字符串表格。这部分的每一项都与elf格式程序的header部分中以shdr开始的部分相对应。
如果位6被设置,则multiboot_info中mmap_length,mmap_addr部分有效。这两部分标明了由bios所提供的机器内存分布图所存放的地址以及大小。内存分布图中存放着一个或多个size/struture对,size被用来跳到下一个size/struture。每一个size/struture对的结构如下:
-4 size
1 BaseAddrLow
4 BaseAddrHigh
5 LengthLow
6 LengthHigh
16 Type
Size表明该结构的字节大小。
BaseaddrLow表明低32位开始地址。
BaseAddrHigh表明高32位开始地址,因此开始地址一共是64位。
LengthLow表明内存大小的低32位。
LengthHigh表明内存大小的高32位,因此内存大小的长度也是64位。
Type是一个代表地址范围的变量,它的值为1表示可供使用的RAM。
其它值现在还未定义。
5.2.3 Multiboot进行初始化的步骤
Multiboot的启动过程分为四个步骤:首先,执行汇编程序multiboot.S,初始化结构boot_info,并将该结构开始地址存入寄存器ebx,然后调用multiboot_main函数。
Multiboot_main函数根据boot_info结构初始化机器的硬件环境,例如完成对内存的管理等,接着调用main函数。
Main函数将核心映象由硬盘或软盘上装入内存的一个临时位置,稍后它会被复制到其最终位置。建立一个新的multiboot_info结构,退出时以新multiboot_info结构地址为参数调用boot_start函数。
Boot_start函数将核心映象复制到最终位置,然后开始操作系统核心的执行。
5.3 OSKIT中软件环境的初始化
OSKIT的硬件环境初始化是指系统装入程序完成系统的硬件初始化工作,例如给CPU中的寄存器赋初值,检测系统的内存大小并完成有关的设置工作,操作系统将获得一个32位的运行环境。软件初始化所做的工作就是在此之后初始化系统所必须的数据结构,为下面系统中各个模块的运转作好准备
5.3.1 软件环境初始化的步骤
OSKIT中的软件初始化分为两类:
* 一类是单线程系统初始化
* 另一类是多线程系统初始化
在这里只对单线程系统的初始化进行讨论。
单线程系统的初始化主要完成以下工作:
·获得内存接口指针
·创建全局数据库
·将内存接口在全局数据库中注册
·创建C库环境,获得C库接口指针
·将C库接口在全局数据库中注册
·在全局数据库中获得C库接口指针
5.3.2 软件环境初始化的实现
OSKIT的整个软件初始化由函数oskit_clientos_init完成,现在就对这个函数及其子函数进行分析。下面是oskit_clientos_init的源程序:
oskit_error_t oskit_clientos_init(void)
{
oskit_libcenv_t *libcenv; /* 定义C库接口指针 */
oskit_mem_t *memi; /* 定义存储接口指针 */
/* 获得存储接口指针 */
memi = oskit_mem_init();
assert(memi);
/* 使用上面获得的存储接口创建全局数据库 */
if (oskit_global_registry_create(memi))
panic("oskit_clientos_init: Problem creating global
registry");
/*将存储接口在全局数据库中注册*/
oskit_register(&oskit_mem_iid, (void *) memi);
/* 创建C库环境,并将C库接口在全局数据库中注册*/
oskit_libcenv_create(&libcenv);
oskit_register(&oskit_libcenv_iid, (void *) libcenv);
/* 获得全局数据库中注册的C库接口 */
oskit_load_libc(oskit_get_services());
return 0;
}
现在对oskit_clientos_init实现的细节进行讨论:
·存储接口的初始化:mem_init,这个函数很简短如下所示
oskit_mem_t oskit_mem_init(void)
{ return &oskit_mem;
}
调用这个函数的目的是为了得到存储接口,下面是OSKIT中的存储接口:
static oskit_mem_t oskit_mem = { &mem_ops };
static struct oskit_mem_ops mem_ops = {
mem_query, /* 查询存储接口 */
mem_addref, /* 增加对存储接口的引用 */
mem_release, /* 释放对存储接口的引用 */
mem_alloc, /* 分配内存空间 */
mem_realloc, /* 改变先前分配的空间的大小 */
mem_alloc_aligned, /* 分配页倍数大小的内存空间 */
mem_free, /* 释放内存空间 */
mem_getsize, /* 返回某内存节点的大小 */
mem_alloc_gen, /* 分配一段符合指定地址和尺寸限制的内存*/
mem_avail, /* 获得某内存对象中空闲空间的大小*/
mem_dump, /* 返回某内存对象中的所有内存区域,供调试用*/
};
该接口中的每个成员都是一个存储管理函数的地址,分别实现注释所述的功能。mem_init函数被首先调用的原因是操作系统中几乎所有数据结构的建立都需要分配内存空间(除去在初始化前已经用静态声明了的结构),而要分配空间必须先得到内存对象的接口,因此这个函数被首先调用。
·创建全局数据库:oskit_global_registry_create,该函数如下所示:
oskit_error_t oskit_global_registry_create(
oskit_mem_t *memobject)
{
oskit_error_t rc;
assert(memobject);
/* 创建全局数据库,并获得该数据库的服务接口 */
if ((rc = oskit_services_create(memobject,
&global_registry)))
return rc;
return 0;
}
全局数据库是操作系统中为记录与系统直接相关的接口使用情况所建立的数据库,系统中使用的关键接口都需要在该数据库中注册。有关数据库以及接口注册的问题请参考第4章的有关内容,这里不再重复。
·初始化C库环境:oskit_libcenv_create,该函数如下
static struct genv default_libcenv;
oskit_error_t oskit_libcenv_create (
oskit_libcenv_t **out_iface )
{
struct genv *g = &default_libcenv;/*给genv类型指针g赋值*/
if (g->count) {
*out_iface = &g->libcenvi;/*若该结构已经初始化则返回*/
return 0;
}
g->libcenvi.ops = &libcenv_ops;
/* 给结构g中C库接口指针赋值 */
g->count = 10000;
/*给终端接口指针赋值*/
g->console = (oskit_ttystream_t *)
default_console_stream;
g->exit = oskit_libc_exit; /*给退出函数指针赋值*/
/*给信号初始化函数指针赋值*/
g->siginit = oskit_sendsig_init;
/*返回C库对象的接口指针
initial_clientos_libcenv = *out_iface = &g->libcenvi;
return 0;
}
Genv结构的定义是:
struct genv {
oskit_libcenv_t libcenvi; /* C库接口指针 */
int count; /* 引用计数 */
oskit_fsnamespace_t *fsn; /* 文件命名空间接口指针 */
char hostname[256]; /* 主机名 */
oskit_ttystream_t *console; /* 终端接口指针 */
void (*exit)(int); /* 退出函数指针 */
/* 信号初始化函数指针 */
void (*siginit) (int (*func)(int, int, void *));
#ifndef PTHREADS
/* 用于在单线程系统中 */
oskit_timer_t *sleep_timer;
oskit_osenv_sleep_t *sleep_iface;
osenv_sleeprec_t *sleeprec;
#endif
};
C库接口的结构如下:
static struct oskit_libcenv_ops libcenv_ops = {
libcenv_query, /* C库接口查询函数 */
libcenv_addref, /* C库接口增加引用函数 */
libcenv_release, /* C库接口释放函数 */
libcenv_getfsnamespace, /* 获得文件命名空间接口 */
libcenv_setfsnamespace, /* 设置文件命名空间接口 */
libcenv_gethostname, /* 获得主机名 */
libcenv_sethostname, /* 设置主机名 */
libcenv_exit, /* 应用程序退出函数 */
libcenv_setexit, /* 设置应用程序退出函数 */
libcenv_getconsole, /* 获得终端流接口 */
libcenv_setconsole, /* 设置终端流接口 */
libcenv_signals_init, /* POSIX的信号初始化函数 */
libcenv_setsiginit, /* 设置POSIX的信号初始化函数 */
libcenv_sleep_init, /* 睡眠结构初始化函数 */
libcenv_sleep, /* 睡眠函数 */
libcenv_wakeup, /* 唤醒函数 */
libcenv_clone, /* C库对象克隆函数 */
};
·在全局数据库中获得C库接口,完成这项工作需要调用两个函数:
oskit_get_services,它返回全局数据库的地址
oskit_load_lib,调用了函数oskit_services_lookup_first,在全局数据库中查找以oskit_libcenv_iid注册的第一个接口,并返回该接口地址。