与Boot引导程序挥手作别后,此刻的处理器控制权已经移交给 Loader
引导加载程序。Loader
引导加载程序任重而道远,它必须在内核程序执行前,为其准备好一切所需数据,比如硬件检测信息、处理器模式切换、向内核传递参数等。
主要有几个事情要完成
- 检测硬件信息:比如内存的大小
- 处理器模式切换:从实模式切换到保护模式
- 向内核传递数据:启动的一些参数传递
整体代码在 loader.asm
Loader 程序 #
org 10000h
jmp Label_Start
%include "fat12.inc"
; 定义内核从 0x10000 开始
BaseOfKernelFile equ 0x00
OffsetOfKernelFile equ 0x100000
BaseTmpOfKernelAddr equ 0x00
; 临时的内核转存空间
; BIOS在实模式下只支持上限为1 MB的物理地址空间寻址,所以必须先将内核程序读入到临时转存空间,然后再通过特殊方式搬运到1 MB以上的内存空间中。
OffsetTmpOfKernelFile equ 0x7E00
MemoryStructBufferAddr equ 0x7E00
A20 开启 #
A20 此项功能属于历史遗留问题。最初的处理器只有20根地址线,这使得处理器只能寻址1MB以内的物理地址空间,如果超过1 MB范围的寻址操作,也只有低20位是有效地址。随着处理器寻址能力的不断增强,20根地址线已经无法满足今后的开发需求。为了保证硬件平台的向下兼容性,便出现了一个控制开启或禁止1 MB以上地址空间的开关。当时的8042键盘控制器上恰好有空闲的端口引脚(输出端口P2,引脚P21),从而使用此引脚作为功能控制开关,即A20功能。如果A20引脚为低电平(数值0),那么只有低20位地址有效,其他位均为0。
;======= open address A20
; 通过 92 端口打开 A20
push ax
in al, 92h
or al, 00000010b
out 92h, al
pop ax
; 禁用中断
cli
; 加载 GDT (全局描述符)
db 0x66
lgdt [GdtPtr]
mov eax, cr0
or eax, 1
mov cr0, eax
mov ax, SelectorData32
mov fs, ax
mov eax, cr0
; 关闭保护模式
and al, 11111110b
mov cr0, eax
sti
这里需要注意一点的是,在物理平台下,当段寄存器拥有这种特殊能力后,如果重新对其赋值的话,那么它就会失去特殊能力,转而变回原始的实模式段寄存器。但是Bochs虚拟机貌似放宽了对寄存器的检测条件,即使重新向FS段寄存器赋值,FS段寄存器依然拥有特殊能力。 这里可不是正确的路子
查找内核文件 #
;======= search kernel.bin
mov word [SectorNo], SectorNumOfRootDirStart
Lable_Search_In_Root_Dir_Begin:
cmp word [RootDirSizeForLoop], 0
jz Label_No_LoaderBin
dec word [RootDirSizeForLoop]
mov ax, 00h
mov es, ax
mov bx, 8000h
mov ax, [SectorNo]
mov cl, 1
call Func_ReadOneSector
mov si, KernelFileName
mov di, 8000h
cld
mov dx, 10h
和 Boot.bin
一样,找到内核加载进去,当加载成功之后,会打印一个 G
Label_File_Loaded:
mov ax, 0B800h
mov gs, ax
mov ah, 0Fh ; 0000: 黑底 1111: 白字
mov al, 'G'
mov [gs:((80 * 0 + 39) * 2)], ax ; 屏幕第 0 行, 第 39 列。
关闭磁盘马达 #
KillMotor:
push dx
mov dx, 03F2h
mov al, 0
out dx, al
pop dx
获取内存信息 #
;======= get memory address size type
mov ax, 1301h
mov bx, 000Fh
mov dx, 0400h ;row 4
mov cx, 24
push ax
mov ax, ds
mov es, ax
pop ax
mov bp, StartGetMemStructMessage
int 10h
mov ebx, 0
mov ax, 0x00
mov es, ax
mov di, MemoryStructBufferAddr
Label_Get_Mem_Struct:
mov eax, 0x0E820
mov ecx, 20
mov edx, 0x534D4150
int 15h
jc Label_Get_Mem_Fail
add di, 20
cmp ebx, 0
jne Label_Get_Mem_Struct
jmp Label_Get_Mem_OK
获取 VGA 信息 #
;======= get SVGA information
mov ax, 1301h
mov bx, 000Fh
mov dx, 0800h ;row 8
mov cx, 23
push ax
mov ax, ds
mov es, ax
pop ax
mov bp, StartGetSVGAVBEInfoMessage
int 10h
mov ax, 0x00
mov es, ax
mov di, 0x8000
mov ax, 4F00h
int 10h
cmp ax, 004Fh
jz .KO
模式切换 #
上面那些内容都属于硬件的 API
对我们掌握操作系统帮助有限,我们下面来看操作系统相关的东西,我们需要从实模式
-> 保护模式
-> IA-32e 模式
实模式 #
笔者认为,这事情对于软件工程师来说是和硬件的项目配合。
在处理器切换到保护模式前,还必须初始化GDTR寄存器、IDTR寄存器(亦可推迟到进入保护模式后,使能中断前)、控制寄存器CR1~4、MTTRs内存范围类型寄存器。
- 系统数据结构:
- 系统在进入保护模式前,必须创建一个拥有代码段描述符和数据段描述符的GDT(Globad DescriptorTable,全局描述符表)(第一项必须是NULL描述符),并且一定要使用LGDT汇编指令将其加载到GDTR寄存器。
- 保护模式的栈寄存器SS,使用可读写的数据段即可,无需创建专用描述符。对于多段式操作系统,可采用LDT(Local DescriptorTable,局部描述符表)(必须保存在GDT表的描述符中)来管理应用程序,多个应用程序可独享或共享一个局部描述符表LDT。如果希望开启分页机制,
- 则必须准备至少一个页目录项和页表项。(如果使用4 MB页表,那么准备一个页目录即可。)
- 中断和异常
- IDT由若干个门描述符组成,如果采用中断门或陷阱门描述符,它们可以直接指向异常处理程序;如果采用任务门描述符,则必须为处理程序准备TSS段描述符、额外的代码和数据以及任务段描述符等结构。如果处理器允许接收外部中断请求,那么IDT还必须为每个中断处理程序建立门描述符。
- 分页机制
- CR0控制寄存器的PG标志位用于控制分页机制的开启与关闭。在开启分页机制(置位PG标志位)前,必须在内存中创建一个页目录和页表(此时的页目录和页表不可使用同一物理页),并将页目录的物理地址加载到CR3控制寄存器(或称PDBR寄存器)。当上述工作准备就绪后,可同时置位控制寄存器CR0的PE标志位和PG标志位,来开启分页机制。
- 多任务机制
- 如果希望使用多任务机制或允许改变特权级,则必须在首次执行任务切换前,创建至少一个任务状态段TSS结构和附加的TSS段描述符。(当特权级切换至0、1、2时,栈段寄存器与栈指针寄存器皆从TSS段结构中取得。)在使用TSS段结构前,必须使用LTR汇编指令将其加载至TR寄存器,这个过程只能在进入保护模式后执行。
;======= init IDT GDT goto protect mode
; 初始化 GDT
cli ;======close interrupt
db 0x66
lgdt [GdtPtr]
; db 0x66
; lidt [IDT_POINTER]
mov eax, cr0
or eax, 1
mov cr0, eax
jmp dword SelectorCode32:GO_TO_TMP_Protect
IA-32e 模式 #
GO_TO_TMP_Protect:
;======= go to tmp long mode
mov ax, 0x10
mov ds, ax
mov es, ax
mov fs, ax
mov ss, ax
mov esp, 7E00h
call support_long_mode
test eax, eax
jz no_support
;======= init temporary page table 0x90000
mov dword [0x90000], 0x91007
mov dword [0x90800], 0x91007
mov dword [0x91000], 0x92007
mov dword [0x92000], 0x000083
mov dword [0x92008], 0x200083
mov dword [0x92010], 0x400083
mov dword [0x92018], 0x600083
mov dword [0x92020], 0x800083
mov dword [0x92028], 0xa00083
;======= load GDTR
db 0x66
lgdt [GdtPtr64]
mov ax, 0x10
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
mov esp, 7E00h
;======= open PAE
mov eax, cr4
bts eax, 5
mov cr4, eax
;======= load cr3
mov eax, 0x90000
mov cr3, eax
;======= enable long-mode
mov ecx, 0C0000080h ;IA32_EFER
rdmsr
bts eax, 8
wrmsr
;======= open PE and paging
mov eax, cr0
bts eax, 0
bts eax, 31
mov cr0, eax
jmp SelectorCode64:OffsetOfKernelFile
;======= test support long mode or not
support_long_mode:
mov eax, 0x80000000
cpuid
cmp eax, 0x80000001
setnb al
jb support_long_mode_done
mov eax, 0x80000001
cpuid
bt edx, 29
setc al
Jump to Kernel #
jmp SelectorCode64:OffsetOfKernelFile
跳转到内核即可。 伴随着Loader引导加载程序最后一条指令(远跳转指令)的执行,处理器的控制权就移交到了内核程序手上。 此刻,Loader引导加载程序已完成了它的使命,其占用的内存空间可以释放或另作他用。目前系统虽已进入IA-32e模式, 但这只是临时中转模式,接下来的内核程序将会为系统重新创建IA-32e模式的段结构和页表结构。