系统环境:
OS:CentOS Stream release 9 (cmd: cat /etc/redhat-release)
Linux Kernel:Linux 5.14.0-142.el9.x86_64 (cmd: uname -a)
1. 前言
经过上一篇文章,我们已经清楚的了解了 FAT12
文件系统的结构以及它的工作原理,那么在本文就基于之前我们写的最基本的 boot
程序 imboot.S
之上,将其修改为 FAT12
文件系统的引导扇区类型,并为其添加相应的功能,来实现加载存放在 FAT12
文件系统中的 Loader
程序的功能。由于本文需要用到上一篇文章《FAT12 文件系统》中的内容,笔者建议也打开上篇文章对照着看。话不多说,do it.
2. 修改为 FAT12 文件系统类型引导扇区
相信各位还能清楚的记得 FAT12
文件系统类型的引导扇区是什么(如果忘记了,快点击 “上一篇” 查看),首先 FAT12
的引导扇区占用了 1
个扇区,也就是 512 B
,它不仅包含了 FAT12
文件系统的结构信息,同时包含 boot
程序,并以最后两个字节为 0x55
和 0xAA
作为引导扇区结束的标识。
这次我们就另外新建一个文件吧,将其命名为 imboot-v0.2.S
,代表这是我们的第二个版本的 boot
程序。
[imaginemiracle@imos-ws imboot-v0.2]$ vim imboot-v0.2.S
我们来根据上一篇中的 MBR
表格来编写第一部分代码:
org 0x7c00
; 定义栈指针
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_JmpBoot
jmp short Label_Boot_Start
nop
BS_OEMName db 'IMboot ' ; 长度必须为 8,不足以空格填充
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,不足用空格补齐
2.1. org 0x7c00
org
是 Origin
的缩写,作用为指定程序的起始地址。与之前一样,我们需要将 boot
程序的地址指定到 0x7c00
的位置,BIOS
运行后会跳入 0x7c00
这个地址,如果不指定则默认程序的起始地址为 0x0000
,那么 BIOS
将不能正常加载到我们的 boot
程序。
2.2. Loader 程序位置的计算
equ
之前也解释过,事实上就是让一个字符串标识符来代表一个具体的值,这里的有两个与 Loader
相关的定义,BaseOfLoader equ 0x1000
和 OffsetOfLoader equ 0x00
,分别表示 Loader
程序的基地址和偏移地址(这里的地址均指的是物理地址)。通过基地址加上偏移地址的计算得到 Loader
的实际地址(这里的加,并不是求和,可以理解为拼接的意思)。计算方法如下:
// BaseOfLoader == 0x1000
// OffsetOfLoader == 0x00
BaseOfLoader << 4 + OffsetOfLoader = 0x10000
2.3. 根目录扇区总数的计算
根目录扇区总数 RootDirSectors
为 14
也是通过计算得到。
计算方法:(BPB_RootEntCnt * 32 + BPB_BytesPerSec - 1) / BPB_BytesPerSec
实际计算:(224 * 32 + 512 - 1) / 512 = 14
这里加上的 BPB_BytesPerSec - 1
也就是 512 - 1
的目的是为了使当所有的根目录项不能填满完整的扇区时该公式也能够成立。简单来讲就是当根目录项存在不能占满一个扇区时,为其分配一块完整的扇区,以避免根目录缺失或不存在。
2.4. 根目录起始扇区号和 FAT1 表起始扇区号的计算
根目录起始扇区号 SectorNumOfRootDirStart
的值为 19
。该值是通过当前区域之前的所有占用扇区数相加得到。
计算方法:MBR + FAT1 + FAT2
实际计算:1 + 9 + 9 = 19
FAT1
表起始扇区号 SectorNumOfFAT1Start
的值为 1
。在 FAT1
之前只有引导扇区 MBR
占用了 1
个扇区,那么 FAT1
的起始扇区号应该为 1
。
2.5. SectorBalance
可能你比较好奇这里为什么定义了一个 SectorBalance
值为 17
,它是做什么用的,好像 17
在之前也没有见过。不要慌,这就解释。
我们知道数据区的起始扇区号应该为 33
(根目录起始扇区号 + 根目录扇区总数 = 19 + 14
= 33
)。
ok,还记得在上篇文章中,我们在索引 imboot.txt
文件在数据区的存放位置时是否因为数据区的起始簇对于根目录项中记录的起始簇值为 2
,因此,我们特意将记录的 7
号簇,在计算的时候减了 2
。如果忘记的话,没关系,笔者将上篇文章的有关部分截图贴在这里了,帮你回忆。
那么看到这里,其实已经大概清楚了这个 17
是做什么的了。它主要用来在计算数据区的簇号(扇区号)时使用,这样避免在计算时再减去 2
,使得代码更见简洁易读。它有个优雅的名字叫做 “数据区平衡”。
3. 增加软盘读取功能
3.1. 代码实现
为了能够读取 FAT12
格式软盘,我们必须在代码中实现软盘的读取功能。具体代码如下:
; 读取磁盘扇区模块(函数),每次读取一个扇区
Func_ReadOneSector:
push bp
mov bp, sp
sub esp, 2
mov byte [bp - 2], cl
push bx
mov bl, [BPB_SecPerTrk]
div bl ; 默认被除数保存在 ax 中,ax/bl 商保存在 al,余数保存在 ah 中
inc ah ; inc 加一指令
mov cl, ah
mov dh, al
shr al, 1 ; shr 逻辑右移
mov ch, al
and dh, 1
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
add esp, 2
pop bp
ret
这里在转入执行时涉及栈的保存,并且在返回时又有栈的还原,这已经和 C
语言中的函数操作一样了,因此可以将这段代码块视为一个功能函数,其作用为读取软盘扇区,每次读取长度为一个扇区(512B
)。
3.2. INT 13h 中断
有关软盘操作,实际上是利用 BIOS
中的中断服务程序 INT 13h
来实现的,在之前的文章中也有介绍过,忘了没关系再瞅两眼就想起来了。而本文中所用到的是 INT 13h
中断的主功能号 AH = 02h
实现软盘扇区的读取操作。其对应寄存器的详细功能如下:
中断服务 | 主功能号 AH 值 | 功能描述 | AL 功能 | CH 功能 | CL 功能 | DH 功能 | DL 功能 | ES:BX |
---|---|---|---|---|---|---|---|---|
INT 13h | 02h | 读取软盘扇区 | 读入的扇区数 (必须非 0 ) | 磁道号 (柱面号) 的低 8 位 | 扇区号 1~63 (bit 0~5) ;磁道号 (柱面号) 的高 2 位 (bit 6~7,只对硬盘有效) | 磁头号 | 驱动器号(如果操作的是硬盘驱动器,bit 7 必须被置位) | 数据缓冲区 |
3.3. 传入的参数与 INT 13h 的配置参数转换
Func_ReadOneSector
函数需要用到三个寄存器作为参数传递使用:
AX
保存待读取的磁盘起始扇区号CL
保存读入的扇区数量ES:BX
保存目标缓冲区起始地址 (读取磁盘内容的保存位置)。
由于 Func_ReadOneSector
函数传入的磁盘扇区号是逻辑块寻址 (Logical Block Address——LBA
),而 INT 13h
,AH = 02h
中断服务只能识别柱面/磁头/扇区 (Cylinder/Head/Sector——CHS)
,因此在两者之间需要转换,转换方法如下:
3.4. 代码详解
OK,在了解了这些之后,笔者这小段函数的代码逐行解释,以帮助各位快速理解。
首先必须清楚一点,在跳转到 Func_ReadOneSector
执行前,一定会为 AX
、CL
、ES:BX
寄存器分别保存好需要传入的值。
- 第一行:将上一个函数的栈基址寄存器
bp
的值压栈进行保存; - 第二行:将栈顶寄存器
sp
的值保存进栈基址寄存器bp
,即使bp
指向栈顶;(此时bp == sp
) - 第三行:将栈顶寄存器
esp
的值减2
,即使esp
向下移动两个字节,目的为了在栈中申请两个字节的空间,用于保存之后的数值; - 第四行:将寄存器
cl
中的值保存到刚刚开辟的栈的空间中; - 第五行:将寄存器
bx
压栈保存;(此时esp
将会向下移动bx
大小的单位,即向下移动16 bit
,两个字节) - 第六行:将
BPB_SecPerTrk
的值保存到寄存器bl
中;(此时bl == 18
) - 第七行:用
ax
的值除以bl
的值,并将结果的商保存到al
中,余数保存到ah
中;(此处的结果需根据传入的ax
的值决定) - 第八行:将
ah
自加一;(由于磁道内的起始扇区号是从1
开始计数,因此需加一操作) - 第九行:将
ah
的值保存到cl
中,即将计算后的起始扇区号写入cl
寄存器,以待中断服务使用; - 第十行:将
al
的值保存到dh
中; - 第十一行:将
al
寄存器的值逻辑右移1
; - 第十二行:将
al
寄存器的值保存到ch
,即将计算后的磁道号(柱面号)写入ch
寄存器,以待中断服务使用; - 第十三行:将
dh
寄存器的值和0x1
做逻辑与操作,计算出磁头号,以待中断服务使用; - 第十四行:将数据弹出栈,并保存到
bx
中; - 第十五行:将
BS_DrvNum
的值保存到bl
中; - 第十六行:定义标号
Label_Go_On_Reading
; - 第十七行:将
2
写入ah
寄存器,表示准备使用INT 13h
的02h
号主功能; - 第十八行:将栈中
bp - 2
位置的值保存到al
中,即最开始传入参数的读入扇区数量保存到al
,以待中断服务使用; - 第十九行:开启中断
int 13h
; - 第二十行:条件跳转语句,当检测到
CF == 1
时跳转到Label_Go_On_Reading
处,即检测还没读取完时继续配置中断读取;(当数据读取成功时CF
标志位会被自动复位,即使CF = 0
) - 第二十一行:将
esp
向上移动两个字节,释放之前开辟的栈空间; - 第二十二行:将
bp
弹出栈; - 第二十三行:调用
ret
指令恢复主调函数现场,即栈指针恢复到进入前的状态。
总的来讲,Func_ReadOneSector
模块的功能就是将指定的扇区内容读取到 ES:BX
指定的位置处。
4. 目标文件搜索功能
4.1. 代码实现
文件的搜索功能则是基于上面实现的软盘读取功能函数实现的,具体代码如下:
;====== 查找 imloader.bin 文件
Label_Imboot_Begin:
mov word [SectorNo], SectorNumOfRootDirStart ; SectorNumOfRootDirStart 19
Label_Search_In_Root_Dir_Begin:
cmp word [RootDirSizeForLoop], 0 ; RootDirSizeForLoop == RootDirSectors == 14
jz Label_No_LoaderBin
dec word [RootDirSizeForLoop] ; RootDirSizeForLoop 自减
mov ax, 0x00
mov es, ax
mov bx, 0x8000
mov ax, [SectorNo]
mov cl, 1
call Func_ReadOneSector
mov si, LoaderFileName
mov di, 0x8000
cld
mov dx, 0x10
Label_Search_For_LoaderBin:
cmp dx, 0
jz Label_Goto_Next_Sector_In_Root_Dir
dec dx
mov cx, 11
Label_Cmp_FileName:
cmp cx, 0
jz Label_FileName_Found
dec cx
lodsb
cmp al, byte [es:di]
jz Label_Go_On
jmp Label_Different
Label_Go_On:
inc di
jmp Label_Cmp_FileName
Label_Different:
and di, 0x0ffe0
add di, 0x20
mov si, LoaderFileName
jmp Label_Search_For_LoaderBin
Label_Goto_Next_Sector_In_Root_Dir:
add word [SectorNo], 1
jmp Label_Search_In_Root_Dir_Begin
4.2. Inter 标志寄存器 EFLAGS Register
标志寄存器 EFLAGS Register
(ZF
为零标志位)
4.3. CMP 与 JZ 指令
CMP
指令:cmp 目的操作数, 源操作数
CMP结果 | ZF | CF |
---|---|---|
目的操作数 < 源操作数 | 0 | 1 |
目的操作数 > 源操作数 | 0 | 1 |
目的操作数 = 源操作数 | 1 | 0 |
JZ
指令:当 ZF
为 1
时发生跳转。
4.4. CLD 指令
cld
指令与 std
指令是一对相对的操作指令,这两个指令均是,来操作方向标志位 DF (Direction Flag)
。cld
将 DF
复位,即使 DF = 0
,std
则使 DF
置位,即使 DF = 1
。通过使用 cld
和 std
指令控制方向标志 DF
的值,从而决定内存地址是增加(DF = 0
时向高地址增加)还是减小(DF = 1
时向低地址减小)。
4.5. LODSB 指令
与之同一系列的指令为LODSB/LODSW/LODSD/LOD,与之相对系列的指令为STOSB/STOSW/STOSD/STOSQ。他们具体的功能如下:
指令 | 描述 | DF = 0 | DF = 1 |
---|---|---|---|
lodsb | 将 ds:si 寄存器指向地址处的数据取出并保存到 al 寄存器中 | si = si + 1 | si = si - 1 |
lodsw | 将 ds:si 寄存器指向地址处的数据取出并保存到 ax 寄存器中 | si = si + 1 | si = si - 1 |
lodsd | 将 ds:esi 寄存器指向地址处的数据取出并保存到 eax 寄存器中 | esi = esi + 1 | esi = esi - 1 |
lodsq | 将 ds:rsi 寄存器指向地址处的数据取出并保存到 rax 寄存器中 | rsi = rsi + 1 | rsi = rsi - 1 |
stosb | 将 al 寄存器中的值取出保存到 es:di 指向的地址处 | di = di + 1 | di = di - 1 |
stosw | 将 ax 寄存器中的值取出保存到 es:di 指向的地址处 | di = di + 1 | di = di - 1 |
stosd | 将 eax 寄存器中的值取出保存到 es:edi 指向的地址处 | edi = edi + 1 | edi = edi - 1 |
stosq | 将 rax 寄存器中的值取出保存到 es:rdi 指向的地址处 | rdi = rdi + 1 | rdi = rdi - 1 |
⭐特别小知识:
需要注意的是,其中目的字符串必须保存在附加段中,即必须是 es:di
;
而源字符串允许使用段跨越前缀来修饰,但偏移地址必须是 si
,因此上述 si
的前缀段可以是 es
也可以是 ds
。
4.6. 代码详解
好了,现在我们已经掌握了足够的知识可以完全看懂这段代码了。接下来将逐行分析这段代码:(仅指有效代码行,定义标号的地方就不再说明了)
- 第一行:代码开始首先获取根目录的起始扇区号,并将其保存到临时变量
SectorNo
中;
🚩Label_Search_In_Root_Dir_Begin:
- 第二行:用
cmp
指令比较临时变量RootDirSizeForLoop
与0
的结果;(当前代码RootDirSizeForLoop
的初始值为0
)
[注]:cmp 指令,实际相当于做减法操作,并根据结果不同将会修改标志寄存器 PSW 中的 ZF 和 CF。
- 第三行:当
RootDirSizeForLoop
的值等于0
时发生跳转到Label_No_LoaderBin
处执行;
[注]:由于当前还未实现 Loader 程序并无法加载并读取,因此这里先让其执行检测无 Loader 的情况。
- 第四行:
RootDirSizeForLoop
自减1
; - 第五行:将
0x00
保存到ax
寄存器中; - 第六行:将
ax
的值保存到es
段寄存器中,初始化段寄存器es
; - 第七行:将
0x8000
保存到bx
寄存器中,由es
和bx
组成的es:bx
==es << 4 + bx
==0x8000
将表示读取数据的保存地址,调用Func_ReadOneSector
函数前用寄存器传参,虽然这里并不是参数传递作用,而是为了给INT 13h
中断服务使用; - 第八行:将根目录的起始扇区号
19
写入ax
寄存器中,将作为参数给Func_ReadOneSector
函数使用; - 第九行:为
cl
寄存器写入1
,函数传参,表示要读取的扇区数量为1
; - 第十行:调用
Func_ReadOneSector
函数,转入其地址执行;
[注]:此时,Func_ReadOneSector 函数已经将读取的目录扇区内容写入以 0x8000 为起始地址的内存空间中了。
- 第十一行:将
LoaderFileName
的内容保存到si
寄存器中,LoaderFileName
是定义的一个字符串,值为“IMLOADERBIN”
;(si
保存之后需要比较的字符串) - 第十二行:将
0x8000
保存到di
寄存器中;(di
保存要读取字符的地址) - 第十三行:执行
cld
指令,将DF
置为0
;(之后将遵循地址增加的方向) - 第十四行:将
0x10
保存到dx
寄存器中;(dx
保存每个扇区能存储的根目录项的数量,用于计数功能,512 ÷ 32 = 16 = 0x10
)
🚩Label_Search_For_LoaderBin:
- 第十五行:用
cmp
比较dx
的值与0
的大小;(参考本文 4.3 小节) - 第十六行:检测
cmp
的结果改变的ZF
,当ZF = 1
时转入Label_Goto_Next_Sector_In_Root_Dir
处执行;(ZF = 1
时,表示dx == 0
,表示当前扇区已读取完,但仍未匹配到文件名) - 第十七行:
dx
自减1
;(计数功能,表示还有dx
个目录项未读) - 第十八行:将
11
保存到cx
寄存器中;(目录项中用于保存文件名的长度为11 bit
,用于计数功能,表示还有cx
个字符未匹配到)
🚩Label_Cmp_FileName:
- 第十九行:用
cmp
比较cx
的值与0
的大小;(参考本文 4.3 小节) - 第二十行:检测
cmp
的结果改变的ZF
,当ZF = 1
时转入Label_FileName_Found
处执行;(ZF = 1
时,表示cx == 0
,表示文件名的所有字符都已经匹配到) - 第二十一行:
cx
自减1
;(计数功能,表示还有cx
个字符未匹配到) - 第二十二行:执行
lodsb
指令,读取ds:di
指向地址处的数据,保存到al
中,并将si
加一(因为DF = 0
,若DF = 1
,则si
减一);(读取扇区中目录项的一个字节) - 第二十三行:用
cmp
指令比较al
与es:di
,字符匹配; - 第二十四行:检测
cmp
的结果改变的ZF
,当ZF = 1
时跳转到Label_Go_On
处执行; - 第二十五行:直接跳转至
Label_Different
处执行,由于未匹配成功,则准备匹配下一个目录项;
🚩Label_Go_On:
- 第二十六行:
di
加一;(准备读取下一个字节内存单元) - 第二十七行:直接跳转至
Label_Cmp_FileName
处执行,继续读取字符与文件名的下一个字符比较;
🚩Label_Different:
- 第二十八行:用当前字符地址
di
按位与上0xffe0
,并将结果保存到di
中,从而获取当前目录项的起始地址; - 第二十九行:为
di
加上0x20
(32
),使得di
指向下一个目录项的起始地址; - 第三十行:将
LoaderFileName
的内容保存到si
寄存器中,LoaderFileName
是定义的一个字符串,值为“IMLOADERBIN”
;(重置si
) - 第三十一行:直接跳转至
Label_Search_For_LoaderBin
处执行,准备读取下一个目录项;
🚩Label_Goto_Next_Sector_In_Root_Dir:
- 第三十二行:为临时变量
SectorNo
加1
,扇区索引号加1
,表示切换到下一个扇区; - 第三十三行:直接跳转至
Label_Search_In_Root_Dir_Begin
处执行。
5. 提示目标文件不存在功能
当程序并没有找到 Loader
文件时,我们应该让它提示我们点什么,不然我们并不知道这时候在发生些什么,因此需要实现一个能够在没有找到目标文件时提示我们的功能。
5.1. 代码实现
具体代码如下:
;====== display on screen : ERROR: Loader not found
Label_No_LoaderBin:
mov ax, 0x1301
mov bx, 0x008c
mov dx, 0x100
mov cx, 24
push ax
mov ax, ds
mov es, ax
pop ax
mov bp, NoLoaderMessage
int 0x10
jmp $
5.2. INT 10h,AH = 13h 中断服务
这段代码使用了 BIOS
中的 INT 10h
,主功能号 AH = 13h
的中断服务,其配置详情在之前的文章中也有提到过,不过相信各位也不想翻过去看吧,这里再次贴出来(你说你记住了?我不信!🙃)
中断服务 | 主功能号 AH 值 | 功能描述 | AL 功能 | BH 功能 | BL 功能 | CH 功能 | CL 功能 | DH 功能 | DL 功能 | ES:BP |
---|---|---|---|---|---|---|---|---|---|---|
INT 10h | 13h | 显示一行字符串 | 写入模式:00h : 字符串属性由 BL 控制,长度由 CX 控制(单位: Byte),不改变光标位置;01h : 同上,但显示结束更新光标到字符串结尾处;02h : 字符串由其最后的字节控制,CX 控制单位变为 Word ,不改变光标位置;03h : 同 02h ,但更新光标位置到字符串结尾 | 页码 | 字符属性和颜色属性: * bit 0~2:字体颜色(0:黑1:蓝2:绿3:青4:红5:紫6:棕7:白) * bit 3:字体亮度(0:默认1:高亮) * bit 4~6:背景颜色(颜色的数值同上) * bit 7:字体闪烁(0:关闭1:使能) | 字符串长度 | 字符串长度 | 光标所在行号 | 光标所在列号 | 要显示字符串的内存地址 |
5.3. 代码详解
这段代码只使用了中断功能,而且只打印 NoLoaderMessage
代表的字符串而已,所以很简单。
🚩Label_No_LoaderBin:
- 第一行:为
ax
赋值为0x1301
;(即ah = 13h
,al = 01h
,al
功能参考上表) - 第二行:为
bx
赋值为0x008c
;(即bh = 00h
,bl = 8ch
,bl
控制的属性参考上表,算了直接解释吧,bl = 1000_1100b
,即,bit 7 = 1,bit 3 = 1,bit 0~2 = 100b = 4
,故字符串属性为,字符高亮且闪烁,颜色为红色) - 第三行:为
dx
赋值为0x0100
;(即dh = 01h
,dl = 00h
,dx
控制光标的行列,这里表示第1
行,第0
列)
[注]:行和列的起始号均为 0。
- 第四行:为
cx
赋值为24
;(表示字符串长度) - 第五行:将
ax
寄存器压栈保存; - 第六行:将
ds
的值保存到ax
中; - 第七行:将
ax
的值保存到es
中;(用ds
初始化es
,用于数据的访存) - 第八行:
pop
出栈,出栈的数据保存到ax
中; - 第九行:将
NoLoaderMessage
字符串地址保存到bp
寄存器中;(中断会显示es:bp
保存的内容) - 第十行:开启中断
INT 10h
; - 第十一行:跳转到当前指令地址执行——死循环。
6. FAT 表项解析功能
当匹配到 Loader
程序后,从根目录项中只能获取文件存储的起始簇号,而后续内容需要根据 FAT
表中对应的表项查询,并一次将文件在数据区的存储扇区加载到内存中,这里便涉及到 FAT
表项的解析工作,那么需要我们添加一个可以用来解析 FAT
表项的功能模块。
6.1. 代码实现
具体代码如下:
Func_GetFATEntry:
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
Func_GetFATEntry
模块的功能是根据当前 FAT
表项的索引号找出下一个 FAT
表项,调用该模块时需要使用 ax
寄存器作为参数输入提供给该模块,ax
保存当前的 FAT
表项号。
6.2. MUL 和 DIV 指令
mul
是乘法指令,必须要求做乘法的两个数位宽必须相同,即要么都是 8 bit
,要么都是 16 bit
。
其中一个默认的操作数被保存在 ax
寄存器中,若选择做 8 bit
的乘法则保存到 al
中即可。
使用方式:
mov ax, 6
mov bx, 2
mul bx
如上示例则表示 ax * bx == 6 * 2
。操作的结果默认选择高 16
位保存到 dx
中,低 16
位保存在 ax
中。
div
是除法指令。必须要求做除法的两个数位宽必须相同,即要么都是 8 bit
,要么都是 16 bit
。这一点同 mul
一样。
其中被除数默认保存在 ax
中(若超过 16 bit
,则高 16 bit
保存在 dx
,低 16 bit
放在 ax
中。
当除数为 8 bit
时,计算结果的商将保存到 al
中,余数保存到 ah
中。
当除数为 16 bit
时,计算结果的商保存在 ax
中,余数保存到 dx
中。
使用格式:
mov ax, 7
mov bx, 2
div bx
上述示例表示 ax ÷ bx = 7 ÷ 2 = 3.5
,ax == 3
, dx == 1
。(ax
是表示 16 bit
的数)
6.3. XOR 指令
xor
是异或指令。使用格式如下:
xor dest, src
其功能是将 dest
与 src
按位做逻辑异或操作,并将结果保存到 dest
中。异或的基本运算如下:(异或符号:⊕)
x | y | x⊕y |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
或者也可以更简单的理解为,相同异或则为 0
,不同异或则为 1
6.4. FAT12 的 FAT 表项的读取规律
通过笔者的上一篇文章《FAT12 文件系统》我们已经很清楚的了解到 FAT12
文件系统的结构了,其中 FAT
表的每个表项由一个完整的字节和另一个字节的一半组成。我们再来看一下上篇文章中的这个图,复习一下如何查看 FAT12
文件系统中的表项吧。
相信大家看到就会想起表项如何组合了吧,如果还是没想起来,那再翻回去再看一下,本文就不再赘述。
这张图是一个 FAT12
文件结构打开后的样子,直接跳转至 FAT1
表的位置。这里笔者将其中前 5
个表项画了出来,由于目录项存储的 FAT
表项的有效起始号为 2
,因此我们可以不用关心 0
和 1
号表项。可以很清楚的看到每一个表项是由两个字节组成的(需要经过变换)。
那么我们可以使用一个 16 bit
的寄存器一次读入两个字节的数据,再进行处理,现在用 2
号表项和 3
号表项作为示例。
先不考虑如何找到表项的起始地址,这里我们直接将组成 2
号表项的 03h 40h
读入 ax
寄存器中,由于 Inter
采用的是大端存储,因此读入到 ax
寄存器后,ax
保存的值将会是 4003h
。我们只需要将 ax
按位逻辑与上 0FFFh
,就能得到我们需要的 FAT[2]
。
再来看 3
号表项,将组成 3
号表项的 40h 00h
读入 ax
寄存器中,因为大端存储的缘故,ax
中保存的值将会是 0040h
,我们只需要将 ax
右移 4
位,则得到了 FAT[3]
。
其实你发现了,原来这还是有规律的,偶数表项依照 FAT[2]
的处理方法,而奇数项依照 FAT[3]
的处理方法,我们将轻而易举的获取到对应表项的值。
6.5. 代码详解
这段代码没有涉及到特别多的知识,因此这里就直接解释代码含义。
首先明确一点,该模块需要 ax
做参数传入,ax
保存当前 FAT
表项的索引号。
🚩Func_GetFATEntry:
- 第一、二、三行:分别将
es
、bx
、ax
保存的值压入栈中存储; - 第四行:将
ax
保存的值清零; - 第五行:用
ax
寄存器初始化es
段寄存器; - 第六行:
pop
将一个数据弹出栈,并将其保存到ax
中;(栈中数据遵循先进后出,这时ax
保存的值正是压栈之前ax
的值) - 第七行:将
Odd
变量赋值为0
;(Odd
为定义的一个变量,初始值为0
) - 第八行:为
bx
寄存器赋值为3
; - 第九行:将
ax
的值乘上bx
,并将结果分别保存到dx
(高16 bit
) 和ax
(低16 bit
) 中。 - 第十行:为
bx
寄存器赋值为2
; - 第十一行:将
ax
的值除上bx
的值,ax
保存除法的商
,dx
保存除法的余数;(此时ax
保存的是当前表项在FAT
表中的字节偏移) - 第十二行:用
cmp
比较dx
与0
的大小;(当dx == 0
时将会将ZF
置位,即ZF = 1
) - 第十三行:为临时变量
Odd
变量赋值为1
。
我们都清楚了在开始时 ax
保存的是当前 FAT
表项的索引号,经过上述步骤将 ax * 3 / 2
,在这里解释一下为什么需要先乘上 3
再除上 2
。
首先我们知道的是每个表项长度为 12 bit
,也就是 1.5
个字节,若要计算当前表项的地址偏移那么需要知道从 FAT
表开始到当前表项的长度有多少,也就是说用当前表项索引 x 每个表项长度 = 当前表项长度偏移 ,而每个表项长度为 1.5
,那么给表项先乘上 3
再除以 2
,事实上相当于给当前表项乘以 1.5
,目的为了计算当前表项的长度偏移。
其中 Odd
是定义的一个变量,用于标志当前表项是索引的奇偶性,初始值为 0
- 偶数表项:
Odd
为0
; - 奇数表项:
Odd
为1
。
🚩Label_Even:
- 第十四行:
dx
与dx
自己做异或操作,等价与清零操作;(dx = 0
) - 第十五行:将
BPB_BytesPerSec
的值赋值给bx
,BPB_BytesPerSec
表示每个扇区的大小512 B
; - 第十六行:用
ax
除以bx
,即用当前表项的字节偏移 ÷ 每个扇区大小
=当前表项的扇区偏移
,ax
此时将会保存当前表项的扇区偏移
,dx
保存计算的余数,即保存当前表项所在扇区的字节偏移
; - 第十七行:将
dx
压栈保存; - 第十八行:将
0x8000
赋值给bx
,以待Func_ReadOneSector
函数中的INT 10h,AH = 02h
使用,表示读取的扇区保存到的地址; - 第十九行:为
ax
加上SectorNumOfFAT1Start
,即用当前表项的扇区号偏移 + FAT 表起始扇区号 = 当前表项的起始扇区号
,将结果赋值给ax
,以待Func_ReadOneSector
函数使用,表示要读入的扇区起始号; - 第二十行:为
cl
赋值为2
,以待Func_ReadOneSector
函数使用,表示要读入的扇区数量; - 第二十一行:调用
Func_ReadOneSector
。 - 第二十二行:弹出栈中数据保存到
dx
中,dx
此时保存当前表项在所在扇区的字节偏移
; - 第二十三行:为
bx
加上dx
,bx
此时保存的当前表项所在扇区起始字节一直到下一个扇区的内容,加上dx
表项在当前扇区的字节偏移
,即得到了当前表项的具体地址,将其保存在bx
中; - 第二十四行:读取
es:bx
到ax
中,读取长度为16 bit
,正好读取到组成当前表项的两个字节内容; - 第二十五行:比较
Odd
与1
是否相同; - 第二十六行:若不相同则跳转到
Label_Even_2
处执行,若相同则继续; - 第二十七行:此行只会在
Odd = 1
时即表示表项为奇数项时执行,将ax
右移4
位; - 第二十八行:
ax
按位逻辑与上0x0FFF
,则得到了12 bit
的表项; - 第二十九、三十行:将之前保存的
bx
、es
值弹出栈再保存回bx
、es
中。 - 第三十一行:执行
ret
恢复栈指针回到主调指令处执行。
这里需要读入 两个扇区
,这是为了当一个表项横跨两个扇区时,依然能够读取完整,即当这个表项由一个扇区的最后一个字节和下一个扇区的第一个字节组成。
7. 加载 imLoader.bin 文件到内存模块
我们已经实现了 “读取软盘扇区功能” 和 “解析 FAT 表项功能”,接下来则可以利用这两个模块实现将 imLoader.bin
文件的数据从软盘扇区读取并保存到指定地址处,完成文件加载到内存的过程。
7.1. 代码实现
来看具体代码:
Label_FileName_Found:
mov ax, RootDirSectors
and di, 0x0ffe0
add di, 0x01a
mov cx, word [es:di]
push cx
add cx, SectorBalance
add cx, ax
mov ax, BaseOfLoader
mov es, ax
mov bx, OffsetOfLoader
mov ax, cx
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
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:
jmp $
总的来讲,这个模块会根据根目录项中和 FAT
表项从而索引到数据区保存到 imLoader.bin
文件扇区,并将扇区按顺序以扇区为单位的读入到指定地址处。
7.2. INT 10h,AH = 0Eh 中断服务
在这段代码中使用了 INT 10h,AH = 0Eh
中断服务,该中断的功能是在屏幕上显示一个字符。具体参数说明如下:
中断服务 | 主功能号 AH 值 | 功能描述 | AL 功能 | BL 功能 |
---|---|---|---|---|
INT 10h | 0Eh | 显示一个字符 | 待显示的字符 | 前景色 |
7.3. 代码详解
需要注意的一点是,此模块是由 “目标文件搜索模块” 调用,只有当在目录项中匹配到 imLoader.bin
这个文件名时才会跳转到此处执行。
; 目标文件搜索模块部分代码
Label_Cmp_FileName:
cmp cx, 0
jz Label_FileName_Found
dec cx
lodsb
cmp al, byte [es:di]
jz Label_Go_On
jmp Label_Different
Label_Go_On:
inc di
jmp Label_Cmp_FileName
可以看到此处 di
寄存器一直存储着目录项中文件名的字符地址,倘若匹配成功,跳转入 Label_FileName_Found
也就是当前正在解释的模块,会将文件加载到内存。注意!此时的 di
寄存器将保存的是目录项中文件名的最后一个字节的地址,由于目录项文件名长度固定为 11 bit
,则此时的 di
相当于目录项起始地址向后偏移 11 bit
的地址。
由于此模块中代码涉及栈操作较多,为了避免读者们搞混,便于大家理解,这里笔者将会在栈环境改变时以图示方式表述当时栈环境。
🚩Label_FileName_Found:
- 第一行:将
RootDirSectors
的值赋值给ax
,RootDirSectors
表示根目录部分的扇区总数,值为14
; - 第二行:将
di
按位逻辑与上0xFFE0
,di
原本保存的目录项中文件名最后一个字节的地址,此步骤计算后di
将会得到目录项的起始地址; - 第三行:为
di
加上0x01A
,即加上26
,将得到目录项保存的文件起始簇的起始地址;
[注]:起始第二行和第三行完全可以用一步替代,直接 mov di, 0x00F0 替代上面两行完全是等价的。不过上面的更直观一些。
- 第四行:读取
es:di
地址处的数据,保存到cx
,读取长度16 bit
,此处数据为文件起始簇号; - 第五行:将
cx
的值压栈保存,以待之后使用;此时栈环境变化如下:
cx
此时保存的是文件的起始簇号。
- 第六行:为
cx
加上SectorBalance
,为了得到正确的扇区偏移,这里并不用真正的根目录起始扇区号SectorNumOfRootDirStart = 19
做为操作数,而是使用选用SectorBalance = 17
,这个原因在上文已经解释过了,是因为根目录项的文件起始簇号2
,对应的是数据区的第一个扇区; - 第七行:再为
cx
加上ax
保存的根目录扇区总数,则得到了文件在整个FAT12
文件结构中的扇区号; - 第八行:将
BaseOfLoader
需要加载文件的基地址赋值给ax
; - 第九行:将
ax
的值赋值给段寄存器es
,由于段寄存器不能直接用内存赋值只能用寄存器赋值; - 第十行:将
OffsetOfLoader
加载文件的偏移地址赋值给bx
,稍后会提供给Func_ReadOneSector
使用; - 第十一行:将
cx
保存的文件起始扇区号赋值给ax
,稍后会提供给Func_ReadOneSector
使用;
🚩Label_Go_On_Loading_File:
- 第十二、十三行:将
ax
、bx
压栈保存;此时栈环境的变化如下:
- 第十四行:将
0x0E
赋值给ah
寄存器,准备使用中断的0Eh
主功能号; - 第十五行:将待显示的字符
'.'
保存到al
寄存器中; - 第十六行:将
0x0F
赋值给bl
; - 第十七行:开启中断
int 10h
,将调用中断服务打印字符到屏幕上; - 第十八、十九行;分别将栈中元素弹出栈,并分别保存到
bx
、ax
中;此时栈环境的变化如下:
- 第二十行:将
1
赋值给cl
寄存器,准备调用Func_ReadOneSector
,表示读入1
个扇区; - 第二十一行:调用
Func_ReadOneSector
,读取一个扇区内容到BaseOfLoader : OffsetOfLoader
地址处; - 第二十二行:将栈中元素弹出栈,并保存到
ax
中,此时栈环境变化如下:
- 第二十三行:调用
Func_GetFATEntry
,读取ax
保存的文件簇号对应的FAT
表项值,结果保存到ax
中; - 第二十四行:比较读取的
FAT
表项值ax
是否为0x0FFF
,即检查文件是否结束; - 第二十五行:如果文件结束,则跳转到
Label_File_Loaded
处执行; - 第二十六行:将
ax
压栈保存,此时栈环境变化如下:
- 第二十七行:将根目录扇区总数
RootDirSectors = 14
赋值给dx
; - 第二十八行:为
ax
加上用来计算使用的 “根目录起始扇区号”SectorBalance
; - 第二十九行:为
ax
加上保存根目录扇区总数的dx
,则得到了指向文件下一个存储扇区位置的ax
; - 第三十行:为
bx
加上每扇区大小BPB_BytesPerSec
,让待加载地址向后移动一个扇区,准备加载文件的下一个扇区数据; - 第三十一行:跳回到
Label_Go_On_Loading_File
处执行,再加载文件的一个扇区到指定地址;
🚩Label_File_Loaded:
第三十二行:跳转到当前指令执行——死循环。
这行指令原本应该表示 imLoader.bin
已经加载完成,准备跳入加载地址执行,但暂时我们先不这样做,我们先让它死循环,因为我们的 imLoader.bin
还没有实现,因此先等等。
8. imboot 程序的其它变量的定义以及写入引导扇区结尾标志
在上文中使用了诸多并没有见过的临时变量和字符串如:SectorNo
、Odd
、NoLoaderMessage
等等,都是在文件最后定义的,具体代码如下:
;====== tmp variable
RootDirSizeForLoop dw RootDirSectors
SectorNo dw 0
Odd db 0
;====== display message
IMBootMessage: dd "The imboot is working!"
NoLoaderMessage: db "ERROR: Loader not found."
LoaderFileName: db "IMLOADERBIN", 0 ; 这里的 0 表示该字符串以 0 结尾, '\0' == 0。
times 510 - ($ - $$) db 0
dw 0xaa55 ; BIOS会检测软盘的第 0 磁头第 1 扇区最后两个字节是否为 0x55aa(代码中写成 0xaa55 是由于x86 使用小端存储) ,以确定这个扇区是否是引导扇区
千万不能忘记在整个引导扇区结尾处需要以 0x55AA
作为结束标志,否则 BIOS
将不能识别出该扇区是引导扇区。
9. 目前为止的完整程序
为了大家实验方便,笔者贴出到目前为止的完整代码如下:
org 0x7c00
; 定义栈指针
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_JmpBoot
jmp short Label_Boot_Start
nop
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,不足用空格补齐
Label_Boot_Start:
mov ax, cs ; 将代码寄存器 cs 的段基地址设置到 ds、es、ss寄存器
mov ds, ax
mov es, ax
mov ss, ax
mov sp, BaseOfStack
; 清屏
mov ax, 0600h
mov bx, 0700h
mov cx, 0h
mov dx, 0184fh
int 10h
; 设置光标位置
mov ax, 0200h
mov bx, 0000h
mov dx, 0000h
int 10h
; 在屏幕上打印字符串 "The imboot is working!"
mov ax, 1301h
mov bx, 000fh
mov dx, 0000h
mov cx, 22
push ax
mov ax, ds
mov es, ax
pop ax
mov bp, IMBootMessage
int 10h
; 软盘驱动器复位
xor ah, ah
xor dl, dl
int 13h
;jmp $
jmp Label_Imboot_Begin
; 填充 0 到结尾前 2 个字节
; 读取磁盘扇区模块(函数),每次读取一个扇区
Func_ReadOneSector:
push bp
mov bp, sp
sub esp, 2
mov byte [bp - 2], cl
push bx
mov bl, [BPB_SecPerTrk]
div bl ; 默认被除数保存在 ax 中,ax/bl 商保存在 al,余数保存在 ah 中
inc ah ; inc 加一指令
mov cl, ah
mov dh, al
shr al, 1 ; shr 逻辑右移
mov ch, al
and dh, 1
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 ; 检测 CF = 1 时发生跳转,当中断服务程序读取完成后会将 CF 置为 0
add esp, 2
pop bp
ret
;====== 查找 imloader.bin 文件
Label_Imboot_Begin:
mov word [SectorNo], SectorNumOfRootDirStart ; SectorNumOfRootDirStart == 19
Label_Search_In_Root_Dir_Begin:
cmp word [RootDirSizeForLoop], 0 ; RootDirSizeForLoop == RootDirSectors == 14
jz Label_No_LoaderBin
dec word [RootDirSizeForLoop] ; RootDirSizeForLoop 自减
mov ax, 0x00
mov es, ax
mov bx, 0x8000
mov ax, [SectorNo]
mov cl, 1
call Func_ReadOneSector
mov si, LoaderFileName
mov di, 0x8000
cld
mov dx, 0x10
Label_Search_For_LoaderBin:
cmp dx, 0
jz Label_Goto_Next_Sector_In_Root_Dir
dec dx
mov cx, 11
Label_Cmp_FileName:
cmp cx, 0
jz Label_FileName_Found
dec cx
lodsb
cmp al, byte [es:di]
jz Label_Go_On
jmp Label_Different
Label_Go_On:
inc di
jmp Label_Cmp_FileName
Label_Different:
and di, 0x0ffe0
add di, 0x20
mov si, LoaderFileName
jmp Label_Search_For_LoaderBin
Label_Goto_Next_Sector_In_Root_Dir:
add word [SectorNo], 1
jmp Label_Search_In_Root_Dir_Begin
;====== display on screen : ERROR: Loader not found
Label_No_LoaderBin:
mov ax, 0x1301
mov bx, 0x008c
mov dx, 0x100
mov cx, 24
push ax
mov ax, ds
mov es, ax
pop ax
mov bp, NoLoaderMessage
int 0x10
jmp $
;====== get FAT Entry
Func_GetFATEntry:
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
;====== found imloader.bin name in root director struct
Label_FileName_Found:
mov ax, RootDirSectors
and di, 0x0ffe0
add di, 0x01a
mov cx, word [es:di]
push cx
add cx, SectorBalance
add cx, ax
mov ax, BaseOfLoader
mov es, ax
mov bx, OffsetOfLoader
mov ax, cx
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
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:
;jmp $
jmp BaseOfLoader:OffsetOfLoader
;====== tmp variable
RootDirSizeForLoop dw RootDirSectors
SectorNo dw 0
Odd db 0
;====== display message
IMBootMessage: dd "The imboot is working!"
NoLoaderMessage: db "ERROR: Loader not found."
LoaderFileName: db "IMLOADERBIN", 0 ; 这里的 0 表示该字符串以 0 结尾, '\0' == 0。
times 510 - ($ - $$) db 0
dw 0xaa55 ; BIOS会检测软盘的第 0 磁头第 1 扇区最后两个字节是否为 0x55aa(代码中写成 0xaa55 是由于x86 使用小端存储) ,以确定这个扇区是否是引导扇区
10. 测试无 imLoader 程序的效果
到目前为止,我们所完成的代码是可以运行的,其结果就是没有检测到 imLoader.bin
文件会输出我们定义好的错误信息。
首先将编写好的代码编译:
[imaginemiracle@imos-ws imboot-v0.2]$ nasm imboot-v0.2.S -o imboot-v0.2.bin
使用 bximage
创建一个新的软盘,这里笔者命名为 imboot-v0.2.img
,创建软盘步骤参考上一篇文章的第三小节《制作软盘镜像文件》。
将编译生成的 imboot-v0.2.bin
写入新创建的软盘镜像 imboot-v0.2.img
:
[imaginemiracle@imos-ws imboot]$ dd if=./imboot-v0.2/imboot-v0.2.bin of=../img/imboot-v0.2.img bs=512 count=1 conv=notrunc
[注]:这里的文件路径根据自己文件的路径填写。
修改 boch
的配置文件 bochsrc
,将其中 floppya
项中的软盘路径修改为新创建的软盘的绝对路径:
运行 boch
,选择默认的 6
[imaginemiracle@imos-ws bochs-run]$ bochs -f bochsrc
等待黑色窗口出现后,在命令行输入 c
并回车,让其加载并运行软盘镜像。
这个时候便会看到我们的 boot
程序正常在运行,不过并没有找到与 imLoader.bin
文件名匹配的目录项,并打印如下的红色闪烁提示信息,警告我们。
11. 编写 imLoader 程序
猜测大家应该和笔者一样累了吧,你们看了这么多字,我敲了这么多字,坚持啊!🌠接下来的任务就很简单了。
11.1. imLoader 完整代码
这里的 Loader
代码很简单,几乎和上一篇文章中的代码相同,同样使用 INT 10h,AH = 13h
中断服务,将一个字符串输出到屏幕上,然后根据 ax
、bx
、cx
、dx
的控制输出位置和字符属性。
org 10000h
mov ax, cs
mov ds, ax
mov es, ax
mov ax, 0x00
mov ss, ax
mov sp, 0x7c00
;====== 利用中断服务在屏幕上输出 "imLoader is running... (^v^)" 的字样
mov ax, 1301h
mov bx, 000fh
mov dx, 0200h ; 设置光标在第 2 行
mov cx, 28
push ax
mov ax, ds
mov es, ax
pop ax
mov bp, ImLoaderRunning
int 10h
jmp $
;====== imLoaderRunning
ImLoaderRunning: db "imLoader is running... (^v^)"
这段代码就不再做过多的解释了,相信经过这么多学习,以大家聪明的头脑一定没问题的(如果还是有问题的话,欢迎在文章下留言提问,也更加欢迎其他读者来做解答)。
11.2. 将 imLoader 程序保存到 imboot-v0.2.img 软盘镜像中
首先将编写好的代码进行编译:
[imaginemiracle@imos-ws imLoader]$ nasm imLoader.S -o imLoader.bin
由于我们之前使用 dd
命令将 FAT12
文件系统结构的 imboot.bin
写入到 imboot-v0.2.img
软盘镜像中,此过程已经将软盘镜像格式化为 FAT12
类型,因此这里就可以直接使用 mount
命令将其挂载到指定目录访问了。
首先创建一个目录用于挂载使用
[imaginemiracle@imos-ws img]$ mkdir imboot_fat
将 imboot-v0.2.img
软盘镜像挂载到新建目录:
[imaginemiracle@imos-ws img]$ sudo mount imboot-v0.2.img imboot_fat/ -t vfat -o loop
[imaginemiracle@imos-ws img]$ cd imboot_fat/
参数解释:-t vfat
指定挂载的文件系统类型,-o loop
将文件描述为磁盘分区。
将编译好的 imLoader.bin
文件拷贝到挂载目录:
[imaginemiracle@imos-ws imboot_fat]$ sudo cp ../../imLoader/imLoader.bin ./
执行 sync
命令强制同步磁盘,并取消挂载:
[imaginemiracle@imos-ws imboot_fat]$ sync
[imaginemiracle@imos-ws imboot_fat]$ cd ..
[imaginemiracle@imos-ws img]$ sudo umount imboot_fat/
11.3. 挂载时可能会遇到的错误
若挂载时遇到这样的错误,则表示系统不能识别当前的文件系统类型。
mount: ****: wrong fs type, bad option, bad superblock on /dev/loop0, missing codepage or helper program, or other error.
这个时候应该是你修改过代码开头部分的某些指定字符串内容,若是这样,一定要按要求设定,每个名字必须符合 FAT12
每一项指定的长度,若不够必须使用空格填充,若超过请删除至合规长度,否则系统将无法识别到该文件系统类型。
12. 修改 imboot 程序并写入 imboot-v0.2.img 软盘镜像
如上已经将 imLoader
程序完成了,不过还不要着急去运行,还有一件事情没做,还记得在 boot
代码中加载完 imLoader
后要跳转到 imLoader
的加载地址去执行,而当时还没有 Loader
程序暂时在那块写的是 jmp $
死循环,这时候我们需要修改过来了。
12.1. 需要修改的代码部分
将 Label_File_Loaded
中的跳转指令跳转到 imLoader
的加载地址即可。
Label_File_Loaded:
;jmp $
jmp BaseOfLoader:OffsetOfLoader
这样写的寻址方式为 BaseOfLoader << 4 + OffsetOfLoader
,即 0x1000 << 4 + 0x00 == 0x10000
。
12.2 编译修改后的文件
使用 nasm
编译 imboot-v0.2.S
文件。
[imaginemiracle@imos-ws imboot-v0.2]$ nasm imboot-v0.2.S -o imboot-v0.2.bin
12.3. 写入镜像
将编译生成的二进制文件写入镜像。
[imaginemiracle@imos-ws imboot-v0.2]$ dd if=imboot-v0.2.bin of=../../img/imboot-v0.2.img bs=512 count=1 conv=notrunc
13. 运行 bochs
因为之前已经修改过配置文件了,这里直接运行 bochs
,运行方法就不再讲了,直接看结果吧!(看到微笑就表示成功了哦!)
怎么说呢,经过这么多努力深入了解了 bootloader
的工作流程,并成功按照标准的流程完成自己的 bootloader
程序,此刻的心情应该是向上图中的表情一样吧,面带微笑。不过一切才刚刚开始~
#本文完
觉得这篇文章对你有帮助的话,就留下一个赞吧^v^*
请尊重作者,转载还请注明出处!感谢配合~
[作者]: Imagine Miracle
[版权]: 本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
[本文链接]: https://blog.csdn.net/qq_36393978/article/details/126369492