上一篇其实已经说完了boot的大体工做,可是Linux在最后进入操做系统以前还有一些操做,好比进入保护模式。在我本身的FragileOS里进入保护模式是在引导程序结束后完成的。git
实模式到保护模式属于操做系统的一个大坎,因此须要先提一下github
实模式和保护模式都是CPU的工做模式,它们的主要区别就是寻址方式数组
实模式出现于早期8088CPU时期。当时因为CPU的性能有限,一共只有20位地址线(因此地址空间只有1MB),以及8个16位的通用寄存器,以及4个16位的段寄存器。因此为了可以经过这些16位的寄存器去构成20位的主存地址,必须采起一种特殊的方式。访问内存的就变成了:数据结构
物理地址 = 段基址 << 4 + 段内偏移
app
随着CPU的发展,能够访问的内存空间也从1MB变为如今4GB,寄存器的位数也变为32位。而且在实模式下,用户程序对内存的访问很是自由,没有任何限制,随随便便就能够修改任何一个内存单元。因此实模式已经不能知足时代的要求了,保护模式就应运而生了ide
保护模式的偏移值变成了32位,寻址方式仍然须要段寄存器,可是这些段寄存器存放的再也不是段基址了,而是相似一个数组的索引函数
而这个数组就是一个就作全局描述符表 *(GDT)*的东西,GDT中含有一个个表项,每个表项称为段描述符。性能
而咱们经过段寄存器里的的这个索引,能够找到对应的表项。段描述符存放了段基址、段界限、内存段类型属性fetch
处理器内部有一个 48 位的寄存器,称为全局描述符表寄存器(GDTR)。也就是为了来记录GDT的this
段描述符
一部分代码
[SECTION .gdt] ; 利用宏定义定义gdt
; 段基址 段界限 属性
LABEL_GDT: Descriptor 0, 0, 0
LABEL_DESC_CODE32: Descriptor 0, 0fffffh, DA_C | DA_32 | DA_LIMIT_4K
LABEL_DESC_VIDEO: Descriptor 0B8000h, 0fffffh, DA_DRW
LABEL_DESC_VRAM: Descriptor 0, 0fffffh, DA_DRW | DA_LIMIT_4K
in al, 92h ; 切换到保护模式
or al, 00000010b
out 92h, al
mov eax, cr0
or eax , 1
mov cr0, eax
复制代码
如今来看看Linux在启动前最后还作了什么
setup.s主要的任务就是从BIOS拿到系统数据而后存放到一个内存位置
获取当前光标的位置
mov ax,#INITSEG ! this is done in bootsect already, but...
mov ds,ax
mov ah,#0x03 ! read cursor pos
xor bh,bh
int 0x10 ! save it in known place, con_init fetches
mov [0],dx ! it from 0x90000.
复制代码
获取内存大小
mov ah,#0x88
int 0x15
mov [2],ax
复制代码
检查如今的显示方式
mov ah,#0x0f
int 0x10
mov [4],bx ! bh = display page
mov [6],ax ! al = video mode, ah = window width
复制代码
进入保护模式的代码也在setup中
首先先把内核SYSTEM部分移动到0位置,在以前它是被读入在0x10000位置
mov ax,#0x0000
cld ! 'direction'=0, movs moves forward
do_move:
mov es,ax ! destination segment
add ax,#0x1000
cmp ax,#0x9000
jz end_move
mov ds,ax ! source segment
sub di,di
sub si,si
mov cx,#0x8000
rep
movsw
jmp do_move
复制代码
而后就是加载上面说的全局描述符表和中断向量表
中断向量表前面没有提过,可是比较简单,有点相似GDT,就是 操做系统必须维护一份中断向量表,每个表项纪录一个中断处理程序(ISR,Interrupt Service Routine)的地址
end_move:
mov ax,#SETUPSEG ! right, forgot this at first. didn't work :-)
mov ds,ax
lidt idt_48 ! load idt with 0,0
lgdt gdt_48 ! load gdt with whatever appropriate
复制代码
再接着就是打开A20地址线,若是不打开A20地址线,即便在保护模式下最大寻址仍是1M
call empty_8042
mov al,#0xD1 ! command write
out #0x64,al
call empty_8042
mov al,#0xDF ! A20 on
out #0x60,al
call empty_8042
复制代码
初始化8259A芯片,8259A是专门为了对8085A和8086/8088进行中断控制而设计的芯片,它是能够用程序控制的中断控制器。单个的8259A能管理8级向量优先级中断。 对于对硬件的初始化其实就是依照CPU的固定套路
部分代码
mov al,#0x11 ! initialization sequence
out #0x20,al ! send it to 8259A-1
.word 0x00eb,0x00eb ! jmp $+2, jmp $+2
out #0xA0,al ! and to 8259A-2
复制代码
最后的最后,终于能够正式进入保护模式,能够看到这里进入保护模式的方法和我上面的move cr0 ax不太同样,Linux之因此使用这种方法是为了兼容286以前的CPU,另外须要注意的是在进入保护模式以后须要立马执行一条段间跳转来让CPU刷新指令队列,这里跳转的描述就已是用段值来描述了,段指的第三位到第十五位用来指向GDT里的索引(1000),也就是跳到第2个段描述符里记录的地址
mov ax,#0x0001 ! protected mode (PE) bit
lmsw ax ! This is it!
jmpi 0,8 ! jmp offset 0 of segment 8 (cs)
复制代码
第二个GTD段描述符,因此上面也就是跳转到内存0处
.word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb)
.word 0x0000 ! base address=0
.word 0x9A00 ! code read/exec
.word 0x00C0 ! granularity=4096, 386
复制代码
再往下就是正式进入到了内核部分,在此以前须要再提一下IDT和分页管理机制
中断描述符表把每一个中断或异常编号和一个指向中断处理事件服务程序的描述符联系起来。同GDT和LDT同样,IDT是一个8-字节的描述符数组。和GDT、LDT不一样的是,IDT的第一项能够包含一个描述符。为了造成一个在IDT内的索引,处理器把中断、异常标识号乘以8之后来作为IDT的索引。由于只有256个编号,IDT没必要包含超过256个描述符。它能够包含比256更少的项,只是那些须要使用的中断、异常的项。
IDT能够在内存的任意位置。处理器经过IDT寄存器(IDTR)来定位IDT。指令LIDT和SIDT用来操做IDTR。
将用户程序(进程)的逻辑地址空间分红若干个页(4KB)并编号,同时将内存的物理地址也分红若干个块或页框 4KB)并编号,这样也就是为了让全部的应用程序看都像是独占一片内存,起始地址都是为0,最后再创建一个页表存储着页到页框也就是真实内存地址的映射
在内存里有一个寄存器(PTR)来存储页表
咱们经过设置CR0寄存器的PG位来开启分页功能,而其它操做就都由CPU来完成,固然前提是咱们有一张页表
为了减小内存的占用量,80X86采用了分级页表
页目录有2的十次方个4字节的表项,这些表项指向对应的二级表,线性地址的最高10位做为页目录用来寻找二级表的索引
二级页表里的表项含有相关页面的20位物理基地址,二级页表使用线性地址中间10位来做为寻找表项的索引
因此说CPU寻址一共须要进行两步:
head.s这部分其实已是进入了内核部分了,可是在Linux0.12里仍是把它归为Boot部分。这一部分的主要工做是从新设置GDT和IDT,而后在设置管理内存的分页处理机制 (在进入保护模式后,Linux用的就是AT&T的汇编语法了,最显著的差异就是源操做数和目的数的位置对调了)
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
复制代码
setup_gdt:
lgdt gdt_descr
ret
gdt_descr:
.word 256*8-1 # so does gdt (not that that's any
.long gdt # magic number, but it works for me :^)
.align 8
复制代码
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.
复制代码
STOS指令:将AL/AX/EAX的值存储到[EDI]指定的内存单元 CLD清除方向标志和STD设置方向标志,当方向标志是0,该指令经过递增的指针数据每一次迭代以后(直到ECX是零或一些其它条件,这取决于REP前缀的香味)工做,而若是该标志是1,指针递减。
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 */
复制代码
这一节主要是描述了保护模式和一些CPU须要的数据结构。这几篇文章至关于讲述了一台计算机启动的时候都发生了什么。