操作系统开发系列——Bootloaders
Translated By Matrix7
本系列旨在展示和教授如何从零开始开发一个操作系统。
简介
欢迎!在前一章中我们学习了如何加载和执行一个扇区。同时我们学习了汇编语言中的Ring以及详细了解了BIOS参数块(BPB)。
在这一章中,我们将用我们所学的所有东西去解析FAT12文件系统,同时通过名字来加载我们的二级loader。
这一章中会包含很多的代码。我将会尽我所能去解释每一个细节。同时,这一章中会需要一些数学知识。准备好了吗?
cli和hlt
你可能会好奇,为什么我所有的演示程序都以“cli”和“hlt”指令结尾。事实上它非常简单。如果没有一种停止程序的某种方式,则CPU会超出你的程序去执行一些随机的指令。这将最终导致结束于一个三重故障。
我清除中断的原因是因为中断会被执行(在系统没有停止之前),甚至在我想要停止系统的时候。这会产生一些问题。因此,如果仅仅只有hlt指令(没有cli)也会是cpu产生三重故障。
因此,我总是以cli和hlt来结束我的演示。
文件系统——理论
好极了!是时候来学习文件系统了:)
一个文件系统就是一个详细的规格。用于帮组我们建立磁盘上“文件”的概念。
一个文件是一组代表某种东西的数据。这些数据可以是任何我们需要的。它完全依赖于我们如何解释这些数据。
正如你所知,一个扇区是512字节。一个文件在磁盘是以跨越扇区的形式存储。如果文件超出了512字节,我们需要给它分配更多的扇区。因为并不是所有的文件都是均匀的512字节,我们需要填充剩下的字节(那些文件不需要的)。就像我们在我们的bootloader中所做的一样。
如果一个文件跨越几个扇区,在FAT文件系统中我们称这些扇区为一个簇。例如,我们的内核将会很可能跨越许多个扇区。为了加载我们的内核,我们需要从它所在的地方加载簇(几个扇区)。
如果一个文件跨越了都个扇区(不连续)跨越了多个簇,那么就称它形成了碎片。我们将需要收集文件的不同部分。
有许多不同种类的文件系统。其中一些被广泛运用(如FAT12,FAT16,FAT32,NTFS,ext(Linux),HFS(用在比较老的MAC中));其他的一些文件系统仅仅被特定的公司内部使用(如GFS——Google File System)。
许多的操作系统开发人员在FAT文件系统的基础上创建他们自己的文件系统(或者甚至一些完全新的东西)。这些通常没有最常用的文件系统(如FAT和NTFS)好。
OK,我们现在知道了一点文件系统的知识。我们将会使用FAT12文件系统,因为它简单。如果我们想,我们同样可以用完全不同的另外一种:)
FAT12文件系统——理论
FAT12是第一个FAT(File Allocation Table)文件系统,发布于1977年,用在微软磁盘BASIC版。FAT12,作为一款比较老的,通常用于软盘的文件系统,通常有一些限制。
- FAT12不支持分层目录。这意味着它只有一个目录——根目录。
- 簇地址仅为12位长,限制了最大的簇数为4096
- 文件名以一个12位的标识存储在文件分配表中。簇地址代表文件的其实簇。
- 因为簇地址的限制,最大文件数为4077
- 磁盘的大小仅以16位计数扇区,限制其大小为32MB大小
- FAT12使用“0x01”标识分区
既然有这么大的限制,那么为什么我们要使用FAT12文件系统呢?
FAT16支持目录,以及超过64000个文件因为它使用了一个16位的簇(文件)地址,与其FAT16的名字相符。然而,FAT16和FAT12非常相似。
为了简便,我们将使用FAT12。我们以后可能会用FAT16(或者甚至FAT32)来完善我们的系统:)(FAT32与FAT12/16有相当大的区别,所以我们后来可能最终使用FAT16)。
FAT12文件系统——磁盘存储
为了更多地理解FAT12文件系统以及它是如何工作的,我们最好首先看看一个典型的格式化后的磁盘的结构。
| 引导扇区 | 额外保留扇区 | 文件分配表 1 | 文件分配表 2 | 根目录 (仅用于FAT12/FAT16) | 数据区域包含文件和文件夹 |
这是一个典型的格式化后的FAT12磁盘,从bootloader到磁盘的最后一个扇区。
理解这个结构对于我们加载和搜索我们的文件非常关键。
注意一个磁盘上有2个FAT。它的位置在保留扇区(或者bootloader,如果没有保留扇区)的右边。
同样注意:根目录在所有的FAT的右边。这意味着…
如果我们将每个FAT的扇区个数加起来,还有保留扇区,我们就可以得到根目录的第一个扇区。通过搜索根目录中的一个简单字符串(我们的文件名),我们就能够找到文件在磁盘上所在的确切扇区:)。
让我们更进一步学习…
引导扇区
这部分包含BIOS参数块和bootloader。是的——我们的。BIOS参数块包含用于描述我们磁盘的信息。
额外保留扇区
还记得我们的BPB中的bpbReservedSectors 成员吗?任何额外的保留扇区被储存在这,就在引导扇区的后面。
文件分配表(FATs)
我们知道一个簇表示磁盘上一系列连续的扇区,每个簇的大小一般在2KB到32KB之间。文件片段是通过一个簇到另一个簇不断链接起来的,使用了一种通用的数据结构,比如说一个链表。
总共有两个文件分配表。然而,后一个文件分配表只是前一个的拷贝,用于数据恢复的目的,通常不会使用它。
文件分配表(FAT)是一个映射到相应簇的实体的链表。他们帮助我们标识重要信息,以助于在这些簇中存储数据。
每一个实体是一个12位的值代表一个簇。文件分配表是一个类似于链表的数据结构,它的实体用于帮助我们表示正在使用的簇。
为了更好地理解,让我们看看它的一些可能值:
- 标识未使用的簇的值:0x00
- 标识保留簇的值:0x01
- 正在使用的簇——它的值代表下一个簇:0x002到0Xfef
- 保留值:0xFF0到0xFF6
- 标识坏簇的值:0xFF7
- 标识文件中最后一个簇的值:0xFF8到0xFFF
一个文件分配表就是这些值的一个数组——这就是全部。当我们从根目录中找到起始扇区,我们就可以查询FAT找出需要加载哪些簇。如何实现?我们只需要检查它的值。如果值在0x02到0xfef之间 ,它的值就表示访问这个文件所需要加载的下一个簇。
让我们更深入地学习它。一个簇,正如你所知,代表一系列扇区。我们在BIOS参数块中定义一个簇所代表的扇区数:
bpbBytesPerSector: DW 512 bpbSectorsPerCluster: DB 1
在这里,一个簇是一个扇区。当我们得到了Stage 2的第一个扇区(我们通过根目录获取),我们就可以将这个扇区作为FAT中的起始簇号。一旦我们找到了起始簇,我们只需要引用FAT来确定簇(FAT就是一个12位数的数组。我们只需要将这些数与上面列出来的值比较就可以确定如何处理它们。)
根目录表
现在,它对我们非常重要:)
根目录是一个表示相关文件和目录信息的32字节值的表。这个32位值使用如下的格式:
- 字节0-7:DOS文件名(多的用空格填充)
- 字节8-10:DOS文件扩展名(多的用空格填充)
- 字节11:文件属性。这是一个位模式:
- 位0:只读
- 位1:隐藏
- 位2:系统
- 位3:卷标签
- 位4:这是一个子目录
- 位5:存档
- 位6:设备(内部使用)
- 位7:未使用
- 字节12:未使用
- 字节13:创建时间单位毫秒
- 字节14-15:创建时间,使用下面的格式:
- 位0-4:秒(0-29)
- 位5-10:分钟(0-59)
- 位11-15:小时(0-23)
- 字节16-17:创建日期,使用下面的格式:
- 位0-4:年(0=1980;127=2107)
- 位5-8:月(1=一月;12=十二月)
- 位9-15:日(1-31)
- 字节18-19:上一次访问日期(和上面一样的格式)
- 字节20-21:EA索引(用在OS/2和NT中,不用担心它)
- 字节22-23:上一次修改时间(格式参照字节14-15)
- 字节24-25:上一次修改日期(格式参照字节16-17)
- 字节26-27:第一个簇
- 字节28-32:文件大小
我加粗了其中重要的部分——其他的都是垃圾,微软增加了我们后面在创建FAT12驱动器时要增加的东西。等一下!
还记得DOS文件名被限制在11字节?这是为什么?
- 字节0-7:DOS文件名(多的用空格填充)
- 字节8-10:DOS文件扩展名(多的用空格填充)
0到10,嗯…总共11字节。如果一个文件名小于11字节,将会错过数据实体(上面显示的32字节实体表)。这当然很糟糕:)因此,我们不得不用字符填充文件名,保证它是11字节。
还记得上一章中我解释过什么是内部和外部文件名吗?我解释过的文件名结构是内部文件名。因为它被限制为11字节,所以文件名“Stage2.sys”必须为
"STAGE2 SYS" (Note the padding!)
搜索和读取FAT12——理论
好,阅读上面的内容之后,你可能厌倦我说“FAT12”了:)
无论如何…这些信息都对我们来说非常重要!
我们下面将更多地参考BIOS参数块(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: B 0 bsExtBootSignature: DB 0x29 bsSerialNumber: DD 0xa0a1a2a3 bsVolumeLabel: DB "MOS FLOPPY " bsFileSystem: DB "FAT12 "
请参看前一章中关于每个成员的详细解释。
我们要做的是试着去加载一个二级加载器。让我们看看我们具体需要做些什么:
从文件名开始
第一件要做的事儿就是创建一个好的文件名。记住:文件名必须准确地为11字节,以保证我们不会破坏根目录。
我使用“STAGE2.SYS”作为我的二级loader名。你可以在上一节中查看它的内部文件名。
创建Stage 2
好了,Stage 2是一个独立于bootloader的程序。我们的Stage2将会非常类似一个DOS COM程序,是不是很酷?
到目前为止,Stage2所做的仅仅是打印一个消息,并停机。这些你在bootloader中已经见过了:
; Note: Here, we are executed like a normal ; COM program, but we are still in Ring 0. ; We will use this loader to set up 32 bit ; mode and basic exception handling ; This loaded program will be our 32 bit Kernel. ; We do not have the limitation of 512 bytes here, ; so we can add anything we want here! org 0x0 ; offset to 0, we will set segments later bits 16 ; we are still in real mode ; we are loaded at linear address 0x10000 jmp main ; jump to main ;*************************************************; ; Prints a string ; DS=>SI: 0 terminated string ;************************************************; Print: lodsb ; load next byte from string from SI to AL or al, al ; Does AL=0? jz PrintDone ; Yep, null terminator found-bail out mov ah, 0eh ; Nope-Print the character int 10h jmp Print ; Repeat until null terminator found PrintDone: ret ; we are done, so return ;*************************************************; ; Second Stage Loader Entry Point ;************************************************; main: cli ; clear interrupts push cs ; Insure DS=CS pop ds mov si, Msg call Print cli ; clear interrupts to prevent triple faults hlt ; hault the system ;*************************************************; ; Data Section ;************************************************; 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:)
第一步:加载根目录表
现在是加载Stage2.sys的时候了!在这里我们将会多次引用根目录表,以及BIOS参数块以获取磁盘信息。
步骤1:获取根目录的大小
好了,第一步我们需要获取根目录的大小。
为了获取其大小,只需要将根目录中实体的个数加起来。看起来非常简单:)
在Windows中,当你增加一个文件或者目录到一个以FAT12格式化的磁盘,Windows会自动地将文件信息加到根目录中去,因此我们不需要担心它。这使得事情更加简单。
将根目录实体数除以每个扇区的字节数就可以知道根实体使用了多少个扇区。
下面是一个例子:
mov ax, 0x0020 ; 32 byte directory entry
mul WORD [bpbRootEntries] ; number of root entrys
div WORD [bpbBytesPerSector] ; get sectors used by root directory
记住根目录表是一个代表文件信息的32字节值(实体)表。
很好,我们知道了加载根目录所需的扇区数。现在,让我们寻找要加的起始扇区吧:)
步骤2:获取根目录的起始处
这同样是非常简单的一步。首先,让我们再次看一看一个FAT12格式化的磁盘的结构:
| 引导扇区 | 额外保留扇区 | 文件分配表 1 | 文件分配表 2 | 根目录 (仅用于FAT12/FAT16) | 数据区域包含文件和文件夹 |
好的,注意根目录就在两个文件分配表和保留扇区的后面。换句话说,只需要将文件分配表和保留扇区加起来,你就可以找到根目录!
例如…
mov al, [bpbNumberOfFATs] ; Get number of FATs (Useually 2)
mul [bpbSectorsPerFAT] ; number of FATs * sectors per FAT; get number of sectors
add ax, [bpbReservedSectors] ; add reserved sectors
; Now, AX = starting sector of root directory
很简单,嗯?现在,我们只需要将扇区读取到内存的特定位置:
mov bx, 0x0200 ; load root directory to 7c00:0x0200
call ReadSectors
根目录——完整的例子
这段代码是直接从本章结束的bootloader中抽取的代码。它加载了根目录:
LOAD_ROOT:
; compute size of root directory and store in "cx"
xor cx, cx
xor dx, dx
mov ax, 0x0020 ; 32 byte directory entry
mul WORD [bpbRootEntries] ; total size of directory
div WORD [bpbBytesPerSector] ; sectors used by directory
xchg ax, cx
; compute location of root directory and store in "ax"
mov al, BYTE [bpbNumberOfFATs] ; number of FATs
mul WORD [bpbSectorsPerFAT] ; sectors used by FATs
add ax, WORD [bpbReservedSectors] ; adjust for bootsector
mov WORD [datasector], ax ; base of root directory
add WORD [datasector], cx
; read root directory into memory (7C00:0200)
mov bx, 0x0200 ; copy root dir above bootcode
call ReadSectors
第二步:寻找Stage 2
好了,现在根目录表已经被加载。看看上面代码,我们将它加载到0x200。现在,去寻找我们的文件。
让我们再次回去看看32字节的根目录表(根目录表这一节)。记住最前面的11字节表示文件名,同时记住,因为每个根目录实体是32字节,每个32字节都会是下一个实体的开始——指向下一个实体的最先的11字节。
从现在开始,我们所需要做的就是比较文件名,然后跳到下一个实体(32字节),然后再次测试,直到我们搜索到扇区的结尾。例如…
; browse root directory for binary image
mov cx, [bpbRootEntries] ; the number of entrys. If we reach 0, file doesnt exist
mov di, 0x0200 ; Root directory was loaded here
.LOOP:
push cx
mov cx, 11 ; eleven character name
mov si, ImageName ; compare the 11 bytes with the name of our file
push di
rep cmpsb ; test for entry match
pop di
je LOAD_FAT ; they match, so begin loading FAT
pop cx
add di, 32 ; they dont match, so go to next entry (32 bytes)
loop .LOOP
jmp FAILURE ; no more entrys left, file doesnt exist :(
成功进入下一步…
第三步:加载文件分配表
步骤1:获取起始簇
好了,现在根目录被加载,我们也找到了文件实体。那么我们如何获取其实簇呢?
- 字节26-27:第一个簇
- 字节28-32:文件大小
这看起来很熟悉:)为了获取其实簇,只需要引用文件实体中的字节26:
mov dx, [di + 0x001A] ; di contains starting address of entry. Just refrence byte 26 (0x1A) of entry ; Yippe--dx now stores the starting cluster number
起始簇在加载文件时对我们来说非常重要。
步骤2:获取文件分配表的大小
让我们再次更加具有针对性地看看BIOS参数块。
bpbNumberOfFATs: DB 2 bpbSectorsPerFAT: DW 9
好,那么我们如何知道两个文件本配表用了多少个扇区呢?只需要将每个文件分配表的扇区数乘以文件分配表的个数:)看起来很简单,…但是…
xor ax, ax
mov al, [bpbNumberOfFATs] ; number of FATs
mul WORD [bpbSectorsPerFAT] ; multiply by number of sectors per FAT
; ax = number of sectors the FATs use!
永远不用担心,就是这么简单^^
步骤3:加载文件分配表
现在我们知道了我们需要读取多少个扇区。那么,嗯…读取它吧:)
mov bx, 0x0200 ; address to load to
call ReadSectors ; load the FAT table
耶!现在FAT已经被加载(不完全!),将在stage 2中加载!
文件分配表——完整的例子
下面是bootloader抽取的完整代码:
LOAD_FAT:
; save starting cluster of boot image
mov si, msgCRLF
call Print
mov dx, WORD [di + 0x001A]
mov WORD [cluster], dx ; file's first cluster
; compute size of FAT and store in "cx"
xor ax, ax
mov al, BYTE [bpbNumberOfFATs] ; number of FATs
mul WORD [bpbSectorsPerFAT] ; sectors used by FATs
mov cx, ax
; compute location of FAT and store in "ax"
mov ax, WORD [bpbReservedSectors] ; adjust for bootsector
; read FAT into memory (7C00:0200)
mov bx, 0x0200 ; copy FAT above bootcode
call ReadSectors
LBA和CHS
加载镜像的过程中,我们所需要做的就是加载FAT中的每一个簇。
这里还有一个小问题我们没有讨论。好了,我们有了一个从文件分配表获取的簇号。但是,我们如何使用它呢?
问题在于,簇表示一个线性地址,然而,为了加载扇区,我们需要一个段/磁道/面地址。(中断0x13)
有两种方法访问磁盘。通过柱面/面/扇区(CHS)地址或者逻辑快地址(LBA)。
LBA表示一个被索引的磁盘地址。第一个块是0,然后是1,继续下去。LBA仅仅代表从0开始连续被编号的扇区。没有比着更基本的了。
我们必须知道如何在LBA和CHS之间相互转换。
将CHS转换为LBA
转换CHS为LBA的公式为:
LBA = (cluster - 2 ) * sectors per cluster
这非常简单。:)下面是一个例子:
sub ax, 0x0002 ; subtract 2 from cluster number
xor cx, cx
mov cl, BYTE [bpbSectorsPerCluster] ; get sectors per cluster
mul cx ; multply
将LBA转换为CHS
这有一点点复杂,但相对来说还是很简单:
absolute sector = (logical sector / sectors per track) + 1
absolute head = (logical sector / sectors per track) MOD number of heads
absolute track = logical sector / (sectors per track * number of heads)
下面是例子…
LBACHS:
xor dx, dx ; prepare dx:ax for operation
div WORD [bpbSectorsPerTrack] ; divide by sectors per track
inc dl ; add 1 (obsolute sector formula)
mov BYTE [absoluteSector], dl
; these forumlas are very simular...
xor dx, dx ; prepare dx:ax for operation
div WORD [bpbHeadsPerCylinder] ; mod by number of heads (Absolue head formula)
mov BYTE [absoluteHead], dl ; everything else was already done from the first formula
mov BYTE [absoluteTrack], al ; not much else to do :)
ret
不是太难,我希望:)
加载簇
好了,在加载Stage 2的过程中,我们首先需要获取文件分配表中的簇。这很简单。然后,将簇号转换为我们可以读取的LBA:
mov ax, [cluster] ; cluster to read
pop bx ; buffer to read into
call ClusterLBA ; convert cluster to LBA
xor cx, cx
mov cl, [bpbSectorsPerCluster] ; sectors to read
call ReadSectors ; read in cluster
push bx
获取下一个簇
这个有点棘手。
好了,记住,每个文件分配表实体中的簇号是12位。这就是问题所在。如果我们读取1字节,我们仅仅复制了簇号中的一部分!
因此,我们必须读取一个WORD(2字节)值。
好了,现在,我们又遇到问题。复制2字节(从一个12位值)以为这我们将会复制下一个簇实体的一部分。例如,想象你的下面是你的文件分配表:
Note: Binary numbers seperated in bytes.
Each 12 bit FAT cluster entry is displayed.
| |
01011101 0111010 01110101 00111101 0011101 0111010 0011110 0011110
| | | | | |
| |1st cluster | |3rd cluster-| |
|-0 cluster ----| |2nd cluster---| |4th cluster----|
注意到所有偶数的簇需要复制所有的第一个字节和第二字节的一部分。同样所有奇数簇需要复制一部分第一字节和所有的第二字节!
好了,我们所需要做的就是从文件分配表(我们的簇)读取2个字节(WORD)值。
如果这个簇是偶数,将高4位掩盖掉,因为它属于下一个簇。
如果是奇数,向低位移动4位(丢弃第一个簇使用的位)。例如…
; compute next cluster
mov ax, WORD [cluster] ; identify current cluster from FAT
; is the cluster odd or even? Just divide it by 2 and test!
mov cx, ax ; copy current cluster
mov dx, ax ; copy current cluster
shr dx, 0x0001 ; divide by two
add cx, dx ; sum for (3/2)
mov bx, 0x0200 ; location of FAT in memory
add bx, cx ; index into FAT
mov dx, WORD [bx] ; read two bytes from FAT
test ax, 0x0001
jnz .ODD_CLUSTER
; Remember that each entry in the FAT is a 12 but value. If it represents
; a cluster (0x002 through 0xFEF) then we only want to get those 12 bits
; that represent the next cluster
.EVEN_CLUSTER:
and dx, 0000111111111111b ; take low twelve bits
jmp .DONE
.ODD_CLUSTER:
shr dx, 0x0004 ; take high twelve bits
.DONE:
mov WORD [cluster], dx ; store new cluster
cmp dx, 0x0FF0 ; test for end of file
jb LOAD_IMAGE ; we are not done yet--go to next cluster
演示
第一幅图显示bootloader加载Stage 2成功。Stage 2打印加载操作系统的消息。
第二幅图显示一个当无法发现文件(从根目录中)时打印的错误消息。
演示中博阿含这节课中的绝大部分代码,2个源文件,2个目录,以及2个批处理程序。第一个目录包含Stage 1程序——我们的bootloader,第二个目录包含我们的Stage 2程序——STAGE2.SYS。
结论
哇噢,这一章非常难写。因为从细节上解释一个复杂的话题,还必须使它学起来很简单
,这非常难。我相信我做的还不错:)
如果你有任何建议来提高这一章,请告诉我:)
好了…我想我们可以:和bootloader说再见了!
下一章中,我们会开始创建Stage 2。我们会学习A20,学习保护模式的更多细节…
再见了!
未完待续!
原文地址:http://www.brokenthorn.com/Resources/OSDev6.html
译者英文水平有限,翻译不妥处,欢迎大家指正!
作者:迷途羔羊 | 本文链接:【翻译】操作系统开发系列——Bootloaders 4
刚才才发现 大概2个月前拜访过你的这个系列的文章.看了下从一年前开始写,陆陆续续,也应该是去动力过. 国内环境都是转的多看热闹的多,有时让人些许心寒. 恩.敬佩~
前面说翻译了一部分的本来就是说你,当时没什么印象,发现阅读器刷新了这个相似的文章才想起来- -
嘿嘿,总之,加油~(可能帮不上实质的忙~)
[Reply]
Matrix7
Reply:
June 10th, 2011 at 08:20
我是说。。。貌似翻译的就我一个,确实断了好长时间,因为之前很多事情,而且翻译的确是个很苦很累的活儿,翻译一篇要花好长时间。网页上的google翻译我看到了,但是我觉得机器翻译出来的没法看吧!后面我加快进度吧,争取今年能翻译个80%。
[Reply]
又一位翻译这个教材的.看来这个教程的确是很经典的. 不知道您注意到没有 http://www.brokenthorn.com/Resources/OSDev0.html 的开篇上面有谷歌翻译,网站的人员也很有心.
我见到过挺多只译了一部分的版本. 我觉得翻译这篇是个挺巨大的工程.
[Reply]