• 热门专题

一个操作系统的实现(11)让操作系统进入保护模式

作者:  发布日期:2016-06-15 21:16:24
Tag标签:模式  
  • 这节首先介绍了突破引导扇区只有512字节的原理,然后介绍了FAT12文件系统,最后通过实验加载loader并将控制权交给loader来实现突破512字节的束缚。

    突破512字节的限制

    前面所用的引导扇区只有512字节。然而实际上操作系统在启动过程需要做的事情是很多的。所以需要通过某种方法突破512字节的限制。

    那么如何突破512字节的限制呢?一种方法是再建立一个文件,通过引导扇区把它加载到内存,然后把控制权教给它。这样,512字节的束缚就没有了。

    这里被引导扇区加载进内存的并不是操作系统的内核。因为从开机到开始运行,操作系统经历了“引导→加载内核入内存→跳入保护模式→开始执行内核”这样一个过程。也就是说,才内核开始执行之前不但要加载内核,而且还有准备保护模式等一系列工作,如果全都交给引导扇区来做,512字节很可能是不够用的。因此,这里加载进内存的并不是内核,而是另外一个模块叫Loader。引导扇区把Loader加载进内存并把控制权交给它。上面所说的其他工作都交给Loader来做。Loader没有512字节的限制。所以会灵活很多。

    接下来最主要的是如何找到Loader文件并加载进入内存。首先介绍FAT12文件系统

    FAT12

    FAT的全称是File Allocation Table。它是DOS时代就开始使用的文件系统(File System),现在的软盘上面仍旧使用此文件系统。FAT把磁盘划分成若干层次以方便组织和管理,这些层次如下:

    扇区(Sector):磁盘上的最小数据单元。

    簇(Cluster):一个或多个扇区。

    分区(Partition):通常指整个文件系统。

    下面是FAT12格式的软盘的结构:

    引导扇区

    首先是引导扇区,它位于第0个扇区。它的结构如下图

    引导扇区有一个很重要的数据结构叫做BPB(BIOS ParameterBlock),它以BPB_开头。以BS_开头的域不属于BPB,只是引导扇区(Boot Sector)的一部分。

    FAT

    可以看到有两个FAT表,FAT2可看作是FAT1的备份,他们通常是一样的。FAT有点像是一个位图。每12位称为一个FAT项(FATEntry),代表一个簇。

    通常FAT项的值代表的是文件下一个簇号。从这里可以计算出FAT12中数据区的最大簇号是2^12=4K,如果每簇512字节,那么最大数据量是4K×512B=2MB

    当FAT表项的值大于或等于0xFF8时,表示当前簇已经是文件的最后一个簇。如果值为0xFF7,表示它是一个坏簇。

    其中第0个和第1个FAT项始终不使用,从第2个FAT项开始表示数据区的每一个簇。也就是说,第二个FAT项表示数据区的第一个簇,所以数据区的第一个簇号是2。

    根目录区

    根目录区位于第二个FAT表之后,开始的扇区号是19,它由若干个目录条目(Directory Entry)组成,条目最多有BPB_RootEntCnt个。由于根目录区的大小是依赖于BPB_RootEntCnt的,所以长度不固定。

    根目录区的每一个条目占用32字节,格式如下:

    根目录区主要定义了名称、属性、时间、开始簇号、大小。

    数据区

    数据区的簇号从2开始。这是因为上面所说的FAT表项从第二个开始。因为根目录区长度不是固定的。所以需要计算数据区的第一个簇号的位置。

    如何读取某一文件

    首先是进入根目录区根据文件名和属性来寻找文件。找到文件目录项后根据目录向中的开始簇号读取文件第一簇的信息,接下来查看FAT表项,找到文件的下一簇号是啥?如果小于0xFF7,则数据没读取完,如果大于或等于0xFF8则说明文件读取结束

    接下来,实现一个最简单的loader并实现加载过程。主要有如下几步:

    制作一个DOS可以识别的引导盘

    引导扇区需要有BPB等头信息才能被微软识别,我们首先加上它,代码大致如下:

     30         ; 下面是 FAT12 磁盘的头
     31         BS_OEMName      DB 'ForrestY'   ; OEM String, 必须 8 个字节
     32         BPB_BytsPerSec  DW 512          ; 每扇区字节数
     33         BPB_SecPerClus  DB 1            ; 每簇多少扇区
     34         BPB_RsvdSecCnt  DW 1            ; Boot 记录占用多少扇区
     35         BPB_NumFATs     DB 2            ; 共有多少 FAT 表
     36         BPB_RootEntCnt  DW 224          ; 根目录文件数最大值
     37         BPB_TotSec16    DW 2880         ; 逻辑扇区总数
     38         BPB_Media       DB 0xF0         ; 媒体描述符
     39         BPB_FATSz16     DW 9            ; 每FAT扇区数
     40         BPB_SecPerTrk   DW 18           ; 每磁道扇区数
     41         BPB_NumHeads    DW 2            ; 磁头数(面数)
     42         BPB_HiddSec     DD 0            ; 隐藏扇区数
     43         BPB_TotSec32    DD 0            ; 如果 wTotalSectorCount 是 0 由这个值记录扇区数
     44         BS_DrvNum       DB 0            ; 中断 13 的驱动器号
     45         BS_Reserved1    DB 0            ; 未使用
     46         BS_BootSig      DB 29h          ; 扩展引导标记 (29h)
     47         BS_VolID        DD 0            ; 卷序列号
     48         BS_VolLab       DB 'OrangeS0.02'; 卷标, 必须 11 个字节
     49         BS_FileSysType  DB 'FAT12   '   ; 文件系统类型, 必须 8个字节 

    现在的软盘已经能够被DOS和Linux识别了,我们已经可以方便地往上添加或删除文件了。

    编写一个简单的loader程序

    要将Loader加载到内存中,首先需要有一个Loader。所以接下来就是写一个最简单的loader,代码如下:

      2 org     0100h
      3 
      4         mov     ax, 0B800h
      5         mov     gs, ax
      6         mov     ah, 0Fh                         ; 0000: 黑底    1111: 白字
      7         mov     al, 'L'
      8         mov     [gs:((80 * 0 + 39) * 2)], ax    ; 屏幕第 0 行, 第 39 列。
      9 
     10         jmp     $               ; Start

    将此代码大保存在loader.asm文件中。这段代码被编译成.COM文件直接在DOS下执行,效果是在屏幕中央输出字符L,然后进入死循环。在这里,我们用下面的命令行来编译:

    $ nasm loader.asm -o loader.bin

    这里面编译出的二进制代码加载到内存的任意位置都可以正确执行,但是我们要扩展它,为了将来的执行不会出现问题,要保证把它放入某个段内偏移0x100的位置。

    加载loader进入内存

    int 13h

    加载软盘上的一个文件进入内存,使用的是BIOS中断int 13h。它的用法如下图:

    从上图可以看出,中断需要的参数不是从第0扇区开始的扇区号,而是柱面号、磁头号以及在当前柱面上的扇区号三个分量。所以要通过下图方法来转换:

    软盘相对扇区号的转换

    转换的原理如下:

    首先,1.44M的软盘结构:一个软盘包括2个盘面(0和1),每个盘面有80条磁道(磁柱),每个磁道有18个扇区,每个扇区大小位512Byte。所以总容量:2×80×18×512Byte=1474569Byte=1.44MB

    然后,从第0扇区开始一次编号叫做相对扇区,它与物理位置的关系如下:

    0面,0道,1扇区             0 
    0面,0道,2扇区             1 
    0面,0道,3扇区             2 
    ...
    0面,0道,18扇区           17 
    1面,0道,1扇区            18 
    ...
    1面,0道,18扇区           35 
    0面,1道,1扇区            36 
    ...
    0面,1道,18扇区           53 
    1面,1道,1扇区            54

    读软盘扇区

    因为loader可能包含多个扇区,所以接下来写一个读软盘扇区的函数:

    215 ;----------------------------------------------------------------------------
    216 ; 函数名: ReadSector
    217 ;----------------------------------------------------------------------------
    218 ; 作用:
    219 ;       从第 ax 个 Sector 开始, 将 cl 个 Sector 读入 es:bx 中
    220 ReadSector:
    221         ; -----------------------------------------------------------------------
    222         ; 怎样由扇区号求扇区在磁盘中的位置 (扇区号 -> 柱面号, 起始扇区, 磁头号)
    223         ; -----------------------------------------------------------------------
    224         ; 设扇区号为 x
    225         ;                           ┌ 柱面号 = y >> 1
    226         ;       x           ┌ 商 y ┤
    227         ; -------------- => ┤      └ 磁头号 = y & 1
    228         ;  每磁道扇区数     │
    229         ;                   └ 余 z => 起始扇区号 = z + 1
    230         push    bp
    231         mov     bp, sp
    232         sub     esp, 2                  ; 辟出两个字节的堆栈区域保存要读的扇区数: byte [bp-2]
    233 
    234         mov     byte [bp-2], cl
    235         push    bx                      ; 保存 bx
    236         mov     bl, [BPB_SecPerTrk]     ; bl: 除数
    237         div     bl                      ; y 在 al 中, z 在 ah 中
    238         inc     ah                      ; z ++
    239         mov     cl, ah                  ; cl <- 起始扇区号
    240         mov     dh, al                  ; dh <- y
    241         shr     al, 1                   ; y >> 1 (其实是 y/BPB_NumHeads, 这里BPB_NumHeads=2)
    242         mov     ch, al                  ; ch <- 柱面号
    243         and     dh, 1                   ; dh & 1 = 磁头号
    244         pop     bx                      ; 恢复 bx
    245         ; 至此, "柱面号, 起始扇区, 磁头号" 全部得到 ^^^^^^^^^^^^^^^^^^^^^^^^
    246         mov     dl, [BS_DrvNum]         ; 驱动器号 (0 表示 A 盘)
    247 .GoOnReading:
    248         mov     ah, 2                   ; 读
    249         mov     al, byte [bp-2]         ; 读 al 个扇区
    250         int     13h
    251         jc      .GoOnReading            ; 如果读取错误 CF 会被置为 1, 这时就不停地读, 直到正确为止
    252 
    253         add     esp, 2
    254         pop     bp
    255 
    256         ret

    上面的代码用到了堆栈,所以程序开头要初始化ss和esp:

     14 BaseOfStack             equ     07c00h  ; Boot状态下堆栈基地址(栈底, 从这个位置向低地址生长)
     52         mov     ax, cs
     53         mov     ds, ax
     54         mov     es, ax
     55         mov     ss, ax
     56         mov     sp, BaseOfStack

    读扇区的函数写好了,接下来就开始在软盘中寻找Loader.bin

    寻找loader

    主要包括两个寻找:

    在根目录区寻找Loader的第一个扇区

    在FAT表中寻找Loader的其余扇区

    根目录区寻找loader.bin

     72 ; 下面在 A 盘的根目录寻找 LOADER.BIN
     73         mov     word [wSectorNo], SectorNoOfRootDirectory
     74 LABEL_SEARCH_IN_ROOT_DIR_BEGIN:
     75         cmp     word [wRootDirSizeForLoop], 0   ; ┓
     76         jz      LABEL_NO_LOADERBIN              ; ┣ 判断根目录区是不是已经读完
     77         dec     word [wRootDirSizeForLoop]      ; ┛ 如果读完表示没有找到 LOADER.BIN
     78         mov     ax, BaseOfLoader
     79         mov     es, ax            ; es<-BaseOfLoader
     80         mov     bx, OffsetOfLoader; bx<-OffsetOfLoader于是,es:bx = BaseOfLoader:OffsetOfLoader
     81         mov     ax, [wSectorNo] ; ax <- Root Directory 中的某 Sector 号
     82         mov     cl, 1
     83         call    ReadSector
     84 
     85         mov     si, LoaderFileName      ; ds:si -> "LOADER  BIN"
     86         mov     di, OffsetOfLoader      ; es:di -> BaseOfLoader:0100 = BaseOfLoader*10h+100
     87         cld
     88         mov     dx, 10h
     89 LABEL_SEARCH_FOR_LOADERBIN:
     90         cmp     dx, 0                                   ; ┓循环次数控制,
     91         jz      LABEL_GOTO_NEXT_SECTOR_IN_ROOT_DIR      ; ┣如果已经读完了一个 Sector,
     92         dec     dx                                      ; ┛就跳到下一个 Sector
     93         mov     cx, 11
     94 LABEL_CMP_FILENAME:
     95         cmp     cx, 0
     96         jz      LABEL_FILENAME_FOUND    ; 如果比较了 11 个字符都相等, 表示找到
     97 dec     cx
     98         lodsb                           ; ds:si -> al
     99         cmp     al, byte [es:di]
    100         jz      LABEL_GO_ON
    101         jmp     LABEL_DIFFERENT         ; 只要发现不一样的字符就表明本 DirectoryEntry 不是
    102 ; 我们要找的 LOADER.BIN
    103 LABEL_GO_ON:
    104         inc     di
    105         jmp     LABEL_CMP_FILENAME      ;       继续循环
    106 
    107 LABEL_DIFFERENT:
    108         and     di, 0FFE0h                ; else ┓        di &= E0 为了让它指向本条目开头
    109         add     di, 20h                   ;     ┃
    110         mov     si, LoaderFileName        ;     ┣ di += 20h  下一个目录条目
    111         jmp     LABEL_SEARCH_FOR_LOADERBIN;    ┛
    112 
    113 LABEL_GOTO_NEXT_SECTOR_IN_ROOT_DIR:
    114         add     word [wSectorNo], 1
    115         jmp     LABEL_SEARCH_IN_ROOT_DIR_BEGIN
    116 
    117 LABEL_NO_LOADERBIN:
    118         mov     dh, 2                   ; "No LOADER."
    119         call    DispStr                 ; 显示字符串
    120 %ifdef  _BOOT_DEBUG_
    121         mov     ax, 4c00h               ; ┓
    122         int     21h                     ; ┛没有找到 LOADER.BIN, 回到 DOS
    123 %else
    124         jmp     $                       ; 没有找到 LOADER.BIN, 死循环在这里
    125 %endif
    126 
    127 LABEL_FILENAME_FOUND:                   ; 找到 LOADER.BIN 后便来到这里继续
    128         mov     ax, RootDirSectors
    129         and     di, 0FFE0h              ; di -> 当前条目的开始
    130         add     di, 01Ah                ; di -> 首 Sector
    131         mov     cx, word [es:di]
    132         push    cx                      ; 保存此 Sector 在 FAT 中的序号
    133         add     cx, ax
    134         add     cx, DeltaSectorNo       ; cl <- LOADER.BIN的起始扇区号(0-based)
    135         mov     ax, BaseOfLoader
    136         mov     es, ax                  ; es <- BaseOfLoader
    137         mov     bx, OffsetOfLoader      ; bx <- OffsetOfLoader
    138         mov     ax, cx                  ; ax <- Sector 号

    上面的代码的逻辑过程是:遍历根目录区所有的扇区,将每一个扇区加载入内存,然后从中寻找文件名为loader.bin的条目,指导找到为止。找到的那一刻,es:di是指向条目中字母N后面的哪个字符。其中有一些宏定义如下:

     17 BaseOfLoader            equ     09000h  ; LOADER.BIN 被加载到的位置 ----  段地址
     18 OffsetOfLoader          equ     0100h   ; LOADER.BIN 被加载到的位置 ---- 偏移地址
     19 
     20 RootDirSectors          equ     14      ; 根目录占用空间
     21 SectorNoOfRootDirectory equ     19      ; Root Directory 的第一个扇区号
    

    还有一些变量和字符串的值定义如下:

    176 ;============================================================================
    177 ;变量
    178 ;----------------------------------------------------------------------------
    179 wRootDirSizeForLoop     dw      RootDirSectors  ; Root Directory 占用的扇区数, 在循环中会递减至零.
    180 wSectorNo               dw      0               ; 要读取的扇区号
    181 bOdd                    db      0               ; 奇数还是偶数
    182 
    183 ;============================================================================
    184 ;字符串
    185 ;----------------------------------------------------------------------------
    186 LoaderFileName          db      "LOADER  BIN", 0        ; LOADER.BIN 之文件名
    187 ; 为简化代码, 下面每个字符串的长度均为 MessageLength
    188 MessageLength           equ     9
    189 BootMessage:            db      "Booting  "; 9字节, 不够则用空格补齐. 序号 0
    190 Message1                db      "Ready.   "; 9字节, 不够则用空格补齐. 序号 1
    191 Message2                db      "No LOADER"; 9字节, 不够则用空格补齐. 序号 2
    192 ;============================================================================

    读取过程中会打印一些字符,打印字符串的函数如下:

    195 ;----------------------------------------------------------------------------
    196 ; 函数名: DispStr
    197 ;----------------------------------------------------------------------------
    198 ; 作用:
    199 ;       显示一个字符串, 函数开始时 dh 中应该是字符串序号(0-based)
    200 DispStr:
    201         mov     ax, MessageLength
    202         mul     dh
    203         add     ax, BootMessage
    204         mov     bp, ax                  ; ┓
    205         mov     ax, ds                  ; ┣ ES:BP = 串地址
    206         mov     es, ax                  ; ┛
    207         mov     cx, MessageLength       ; CX = 串长度
    208         mov     ax, 01301h              ; AH = 13,  AL = 01h
    209         mov     bx, 0007h               ; 页号为0(BH = 0) 黑底白字(BL = 07h)
    210         mov     dl, 0
    211         int     10h                     ; int 10h
    212         ret

    loader的第一个扇区找到了,接下来寻找loader的剩下扇区,在FAT表项中寻找下一个扇区号。

    由扇区号寻找FAT项的值

     22 SectorNoOfFAT1          equ     1       ; FAT1 的第一个扇区号 = BPB_RsvdSecCnt
    ...
    258 ;----------------------------------------------------------------------------
    259 ; 函数名: GetFATEntry
    260 ;----------------------------------------------------------------------------
    261 ; 作用:
    262 ;       找到序号为 ax 的 Sector 在 FAT 中的条目, 结果放在 ax 中
    263 ;       需要注意的是, 中间需要读 FAT 的扇区到 es:bx 处, 所以函数一开始保存了 es 和 bx
    264 GetFATEntry:
    265         push    es
    266         push    bx
    267         push    ax
    268         mov     ax, BaseOfLoader; `.
    269         sub     ax, 0100h       ;  | 在 BaseOfLoader 后面留出 4K 空间用于存放 FAT
    270         mov     es, ax          ; /
    271         pop     ax
    272         mov     byte [bOdd], 0
    273         mov     bx, 3
    274         mul     bx                      ; dx:ax = ax * 3
    275         mov     bx, 2
    276         div     bx                      ; dx:ax / 2  ==>  ax <- 商, dx <- 余数
    277         cmp     dx, 0
    278         jz      LABEL_EVEN
    279         mov     byte [bOdd], 1
    280 LABEL_EVEN:;偶数
    281         ; 现在 ax 中是 FATEntry 在 FAT 中的偏移量,下面来
    282         ; 计算 FATEntry 在哪个扇区中(FAT占用不止一个扇区)
    283         xor     dx, dx
    284         mov     bx, [BPB_BytsPerSec]
    285         div     bx ; dx:ax / BPB_BytsPerSec
    286                    ;  ax <- 商 (FATEntry 所在的扇区相对于 FAT 的扇区号)
    287                    ;  dx <- 余数 (FATEntry 在扇区内的偏移)。
    288         push    dx
    289         mov     bx, 0 ; bx <- 0 于是, es:bx = (BaseOfLoader - 100):00
    290         add     ax, SectorNoOfFAT1 ; 此句之后的 ax 就是 FATEntry 所在的扇区号
    291         mov     cl, 2
    292         call    ReadSector ; 读取 FATEntry 所在的扇区, 一次读两个, 避免在边界
    293                            ; 发生错误, 因为一个 FATEntry 可能跨越两个扇区
    294         pop     dx
    295         add     bx, dx
    296         mov     ax, [es:bx]
    297         cmp     byte [bOdd], 1
    298         jnz     LABEL_EVEN_2
    299         shr     ax, 4
    300 LABEL_EVEN_2:
    301         and     ax, 0FFFh
    302 
    303 LABEL_GET_FAT_ENRY_OK:

    上面寻找loader的工作已经做完了,接下来加载loader:

    127 LABEL_FILENAME_FOUND:                   ; 找到 LOADER.BIN 后便来到这里继续
    128         mov     ax, RootDirSectors
    129         and     di, 0FFE0h              ; di -> 当前条目的开始
    130         add     di, 01Ah                ; di -> 首 Sector
    131         mov     cx, word [es:di]
    132         push    cx                      ; 保存此 Sector 在 FAT 中的序号
    133         add     cx, ax
    134         add     cx, DeltaSectorNo       ; cl <- LOADER.BIN的起始扇区号(0-based)
    135         mov     ax, BaseOfLoader
    136         mov     es, ax                  ; es <- BaseOfLoader
    137         mov     bx, OffsetOfLoader      ; bx <- OffsetOfLoader
    138         mov     ax, cx                  ; ax <- Sector 号
    139 
    140 LABEL_GOON_LOADING_FILE:
    141         push    ax                      ; `.
    142         push    bx                      ;  |
    143         mov     ah, 0Eh                 ;  | 每读一个扇区就在 "Booting  " 后面
    144         mov     al, '.'                 ;  | 打一个点, 形成这样的效果:
    145         mov     bl, 0Fh                 ;  | Booting ......
    146         int     10h                     ;  |
    147         pop     bx                      ;  |
    148         pop     ax                      ; /
    149 
    150         mov     cl, 1
    151         call    ReadSector
    152         pop     ax                      ; 取出此 Sector 在 FAT 中的序号
    153         call    GetFATEntry
    154         cmp     ax, 0FFFh
    155         jz      LABEL_FILE_LOADED
    156         push    ax                      ; 保存 Sector 在 FAT 中的序号
    157         mov     dx, RootDirSectors
    158         add     ax, dx
    159         add     ax, DeltaSectorNo
    160         add     bx, [BPB_BytsPerSec]
    161         jmp     LABEL_GOON_LOADING_FILE
    162 LABEL_FILE_LOADED:
    163 
    164         mov     dh, 1                   ; "Ready."
    165         call    DispStr                 ; 显示字符串
    

    向loader交出控制权

    万事具备,只差最后一步,向loader交出控制权,可以理解为直接跳转到loader所在的代码执行:

    167 ; ****************************************************************************
    168         jmp     BaseOfLoader:OffsetOfLoader     ; 这一句正式跳转到已加载到内
    169                                                 ; 存中的 LOADER.BIN 的开始处,
    170                                                 ; 开始执行 LOADER.BIN 的代码。
    171                                                 ; Boot Sector 的使命到此结束。
    172 ; ****************************************************************************
    

    接下来看成果

    bochs调试与运行

    $ nasm boot.asm -o boot.bin
    $ nasm loader.asm -o loader.bin
    $ dd if=boot.bin of=a.img bs=512 count=1 conv=notrunc
    $ sudo mount -o loop a.img /mnt/floppy
    $ sudo cp loader.bin /mnt/floppy/ -v
    $ sudo umount /mnt/floppy

    运行结果如下:

    源代码

    ;%define    _BOOT_DEBUG_
    ; 做 Boot Sector 时一定将此行注释掉!将此行打开后用 nasm Boot.asm -o Boot.com 做成一个.COM文件易于调试
    
    %ifdef    _BOOT_DEBUG_
        org  0100h            ; 调试状态, 做成 .COM 文件, 可调试
    %else
        org  07c00h            ; Boot 状态, Bios 将把 Boot Sector 加载到 0:7C00 处并开始执行
    %endif
    
    ;=========================================================
    %ifdef    _BOOT_DEBUG_
    BaseOfStack        equ    0100h    ; 调试状态下堆栈基地址(栈底, 从这个位置向低地址生长)
    %else
    BaseOfStack        equ    07c00h    ; Boot状态下堆栈基地址(栈底, 从这个位置向低地址生长)
    %endif
    
    BaseOfLoader        equ    09000h    ; LOADER.BIN 被加载到的位置 ----  段地址
    OffsetOfLoader        equ    0100h    ; LOADER.BIN 被加载到的位置 ---- 偏移地址
    
    RootDirSectors        equ    14    ; 根目录占用空间
    SectorNoOfRootDirectory    equ    19    ; Root Directory 的第一个扇区号
    SectorNoOfFAT1        equ    1    ; FAT1 的第一个扇区号 = BPB_RsvdSecCnt
    DeltaSectorNo        equ    17;DeltaSectorNo = BPB_RsvdSecCnt + (BPB_NumFATs * FATSz) - 2
    ; 文件的开始Sector号 = DirEntry中的开始Sector号 + 根目录占用Sector数目 + DeltaSectorNo
    ;=========================================================
    
        jmp short LABEL_START        ; Start to boot.
        nop                ; 这个 nop 不可少
    
        ; 下面是 FAT12 磁盘的头
        BS_OEMName    DB 'ForrestY'    ; OEM String, 必须 8 个字节
        BPB_BytsPerSec    DW 512        ; 每扇区字节数
        BPB_SecPerClus    DB 1        ; 每簇多少扇区
        BPB_RsvdSecCnt    DW 1        ; Boot 记录占用多少扇区
        BPB_NumFATs    DB 2        ; 共有多少 FAT 表
        BPB_RootEntCnt    DW 224        ; 根目录文件数最大值
        BPB_TotSec16    DW 2880        ; 逻辑扇区总数
        BPB_Media    DB 0xF0        ; 媒体描述符
        BPB_FATSz16    DW 9        ; 每FAT扇区数
        BPB_SecPerTrk    DW 18        ; 每磁道扇区数
        BPB_NumHeads    DW 2        ; 磁头数(面数)
        BPB_HiddSec    DD 0        ; 隐藏扇区数
        BPB_TotSec32    DD 0        ; 如果 wTotalSectorCount 是 0 由这个值记录扇区数
        BS_DrvNum    DB 0        ; 中断 13 的驱动器号
        BS_Reserved1    DB 0        ; 未使用
        BS_BootSig    DB 29h        ; 扩展引导标记 (29h)
        BS_VolID    DD 0        ; 卷序列号
        BS_VolLab    DB 'OrangeS0.02'; 卷标, 必须 11 个字节
        BS_FileSysType    DB 'FAT12   '    ; 文件系统类型, 必须 8个字节  
    
    LABEL_START:    
        mov    ax, cs
        mov    ds, ax
        mov    es, ax
        mov    ss, ax
        mov    sp, BaseOfStack
    
        ; 清屏
        mov    ax, 0600h        ; AH = 6,  AL = 0h
        mov    bx, 0700h        ; 黑底白字(BL = 07h)
        mov    cx, 0            ; 左上角: (0, 0)
        mov    dx, 0184fh        ; 右下角: (80, 50)
        int    10h            ; int 10h
    
        mov    dh, 0            ; "Booting  "
        call    DispStr            ; 显示字符串
    
        xor    ah, ah    ; ┓
        xor    dl, dl    ; ┣ 软驱复位
        int    13h    ; ┛
    
    ; 下面在 A 盘的根目录寻找 LOADER.BIN
        mov    word [wSectorNo], SectorNoOfRootDirectory
    LABEL_SEARCH_IN_ROOT_DIR_BEGIN:
        cmp    word [wRootDirSizeForLoop], 0    ; ┓
        jz    LABEL_NO_LOADERBIN        ; ┣ 判断根目录区是不是已经读完
        dec    word [wRootDirSizeForLoop]    ; ┛ 如果读完表示没有找到 LOADER.BIN
        mov    ax, BaseOfLoader
        mov    es, ax            ; es <- BaseOfLoader
        mov    bx, OffsetOfLoader    ; bx <- OffsetOfLoader 于是, es:bx = BaseOfLoader:OffsetOfLoader
        mov    ax, [wSectorNo]    ; ax <- Root Directory 中的某 Sector 号
        mov    cl, 1
        call    ReadSector
    
        mov    si, LoaderFileName    ; ds:si -> "LOADER  BIN"
        mov    di, OffsetOfLoader    ; es:di -> BaseOfLoader:0100 = BaseOfLoader*10h+100
        cld
        mov    dx, 10h
    LABEL_SEARCH_FOR_LOADERBIN:
        cmp    dx, 0                                        ; ┓循环次数控制,
        jz    LABEL_GOTO_NEXT_SECTOR_IN_ROOT_DIR    ; ┣如果已经读完了一个 Sector,
        dec    dx                                            ; ┛就跳到下一个 Sector
        mov    cx, 11
    LABEL_CMP_FILENAME:
        cmp    cx, 0
        jz    LABEL_FILENAME_FOUND    ; 如果比较了 11 个字符都相等, 表示找到
    dec    cx
        lodsb                ; ds:si -> al
        cmp    al, byte [es:di]
        jz    LABEL_GO_ON
        jmp    LABEL_DIFFERENT        ; 只要发现不一样的字符就表明本 DirectoryEntry 不是
    ; 我们要找的 LOADER.BIN
    LABEL_GO_ON:
        inc    di
        jmp    LABEL_CMP_FILENAME    ;    继续循环
    
    LABEL_DIFFERENT:
        and    di, 0FFE0h                        ; else ┓    di &= E0 为了让它指向本条目开头
        add    di, 20h                            ;     ┃
        mov    si, LoaderFileName                    ;     ┣ di += 20h  下一个目录条目
        jmp    LABEL_SEARCH_FOR_LOADERBIN;    ┛
    
    LABEL_GOTO_NEXT_SECTOR_IN_ROOT_DIR:
        add    word [wSectorNo], 1
        jmp    LABEL_SEARCH_IN_ROOT_DIR_BEGIN
    
    LABEL_NO_LOADERBIN:
        mov    dh, 2            ; "No LOADER."
        call    DispStr            ; 显示字符串
    %ifdef    _BOOT_DEBUG_
        mov    ax, 4c00h        ; ┓
        int    21h            ; ┛没有找到 LOADER.BIN, 回到 DOS
    %else
        jmp    $            ; 没有找到 LOADER.BIN, 死循环在这里
    %endif
    
    LABEL_FILENAME_FOUND:            ; 找到 LOADER.BIN 后便来到这里继续
        mov    ax, RootDirSectors
        and    di, 0FFE0h        ; di -> 当前条目的开始
        add    di, 01Ah        ; di -> 首 Sector
        mov    cx, word [es:di]
        push    cx            ; 保存此 Sector 在 FAT 中的序号
        add    cx, ax
        add    cx, DeltaSectorNo    ; cl <- LOADER.BIN的起始扇区号(0-based)
        mov    ax, BaseOfLoader
        mov    es, ax            ; es <- BaseOfLoader
        mov    bx, OffsetOfLoader    ; bx <- OffsetOfLoader
        mov    ax, cx            ; ax <- Sector 号
    
    LABEL_GOON_LOADING_FILE:
        push    ax            ; `.
        push    bx            ;  |
        mov    ah, 0Eh            ;  | 每读一个扇区就在 "Booting  " 后面
        mov    al, '.'            ;  | 打一个点, 形成这样的效果:
        mov    bl, 0Fh            ;  | Booting ......
        int    10h            ;  |
        pop    bx            ;  |
        pop    ax            ; /
    
        mov    cl, 1
        call    ReadSector
        pop    ax            ; 取出此 Sector 在 FAT 中的序号
        call    GetFATEntry
        cmp    ax, 0FFFh
        jz    LABEL_FILE_LOADED
        push    ax            ; 保存 Sector 在 FAT 中的序号
        mov    dx, RootDirSectors
        add    ax, dx
        add    ax, DeltaSectorNo
        add    bx, [BPB_BytsPerSec]
        jmp    LABEL_GOON_LOADING_FILE
    LABEL_FILE_LOADED:
    
        mov    dh, 1            ; "Ready."
        call    DispStr            ; 显示字符串
    ; ***************************************************************************
        jmp    BaseOfLoader:OffsetOfLoader    ; 这一句正式跳转到已加载到内
                            ; 存中的 LOADER.BIN 的开始处,
                            ; 开始执行 LOADER.BIN 的代码。
                            ; Boot Sector 的使命到此结束。
    ; ***************************************************************************
    
    
    
    ;============================================================================
    ;变量
    ;----------------------------------------------------------------------------
    wRootDirSizeForLoop    dw    RootDirSectors    ; Root Directory 占用的扇区数, 在循环中会递减至零.
    wSectorNo        dw    0        ; 要读取的扇区号
    bOdd            db    0        ; 奇数还是偶数
    
    ;============================================================================
    ;字符串
    ;----------------------------------------------------------------------------
    LoaderFileName        db    "LOADER  BIN", 0    ; LOADER.BIN 之文件名
    ; 为简化代码, 下面每个字符串的长度均为 MessageLength
    MessageLength        equ    9
    BootMessage:        db    "Booting  "; 9字节, 不够则用空格补齐. 序号 0
    Message1        db    "Ready.   "; 9字节, 不够则用空格补齐. 序号 1
    Message2        db    "No LOADER"; 9字节, 不够则用空格补齐. 序号 2
    ;============================================================================
    
    
    ;----------------------------------------------------------------------------
    ; 函数名: DispStr
    ;----------------------------------------------------------------------------
    ; 作用:
    ;    显示一个字符串, 函数开始时 dh 中应该是字符串序号(0-based)
    DispStr:
        mov    ax, MessageLength
        mul    dh
        add    ax, BootMessage
        mov    bp, ax            ; ┓
        mov    ax, ds            ; ┣ ES:BP = 串地址
        mov    es, ax            ; ┛
        mov    cx, MessageLength    ; CX = 串长度
        mov    ax, 01301h        ; AH = 13,  AL = 01h
        mov    bx, 0007h        ; 页号为0(BH = 0) 黑底白字(BL = 07h)
        mov    dl, 0
        int    10h            ; int 10h
        ret
    
    
    ;----------------------------------------------------------------------------
    ; 函数名: ReadSector
    ;----------------------------------------------------------------------------
    ; 作用:
    ;    从第 ax 个 Sector 开始, 将 cl 个 Sector 读入 es:bx 中
    ReadSector:
        ; -----------------------------------------------------------------------
        ; 怎样由扇区号求扇区在磁盘中的位置 (扇区号 -> 柱面号, 起始扇区, 磁头号)
        ; -----------------------------------------------------------------------
        ; 设扇区号为 x
        ;                           ┌ 柱面号 = y >> 1
        ;       x           ┌ 商 y ┤
        ; -------------- => ┤      └ 磁头号 = y & 1
        ;  每磁道扇区数     │
        ;                   └ 余 z => 起始扇区号 = z + 1
        push    bp
        mov    bp, sp
        sub    esp, 2            ; 辟出两个字节的堆栈区域保存要读的扇区数: byte [bp-2]
    
        mov    byte [bp-2], cl
        push    bx            ; 保存 bx
        mov    bl, [BPB_SecPerTrk]    ; bl: 除数
        div    bl            ; y 在 al 中, z 在 ah 中
        inc    ah            ; z ++
        mov    cl, ah            ; cl <- 起始扇区号
        mov    dh, al            ; dh <- y
        shr    al, 1            ; y >> 1 (其实是 y/BPB_NumHeads, 这里BPB_NumHeads=2)
        mov    ch, al            ; ch <- 柱面号
        and    dh, 1            ; dh & 1 = 磁头号
        pop    bx            ; 恢复 bx
        ; 至此, "柱面号, 起始扇区, 磁头号" 全部得到 ^^^^^^^^^^^^^^^^^^^^^^^^
        mov    dl, [BS_DrvNum]        ; 驱动器号 (0 表示 A 盘)
    .GoOnReading:
        mov    ah, 2            ; 读
        mov    al, byte [bp-2]        ; 读 al 个扇区
        int    13h
        jc    .GoOnReading        ; 如果读取错误 CF 会被置为 1, 这时就不停地读, 直到正确为止
    
        add    esp, 2
        pop    bp
    
        ret
    
    ;----------------------------------------------------------------------------
    ; 函数名: GetFATEntry
    ;----------------------------------------------------------------------------
    ; 作用:
    ;    找到序号为 ax 的 Sector 在 FAT 中的条目, 结果放在 ax 中
    ;    需要注意的是, 中间需要读 FAT 的扇区到 es:bx 处, 所以函数一开始保存了 es 和 bx
    GetFATEntry:
        push    es
        push    bx
        push    ax
        mov    ax, BaseOfLoader; `.
        sub    ax, 0100h    ;  | 在 BaseOfLoader 后面留出 4K 空间用于存放 FAT
        mov    es, ax        ; /
        pop    ax
        mov    byte [bOdd], 0
        mov    bx, 3
        mul    bx            ; dx:ax = ax * 3
        mov    bx, 2
        div    bx            ; dx:ax / 2  ==>  ax <- 商, dx <- 余数
        cmp    dx, 0
        jz    LABEL_EVEN
        mov    byte [bOdd], 1
    LABEL_EVEN:;偶数
        ; 现在 ax 中是 FATEntry 在 FAT 中的偏移量,下面来
        ; 计算 FATEntry 在哪个扇区中(FAT占用不止一个扇区)
        xor    dx, dx            
        mov    bx, [BPB_BytsPerSec]
        div    bx ; dx:ax / BPB_BytsPerSec
               ;  ax <- 商 (FATEntry 所在的扇区相对于 FAT 的扇区号)
               ;  dx <- 余数 (FATEntry 在扇区内的偏移)。
        push    dx
        mov    bx, 0 ; bx <- 0 于是, es:bx = (BaseOfLoader - 100):00
        add    ax, SectorNoOfFAT1 ; 此句之后的 ax 就是 FATEntry 所在的扇区号
        mov    cl, 2
        call    ReadSector ; 读取 FATEntry 所在的扇区, 一次读两个, 避免在边界
                   ; 发生错误, 因为一个 FATEntry 可能跨越两个扇区
        pop    dx
        add    bx, dx
        mov    ax, [es:bx]
        cmp    byte [bOdd], 1
        jnz    LABEL_EVEN_2
        shr    ax, 4
    LABEL_EVEN_2:
        and    ax, 0FFFh
    
    LABEL_GET_FAT_ENRY_OK:
    
        pop    bx
        pop    es
        ret
    ;----------------------------------------------------------------------------
    
    times     510-($-$$)    db    0    ; 填充剩下的空间,使生成的二进制代码恰好为512字节
    dw     0xaa55                ; 结束标志
About IT165 - 广告服务 - 隐私声明 - 版权申明 - 免责条款 - 网站地图 - 网友投稿 - 联系方式
本站内容来自于互联网,仅供用于网络技术学习,学习中请遵循相关法律法规