第五章 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注冊的第一個接口,並返回該接口地址。