21 FAT16 文件系统实战——抛弃软盘,从硬盘启动

本来是想写应用程序的,但是吧,这个问题吧,它这个这个,略微有一些难度,知道吧,所以说先挑比较简单的写。

本节我们要重温第 1-6 节的恐惧,用一节的时间速通一个在硬盘上的引导加载器,然后就可以抛掉现在这个不伦不类的软盘启动,硬盘放数据的框架了。

引导扇区比较好改,先从引导扇区开始吧。和软盘版的引导扇区相比,主要要修改的部分有以下几点:

  • ReadSector 要从读取软盘改成读取硬盘。
  • 硬盘使用 FAT16 文件系统,所以 GetFATEntry 需要同步修改。
  • 还是因为 FAT16 文件系统,主循环中判断文件是否结束的条件也要略作修改。

其余的部分均可保持不变。

我们先进入项目根目录,然后新建 boot.asmloader.asmloader.asm 我们仍旧选择使用第 3-4 节使用的白板 Loader:

代码 21-1 白板 Loader(loader.asm)

    org 0100h

    mov ax, 0B800h
    mov gs, ax ; 将gs设置为0xB800,即文本模式下的显存地址
    mov ah, 0Fh ; 显示属性,此处指白色
    mov al, 'L' ; 待显示的字符
    mov [gs:((80 * 0 + 39) * 2)], ax ; 直接写入显存

    jmp $ ; 卡死在此处

将白板 Loader 用 ftcopy 命令写入硬盘:

(图 21-1 具体命令)

boot.asm 中粘贴原先软盘版的 boot.asm 的所有内容,并将 load.incpm.inc 一并复制到根目录,然后就可以开始修改了。

首先来修改 boot.asm 中获取 FAT 项的部分:

代码 21-2 硬盘版 GetFATEntry(boot.asm)

GetFATEntry: ; 返回第ax个簇的值
    push es
    push bx
    push ax ; 都会用到,push一下
    mov ax, BaseOfLoader
    sub ax, 0100h
    mov es, ax
    pop ax
    mov bx, 2
    mul bx ; 每一个FAT项是两字节,给ax乘2就是偏移
LABEL_GET_FAT_ENTRY:
    ; 将ax变为扇区号
    xor dx, dx
    mov bx, [BPB_BytsPerSec]
    div bx ; dx = ax % 512, ax /= 512
    push dx ; 保存dx的值
    mov bx, 0 ; es:bx已指定
    add ax, SectorNoOfFAT1 ; 对应扇区号
    mov cl, 1 ; 一次读一个扇区即可
    call ReadSector ; 直接读入
    ; bx 到 bx + 512 处为读进扇区
    pop dx
    add bx, dx ; 加上偏移
    mov ax, [es:bx] ; 读取,那么这里就是了
LABEL_GET_FAT_ENTRY_OK: ; 胜利执行
    pop bx
    pop es ; 恢复堆栈
    ret

修改的部分主要有:乘1.5的部分变成了乘2;读取的扇区数由两个降到一个;删掉了 FAT12 时期对 FAT 解压缩的处理。

读取扇区的部分则直接仿着四节前的那个硬盘驱动写就行了:

代码 21-3 硬盘版 ReadSector(boot.asm)

ReadSector: ; 读硬盘扇区
; 从第eax号扇区开始,读取cl个扇区至es:bx
    push esi
    push di
    push es
    push bx
    mov esi, eax
    mov di, cx ; 备份ax,cx

; 读硬盘 第一步:设置要读取扇区数
    mov dx, 0x1f2
    mov al, cl
    out dx, al

    mov eax, esi ; 恢复ax

; 第二步:写入扇区号
    mov dx, 0x1f3
    out dx, al ; LBA 7~0位,写入0x1f3

    mov cl, 8
    shr eax, cl ; LBA 15~8位,写入0x1f4
    mov dx, 0x1f4
    out dx, al

    shr eax, cl
    mov dx, 0x1f5
    out dx, al ; LBA 23~16位,写入0x1f5

    shr eax, cl
    and al, 0x0f ; LBA 27~24位
    or al, 0xe0 ; 表示当前硬盘
    mov dx, 0x1f6 ; 写入0x1f6
    out dx, al

; 第三步:0x1f7写入0x20,表示读
    mov dx, 0x1f7 
    mov al, 0x20
    out dx, al

; 第四步:检测硬盘状态
.not_ready:
    nop
    in al, dx ; 读入硬盘状态
    and al, 0x88 ; 分离第4位,第7位
    cmp al, 0x08 ; 硬盘不忙且已准备好
    jnz .not_ready ; 不满足,继续等待

; 第五步:将数据从0x1f0端口读出
    mov ax, di ; di为要读扇区数,共需读di * 512 / 2次
    mov dx, 256
    mul dx
    mov cx, ax

    mov dx, 0x1f0
.go_on_read:
    in ax, dx
    mov [es:bx], ax
    add bx, 2
    loop .go_on_read
; 结束
    pop bx
    pop es
    pop di
    pop esi
    ret

这里需要注意,ReadSector 调用前后会修改 bxdiesi,如果自己写的话要注意备份。

由于换了 FAT16,boot.asm 开头的 %include "fat12hdr.inc" 也要同步更换为 %include "fat16hdr.inc",这里面的内容对照着格式化函数和 file.h 很容易写出:

代码 21-4 FAT16 相关常量(fat16hdr.inc)

    BS_OEMName     db 'tutorial'    ; 固定的8个字节
    BPB_BytsPerSec dw 512           ; 每扇区固定512个字节
    BPB_SecPerClus db 1             ; 每簇固定1个扇区
    BPB_RsvdSecCnt dw 1             ; MBR固定占用1个扇区
    BPB_NumFATs    db 2             ; 我们实现的FAT16文件系统有2个FAT表
    BPB_RootEntCnt dw 512           ; 根目录区32个扇区,一个目录项32字节,共计32*512/32=512个目录项
    BPB_TotSec16   dw 0             ; 80MB硬盘的大小过大,不足以放到TotSec16
    BPB_Media      db 0xF8          ; 介质描述符,硬盘为0xF8
    BPB_FATSz16    dw 32            ; 一个FAT表所占的扇区数,FAT16 文件系统固定为32个扇区
    BPB_SecPerTrk  dw 63            ; 每磁道扇区数,80MB硬盘为63
    BPB_NumHeads   dw 16            ; 磁头数,bximage 的输出告诉我们是16个
    BPB_HiddSec    dd 0             ; 隐藏扇区数,没有
    BPB_TotSec32   dd 41943040      ; 若之前的 BPB_TotSec16 处没有记录扇区数,则由此记录,如果记录了,这里直接置0即可
    BS_DrvNum      db 0x80          ; int 13h 调用时所读取的驱动器号,由于挂载的是硬盘所以0x80 
    BS_Reserved1   db 0             ; 未使用,预留
    BS_BootSig     db 29h           ; 扩展引导标记
    BS_VolID       dd 0             ; 卷序列号,由于只挂载一个盘所以为0
    BS_VolLab      db 'OS-tutorial' ; 卷标,11个字节
    BS_FileSysType db 'FAT16   '    ; 由于是 FAT16 文件系统,所以写入 FAT16 后补齐8个字节

FATSz                   equ 32      ; BPB_FATSz16
RootDirSectors          equ 32      ; 根目录大小
SectorNoOfRootDirectory equ 65      ; 根目录起始扇区
SectorNoOfFAT1          equ 1       ; 第一个FAT表的开始扇区
DeltaSectorNo           equ 63      ; 由于第一个簇不用,所以RootDirSectors要-2再加上根目录区首扇区和偏移才能得到真正的地址,故把RootDirSectors-2封装成一个常量

最后一处修改是在主循环的 LABEL_GOON_LOADING_FILE 附近:

代码 21-5 硬盘版主循环(boot.asm)

    cmp ax, 0FFFFh ; 这里!原本是0FFF,但FAT16的文件结束时FFFF,所以这里要修改
    jz LABEL_FILE_LOADED ; 若此项=0FFFF,代表文件结束,直接跳入Loader
    push ax ; 重新存储FAT号,但此时的FAT号已经是下一个FAT了

至此,硬盘版引导扇区修改完成,完整代码如下:

代码 21-6 硬盘引导扇区-完整版(boot.asm)

    org 07c00h ; 告诉编译器程序将装载至0x7c00处

BaseOfStack             equ 07c00h ; 栈的基址

    jmp short LABEL_START
    nop ; BS_JMPBoot 由于要三个字节而jmp到LABEL_START只有两个字节 所以加一个nop

%include "fat16hdr.inc" ; 没错它会db一遍
%include "load.inc" ; 代替之前的常量

LABEL_START:
    mov ax, cs
    mov ds, ax
    mov es, ax ; 将ds es设置为cs的值(因为此时字符串和变量等存在代码段内)
    mov ss, ax ; 将堆栈段也初始化至cs
    mov sp, BaseOfStack ; 设置栈顶

    mov ax, 0600h ; AH=06h:向上滚屏,AL=00h:清空窗口
    mov bx, 0700h ; 空白区域缺省属性
    mov cx, 0 ; 左上:(0, 0)
    mov dx, 0184fh ; 右下:(80, 25)
    int 10h ; 执行

    mov dh, 0
    call DispStr ; Booting

    xor ah, ah ; 复位
    xor dl, dl
    int 13h ; 执行软驱复位

    mov word [wSectorNo], SectorNoOfRootDirectory ; 开始查找,将当前读到的扇区数记为根目录区的开始扇区(19)
LABEL_SEARCH_IN_ROOT_DIR_BEGIN:
    cmp word [wRootDirSizeForLoop], 0 ; 将剩余的根目录区扇区数与0比较
    jz LABEL_NO_LOADERBIN ; 相等,不存在Loader,进行善后
    dec word [wRootDirSizeForLoop] ; 减去一个扇区
    mov ax, BaseOfLoader
    mov es, ax
    mov bx, OffsetOfLoader ; 将es:bx设置为BaseOfLoader:OffsetOfLoader,暂且使用Loader所占的内存空间存放根目录区
    mov ax, [wSectorNo] ; 起始扇区:当前读到的扇区数(废话)
    mov cl, 1 ; 读取一个扇区
    call ReadSector ; 读入

    mov si, LoaderFileName ; 为比对做准备,此处是将ds:si设为Loader文件名
    mov di, OffsetOfLoader ; 为比对做准备,此处是将es:di设为Loader偏移量(即根目录区中的首个文件块)
    cld ; FLAGS.DF=0,即执行lodsb/lodsw/lodsd后,si自动增加
    mov dx, 10h ; 共16个文件块(代表一个扇区,因为一个文件块32字节,16个文件块正好一个扇区)
LABEL_SEARCH_FOR_LOADERBIN:
    cmp dx, 0 ; 将dx与0比较
    jz LABEL_GOTO_NEXT_SECTOR_IN_ROOT_DIR ; 继续前进一个扇区
    dec dx ; 否则将dx减1
    mov cx, 11 ; 文件名共11字节
LABEL_CMP_FILENAME: ; 比对文件名
    cmp cx, 0 ; 将cx与0比较
    jz LABEL_FILENAME_FOUND ; 若相等,说明文件名完全一致,表示找到,进行找到后的处理
    dec cx ; cx减1,表示读取1个字符
    lodsb ; 将ds:si的内容置入al,si加1
    cmp al, byte [es:di] ; 此字符与LOADER  BIN中的当前字符相等吗?
    jz LABEL_GO_ON ; 下一个文件名字符
    jmp LABEL_DIFFERENT ; 下一个文件块
LABEL_GO_ON:
    inc di ; di加1,即下一个字符
    jmp LABEL_CMP_FILENAME ; 继续比较

LABEL_DIFFERENT:
    and di, 0FFE0h ; 指向该文件块开头
    add di, 20h ; 跳过32字节,即指向下一个文件块开头
    mov si, LoaderFileName ; 重置ds:si
    jmp LABEL_SEARCH_FOR_LOADERBIN ; 由于要重新设置一些东西,所以回到查找Loader循环的开头

LABEL_GOTO_NEXT_SECTOR_IN_ROOT_DIR:
    add word [wSectorNo], 1 ; 下一个扇区
    jmp LABEL_SEARCH_IN_ROOT_DIR_BEGIN ; 重新执行主循环

LABEL_NO_LOADERBIN: ; 若找不到loader.bin则到这里
    mov dh, 2
    call DispStr; 显示No LOADER
    jmp $

LABEL_FILENAME_FOUND:
    mov ax, RootDirSectors ; 将ax置为根目录首扇区(19)
    and di, 0FFE0h ; 将di设置到此文件块开头
    add di, 01Ah ; 此时的di指向Loader的FAT号
    mov cx, word [es:di] ; 获得该扇区的FAT号
    push cx ; 将FAT号暂存
    add cx, ax ; +根目录首扇区
    add cx, DeltaSectorNo ; 获得真正的地址
    mov ax, BaseOfLoader
    mov es, ax
    mov bx, OffsetOfLoader ; es:bx:读取扇区的缓冲区地址
    mov ax, cx ; ax:起始扇区号

LABEL_GOON_LOADING_FILE: ; 加载文件
    push ax
    push bx
    mov ah, 0Eh ; AH=0Eh:显示单个字符
    mov al, '.' ; AL:字符内容
    mov bl, 0Fh ; BL:显示属性
; 还有BH:页码,此处不管
    int 10h ; 显示此字符
    pop bx
    pop ax ; 上面几行的整体作用:在屏幕上打印一个点

    mov cl, 1
    call ReadSector ; 读取Loader第一个扇区
    pop ax ; 加载FAT号
    call GetFATEntry ; 加载FAT项
    cmp ax, 0FFFFh
    jz LABEL_FILE_LOADED ; 若此项=0FFF,代表文件结束,直接跳入Loader
    push ax ; 重新存储FAT号,但此时的FAT号已经是下一个FAT了
    mov dx, RootDirSectors
    add ax, dx ; +根目录首扇区
    add ax, DeltaSectorNo ; 获取真实地址
    add bx, [BPB_BytsPerSec] ; 将bx指向下一个扇区开头
    jmp LABEL_GOON_LOADING_FILE ; 加载下一个扇区

LABEL_FILE_LOADED:
    mov dh, 1 ; 打印第 1 条消息(Ready.)
    call DispStr
    jmp BaseOfLoader:OffsetOfLoader ; 跳入Loader!

wRootDirSizeForLoop dw RootDirSectors ; 查找loader的循环中将会用到
wSectorNo           dw 0              ; 用于保存当前扇区数
bOdd                db 0              ; 这个其实是下一节的东西,不过先放在这也不是不行

LoaderFileName      db "LOADER  BIN", 0 ; loader的文件名

MessageLength       equ 9 ; 下面是三条小消息,此变量用于保存其长度,事实上在内存中它们的排序类似于二维数组
BootMessage:        db "Booting  " ; 此处定义之后就可以删除原先定义的BootMessage字符串了
Message1            db "Ready.   " ; 显示已准备好
Message2            db "No LOADER" ; 显示没有Loader

DispStr:
    mov ax, MessageLength
    mul dh ; 将ax乘以dh后,结果仍置入ax(事实上远比此复杂,此处先解释到这里)
    add ax, BootMessage ; 找到给定的消息
    mov bp, ax ; 先给定偏移
    mov ax, ds
    mov es, ax ; 以防万一,重新设置es
    mov cx, MessageLength ; 字符串长度
    mov ax, 01301h ; ah=13h, 显示字符的同时光标移位
    mov bx, 0007h ; 黑底白字
    mov dl, 0 ; 第0行,前面指定的dh不变,所以给定第几条消息就打印到第几行
    int 10h ; 显示字符
    ret

ReadSector: ; 读硬盘扇区
; 从第eax号扇区开始,读取cl个扇区至es:bx
    push esi
    push di
    push es
    push bx
    mov esi, eax
    mov di, cx ; 备份ax,cx

; 读硬盘 第一步:设置要读取扇区数
    mov dx, 0x1f2
    mov al, cl
    out dx, al

    mov eax, esi ; 恢复ax

; 第二步:写入扇区号
    mov dx, 0x1f3
    out dx, al ; LBA 7~0位,写入0x1f3

    mov cl, 8
    shr eax, cl ; LBA 15~8位,写入0x1f4
    mov dx, 0x1f4
    out dx, al

    shr eax, cl
    mov dx, 0x1f5
    out dx, al ; LBA 23~16位,写入0x1f5

    shr eax, cl
    and al, 0x0f ; LBA 27~24位
    or al, 0xe0 ; 表示当前硬盘
    mov dx, 0x1f6 ; 写入0x1f6
    out dx, al

; 第三步:0x1f7写入0x20,表示读
    mov dx, 0x1f7 
    mov al, 0x20
    out dx, al

; 第四步:检测硬盘状态
.not_ready:
    nop
    in al, dx ; 读入硬盘状态
    and al, 0x88 ; 分离第4位,第7位
    cmp al, 0x08 ; 硬盘不忙且已准备好
    jnz .not_ready ; 不满足,继续等待

; 第五步:将数据从0x1f0端口读出
    mov ax, di ; di为要读扇区数,共需读di * 512 / 2次
    mov dx, 256
    mul dx
    mov cx, ax

    mov dx, 0x1f0
.go_on_read:
    in ax, dx
    mov [es:bx], ax
    add bx, 2
    loop .go_on_read
; 结束
    pop bx
    pop es
    pop di
    pop esi
    ret

GetFATEntry: ; 返回第ax个簇的值
    push es
    push bx
    push ax ; 都会用到,push一下
    mov ax, BaseOfLoader
    sub ax, 0100h
    mov es, ax
    pop ax
    mov bx, 2
    mul bx ; 每一个FAT项是两字节,给ax乘2就是偏移
LABEL_GET_FAT_ENTRY:
    ; 将ax变为扇区号
    xor dx, dx
    mov bx, [BPB_BytsPerSec]
    div bx ; dx = ax % 512, ax /= 512
    push dx ; 保存dx的值
    mov bx, 0 ; es:bx已指定
    add ax, SectorNoOfFAT1 ; 对应扇区号
    mov cl, 1 ; 一次读一个扇区即可
    call ReadSector ; 直接读入
    ; bx 到 bx + 512 处为读进扇区
    pop dx
    add bx, dx ; 加上偏移
    mov ax, [es:bx] ; 读取,那么这里就是了
LABEL_GET_FAT_ENTRY_OK: ; 胜利执行
    pop bx
    pop es ; 恢复堆栈
    ret

times 510 - ($ - $$) db 0
db 0x55, 0xaa ; 确保最后两个字节是0x55AA

编译,运行,命令如下:

(图 21-2 编译运行命令)

效果如下:

(图 21-3 白色的 L,很熟悉对吧)

对于 Loader,在进行完上述修改以后,把 LABEL_FILE_LOADED 中的 call KillMotor 以及 KillMotor 函数一并删除即可,这里不多赘述,贴一遍完整代码:

代码 21-7 硬盘版 Loader-完整版(loader.asm)

    org 0100h ; 告诉编译器程序将装载至0x100处

BaseOfStack                 equ 0100h ; 栈的基址

    jmp LABEL_START

%include "fat16hdr.inc" ; 没错它会再db一遍
%include "load.inc" ; 代替之前的常量
%include "pm.inc" ; 保护模式相关

; GDT
LABEL_GDT:          Descriptor 0,            0, 0                            ; 占位用描述符
LABEL_DESC_FLAT_C:  Descriptor 0,      0fffffh, DA_C | DA_32 | DA_LIMIT_4K   ; 32位代码段,平坦内存
LABEL_DESC_FLAT_RW: Descriptor 0,      0fffffh, DA_DRW | DA_32 | DA_LIMIT_4K ; 32位数据段,平坦内存
LABEL_DESC_VIDEO:   Descriptor 0B8000h, 0ffffh, DA_DRW | DA_DPL3             ; 文本模式显存,后面用不到了

GdtLen equ $ - LABEL_GDT                                                    ; GDT的长度
GdtPtr dw GdtLen - 1                                                        ; gdtr寄存器,先放置长度
       dd BaseOfLoaderPhyAddr + LABEL_GDT                                   ; 保护模式使用线性地址,因此需要加上程序装载位置的物理地址(BaseOfLoaderPhyAddr)

SelectorFlatC       equ LABEL_DESC_FLAT_C  - LABEL_GDT                      ; 代码段选择子
SelectorFlatRW      equ LABEL_DESC_FLAT_RW - LABEL_GDT                      ; 数据段选择子
SelectorVideo       equ LABEL_DESC_VIDEO   - LABEL_GDT + SA_RPL3            ; 文本模式显存选择子

LABEL_START:
    mov ax, cs
    mov ds, ax
    mov es, ax ; 将ds es设置为cs的值(因为此时字符串和变量等存在代码段内)
    mov ss, ax ; 将堆栈段也初始化至cs
    mov sp, BaseOfStack ; 设置栈顶

    mov dh, 0
    call DispStr ; Loading

    mov word [wSectorNo], SectorNoOfRootDirectory ; 开始查找,将当前读到的扇区数记为根目录区的开始扇区(19)
    xor ah, ah ; 复位
    xor dl, dl
    int 13h ; 执行软驱复位
LABEL_SEARCH_IN_ROOT_DIR_BEGIN:
    cmp word [wRootDirSizeForLoop], 0 ; 将剩余的根目录区扇区数与0比较
    jz LABEL_NO_KERNELBIN ; 相等,不存在Kernel,进行善后
    dec word [wRootDirSizeForLoop] ; 减去一个扇区
    mov ax, BaseOfKernelFile
    mov es, ax
    mov bx, OffsetOfKernelFile ; 将es:bx设置为BaseOfKernel:OffsetOfKernel,暂且使用Kernel所占的内存空间存放根目录区
    mov ax, [wSectorNo] ; 起始扇区:当前读到的扇区数(废话)
    mov cl, 1 ; 读取一个扇区
    call ReadSector ; 读入

    mov si, KernelFileName ; 为比对做准备,此处是将ds:si设为Kernel文件名
    mov di, OffsetOfKernelFile ; 为比对做准备,此处是将es:di设为Kernel偏移量(即根目录区中的首个文件块)
    cld ; FLAGS.DF=0,即执行lodsb/lodsw/lodsd后,si自动增加
    mov dx, 10h ; 共16个文件块(代表一个扇区,因为一个文件块32字节,16个文件块正好一个扇区)
LABEL_SEARCH_FOR_KERNELBIN:
    cmp dx, 0 ; 将dx与0比较
    jz LABEL_GOTO_NEXT_SECTOR_IN_ROOT_DIR ; 继续前进一个扇区
    dec dx ; 否则将dx减1
    mov cx, 11 ; 文件名共11字节
LABEL_CMP_FILENAME: ; 比对文件名
    cmp cx, 0 ; 将cx与0比较
    jz LABEL_FILENAME_FOUND ; 若相等,说明文件名完全一致,表示找到,进行找到后的处理
    dec cx ; cx减1,表示读取1个字符
    lodsb ; 将ds:si的内容置入al,si加1
    cmp al, byte [es:di] ; 此字符与KERNEL  BIN中的当前字符相等吗?
    jz LABEL_GO_ON ; 下一个文件名字符
    jmp LABEL_DIFFERENT ; 下一个文件块
LABEL_GO_ON:
    inc di ; di加1,即下一个字符
    jmp LABEL_CMP_FILENAME ; 继续比较

LABEL_DIFFERENT:
    and di, 0FFE0h ; 指向该文件块开头
    add di, 20h ; 跳过32字节,即指向下一个文件块开头
    mov si, KernelFileName ; 重置ds:si
    jmp LABEL_SEARCH_FOR_KERNELBIN ; 由于要重新设置一些东西,所以回到查找Kernel循环的开头

LABEL_GOTO_NEXT_SECTOR_IN_ROOT_DIR:
    add word [wSectorNo], 1 ; 下一个扇区
    jmp LABEL_SEARCH_IN_ROOT_DIR_BEGIN ; 重新执行主循环

LABEL_NO_KERNELBIN: ; 若找不到kernel.bin则到这里
    mov dh, 2
    call DispStr ; 显示No KERNEL
    jmp $

LABEL_FILENAME_FOUND:
    mov ax, RootDirSectors ; 将ax置为根目录首扇区(19)
    and di, 0FFF0h ; 将di设置到此文件块开头

    push eax
    mov eax, [es:di + 01Ch]
    mov dword [dwKernelSize], eax
    pop eax

    add di, 01Ah ; 此时的di指向Kernel的FAT号
    mov cx, word [es:di] ; 获得该扇区的FAT号
    push cx ; 将FAT号暂存
    add cx, ax ; +根目录首扇区
    add cx, DeltaSectorNo ; 获得真正的地址
    mov ax, BaseOfKernelFile
    mov es, ax
    mov bx, OffsetOfKernelFile ; es:bx:读取扇区的缓冲区地址
    mov ax, cx ; ax:起始扇区号

LABEL_GOON_LOADING_FILE: ; 加载文件
    push ax
    push bx
    mov ah, 0Eh ; AH=0Eh:显示单个字符
    mov al, '.' ; AL:字符内容
    mov bl, 0Fh ; BL:显示属性
; 还有BH:页码,此处不管
    int 10h ; 显示此字符
    pop bx
    pop ax ; 上面几行的整体作用:在屏幕上打印一个点

    mov cl, 1
    call ReadSector ; 读取Kernel第一个扇区
    pop ax ; 加载FAT号
    call GetFATEntry ; 加载FAT项
    cmp ax, 0FFFFh
    jz LABEL_FILE_LOADED ; 若此项=0FFF,代表文件结束,直接跳入Kernel
    push ax ; 重新存储FAT号,但此时的FAT号已经是下一个FAT了
    mov dx, RootDirSectors
    add ax, dx ; +根目录首扇区
    add ax, DeltaSectorNo ; 获取真实地址
    add bx, [BPB_BytsPerSec] ; 将bx指向下一个扇区开头
    jmp LABEL_GOON_LOADING_FILE ; 加载下一个扇区

LABEL_FILE_LOADED:
    mov dh, 1 ; "Ready."
    call DispStr
; 准备进入保护模式
    lgdt [GdtPtr] ; 加载gdt
    cli ; 关闭中断

    in al, 92h ; 开启A20地址线
    or al, 00000010b
    out 92h, al

    mov eax, cr0
    or eax, 1 ; CR0.PE=1,进入保护模式
    mov cr0, eax

    jmp dword SelectorFlatC:(BaseOfLoaderPhyAddr + LABEL_PM_START) ; 进入32位段,彻底进入保护模式

dwKernelSize        dd 0              ; Kernel大小
wRootDirSizeForLoop dw RootDirSectors ; 查找Kernel的循环中将会用到
wSectorNo           dw 0              ; 用于保存当前扇区数
bOdd                db 0              ; 这个其实是下一节的东西,不过先放在这也不是不行

KernelFileName      db "KERNEL  BIN", 0 ; Kernel的文件名

MessageLength       equ 9 ; 下面是三条小消息,此变量用于保存其长度,事实上在内存中它们的排序类似于二维数组
BootMessage:        db "Loading  " ; 此处定义之后就可以删除原先定义的BootMessage字符串了
Message1            db "Ready.   " ; 显示已准备好
Message2            db "No KERNEL" ; 显示没有Kernel

DispStr: ; void DispStr(char idx);
; idx -> dh
; 基于bios功能:
; int 10h : ah=13h, 打印字符串
    mov ax, MessageLength
    mul dh ; 将ax乘以dh后,结果仍置入ax(事实上远比此复杂,此处先解释到这里)
    add ax, BootMessage ; 找到给定的消息
    mov bp, ax ; 先给定偏移
    mov ax, ds
    mov es, ax ; 以防万一,重新设置es
    mov cx, MessageLength ; 字符串长度
    mov ax, 01301h ; ah=13h, 显示字符的同时光标移位
    mov bx, 0007h ; 黑底白字
    mov dl, 0 ; 第0行,前面指定的dh不变,所以给定第几条消息就打印到第几行
    add dh, 3 ; 给dh加3,避免与boot打印的消息重叠
    int 10h ; 显示字符
    ret

ReadSector: ; 读硬盘扇区
; 从第eax号扇区开始,读取cl个扇区至es:bx
    push esi
    push di
    push es
    push bx
    mov esi, eax
    mov di, cx ; 备份ax,cx

; 读硬盘 第一步:设置要读取扇区数
    mov dx, 0x1f2
    mov al, cl
    out dx, al

    mov eax, esi ; 恢复ax

; 第二步:写入扇区号
    mov dx, 0x1f3
    out dx, al ; LBA 7~0位,写入0x1f3

    mov cl, 8
    shr eax, cl ; LBA 15~8位,写入0x1f4
    mov dx, 0x1f4
    out dx, al

    shr eax, cl
    mov dx, 0x1f5
    out dx, al ; LBA 23~16位,写入0x1f5

    shr eax, cl
    and al, 0x0f ; LBA 27~24位
    or al, 0xe0 ; 表示当前硬盘
    mov dx, 0x1f6 ; 写入0x1f6
    out dx, al

; 第三步:0x1f7写入0x20,表示读
    mov dx, 0x1f7 
    mov al, 0x20
    out dx, al

; 第四步:检测硬盘状态
.not_ready:
    nop
    in al, dx ; 读入硬盘状态
    and al, 0x88 ; 分离第4位,第7位
    cmp al, 0x08 ; 硬盘不忙且已准备好
    jnz .not_ready ; 不满足,继续等待

; 第五步:将数据从0x1f0端口读出
    mov ax, di ; di为要读扇区数,共需读di * 512 / 2次
    mov dx, 256
    mul dx
    mov cx, ax

    mov dx, 0x1f0
.go_on_read:
    in ax, dx
    mov [es:bx], ax
    add bx, 2
    loop .go_on_read
; 结束
    pop bx
    pop es
    pop di
    pop esi
    ret

GetFATEntry: ; 返回第ax个簇的值
    push es
    push bx
    push ax ; 都会用到,push一下
    mov ax, BaseOfLoader
    sub ax, 0100h
    mov es, ax
    pop ax
    mov bx, 2
    mul bx ; 每一个FAT项是两字节,给ax乘2就是偏移
LABEL_GET_FAT_ENTRY:
    ; 将ax变为扇区号
    xor dx, dx
    mov bx, [BPB_BytsPerSec]
    div bx ; dx = ax % 512, ax /= 512
    push dx ; 保存dx的值
    mov bx, 0 ; es:bx已指定
    add ax, SectorNoOfFAT1 ; 对应扇区号
    mov cl, 1 ; 一次读一个扇区即可
    call ReadSector ; 直接读入
    ; bx 到 bx + 512 处为读进扇区
    pop dx
    add bx, dx ; 加上偏移
    mov ax, [es:bx] ; 读取,那么这里就是了
LABEL_GET_FAT_ENTRY_OK: ; 胜利执行
    pop bx
    pop es ; 恢复堆栈
    ret

[section .s32]
align 32
[bits 32]
LABEL_PM_START:
    mov ax, SelectorVideo ; 按照保护模式的规矩来
    mov gs, ax ; 把选择子装入gs

    mov ah, 0Fh
    mov al, 'P'
    mov [gs:((80 * 0 + 39) * 2)], ax ; 这一部分写入显存是通用的

    mov ax, SelectorFlatRW ; 数据段
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov ss, ax
    mov esp, TopOfStack

; cs的设定已在之前的远跳转中完成
    call InitKernel ; 重新放置内核
    jmp SelectorFlatC:KernelEntryPointPhyAddr ; 进入内核,OS征程从这里开始

MemCpy: ; void memcpy(void *dest, const void *src, size_t size);
; ds:参数2 ==> es:参数1,大小:参数3
    push ebp
    mov ebp, esp ; 保存ebp和esp的值

    push esi
    push edi
    push ecx ; 暂存这三个,要用

    mov edi, [ebp + 8] ; [esp + 4] ==> 第一个参数,目标内存区
    mov esi, [ebp + 12] ; [esp + 8] ==> 第二个参数,源内存区
    mov ecx, [ebp + 16] ; [esp + 12] ==> 第三个参数,拷贝的字节大小
.1:
    cmp ecx, 0 ; if (ecx == 0)
    jz .2 ; goto .2;

    mov al, [ds:esi] ; 从源内存区中获取一个值
    inc esi ; 源内存区地址+1
    mov byte [es:edi], al ; 将该值写入目标内存
    inc edi ; 目标内存区地址+1

    dec ecx ; 拷贝字节数大小-1
    jmp .1 ; 重复执行
.2:
    mov eax, [ebp + 8] ; 目标内存区作为返回值

    pop ecx ; 以下代码恢复堆栈
    pop edi
    pop esi
    mov esp, ebp
    pop ebp

    ret

InitKernel: ; void InitKernel();
    xor esi, esi ; esi = 0;
    mov cx, word [BaseOfKernelFilePhyAddr + 2Ch] ; 这个内存地址存放的是ELF头中的e_phnum,即Program Header的个数
    movzx ecx, cx ; ecx高16位置0,低16位置入cx
    mov esi, [BaseOfKernelFilePhyAddr + 1Ch] ; 这个内存地址中存放的是ELF头中的e_phoff,即Program Header表的偏移
    add esi, BaseOfKernelFilePhyAddr ; Program Header表的具体位置
.Begin:
    mov eax, [esi] ; 首先看一下段类型
    cmp eax, 0 ; 段类型:PT_NULL或此处不存在Program Header
    jz .NoAction ; 本轮循环不执行任何操作
    ; 否则的话:
    push dword [esi + 010h] ; p_filesz
    mov eax, [esi + 04h] ; p_offset
    add eax, BaseOfKernelFilePhyAddr ; BaseOfKernelFilePhyAddr + p_offset
    push eax
    push dword [esi + 08h] ; p_vaddr
    call MemCpy ; 执行一次拷贝
    add esp, 12 ; 清理堆栈
.NoAction: ; 本轮循环的清理工作
    add esi, 020h ; 下一个Program Header
    dec ecx
    jnz .Begin ; jz过来的话就直接ret了

    ret

[section .data1]
StackSpace: times 1024 db 0 ; 栈暂且先给1KB
TopOfStack  equ $ - StackSpace ; 栈顶

如今硬盘 bootloader 已成,直接把原本的 boot.asmloader.asm 替换为现在的 boot.asmloader.asm,并用 fat16hdr.inc 替换 fat12hdr.inc 即可。

Makefile 也要进行修改,从此以后不再生成 a.img 了,而是生成 hd.img

代码 21-8 新版 Makefile(Makefile)

hd.img : out/boot.bin out/loader.bin out/kernel.bin
    ftimage hd.img -size 80 -bs out/boot.bin
    ftcopy hd.img -srcpath out/loader.bin -to -dstpath /loader.bin
    ftcopy hd.img -srcpath out/kernel.bin -to -dstpath /kernel.bin

run : hd.img
    qemu-system-i386 -hda hd.img

由于现在每次编译都会重新创建硬盘镜像,所以之前的写入测试文件 iloveado.fai 将不复存在,那就返璞归真,用一行简单的打印证明我们进入了内核吧:

代码 21-9 kernel/main.c

void kernel_main() // kernel.asm会跳转到这里
{
    monitor_clear();
    init_gdtidt();
    init_memory();
    init_timer(100);
    init_keyboard();
    asm("sti");

    task_t *task_a = task_init();
    task_t *task_shell = create_kernel_task(shell);
    //task_run(task_shell);

    printk("Hello, HD Boot!");

    while (1);
}

(shell:请你认真的看一看我……我至今为止有被用过哪怕一次么?)

使用 make default 全部重新编译,运行,效果如下图:

(图 21-4 硬盘启动成功)

终于,在整整21节之后,我们并不是很紧地跟上了时代潮流,将软盘扔进了历史的垃圾堆,事实证明这是颇不具有里程碑意义的一件不是很大的事。

这一节本来是想放在最后写的,耐不住老有人催,所以提前写了,应用程序什么的就放在下一节吧!