日期:2014-05-16  浏览次数:20989 次

linux启动过程浅析(3)

?

这是本文得第三部分,在前两部分中,我已经讲述了Linux操作系统是如何被机器boot到,并且load到制定的内存地址的.我们将继续第二部分的内

容,看看操作系统在完成了bootsect.s和setup.s的运行后,在head.s中做了些什么.
让我们回忆一下,在setup.s中,我们把整个system模块从地址0x10000出往下移动了0x10000的距离,也就是说,现在system模块已经位于0x0000地

址上了.而且,由于head.s会被编译到system模块的最前处,所以在head.s开始运行是,程序计数器指向的位置其实是0x0000处.
从这段程序开始,Linux应该算已经被正式load完成了,并且也顺利进入了保护模式.接下来的工作,就想所有具有一定规模的系统一样,需要开始

初始化了.我根据编程语会有对系统的进一步初始化言的区别,将Linux的初始化过程分为两部分.第一部分为head.s中的初始化工作,可以称之为

asm初始化.而接下来系统将会进入的main()函数中,,可以称之为c初始化.让我们先来看一看asm初始化的过程:

.text
.globl idt,gdt,pg_dir,tmp_floppy_area
pg_dir:
.globl startup_32
startup_32:
movl $0x10,%eax
mov %ax,%ds
mov %ax,%es
mov %ax,%fs
mov %ax,%gs
首先出现的这一段作用是把ds,es,fs,gs段寄存器的内容全部指向在setup.s中设置的GDT的数据段.是否还记得,在GDT中我们定义了三个段描述

符,第一个为全零,其实是弃之不用的,第二个与第三个都指向了地址0x0000处,分别为代码段和数据段. 需要注意的是,我们已经进入了保护模式

,也就是说现在段寄存器中存放的已经不应该是段的起始地址了,而是应该为段选择符. 在movl $0x10,%eax中,直接数0x10展开成二进制既

是:0000000000010000. 对于intel 80x86系列的CPU来说,选择符的0位和1位表示特权级别, 2位是TI(table indicator),它为0时表示使用GDT,

为1时表示使用LDT.而从3位到15位才是需要选择的描述符的index.如图:
+--------------------------------------------+
+ INDEX | TI | RPL |
+--------------------------------------------+
15 3 2 1 0
所以,0000000000010000从3位到15位实际上时0000000000010,既是2.也就是说,我们把ds,es,fs,gs全部设置成GDT的第二项.

接下来的一行:
lss stack_start,%esp
的意思是将stack_start放置到ss:esp中,既设置了堆栈起始指针.

然后:
call setup_idt
用来调用setup_idt子过程来设置中断描述符表.让我们来看一下中断这个过程中都做了些什么:
setup_idt:
lea ignore_int,%edx
movl $0x00080000,%eax
movw %dx,%ax/* selector = 0x0008 = cs */
movw $0x8E00,%dx/* interrupt gate - dpl=0, present */

lea idt,%edi
mov $256,%ecx
rp_sidt:
movl %eax,(%edi)
movl %edx,4(%edi)
addl $8,%edi
dec %ecx
jne rp_sidt
lidt idt_descr
ret
这段程序的前半部分是将中断描述符的内容存入eax,edx中. eax中存放的是中断描述符的低四字节的值,edx中存放的是中断描述符的高四字节

的值.我们可以看到,程序把ignore_int放入了eax的低16位中,这个位置正是描述符指向中断处理程序指针的存放地址.我们可以看一下

ignore_int子过程:
int_msg:
.asciz "Unknown interrupt\n\r"
.align 2
ignore_int:
pushl %eax
pushl %ecx
pushl %edx
push %ds
push %es
push %fs
movl $0x10,%eax
mov %ax,%ds
mov %ax,%es
mov %ax,%fs
pushl $int_msg
call printk
popl %eax
pop %fs
pop %es
pop %ds
popl %edx
popl %ecx
popl %eax
iret
其实这个过程任何实质性的工作都没有,仅仅是把int_msg打印了一遍.事实上,Linux在目前状态下,无意去做真正的中断向量表的初始化,而是把

所有的中断处理程序全部置位这个"哑"函数.真正的处理函数由各个模块按照自己需要自行初始化.
我们还可以看到,程序把0x0008放入了eax的高16位中(低16位被随后的复制语句重写成了ignor_int的相对地址),0x0008既是0000000000001000,

根据前文所讲的选择符的规则,既是指向GDT中第一个描述符.这很正确,因为我们在setup.s中把它初始化位了代码断描述符.
在setup_idt子过程的后半部分,我们遍历了整个IDT的范围,把每个中断描述符都设置成指向ignor_int的内容.最后我们把中断描述符表的头指

针load到机器中去(使用lidt操作).

又然后:
call setup_gdt
调用了setup_gdt子过程,顾名思义,就是设置全局描述符表的子过程.让我们也来看一下:
setup_gdt:
lgdt gdt_descr
ret
这里很简单,只是把gdt_descr的值load进来.因为我们并不需要完全设置gdt的256项. gdt的头几项是直接hard code的:
gdt_descr:
.word 256*8-1# so does gdt (not that that's any
.long gdt# magic number, but it works for me :^)

gdt:.quad 0x0000000000000000/* NULL descriptor */
.quad 0x00c09a0000000fff/* 16Mb */
.quad 0x00c0920000000fff/* 16Mb */
.quad 0x0000000000000000/* TEMPORARY - don't use */
.fill 252,8,0/* space for LDT's and TSS's etc */
到现在为止,Linux已经有了新的gdt了,在setup.s中的gdt将不在被使用.
我们可以看一下Linux现在的gdt表中都有些什么:
第一项:.quad 0x0000000000000000这是一向全零的值,事实上,GDT的第一项都是不被使用的.
第二项:.quad 0x00c09a0000000fff,先看最低的16位:0x0fff,这个位置的值表示段限长,单位是4K(这是由颗粒度位,23位决定的).所以段现场

为16M.事实上第二与第三个全局描述符其实是系统级别的代码运行需要的代码断和数据段,而0.11版本的linux又最多支持16Mb的内存. 因为系

统在最高级别上应该对所有内存区域都具有掌控权,所以将段长设置为了16M.段描述符的基址其实由这64位中的32位指出,但是这32位被分别存

放在了不同的3段中,分别是16-31,32-39,24-31位上,由于这些位在第二项与第三项上全为0,所以基址既是0x0000.这也很正确,因为现在system

模块的确已经在0x0000其实的地址上了.另外,在其中,还由一些段的类型,特权级别等信息.最后,这个段描述符的含义就是从0x0000处其实的,拥

有最高级别特权的,长度为16M(覆盖整个内存区域)的段.
第三项:.quad 0x00c0920000000fff,与第二项的差别仅仅是段的类型不同,分别为代码段和数据段