译自:http://www.brokenthorn.com/Resources/OSDev6.html
第6 章:引导加载器4
by Mike, 2009
本系列文章旨在向您展示并说明如何从头开发一个操作系统。
介绍
欢迎!在前一章中我们讨论了如何加载并执行一个扇区。我们也了解了汇编语言的环,并且详细了解了BIOS 参数块 (BPB) 。
在本章中,我们将用我们所知的所有信息来解析FAT12 文件系统并根据文件名加载我们的第二段引导加载器。
为此本章有很多代码。我会尽我所能,详加解释,这章里,也有些数学。
准备好了吗?
cli 和 hlt
你可能会好奇为什么我总是有"cli" 和 "hlt" 结束程序。这实际很简单。如果不以某正方式结束程序,CUP 会超出我们代码的部分而执行一些随机的指令,如果这样,会带来一个三重错误。
禁止中断的目的是执行中断(系统没有完全停机)是我们不希望的。这会导致错误,仅是有hlt 指令 (不使用 cli )会导致三重错误。
因此,我总以cli 和 hlt 来结束程序。
文件系统 – 理论
是的!到了说文件系统的时间了!
文件系统是一个规范。它帮助我们在磁盘上创建“文件”。
文件是代表某些事情的一组数据,数据可以是我们想要的任何东西,这取决于如何解释数据。
如你所知,每扇区512 字节。文件按扇区保存在磁盘上。如果文件比512 字节大,我们给它更多的扇区。因为并不是所有的文件大小都是512 字节的整数倍,我们需要填充剩余的部分(文件不使用它们),就像我们在引导加载器中所作的一样。
如果文件分布在几个扇,我们把这些扇称作FAT 文件系统的簇。比如,内核往往会占用很多扇,为了加载内核,我们需要从它所在的位置加载这个簇(这些扇)。如果文件分布在不同簇的几个扇(不连续),它被称为“碎片”,我们需要收集文件的不同部分。
有很多不同的文件系统。有些使用广泛(像FAT12, FAT16, FAT32, NTFS, ext (Linux), HFS ( 只在MAC 下使用) ;其它的一些只被特殊的公司或个人使用( 像 GFS - Google File System) 。
许多操作系统开发者会创造新的FATA 版本(甚至是全新的),这些一般没有流行的文件系统( 像FAT 和NTFS) 好。
好了,我们知道了文件系统的一些基础知识。为了简单,我们使用FAT12 。如果你想,完全可以用其它的 J
FAT12 文件系统- 理论
FAT12 是第一个FAT 文件系统,发布于1977 ,并应用在Microsoft Disk BASIC 中。FAT12 一般用在软盘上,它有一些限制。
这是很大的限制,我们为什么要用FAT12 呢?
FAT16 使用16 比特作为簇(文件)的地址,它支持文件夹并且最多可以有64,000 个文件。FAT16 和 FAT12 非常相似。
简单起见,我们使用FAT12 。后面我们可能支持FAT16 ( 甚至FAT32)(FAT32 与FAT 12/16 差别很大,所以我们更可能会在后面使用FAT16)
FAT12 文件系统 – 磁盘分布
为了更好的理解FAT12 以及了解它如何工作,我们最好是看看它在一个格式化号的物理磁盘上的结构。
引导扇 |
保留扇 |
FAT1 |
FAT2 |
根目录( 仅在FAT12/FAT16) | 数据区 |
这是一个典型的FAT12 磁盘,包括了从引导扇区开始到磁盘的的最后一个扇区。
理解这个结构对于文件的搜索和加载是很重要的。
注意在磁盘上有两个FAT 。它们正跟在保留扇之后(或者引导扇之后,如果没有保留扇的话)。
另外注意:根目录正好在FAT 之后。 这意味着……
如果我们把每个FAT 的扇区数和保留扇区数加起来,就得到了根目录的第一个扇区。 通过 在根目录搜索一个简单是字符串(我们的文件名)我们就可以找到保存文件的扇区。
详细些……
引导扇
这是BIOS 参数块和引导加载器所在的扇区。BIOS 参数块包含有对磁盘的描述信息。
附加保留扇
还记得在BPB 中的bpbReservedSectors 字段吗?所有的附加保留扇都在这里,正好在引导扇之后。
文件分配表(FAT)
簇是一系列连续的扇区。簇的大小一般是2 KB 到32 KB 。文件片段是连在一起的( 使用一个常见的数据结构——链表——从一个扇区连到另一个) 。
有两个FAT ,但其中一个仅仅是另一个的副本,这用于数据恢复的目的。后一个总不使用。
文件分配表(FAT) 是一个项目的列表,他把文件和簇联系在一起。它对我们将数据保存在这些簇中相当重要。
每一项都有12 比特,代表一个簇。FAT 是一个像链表一样的结构,用于标识哪个簇正在被使用。
为了更好的理解,我们看看它们可能的取值:
FAT 仅仅是上面这些值构成的简单数组,仅仅这样。当我们从根目录找到一个文件的起始簇后,我们就可以通过查找FAT 来决定加载哪个簇。怎么做呢?我们简单的检查这个值。如果这个值在0x02 和 0xfef 之间,这个值表示我们要加载的下一个簇。
让我们更深入的看看这个问题。一个簇,如你所知,代表一系列扇区。我们在BPB 中定义了一个簇所包含的扇区数:
bpbBytesPerSector: DW 512
bpbSectorsPerCluster: DB 1
在这里,每个簇1 扇区。当我们找到Stage2 的第一个扇区(我们从根目录中得到),我们用这个扇区作为FAT 的起始簇。一旦我们找到了起始簇,我们就可以通过查找FAT 来确定下一个簇(FAT 仅仅是32 位数的数组,我们只需要上面的列表确定做什么就行了)。
根目录表
现在,这对于我们非常重要。
更文件夹是一个表,表中每项都是32 字节,表示文件及文件夹的信息。这32 字节的格式如下:
我加粗了重要的部分——剩下的是Microsoft 要考虑的,我们会在创建FAT12 驱动器时再考虑,还有些时候呢。
等等!还记得DOS 的文件名限制在11 字节吗?这样:
0 到 10, hmm... 是11 字节。一个不足11 字节的文件名会与上面的数据项(上面列出的32 字节)不匹配。当然,这不行,我们得扩展使它变成11 字节。
记得我们在前面的教程中说的内部名和外部名吗?我现在解释的结构是内部名。它被现在在11 字节所以文件名"Stage2.sys" 会变成:
"STAGE2 SYS" ( 注意扩展!)
查找并读取FAT12 – 理论
好的,看完了上面内容,你可能已经很烦我再说"FAT12" 了。
上面的信息,怎么起作用的呢?
后面我们将会参考BPB 。这是一个我们在前面的教程中创建的BPB :
bpbBytesPerSector: DW 512
bpbSectorsPerCluster: DB 1
bpbReservedSectors: DW 1
bpbNumberOfFATs: DB 2
bpbRootEntries: DW 224
bpbTotalSectors: DW 2880
bpbMedia: DB 0xF0
bpbSectorsPerFAT: DW 9
bpbSectorsPerTrack: DW 18
bpbHeadsPerCylinder: DW 2
bpbHiddenSectors: DD 0
bpbTotalSectorsBig: DD 0
bsDriveNumber: DB 0
bsUnused: DB 0
bsExtBootSignature: DB 0x29
bsSerialNumber: DD 0xa0a1a2a3
bsVolumeLabel: DB "MOS FLOPPY "
bsFileSystem: DB "FAT12 "
请参考前一章中对每一个成员的解释。
我们要做的是加载第二段加载器。我们需要看的详细些:
从一个文件名开始
第一件事是创造一个好的文件名。记住:文件名必须11 个字节,以免损坏根目录。
我使用 "STAGE2.SYS" 来命名我的第二段。你可以在上面看到一个内部文件名的例子。
创建Stage 2
好了,Stage2 代表那个引导加载器之后执行的程序。我们的Stage2 和DOS COM 程序很相似,听起来很酷,不是吗?
Stage2 要做的事只有打印一个消息,然后停机。这些你已经在引导加载器那部分见过了:
; 注意:这里我们就像执行一个通常的COM 程序
; 但是,是在第0 环。我们将会使用它设置32 位模式
; 和基本的异常控制
; 被加载的程序将会是我们的32 位内核
; 这里没有512 字节的限制,我们可以添加任何想要的
org 0x0 ; 偏移0 ,我们在后面设置段
bits 16 ; 我们在实模式
; 我们被加载到线性地址0x10000 处
jmp main ; 跳到main
;***************************************
; 打印字符串
; DS=>SI: 0 终止的字符串
;***************************************
Print:
lodsb ; 从SI 加载下一个字符到AL
or al, al ; AL=0?
jz PrintDone ; 是,0 终止,跳出
mov ah, 0eh ; 不是,打印字符
int 10h
jmp Print ; 重复,直到到达结尾
PrintDone:
ret ; 完成返回
;*************************************************;
; Stage2 入口点
;************************************************;
main:
cli ; 禁止中断
push cs ; 确保DS=CS
pop ds
mov si, Msg
call Print
cli ; 禁止中断以避免三重错误
hlt ; 使系统停机
;*************************************************;
; 数据区
;************************************************;
Msg db "Preparing to load operating system...",13,10,0
使用NASM 汇编,仅仅汇编为二进制文件(COM 程序是二进制的), 并把它负责到磁盘映像中。如:
nasm -f bin Stage2.asm -o STAGE2.SYS
copy STAGE2.SYS A:/STAGE2.SYS
不需要PARTCOPY 。
Step 1: 加载根目录表
现在是时候加载我们的Stage2.sys 了!我们在这个会关注根目录,并且将会从BPB 获取磁盘信息。
Step 1: 获取根目录表大小
首先,我们要知道根文件的大小。
为了得到大小,仅仅需要乘根目录中的项目数,很简单。
在Windows 中,无论你在一个FAT12 的磁盘中添加文件或文件夹, Windows 会自动的在根目录中添加文件线性,不用考虑它,这样问题就简单了。
用每扇区的字节数除根目录项目数,我们会得到根目录占用的扇区数。
这是一个例子:
mov ax, 0x0020 ; 32 字节目录项
mul WORD [bpbRootEntries] ; 根目录数
div WORD [bpbBytesPerSector] ; 得到根目录占用的扇区数
记住根目录是一张表,每个表项32 字节,表示文件信息。
好, 我们知道了对于根目录要加载多少个扇区。现在,让我们找到要加载的起始扇区。
Step 2: 获取根目录表的起点
这是另一个简单事儿,我们再看看,FAT12 的结构:This is another easy one. First, lets look at a FAT12 formatted disk again:
引导扇 |
保留扇 |
FAT1 |
FAT2 |
根目录( 仅在FAT12/FAT16) |
数据区 |
好,注意到根目录在两个FAT 和保留扇之后,换言之,我们仅仅需要FATs + 保留扇, 就找到了根目录!
比如:
mov al, [bpbNumberOfFATs] ; FAT 数( 一般是2)
mul [bpbSectorsPerFAT] ; FAT 数 * 每FAT 的扇区数
; 所有FAT 占用的扇区数
add ax, [bpbReservedSectors] ; 加保留扇
; 现在, AX = 根目录的起始扇
够简单了吧。现在我们只需要把扇区读到内存的某个位置:
mov bx, 0x0200 ; 加载根目录到 7c00:0x0200
call ReadSectors
根目录 – 一个完整示例
这个例子的代码直接来自本章结尾的引导加载器代码。它加载根目录:
LOAD_ROOT:
; 计算根目录大小保存在"cx" 中
xor cx, cx
xor dx, dx
mov ax, 0x0020 ; 32 字节目录项
mul WORD [bpbRootEntries] ; 根目录的总大小
div WORD [bpbBytesPerSector] ; 根目录占用的扇区数
xchg ax, cx
; 计算根目录的位置保存在"ax" 中
mov al, BYTE [bpbNumberOfFATs] ; FAT 数
mul WORD [bpbSectorsPerFAT] ; FAT 占用的扇区数
add ax, WORD [bpbReservedSectors] ; 加保留扇
mov WORD [datasector], ax ; 根目录基地址
add WORD [datasector], cx
; 将根目录读到内存(7C00:0200)
mov bx, 0x0200 ; 复制根目录
call ReadSectors
Step 2: 查找 Stage 2
好,现在根目录表被加载进来了。看看上面的代码, 在0x200 那里 。下面,我们查找文件。
让我们返回32 字节的目录项 ( 前11 字节表示文件名。 还有每个表项32 字节 ,那么每32 字节就是下一个表项的起点——指向下一个表项的前11 个字节 )
因此,我们要做的一切就是比较文件名,跳到下一个32 字节,再试一次,直到扇末尾。比如:
; 浏览根目录
mov cx, [bpbRootEntries]; 表项数,当减到0 时,文件不存在
mov di, 0x0200 ; 根目录被加载在这儿
.LOOP:
push cx
mov cx, 11 ; 11 字节的文件名
mov si, ImageName ; 与我们的文件名比较
push di
rep cmpsb ; 比较是否匹配
pop di
je LOAD_FAT ; 匹配加载FAT
pop cx
add di, 32 ; 不匹配,到下一个表项(加32 字节) loop .LOOP
jmp FAILURE ; 再没有表项,文件不存在:(
下一步……
Step 3: 加载 FAT
Step 1: 获取起始簇
好了,根目录被加载了进来,而且,我们找到了文件对应的表项。我们怎么找到它的起始簇呢?
看起来很像,为了得到起始簇,访问表项的第26 字节:
mov dx, [di + 0x001A] ; di 保存表项起始地址. 访问第26 字节 (0x1A)
; 现在dx 保存有起始簇号
起始簇对于文件加载很重要。
Step 2: 获取FAT 大小
我们再看看BIOS 参数块。
bpbNumberOfFATs: DB 2
bpbSectorsPerFAT: DW 9
好,我们知道两个FAT 占用的数了,只要把上面的两个数相乘,看起来很简单……但是……
xor ax, ax
mov al, [bpbNumberOfFATs] ; FAT 数
mul WORD [bpbSectorsPerFAT] ; 乘以每FAT 扇区数
; ax = FAT 占用的扇区数
不,别想太多,就这么简单^^
Step 3: 加载 FAT
现在,我们知道了要读多少个扇区,那么读它就好了
mov bx, 0x0200 ; 要加载的地址
call ReadSectors ; 加载FAT
是的!FAT 的东西做完了 ( 不完全!), 加载stage 2!
FAT – 一个完整示例
这个完整的例子来自引导加载器:
LOAD_FAT:
; 保存起始扇
mov si, msgCRLF
call Print
mov dx, WORD [di + 0x001A]
mov WORD [cluster], dx ; 文件的第一个簇
; 计算FAT 大小不存在"cx" 中
xor ax, ax
mov al, BYTE [bpbNumberOfFATs] ; FAT 数
mul WORD [bpbSectorsPerFAT] ; 每FAT 扇区数
mov cx, ax
; 计算FAT 起点不存在"ax" 中
mov ax, WORD [bpbReservedSectors] ; 加保留扇
; 将FAT 读入内存 (7C00:0200)
mov bx, 0x0200 ; 复制FAT
call ReadSectors
LBA 和 CHS
在加载映像时,我们得在加载每个扇区时查看FAT 。
这儿有一个我们还没有讨论到的小问题。我们从FAT 得到了一个簇号,但是,怎么用啊 ?
问题是簇号代表一个线性地址,而为了加载扇区,我们得使用磁道/ 磁头/ 扇区这样的地址。 (0x13 号中断)
有两种 方法访问磁盘。通过磁道/ 磁头/ 扇区(Cylinder/Head/Sector (CHS) )addressing 或者逻辑块地址(LBA) .
LBA 表示 磁盘的一个索引位置。第1 个块是0 ,下一个是1 ,等等。LBA 简单的表示从0 开始的序号,再简单不过。
我们需要了解如何在 LBA 和 CHS 之间转换。
将 CHS 转换为 LBA
将 CHS 转换为 LBA 的公式:
LBA = (cluster - 2 ) * 扇区数每簇
够简单。这是例子:
sub ax, 0x0002 ; 从簇号减2
xor cx, cx
mov cl, BYTE [bpbSectorsPerCluster] ; 扇区数每簇
mul cx ; 乘
将 LBA 转换为 CHS
这要复杂些,但也相对简单:
绝对扇区 = ( 逻辑扇 / 扇区数每磁道) + 1
绝对磁头 = ( 逻辑扇 / 扇区数每磁道) MOD 磁头数
绝对磁道 = 逻辑扇 / ( 扇区数每磁道 * 磁头数)
例:
LBACHS:
xor dx, dx ; 准备dx:ax
div WORD [bpbSectorsPerTrack] ; 除扇区数每磁道
inc dl ; 加1 (扇区公式)
mov BYTE [absoluteSector], dl
; 下面很类似
xor dx, dx ; 准备dx:ax
div WORD [bpbHeadsPerCylinder] ; 模磁头数
; (磁头公式)
mov BYTE [absoluteHead], dl ; 第1 个公式中已得到
mov BYTE [absoluteTrack], al ; 不需要再做了
ret
不难吧,我想是的。
加载簇
好了,加载Stage 2, 我们首先需要查看FAT 。很简单,然后把它转换为LBA 这样我们就能读入了:
mov ax, [cluster] ; 要读的簇
pop bx ; 读缓冲
call ClusterLBA ; 转换簇到LBA
xor cx, cx
mov cl, [bpbSectorsPerCluster] ; 要读的扇区
call ReadSectors ; 读簇
push bx
得到下一个簇
这是一个技巧。
好的,记得,每个簇号都是12 比特。 这是一个问题,如果我们读1 字节,我们只得到簇号的一部分!
因此,我们得读一个WORD (2 byte) 。
唉,我们又有一个问题。( 从12 比特的值中) 复制两字节,意味着我们复制了下一个簇的一部分。 比如,想象一下你的FAT :
注意:二进制数按字节分开
每12 比特的簇显示如下
01011101 0 0111010 01110101 00111101 00011101 0111010 0011110 00011110
| | | | | |
| |-----1簇----- | |-----3簇----| |
|----0 簇----| |------2簇------| |------4簇-----|
注意:所有的偶数簇,都占有全部的第1 字节,和第2 字节的一部分;所有的奇数簇,都占有全部的第2 字节,和第1 字节的一部分!
好,因此我们需要从FAT 读两个字节。
如果簇号是偶数, 掩去高4 比特,因为它属于下一个簇。
如果簇号是奇数,右移4 比特(去掉前一个簇使用的比特) 。例如:
; 计算下一个簇
mov ax, WORD [cluster] ; 从FAT 得到当前簇
; 奇数还是偶数?除2 看看!
mov cx, ax ; 复制当前簇
mov dx, ax ; 复制当前簇
shr dx, 0x0001 ; 除2
add cx, dx ; 3/2
mov bx, 0x0200 ; FAT 在内存中的地址
add bx, cx ; FAT 的索引
mov dx, WORD [bx] ; 从FAT 读2 字节
test ax, 0x0001
jnz .ODD_CLUSTER
; FAT 中每项12 比特,如果是0x002 到0xFEF ,
; 我们只需要读取这12 比特,它代表下一个簇
.EVEN_CLUSTER:
and dx, 0000111111111111b ; 取低12 位
jmp .DONE
.ODD_CLUSTER:
shr dx, 0x0004 ; 取高12 位
.DONE:
mov WORD [cluster], dx ; 保存新簇
cmp dx, 0x0FF0 ; 是否是文件结尾?
jb LOAD_IMAGE ; 完成,下一个簇
Demo
第一个截屏显示引导加载器加载Stage 2 成功,Stage 2 打印加载操作系统信息。
第二个截屏显示:当文件(在根目录中)不存在时,显示一个错误信息。
这个演示,包含了本章中的大部分代码,有2 个源文件,2 个目录和2 个批处理文件。第1 个文件夹包含stage 1 程序——我们的引导加载器,第2 个文件夹包含stage 2 程序——STAGE2.SYS.
DEMO DOWNLOAD HERE
总结
Wow, 这章很难写。因为很难把一个复杂的话题解释的很详细并且还易于理解,我希望我做到了
如果你对这一章有任何建议使其有所提升的话,请让我知道 J
好的,我想是时候:向引导加载器说再见了!
下一章我们将开始构建Stage 2 。我们会讨论A20 、以及更详细讨论保护模式……
再见!
再见!