快捷搜索:

FreeBSD的Loader和内核初始化

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:

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接口(自变量,参数) */

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:

宏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

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: %dn", 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

您可能还会对下面的文章感兴趣: