Linux-0.11学习总结——引导启动程序部分
1. BIOS启动
Intel设计CPU开机加电即进入16位的实模式下运行,CS的值预设为0xFFFF,IP的值预设为0x0000,这样CS:IP指向了0xFFFF0的地址位置。BIOS程序地址只有8KB,范围为0xFE000~0xFFFFF。
BIOS程序主要实现了以下功能:
a) 构建中断向量表(0x00000~0x003FF)1KB内存空间;
b) BIOS数据区(0x00400~0x004FF)256字节
c) 在大约56KB以后的位置(0x0E2CE)构建中断向量服务程序(8KB)
其中,中断向量表中有256个中断向量,每个中断向量占4个字节,前两个字节为CS,后两个字节为IP。注意,此时的中断与后面保护模式下的中断不一样。
最后,BIOS程序通过int 0x19中断将软驱中第一扇区(512B)程序bootsect内容拷贝至0x07C00处。
2. Bootsect引导
在内存0x07C00处保存的bootsect程序主要完成的功能有三点:1将自身程序复制到0x90000;2加载软驱4个扇区内容setup到0x90200处;3加载软驱240个扇区system模块到0x10000(此时时间较长,故需要在屏幕上显示:Loading System…)
该部分有一个地方非常值得我学习:
jmpi go, INITSEG
go: mov ax, cs
…
这两行代码虽然简短,但非常精妙。Bootsect代码是在一边执行的过程中一边复制到0x90000,增加go一个偏移,使得跳转之后的程序可以在新的位置接着继续往下执行,而不是进入了一个不断复制的死循环。
Bootsect借助BIOS中断int 0x13,将setup.s与system模块分别加载到内存0x90200与0x00000中。最后,程序通过jmpi 0, SETUPSEG完成跳转。
3. Setup程序
该模块程序开始提取内核运行所需要的机器系统数据加载到0x90000~0x901FC位置,覆盖了原来的bootsect部分,这对后面的main函数执行发挥重要的作用!
接下来,setup将system模块的程序从0x10000复制到0x00000位置。然后再设置中断描述符表与全局描述符表。具体代码如下:
lidt [idt_48] ;load idt with 0,0
lgdt [gdt_48] ; load gdt with whateverappropriate
…
gdt:
dw 0,0,0,0 ; dummy
dw 0x07FF ; 8Mb - limit=2047 (2048*4096=8Mb)
dw 0x0000 ; base address=0
dw 0x9A00 ; code read/exec
dw 0x00C0 ; granularity=4096, 386
DATA_DESCRIPTOR:
dw 0x07FF ; 8Mb - limit=2047 (2048*4096=8Mb)
dw 0x0000 ; base address=0
dw 0x9200 ; data read/write
dw 0x00C0 ; granularity=4096, 386
idt_48:
dw 0 ; idt limit=0
dw 0,0 ; idt base=0L
gdt_48:
dw 0x800 ; gdt limit=2048, 256 GDTentries
dw 512+gdt,0x9 ; gdt base = 0X9xxxx
仔细阅读该部分代码,可以很好地理解Intel对中断描述符与全局描述符的数据结构控制。首先,我们要知道IDT共占六个字节,前两个字节为段长度,后四个字节为段基地址;GDT共占八个字节,两个字节为段限长,四个字节为段基地址,其他分别为特权级、段读写权等,如下图:
全局描述符表指向的是可共享的全局段描述符,可以为数据段描述符,也可以为系统段描述符。总共有256个GDT,共256*8=2048个长度,故上述代码0x800代表的就是段限长。而gdt定义的第一个段描述符为0,0,0,0;第二个为0x00C09200000007FF代入上图的描述符即可换算出段基址、段限长等信息。
mov ax,0x0001 ;protected mode (PE) bit
lmsw ax ;This is it;
jmpi 0, 8 ;jmp offset 0 of segment 8 (cs)
lmsw是将0x0001赋值给CR0,其中控制寄存器CR0的最低位PE置高,设定处理器的工作方式为保护模式。Jmpi一句,0为段内便宜,8为保护模式下的段选择符,即1000,低两位为特权级0,第三位0为GDT,若为1即LDT,1代表选择GDT中的第一项,即上述所述的0x00C09200000007FF,可以看出段基址为0,故程序在此跳转到0x00000处执行head.s的程序代码。
4. head.s程序执行
与前面不同的是,从head.s开始程序的汇编变成了AT&T汇编,而之前的汇编均为intel汇编。其中system模块(共120KB,240个扇区)包括head.s的25KB+184B以及剩下的main函数部分。Head程序主要完成的功能在于分页机制以及打开CR0的最高位PG。
最值得一提的是head.s程序最后对main函数的调用。由于这个main函数是操作系统的,调用后没有返回,此处Linux采用的一种巧妙的方法是利用RET调用main函数。
after_page_tables:
pushl $0 #These are the parameters to main :-)
pushl $0
pushl $0
pushl $L6 #return address for main, if it decides to.
pushl $__main
jmp setup_paging
setup_paging:
movl $1024*5,%ecx /* 5 pages - pg_dir+4 page tables */
xorl %eax,%eax
xorl %edi,%edi /* pg_dir is at 0x000 */
cld;rep;stosl
movl $pg0+7,_pg_dir /* set present bit/user r/w */
movl $pg1+7,_pg_dir+4 /* --------- " " --------- */
movl $pg2+7,_pg_dir+8 /* --------- " " --------- */
movl $pg3+7,_pg_dir+12 /* --------- " " --------- */
movl $pg3+4092,%edi
movl $0xfff007,%eax /* 16Mb - 4096 + 7 (r/w user,p) */
std
1: stosl /*fill pages backwards - more efficient :-) */
subl $0x1000,%eax
jge 1b
xorl %eax,%eax /* pg_dir is at 0x0000 */
movl %eax,%cr3 /* cr3 - page directory start */
movl %cr0,%eax
orl $0x80000000,%eax
movl %eax,%cr0 /* set paging (PG) bit */
ret /*this also flushes prefetch-queue */
可以看出在after_page_tables中事先将main函数的入口地址eip手动地压到堆栈中,当下次进行RET返回时,程序将返回该指针地址到eip,从而实现了main函数的跳转。