1.loader的作用
loader的作用就在于切换cpu运行模式并读取加载硬盘上的kernel
文件。由于kernel
文件较为庞大,所以我们会逐个字节将其复制到1M内存之后。
整个loader文件的流程图如下:
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模式
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
的开发。