分享

从头开始编写操作系统(7) 第6章:引导加载器4 - xiaoxiaoyaya的专栏 - ...

 qdhtxxlhz 2011-04-22
  从头开始编写操作系统(7) 第6章:引导加载器4 收藏
译自:http://www./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: 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 代表那个引导加载器之后执行的程序。我们的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: 获取起始簇
好了,根目录被加载了进来,而且,我们找到了文件对应的表项。我们怎么找到它的起始簇呢?
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]       ; 从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 、以及更详细讨论保护模式……
再见!
再见!
发表于 @ 2011年04月14日 08:17:00 | 评论( 0 ) | 编辑| 举报| 收藏
旧一篇:从头开始编写操作系统(6) 第5章:引导加载器3  | 新一篇:从头开始编写操作系统(8) 第7章:系统结构
-

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/xiaoxiaoyaya/archive/2011/04/14/6322112.aspx
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/xiaoxiaoyaya/archive/2011/04/14/6322112.aspx

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多