由于对intel体系结构的不熟悉……导致我在接下来的一章里前进得相当困难。遂采取最笨的方法,把代码3-2的新内容的每一条汇编逐一加上注释,尽可能去理解新的内容。收获当然是有的,但是我很怀疑是否有能力应付后面的内容。
boot启动后,要去搜索根目录中的loader.bin程序,本质上是搜索根目录部分中的loader.bin目录项,搜索到后读取目录项中记录的簇号,但是这个簇号只是起始簇号,因为loader可能会占用1个以上的簇,所以还要根据这个簇号到FAT表中去查找下一个簇号。然后根据每一次读到的簇号,最终将整个loader加载到内存中。
书中对读取软盘的功能做了封装,因为BIOS中断提供的读取功能只接受CHS (Cylinder/Head/Sector,柱面/磁头/扇区),而我们所用的是LBA(逻辑区块地址)是扇区的形式,所以将这个转换过程包装在Func_ReadeOneSector
里。
以下内容摘自书中对BIOS磁盘读取功能的描述
INT 13h,AH=02h 功能:读取磁盘扇区。
AL=读入的扇区数(必须非0); CH=磁道号(柱面号)的低8位; CL=扇区号1~63(bit 0~5),磁道号(柱面号)的高2位(bit 6~7, 只对硬盘有效); DH=磁头号; DL=驱动器号(如果操作的是硬盘驱动器,bit 7必须被置位); ES:BX=>数据缓冲区。
LBA到CHS的转换公式:
\rm LBA扇区号 \div 每磁道扇区数 = \left\{
\begin{aligned}
商Q \rightarrow \left\{
\begin{aligned}
柱面号 & = Q >> 1 \\
磁头号 & = Q \ \& \ 1
\end{aligned}
\right.\\
余数R \rightarrow 起始扇区号 = \rm R + 1
\end{aligned}
\right.
;======= read one sector from floppy
; 软盘读取功能
Func_ReadOneSector:
push bp ; 栈帧寄存器压栈
mov bp, sp ;
sub esp, 2 ; esp - 2 在在栈中开辟两个字节的新空间
mov byte [bp - 2], cl ; 参数cl=读入扇区的数量 存栈
push bx ; BX 存栈
; 用LBA扇区号计算CHS (Cylinder/Head/Sector,柱面/磁头/扇区)
mov bl, [BPB_SecPerTrk] ; 每磁道扇区数装入bl
; div 指令根据除数位数选择寄存器,8位为AX, 16位DX:AX,32位EDX:EAX,
div bl ; AX/BL = AL ... AH , LBA扇区号/每磁道扇区数 = 磁道号……磁道内扇区号
inc ah ; 磁道内起始扇区号从1开始,LBA从0开始,所以要+1
mov cl, ah ; AH移动到CL
mov dh, al ; 商AL移动到DH
shr al, 1 ; 柱面号 = 磁道号>>1
mov ch, al ; 求得的柱面号移动至CH
and dh, 1 ; DH(商)与1与,得到磁头号
pop bx ; 弹出BX
; 调用BIOS中断服务程序
mov dl, [BS_DrvNum] ; DL参数驱动器号
Label_Go_On_Reading:
mov ah, 2 ; 设置中断服务程序,ah=02h,功能是读取磁盘扇区
mov al, byte [bp - 2] ; 读入的扇区数,放入al, [bp-2]是之前输入的参数,指读入扇区数
int 13h ; BIOS中断服务程序
; 中断服务程序从软盘扇区读取数据到内存中,数据读取成功时CF复位,CF=0
; 此时轮询CF位,直到读取完成,完成时CF=0
jc Label_Go_On_Reading ; CF = 1 回到这一块代码开始
add esp, 2 ; 收回2个字节的栈空间
pop bp ; bp出栈
ret ; 返回
; RET
; 弹出栈中的保存的返回地址(之前被CALL存进去的),回到了call Func_ReadOneSector</code></pre>
封装之后,只需传入扇区号即可完成磁盘读取
模块Func_ReadOneSector
功能:读取磁盘扇区。
AX
=待读取的磁盘起始扇区号;
CL
=读入的扇区数量;
ES:BX
=>目标缓冲区起始地址。
文件搜索模块
因为FAT表里只描述文件由哪些簇组成,文件属性都放在目录项里。目录项整个占32Bytes,前11Bytes是文件名。
名称
偏移
长度
描述
DIR_Name
0x00
11
文件名8B,扩展名3B
DIR_Attr
0x0B
1
文件属性
保留
0x0c
10
保留位
DIR_WrtTime
0x16
2
最后一次写入时间
DIR_WrtDate
0x18
2
最后一次写入日期
DIR_FstClus
0x1A
2
起始簇号
DIR_FileSize
0x1C
4
文件大小
下面分析一下比较文件名的过程
首先有两个字符串要比较,一个是在代码中定义的LoaderFileName
,这是我们要找的文件名。另一个是读取到的目录项中的文件名,在书中的代码里,目录项被读取到了8000h
这个地址
因为要比较的是字符串,每次都只能比较一个字符,所以需要两个指针,一个指向[LoaderFileName]
,一个指向目录项的文件名部分。每次比较一个字符,如果相同就移动指针前往下一个字符。在这里,LoaderFileName
的指针是SI
,目录项文件名的指针是DI
。
mov si, LoaderFileName ; loader文件名地址装进SI
mov di, 8000h ; DI = 8000H
指针的操作。对SI
指针的操作,书中使用了LODSB/LODSW/LODSD/LODSQ
(区别是值的位数)指令,
从DS:(R|E)SI寄存器指定的内存地址中读取数据到AL/AX/EAX/RAX寄存器
当数据载入到AL/AX/EAX/RAX寄存器后,(R|E)SI寄存器将会依据R|EFLAGS标志寄存器的DF位(之前用CLD清零的(DF=0))
自动减少或增加载入的数据长度
若DF=0, (R|E)SI++,在本程序中即指向文件名下一个字符的地址
LODSB/LODSW/LODSD/LODSQ
指令可以完成:从内存([DS:(R|E)SI])读入数据到寄存器(AL/AX/EAX/RAX,取决于数据长度)-> 移动SI指针(方向取决于DF
标志位)
对DI
指针的操作,则在比较一次后使用inc
递增。
Label_Go_On:
inc di ; DI++, 移动指向目录项文件名的指针,指向下一个字符
jmp Label_Cmp_FileName ; 比较这个字符</code></pre>
比较两个字符:
cmp al, byte [es:di] ; AL已经存入了SI指向的字符(AL是我们给出的文件名的一个字符),现在与[es:di]比较,[es:di]是从磁盘中读取到的目录的文件名部分的一个字符的值
FAT表项解析模块
因为书中使用的是FAT12文件系统,这个文件系统比较棘手的一点是一个目录项占12bit,也就是一个半Byte,首先要计算表项的存放情况,确定是先一个Byte还是先半个Byte,然后在读取表项时需要先把表项从两个字节里分离出来。
=============================
|AAAAAAAA|AAAABBBB|BBBBBBBB|
=============================
;======= get FAT Entry
Func_GetFATEntry:
; 获取FAT表项
push es ; ES 存栈
push bx ; BX 存栈
push ax ; AX 存栈
mov ax, 00 ; AX = 0
mov es, ax ; ES = AX = 0
pop ax ; AX 出栈
mov byte [Odd], 0 ; 标志位Odd = 0
mov bx, 3 ; BX = 3
mul bx ; AX = BX * AX
mov bx, 2 ; BX = 2
div bx ; AX / BX = AX….DX
cmp dx, 0 ; 比较DX,若为奇数,DX=1,若为偶数则DX=0
jz Label_Even ; 若为偶数,跳转到Label_Even,此时Odd=0
mov byte [Odd], 1 ; 否则标志位Odd设为1
Label_Even:
xor dx, dx ; DX = DX ^ DX = 0, 这样写比mov使用更少的字节
mov bx, [BPB_BytesPerSec] ; BX = 每扇区字节数
div bx ; AX = Fat表项偏移扇区号,DX = FAT表项在扇区中的偏移位置
push dx ; DX 存栈
mov bx, 8000h ; BX = 8000H,目标缓冲区起始地址
add ax, SectorNumOfFAT1Start ; AX = AX + FAT1表起始扇区号(1),得到需要的FAT项所在的的扇区号,作为ReadOneSector的参数之一
mov cl, 2 ; CL = 2 指示ReadOneSector读入两个扇区
call Func_ReadOneSector ; 调用ReadOneSector,把FAT表项所在的扇区读入到8000H
pop dx ; DX 出栈
add bx, dx ; BX = BX + DX,BX = 目标缓冲区起始地址 + FAT表项在扇区中的偏移位置
mov ax, [es:bx] ; 读出FAT表项到AX
cmp byte [Odd], 1 ; Odd == 1?
jnz Label_Even_2 ; Odd != 1 , 跳转到Label_Even_2
shr ax, 4 ; 奇数项右移4位(半字节),把属于前一个FAT表项的部分去掉
Label_Even_2:
and ax, 0fffh ; AX & 0fffh, 只保留前12个bits = 1.5Bytes,即截取该FAT表项
pop bx ; 弹出BX
pop es ; 弹出ES
ret ; 返回,回到call Func_GetFATEntry的地方。 返回值AX = 要找的FAT表项
参数AH=FAT表项号,返回值AX是下一个FAT表项号。
通过不断读出FAT表项号,然后根据FAT表项号计算出数据所在的扇区,读出数据到内存,最终将整个文件加载到内存中。
完整内容
;/***************************************************
; 版权声明
;
; 本操作系统名为:MINE
; 该操作系统未经授权不得以盈利或非盈利为目的进行开发,
; 只允许个人学习以及公开交流使用
;
; 代码最终所有权及解释权归田宇所有;
;
; 本模块作者: 田宇
; EMail: 345538255@qq.com
;
;
;***************************************************/
org 0x7c00
BaseOfStack equ 0x7c00
; Loader 程序起始物理地址
BaseOfLoader equ 0x1000
OffsetOfLoader equ 0x00
RootDirSectors equ 14 ; 根目录所占扇区数
SectorNumOfRootDirStart equ 19
SectorNumOfFAT1Start equ 1
SectorBalance equ 17
jmp short Label_Start
nop
BS_OEMName db 'MINEboot' ; 生产厂商名
BPB_BytesPerSec dw 512 ; 每扇区字节数 512bytes
BPB_SecPerClus db 1 ; 每簇扇区数 1
BPB_RsvdSecCnt dw 1 ; 保留扇区数 1
BPB_NumFATs db 2 ; FAT表的份数 2
BPB_RootEntCnt dw 224 ; 根目录可容纳扇区数 223
BPB_TotSec16 dw 2880 ; 总扇区数
BPB_Media db 0xf0 ; 介质描述符 0xF0 软盘
BPB_FATSz16 dw 9 ; 每Fat扇区数 9
BPB_SecPerTrk dw 18 ; 每磁道扇区数 18
BPB_NumHeads dw 2 ; 磁头数 2
BPB_HiddSec dd 0 ; 隐藏扇区数 0
BPB_TotSec32 dd 0 ; 如果BPB_TotSec16为0, 则由这个值记录扇区数
BS_DrvNum db 0 ; int 13h 的驱动器号 0
BS_Reserved1 db 0 ; 未使用 0
BS_BootSig db 0x29 ; 扩展引导标记 0x29h
BS_VolID dd 0 ; 卷序列号
BS_VolLab db 'boot loader' ; 卷标 'boot loader'
BS_FileSysType db 'FAT12 ' ; 文件系统类型 'FAT12'
Label_Start: ; 跳转位置,跳过元数据, boot引导开始
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, BaseOfStack
;======= clear screen
mov ax, 0600h
mov bx, 0700h
mov cx, 0
mov dx, 0184fh
int 10h
;======= set focus
mov ax, 0200h
mov bx, 0000h
mov dx, 0000h
int 10h
;======= display on screen : Start Booting……
mov ax, 1301h
mov bx, 000fh
mov dx, 0000h
mov cx, 10
push ax
mov ax, ds
mov es, ax
pop ax
mov bp, StartBootMessage
int 10h
; 上面是第一部分
;======= reset floppy 重置软盘
xor ah, ah
xor dl, dl
int 13h
;======= search loader.bin
; SectorNo 扇区号,
; SectorNumOfRootDirStart 起始扇区号 就是根目录的起点
mov word [SectorNo], SectorNumOfRootDirStart ; 搜索loader.bin
Lable_Search_In_Root_Dir_Begin:
; 有一个程序状态寄存器,执行cmp后会给其中几个位(作用不同)设flag
; jz 是选择ZF标志位做跳转条件 执行条件是ZF=0
cmp word [RootDirSizeForLoop], 0 ; 一个扇区一个扇区地查找loader, 比较Root
jz Label_No_LoaderBin ; 等于转移 jz的意思是jump if zero 、
; 没有跳转的话就往下执行
dec word [RootDirSizeForLoop] ; RootDirSizeForLoop - 1 前往下一个扇区
mov ax, 00h ; ax=00h
; 准备调用读取软盘用的Func_ReadOneSector函数,现在要输入参数
; 参数: AX=待读取的磁盘起始扇区号;CL=读入的扇区数量;ES:BX 目标缓冲区起始地址
mov es, ax ; es=ax=0
mov bx, 8000h ; bx=8000h,
mov ax, [SectorNo] ; 要读取扇区号
mov cl, 1 ; 读入一个扇区
call Func_ReadOneSector ; 读入一个扇区,数据装到缓冲区[ES:BX] ([00008000h])
mov si, LoaderFileName ; loader文件名地址装进SI
mov di, 8000h ; DI = 8000H
cld ; CLD flag方向标志位DF清零,在字串操作中使变址寄存器SI或DI的地址指针自动增加,字串处理由前往后。
; SI--, DI--
mov dx, 10h ; DX = 10h 每个扇区可容纳的目录项的个数 512/32=10h,一个根目录项是32Bytes,前11Bytes是文件名
Label_Search_For_LoaderBin:
cmp dx, 0 ; DX==0?
jz Label_Goto_Next_Sector_In_Root_Dir ; DX==0 => ZF = 1, 说明这个扇区的目录项已经读取完了,要到下一个扇区:跳转至jz Label_Goto_Next_Sector_In_Root_Dir,
dec dx ; DX-- 这个扇区还没读完,去找下一个目录项
mov cx, 11 ; cx==11,指目录项的文件名长度
Label_Cmp_FileName:
cmp cx, 0 ; cx==0?
jz Label_FileName_Found ; 若CX==0,说明每一个字符都匹配,即找到文件,跳转到Label_FileName_Found
dec cx ; 否则继续比较下一个字符,CX--,
lodsb ; LODSB/LODSW/LODSD/LODSQ(区别是值的位数)指令:
; 从DS:(R|E)SI寄存器指定的内存地址中读取数据到AL/AX/EAX/RAX寄存器
; 当数据载入到AL/AX/EAX/RAX寄存器后,(R|E)SI寄存器将会依据R|EFLAGS标志寄存器的DF位(之前用CLD清零的(DF=0))
; 自动减少或增加载入的数据长度
; 若DF=0, (R|E)SI++,在本程序中即指向文件名下一个字符的地址
cmp al, byte [es:di] ; AL已经存入了SI指向的字符(AL是我们给出的文件名的一个字符),现在与[es:di]比较,[es:di]是从磁盘中读取到的目录的文件名部分的一个字符的值
jz Label_Go_On ; 相同,跳转到Label_Go_On,准备比较下一个字符
jmp Label_Different ; 不相同,确认与要搜索的文件名不符,跳转到Label_Different,准备读取下一个目录项
Label_Go_On:
inc di ; DI++, 移动指向目录项文件名的指针,指向下一个字符
jmp Label_Cmp_FileName ; 比较这个字符
Label_Different:
; 目录项的文件名不相同
and di, 0ffe0h ; 这个and的目的是,将di指向这个目录项(刚刚搜索的那个)的开头
add di, 20h ; 20H = 32D,然后跳过这个目录项,到达下一个目录项开头
mov si, LoaderFileName ; 重置我们要搜索的文件名的指针,(因为搜索前面的目录项时,这个指针被动过)
jmp Label_Search_For_LoaderBin ; 回到Label_Search_For_LoaderBin
Label_Goto_Next_Sector_In_Root_Dir:
add word [SectorNo], 1 ; SectorNo + 1,扇区号加1,即下一个扇区
jmp Lable_Search_In_Root_Dir_Begin ; 去在下一个扇区里搜索
;======= display on screen : ERROR:No LOADER Found
Label_No_LoaderBin:
; 显示无loader
mov ax, 1301h
mov bx, 008ch
mov dx, 0100h
mov cx, 21
push ax
mov ax, ds
mov es, ax
pop ax
mov bp, NoLoaderMessage
int 10h
jmp $
;======= found loader.bin name in root director struct
Label_FileName_Found:
; 文件名匹配
mov ax, RootDirSectors ; AX = 根目录所占扇区数
and di, 0ffe0h ; di 重新指向找到的这个目录项的开始
add di, 01ah ; di 跳到起始簇号所在的地方
mov cx, word [es:di] ; 取出簇号(簇号占2字节=1字)
push cx ; cx存栈
add cx, ax ; CX = CX + AX,得到数据区开头
add cx, SectorBalance ; CX = CX + 根目录起始区号 - 2。 实际上,扇区号 = 数据区簇号(从2开始)*每簇扇区号 + 根目录起始扇区号 + 根目录所占扇区数 - 2
mov ax, BaseOfLoader ; AX = loader程序起始物理地址,最终要把loader加载到内存的这个地方
mov es, ax ; ES = AX = loader程序起始物理地址
mov bx, OffsetOfLoader ; BX = OffsetOfLoader
mov ax, cx ; AX = CX = (数据所在的)扇区号
Label_Go_On_Loading_File:
push ax ; AX 压栈
push bx ; BX 压栈
; 显示 “...”
mov ah, 0eh ; AH = 0eH
mov al, '.'
mov bl, 0fh
int 10h ; BIOS中断服务程序
pop bx ; BX 出栈
pop ax ; AX 出栈
mov cl, 1 ; CL = 1;
call Func_ReadOneSector ; 读取一个扇区
pop ax ; 弹出AX
call Func_GetFATEntry ; 获取FAT表项,FAT表项在AX中返回
cmp ax, 0fffh ; AX == 0fffh? 检查是否是最后一个簇
jz Label_File_Loaded ; 如果是最后一个簇,跳转到Label_File_Loaded
push ax ; AX压栈
mov dx, RootDirSectors ; DX = 根目录所占扇区数
add ax, dx ; AX = AX + DX
add ax, SectorBalance ; AX + SectorBalance, AX = 文件所在簇号 + 根目录所占扇区数 + 根目录起始扇区 - 2 = FAT表项所指的簇
add bx, [BPB_BytesPerSec] ; BX + 每扇区字节数,目的是将读出的内容追加到上一次读出的内容后面
jmp Label_Go_On_Loading_File ; 跳转至 Label_Go_On_Loading_File,读出下一个扇区
Label_File_Loaded:
jmp $
;======= read one sector from floppy
; 软盘读取功能
Func_ReadOneSector:
push bp ; 栈帧寄存器压栈
mov bp, sp ;
sub esp, 2 ; esp - 2 在在栈中开辟两个字节的新空间
mov byte [bp - 2], cl ; 参数cl=读入山区的数量 存栈
push bx ; BX 存栈
; 用LBA扇区号计算CHS (Cylinder/Head/Sector,柱面/磁头/扇区)
mov bl, [BPB_SecPerTrk] ; 每磁道扇区数装入bl
; div 指令根据除数位数选择寄存器,8位为AX, 16位DX:AX,32位EDX:EAX,
div bl ; AX/BL = AL ... AH , LBA扇区号/每磁道扇区数 = 磁道号……磁道内扇区号
inc ah ; 磁道内起始扇区号从1开始,LBA从0开始,所以要+1
mov cl, ah ; AH移动到CL
mov dh, al ; 商AL移动到DH
shr al, 1 ; 柱面号 = 磁道号>>1
mov ch, al ; 求得的柱面号移动至CH
and dh, 1 ; DH(商)与1与,得到磁头号
pop bx ; 弹出BX
; 调用BIOS中断服务程序
mov dl, [BS_DrvNum] ; DL参数驱动器号
Label_Go_On_Reading:
mov ah, 2 ; 设置中断服务程序,ah=02h,功能是读取磁盘扇区
mov al, byte [bp - 2] ; 读入的扇区数,放入al, [bp-2]是之前输入的参数,指读入扇区数
int 13h ; BIOS中断服务程序
; 中断服务程序从软盘扇区读取数据到内存中,数据读取成功时CF复位,CF=0
; 此时轮询CF位,直到读取完成,完成时CF=0
jc Label_Go_On_Reading ; CF = 1 回到这一块代码开始
add esp, 2 ; 收回2个字节的栈空间
pop bp ; bp出栈
ret ; 返回
; RET
; 弹出栈中的保存的返回地址(之前被CALL存进去的),回到了116 call Func_ReadOneSector
;======= get FAT Entry
Func_GetFATEntry:
; 获取FAT表项
push es ; ES 存栈
push bx ; BX 存栈
push ax ; AX 存栈
mov ax, 00 ; AX = 0
mov es, ax ; ES = AX = 0
pop ax ; AX 出栈
mov byte [Odd], 0 ; 标志位Odd = 0
mov bx, 3 ; BX = 3
mul bx ; AX = BX * AX
mov bx, 2 ; BX = 2
div bx ; AX / BX = AX….DX
cmp dx, 0 ; 比较DX,若为奇数,DX=1,若为偶数则DX=0
jz Label_Even ; 若为偶数,跳转到Label_Even,此时Odd=0
mov byte [Odd], 1 ; 否则标志位Odd设为1
Label_Even:
xor dx, dx ; DX = DX ^ DX = 0, 这样写比mov使用更少的字节
mov bx, [BPB_BytesPerSec] ; BX = 每扇区字节数
div bx ; AX = Fat表项偏移扇区号,DX = FAT表项在扇区中的偏移位置
push dx ; DX 存栈
mov bx, 8000h ; BX = 8000H,目标缓冲区起始地址
add ax, SectorNumOfFAT1Start ; AX = AX + FAT1表起始扇区号(1),得到需要的FAT项所在的的扇区号,作为ReadOneSector的参数之一
mov cl, 2 ; CL = 2 指示ReadOneSector读入两个扇区
call Func_ReadOneSector ; 调用ReadOneSector,把FAT表项所在的扇区读入到8000H
pop dx ; DX 出栈
add bx, dx ; BX = BX + DX,BX = 目标缓冲区起始地址 + FAT表项在扇区中的偏移位置
mov ax, [es:bx] ; 读出FAT表项到AX
cmp byte [Odd], 1 ; Odd == 1?
jnz Label_Even_2 ; Odd != 1 , 跳转到Label_Even_2
shr ax, 4 ; 奇数项右移4位(半字节),把属于前一个FAT表项的部分去掉
Label_Even_2:
and ax, 0fffh ; AX & 0fffh, 只保留前12个bits = 1.5Bytes,即截取该FAT表项
pop bx ; 弹出BX
pop es ; 弹出ES
ret ; 返回,回到call Func_GetFATEntry的地方。 返回值AX = 要找的FAT表项
;======= tmp variable
RootDirSizeForLoop dw RootDirSectors
SectorNo dw 0
Odd db 0
;======= display messages
StartBootMessage: db "Start Boot"
NoLoaderMessage: db "ERROR:No LOADER Found"
LoaderFileName: db "LOADER BIN",0
;======= fill zero until whole sector
times 510 - ($ - $$) db 0
dw 0xaa55
运行效果