一个操作系统的实现(11)让操作系统进入保护模式
这节首先介绍了突破引导扇区只有512字节的原理,然后介绍了FAT12文件系统,最后通过实验加载loader并将控制权交给loader来实现突破512字节的束缚。
突破512字节的限制前面所用的引导扇区只有512字节。然而实际上操作系统在启动过程需要做的事情是很多的。所以需要通过某种方法突破512字节的限制。
那么如何突破512字节的限制呢?一种方法是再建立一个文件,通过引导扇区把它加载到内存,然后把控制权教给它。这样,512字节的束缚就没有了。
这里被引导扇区加载进内存的并不是操作系统的内核。因为从开机到开始运行,操作系统经历了“引导→加载内核入内存→跳入保护模式→开始执行内核”这样一个过程。也就是说,才内核开始执行之前不但要加载内核,而且还有准备保护模式等一系列工作,如果全都交给引导扇区来做,512字节很可能是不够用的。因此,这里加载进内存的并不是内核,而是另外一个模块叫Loader。引导扇区把Loader加载进内存并把控制权交给它。上面所说的其他工作都交给Loader来做。Loader没有512字节的限制。所以会灵活很多。
接下来最主要的是如何找到Loader文件并加载进入内存。首先介绍FAT12文件系统
FAT12FAT的全称是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 retloader的第一个扇区找到了,接下来寻找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运行结果如下:

相关热词:
本站内容来源于网络,如有侵权请与我们联系,我们会及时删除,我们深感抱歉!
注:本站所有信息仅供用于网络技术学习参考,学习中请遵循相关法律法规!
本文地址: https://www.juheyunku.com/jiaob/zh/9860.shtml
相关文章
热门TAG
命令 权重 外链 企业网站 白帽 php 织梦教程 dedecms修改内容 javascript 织梦 功能 标签 调用 详解 服务器 网站流量 实例解析 Dedecms 织梦cms HTML tags标签 python jquery教程 jquery windows SEO优化 蜘蛛 搜索引擎 网站收录 JSP最新文章
-
Servlet使用预设参数
时间:2020-12-27
-
niubijob一个开源的分布式任
时间:2020-12-27
-
前端学HTTP之安全HTTP
时间:2020-12-27
-
技术培训|资源编排 人人都
时间:2020-12-27
-
AR增强现实开发介绍(续)
时间:2020-12-27
-
一个操作系统的实现(11)让
时间:2020-12-27
热门文章
-
Servlet使用预设参数
时间:2020-12-27
-
一个操作系统的实现(11)让操作系统进入保
时间:2020-12-27
-
前端学HTTP之安全HTTP
时间:2020-12-27
-
技术培训|资源编排 人人都可以成为架构
时间:2020-12-27
-
AR增强现实开发介绍(续)
时间:2020-12-27
-
niubijob一个开源的分布式任务调度框架 安
时间:2020-12-27
