【实现操作系统 04】完善 Loader 程序,并加载内核(上)

1. 前言

上一篇我们已经完成了一个较为完整的 Boot 程序,当 Boot 程序运行结束后将会加载 Loader 程序到内存中,并转入 Loader 的加载地址执行 Loader 程序。而在上一篇中写了一个非常简单的 Loader 程序,其本身并没有什么功能,只是输出一行字符串用来提示计算机已经加载了 Loader 程序并执行。本文将会在此前的 Loader 基础之上完善一个 Loader 程序应有的基本功能,在本文笔者将此引导加载程序命名为 IMLoader

2. Loader 程序简介

Loader 程序,继 Boot 之后的下一个运行的代码就是 Loader。在操作系统中一个 Loader 程序的基本工作有:检测硬件信息处理器模式切换向内核传参加载内核并启动

2.1. 检测硬件信息

为了能够保证操作系统能够正确运行,必须要在内核启动之前对运行系统的硬件环境进行检测并获取各种硬件的包含物理地址和硬件本身特性等信息(如:RAMROM、设备寄存器等),并最后会将这些信息交给系统内核管理。

硬件检测和信息获取主要通过 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. 详细描述

在此处首先定义了一个内核加载的地址分别由 BaseOfKernelFileOffsetOfKernelFile 组成,BaseOfKernelFile << 4 + OffsetOfKernelFile = 0x00 << 4 + 0x100000 = 0x100000 = 1MB,即内核加载的物理起始地址在 1MB 处,这是由于在 1MB 以下的物理空间并非完全可用,将会被划分为若干个子空间段,包含常规内存空间、非内存空间以及地址空洞等。且由于日后的内核体积不断增大更是不方便放在可用空间狭小的单元里,因此设定在 1MB 开始,即不显得浪费内存空间,也能够保证有足够的内存空间存放内核。

在接下来又定义了一个临时的转存地址分别由 BaseTmpOfKernelAddrOffsetTmpOfKernelFile 组成,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 编译器生成的代码将会运行在 1632 位的处理器上。BITS 16BITS 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 指令

INOUT 指令均是对端口操作的指令。

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 个控制寄存器,CR0CR1CR2CR3。本文中只用到了 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 表的字节长度。

指令 LGDTSGDT 分别用于加载和保存 GDTR 寄存器的内容。在机器刚上电或处理器复位后,基地址会被默认设置为 0,而长度值被默认设置为 0xFFFF。在保护模式初始化过程中必须给 GDTR 加载一个新值。

保护模式下的段寄存器由 16 位的选择器和 64 位的段描述符寄存器构成如下图。

在这里插入图片描述

在实模式下,GDTR 访问全局描述符表 GDT 是通过段选择子 (Selector) 来完成。段选择子是一个 16 位的寄存器,如下图。

在这里插入图片描述
段选择子包含三部分:描述符索引 (Index)、TI、请求特权级别 (RPL)。其 Index 部分表示所需要段的描述符在全局描述符表的位置,这个位置再根据 GDTR 中存储的全局描述符基址就可以找到相应的描述符。再用描述符表中的段基址加上偏移地址 (SEL << 4 + OFFSET) 就可以转换为实际物理地址。

段选择子中的 TI 只有 01 两种情况,当为 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 文件功能

现在已经开启了 fs4 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

  • 第十二、十三行:分别将 axbx 的数据压入栈中保存;
  • 第十四行:为 ah 寄存器赋值 0x0E;
  • 第十五行:使 al 寄存器保存字符 *
  • 第十六行:为 bl 赋值为 0x0F;
  • 第十七行:开启中断 int 10h,使用主功能号 ah=0Eh 在中断上显示一个字符功能;
  • 第十八、十九行:先后弹出栈中两个元素,分别保存进 bxax 寄存器中;
  • 第二十行:将 cl 赋值为 1
  • 第二十一行:调用 Func_ReadOneSector 模块,读取 IMKernel.bin 文件的一个扇区数据即 512 字节数据保存到 BaseTmpOfKernelAddr << 4 + OffsetTmpOfKernelFile0x7E00 地址处;
    [注]:new 行以上为之前代码部分,大家应该很熟悉,以下部分将是新添加部分。
  • 第二十二到二十六行:先后将 cxfsedidsesi 寄存器的值压入栈中保存;
    [注]:有效行不含带注释行或空格行。
  • 第二十七行:为 cx 赋值为 0x200 (512);(以备后续 loop 使用)
  • 第二十八行:为 ax 赋值为内存存储的基地址 BaseOfKernelFile;(BaseOfKernelFile = 0x00
  • 第二十九行:用存储着欲放置内核基地址 BaseOfKernelFileax 寄存器初始化 fs 段寄存器;
  • 第三十行:将获取临时变量 OffsetOfKernelFileCount 两个字长度的值赋值给 edi 寄存器;
  • 第三十一行:将中转存储的基地址 BaseTmpOfKernelAddr 的值赋值给 ax
  • 第三十二行:用 ax 赋值 ds 段寄存器;
  • 第三十三行:将转存偏移地址 OffsetTmpOfKernelFile 的值赋值给 esi

🚩Label_Mov_Kernel

  • 第三十四行:读取转存 ds:esi 地址处一个字节数据保存到 al
  • 第三十五行:将刚读取到的数据 al 保存到欲存储内核地址 1MB 以上的地址 fs:edi 处,完成一个字节搬运;
  • 第三十六、三十七行:esiedi 分别自加 1
  • 第三十八行:使用 loop 指令跳转到 Label_Mov_Kernel 处重复执行,直到 cx <= 0 为止,此时将完成一个扇区数据的搬运工作;
  • 第三十九行:将 eax 的值设为 0x1000
  • 第四十行:用 eax 初始化 ds 段寄存器,即 ds = 0x1000,为了下面能正常使用本地变量,因为之前修改过 ds 寄存器,因此需要将其改回原来的值。为什么是 0x1000,这里可以点击查看之前的 图片 看一下在修改之前 ds 的值也是 0x1000,便将其修改回去罢了。
  • 第四十一行:将 edi 的值保存到临时变量 OffsetOfKernelFileCount 中;
  • 第四十二到四十七行:分别弹出栈中 6 个元素,并先后分别保存进 esidsedifscxax 中,需特别注意 ax 保存的值,这个值是该模块最开始第一次压入栈中的值,即根目录项中的文件起始簇号,也就是 FAT 表项的索引号;
  • 第四十八行:调用 Func_GetFATEntry 读取对应索引号的 FAT 表项,并将其结果保存到 ax 中;
  • 第四十九行:比较 ax0x0FFF,查看当前簇是否是结束簇;
  • 第五十行:当 ax == 0x0FFF 则跳转到 Label_File_Loaded 执行,否则跳过执行下一行指令;
  • 第五十一行:将保存着文件在数据区的存储簇号 ax 的数据压栈保存,以待下一次 popax 中使用;
  • 第五十二行到五十四:根据 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 的位置。该方法与 BIOSINT 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 15hEAX=E820h 功能时的相关寄存器详情:(Label_Get_Mem_Struct 的代码就是根据下表配置的)

在这里插入图片描述

  • EAX:是设置 INT 15h 中断的主功能号;(这里使用 E820h
  • EBX:该寄存器保存中断服务返回的值,用于表示下一个映射物理内存范围,若返回值为 0,则表示所有物理内存映射完成。如果是第一次调用,则必须设置 EBX0
  • ES:DIBIOS 会将地址范围描述结构信息填充到以此为起始地址的内存空间;
  • 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 芯片的显示模式

在上述代码注释中的 0x1800x143 是显示模式号,其具体信息如下:

模式物理地址像素点位数
0x1801440900e000_0000h32bit
0x143800600e000_0000h32bit

通过设置不同的显示模式号,可以配置出不同的屏幕分辨率、每个像素点的数据位宽、颜色格式等属性。

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

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Imagine Miracle

爱你哟 =^ v ^=

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值