(1)操作系统学习笔记——FAT12文件系统与Loader的加载


(1)操作系统学习笔记——FAT12文件系统与Loader的加载

之前我们利用BIOS实现了一个打印字符串的操作,现在我们要在这基础上加入文件加载的功能,以完成boot的操作。Boot程序主要的是负责开机启动和加载Loader程序;Loader引导加载程序则用于完成配置硬件工作环境、引导加载内核等任务。
加载Loader程序最理想的方法自然是从文件系统中把Loader程序加载到内存中。那么此处的文件系统就选择比较简单的FAT12。在将软盘格式化成FAT12文件系统的过程中,FAT类文件系统会对软盘里的扇区进行结构化处理,进而把软盘扇区划分成: 引导扇区、FAT表、根目录区和数据区 这4部分。

  • 引导扇区
    FAT12文件系统的引导扇区不仅包含有引导程序,还有FAT12文件系统的整个组成结构信息。下表描述了FAT12文件系统的引导扇区结构:

    名称 偏移 长度 内容 本系统引导程序数据
    BS_jmpBoot 0 3 跳转指令 jmp short Label_Start nop
    BS_OEMName 3 8 生产厂商名 'MINEboot'
    BPB_BytesPreSec 11 2 每扇区字节数 512
    BPB_SecPreClus 13 1 每簇扇区数 1
    BPB_RsvSecCnt 14 2 保留扇区数 1
    BPB_NumFATs 16 1 FAT表的份数 2
    BPB_RootEntCnt 17 2 根目录可容纳的目录项数 224
    BPB_TotSec16 19 2 总扇区数 2880
    BPB_Media 21 1 介质描述符 0xF0
    BPB_FATSz16 22 2 每FAT扇区数 9
    BPB_SecPreTrk 24 2 每磁道扇区数 18
    BPB_NumHeads 26 2 磁头数 2
    BPB_HiddSec 28 4 隐藏扇区数 0
    BPB_TotSec32 32 4 如果BPB_Tot16值为0,则由这个值记录扇区数 0
    BS_DrvNum 36 1 int 13h的驱动器号 0
    BS_Reserved1 37 1 未使用 0
    BS_BootSig 38 1 扩展引导标记(0x29) 0x29
    BS_VolID 39 4 卷序列号 0
    BS_VolLab 43 11 卷标 'boot loader'
    BS_FileSysType 54 8 文件那系统类型 'FAT12'
    引导代码 62 448 引导代码、数据及其他信息
    结束标志 510 2 结束标志0xAA55 0xAA55
  • FAT表
    FAT文件系统以簇来为单位来分配数据区的存储空间(扇区), 每个簇的长度为:BPB_BytesPreSec * BPB_SecPreClus字节,数据区的簇号与FAT表的表项是一一对应关系。因此,文件在FAT类文件系统的存储单位是簇,而非字节或扇区。这种设计方式可以将磁盘存储空间按固定储片(页)有效管理起来,进而按照文件偏移,分片段访问文件内的数据,就不必一次将文件里的数据全部读取出来。
    FAT表中的表项位宽与FAT类型有关,例如,FAT12文件系统的表项位宽就是12bit、FAT16文件系统的表项位宽就是16bit、FAT32文件系统的表项位宽就是32bit。当文件的体积增大时,其所需的磁盘存储空间也会增加,随时间的推移,文件系统将无法保证文件中的数据存储在连续的磁盘扇区内,文件往往被分成若干个片段。借助FAT表项,可将这些不连续的文件片段按簇号链接起来,这个链接原理与链表极为相似。下面是对FAT表项取值的说明:

    FAT项 实例值 描述
    0 0xFF0 磁盘标志字,低字节与BPB_Media数值保持一致
    1 0xFFF 第一个簇已经被占用
    0x000:可用簇
    0x002~0xFEF:已用簇标识下一个簇的簇号
    0xFF0 ~ 0xFF6:保留簇
    0xFF7:坏簇
    0xFF8~0xFFF:文件的最后一个簇
    

    在编写程序的时候我们直接跳过FAT[0]和FAT[1]即可,并不使用它们,不必要理会其中的值。

  • 根目录和数据区
    从本质上讲,根目录和数据区都保存着与文件相关的数据,只不过根目录只能保存目录项信息,而数据区不但可以保存目录项信息,还可以保存文件内的数据。
    此处所提及的目录项是由一个32B组成的结构体,它既可以 表示成一个目录,又可以表示成一个文件。

    名称 偏移 长度 描述
    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 文件大小

    注: 对于DIR_FstClus字段必须特别注意,它描述了文件在磁盘中存放的具体位置。由于 FAT[0] 和 FAT[1] 是保留项,不能用于数据区的簇索引,因此数据区的第一个有效簇号是2,而不是0或1。

程序编写

FAT12文件系统数据

首先是为虚拟软盘创建FAT12文件系统引导扇区数据:

org 0x7c00

; 设置栈指针的位置
BaseOfStack equ 0x7c00

; Loader的位置(20位)
; BaseOfLoader << 4 + OffsetOfLoader = 0x10000
BaseOfLoader	equ	0x1000
OffsetOfLoader	equ	0x00

; 根目录占用的扇区数:(BPB_RootEntCnt * 32 + BPB_BytesPerSec - 1) / BPB_BytesPerSec
; (224 * 32 + 512 - 1) / 512 = 14
RootDirSectors	equ	14

; 根目录的起始扇区号 = 
; 保留扇区数(BPB_RsvdSecCnt) + FAT表扇区数(BPB_FATSz16) + FAT表份数(BPB_NumFATs) = 1 + 9 * 2 = 19
SectorNumOfRootDirStart	equ	19

; FAT表的起始扇区号,引导扇区的扇区号是0
SectorNumOfFAT1Start	equ	1

; 用于平衡文件(或目录)的起始簇号与数据区起始簇号的差值。
SectorBalance	equ	17	

;****************************************************
        ; 程序一开始就跳转到Label_Start的位置处执行
        jmp	short Label_Start
        nop
;****************************************************
        ; 生产厂商名(8 Bits)
        BS_OEMName	db	'Testboot'
        ; 每扇区字节数
        BPB_BytesPerSec	dw	512
        ; 每簇扇区数
        BPB_SecPerClus	db	1
        ; 保留扇区数
        BPB_RsvdSecCnt	dw	1
        ; FAT表的份数
        BPB_NumFATs	db	2
        ; 根目录可容纳的的目录项数
        BPB_RootEntCnt	dw	224
        ; 总扇区数(因为使用的是1.44M的软盘)
        BPB_TotSec16	dw	2880
        ; 介质描述符
        BPB_Media	db	0xf0
        ; 每FAT扇区数
        BPB_FATSz16	dw	9
        ; 每磁道扇区数
        BPB_SecPerTrk	dw	18
        ; 磁头数
        BPB_NumHeads	dw	2
        ; 隐藏扇区数
        BPB_HiddSec	dd	0
        ; 如果BPB_FATSz16=0,那么这个值记录扇区数
        BPB_TotSec32	dd	0
        ; int 0x13的驱动器号
        BS_DrvNum	db	0
        ; 未使用(不用管)
        BS_Reserved1	db	0
        ; 扩展引导标记
        BS_BootSig	db	0x29
        ; 卷序列号
        BS_VolID	dd	0
        ; 卷标(11 Bits)
        BS_VolLab	db	'boot loader'
        ; 文件系统类型(8 Bits)
        BS_FileSysType	db	'FAT12   '

这段代码是在我们的程序一开始就定义的,但是程序是从上往下顺序执行的,代码中有想当一部分是数据,并不是代码,所以我们一开始就需要使用无条件跳转指令jmp short Label_Start,跳转到Label_Start标签处开始执行代码。

读取磁盘扇区模块(函数)

有了FAT12文件系统引导扇区的数据,那么还要一个软盘的读取功能模块(也可以理解为函数),该模块的作用就是读取软盘,每次读取一个扇区:

;==============================
; 模块(可以理解为C语言中的函数)
; 从软驱中读取扇区
; 参数说明:
;       AX = 待读取的磁盘起始扇区号
;       CL= 读入的扇区数量
;       EX : BX => 数据缓冲区起始地址
Func_ReadOneSector:
	
	push	bp
	mov	bp,	sp
	sub	esp,	2
	mov	byte	[bp - 2],	cl
	push	bx
	mov	bl,	[BPB_SecPerTrk]
	div	bl
	inc	ah
	mov	cl,	ah
	mov	dh,	al
	shr	al,	1
	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
;==============================

同样地,读取扇区,也借助了BIOS的 0x13号中断,功能号为 AH=0x02 ,该中断服务各寄存器参数说明如下:

  • INT 0x13 AH = 0x02: 读取磁盘扇区
    • AL = 读入的扇区数(非0)
    • CH = 磁道号(柱面号)的低8位
    • CL = 扇区号1~63(bit 0~5),磁道号(柱面号)的高两位(bit 6~7,只对硬盘有效)
    • DH = 磁头号
    • DL=驱动器号(如果是硬盘驱动器,bit 7必须被置位)
    • ES : BX => 数据缓冲区
    • 出口参数:CF=0——操作成功,AH=00H,AL=传输的扇区数,否则,AH=状态代码(具体可以自行百度或Google)

模块Func_ReadOneSector仅仅是对BIOS的0x13号中断0x02号功能的封装,以简化磁盘读取扇区的的操作过程,该模块是需要参数的,参数说明如下:

  • AX = 待读取的磁盘起始扇区号
  • CL= 读入的扇区数量
  • EX : BX => 数据缓冲区起始地址

需要说明的是,模块Func_ReadOneSector的参数中传入的磁盘扇区号是 LBA(Logical Block Address,逻辑块寻址) 格式的,而BIOS的0x13号中断0x02号功能只能接受 CHS(Cylinder/Head/Sector,柱面/磁头/扇区) 格式的磁盘扇区号,那么就需要将LBA格式的转换为CHS格式的,通过以下公式,便可以转换:

\[LBA扇区号 \div 每磁道扇区数 \begin{cases} 商Q → \begin{cases} 柱面号 = Q >> 1,\\ 磁头号 = Q \& 1,\\ \end{cases}\\ 余数R → 起始扇区号 = R + 1,\\ \end{cases} \]

  1. 模块Func_ReadOneSector一开始,先保存栈帧寄存器(bp)和栈寄存器(sp)中的数值(bp存储在栈中,sp保存在bp寄存器中)
  2. 然后从栈中开辟两个字节的存储空间,即是sp向下移动两个字节(sub esp, 2)。然后将CL中的内容(1个字节,参数,读入扇区数)存入栈中,因为接下来要使用到CL寄存器。注: CL中的内容是一个字节,而栈项要对齐,所以要要用两个字节保存一个字节的内容,即使浪费了一个字节。
  3. 然后就是把BX的内容也保存到栈中,因为接下来要用到BX
  4. 使用AX寄存器(待读取的磁盘起始扇区号)除以BL寄存器(每磁道扇区数),计算出目标磁道号(商:AL寄存器)和目标磁道内的起始扇区号(余数:AH寄存器),又因为磁道内的起始扇区号是从1开始计数的,故将此余数值加1(inc ah; mov cl, ah)紧接着按照以上公式计算出磁道号(即是柱面号)、磁头号。
  5. 恢复BX
  6. 获取驱动器号BS_DrvNum
  7. 设置0x13中断功能号AH=0x02,设置读入扇区数mov al, byte [bp - 2](我们之前把参数CL读入扇区数保存在了栈中,所以现在用bp去索引它)。
  8. 然后调用0x13号中断。
  9. 判断是否读取成功(即是若CF位为0,则成功),否则(CF=1)回到步骤7

LOADER.BIN文件搜索

当我们实现了读取扇区的功能后,我们就可以实现文件搜索功能了:

; ================查找文件loader.bin================
    ;保存根目录的起始扇区号([SectorNo]是一个临时变量地址)
    ; 赋值为:SectorNumOfRootDirStart(根目录的起始扇区号)
    ; 初始值为:19(前面已算出)
    mov	word	[SectorNo],	SectorNumOfRootDirStart

Lable_Search_In_Root_Dir_Begin:

    ; RootDirSizeForLoop也是一个临时变量,值是:RootDirSectors(14)
    ; 判断是否为0,如果是提示找不到loader.bin
    cmp	word	[RootDirSizeForLoop],	0
    jz	Label_No_LoaderBin
    ; 查找的扇区减1
    dec	word	[RootDirSizeForLoop]

    ;设置读扇区模块的参数 EX:BX、AX、CL
    ; 对应缓冲区起始、待读取的磁盘起始扇区号、读入扇区数量
    mov	ax,	00h
    mov	es,	ax
    mov	bx,	8000h
    mov	ax,	[SectorNo]
    mov	cl,	1
    ; 调用读扇区模块
    call	Func_ReadOneSector
    ; 开始查找loader.bin
    mov	si,	LoaderFileName  ;文件名源地址
    mov	di,	8000h           ;文件名目标地址
    cld     ;DF复位=0,往高地址递进
    ; dx记录每个扇区可容纳的目录项目个数
    mov	dx,	10h
	
Label_Search_For_LoaderBin:

    ;如果dx为0,说明比较完了,开始读取下一个根目录扇区
    cmp	dx,	0
    jz	Label_Goto_Next_Sector_In_Root_Dir
    
    ;否则dx减1(目录项减1)
    dec	dx
    ; cx保存目录项文件名长度 11 bits
    mov	cx,	11

Label_Cmp_FileName:

    ; 判断文件名是否比较完
    ; 即是cx是否为0,能比较完的话,说明已经找到了
    cmp	cx,	0
    jz	Label_FileName_Found

    ;否则没有比较完,cx -= 1,开始比较下一个字符
    dec	cx
    ; 从DS:SI读取一个字节到寄存器AL,然后SI += 1(取决于DF)
    lodsb
    ; 比较目标字串中对应的字符与al中的字符,看是否相等
    cmp	al,	byte	[es:di]
    ; 如果相等,继续比较下一个字符(跳转到Label_Go_On)
    jz	Label_Go_On
    ; 如果不相等,就下一个文件项
    jmp	Label_Different

Label_Go_On:
	
    ; 目标字串指针加一,然后继续比较
    inc	di
    jmp	Label_Cmp_FileName

Label_Different:

    ; di的低5位清零,
    and	di,	0ffe0h
    ; di += 32,表示指向下一个目录项
    add	di,	20h
    ; si指向LoaderFileName的起始地址
    mov	si,	LoaderFileName
    ; 继续比较
    jmp	Label_Search_For_LoaderBin

Label_Goto_Next_Sector_In_Root_Dir:
	
    ; 在当前扇区找不到loader.bin
    ; 那么就在下一个扇区找,扇区号+1:[SectorNo]+1
    add	word	[SectorNo],	1
    jmp	Lable_Search_In_Root_Dir_Begin

注意: FAT12文件系统的文件名是不分大小写的,即是将小写字母命名的文件赋值到FAT12文件系统内,文件系统也会为其创建大写字母的文件名和目录项。而小写字母文件名只作为其显示名,而真正的数据内容皆保存在对应的大写字母目录项。所以这里应该搜索大写字母的文件名字符串。

找不到LOADER.BIN文件

当上面那一段程序的指令cmp word [RootDirSizeForLoop], 0判断发现[RootDirSizeForLoop]中的数据为0的时候,说明没有找到LOADER.BIN文件,那么就提示找不到LOADER.BIN。

; ========= 打印屏幕信息 : ERROR:No LOADER Found =========

Label_No_LoaderBin:

        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	$
; =======================================================

当boot程序找不到LOADER.BIN文件会有如下所示的结果:

FAT表项解析

当找到loader.bin文件后,便可以根据FAT表项提供的簇号顺序依次加载扇区数据到内存中,这个过程会涉及FAT表项的解析工作:

;==============================
; 模块(可以理解为C语言中的函数)
; 根据当前FAT表项索引出下一个FAT表项
; 参数说明:
;       AX = FAT表项号(输入/输出参数)
Func_GetFATEntry:

    push	es
    push	bx
    push	ax
    mov	ax,	00
    mov	es,	ax
    pop	ax
    ; Odd为奇偶标志变量(奇数1,偶数0)
    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:

    ; 读取FAT表扇区,总共两个扇区
    xor	dx,	dx
    mov	bx,	[BPB_BytesPerSec]
    div	bx
    push	dx
    mov	bx,	8000h
    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,	0fffh
    pop	bx
    pop	es
    ret
;==============================
  • 该模块需要的参数: AX = FAT表项号(输入/输出参数)

在这段程序中,首先把FAT表项号(AX)保存,并将奇偶标志变量置0,因为每个FAT表项占1.5B,并不是偶数对齐,所以就将FAT表项乘以3再除以2(也就是扩大1.5倍),然后判断余数的奇偶性并保存在变量[Odd]中(奇数为1,偶数为0),再将计算结果除以每扇区字节数,商即为FAT表项的偏移扇区号,余数为FAT表项在扇区中的位置。然后通过Func_ReadOneSector模块,连续读入两个扇区,这样做的目的是,为了解决FAT表项横跨两个扇区的问题,最后根据奇偶标志变量[Odd]处理奇偶项错位问题,即是奇数项向右移动4位。当然如果觉得麻烦的话,可以将FAT12文件系统换成FAT16文件系统。

从FAT12文件系统中加载loader.bin到内存

在完成Func_ReadOneSectorFunc_GetFATEntry模块后,就可以借助这两个模块把loader.bin文件的数据从软盘扇区读取到指定地址中:

; =======================================================
; 在根目录中找到名为loader.bin的文件了
Label_FileName_Found:
        ;如果找到了的话,当前es:di指向的就是目标表项
        ; AX保存根目录占用扇区数
        mov	ax,	RootDirSectors
        ; 定位至表项起始簇号的位置(偏移0x1a)
        and	di,	0ffe0h
        add	di,	01ah
        ; 起始簇号保存到cx
        mov	cx,	word	[es:di]
        push	cx
        ; 计算数据区起始扇区号
        ; 数据区起始扇区号=根目录起始扇区号+根目录所占扇区数-2
        add	cx,	ax
        ; SectorBalance = 根目录起始扇区号 - 2
        add	cx,	SectorBalance
        ; 配置loader的加载位置 EX:BX
        mov	ax,	BaseOfLoader
        mov	es,	ax
        mov	bx,	OffsetOfLoader
        ; ax = loader起始扇区号
        mov	ax,	cx

Label_Go_On_Loading_File:
        ; 显示字符 '.'
        push	ax
        push	bx
        mov	ah,	0eh
        mov	al,	'.'
        mov	bl,	0fh
        int	10h
        pop	bx
        pop	ax

        ; 读入loader
        ; 每读入一个扇区就通过Func_GetFATEntry模块获取下一个FAT表项
        ; 然后继续读入数据
        mov	cl,	1
        call	Func_ReadOneSector
        ; 恢复簇号
        pop	ax
        ; 获取下一个簇号
        call	Func_GetFATEntry
        ; 查看是否已经读完了
        cmp	ax,	0fffh
        jz	Label_File_Loaded
        ; 否则保存当前获取到簇号
        push	ax
        ; 计算loader文件的下一个簇的位置
        mov	dx,	RootDirSectors
        add	ax,	dx
        add	ax,	SectorBalance
        add	bx,	[BPB_BytesPerSec]
        ; 继续读
        jmp	Label_Go_On_Loading_File

Label_File_Loaded:
	
        jmp $
; =======================================================

Label_FileName_Found模块中,程序首先会取得目录DIR_FstClus字段的数值,并且通过配置ES:BX来指定loader程序在内存中的加载位置,再通过loader.bin程序的起始簇号计算出与其对应的扇区号。然后每读一个扇区前就答应一个字符'.',每读入一个扇区的数据就通过Func_GetFATEntry模块获取下一个FAT表项,直到Func_GetFATEntry返回0xfff为止,当全部读完之后就跳转到Label_File_Loaded处,准备执行loader.bin程序。

这段代码中使用了BIOS中断服务程序INT 0x10AH=0x0e号功能,它能在屏幕上显示一个字符详细的寄存器参数如下:

  • INT 0x10; AH=0x0e功能: 在屏幕上显示一个字符。
    • AL=待显示字符
    • BL=前景色

因为并未进入Loader程序的开发,所以就让程序死循环在Label_File_Loaded这里。

剩下的部分

代码剩下的部分就是临时变量和日志信息字符串了:

; ==================临时变量==================

RootDirSizeForLoop	dw	RootDirSectors
SectorNo		dw	0
Odd			db	0

; ==================打印信息==================

StartBootMessage:	db	"Start Boot"
NoLoaderMessage:	db	"ERROR:No LOADER Found"
LoaderFileName:		db	"LOADER  BIN",0  ; loader文件名

注: NASM编译器中的单引号和双引号作用相同,并非C语言规定的双引号会在结尾添加字符'\0',在NASM中必须自行添加。

再有就是,文件需要填满512个字节(一个扇区大小),和末尾的0xaa55

; 扇区剩下的字节全部填满0(除了最后两个字节)
    times 510 - ($ - $$) db 0
    dw 0xaa55

总体代码

所以总的代码看起来应该是这样的:

; File name: boot.asm
; org 用于指定程序的起始地址
; 如果不使用org指定程序的起始地址
; 那么编译器会把0x0000作为程序的起始地址。
org 0x7c00

; 设置栈指针的位置
BaseOfStack equ 0x7c00

; Loader的位置(20位)
; BaseOfLoader << 4 + OffsetOfLoader = 0x10000
BaseOfLoader	equ	0x1000
OffsetOfLoader	equ	0x00

; 根目录占用的扇区数:(BPB_RootEntCnt * 32 + BPB_BytesPerSec - 1) / BPB_BytesPerSec
; (224 * 32 + 512 - 1) / 512 = 14
RootDirSectors	equ	14
; 根目录的起始扇区号 = 
; 保留扇区数(BPB_RsvdSecCnt) + FAT表扇区数(BPB_FATSz16) + FAT表份数(BPB_NumFATs) = 1 + 9 * 2 = 19
SectorNumOfRootDirStart	equ	19
; FAT表的起始扇区号,引导扇区的扇区号是0
SectorNumOfFAT1Start	equ	1
; 用于平衡文件(或目录)的起始簇号与数据区起始簇号的差值。
SectorBalance	equ	17	

;****************************************************
        ; 真正的代码在这开始,前面的都相当于宏定义
        ; 程序一开始就跳转到Label_Start的位置处执行
        jmp	short Label_Start
        nop
;****************************************************
        ; 生产厂商名(8 Bits)
        BS_OEMName	db	'Testboot'
        ; 每扇区字节数
        BPB_BytesPerSec	dw	512
        ; 每簇扇区数
        BPB_SecPerClus	db	1
        ; 保留扇区数
        BPB_RsvdSecCnt	dw	1
        ; FAT表的份数
        BPB_NumFATs	db	2
        ; 根目录可容纳的的目录项数
        BPB_RootEntCnt	dw	224
        ; 总扇区数(因为使用的是1.44M的软盘)
        BPB_TotSec16	dw	2880
        ; 介质描述符
        BPB_Media	db	0xf0
        ; 每FAT扇区数
        BPB_FATSz16	dw	9
        ; 每磁道扇区数
        BPB_SecPerTrk	dw	18
        ; 磁头数
        BPB_NumHeads	dw	2
        ; 隐藏扇区数
        BPB_HiddSec	dd	0
        ; 如果BPB_FATSz16=0,那么这个值记录扇区数
        BPB_TotSec32	dd	0
        ; int 0x13的驱动器号
        BS_DrvNum	db	0
        ; 未使用(不用管)
        BS_Reserved1	db	0
        ; 扩展引导标记
        BS_BootSig	db	0x29
        ; 卷序列号
        BS_VolID	dd	0
        ; 卷标(11 Bits)
        BS_VolLab	db	'boot loader'
        ; 文件系统类型(8 Bits)
        BS_FileSysType	db	'FAT12   '

Label_Start:
        ; 初始化一些段寄存器和栈指针
        mov ax, cs
        mov ds, ax
        mov es, ax
        mov ss, ax
        mov sp, BaseOfStack

        ; 清屏
        mov ax, 0600h
        mov bx, 0700h
        mov cx, 0
        mov dx, 184fh
        int 10h     ; 调用0x10号中断

        ; 设置光标位置
        mov ax, 0200h
        mov bx, 0000h
        mov dx, 0000h
        int 10h
    
        ; 打印字符:Start Bootint......
        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

        ; 初始化软盘驱动器
        xor ah, ah
        xor dl, dl
        int 13h

; ================查找文件loader.bin================
        ;保存根目录的起始扇区号([SectorNo]是一个临时变量地址)
        ; 赋值为:SectorNumOfRootDirStart(根目录的起始扇区号)
        ; 初始值为:19(前面已算出)
        mov	word	[SectorNo],	SectorNumOfRootDirStart

Lable_Search_In_Root_Dir_Begin:

        ; RootDirSizeForLoop也是一个临时变量,值是:RootDirSectors(14)
        ; 判断是否为0,如果是提示找不到loader.bin
        cmp	word	[RootDirSizeForLoop],	0
        jz	Label_No_LoaderBin
        ; 查找的扇区减1
        dec	word	[RootDirSizeForLoop]
    
        ;设置读扇区模块的参数 EX:BX、AX、CL
        ; 对应缓冲区起始、待读取的磁盘起始扇区号、读入扇区数量
        mov	ax,	00h
        mov	es,	ax
        mov	bx,	8000h
        mov	ax,	[SectorNo]
        mov	cl,	1
        ; 调用读扇区模块
        call	Func_ReadOneSector
        ; 开始查找loader.bin
        mov	si,	LoaderFileName  ;文件名源地址
        mov	di,	8000h           ;文件名目标地址
        cld     ;DF复位=0,往高地址递进
        ; dx记录每个扇区可容纳的目录项目个数
        mov	dx,	10h
	
Label_Search_For_LoaderBin:

        ;如果dx为0,说明比较完了,开始读取下一个根目录扇区
        cmp	dx,	0
        jz	Label_Goto_Next_Sector_In_Root_Dir

        ;否则dx减1(目录项减1)
        dec	dx
        ; cx保存目录项文件名长度 11 bits
        mov	cx,	11

Label_Cmp_FileName:

        ; 判断文件名是否比较完
        ; 即是cx是否为0,能比较完的话,说明已经找到了
        cmp	cx,	0
        jz	Label_FileName_Found

        ;否则没有比较完,cx -= 1,开始比较下一个字符
        dec	cx
        ; 从DS:SI读取一个字节到寄存器AL,然后SI += 1(取决于DF)
        lodsb
        ; 比较目标字串中对应的字符与al中的字符,看是否相等
        cmp	al,	byte	[es:di]
        ; 如果相等,继续比较下一个字符(跳转到Label_Go_On)
        jz	Label_Go_On
        ; 如果不相等,就下一个文件项
        jmp	Label_Different

Label_Go_On:
	
  ; 目标字串指针加一,然后继续比较
	inc	di
	jmp	Label_Cmp_FileName

Label_Different:

        ; di的低5位清零,
        and	di,	0ffe0h
        ; di += 32,表示指向下一个目录项
        add	di,	20h
        ; si指向LoaderFileName的起始地址
        mov	si,	LoaderFileName
        ; 继续比较
        jmp	Label_Search_For_LoaderBin

Label_Goto_Next_Sector_In_Root_Dir:
	
        ; 在当前扇区找不到loader.bin
        ; 那么就在下一个扇区找,扇区号+1:[SectorNo]+1
        add	word	[SectorNo],	1
        jmp	Lable_Search_In_Root_Dir_Begin


; ========= 打印屏幕信息 : ERROR:No LOADER Found =========

Label_No_LoaderBin:

        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	$
; =======================================================

; =======================================================
; 在根目录中找到名为loader.bin的文件了
Label_FileName_Found:
        ;如果找到了的话,当前es:di指向的就是目标表项
        ; AX保存根目录占用扇区数
        mov	ax,	RootDirSectors
        ; 定位至表项起始簇号的位置(偏移0x1a)
        and	di,	0ffe0h
        add	di,	01ah
        ; 起始簇号保存到cx
        mov	cx,	word	[es:di]
        push	cx
        ; 计算数据区起始扇区号
        ; 数据区起始扇区号=根目录起始扇区号+根目录所占扇区数-2
        add	cx,	ax
        ; SectorBalance = 根目录起始扇区号 - 2
        add	cx,	SectorBalance
        ; 配置loader的加载位置 EX:BX
        mov	ax,	BaseOfLoader
        mov	es,	ax
        mov	bx,	OffsetOfLoader
        ; ax = loader起始扇区号
        mov	ax,	cx

Label_Go_On_Loading_File:
        ; 显示字符 '.'
        push	ax
        push	bx
        mov	ah,	0eh
        mov	al,	'.'
        mov	bl,	0fh
        int	10h
        pop	bx
        pop	ax

        ; 读入loader
        ; 每读入一个扇区就通过Func_GetFATEntry模块获取下一个FAT表项
        ; 然后继续读入数据
        mov	cl,	1
        call	Func_ReadOneSector
        ; 恢复簇号
        pop	ax
        ; 获取下一个簇号
        call	Func_GetFATEntry
        ; 查看是否已经读完了
        cmp	ax,	0fffh
        jz	Label_File_Loaded
        ; 否则保存当前获取到簇号
        push	ax
        ; 计算loader文件的下一个簇的位置
        mov	dx,	RootDirSectors
        add	ax,	dx
        add	ax,	SectorBalance
        add	bx,	[BPB_BytesPerSec]
        ; 继续读
        jmp	Label_Go_On_Loading_File

Label_File_Loaded:
	
    jmp $
; =======================================================

;==============================
; 模块(可以理解为C语言中的函数)
; 从软驱中读取扇区
; 参数说明:
;       AX = 待读取的磁盘起始扇区号
;       CL= 读入的扇区数量
;       EX : BX => 数据缓冲区起始地址
Func_ReadOneSector:
	
        push	bp
        mov	bp,	sp
        sub	esp,	2
        mov	byte	[bp - 2],	cl
        push	bx
        mov	bl,	[BPB_SecPerTrk]
        div	bl
        inc	ah
        mov	cl,	ah
        mov	dh,	al
        shr	al,	1
        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语言中的函数)
; 根据当前FAT表项索引出下一个FAT表项
; 参数说明:
;       AX = FAT表项号(输入/输出参数)
Func_GetFATEntry:

        push	es
        push	bx
        push	ax
        mov	ax,	00
        mov	es,	ax
        pop	ax
        ; Odd为奇偶标志变量(奇数1,偶数0)
        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:

        ; 读取FAT表扇区,总共两个扇区
        xor	dx,	dx
        mov	bx,	[BPB_BytesPerSec]
        div	bx
        push	dx
        mov	bx,	8000h
        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,	0fffh
        pop	bx
        pop	es
        ret
;==============================


; ==================临时变量==================

RootDirSizeForLoop	dw	RootDirSectors
SectorNo		dw	0
Odd			db	0

; ==================打印信息==================

StartBootMessage:	db	"Start Boot"
NoLoaderMessage:	db	"ERROR:No LOADER Found"
LoaderFileName:		db	"LOADER  BIN",0  ; loader文件名


; 扇区剩下的字节全部填满0(除了最后两个字节)
        times 510 - ($ - $$) db 0
        dw 0xaa55

编译运行:

(需要配置环境,之前那篇文章已经配置过了,这里不再重复)
大概的执行步骤是这样的:

  1. 把上面的代码保存为boot.asm

  2. 使用NAME编译它:

> nasm ./boot.asm -o boot.bin
  1. 使用dd把内容写进boot.img中去:
> dd if=boot.bin of=boot.img bs=512 count=1

使用VSCode的Hexdump扩展查看是否已经写入进去:

  1. 启动虚拟机,因为之前已经配置过环境,所以Bochs虚拟机在弹出窗口后,直接点Start按钮或者回车就行了。
> bochs -f .\bochsrc.bxrc
  1. 查看结果:

因为还没有编写loader.bin所以,结果会提示如上图所示(红色部分应该是闪烁的,但是由于是图片,无法展示)。

那么对于读取Loader文件的功能模块要等到编写好Loader文件才能再进行测试了。




  • 参考文献:
    [1] 田宇.一个64位操作系统的设计与实现[M].北京:人民邮电出版社,2018.5.


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM