从头开始编写操作系统(7) 第6章:引导加载器4

译自: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: DB 1

在这里,每个簇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: 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 代表那个引导加载器之后执行的程序。我们的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 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: 获取起始簇

好了,根目录被加载了进来,而且,我们找到了文件对应的表项。我们怎么找到它的起始簇呢?

  • Bytes 26-27 : 起始簇
  • Bytes 28-32 : 文件大小

看起来很像,为了得到起始簇,访问表项的第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]       ; 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

第一个截屏显示引导加载器加载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)