Linux-0.11学习总结——引导启动程序部分

来源:互联网 发布:cms二次开发网站建设 编辑:程序博客网 时间:2024/05/22 08:22

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, INITSEGgo: 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,0lgdt   [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=0dw    0x9A00             ; code read/execdw    0x00C0             ; granularity=4096, 386 DATA_DESCRIPTOR:dw    0x07FF              ; 8Mb - limit=2047 (2048*4096=8Mb)dw    0x0000              ; base address=0dw    0x9200              ; data read/writedw    0x00C0             ; granularity=4096, 386 idt_48:dw    0                          ; idt limit=0dw    0,0                      ; idt base=0L gdt_48:dw    0x800                ; gdt limit=2048, 256 GDTentriesdw    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) bitlmsw       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) */       std1:    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函数的跳转。

原创粉丝点击