从头开始编写操作系统(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版本(甚至是全新的),这些一般没有流行的文件系统(FATNTFS)好。

好了,我们知道了文件系统的一些基础知识。为了简单,我们使用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)(FAT32FAT 12/16差别很大,所以我们更可能会在后面使用FAT16)

FAT12 文件系统磁盘分布

为了更好的理解FAT12以及了解它如何工作,我们最好是看看它在一个格式化号的物理磁盘上的结构。

引导扇

保留扇

FAT1

FAT2

根目录(仅在FAT12/FAT16)

数据区

这是一个典型的FAT12磁盘,包括了从引导扇区开始到磁盘的的最后一个扇区。

理解这个结构对于文件的搜索和加载是很重要的。

注意在磁盘上有两个FAT。它们正跟在保留扇之后(或者引导扇之后,如果没有保留扇的话)。

另外注意:根目录正好在FAT之后。这意味着……

如果我们把每个FAT的扇区数和保留扇区数加起来,就得到了根目录的第一个扇区。通过在根目录搜索一个简单是字符串(我们的文件名)我们就可以找到保存文件的扇区。

详细些……

引导扇

这是BIOS参数块和引导加载器所在的扇区。BIOS参数块包含有对磁盘的描述信息。

附加保留扇

还记得在BPB中的bpbReservedSectors字段吗?所有的附加保留扇都在这里,正好在引导扇之后。

文件分配表(FAT)

簇是一系列连续的扇区。簇的大小一般是2 KB32 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代表那个引导加载器之后执行的程序。我们的Stage2DOS 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]       ; FAT2字节

 

          test    ax, 0x0001

          jnz     .ODD_CLUSTER

         

; FAT中每项12比特,如果是0x0020xFEF

; 我们只需要读取这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

http://www.brokenthorn.com/Resources/images/boot.gif

第一个截屏显示引导加载器加载Stage 2成功,Stage 2 打印加载操作系统信息。

第二个截屏显示:当文件(在根目录中)不存在时,显示一个错误信息。

这个演示,包含了本章中的大部分代码,有2个源文件,2个目录和2个批处理文件。第1个文件夹包含stage 1程序——我们的引导加载器,第2个文件夹包含stage 2程序——STAGE2.SYS.

DEMO DOWNLOAD HERE

总结

Wow,这章很难写。因为很难把一个复杂的话题解释的很详细并且还易于理解,我希望我做到了

如果你对这一章有任何建议使其有所提升的话,请让我知道J

好的,我想是时候:向引导加载器说再见了!

下一章我们将开始构建Stage 2。我们会讨论A20、以及更详细讨论保护模式……

再见!

再见!

原创粉丝点击