从零开始写一个操作系统 —— 2.loader

900 阅读2分钟

1.loader的作用

loader的作用就在于切换cpu运行模式并读取加载硬盘上的kernel文件。由于kernel文件较为庞大,所以我们会逐个字节将其复制到1M内存之后。

整个loader文件的流程图如下:

截屏2021-04-14 16.54.43.png

2.loader的实现

2.1 在文件系统中寻找kernel文件

loader中寻找kernel文件与在boot中寻找loader文件逻辑完全相同,区别只是将我们所需寻找到文件名从LOADER BIN改成KERNEL BIN

2.2 找到kernel之后

找到了kernel文件之后,与boot相似,我们首先需要获得kernel文件的文件大小以及存放的扇区位置,同样用到在boot中使用的Func_DirPointerToStartSectorOfFile以及Func_DirPointerToStartSectorOfFilePointer。所以在加载kernel文件之前的代码应该是

;===开辟内存空间来保存kernel文件的起始扇区以及kernel文件所占扇区的大小
SectorOfKernel		dw	0
StartSectorOfKernel	dw	0

;===定义保护模式段描述符
gdt_start_desc  dq  0x0000000000000000
gdt_code_desc   dq  0x00cf9a000000ffff
gdt_data_desc   dq  0x00cf92000000ffff
gdt_size    dw  $ - gdt_start_desc
gdt_base    dd  gdt_start_desc

Label_KernelFound:
    ;将获得的kernel目录指针转为kernel文件的起始扇区以及kernel文件所占扇区的大小
    call Func_DirPointerToSectorOfFile
    mov word[SectorOfKernel], ax
    call Func_DirPointerToStartSectorOfFilePointer
    mov word[StartSectorOfKernel], ax
    ;===加载保护模式段描述符表
    lgdt [gdt_size]
    ;===开启A20地址线
    in al, 0x92
    or al, 00000010B
    out 0x92, al
    ;===通过cr0开启保护模式
    mov eax, cr0
    or eax, 1
    mov cr0, eax
    ;===将保护模式的数据段描述符放入fs段寄存器中
    mov ax, 0x0010
    mov fs, ax 
    ;===关闭保护模式
    mov eax, cr0
    and al, 11111110b
    mov cr0, eax

保护模式段描述符详见从零开始写一个操作系统 —— 1.5实模式与保护模式。在找到以及获得了kernel的大小与位置后,就可以开始着手加载kernel文件。而与加载loader文件不同的是,kernel文件整体大小可能会超出BIOS中断13h所能读取的范围,以及所需放置的1M内存后无法通过实模式寻址,所以我们需要通过先开启保护模式,将保护模式的段描述符放入fs段寄存器中,然后再退出保护模式。虽然此时已经退出保护模式,但是fs段寄存器中依然为保护模式的段描述符,所以我们此时可以即在实模式下使用BIOS中断,也可以通过保护模式的寻址方式访问1M内存之后的内容。

2.3 加载kernel

加载一个kernel扇区至缓冲区与加载loader时所用的函数一样,区别是此时我们仅仅需要它帮我们加载一个扇区就可以,而复制一个扇区到1M内存之后的函数如下

OffsetOfCache		equ	0x9000
OffsetOfKernel		equ	0x100000
ByteIndexOfKernel	dd	0

Func_CopySingleSectorOfKernel:
    ;loop指令会循环运行cx寄存器中数据的次数
    mov cx, 512
    ;lodsb指令会逐个将si寄存器所指向的内存地址中的数据加载至al寄存器中
    mov si, OffsetOfCache
    ;edi指向kernel的起始位置
    mov edi, OffsetOfKernel
Label_CopySingleByteOfSector:
    lodsb
    mov edx, dword[ByteIndexOfKernel]
    ;fs:edi+edx指向当前kernel所应该放置的内存地址
    mov byte[fs:edi + edx], al
    ;每放置完成一个数据,当前kernel索引加一
    inc dword[ByteIndexOfKernel]
    loop Label_CopySingleByteOfSector
    ret

所以整体加载kernel文件的程序为

Label_LoadKernel:
	mov	word[Index], 0
Label_ForIndexInSectorOfKernel:
	;所需要读取的扇区数量
	mov si, 1
	;读取扇区开始的位置
	mov di, word[StartSectorOfKernel]
	;数据放置的位置
	mov dx, OffsetOfCache
	call Func_ReadSector
	Call Func_CopySingleSectorOfKernel
	inc word[Index]
	inc word[StartSectorOfKernel]
	mov ax, word[Index]
	cmp ax, word[SectorOfKernel]
	jne Label_ForIndexInSectorOfKernel
	;重新打开保护模式
	mov eax, cr0
	or eax, 1
	mov cr0, eax
	;加载保护模式数据段描述符至ds段寄存器
	mov ax, 0x0010
	mov ds, ax
	;跳转到IA-32e进入段
	jmp 0x0008:Label_ToLongMode

3.切换至IA-32e模式

截屏2021-04-16 19.29.50.png

IA-32e模式详见从零开始写一个操作系统 —— 1.6 IA-32e模式。与保护模式类似,IA-32e模式寻址方式也需要通过GDT全局描述符表寻址。所以我们第一步需要和保护模式一样,建立全局描述符表。

gdt64_start_desc dq 0x0000000000000000
gdt64_code_desc  dq 0x0020980000000000
gdt64_data_desc  dq 0x0000920000000000

gdt64_size  dw  $ - gdt64_start_desc
gdt64_base  dd  gdt64_start_desc

IA-32e模式取消了保护模式中段描述符的段基址与段限长,使得描述符可以直接寻址整个地址空间。然后我们需要建立页表:

PML4:
    mov dword[0x90000], 0x91007
PDPT:
    mov dword[0x91000], 0x92007
PDT:
    mov dword[0x92000], 0x000087

在第三级页表PDT中,通过0x87结尾,表示使用2MB大小的页。接下来就是开启IA-32e模式:

;加载IA-32e模式段描述符表
lgdt [gdt64_size]
;将各个段寄存器载入数据段描述符表
mov ax, 0x10
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
;通过将cr4控制寄存器第5位置1开启PAE页表功能
mov eax, cr4
bts eax, 5
mov cr4, eax
;将PML4页表基址载入cr3控制寄存器
mov eax, 0x90000
mov cr3, eax
;开启IA-32e模式
mov ecx, 0x0c0000080
rdmsr
bts eax, 8
wrmsr
;通过将cr0寄存器第0位及第31位置1,开启页表
mov eax, cr0
bts eax, 0
bts eax, 31
mov cr0, eax

最终,通过jmp 0x0008:0x100000长跳转,使得程序开始运行我们放置于1M内存中的内核程序。接下来就是内核程序kernel的开发。