首先,应该了解磁盘里面存放数据的结构:
bootsect.s : 1个扇区
setup.s : 4个扇区
system模块: 约240个扇区
启动之后会读取bootsect.s到0x7c00:0x0000地址内,并从当前地址开始执行bootsect.s的代码。
bootsect.s所完成的任务大概如下:
1.把自身复制到0x9000:0x0000处,之后jmp go, INITSEG,则CS:0x9000 ip:(offset go) 处继续执行。
2.设置ss:sp为0x9000:0xff00 -> 0x9ff00处。(之后从磁盘读取setup.s需要用int 0x13的软中断,需设置堆栈,并且堆栈的值是远大于0x90000,保证不会覆盖掉bootsect.s和setup.s的代码)
3.加载setup.s(磁盘的第2个扇区之后的4个扇区)到0x90200处
4.读取system模块到0x10000地址处。(大小为192KB,SYSSIZE = 0x3000 节,一节16字节)
那么占用的地址为0x10000 ~ 0x40000
5. 保存设备号到root_dev地址(508)处,供之后程序所用。
( 249 .org 508
250 root_dev:
.word ROOT_DEV
252 boot_flag:
.word 0x55AA
)
最后 jmpi 0, 0x9020 地址出0x90200执行,为setup.s的代码。
此时内存分布如下图:
setup.s所完成的工作:
1.询问BIOS有关内存、磁盘、其他参数,并保存到0x90000 ~ 0x901ff 地址出(之前的bootsect.s代码位置)
2.进入保护模式
2.1 cli禁止中断
2.2 把system模块移动到0x00000地址处(复制的区间是0x10000 - 0x90000)
注:之所以bootsect.s不直接复制到0x00000的地址处是因为,还需要利用BIOS去得到一些参数。
就像第一步所做的一样,就是利用BIOS去得到参数。
2.3 lidt idt_48
2.4 lgdt gdt_48
2.5 开始A20地址线,并重新对中断编写(0x20 - 0x2f)
2.6 CR0 的bit0 = 1,CPU切换到保护模式
2.7 jmpi 0, 8 真正进入保护模式!!!
正在上传…重新上传取消正在上传…重新上传取消提供参数所对应的内存地址和描述:
现在大概说说gdt和ldt
205 gdt:
206 .word 0, 0, 0, 0
207
208 .word 0x07ff ! 段长 8Mb -limit 2047 (2047 * 4096 = 8Mb)
209 .word 0x0000 ! 基址 0
210 .word 0x9A00 ! 代码段,只读、可执行
211 .word 0x00c0 !颗粒度4096, 32位模式
212
213 .word 0x7F00 !段长 8Mb -limit 2047 (2047 * 4096 = 8Mb)
214 .word 0x0000 !基址 0
215 .word 0x9200 !数据段、可读、可写
216 .word 0x00c0 !颗粒度4096, 32位模式
第一个描述符空描述符、第二个为代码段描述符、第三个为数据段描述符。
218 idt_48:
219 .word 0
210 .word 0, 0
CPU要求进入保护模式时候设置IDT寄存器,这里暂且设为空。之后会在进行重新设置。
222 gdt_48:
223 .word 0x800
224 .word 512 + gdt, 0x9
解释:基地址0x90200 + gdt , 段长0x8000
gdt就是上面的标号,0x90200为setup.s代码的基地址,所以0x90200 + gdt 就是gdt所在的绝对地址。
0x800/ 8 = 0x100 为256个项。
最后在解释一下jmpi 0, 8
二进制8: 1000
bit1 - bit0 : RPL = 0
bit2 : TI = 0 (GDT表中找段描述符)
bit15-bit3 : 1 (GDT表中的第一个描述符)
由该描述符可以知道所在段的基地址为0,由jmpi 0, 8指令知道偏移地址0,所以现在要跳到地址为0的地方去执行代码,也就是system模块(head.s)。
现在在看一下此时的内存分布:
现在会跳转到system模块的最开始的head.s代码执行。
明天再继续分析下去到最后到main函数。
最后在总结一下,前面这3个汇编程序到底给操作系统提供了些什么信息。
执行head.s的代码已经是在保护模式下,那么先大概说一些它所完成的功能:
1.重新设置IDT表
2.重新设置GDT表、所以setup.s所设置的2个描述符仅仅为临时的,之后就无效了。
3.设置页目录和页表
4.开启分页机制,然后跳转到main函数。
注意head.s中开头的
.globl _idt, _gdt, _pg_dir, _tmp_floppy_area
把它们设置为全局的,之后的C里面的函数都会调用这几个变量的。
比如在head.h这个头文件:
typedef struct desc_struct
{
unsigned long a, b
}desc_table[256];
extern desc_table idt, gdt;
所设置的外部变量就是这里的_idt和_gdt。
lss _stack_start, %esp 这条指令
在sched.h中有这么定义:
//在其他地方定义了 PAGE_SIZE 4096
long user_stack [ PAGE_SIZE>>2 ] ;
struct
{
long * a;
short b;
} stack_start = { & user_stack [PAGE_SIZE>>2] , 0x10 };
解释:
long 型数据长度为4个字节、 long user_stack[4096 >> 2] => long user_stack[1024]
1024个long型变量,长度则为4096 一页的长度。
&user_stack[PAGE_SIZE>>2] => &user_stack[1024] 最后一个数据的下标为1023。但是在堆栈操作时候是先递减ESP的值,然后再存放值。
所以当堆栈操作时候是这样的: esp = esp - 4
[esp] = 变量
等价于数据放入到user_stack[1023]。所以说,esp指向一页的末端。
那么该指令执行后,ss = 0x10 esp = &user_stack[1024]....
call setup_idt 设置IDT表
call setup_gdt 设置GDT表
先了解一下IDT描述符的结构:
中断门:
[31 16] [15] [14 13] [12 8] [7 5] [4 0]
过程入口偏移地址31-16 P DPL 01110 000 0000
[31 16] [15 0]
段选择符 过程入口偏移地址15-0
setup_idt:
lea ignore_int, %edx
movl $0x00080000, %eax
movw %dx, %ax
movw $0x8E00, %dx
lea _idt, %edi
mov $256, %ecx
rp_sidt:
movl %eax, (%edi)
movl %edx, 4($edi)
addl $8, %edi
jne rp_sdi
lidt idt_descr
ret
该段代码 ignore_init为过程偏移地址
0x00080000 主要的就是前面0008 作为选择符
0x8E000000 主要为8E00 :1000 1110 0000 0000 存在、DPL = 0、01110 中断门
最后用edx来存放高4字节、eax存放低4字节。 作为一个中断描述符
其实它们最后表示为:
x x x x 8E00
0 0 0 8 x x x x
lea _idt, %edi
_idt标号在head.s 的232行:
_idt:
.fill 256, 8, 0
所以表示为256项、每项为8个字节。
rp_sidt:
。。。
。。。
。。。
jne rp_sidt
该段作用是对这个255项进行赋值,之前的edx和eax。这样之后就是所有终端描述符最终都指向
ignore_int这个过程。至于这个过程做什么事、仅仅是打印一段信息,然后返回,没有实际用处。
到main函数时候,会在继续具体的赋值各个中段描述符。
lidt idt_descr //设置IDTR寄存器
idt_descr:
.word 256 * 8 - 1 段限长
.long _idt idt表的线性地址
setup_gdt:
lgdt gdt_descr //设置GDTR寄存器
ret
gdt_descr:
.word 256 * 8 - 1
.long _gdt
_gdt:
.quad 0x0000000000000000
.quad 0x00c09a0000000f f f
.quad 0x00c0920000000f f f
.quad 0x0000000000000000
.fill 252, 8, 0
所以_gdt和_idt是差不多的。都是设置了256项的8字节长度。那么_gdt存放的就是GDT表项。
第一个描述符为空、第二个为代码段、第三个为数据段、第四个为系统段(不用)
剩下的252个是供进程的IDT和TSS段的描述符。
这里的代码段和数据段的限长为16M,就是整个内存的长度(默认内存大小为16M)。所以说内核可以访问整个内存。
最后就是设置页目录和页表接着开启分页,然后跳转到main
head.s:
16: pg_dir:
114: .org 0x1000
pg0:
117: .org 0x2000
pg1:
120: .org 0x3000
pg2:
123: .org 0x4000
pg3:
126: .org 0x5000
132: _tmp_floop_area:
.fill 1024, 1, 0
pg_dir: [0 - 0xfff] 4096
pg0 : [0x1000 - 0x1fff] 4096
pg1 : [0x2000 - 0x2fff] 4096
pg2 : [0x3000 - 0x3fff] 4096
pg2 : [0x4000 - 0x4fff] 4096
设置页目录和页表代码:
setup_paging:
movl $1024*5,%ecx //4个页表1个页目录 5 页内存清零
xorl %eax,%eax
xorl %edi,%edi
cld;rep;stosl
movl $pg0+7,pg_dir pg_dir 第一个页目录项 pg0 为第一个页表的基地址 7 为页存在、 用户可读可写、
movl $pg1+7,pg_dir+4 pg_dit + 4第二个页目录项 pg0 为第二个页表的基地址 7 为页存在、 用户可读可写、
movl $pg2+7,pg_dir+8 。。。
movl $pg3+7,pg_dir+12 。。。
movl $pg3+4092,%edi edi指向第3页表的最后一项 (4092指向最后一项、因为一项长度为4)
movl $0xfff007,%eax /* 16Mb - 4096 + 7 (r/w user,p) */
std
1: stosl
subl $0x1000,%eax //设置3个页表内容
jge 1b //一个页表可以存放1024个表项,一个表项指向一个页(4096字节)
//16M = 4096 * 4096 所以需要4096个表项、4096 / 1024 = 4 ,4个页表来映射
xorl %eax,%eax //pg_dir地址在0x00000000
movl %eax,%cr3 //页目录地址存放到CR3
movl %cr0,%eax //置位PG 、开启分页机制
orl $0x80000000,%eax
movl %eax,%cr0 /* set paging (PG) bit */
ret /* this also flushes prefetch-queue */
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
L6:
jmp L6 # main should never return here, but
# just in case, we know what happens.
pushl $main 则为main函数的入口地址。
jmp setup_paging 就是前面的设置页目录和页表。
最后的ret 则返回到main函数去执行了。
内存布局:
之后,给后面提供了
1. gdt GDT表
2. idt IDT表 (还需要在为各个中断进行赋值)
3.pg_dir 页目录
4.pg0 pg1 pg2 pg3页表 (4个页表)
5.0x90000 - 0x901ff的参数