引导扇区只有 512 字节,太小,基本啥也干不了,我们只能利用引倒扇区把我们的操作系统内核载入内存。但是一下子把内核全部载入进来也不靠谱,还有很多事情没做呢——起码就还没转到安全模式,内核大的话,实模式可装不下。所以正确的姿势是:引导扇区载入一个装载程序,装载程序负责做好准备工作后,再载入真正的内核。
那么引导扇区的任务就很明确了:找到装载程序(Loader.bin),然后把它载入内存。第十天初识FAT12时提到了:通过根目录表项找到文件的第 1 个扇区,再从 FAT 表中找到其他的扇区。
今天的代码不是完全照书抄了,是按照我的理解重写的,代码里我很详细的注释了我的思路,而且结构也很清晰,子函数功能明确,基本是按照模块化设计的——我是深受 C 语言影响。引导扇区去掉填空的 0,大小只有 340 字节,还是比较满意的。如果有人有心在看的话,希望能提出意见。
; Constant.inc
; 常量
; 四彩
; 2015-11-12
%ifndef _CONSTANT_INC
%define _CONSTANT_INC
; ========================================================================================
; 内存中 0x0500 ~ 0x7BFF(29.75 KB) 段和 0x7E00 ~ 0xFFFF(32.5KB)段、
; 0x10000 ~ 0x9FBFF(575 KB)段可自由使用。引导扇区段在加载完 Loader 也可使用。
;
SEGMENTBASEOFTEMP equ 0x7E0 ; 临时数据被加载到内存的段地址(最多 2 个扇区)
SEGMENTBASEOFLOADER equ 0x4000 ; Loader.SYS 被加载到内存的段地址
STACKSIZE equ 0x400 ; Loader 的堆栈大小
; ****************************************************************************************
%endif
; FAT12.inc
; FAT12 文件系统常量及宏定义
; 四彩
; 2015-11-08
%ifndef _FAT12_INC
%define _FAT12_INC
; ========================================================================================
BYTESPERSECTOR equ 512 ; 每扇区字节数
IFATFIRSTSECTOR equ 1 ; FAT 表的起始逻辑扇区号
IROOTDIRECTORYFIRSTSECTOR equ 19 ; 根目录区的起始逻辑扇区号
IDATAFIRSTSECTOR equ 33 ; 数据区的起始逻辑扇区号
; ****************************************************************************************
; ========================================================================================
; FAT12 文件系统的引导扇区头部格式宏
; 调用格式:FAT12Head Label_RealEntry, OEMName, VolLab
; Label_RealEntry : 程序入口标签
; OEMName : 厂商名称(8 字节长,不够的填空格)
; VolLab : 卷标(11 字节长,不够的填空格)
%macro FAT12Head 3
; 名称 偏移 长度 说明 3.5英寸软盘内容
jmp %1 ; 0x00 3 跳转指令,指向程序入口 jmp RealEntry
nop
BS_OEMName db %2 ; 0x03 8 厂商名称 自行定义
BPB_BytsPerSec dw 512 ; 0x0B 2 每扇区字节数 512
BPB_SecPerClus db 1 ; 0x0D 1 每簇扇区数 1
BPB_RsvdSecCnt dw 1 ; 0x0E 2 保留扇区数 1
BPB_NumFATs db 2 ; 0x10 1 FAT表份数 2
BPB_RootEntCnt dw 224 ; 0x11 2 根目录中最多容纳的文件数 224
BPB_TotSec16 dw 2880 ; 0x13 2 扇区总数 (FAT12、16) 2880
BPB_Media db 0xF0 ; 0x15 1 介质描述符 0xF0
BPB_FATSz16 dw 9 ; 0x16 2 每个FAT表所占的扇区数 9
BPB_SecPerTrk dw 18 ; 0x18 2 每磁道扇区数 18
BPB_NumHeads dw 2 ; 0x1A 2 磁头数 2
BPB_HiddSec dd 0 ; 0x1C 4 隐藏扇区数 0
BPB_TotSec32 dd 2880 ; 0x20 4 扇区总数(FAT32) 2880
BS_DrvNum db 0 ; 0x24 1 磁盘驱动器号 0
BS_Reserved1 db 0 ; 0x25 1 保留(供NT使用) 0
BS_BootSig db 0x29 ; 0x26 1 扩展引导标记 0x29
BS_VolD dd 0 ; 0x27 4 卷标序列号 0
BS_VolLab db %3 ; 0x2B 11 卷标 自行定义
BS_FileSysType db 'FAT12' ; 0x36 8 文件系统类型名 FAT12
; 0x3E 448 引导代码及其他填充字符
; 0x1FE 2 结束标志 0xAA55
;
; BPB:BIOS Parameter Block,BIOS 参数块
; BS:Boot Sector,引导扇区
%endmacro
; ****************************************************************************************
; ========================================================================================
; 目录表项结构
struc DirectoryItem
; 字段名 偏移 长度 说明
.DIR_Name resb 11 ; 0x00 11 文件名 8 + 3(大写,不够长度末尾填空格)
.DIR_Attr resb 1 ; 0x0B 1 文件属性
resb 10 ; 0x0C 10 保留
.DIR_WrtTime resw 1 ; 0x16 2 最后修改时间
.DIR_WrtDate resw 1 ; 0x18 2 最后修改日期
.DIR_FstClus resw 1 ; 0x1A 2 此条目对应的开始簇号(即 FAT 表项序号)
.DIR_FileSize resd 1 ; 0x1C 4 文件大小
endstruc
; ****************************************************************************************
%endif
; BootSector.asm
; 引导扇区
; 四彩
; 2015-11-12
; ========================================================================================
; 电脑的启动过程:
; 1、80x86 CPU 启动后(加电或复位),CS : IP 被设置为 0xFFFF : 0x0,CPU 从此处读取指令
; 开始执行。该单元在基本输入输出系统(Basic Input/Output System,BIOS)的地址范围内,
; 这里是一条跳转到 BIOS 中真正启动代码处的指令。
; 2、BIOS 首先进行加电自检(Power-On Self-Test,POST),然后进行更完整的硬件检测,并加载
; 相关设备。
; 3、接下来按启动顺序(Boot Sequence)读取第一个设备的第一个扇区,如果该扇区最后两个字节
; 是 0x55 和 0xAA,表明这个设备可以用于引导;如果不是,表明这个设备不能用于引导,BIOS
; 继续读取启动顺序中的下一个设备……直到找到启动设备。BIOS 把第一个启动设备的第一个扇区
; 读到内存 0x7C00 处,然后把控制权交给该处。
; 4、操作系统通过改写启动设备的第一个扇区,被读入内存后,从内存 0x7C00 处开始接管电脑。
; ****************************************************************************************
; ========================================================================================
; 头文件及常量定义
; ----------------------------------------------------------------------------------------
%include "./INC/Constant.inc"
%include "./INC/FAT12.inc"
; ----------------------------------------------------------------------------------------
org 0x7C00
; ****************************************************************************************
; ========================================================================================
; FAT12 文件系统引导扇区的头部(前 62 字节)
FAT12Head _main, "NASM+GCC", "TestX_v0.01"
; ****************************************************************************************
; ========================================================================================
; FAT12 文件系统引导扇区的引导代码(从第 62 字节开始)
; ----------------------------------------------------------------------------------------
; 程序入口
_main:
cli
cld
xor eax, eax
; 初始化寄存器
mov ax, cs
mov ds, ax
mov ss, ax
mov ax, 0x7C00
mov bp, ax
mov sp, ax
mov si, strBootMsg
call PrintStr
; 寻找 Loader
mov si, LoaderFileName
call SearchFile
; 加载 Loader
push SEGMENTBASEOFLOADER
pop es
mov bx, STACKSIZE
call LoadFile
; 控制权交给已加载到内存的 loader
jmp SEGMENTBASEOFLOADER : STACKSIZE
; 以下定义子函数
; ----------------------------------------------------------------------------------------
; 函数功能:寻找文件的起始位置
; 入口参数:ds : si = 文件名的存放地址
; 出口参数:ax = loader 文件的起始 FAT 表项序号
SearchFile:
push bp
mov bp, sp
sub sp , 2 * 2 ; 为局部变量分配空间
push di
push si
push dx
push cx
push bx
; 待读取的根目录区逻辑扇区号
mov word[bp - 2], IROOTDIRECTORYFIRSTSECTOR
; 待查找的根目录区扇区数
mov word[bp - 2 * 2], IDATAFIRSTSECTOR - IROOTDIRECTORYFIRSTSECTOR
mov di, si
; 逐个扇区寻找
push SEGMENTBASEOFTEMP ; Read1Sector 要用到 es
pop es
.Search_NextSector:
mov ax, [bp - 2]
xor bx, bx
call Read1Sector
; cx 统计一个扇区内未匹配的表项数
mov cx, 16 ; = [BPB_BytsPerSec] / DirectoryItem_size
.Search_ThisSector:
; 匹配文件名
mov si, di
mov dx, 11 ; dx 统计未匹配的文件名字符数
.Match_FileName:
lodsb
cmp al, byte[es : bx]
jnz .Match_NextItem
dec dx
jz .Found
inc bx
jmp .Match_FileName
.Match_NextItem:
and bx, 0b1111111111100000 ; 回当前表项的开始处
add bx, 32 ; 指向下一个表项(一个表项 32 字节,占用 5 位)
loop .Search_ThisSector
; 判断是否读完根目录区所有扇区:读完说明没找到,没读完就继续下一个
dec word[bp - 2 * 2]
jz .NotFound
inc word[bp - 2]
jmp .Search_NextSector
.NotFound:
mov si, strNotFoundFile
call PrintStr
jmp $
.Found:
mov ax, word[es : bx + 0x1A - 11 + 1] ; 指向当前表项中的 .DIR_FstClus
pop bx
pop cx
pop dx
pop si
pop di
mov sp, bp
pop bp
ret
; ----------------------------------------------------------------------------------------
; 函数功能:从软盘装载文件到内存
; 入口参数:ax = 该文件的起始 FAT 表项序号
; es : bx = 存放数据的内存缓冲区地址
; 出口参数:无
LoadFile:
push bp
mov bp, sp
push dx
push cx
push bx
push ax
.Load:
push bx
push ax
add ax, IDATAFIRSTSECTOR - 2 ; FAT 表项序号转换为逻辑扇区号
call Read1Sector
pop ax
call GetEntryValue
pop bx
cmp ax, 0xFF8 ; FAT 表项的值大于等于 0xFF8,表示文件结束
jae .Return ; 未检查坏扇区 —— 虚拟的不会坏的
add bx, BYTESPERSECTOR
jmp .Load
.Return:
POP ax
pop bx
pop cx
pop dx
mov sp, bp
pop bp
ret
; ----------------------------------------------------------------------------------------
; 函数功能:取得 FAT 表中指定序号表项的值
; 入口参数:ax = FAT 表项序号
; 出口参数:ax = 对应的 FAT 表项值(即下一个扇区的 FAT 表项序号)
GetEntryValue:
push bp
mov bp, sp
push es ; 读取 FAT 表时要使用 es 暂存数据
push dx
push cx
push bx
; 计算该表项序号所在的逻辑扇区号和在该扇区的偏移量
xor dx, dx ; 字节号(ax * 12 / 8)
mov bx, 3
mul bx
mov bx, 2
div bx
mov cx, dx ; 保存字节号的奇偶性(0 = 偶数,1 = 奇数)
xor dx, dx
mov bx, BYTESPERSECTOR
div bx
add ax, IFATFIRSTSECTOR ; 逻辑扇区号
push dx ; 保存在该扇区的偏移量
; 读取连续 2 个扇区(表项可能跨扇区)
push cx ; Read1Sector 函数改变了 cx、ax
push ax
push SEGMENTBASEOFTEMP
pop es
xor bx, bx
call Read1Sector
pop ax
inc ax
mov bx, BYTESPERSECTOR
call Read1Sector
pop cx
; 读出 16 位,奇数项取高 12 位、偶数项取低 12 位(低低高高存放原则),得到项值
pop bx ; 偏移量(上面压进去的 dx 值)
mov ax, [es : bx]
jcxz .Even
shr ax, 4
.Even:
and ax, 0b0000111111111111 ; 奇数项高 4 位已为 0 执行此操作值也不变
pop bx
pop cx
pop dx
pop es
mov sp, bp
pop bp
ret
; ----------------------------------------------------------------------------------------
; 函数功能:从软盘读取 1 个逻辑扇区
; 入口参数:ax = 逻辑扇区号
; es : bx = 存放数据的内存缓冲区地址
; 出口参数:同 ah = 2、int 0x13
Read1Sector:
push bp
mov bp, sp
push dx
push cx
; 由 LBA 计算 CHS
mov dl, 18
div dl
mov ch, al
mov dh, al
mov cl, ah
shr ch, 1
inc cl
and dh, 1
; 读一个扇区
mov ax, 0x0201
xor dl, dl
int 0x13
; cmp ah, 0 ; 虚拟软盘不会出错
; jz .Return
; call PrintMsg
; db "Error to read Floppy Disk !", `\r\n`, 0
; jmp $
.Return:
pop cx
pop dx
mov sp, bp
pop bp
ret
; ----------------------------------------------------------------------------------------
; 函数功能:显示字符串
; 入口参数:ds : si = 字符串地址
; 出口参数:无
PrintStr:
push bp
mov bp, sp
push si
push ax
mov ah, 0x0E ; 功能号,0x0E:显示一个字符,光标跟随字符移动
.Print:
lodsb
cmp al, 0 ; 字符串以 0 结尾
je .Return
int 0x10
jmp .Print
.Return:
pop ax
pop si
mov sp, bp
pop bp
ret
; ****************************************************************************************
; ========================================================================================
; FAT12 文件系统引导扇区引导数据部分(字符串)
strNotFoundFile db "Error 404", `\r\n`, 0
strBootMsg db "TestX is booting ...", `\r\n`, 0
LoaderFileName db "LOADER SYS", 0, 0 ; loader 文件名(8 + 3格式,长度不够的填空格)
; ****************************************************************************************
; ========================================================================================
; FAT12 文件系统引导扇区引导代码的剩余部分用 0 填满,最后两个字节置结束标志(0xAA55)
times 510 - ($ - $$) db 0
dw 0xAA55
; ****************************************************************************************
; Loader.asm
; 加载程序
; 四彩
; 2015-11-12
[SECTION .text]
; ========================================================================================
; 常量定义及其他头文件
; ----------------------------------------------------------------------------------------
%include "./INC/Constant.inc"
; ----------------------------------------------------------------------------------------
org STACKSIZE
; ****************************************************************************************
; 程序入口
; ========================================================================================
_main:
; 初始化寄存器
mov ax, cs
mov ds, ax
mov ss, ax
mov bp, STACKSIZE
mov sp, STACKSIZE
call PrintMsg
db "Loader is loaded ...", `\r\n`, 0
mov si, strHelloWorld
mov cl, 0b00000010
mov dx, 0x0510
call ShowStr
jmp $
strHelloWorld db "Hello World !", 0
; ----------------------------------------------------------------------------------------
; 函数功能:显示紧跟在调用指令后定义的字符串
; 入口参数:无
; 出口参数:无
; 注意:本函数改变了寄存器 ax、si 的值,如有必要,父函数应在调用前自行保存
PrintMsg:
pop si ; si = ip
mov ah, 0x0E ; 功能号,0x0E:显示一个字符,光标跟随字符移动
.Loop:
lodsb
cmp al, 0 ; 字符串以 0 结尾
je .Return
int 0x10
jmp .Loop
.Return:
push si ; 恢复 ip
ret
; ----------------------------------------------------------------------------------------
; 函数功能:直接写显存显示字符串
; 入口参数:cl = 颜色属性
; dh、dl = 屏幕行(0 ~ 24)、列坐标(0 ~ 79)
; ds : si = 待显示字符串地址
; 出口参数:无
; 80 * 25 彩色字模式的显存第一页(共 4 页)在内存中的地址为 B8000H ~ B8F9FH,向该地址写入
; 内容将立即显示在屏幕上,共可显示 25 行、80 列,屏幕左上角为原点(0,0)。
; 每个字符在显存中占两个字节,第一个字节是 ASCII 码,第二字节是颜色属性(共 256 种):
; 位: 7 6 5 4 3 2 1 0
; 含义: BL R G B I R G B
; 闪烁 背景颜色 高亮 前景颜色
ShowStr:
push bp
mov bp, sp
push es
push di
push si
push dx
push cx
push ax
mov ax, 0x0B800
mov es, ax
; 由行列坐标计算显存偏移量
mov al, 160
mul dh
mov di, ax
mov al, 2
mul dl
add di, ax
.Loop:
mov al, [ds : si]
cmp al, 0
jz .Return
mov [es : di], al
mov [es : di + 1], cl
inc si
add di, 2
jmp .Loop
.Return:
pop ax
pop cx
pop dx
pop si
pop di
pop es
mov sp, bp
pop bp
ret
; ****************************************************************************************
看下运行效果,还不错,今天的任务结束!