1. 前言
上一篇我们已经完成了一个较为完整的 Boot
程序,当 Boot
程序运行结束后将会加载 Loader
程序到内存中,并转入 Loader
的加载地址执行 Loader
程序。而在上一篇中写了一个非常简单的 Loader
程序,其本身并没有什么功能,只是输出一行字符串用来提示计算机已经加载了 Loader
程序并执行。本文将会在此前的 Loader
基础之上完善一个 Loader
程序应有的基本功能,在本文笔者将此引导加载程序命名为 IMLoader
。
2. Loader 程序简介
Loader
程序,继 Boot
之后的下一个运行的代码就是 Loader
。在操作系统中一个 Loader
程序的基本工作有:检测硬件信息、处理器模式切换、向内核传参、加载内核并启动。
2.1. 检测硬件信息
为了能够保证操作系统能够正确运行,必须要在内核启动之前对运行系统的硬件环境进行检测并获取各种硬件的包含物理地址和硬件本身特性等信息(如:RAM
、ROM
、设备寄存器等),并最后会将这些信息交给系统内核管理。
硬件检测和信息获取主要通过 BIOS
的中断服务来完成。但由于 BIOS
在上电自检出的大部分信息只能在实模式下获取,而运行内核则需要在非实模式下,那么就必须在进入内核程序前将这些信息检测,再以参数的方式传递给内核。
2.2. 处理器模式切换
最开始 BIOS
运行在实模式 (Real Mode
),到 32
位操作系统使用的保护模式 (Protect Mode
),再到 64
位操作系统使用的 IA-32e
模式 (Long Mode
),也称为长模式,Loader
引导加载程序必须经历这三个模式,才能使处理器运行于 64
位的 IA-32e
模式。在各个模式切换过程中,Loader
引导加载程序必须手动创建各运行模式的临时数据,并安装标准流程执行模式间的跳转。其中包含配置系统临时页表的工作。
2.3. 向内核传递数据
Loader
程序向内核程序传递两类数据,分别是控制信息和硬件数据信息。这些数据一方面控制内核程序的执行流程,另一方面为内核程序的初始化提供数据信息支持。
3. IMLoader 程序的开始部分
在 IMLoader
程序开始部分我们需要先定义一些之后使用的标识符信息,包含 FAT12
文件结构信息、kernel
的加载地址、临时内存空间等标识符。
让我们新建一个文件,用来实现完善版本的 Loader
程序,笔者将此文件命名为 IMLoader.S
。
[imaginemiracle@imos-ws imLoader-v0.2]$ vim IMLoader.S
3.1. 代码实现
具体代码如下:
org 10000h
jmp Label_IMLoader_Start
%include "fat12.inc"
BaseOfKernelFile equ 0x00
OffsetOfKernelFile equ 0x100000 ; Kernel Start Address
BaseTmpOfKernelAddr equ 0x00
OffsetTmpOfKernelFile equ 0x7E00
MemoryStructBufferAddr equ 0x7E00
除了 %include "fat12.inc"
之外的几行代码都不需要再做解释,而这行 include
语句是将 fat12.inc
文件引入当前文件,与 C
语言中的 #include<fat12.inc>
同理。
BaseOfKernelFile: 表示内核加载的基地址;
OffsetOfKernelFile: 加载内核的偏移地址;
BaseTmpOfKernelAddr: 内核加载的转存基地址;
OffsetTmpOfKernelFile: 内核加载的转存偏移地址;
OffsetTmpOfKernelFile: 内存结构数据的存储地址。
3.2. 详细描述
在此处首先定义了一个内核加载的地址分别由 BaseOfKernelFile
、OffsetOfKernelFile
组成,BaseOfKernelFile << 4 + OffsetOfKernelFile
= 0x00 << 4 + 0x100000
= 0x100000
= 1MB
,即内核加载的物理起始地址在 1MB
处,这是由于在 1MB
以下的物理空间并非完全可用,将会被划分为若干个子空间段,包含常规内存空间、非内存空间以及地址空洞等。且由于日后的内核体积不断增大更是不方便放在可用空间狭小的单元里,因此设定在 1MB
开始,即不显得浪费内存空间,也能够保证有足够的内存空间存放内核。
在接下来又定义了一个临时的转存地址分别由 BaseTmpOfKernelAddr
、OffsetTmpOfKernelFile
组成,BaseTmpOfKernelAddr << 4 + OffsetTmpOfKernelFile
= 0x00 << 4 + 0x7E00
= 0x7E00
。由于 BIOS
在实模式下最大支持 1MB
的物理空间寻址,因此需要先将内核读入到转存地址空间中,再通过其它方法将内核搬运到 1MB
以上的内存中。当完整搬运后,这段内存便可以空出作为他用,这里便是将其修改为内存结构数据的存储空间,用来提供内核程序在初始化时使用。
3.3. fat12.inc 文件
此处附上笔者添加的 fat12.inc
文件,将此文件放置与 IMLoader.S
文件同目录下即可。
BaseOfStack equ 0x7c00
BaseOfLoader equ 0x1000
OffsetOfLoader equ 0x00 ; 与 BaseOfLoader 组合成为 Loader 程序的物理地址
; BaseOfLoader << 4 + OffsetOfLoader = 0x10000
RootDirSectors equ 14 ; 根目录扇区
SectorNumOfRootDirStart equ 19 ; 根目录起始扇区号
SectorNumOfFAT1Start equ 1 ; FAT1 表起始扇区号
SectorBalance equ 17 ; 用于平衡文件或目录的起始簇号与数据区起始簇号的差值
BS_OEMName db 'IMboot '
BPB_BytesPerSec dw 512
BPB_SecPerClus db 1
BPB_RsvdSecCnt dw 1
BPB_NumFATs db 2
BPB_RootEntCnt dw 224 ; RootDirSectors * 512 / 32 = 224
BPB_TotSec16 dw 2880 ; 1440 * 1024 / 512 = 2880 (fp: 1.44MB)
BPB_Media db 0xf0
BPB_FATSz16 dw 9
BPB_SecPerTrk dw 18
BPB_NumHeads dw 2
BPB_HiddSec dd 0
BPB_TotSec32 dd 0
BS_DrvNum db 0
BS_Reserved1 db 0
BS_BootSig db 0x29
BS_VolID dd 0
BS_VolLab db 'boot loader' ; 必须 11bit,不足用空格补齐
BS_FileSysType db 'FAT12 ' ; 必须 8bit,不足用空格补齐
4. IMLoader 程序开始提示模块
当由 IMBoot
加载完 IMLoader
后,并跳转入 IMLoader
程序执行时,我们让其首先打印一段字符串,表示成功转入 IMLoader
部分。
4.1. 具体实现
具体代码如下:
[SECTION .s16]
[BITS 16]
Label_IMLoader_Start:
mov ax, cs
mov ds, ax
mov es, ax
mov ax, 0x00
mov ss, ax
mov sp, 0x7C00
;====== Print "IMLoader is running...(^v^)"
mov ax, 0x1301
mov bx, 0x000F
mov dx, 0x0200 ; ROW 2, COL 0
mov cx, 28
push ax
mov ax, ds
mov es, ax
pop ax
mov bp, IMLoaderRunning
int 0x10
其实在上一篇中已经实现了该功能,这段代码与上篇文章中的 Loader
代码几乎相同而且较为简单,仅使用了 INT 10h, AH=13h
的中断共能实现打印字符串,因此不再对代码进行相信分析了。
在代码开始添加了两个声明,第一个 [SECTION .s16]
,是在此程序中追加定义了一个名为 .s16
的段。而第二行的 BITS 16
,使用 BITS
伪指令告诉 NASM
编译器生成的代码将会运行在 16
或 32
位的处理器上。BITS 16
、BITS 32
。
当声明是 BITS 16
,即默认处于 16
位宽下,当需要使用 32
位宽的数据指令时,则需要在指令前加入前缀 0x66
,需要使用 32
位的地址指令时需在指令前加入 0x67
。同理在 BITS 32
下访问 16
指令也需要加前缀。
[注]:'BITS 16' 等价于 [BITS 16]。
5. 开启实模式下 4GB 寻址能力
在实模式下通常能寻址的空间只有 1MB
,但接下来将会打破这一限制,将其开启到 4GB
的寻址能力。
5.1. 代码实现
具体代码如下:
;====== Open address A20
push ax
in al, 92h
or al, 00000010b
out 92h, al
pop ax
cli
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
5.2. IN 和 OUT 指令
IN
和 OUT
指令均是对端口操作的指令。
IN
指令通过指令从指定端口数据到寄存器。使用格式如下:
in 寄存器名, 端口号
例:
in al, 21h ; 表示从 21h 端口读取一个字节数据到 al
in ax, 21h ; 表示从 21h 端口读取一个字节数据到 al,从 22h 端口读取一个字节数据到 ah
OUT
指令将寄存器中存储的数据输出到指定端口。使用格式如下:
out 端口号, 寄存器名
例:
out 21h, al ; 表示将 al 中存储的数据写入到 21h 端口
out 21h, ax ; 表示将 ax 中存储的数据写入到 21h 开始的连续两个字节。(port[21h] = al, port[22h] = ah)
5.3. CLI 和 STI 指令
CLI
指令,禁止中断发生;
STI
指令,允许中断发生。
这两个指令只能在内核模式下执行,不允许在用户模式下执行,并且当使用 CLI
禁用中断后,应尽可能快速的使用 STI
恢复中断。
5.4. CR0 寄存器,GDT 表和 LGDT、SGDT 指令
在 Intel
处理器中有 4
个控制寄存器,CR0
、CR1
、CR2
、CR3
。本文中只用到了 CR0
的第 0
位,该位是保护允许位 (Protected Enable
),用于开启保护模式,当 PE = 1
时,表示开启保护模式,PE = 0
时,则运行在实模式。
全局描述符表 (Global Descriptor Table——GDT
),在整个系统环境中,全局描述符表 (GDT
) 只有一张(此处特指单处理器环境),一个处理器对应一个 GDT
表。理论上 GDT
可以被放置在内存的任何位置,只要将放置 GDT
的入口地址让 CPU
知晓即可。在 Intel
处理器中设计人员专门提供了一个全局描述符表寄存器 (Gloal Descriptor Table Register——GDTR
) 用来存放 GDT
表的入口地址,其结构如下图。
GDTR
中保存 GDT
在内存中的基地址和表长界限,其中基地址指定了 GDT
表的第 0
字节在内存空间的地址,表界限则指明 GDT
表的字节长度。
指令 LGDT
、SGDT
分别用于加载和保存 GDTR
寄存器的内容。在机器刚上电或处理器复位后,基地址会被默认设置为 0
,而长度值被默认设置为 0xFFFF
。在保护模式初始化过程中必须给 GDTR
加载一个新值。
保护模式下的段寄存器由 16
位的选择器和 64
位的段描述符寄存器构成如下图。
在实模式下,GDTR
访问全局描述符表 GDT
是通过段选择子 (Selector
) 来完成。段选择子是一个 16
位的寄存器,如下图。
段选择子包含三部分:描述符索引 (Index
)、TI
、请求特权级别 (RPL
)。其 Index
部分表示所需要段的描述符在全局描述符表的位置,这个位置再根据 GDTR
中存储的全局描述符基址就可以找到相应的描述符。再用描述符表中的段基址加上偏移地址 (SEL << 4 + OFFSET
) 就可以转换为实际物理地址。
段选择子中的 TI
只有 0
或 1
两种情况,当为 0
时表示在 GDT
中寻找,为 1
时表示在局部描述符表 LDT
(Local Descriptor Table
) 中查找。
请求特权级别 (RPL
) 表示选择子的特权等级,一共有 4
个特权等级(0
级、1
级、2
级、3
级)。
关于特权级的说明:任务中的每一个段都有一个特定的级别。每当一个程序试图访问某一个段时,就将该程序所拥有的特权级与要访问的特权级进行比较,以决定能否访问该段。系统约定,CPU
只能访问同一特权级或级别较低特权级的段。
OK,这部分内容暂时不需要理解太透彻,只要掌握好我们用到的几个指令以及全局描述符表和段选择子这个概念就好。
5.5. A20 线
当 IBM PC AT
系统被制造出来时,新的 Intel 286
处理器以及以后版本并不兼容旧的 x86
处理器。旧的x86
处理器(Intel 8086
)有 20
位地址总线,这样可以访问最高 1MB
内存。而 Intel 386
和以后版本有 32
地址总线,这样可以访问最高 4GB
的内存。但是旧的 8086
处理器没有这么大的地址总线。为了保持兼容性 Intel
在地址线的第 20
位上制造了一个逻辑 OR
门,以便可以开启或关闭超过 20
位的地址总线。这样,为了兼容旧的处理器,在机器开启时 A20
默认被禁止的。
开启 A20
线有以下几种方法:
- 1. 通过键盘控制器开启,但由于键盘控制器是低速设备,以至于开启的速度相对较慢;
- 2. A20 快速门 (
Fast Gate A20
),使用I/O
端口0x92
来处理A20
信号线; - 3. 使用
BIOS
中断INT 15h, AH=2401
中断服务开启A20
地址线,AH=2400
可禁用A20
地址线,AH=2403
可查看A20
地址线开启状态。
5.6. 代码详解
在了解了上述内容后,现在来开始逐行分析代码:
- 第一行:将
ax
寄存器的值压入栈中保存起来; - 第二行:使用
in
指令从92h
端口读取一个字节的数据并保存到al
中; - 第三行:用
al
的值逻辑或上00000001b
,即将第一位置1
; - 第四行:使用
out
指令将al
中的数据写入到92h
端口; - 第五行:弹出栈中的一个数据保存进
ax
中; - 第六行:使用
cli
指令关闭所有中断; - 第七行:仅填入数据
0x66
,作为指令前缀作用; - 第八行:使用
lgdt
指令将GDT
表的信息加载到GdtPtr
地址处; - 第九行:用
eax
来保存cr0
寄存器的值; - 第十行:用
eax
的值 逻辑或上1
,即将第0
位置1
,第0
位是保护允许位PE
(Protected Enable
); - 第十行:用
eax
的值为cr0
赋值,开启保护模式; - 第十一行:将段选择子
SelectorData32
的值保存到ax
中;(此处暂时不用特别清楚的了解段选择子的概念,先知道是这个东西就好) - 第十二行:将
ax
的值赋值给段寄存器fs
; - 第十三、十四、十五行:与之前开启保护模式相反,此处为关闭保护模式,即开启实模式;
- 第十六行:使用
sti
指令允许中断响应。
可能看到这里,大家还是不能明白这段代码到底做了什么,接下来将以验证的方式来分析这段代码的具体作用。
不过需要正确运行代码,需要先在代码最后添加如下内容:
[SECTION gdt]
LABEL_GDT: dd 0, 0
LABEL_DESC_CODE32: dd 0x0000FFFF, 0x00CF9A00
LABEL_DESC_DATA32: dd 0x0000FFFF, 0x00CF9200
GdtLen equ $ - LABEL_GDT
GdtPtr dw GdtLen - 1
dd LABEL_GDT
SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT
SelectorData32 equ LABEL_DESC_DATA32 - LABEL_GDT
5.7. 验证 1
首先验证不执行这段代码的结果是什么样的,我们在 push ax
这一行指令之前加上 jmp $
,例如下图:
然后编译此代码
[imaginemiracle@imos-ws imLoader-v0.2]$ nasm IMLoader-v0.2.S -o IMLoader-v0.2.bin
并挂载镜像文件,把编译好的文件放入镜像中:
[imaginemiracle@imos-ws img]$ sudo mount -t vfat -o loop imboot-v0.2.img ./imboot_fat/.
[imaginemiracle@imos-ws img]$ cd imboot_fat/
[imaginemiracle@imos-ws imboot_fat]$ sudo cp ../../imboot/imboot-v0.2/imboot-v0.2.bin ./imLoader.bin
[imaginemiracle@imos-ws imboot_fat]$ sync
[注]:此处不清楚在干什么的小伙伴们可以翻一下之前的文章,你应该是没有看之前的向镜像中写入文件的步骤,或者是忘记了。
然后打开 bochs
,运行镜像文件:
[imaginemiracle@imos-ws bochs-run]$ bochs -f bochsrc
之后输入回车,接着输入 `c` 运行。
看到这里,则说明运行正确,接着切换到中断并按快捷键 Ctrl + c
,暂停运行进入调试模式,并输入 sreg
回车,查看各个段寄存器情况:
这里我们主要查看 fs
的范围,可以看到未执行新模块的时候 fs
的范围是 [0x0000_0000, 0x0000_FFFF]
,也就是 fs
的寻址范围是 0x10000
,即 64 KB
大小。
5.8. 验证 2
将之前添加的 jmp $
删除,并在 sti
指令后加入 jmp $
,如下图:
接下来再以之前同样方法写入镜像,并运行,效果如下图:
视觉效果和之前一样没有变化,再以同样的方法查看 fs
的范围:
这时候可以看到 fs
的寻址范围改变为 [0x0000_0000, 0xFFFF_FFFF]
,也就是 fs
的寻址范围是 0x1_0000_0000
,即 4 GB
。
那么现在应该清楚这段代码的作用了,就是为了突破代码在实模式下寻址范围的限制,使其可以寻址更大范围的内存地址。
6. 查找 IMKernel.bin 文件功能
现在已经开启了 fs
的 4 GB
寻址能力,这个 IMLoader
程序的准备工作已经完成,这时候就可以来查找在镜像中需要加载的 IMKernel.bin
文件了。
6.1. 代码实现
具体代码如下:
Label_Search_File_IM:
mov word [SectorNo], SectorNumOfRootDirStart ; SectorNumOfRootDirStart == 19
Label_Search_In_Root_Dir_Begin:
cmp word [RootDirSizeForLoop], 0
jz Label_No_KernelBin
dec word [RootDirSizeForLoop]
mov ax, 0x00
mov es, ax
mov bx, 0x8000
mov ax, [SectorNo]
mov cl, 1
call Func_ReadOneSector
mov si, KernelFileName
mov di, 0x8000
cld ; CF == 0
mov dx, 0x10 ; 512 / 32 = 16 = 0x10
Label_Search_For_KernelBin:
cmp dx, 0
jz Label_Next_RootSector
dec dx
mov cx, 11 ; sizeof(DirEntry->Name) == 11
Label_Cmp_FileName:
cmp cx, 0
jz Label_FileName_Found ; Found imkernel.bin
dec cx
lodsb ; mov al, byte [es:si]
cmp al, byte [es:di]
jz Label_Go_On
jmp Label_Different
Label_Go_On:
jmp Label_No_Test
mov ax, 0x1301
mov bx, 0x000F
mov dx, word [PrintRow]
add word [PrintRow], 0x0100
mov cx, 28
push ax
mov ax, ds
mov es, ax
pop ax
mov bp, IMLoaderRunning
int 0x10
Label_No_Test:
inc di
jmp Label_Cmp_FileName
Label_Different:
and di, 0xFFE0
add di, 0x20
mov si, KernelFileName
jmp Label_Search_For_KernelBin
Label_Next_RootSector:
add word [SectorNo], 1
jmp Label_Search_In_Root_Dir_Begin
Label_No_KernelBin:
mov ax, 0x1301
mov bx, 0x008C
mov dx, 0x0300
mov cx, 35
push ax
mov ax, ds
mov es, ax
pop ax
mov bp, NoKernelMessage
int 0x10
jmp $
;====== Read disk, once one sector
; Function: ReadOneSector
; REG Parameter:
; AX: The sector number for read
; CL: Read sector count
; BX: Address of save.
Func_ReadOneSector:
push bp
mov bp, sp
sub esp, 2
mov byte [bp - 2], cl
push bx
mov bl, [BPB_SecPerTrk] ; BPB_SecPerTrk = 18
div bl ; ax / bl = al(...ah)
inc ah
mov cl, ah
mov dh, al
and dh, 1
shr al, 1
mov ch, al
pop bx
mov dl, [BS_DrvNum]
Label_Go_On_Reading:
mov ah, 2
mov al, byte [bp - 2]
int 13h
jc Label_Go_On_Reading ; check CF == 1, then jump
add esp, 2
pop bp
ret
;====== search file module variables
SectorNo dw 0
RootDirSizeForLoop dw RootDirSectors ; RootDirSectors == 14
Odd db 0
PrintRow dw 0x0600
PrintCol dw 40
这段代码虽然看起来很多,但实际上只有两个模块,一个是定义的提供软盘扇区读取的功能模块 Func_ReadOneSector
,和一个搜索文件的主模块 Label_Search_File_IM
。而且这两个模块在上篇文章中也解释到了,与之前不同的仅仅是文件名不同,此处的 KernelFileName
的定义如下:
KernelFileName: db "IMKERNELBIN", 0
那么这个模块就不再多做说明,若有不清楚的地方可以翻看上一篇文章查阅,或者留言在下方都可以。当文件名匹配成功后,将会跳入 Label_FileName_Found
模块。
7. 文件匹配成功模块——验证版
其实到此为止也已经写了不少行代码了,如果对自己写的代码心里没底的话,现在将会是一个测试的好时机。我们清楚的是,当文件没匹配到将会执行 Label_No_KernelBin
,打印一段报错的字符串,当匹配成功后将会跳入 Label_FileName_Found
模块。
此处我们暂时先实现一个简单的 Label_FileName_Found
用来验证使用。
7.1. 代码实现
具体代码如下:
Label_FileName_Found:
mov ax, 0x1301
mov bx, 0x008A
mov dx, 0x0400
mov cx, 28
push ax
mov ax, ds
mov es, ax
pop ax
mov bp, FoundKernelMesg
int 0x10
jmp $
;====== search file module string
KernelFileName: db "IMKERNELBIN", 0
NoKernelMessage: db "ERROR: No search IMKernel.bin (-_-)"
FoundKernelMesg: db "IMKernel.bin is found. (^v^)"
这段代码很简单,利用 INT 10h,AH=13h
中断功能打印一段字符串。
7.2. 验证 1
记得先把之前的验证使用的 jmp $
删掉啊。
我们首先来验证当没有 IMKernel.bin
文件的情况。将添加好的代码编译,并放入挂载的镜像中。
[imaginemiracle@imos-ws imboot_fat]$ ls
IMLoader.bin
运行 bochs
查看效果。
可以看到程序正确的执行到了没有 IMKernel.bin
文件的模块。
7.3. 验证 2
接下来验证当有 IMKernel.bin
的情况,首先需要先制造一个 IMKernel.bin
,很简单,只需要将 IMLoader.bin
复制一份或者随便创建个文件,在里面添加些内容都是可以的。这里笔者选择直接复制来的快些。
[imaginemiracle@imos-ws imboot_fat]$ sudo cp IMLoader.bin IMKernel.bin
[imaginemiracle@imos-ws imboot_fat]$ sync
[imaginemiracle@imos-ws imboot_fat]$ ls
IMKernel.bin IMLoader.bin
然后再次执行 bochs
查看效果。
当看到屏幕中冒出绿的你发慌的文字,那就说明到目前为止,我们所写的代码是没有问题的,功能都是正确的。OK,那就可以继续放心的往下继续编写了。
8. 文件匹配成功模块——完整版
在找到 IMKernel.bin
文件后,我们则需要将其加载到内存中,这里我们先将其加载到实模式可正常寻址的 1 MB
以内的 0x7E00
位置,再将其通过 fs
转存到 1 MB
为起始地址处。但是还记得吗?加载文件时,还需要对其 FAT
表项解析,当前的程序还没有 FAT
表项解析模块,先把 FAT
表项解析模块添加进来吧。
8.1. 目录项解析模块
代码如下:
;====== Get FAT Entry
; REG Parameter:
; AX: FAT Entry Index
Func_GetEntry:
push es
push bx
push ax
mov ax, 00
mov es, ax
pop ax
mov byte [Odd], 0
mov bx, 3
mul bx
mov bx, 2
div bx
cmp dx, 0
jz Label_Even
mov byte [Odd], 1
Label_Even:
xor dx, dx
mov bx, [BPB_BytesPerSec]
div bx
push dx
mov bx, 0x8000
add ax, SectorNumOfFAT1Start
mov cl, 2
call Func_ReadOneSector
pop dx
add bx, dx
mov ax, [es:bx]
cmp byte [Odd], 1
jnz Label_Even_2
shr ax, 4
Label_Even_2:
and ax, 0x0FFF
pop bx
pop es
ret
这段代码根据 ax
中的 FAT
表项索引,读取对应索引表项中的数据,并将最终的表项值存储于 ax
寄存器中,这部分代码也是直接使用的上篇文章中的代码,此处也就不再过多的阐述了。
8.2. 文件加载模块
此处将要实现 Label_FileName_Found
模块,不过也是基于之前我们写过的代码添加和修改一部分内容完成,也会很好理解。
代码如下:
Label_FileName_Found:
mov ax, RootDirSectors ; RootDirSectors = 14
and di, 0xFFE0 ; Get RoorDir Entry Start Address
add di, 0x1A ; Get File start cluster index
mov cx, word [es:di] ; Get file start cluster index
push cx
add cx, ax
add cx, SectorBalance ; calculate file start cluster real sector index in data sector
mov eax, BaseTmpOfKernelAddr ; 0x00
mov es, eax
mov bx, OffsetTmpOfKernelFile ; 0x7E00
mov ax, cx ; args ready for Func_ReadOneSector
Label_Go_On_Loading_File:
push ax
push bx
mov ah, 0x0E
mov al, '*'
mov bl, 0x0F
int 0x10
pop bx
pop ax
mov cl, 1
call Func_ReadOneSector
;pop ax
;++++++++++++++ new
push cx
;push eax
push fs
push edi
push ds
push esi
mov cx, 0x0200 ; 0x200 = 512
mov ax, BaseOfKernelFile ; BaseOfKernelFile = 0x00
mov fs, ax
;jmp $
mov edi, dword [OffsetOfKernelFileCount]
mov ax, BaseTmpOfKernelAddr
mov ds, ax
mov esi, OffsetTmpOfKernelFile
Label_Mov_Kernel:
mov al, byte [ds:esi] ; start of BaseTmpOfKernelAddr << 4 + OffsetTmpOfKernelFile = 0x7E00
mov byte [fs:edi], al
inc esi
inc edi
loop Label_Mov_Kernel ; if cx > 0, then jump to Label_Mov_kernel (init cx = 512)
mov eax, 0x1000
mov ds, eax ; set ds = 0x1000
mov dword [OffsetOfKernelFileCount], edi
pop esi
pop ds
pop edi
pop fs
;pop eax
pop cx
pop ax
;-------------- end of new
call Func_GetFATEntry
cmp ax, 0x0FFF
jz Label_File_Loaded
push ax
mov dx, RootDirSectors
add ax, SectorBalance
add ax, dx
;add bx, [BPB_BytesPerSec]
jmp Label_Go_On_Loading_File
Label_File_Loaded:
mov ax, 0x0B800
mov gs, ax
mov ah, 0x0F
mov al, 'O'
mov [gs:((80 * 0 + 39) * 2)], ax
mov al, 'v'
mov [gs:((80 * 0 + 40) * 2)], ax
mov al, 'e'
mov [gs:((80 * 0 + 41) * 2)], ax
mov al, 'r'
mov [gs:((80 * 0 + 42) * 2)], ax
jmp $
;====== variables
SectorNo dw 0
RootDirSizeForLoop dw RootDirSectors ; RootDirSectors == 14
Odd db 0
PrintRow dw 0x0600
PrintCol dw 40
OffsetOfKernelFileCount dd OffsetOfKernelFile
这段代码虽然不难,但希望各位读者能够完全掌握。这其中由两个主要模块组成,一个是 Label_FileName_Found
用来提取文件目录项中的文件起始簇号,并根据 FAT
表项的内容逐扇区的读取 IMKernel.bin
文件,并利用 Loop
循环以字节为单位的逐扇区搬运到 1MB
为起始地址的内存空间上,另一个是 Label_File_Loaded
用于在搬运结束后打印信息提示搬运完成。
8.3. 代码详解
虽然此段代码并没有难度,但笔者认为还是有必要详细解读一下这段代码内容,需要注意的是当跳转到此段代码执行时,di
寄存器保存着当前目录项的文件名最后一个字节地址。
🚩Label_FileName_Found
- 第一行:将根目录区的起始扇区号
RootDirSectors
的值赋值给ax
;(RootDirSectors
=14
) - 第二行:用保存着目录项文件名最后一个字节地址
di
寄存器按位逻辑与上0xFFE0
,以此操作来获取目录项的首地址; - 第三行:为保存着目录项首地址的
di
寄存器加上文件起始簇在目录项中的偏移大小0x1A
(26
),以此得到文件的起始簇号地址; - 第四行:读取文件起始簇号
es:di
并保存到cx
中; - 第五行:将文件起始簇号
cx
的值压入栈中保存; - 第六行:将文件起始簇号
cx
加上根目录项的起始扇区号ax
; - 第七行:将
cx
加上用于计算使用的数据区起始簇号SectorBalance
,即得到文件数据的起始扇区号; - 第八行:将
BaseTmpOfKernelAddr
的值保存到eax
中;(BaseTmpOfKernelAddr
=0x00
) - 第九行:用
eax
初始化es
段寄存器; - 第十行:将
OffsetTmpOfKernelFile
的值保存到bx
中;(OffsetTmpOfKernelFile
=0x7E00
)
[注]:这两步操作是为了为 Func_ReadOneSector 读取数据的保存地址 es:bx,文件读取到这个地址只是暂存起来,这里并不会用于真正存储的地址,稍后会将其搬运到 1MB 处。
- 第十一行:将保存着文件起始扇区号的
cx
的值赋值给ax
,以供稍后调用Func_ReadOneSector
使用;
🚩Label_Go_On_Loading_File
- 第十二、十三行:分别将
ax
、bx
的数据压入栈中保存; - 第十四行:为
ah
寄存器赋值0x0E
; - 第十五行:使
al
寄存器保存字符*
; - 第十六行:为
bl
赋值为0x0F
; - 第十七行:开启中断
int 10h
,使用主功能号ah=0Eh
在中断上显示一个字符功能; - 第十八、十九行:先后弹出栈中两个元素,分别保存进
bx
、ax
寄存器中; - 第二十行:将
cl
赋值为1
; - 第二十一行:调用
Func_ReadOneSector
模块,读取IMKernel.bin
文件的一个扇区数据即512
字节数据保存到BaseTmpOfKernelAddr << 4 + OffsetTmpOfKernelFile
即0x7E00
地址处;
[注]:new 行以上为之前代码部分,大家应该很熟悉,以下部分将是新添加部分。
- 第二十二到二十六行:先后将
cx
、fs
、edi
、ds
、esi
寄存器的值压入栈中保存;
[注]:有效行不含带注释行或空格行。
- 第二十七行:为
cx
赋值为0x200
(512
);(以备后续loop
使用) - 第二十八行:为
ax
赋值为内存存储的基地址BaseOfKernelFile
;(BaseOfKernelFile
=0x00
) - 第二十九行:用存储着欲放置内核基地址
BaseOfKernelFile
的ax
寄存器初始化fs
段寄存器; - 第三十行:将获取临时变量
OffsetOfKernelFileCount
两个字长度的值赋值给edi
寄存器; - 第三十一行:将中转存储的基地址
BaseTmpOfKernelAddr
的值赋值给ax
; - 第三十二行:用
ax
赋值ds
段寄存器; - 第三十三行:将转存偏移地址
OffsetTmpOfKernelFile
的值赋值给esi
;
🚩Label_Mov_Kernel
- 第三十四行:读取转存
ds:esi
地址处一个字节数据保存到al
; - 第三十五行:将刚读取到的数据
al
保存到欲存储内核地址1MB
以上的地址fs:edi
处,完成一个字节搬运; - 第三十六、三十七行:
esi
、edi
分别自加1
; - 第三十八行:使用
loop
指令跳转到Label_Mov_Kernel
处重复执行,直到cx <= 0
为止,此时将完成一个扇区数据的搬运工作; - 第三十九行:将
eax
的值设为0x1000
; - 第四十行:用
eax
初始化ds
段寄存器,即ds = 0x1000
,为了下面能正常使用本地变量,因为之前修改过ds
寄存器,因此需要将其改回原来的值。为什么是0x1000
,这里可以点击查看之前的 图片 看一下在修改之前ds
的值也是0x1000
,便将其修改回去罢了。 - 第四十一行:将
edi
的值保存到临时变量OffsetOfKernelFileCount
中; - 第四十二到四十七行:分别弹出栈中
6
个元素,并先后分别保存进esi
、ds
、edi
、fs
、cx
、ax
中,需特别注意ax
保存的值,这个值是该模块最开始第一次压入栈中的值,即根目录项中的文件起始簇号,也就是FAT
表项的索引号; - 第四十八行:调用
Func_GetFATEntry
读取对应索引号的FAT
表项,并将其结果保存到ax
中; - 第四十九行:比较
ax
与0x0FFF
,查看当前簇是否是结束簇; - 第五十行:当
ax == 0x0FFF
则跳转到Label_File_Loaded
执行,否则跳过执行下一行指令; - 第五十一行:将保存着文件在数据区的存储簇号
ax
的数据压栈保存,以待下一次pop
到ax
中使用; - 第五十二行到五十四:根据
FAT
表项值计算文件的下一个簇的扇区号; - 第五十五行:跳转到
Label_Go_On_Loading_File
重复执行,直到文件读取结束;
🚩Label_File_Loaded
这段代码的功能是将一个字符显示到屏幕的指定坐标处,这里笔者一个共让其显示了 4
个字符,组合起来是单词 Over
,表示文件的加载结束。
对于代码本身没有什么特别需要解释的地方,全部使用 mov
指令做简单的赋值操作。主要解释的是这些操作的含义。在代码开始时首先将 GS
段寄存器的基地址设置在 0xB800
的位置,并将 AH
寄存器的值赋为 0x0F
,为 AL
赋值为 O
,接着将 AX
寄存器的值填充到地址 0xB800
向后偏移 (80 x 0 + 39) x 2
的位置。该方法与 BIOS
的 INT 10h
中断服务程序相比,更加符合显存的操作习惯。从内存地址 0xB800
开始,是一段专门用于显示字符的内存空间,每个字符占用两个字节的内存空间,其中低字节保存显示的字符,高字节保存字符的颜色属性。 此处的 0x0F
表示字符使用白色字体、黑色背景。到目前为止,这段程序是可以直接执行的,
在最开始的 1 MB
物理地址空间内,不仅由显示字符的内存空间,还有显示像素的内存空间以及其他用途的内存空间。这段代码仅让读者了解到显存的操作方法,毕竟不能太依赖于 BIOS
提供的中断服务。
8.4. 验证代码
到目前为止,我们写的代码是完全可以执行下去的,按照熟悉的流程从编译到将文件放入镜像中,再到运行 bochs
的详细步骤就不再提供了,相信大家已经完全熟悉了,那么直接看运行结果吧。
我们看到成功的输出了 Over
的字样,就说明我们 IMKernel.bin
文件加载并搬运成功。在第三行之所以会打印这么多的 *
是因为程序在每搬运一个扇区时将会输出 *
作为提示,这里笔者将 IMKernel.bin
文件改大了,因此输出的 *
也变多了,大家自己也可以找一个较大的文件过来尝试。
9. 关闭软驱马达模块
当 Loader
程序加载完成 IMKernel.bin
后,软盘驱动器将不再使用,我们需要将其关闭;
9.1. 代码实现
具体代码如下:
KillMotor:
push dx
mov dx, 0x03F2
mov al, 0
out dx, al
pop dx
这段代码较为简单,可以看出这里是通过操作 0x03F2
端口完成关闭软驱马达的。
9.2. 0x03F2 端口
0x03F2
端口的控制功能如下:
bit | 名称 | 描述 |
---|---|---|
7 | MOT_EN3 | 控制软驱 D 马达,1:启动;0:关闭 |
6 | MOT_EN2 | 控制软驱 C 马达,1:启动;0:关闭 |
5 | MOT_EN1 | 控制软驱 B 马达,1:启动;0:关闭 |
4 | MOT_EN0 | 控制软驱 A 马达,1:启动;0:关闭 |
3 | DMA_INT | 1:允许 DMA 和中断请求;0:禁用 DMA 和中断请求 |
2 | RESET | 1:允许软盘控制器发送控制信息;0:复位软盘驱动器 |
1 | DRV_SEL1 | 00~11 用于选择软盘驱动器 A~D |
0 | DRV_SEL0 |
10. 获取物理地址信息模块
内核程序以及成功利用转存空间搬运到 1 MB
以上的地址空间,此时这段转存空间可以作为他用,这里将其用于保存物理地址空间信息,之后操作系统将会在初始化内存管理单元时解析此时读取的内存结构数组。
10.1. 代码实现
具体代码如下:
;====== Get memory address size type
mov ax, 0x1301
mov bx, 0x000F
mov dx, 0x0500
mov cx, 24
push ax
mov ax, ds
mov es, ax
pop ax
mov bp, GetMemStructMessage_Start
int 0x10
mov ebx, 0
mov ax, 0x00
mov es, ax
mov di, MemoryStructBufferAddr ; 0x7E00
Label_Get_Mem_Struct:
mov eax, 0x0E820
mov ecx, 20
mov edx, 0x534D4150
int 0x15
jc Label_Get_Mem_Fail ; if CF==1, then jmp
add di, 20
cmp ebx, 0 ; 判断是否映射完成
jne Label_Get_Mem_Struct ; if ZF==0, then jmp Label_Get_Mem_Struct
jmp Label_Get_Mem_OK
Label_Get_Mem_Fail:
mov ax, 0x1301
mov bx, 0x000F
mov dx, 0x0600
mov cx, 25
push ax
mov ax, ds
mov es, ax
pop ax
mov bp, GetMemStructErrMessage
int 0x10
jmp $
Label_Get_Mem_OK:
mov ax, 0x1301
mov bx, 0x000F
mov dx, 0x0700
mov cx, 29
push ax
mov ax, ds
mov es, ax
pop ax
mov bp, GetMemStructOKMessage
int 0x10
此处应该绝大多数代码都没问题,唯一可能不清楚的就是关于如何获取内存结构信息。物理地址空间信息由一个结构体数组构成,这里利用 BIOS
中断 INT 15h, AX=E820h
,来将其读取到 0X7E00
地址处,由于读取这个结构的操作码为 E820h
,因此人们也常称为 E820
表,计算机平台的地址空间划分情况都能从这个结构体数组中反映出来。
10.2. INT 15h, AX = E820h
这里是 《Advanced Configuration and Power Interface (ACPI) Specification V6.3》中对 INT 15h, AX=E820h
的描述。最新版本是 6.4
但只有 Doc
版,Advanced Configuration and Power Interface (ACPI) Specification v6.4,不过新旧版本对于此处的描述都是一样的。
大概意思为:此接口仅在基于 IA-PC
的系统上以实模式使用,并为所有已安装的 RAM
,以及 BIOS
保留的物理内存范围。 返回地址映射通过对该接口的连续调用; 每个返回关于单个物理范围的信息地址。每个范围都包含一个类型,指示如何处理物理地址范围由 OSPM
。
在 x86 real mode
模式下,这个结构以 mmap (memory map)
的形式提供实际系统的 System RAM
物理内存分布情况、被 BIOS
设置为保留 (Reserved
) 的地址范围,以及内存空洞等。(在实际情况中,尤其是 64
位系统下,实际的物理内存远远达不到 64
位用尽的情况,BIOS
将一部分物理地址空间分配给 PCI
以及 ACPI
使用)
以下是使用 INT 15h
的 EAX=E820h
功能时的相关寄存器详情:(Label_Get_Mem_Struct
的代码就是根据下表配置的)
- EAX:是设置
INT 15h
中断的主功能号;(这里使用E820h
) - EBX:该寄存器保存中断服务返回的值,用于表示下一个映射物理内存范围,若返回值为
0
,则表示所有物理内存映射完成。如果是第一次调用,则必须设置EBX
为0
; - ES:DI:
BIOS
会将地址范围描述结构信息填充到以此为起始地址的内存空间; - ECX:指定
Address Range Descriptor Structure
的大小,最小设置为20 Bytes
; - EDX:这里固定设置为
0x534D4150
(SMAP
)。
10.3. Address Range Descriptor Structure
本文暂时没有涉及对此结构解析的过程,为不影响本文进度此处不对该结构做任何解释,这里先让各位了解一下:
10.4. 验证
笔者的建议是,我们写完一个可以用的功能模块就验证一次,这样会避免到最后调试起来过于麻烦,也能增强大家开发的自信心。
直接在打印完成信息后添加一行 jmp $
就好,然后运行 bochs
查看效果吧!
看来我们的代码没有什么问题,OK,删掉用来验证的 jmp $
然后继续吧!
11. 字符显示模块
该模块将会实现一个可直接将一个十六进制数显示在屏幕上,该模块需要使用 AL
作为参数使用,功能为:
AL
:待显示的十六进制数(长度:1 Byte
)。
11.1. 代码实现
具体代码如下:
[SECTION .s16lib]
[BITS 16]
;====== Display num in al
Label_DisplayAL:
push ecx
push edx
push edi
mov edi, [DisplayPosition]
mov ah, 0x0F
mov dl, al
shr al, 4
mov ecx, 2
.begin:
and al, 0x0F
cmp al, 9
ja .1
add al, '0'
jmp .2
.1:
sub al, 0x0A
add al, 'A'
.2:
mov [gs:edi], ax
add edi, 2
mov al, dl
loop .begin
mov [DisplayPosition], edi
pop edi
pop edx
pop ecx
ret
11.2. 代码详解
在进入执行该段代码时,AL
寄存器保存着需要显示的字符。首先将在此模块中即将改变的几个寄存器值保存在栈中,接着将保存屏幕偏移值的变量 DisplayPosition
赋值给 EDI
寄存器,并 AH
中写入显示字体的颜色属性,这里的值为 0X0F
表示白字黑底。
接着先将 AL
的值保存到 DL
中,因为需要先显示字节中的高四位,需要先将其备份一份。紧接着将 AL
的值向右移动 4
位,设置循环技术寄存器 ECX
的值为 2
;然后判断 AL
的值是否大于 9
,如果是则跳转到 .1
处,先为其减去 0x0A
,再加上 'A'
,若不是则直接加上 '0'
,目的是为了转换为对应的字符数值,此时 AL
的高 4
位已经显示出来,现在将保存屏幕偏移值的 EDI
加上 2
,即表示坐标向右移动一个单位,然后循环将 AL
的低 4
位显示出来。
12. 显示模式配置模块
该模块将设置 SVGA
芯片的显示模式,并利用 Label_DisplayAL
模块将其支持的显示模式号打印出来。
12.1. 代码实现
具体代码如下:
Label_SVGA_VBE_Start:
;====== Get SVGA information
mov ax, 0x1301
mov bx, 0x000F
mov dx, 0x0800 ; row: 8
mov cx, 24
push ax
mov ax, ds
mov es, ax
pop ax
mov bp, StartGetSVGAVBEInfoMessage
int 0x10
mov ax, 0x00
mov es, ax
mov di, 0x8000
mov ax, 0x4F00
int 0x10
cmp ax, 0x004F
jz .KO
;====== Fail
mov ax, 0x1301
mov bx, 0x008C
mov dx, 0x0900
mov cx, 25
push ax
mov ax, ds
mov es, ax
pop ax
mov bp, GetSVGAVBEInfoErrMessage
int 0x10
jmp $
.KO:
mov ax, 0x1301
mov bx, 0x000F
mov dx, 0x0A00 ; row: 10
mov cx, 29
push ax
mov ax, ds
mov es, ax
pop ax
mov bp, GetSVGAVBEInfoOKMessage
int 0x10
;====== Get SVGA Mode Info
mov ax, 0x1301
mov bx, 0x000F
mov dx, 0x0C00
mov cx, 25
push ax
mov ax, ds
mov es, ax
pop ax
mov bp, StartGetSVGAModeInfoMessage
int 0x10
mov ax, 0x00
mov es, ax
mov si, 0x800E
mov esi, dword [es:si]
mov edi, 0x8200
Label_SVGA_Mode_Info_Get:
mov cx, word [es:esi]
;====== Display SVGA Mode information
push ax
mov ax, 0x00
mov al, ch
call Label_DisplayAL
mov ax, 0x00
mov al, cl
call Label_DisplayAL
pop ax
;======
cmp cx, 0x0FFFF
jz Label_SVGA_Mode_Info_Finish
mov ax, 0x4F01
int 0x10
cmp ax, 0x004F
jnz Label_SVGA_Mode_Info_FAIL
add esi, 2
add edi, 0x100
jmp Label_SVGA_Mode_Info_Get
Label_SVGA_Mode_Info_FAIL:
mov ax, 0x1301
mov bx, 0x008C
mov dx, 0x0D00
mov cx, 26
push ax
mov ax, ds
mov es, ax
pop ax
mov bp, GetSVGAModeInfoErrMessage
int 0x10
Label_SET_SVGA_Mode_VESA_VBE_FAIL:
jmp $
Label_SVGA_Mode_Info_Finish:
mov ax, 0x1301
mov bx, 0x000F
mov dx, 0x0E00
mov cx, 30
push ax
mov ax, ds
mov es, ax
pop ax
mov bp, GetSVGAModeInfoOKMessage
int 0x10
;jmp $
这段代码的功能是通过读取 Bochs
虚拟平台中的 SVGA
芯片支持的显示模式号,然后通过调用 Label_DisplayAL
模块将其打印出来。虽然这段代码较长,但大多都是利用 INT 10h, AH=13h
中断服务打印字符串,其余部分代码本身也比较简单,只是大家应该不太明白其含义罢了。各位读者可以暂时不用理会这段代码的原理,大概清楚他的功能即可。(千万别被这个劝退了呀!!!)
12.2. 验证 1
可以看到在代码的最后一行笔者已经添加了 jmp $
但是被注释了,现在开启这一行指令,编译并运行测试查看一下效果吧。
上面一行输出的 SVGA
芯片支持的显示模式号,可以对比之前的运行结果来看 之前的图片,会发现 IMBoot
程序启动时打印的字符串被覆盖掉了,这说明 IMBoot
程序中的字符串也在显存的 0x0B800
起始的内存空间里。测试完记得删掉 jmp $
或者注释掉。
12.2. 配置 SVGA
到现在为止我们已经获取到了 SVGA
芯片所支持的所有显示模式,接下来就是配置它了。这里将其分辨率配置为 1440 x 900
,其代码如下:(紧接着上面的代码写就好)
;====== Set the SVGA Mode(VESA VBE)
mov ax, 0x4F02
mov bx, 0x4180 ; mode set: 0x180 or 0x143
int 0x10
cmp ax, 0x004F
jnz Label_SET_SVGA_Mode_VESA_VBE_FAIL
;jmp $
12.3. SVGA 芯片的显示模式
在上述代码注释中的 0x180
和 0x143
是显示模式号,其具体信息如下:
模式 | 列 | 行 | 物理地址 | 像素点位数 |
---|---|---|---|---|
0x180 | 1440 | 900 | e000_0000h | 32bit |
0x143 | 800 | 600 | e000_0000h | 32bit |
通过设置不同的显示模式号,可以配置出不同的屏幕分辨率、每个像素点的数据位宽、颜色格式等属性。
12.4. 验证 2
在上面的代码最后一行添加 jmp $
来测试运行,结果如下。
可以看出来运行起来的整个屏幕都变大了,具体变为 1440 x 900
(图中蓝色矩形框所选范围),那看来是我们设置成功啦!(由于图片分辨率较高,在博客中可能直接看的话会模糊,建议点开后 图片 查看)
13. 一些变量和字符串的定义
文中出现的诸多未在代码中展示的字符串和变量,是因为需要将其定义在代码的末尾,使其不影响代码运行。
;====== variables
SectorNo dw 0
RootDirSizeForLoop dw RootDirSectors ; RootDirSectors == 14
Odd db 0
PrintRow dw 0x0600
PrintCol dw 40
OffsetOfKernelFileCount dd OffsetOfKernelFile
DisplayPosition dw ((80 * 0 + 0) * 2)
[SECTION gdt]
LABEL_GDT: dd 0, 0
LABEL_DESC_CODE32: dd 0x0000FFFF, 0x00CF9A00
LABEL_DESC_DATA32: dd 0x0000FFFF, 0x00CF9200
GdtLen equ $ - LABEL_GDT
GdtPtr dw GdtLen - 1
dd LABEL_GDT
SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT
SelectorData32 equ LABEL_DESC_DATA32 - LABEL_GDT
;====== search file module string
KernelFileName: db "IMKERNELBIN", 0
NoKernelMessage: db "ERROR: No search IMKernel.bin (-_-)"
FoundKernelMesg: db "IMKernel.bin is found. (^v^)"
;====== String
IMLoaderRunning: db "IMLoader is running... (^v^)"
GetMemStructMessage_Start: db "Start Get Memory Struct."
GetMemStructErrMessage: db "Get Memory Struct Failed!"
GetMemStructOKMessage: db "Get Memory Struct Successful!"
StartGetSVGAVBEInfoMessage: db "Start Get SVGA VBE Info."
GetSVGAVBEInfoErrMessage: db "Get SVGA VBE Info Failed!"
GetSVGAVBEInfoOKMessage: db "Get SVGA VBE Info Successful!"
StartGetSVGAModeInfoMessage: db "Start Get SVGA Mode Info."
GetSVGAModeInfoErrMessage: db "Get SVGA Mode Info Failed!"
GetSVGAModeInfoOKMessage: db "Get SVGA Mode Info Successful!"
# 不算结语的结语
注意:Loader 程序到此并未完成,但由于本文篇幅已经过于的长了,再阅读下去只会让各位读者产生疲惫,反而起到不好的影响。因此,笔者将下半部分内容写在了下一篇文章中,需要立即查看的读者可以现在点击继续学习!(如果是一口气看到这里的话,那么笔者建议站起来走走,喝杯茶休息一会也是甚好的选择😘)
非常感谢各位的耐心阅读,辛苦了!
下一篇文章链接:
《【实现操作系统 05】完善 Loader 程序,并加载内核(下)》
#参考文章
《全局描述符表 GDT》: https://www.techbulo.com/708.html
《BIOS Interrupts and Functions》: http://jyywiki.cn/pages/OS/manuals/BIOS-interrupts.pdf
《Advanced Configuration and Power Interface (ACPI) Specification》: https://uefi.org/sites/default/files/resources/ACPI_6_3_final_Jan30.pdf