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

本文详述Linux-0.11内核的引导启动过程,从BIOS初始化,到Bootsect引导加载,再到Setup程序执行,最后是head.s的分页机制。讲解了中断向量表、BIOS中断、内存复制及保护模式切换等关键步骤。
摘要由CSDN通过智能技术生成

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函数的跳转。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值