mbr,也就是boot的作用就是加载loader,因为只有512字节的量,能做的事很少,在下一阶段,也就是loader中能进行更多的操作,这就涉及到了加载loader文件的知识
文件系统
loader是以文件的形式存在,不是之前的读取磁盘信息的操作,文件的存在依附于文件系统,该系统用的是FAT16的文件系统格式,至于为什么用FAT16,因为其它的还没学过。
为什么要用到文件系统?计算机记录信息无非是将信息写到磁盘上,但是如果没有文件系统的管理,在比如增加一些内容,磁盘空间哪些空闲这类问题的处理上将很麻烦,文件系统就是为了方便在存储设备上存储数据而抽象出来的一种数据管理方式
FAT16
FAT(File Allocation Table),文件分配表,FAT16使用的是16位的空间来表示每个扇区的配置文件,所以有个16,其有一个先天的限制,就是磁盘分区最大只能到2GB,而对于目前所写的这个系统已经够用了。
在FAT16文件系统中,通常会将硬盘或者某一分区划分为5个部分,DBR扇区、FAT1表、FAT2表、根目录区、数据区。前面四个是文件属性相关内容,而数据区才是真正我们常看见的那些文件内容。
DBR扇区:是操作系统可以直接访问的第一个扇区,包括一个引导程序和一个称为BPB的本分区参数记录表。BPB参数块记录着本分区的启始扇区、结束扇区、文件存储格式、硬盘介质描述符、根目录大小、FAT个数、分配单元的大小等重要参数
FAT1,FAT2:是簇的链表。在根据目录项获取文件的首簇号后,在FAT 找到对应的簇,可以找到下一个簇,一直到文件结束。对于FAT16,每个簇用16bit来表示,而对于FAT32,使用32bit来表示,这是两者之间的重要区别,在这个系统中没有用到FAT2表,FAT2表就是对FAT1表的备份。
根目录:根目录的作用就是有文件或者目录的首簇号,以及文件的长度(目录的长度是0)。
数据区:注意数据区是从2号簇开始的,不是0号
因此,查找文件的过程应是这样的:在DBR中根据BPB参数得到FAT表和根目录地址,从根目录中获取首簇的簇号,根据FAT表获取整个文件的簇号链表,一个一个簇的读出内容
FAT16引导扇区结构表
名称 | 偏移 | 长度 | 内容 | zuos 值 |
---|---|---|---|---|
BS_jmpBoot | 0 | 3 | 一个短跳转指令 | jmp boot_start nop |
BS_OEMName | 3 | 8 | 厂商名称 | ’ ZUOS ’ |
BPB_BytsPerSec | 11 | 2 | 每扇区字节数 | 0x0200 |
BPB_SecPerClus | 13 | 1 | 每簇扇区数 | 0x01 |
BPB_RsvdSecCnt | 14 | 2 | 保留扇区数(引导扇区的扇区数) | 0x0001 |
BPB_NumFATs | 16 | 1 | FAT表的份数 | 0x01 |
BPB_RootEntCnt | 17 | 2 | 根目录可容纳的目录项数 | 0x0200 |
BPB_TotSec16 | 19 | 2 | 扇区总数 | 0x2000(4MB) |
BPB_Media | 21 | 1 | 介质描述符 | 0xf8 |
BPB_FATSz16 | 22 | 2 | 每个FAT表扇区数 | 0x0020 |
BPB_SecPerTrk | 24 | 2 | 每磁道扇区数 | 0x0020 |
BPB_NumHeads | 26 | 2 | 磁头数 | 0x0040 |
BPB_HiddSec | 28 | 4 | 隐藏扇区数 | 0x00000000 |
BPB_TotSec32 | 32 | 4 | 如果BPB_TotSec16是0,由这个值记录扇区数。 | 0x00000000 |
BS_DrvNum | 36 | 1 | int 13h的驱动器号 | 0x80 |
BS_Reservedl | 37 | 1 | 未使用 | 0x00 |
BS_BootSig | 38 | 1 | 扩展引导标记 | 0x29 |
BS_VolID | 39 | 4 | 卷序列号 | 0x00000000 |
BS_VolLab | 43 | 11 | 卷标 | ’ ZU OS ’ |
BS_FileSysType | 54 | 8 | 文件系统类型 | FAT16 |
引导代码及其它 | 62 | 448 | 引导代码、数据及其它填充字符等 | |
结束标志 | 510 | 2 | 0xAA55 | 0xAA55 |
此表可以看出,引导扇区的前62个字节是固定的
由于不使用FAT2表,zuos的扇区分布如下,总空间为4MB,这是跟着教程做的大小,之后熟悉了再改
FAT16目录项
文件夹也叫目录,文件夹中存放的就是该文件夹下文件和子文件夹的目录项,一个目录项对应于一个文件或者文件夹,每个目录项有32个字节,每个扇区可以存放16个目录项,结构如下
名称 | 偏移 | 长度 | 描述 |
---|---|---|---|
DIR_Name | 0 | 11 | 文件名8字节 |
DIR_Attr | 11 | 1 | 目录项属性(0x10代表文件夹,0x20代表文件) |
保留位 | 12 | 10 | 保留位 |
DIR_WrtTime | 22 | 2 | 最后一次写入时间 |
DIR_WrtDate | 24 | 2 | 最后一次写入日期 |
DIR_FstClus | 26 | 2 | 起始簇号 |
DIR_FileSize | 28 | 4 | 文件大小 |
FAT表
FAT16的数据区是以簇作为编号的,一个簇可以包含一个或多个扇区,在zuos中为了简单就以一个簇对应一个扇区设计,簇的编号是从2~0xffef,也就是前面两位和最后16位不可用,FAT16可管理的簇就有
2
16
−
18
=
65518
2^{16}-18=65518
216−18=65518个。
查找并访问一个文件的流程如下:
- 在根目录中查找目录项中文件名与要查找的文件名相同的进行下一步,没有就结束
- 根据目录项中记载的文件首簇地址在FAT表中查看该簇是否有后续的簇,并读取该簇对应的扇区
- 如果该簇存储的值为0表示该簇还未使用,如果值大于0xfff8,表示这是文件的最后一个簇,如果不是这些情况,值就是下一个簇的簇号
- 循环第三步,直到簇号大于0xfff8
FAT表的访问过程示意图如下,注意,簇号是默认的,并不会有存储本簇簇号的地方。
该表表示本文件共有三个簇,即5、10、16,到16后访问结束
FAT16表项值说明
FAT表项 | 实例值 | 描述 |
---|---|---|
0 | 0xfff8 | 磁盘表示字(实际无用,设为0即可。) |
1 | 0xffff | 第一个簇不可用(实际无用,设为0即可。) |
2 3…… | 0x0003 0x0004…… | 0x0000:可用簇 0x00020xffef:已用簇,标识下一个簇的簇号<br>0xfff00xfff6:保留簇 0xfff7:坏簇 0xfff8~0xffff:文件的最后一个簇 |
在前面的设计中,将FAT表设计为32个扇区,每个扇区有 512 B / 16 b i t = 256 512B/16bit=256 512B/16bit=256个FAT表项,共有 256 ∗ 32 = 8192 256*32=8192 256∗32=8192个FAT表项,0、1号簇不可用,所以共可管理8190个簇或扇区,在前面数据区的划分是65~0x1fff,共8091个扇区,所以是够用的,
从磁盘中读取文件
读取文件流程
按照读第一个目录表扇区,第一个FAT表扇区
boot.asm
;---------------- 定义常量 ----------------
; FAT16目录项偏移
; | 名称 | 偏移 | 描述 |
DIR_Name equ 0 ; 8B,文件名
DIR_Attr equ 11 ; 1B,目录项属性(0x10代表文件夹,0x20代表文件)
DIR_WrtTime equ 22 ; 2B,最后一次写入日期
DIR_WrtDate equ 24 ; 2B,最后一次写入时间
DIR_FstClus equ 26 ; 2B,首簇簇号
DIR_FileSize equ 28 ; 4B,文件大小
BOOT_ADDRESS equ 0x7c00 ; boot程序的存放地址
DISK_BUFFER equ 0x7e00 ; 加载到磁盘时临时存放地址
DISK_SIZE equ 4 ; 磁盘总大小4M
FAT1_SECTORS equ 32 ; FAT1所占扇区数
ROOT_DIR_SECTORS equ 32 ; 根目录所占扇区数
FILE_NAME_LEN equ 11 ; 文件名长度,8(文件名)+3(文件属性)
DIR_ENTRY_SIZE equ 32 ; 目录项大小
DIR_ENTRY_PER_SECTOR equ 16 ; 每个扇区所容纳的目录项数
SECTOR_NUM_OF_FAT1_START equ 1 ; FAT1表的起始扇区
SECTOR_NUM_OF_ROOT_DIR_START equ 33 ; 根目录的起始扇区
SECTOR_NUM_OF_DATA_START equ 65 ; 数据区的起始扇区
SECTOR_CLUSTER_BALANCE equ 63 ; 数据的扇区数是从65开始,簇号是从2开始,这个值就是用来平衡这两个值
LOADER_ADDRESS equ 0x1000 ; loader进入内存的存放地址
VIDEO_SEGMENT_ADDRESS equ 0xb800 ; 显存段的首地址
VIDEO_MAX_CHAR_DIS equ 2000 ; 屏幕最大的显示字符数
STACK_BOTTOM equ LOADER_ADDRESS ; 栈底地址(把栈放在loader的前面)
; ---------------- MBR START --------------
org BOOT_ADDRESS
jmp boot_start
nop
; BPB参数
BS_OEMName db ' ZUOS ' ; 厂商名字,固定8字节
BPB_BytesPerSec dw 0x0200 ; 每个扇区的字节数,512
BPB_SecPerClus db 0x01 ; 每个簇的扇区数,1
BPB_RsvdSecCnt dw 0x0001 ; 保留扇区数量,一个引导扇区
BPB_NumFATs db 0x01 ; FAT表的个数,1
BPB_RootEntCnt dw 0x0200 ; 根目录扇区数,512
BPB_TotSec16 dw 0x2000 ; 扇区总数8192
BPB_Media db 0xf8 ; 介质描述符,硬盘
BPB_FATSz16 dw 0x0020 ; 每个FAT表的扇区数,32
BPB_SecPerTrk dw 0x0020 ; 每个磁道的扇区数,32
BPB_NumHeads dw 0x0040 ; 磁头数,64
BPB_HiddSec dd 0x00000000 ; 隐藏扇区数
BPB_TotSec32 dd 0x00000000 ; 如果BPB_TotSec16是0,由这个值记录扇区数。
BS_DrvNum db 0x80 ; int 13h的驱动器号
BS_Reservedl db 0x00 ; 保留
BS_BootSig db 0x29 ; 扩展引导标记
BS_VolID dd 0x00000000 ; 卷序列号
BS_VolLab db ' ZU OS ' ; 卷标,固定11字节
BS_FileSysType db 'FAT16 ' ; 文件系统类型,8字节
; ---------------- BOOT START -----------------
boot_start:
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov sp,STACK_BOTTOM
mov ax,VIDEO_SEGMENT_ADDRESS
mov gs,ax ; 用gs作为储存显示段地址,之前用的是es,如果运行系统后没有显示,可能是print_string中的es没有改
; 清空屏幕
call clear_screen
; 打印boot start
mov si,boot_start_message
mov di,1
call print_string
; 读取根目录数据,存放到DISK_BUFFER指定的内存地址,由于目前只有一个loader.bin文件,就读其开始的第一个扇区
mov esi,SECTOR_NUM_OF_ROOT_DIR_START
mov di,DISK_BUFFER
call read_one_sector
mov bx,0 ; bx用来表示第几个目录项文件名和loader.bin 相等
cld ; cld将标志位DF置0,在串处理指令中控制每次操作后让si和di自动递增。std相反。下面repe cmpsb会用到
; 寻找目录项中文件名和loader.bin相同的目录项
next_dir_entry:
mov si,bx
shl si,5 ; 乘以32,目录项的大小,这样si的值才是目录项的首地址
add si,DISK_BUFFER ; DISK_BUFFER处是存放刚才读出的根目录数据,加上后才是目录项在内存中的地址
mov di,loader_bin ;目标地址指向loader程序在硬盘中的正确文件名
mov cx,FILE_NAME_LEN ;字符比较次数为FAT16文件名长度,每比较一个字符,cx会自动减一。
repe cmpsb ;逐字节比较ds:si和es:di指向的两个字符串。就是这里使用了es,所以之前的显存段地址才要用gs代替
jcxz loader_found ;当cx为0时跳转。cx为0表示上面比较的两个字符串相同。找到了loader文件。
inc bx ; 如果cx不为0,说明还没比对完,两个字符串就不等了,就要比较下一个目录项了
cmp bx,DIR_ENTRY_PER_SECTOR ; 这里应该是比较目录项的总个数,但是目前的设计,loader只能是在根目录下,所以读取的根目录扇区中没有就代表没发现该文件
jl next_dir_entry ; 有符号数比较,检查下一个目录项(这里怎么会用有符号跳转不清楚,明明两个都是无符号数)
jmp loader_not_found
; 发现了该文件
loader_found:
; 输出字符串loader found
mov si,loader_found_message
mov di,81
call print_string
shl bx,5
add bx,DISK_BUFFER
mov bx,[bx+DIR_FstClus] ; 同上面si乘32加DISK_BUFFER理由一样,这样bx才是指向对应目录项的内存首地址,bx加上首簇的偏移地址,其内容就是首簇簇号
mov esi,SECTOR_NUM_OF_FAT1_START
mov di,DISK_BUFFER
call read_one_sector ; 将FAT1表的数据读出,覆盖到原来根目录的数据上,后面这里的数据就是FAT表的数据了
mov bp,LOADER_ADDRESS ; 将loader.bin准备读入存放的地址赋值给bp
; 读取loader函数
read_loader:
xor esi,esi ; 将esi全部置0
mov si,bx ; 将bx赋值给si,bx现在存放的是文件簇号
add esi,SECTOR_CLUSTER_BALANCE ; 加上扇区-簇号的偏移量,就得到该簇号在数据区中扇区的对应扇区号
mov di,bp ; 将扇区数据读入LOADER_ADDRESS指定的地址
call read_one_sector
add bp,512 ; bp加上512,就是下一个扇区要在内存中存放的地址
shl bx,1 ; bx乘2,是因为每个簇号要占两个字节,乘2才是存放下一个簇号的地址
mov bx,[bx+DISK_BUFFER]
cmp bx,0xfff8 ; 如果bx小于0xfff8,继续读下一个扇区
jb read_loader
; 读取结束,跳转到存放loader.bin的地址中,这样就开始执行loader.bin文件中的内容了
read_loader_finish:
jmp LOADER_ADDRESS
; 文件没发现,打印个信息
loader_not_found:
mov si,loader_not_found_message
mov di,81
call print_string
stop:
hlt
jmp stop
; 清除屏幕函数
; 输入参数:gs,0xb800
clear_screen:
mov ah,0x00 ;黑底黑字
mov al,' ' ;空格
mov cx,VIDEO_MAX_CHAR_DIS ; 将cx定义为最大字符数,在后面使用的loop,会在每一次执行时将cx减1
.write_blank: ; 子标量,用于循环定义内存空间,其完整格式应该是clear_screen.write_blank
mov bx,cx
dec bx
shl bx,1 ; 将bx作为偏移量,自减再左移一位表示减一再乘二,其值刚好是对应的字符的起始地址(一个字符的表示占两个字节)
mov [gs:bx],ax
loop .write_blank ; 循环
ret
; 打印字符串函数
; 输入参数:si,字符串首地址 ; di:从屏幕上的第几个字符开始
print_string:
mov ah,0x07
shl di,1
.print_char: ; 打印单个字符的局部标号
mov al,[si] ; 将si对应的字符赋值给al
cmp al,0 ; 字符与0比较,因为在boot_meaasge字符串定义的最后是以0结尾的
jz .end_print ; 如果是0就结束打印
mov [gs:di],ax ; 将ax的值给到文本模式的内存空间
add di,2 ; 偏移量加2(因为一个字符用两个字节表示)
inc si ; si加一,指向下一个字符地址
jmp .print_char ; 循环
.end_print:
ret
; 读取一个扇区函数(主控制器主盘)
; 输入参数:esi,LBA ; ds:di,数据存放地址
; 输出参数:无
read_one_sector:
; 读取状态,看是否第7位是0,第6位是否是1,也就是表示硬盘空闲且就绪的状态
mov dx,0x1f7
.not_ready1:
nop ; 一个短暂的停顿
in al,dx ; 将端口数据读入al中
and al,0xc0 ; 将al和1100 0000做与运算,其结果是将al的第6,7位保留下来,其他都是0
cmp al,0x40 ; 将al和0100 0000做比较,如果相等,证明硬盘空闲且就绪,如果不是就循环再检测
jne .not_ready1
; 设置要读取扇区的数量
mov dx,0x1f2
mov al,1
out dx,al
; 将LBA的前14位设置到0x1f3~0x1f5端口上
mov eax,esi
mov dx,0x1f3
out dx,al
shr eax,8 ; 右移8位,这样al的值就是第9-16位的LBA值
mov dx,0x1f4
out dx,al
shr eax,8
mov dx,0x1f5
out dx,al
; 设置0x1f6端口,前四位是LBA的最后四位,后四位,依据通过LBA从主设备读取数据的要求,应该设置为1110
shr eax,8
mov dx,0x1f6
and al,0x0f ; 将al的前四位进行保留,也就是LBA的后四位
or al,0xe0 ; 和1110进行或的结果,就是将al的后四位设置为1110
out dx,al
; 写入读硬盘命令
mov dx,0x1f7
mov al,0x20 ; 0x20是读命令
out dx,al
; 再次检查硬盘状态是否为空闲,且可以从硬盘中读数据的状态,也就是0x1f7应该为10001000
.not_ready2:
nop
in al,dx
and al,0x88 ; 将al的第7和第3位保留
cmp al,0x08 ; 判断第7位是否为0,第三位是否为1
jne .not_ready2
mov cx,256 ; 由于每次只能接受两个字节,所以一个扇区需要读取256次
mov dx,0x1f0
.read_data:
in ax,dx
mov [di],ax ; 将读出的数据存入0X7e00开始的地址
add di,2
loop .read_data
ret
loader_bin: db "LOADER BIN",0 ; 注意要用个0结尾
boot_start_message:db "zuos boot start",0
loader_not_found_message: db "loader not find!!",0
loader_found_message: db "loader found",0
times 510-($-$$) db 0
db 0x55,0xaa
loader.asm
只打印了一个提示信息
org 0x1000
;打印字符串
mov si,loader_start_string
mov di,161 ;屏幕第3行显示
call func_print_string
stop:
hlt
jmp stop
;打印字符串函数
;输入参数:ds:si,di。
;输出参数:无。
;si 表示字符串起始地址,以0为结束符。
;di 表示字符串在屏幕上显示的起始位置(0~1999)
func_print_string:
mov ah,0x07 ;ah 表示字符属性 黑底白字
shl di,1 ;乘2(屏幕上每个字符对应2个显存字节)
.start_char:
mov al,[si]
cmp al,0
jz .end_print
mov [gs:di],ax
inc si
add di,2
jmp .start_char
.end_print:
ret
loader_start_string:db "zuos loader start.",0
执行操作
- 编译
nasm boot.asm -o boot.bin
nasm loader.asm -o loader.bin
- 装入硬盘并运行
dd if=/dev/zero of=boot.img bs=1M count=4
dd if=boot.bin of=boot.img conv=notrunc
运行结果如下
3. 装入loader.bin文件再运行
uzuos-> sudo mount boot.img /mnt/ -t msdos -o loop
uzuos-> sudo cp loader.bin /mnt/
uzuos-> sudo sync
uzuos-> sudo umount /mnt/
运行结果如下