引导扇区只有 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 ; ****************************************************************************************
看下运行效果,还不错,今天的任务结束!