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

这节首先介绍了突破引导扇区只有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

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

 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

主要包括两个寻找:

  1. 在根目录区寻找Loader的第一个扇区
  2. 在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                ; 结束标志
时间: 2024-11-10 04:42:41

一个操作系统的实现(11)-让操作系统进入保护模式的相关文章

一个操作系统的实现(7)-获取机器内存并进行合理分页

在前面的程序中,我们用了4MB的空间来存放页表,并用它映射了4GB的内存空间,而我们的物理内存不见得有这么大,这显然是太浪费了.如果我们的内存总数只有16MB的话,只是页表就占用了25%的内存空间.而实际上,如果仅仅是对等映射的话,16MB的内存只要4个页表就够了.所以,我们有必要知道内存有多大,然后根据内存大小确定多少页表是够用的.而且,一个操作系统也必须知道内存的容量,以便进行内存管理. 克勤克俭用内存 这里利用中断15h来获取计算机的内存. 在调用中断15h之前,我们需要填充下列寄存器:

保护模式详解

在ia32下,cpu有两种工作模式:实模式和保护模式. 在实模式下,16位的寄存器用"段+偏移"的方法计算有效地址. 段寄存器始终是16位的.在实模式下,段值xxxxh表示的以xxxx0h开始的一段内存.但在保护模式下,段寄存器的值变成了一个索引(还有附加信息)这个索引指向了一个数据结构的表(gdt/ldt)项,表项(描述符)中详细定义了段的其实地址.界限.属性等内容. 保护模式需要理解:描述符,选择子 描述符包括,存储段描述符(代码段,数据段,堆栈段),系统描述符(任务状态段TSS,

我是如何学习写一个操作系统(三):操作系统的启动之保护模式

前言 上一篇其实已经说完了boot的大致工作,但是Linux在最后进入操作系统之前还有一些操作,比如进入保护模式.在我自己的FragileOS里进入保护模式是在引导程序结束后完成的. 实模式到保护模式属于操作系统的一个大坎,所以需要先提一下 从实模式到保护模式 实模式和保护模式都是CPU的工作模式,它们的主要区别就是寻址方式 实模式出现于早期8088CPU时期.当时由于CPU的性能有限,一共只有20位地址线(所以地址空间只有1MB),以及8个16位的通用寄存器,以及4个16位的段寄存器.所以为了

一个操作系统的实现(2)-认识保护模式

今天开始学习intel处理器的保护模式.书的第二章 这节讲述的是如何从实模式进入保护模式.用的例子是在保护模式下向屏幕上输出字符P 如何进入保护模式呢?主要步骤如下: 0. 进入保护模式的步骤 准备GDT 用lgdt加载gdtr 打开A20 置r0的PE位位1 跳转,进入保护模式 下面是书的例子: 1. 进入保护模式实例 ; ========================================== ; pmtest1.asm ; 编译方法:nasm pmtest1.asm -o pm

一个操作系统的实现(3)-保护模式进阶

上节内容是从实模式进入到保护模式,只是进入保护模式打印了一个字母P.但是没有体现出保护模式的优势,也没有从保护模式中返回.这节就是要体验保护模式下读写大地址内存的能力和从保护模式返回到实模式. 这节要做的内容如下:首先在屏幕的第11行输出In Protect Mode now. ^-^.然后在屏幕第12行输出内存中起始地址为5MB的连续的8个字节.然后向这个以5MB开始的内存中写入ABCDEFGH.再次在第13行输出这8个字节.结果演示如下: 源代码300多行,很长,分段讲述,主要讲新增的部分

《Orange&#39;S:一个操作系统的实现》笔记(一)

感觉自己对于操作系统始终没有一个清楚的概念,尤其最近困扰于实模式.保护模式以及寻址方式等一些概念.转而一想,所有的程序,最终都是操作的计算机资源,需要和操作系统打交道,所以操作系统有必要深入了解一下.最终想要自己动手编写一个简单的版本,上网查.网友对于于渊的<Orange'S:一个操作系统的实现>和<30天自制操作系统>评价挺高的,先选<orange>为学习手册.<30>为参考手册,开始自己的操作系统之旅. 首先是平台的搭建问题,首先因本人编程一般都是在自己

0.11之路(四):从实模式到保护模式

(一)关中断并将system移动到内存地址起始位置 0x00000 将CPU的标志寄存器(EFLAGS)中的中断允许标志(IF)置0.这样系统不会再响应中断,直到main函数中能够适应保护模式的中断服务体系重建完毕才会打开,那时候响应中断的服务程序将不再是BIOS提供的中断服务程序,而是系统自身提供的. 就是要完成实模式下的中断向量表和保护模式下的中断描述符表(IDT)的交接工作.借助关中断(cli)和开中断(sti)完成这个过程的创建,即在创建过程中不能去响应中断,否则没有对应的中断程序,系统

一个操作系统的实现(4)-认识LDT

看到这里,你应该已经很了解GDT了,如果还不了解GDT.请看这篇文章:一个操作系统的实现(2)-认识保护模式,认识保护模式那篇文章的最后详细介绍了由16位寻址升级到32位寻址而引入的GDT. LDT(Local Descriptor Table):从名字上面就可以看出来它与GDT(Gobal Descriptor Table)的区别.GDT是全局描述符表,LDT是局部描述符表(相对于GDT). 下面仍然是从代码的角度讲解什么是LDT.主要讲解在上一节的基础上增加的代码.在文章的最后会附上所有代码

【自制操作系统04】从实模式到保护模式

通过前三章的努力,我们成功将控制权转交给了 loader.asm 这个程序.具体说就是 bios 通过加载并跳转到 0x7c00(IMB大叔们定的) 把控制权转交给了我们操作系统的第一个汇编程序 mbr.asm,然后 mbr.asm 里做的事就是通过加载 loader 程序并跳转到 0x900(这个是我们自己定的)把控制权转交给了 loader.asm 程序,目前这个程序里还只是向屏幕输出一行字符串"loader",今天我们就将扩展它.并且今天我们要做的事,是操作系统中的第一个精彩之处