FreeBSD
loader也是一個 BTX 客戶,在這裡不作詳述。已有一部內容全面的手冊 loader(8) ,由Mike Smith書寫。比loader更底層的BTX的機理已經在前面討論過。 loader 的主要任務是引導內核。當內核被裝入內存後,即被loader調用:
sys/boot/common/boot.c:
/* 從loader中調用內核中對應的exec程序 */
module_formats[km->m_loader]->l_exec(km);loader跳轉至哪裡呢?那就是內核的入口點。讓我們來看一下鏈接內核的命令:sys/conf/Makefile.i386:
ld -elf -Bdynamic -T /usr/src/sys/conf/ldscript.i386 -export-dynamic -dynamic-linker /red/herring -o kernel -X locore.o
在這一行中有一些有趣的東西。首先,內核是一個ELF動態鏈接二進制文件,可是動態鏈接器卻是/red/herring,一個莫須有的文件。其次,看一下文件sys/conf/ldscript.i386,可以對理解編譯內核時ld的選項有一些啟發。閱讀最前幾行,字符串sys/conf/ldscript.i386:
ENTRY(btext)
表示內核的入口點是符號 `btext'。這個符號在locore.s中定義:sys/i386/i386/locore.s:
.text
/**********************************************************************
*
* This is where the bootblocks start us, set the ball rolling...
* 入口
*/
NON_GPROF_ENTRY(btext)
首先將寄存器EFLAGS設為一個預定義的值0x00000002,然後初始化所有段寄存器:sys/i386/i386/locore.s
/* 不要相信BIOS給出的EFLAGS值 */
pushl $PSL_KERNEL
popfl
/*
* 不要相信BIOS給出的%fs、%gs值。相信引導過程中設定的%cs、%ds、%es、%ss值
*/
mov %ds, %ax
mov %ax, %fs
mov %ax, %gs
btext調用例程recover_bootinfo(),identify_cpu(),create_pagetables()。
這些例程也定在locore.s之中。這些例程的功能如下:recover_bootinfo
這個例程分析由引導程序傳送給內核的參數。引導內核有3種方式:
由loader引導(如前所述), 由老式磁盤引導塊引導,無盤引導方式。
這個函數決定引導方式,並將結構struct bootinfo存儲至內核內存。
identify_cpu 這個函數偵測CPU類型,將結果存放在變量_cpu中。
create_pagetables 這個函數為分頁表在內核內存空間頂部分配一塊空間,
並填寫一定內容 下一步是開啟VME(如果CPU有這個功能):
testl $CPUID_VME, R(_cpu_feature)
jz 1f
movl %cr4, %eax
orl $CR4_VME, %eax
movl %eax, %cr4
然後,啟動分頁模式:/* Now enable paging */
movl R(_IdlePTD), %eax
movl %eax,%cr3 /* load ptd addr into mmu */
movl %cr0,%eax /* get control word */
orl $CR0_PE|CR0_PG,%eax /* enable paging */
movl %eax,%cr0 /* and let's page NOW! */
由於分頁模式已經啟動,原先的實地址尋址方式隨即失效。
隨後三行代碼用來跳轉至虛擬地址: pushl $begin
/* jump to high virtualized address */
ret
/* 現在跳轉至KERNBASE,那裡是操作系統內核被鏈接後真正的入口 */
begin:
函數init386()被調用;隨參數傳遞的是一個指針,指向第一個空閒物理頁。
隨後執行mi_startup()。init386是一個與硬件系統相關的初始化函數,
mi_startup()是個與硬件系統無關的函數(前綴'mi_'表示Machine Independent,
不依賴於機器)。內核不再從mi_startup()裡返回;調用這個函數後,
內核完成引導:sys/i386/i386/locore.s:
movl physfree, %esi
pushl %esi /* 送給init386()的第一個參數 */
call _init386 /* 設置386芯片使之適應UNIX工作 */
call _mi_startup /* 自動配置硬件,掛接根文件系統,等 */
hlt /* 不再返回到這裡! */
1.7.1 init386()init386()定義在sys/i386/i386/machdep.c中,它針對Intel 386芯片進行低級初始化。loader已將CPU切換至保護模式。
loader已經建立了最早的任務。譯者注: 每個"任務"都是與其它“任務”相對獨立的執行環境。
任務之間可以分時切換,這為並發進程/線程的實現提供了必要基礎。對於Intel 80x86任務的描述,在這個任務中,內核將繼續工作。在討論其代碼前,
我將處理器對保護模式必須完成的一系列准備工作一並列出:
初始化內核的可調整參數,這些參數由引導程序傳來准備GDT(全局描述符表)
准備IDT(中斷描述符表)初始化系統控制台初始化DDB(內核的點調試器),如果它被編譯進內核的話初始化TSS(任務狀態段)准備LDT(局部描述符表)建立proc0(0號進程,即內核的進程)的pcb(進程控制塊)init386()首先初始化內核的可調整參數,這些參數由引導程序傳來。先設置環境指針(environment pointer, envp)調用,再調用init_param1()。
envp指針已由loader存放在結構bootinfo中:sys/i386/i386/machdep.c:
kern_envp = (caddr_t)bootinfo.bi_envp + KERNBASE;
/* 初始化基本可調整項,如hz等 */
init_param1();
init_param1()定義在sys/kern/subr_param.c之中。這個文件裡有一些sysctl項,
兩個函數,init_param1()和init_param2()。這兩個函數從init386()中調用:sys/kern/subr_param.c
hz = HZ;
TUNABLE_INT_FETCH("kern.hz", &hz);
TUNABLE__FETCH用來獲取環境變量的值:/usr/src/sys/sys/kernel.h
#define TUNABLE_INT_FETCH(path, var) getenv_int((path), (var))
Sysctlkern.hz是系統時鐘頻率。同時,這些sysctl項被init_param1()設定:
kern.maxswzone, kern.maxbcache, kern.maxtsiz, kern.dfldsiz, kern.dflssiz,
kern.maxssiz, kern.sgrowsiz。然後init386() 准備全局描述符表(Global Descriptors Table, GDT)。
在x86上每個任務都運行在自己的虛擬地址空間裡,這個空間由"段址:偏移量"的數對指定。
舉個例子,當前將要由處理器執行的指令在 CS:EIP,那麼這條指令的線性虛擬地址就是“代碼段虛擬段地址CS” + EIP。為了簡便,段起始於虛擬地址0,終止於界限4G字節。所以,在這個例子中,指令的線性虛擬地址正是EIP的值。
段寄存器,如CS、DS等是選擇符,即全局描述符表中的索引(更精確的說,索引並非選擇符的全部,而是選擇符中的INDEX部分)。譯者注: 對於80386,選擇符有16位,INDEX部分是其中的高13位。
FreeBSD的全局描述符表為每個CPU保存著15個選擇符:sys/i386/i386/machdep.c:
union descriptor gdt[NGDT * MAXCPU]; /* 全局描述符表 */
sys/i386/include/segments.h:
/*
* 全局描述符表(GDT)中的入口
*/
#define GNULL_SEL 0 /* 空描述符 */
#define GCODE_SEL 1 /* 內核代碼描述符 */
#define GDATA_SEL 2 /* 內核數據描述符 */
#define GPRIV_SEL 3 /* 對稱多處理(SMP)每處理器專有數據 */
#define GPROC0_SEL 4 /* Task state process slot zero and up, 任務狀態進程 */
#define GLDT_SEL 5 /* 每個進程的局部描述符表 */
#define GUSERLDT_SEL 6 /* 用戶自定義的局部描述符表 */
#define GTGATE_SEL 7 /* 進程任務切換關口 */
#define GBIOSLOWMEM_SEL 8 /* BIOS低端內存訪問(必須是這第8個入口) */
#define GPANIC_SEL 9 /* 會導致全系統異常中止工作的任務狀態 */
#define GBIOSCODE32_SEL 10 /* BIOS接口(32位代碼) */
#define GBIOSCODE16_SEL 11 /* BIOS接口(16位代碼) */
#define GBIOSDATA_SEL 12 /* BIOS接口(數據) */
#define GBIOSUTIL_SEL 13 /* BIOS接口(工具) */
#define GBIOSARGS_SEL 14 /* BIOS接口(自變量,參數) */
請注意,這些#defines並非選擇符本身,而只是選擇符中的INDEX域,
因此它們正是全局描述符表中的索引。例如,內核代碼的選擇符(GCODE_SEL)的值為0x08。
下一步是初始化中斷描述符表(Interrupt Descriptor Table, IDT)。
這張表在發生軟件或硬件中斷時會被處理器引用。例如,執行系統調用時,用戶應用程序提交INT 0x80 指令。這是一個軟件中斷,處理器用索引值0x80在中斷描述符表中查找記錄。
這個記錄指向處理這個中斷的例程。在這個特定情形中,這是內核的系統調用關口。
譯者注: Intel 80386支持“調用門”,可以使得用戶程序只通過一條call指令就調用內核中的例程。
可是FreeBSD並未采用這種機制,也許是因為使用軟中斷接口可免去動態鏈接的麻煩吧。
另外還有一個附帶的好處:在仿真Linux時,當遇到FreeBSD內核不支持的而又並非關鍵性的系統調用時,內核只會顯示一些出錯信息,這使得程序能夠繼續運行;而不是在真正執行程序之前的初始化過程中
就因為動態鏈接失敗而不允許程序運行。中斷描述符表最多可以有256 (0x100)條記錄。
內核分配NIDT條記錄的內存給中斷描述符表,這裡NIDT=256,是最大值:sys/i386/i386/machdep.c:
static struct gate_descriptor idt0[NIDT];
struct gate_descriptor *idt = &idt0[0]; /* 中斷描述符表 */
每個中斷都被設置一個合適的中斷處理程序。系統調用關口INT 0x80也是如此:sys/i386/i386/machdep.c:
setidt(0x80, &IDTVEC(int0x80_syscall),
SDT_SYS386TGT, SEL_UPL, GSEL(GCODE_SEL, SEL_KPL));
所以當一個用戶應用程序提交INT 0x80指令時,全系統的控制權會傳遞給函數_Xint0x80_syscall,這個函數在內核代碼段中,將被以管理員權限執行。然後,
控制台和DDB(調試器)被初始化:sys/i386/i386/machdep.c:
cninit();
/* 以下代碼可能因為未定義宏DDB而被跳過 */
#ifdef DDB
kdb_init();
if (boothowto & RB_KDB)
Debugger("Boot flags requested debugger");
#endif
任務狀態段(TSS)是另一個x86保護模式中的數據結構。當發生任務切換時,任務狀態段用來讓硬件存儲任務現場信息。局部描述符表(LDT)用來指向用戶代碼和數據。
系統定義了幾個選擇符,指向局部描述符表,它們是系統調用關口和用戶代碼、用戶數據選擇符:/usr/include/machine/segments.h
#define LSYS5CALLS_SEL 0 /* Intel BCS強制要求的 */
#define LSYS5SIGR_SEL 1
#define L43BSDCALLS_SEL 2 /* 尚無 */
#define LUCODE_SEL 3
#define LSOL26CALLS_SEL 4 /* Solaris >=2.6版系統調用關口 */
#define LUDATA_SEL 5
/* separate stack, es,fs,gs sels ? 分別的棧、es、fs、gs選擇符? */
/* #define LPOSIXCALLS_SEL 5*/ /* notyet, 尚無 */
#define LBSDICALLS_SEL 16 /* BSDI system call gate, BSDI系統調用關口 */
#define NLDT (LBSDICALLS_SEL + 1)
然後,proc0(0號進程,即內核所處的進程)的進程控制塊(Process Control Block)
(struct pcb)結構被初始化。proc0是一個struct proc 結構,描述了一個內核進程。
內核運行時,該進程總是存在,所以這個結構在內核中被定義為全局變量:sys/kern/kern_init.c:
struct proc proc0;
結構struct pcb是proc結構的一部分,它定義在/usr/include/machine/pcb.h之中,內含針對i386硬件結構專有的信息,如寄存器的值。1.7.2 mi_startup()這個函數用冒泡排序算法,將所有系統初始化對象,然後逐個調用每個對象的入口:sys/kern/init_main.c:
for (sipp = sysinit; *sipp; sipp++) {
/* ... 省略 ... */
/* 調用函數 */
(*((*sipp)->func))((*sipp)->udata);
/* ... 省略 ... */
}
盡管sysinit框架已經在《FreeBSD開發者手冊》中有所描述,我還是在這裡討論一下其內部原理。
每個系統初始化對象(sysinit對象)通過調用宏建立。讓我們以announce sysinit對象為例。
這個對象打印版權信息:sys/kern/init_main.c:
static void
print_caddr_t(void *data __unused)
{
printf("%s", (char *)data);
}
SYSINIT(announce, SI_SUB_COPYRIGHT, SI_ORDER_FIRST, print_caddr_t, copyright)
這個對象的子系統標識是SI_SUB_COPYRIGHT(0x0800001),數值剛好排在SI_SUB_CONSOLE(0x0800000)後面。所以,版權信息將在控制台初始化之後就被很早的打印出來。
讓我們看一看宏SYSINIT()到底做了些什麼。它展開成宏C_SYSINIT()。
宏C_SYSINIT()然後展開成一個靜態結構struct sysinit。
結構裡申明裡調用了另一個宏DATA_SET:/usr/include/sys/kernel.h:
#define C_SYSINIT(uniquifier, subsystem, order, func, ident) static struct sysinit uniquifier ## _sys_init = { \ subsystem, order, \ func, \ ident \ }; \ DATA_SET(sysinit_set,uniquifier ##
_sys_init);
#define SYSINIT(uniquifier, subsystem, order, func, ident) C_SYSINIT(uniquifier, subsystem, order, (sysinit_cfunc_t)(sysinit_nfunc_t)func, (void *)ident)
宏DATA_SET()展開成MAKE_SET(),宏MAKE_SET()指向所有隱含的
sysinit幻數:/usr/include/linker_set.h
#define MAKE_SET(set, sym) static void const * const __set_##set##_sym_##sym = &sym; __asm(".section .set." #set ",\"aw\""); __asm(".long " #sym); __asm(".previous")
#endif
#define TEXT_SET(set, sym) MAKE_SET(set, sym)
#define DATA_SET(set, sym) MAKE_SET(set, sym)
回到我們的例子中,經過宏的展開過程,將會產生如下聲明:
static struct sysinit announce_sys_init = {
SI_SUB_COPYRIGHT,
SI_ORDER_FIRST,
(sysinit_cfunc_t)(sysinit_nfunc_t) print_caddr_t,
(void *) copyright
};
static void const *const __set_sysinit_set_sym_announce_sys_init =
&announce_sys_init;
__asm(".section .set.sysinit_set" ",\"aw\"");
__asm(".long " "announce_sys_init");
__asm(".previous");
第一個__asm指令在內核可執行文件中建立一個ELF節(section)。這發生在內核鏈接的時候。
這一節將被命令為.set.sysinit_set。這一節的內容是一個32位值——announce_sys_init結構的地址,這個結構正是第二個__asm指令所定義的。第三個__asm指令標記節的結束。
如果前面有名字相同的節定義語句,節的內容(那個32位值)將被填加到已存在的節裡,這樣就構造出了一個32位指針數組。用objdump察看一個內核二進制文件,也許你會注意到裡面有這麼幾個小的節:% objdump -h /kernel
7 .set.cons_set 00000014 c03164c0 c03164c0 002154c0 2**2
CONTENTS, ALLOC, LOAD, DATA
8 .set.kbddriver_set 00000010 c03164d4 c03164d4 002154d4 2**2
CONTENTS, ALLOC, LOAD, DATA
9 .set.scrndr_set 00000024 c03164e4 c03164e4 002154e4 2**2
CONTENTS, ALLOC, LOAD, DATA
10 .set.scterm_set 0000000c c0316508 c0316508 00215508 2**2
CONTENTS, ALLOC, LOAD, DATA
11 .set.sysctl_set 0000097c c0316514 c0316514 00215514 2**2
CONTENTS, ALLOC, LOAD, DATA
12 .set.sysinit_set 00000664 c0316e90 c0316e90 00215e90 2**2
CONTENTS, ALLOC, LOAD, DATA
這一屏信息顯示表明節.set.sysinit_set有0x664字節的大小,所以0x664/sizeof(void *)個sysinit對象被編譯進了內核。
其它節,如.set.sysctl_set表示其它鏈接器集合。
通過定義一個類型為struct linker_set的變量,節.set.sysinit_set將被“收集”
到那個變量裡:sys/kern/init_main.c:
extern struct linker_set sysinit_set; /* XXX */
struct linker_set定義如下:/usr/include/linker_set.h:
struct linker_set {
int ls_length;
void *ls_items[1]; /* ls_length個項的數組, 以NULL結尾 */
};
譯者注: 實際上是說,用C語言結構體linker_set來表達那個ELF節。
第一項是sysinit對象的數量,第二項是一個以NULL結尾的數組,數組中是指向那些對象的指針。回到對mi_startup()的討論,我們清楚了sysinit對象是如何被組織起來的。函數mi_startup()將它們排序,並調用每一個對象。最後一個對象是系統調度器:/usr/include/sys/kernel.h:
enum sysinit_sub_id {
SI_SUB_DUMMY = 0x0000000, /* 不被執行,僅供鏈接器使用 */
SI_SUB_DONE = 0x0000001, /* 已被處理*/
SI_SUB_CONSOLE = 0x0800000, /* 控制台*/
SI_SUB_COPYRIGHT = 0x0800001, /* 最早使用控制台的對象 */
...
SI_SUB_RUN_SCHEDULER = 0xfffffff /* 調度器:不返回 */
};
系統調度器sysinit對象定義在文件sys/vm/vm_glue.c中,這個對象的入口點是scheduler()。
這個函數實際上是個無限循環,它表示那個進程標識(PID)為0的進程——swapper進程。
前面提到的proc0結構正是用來描述這個進程。
第一個用戶進程是init,由sysinit對象init建立:sys/kern/init_main.c:
static void
create_init(const void *udata __unused)
{
int error;
int s;
s = splhigh();
error = fork1(&proc0, RFFDG | RFPROC, &initproc);
if (error)
panic("cannot fork init: %d\n", error);
initproc->p_flag |= P_INMEM | P_SYSTEM;
cpu_set_fork_handler(initproc, start_init, NULL);
remrunqueue(initproc);
splx(s);
}
SYSINIT(init,SI_SUB_CREATE_INIT, SI_ORDER_FIRST, create_init, NULL)
create_init()通過調用fork1()分配一個新的進程,但並不將其標記為可運行。
當這個新進程被調度器調度執行時,start_init()將會被調用。
那個函數定義在init_main.c中。它嘗試裝載並執行二進制代碼init,先嘗試/sbin/init,然後是/sbin/oinit,/sbin/init.bak,最後是/stand/sysinstall:sys/kern/init_main.c:
static char init_path[MAXPATHLEN] =
#ifdef INIT_PATH
__XSTRING(INIT_PATH);
#else
"/sbin/init:/sbin/oinit:/sbin/init.bak:/stand/sysinstall";
#endif