从头开始编写操作系统(7) 第6章:引导加载器4
来源:互联网 发布:钱塘数据交易中心 编辑:程序博客网 时间:2024/05/29 18:05
译自: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 不支持分级目录,这意味着只有一个文件夹——根目录。
- 簇地址只有12位长,这限制了最多只有4096个簇
- 文件名12个字节,不能相同,簇地址表示文件的起始簇。
- 因为簇大小的限制,最多有4,077个文件
- 磁盘大小保存在一个16位的数值中(以扇区为单位),这限制它,最多有32 MB
- FAT12 使用 "0x01"标示分区
这是很大的限制,我们为什么要用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是一个像链表一样的结构,用于标识哪个簇正在被使用。
为了更好的理解,我们看看它们可能的取值:
- 空闲簇: 0x00
- 保留簇: 0x01
- 使用中的扇——其值表示下一个簇: 0x002 到 0xFEF
- 保留值 : 0xFF0 到 0xFF6
- 坏簇: 0xFF7
- 文件结束的簇: 0xFF8 到 0xFFF
FAT仅仅是上面这些值构成的简单数组,仅仅这样。当我们从根目录找到一个文件的起始簇后,我们就可以通过查找FAT来决定加载哪个簇。怎么做呢?我们简单的检查这个值。如果这个值在0x02 和 0xfef之间,这个值表示我们要加载的下一个簇。
让我们更深入的看看这个问题。一个簇,如你所知,代表一系列扇区。我们在BPB中定义了一个簇所包含的扇区数:
bpbBytesPerSector: DW 512
bpbSectorsPerCluster: DB1
在这里,每个簇1扇区。当我们找到Stage2的第一个扇区(我们从根目录中得到),我们用这个扇区作为FAT的起始簇。一旦我们找到了起始簇,我们就可以通过查找FAT来确定下一个簇(FAT仅仅是32位数的数组,我们只需要上面的列表确定做什么就行了)。
根目录表
现在,这对于我们非常重要。
更文件夹是一个表,表中每项都是32字节,表示文件及文件夹的信息。这32字节的格式如下:
- Bytes 0-7 : DOS文件名(空格扩展)
- Bytes 8-10 : DOS文件扩展(空格扩展)
- Bytes 11 : 文件属性。为模式如下:
- Bit 0 : 只读
- Bit 1 : 隐藏
- Bit 2 : 系统
- Bit 3 : 卷标
- Bit 4 : 文件夹
- Bit 5 : 压缩
- Bit 6 : 设备 (只在内部使用)
- Bit 6 : 未使用
- Bytes 12 : 未使用
- Bytes 13 : 以ms为单位的创建时间
- Bytes 14-15 : 创建时间,格式如下:
- Bit 0-4 : 秒 (0-29)
- Bit 5-10 : 分 (0-59)
- Bit 11-15 : 时 (0-23)
- Bytes 16-17 : 创建日期,格式如下:
- Bit 0-4 : 年 (0=1980; 127=2107)
- Bit 5-8 : 月 (1=1月; 12=12月)
- Bit 9-15 : 日 (0-31)
- Bytes 18-19 : 最后访问日期 (格式同上)
- Bytes 20-21 : EA 索引 (OS/2 和 NT中使用,不用考虑)
- Bytes 22-23 : 最后修改时间 (参考bytes 14-15的格式)
- Bytes 24-25 : 最后修改日期 (参考bytes 16-17 的格式)
- Bytes 26-27 : 第一个簇
- Bytes 28-32 : 文件大小
我加粗了重要的部分——剩下的是Microsoft要考虑的,我们会在创建FAT12驱动器时再考虑,还有些时候呢。
等等!还记得DOS的文件名限制在11字节吗?这样:
- Bytes 0-7 : DOS文件名(空格扩展)
- Bytes 8-10 : DOS文件扩展(空格扩展)
0 到 10, hmm... 是11字节。一个不足11字节的文件名会与上面的数据项(上面列出的32 字节)不匹配。当然,这不行,我们得扩展使它变成11字节。
记得我们在前面的教程中说的内部名和外部名吗?我现在解释的结构是内部名。它被现在在11字节所以文件名"Stage2.sys"会变成:
"STAGE2 SYS" (注意扩展!)
查找并读取FAT12 – 理论
好的,看完了上面内容,你可能已经很烦我再说"FAT12"了。
上面的信息,怎么起作用的呢?
后面我们将会参考BPB。这是一个我们在前面的教程中创建的BPB:
bpbBytesPerSector: DW 512
bpbSectorsPerCluster: DB1
bpbReservedSectors: DW1
bpbNumberOfFATs: DB2
bpbRootEntries: DW224
bpbTotalSectors: DW2880
bpbMedia: DB0xF0
bpbSectorsPerFAT: DW9
bpbSectorsPerTrack: DW18
bpbHeadsPerCylinder: DW2
bpbHiddenSectors: DD0
bpbTotalSectorsBig: DD 0
bsDriveNumber: DB 0
bsUnused: DB0
bsExtBootSignature: DB0x29
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 operatingsystem...",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 isanother 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: 获取起始簇
好了,根目录被加载了进来,而且,我们找到了文件对应的表项。我们怎么找到它的起始簇呢?
- Bytes 26-27 : 起始簇
- Bytes 28-32 : 文件大小
看起来很像,为了得到起始簇,访问表项的第26字节:
mov dx, [di +0x001A] ; di 保存表项起始地址. 访问第26字节 (0x1A)
; 现在dx保存有起始簇号
起始簇对于文件加载很重要。
Step 2: 获取FAT大小
我们再看看BIOS 参数块。
bpbNumberOfFATs: DB2
bpbSectorsPerFAT: DW9
好,我们知道两个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 00111010 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、以及更详细讨论保护模式……
再见!
再见!
- 从头开始编写操作系统(7) 第6章:引导加载器4
- 从头开始编写操作系统(6) 第5章:引导加载器3
- 从头开始编写操作系统(4) 第3章:引导加载器
- 从头开始编写操作系统(5) 第4章:引导加载器2
- 从头开始编写操作系统(1) 第0章:序章
- 从头开始编写操作系统(2) 第1章:介绍
- 从头开始编写操作系统(3) 第2章:基本理论
- 从头开始编写操作系统(8) 第7章:系统结构
- 从头开始编写操作系统(9) 第8章:保护模式
- 从头开始编写操作系统(10) 第9章:开启A20
- 从头开始编写操作系统(11) 第10章:为内核做准备1
- 从头开始编写操作系统
- 【从头开始写操作系统系列】实现一个-GDT(1)
- 【从头开始写操作系统系列】实现一个-GDT(2)
- 【从头开始写操作系统系列】实现一个 GDT(3)
- 操作系统编写之引导扇区
- 开始学习编写操作系统
- 操作系统实践(1)——从引导开始
- 怎样的环境可以创造怎样的性格
- 视角的力量--再说OO设计原则
- Linux ln(link) 命令详解
- 【贪吃蛇—Java程序员写Android游戏】系列 0. 前言几句话
- FLEX 中文手册
- 从头开始编写操作系统(7) 第6章:引导加载器4
- MySQL数据库管理常用命令
- cifs nfs mount O_DIRECT
- 计算机基础知识——计算机的存储单位
- /CAD/cadence/tools/bin/64bit/simvision: 61: cds_plat: not found
- VB.NET获取MAC地址
- Grails 多条件查询和分页
- fgets和gets的用法
- PowerDesigner技巧