分享
 
 
 

FreeBSD的Loader和内核初始化

王朝system·作者佚名  2008-05-19
窄屏简体版  字體: |||超大  

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

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有