突破512字节的限制
前面我们的工作是完成了一个简陋的引导扇区,虽然感觉没做啥,但是我们实际上积累了很多代码,熟悉了保护模式。并且对存储管理、特权级控制等有了一个整体的认识
。下面,我们要想办法将自己的OS进入到保护模式,虽然进入保护模式不难,但是总会收到引导扇区512字节的限制,所以下面,我们再建立一个文件,将其通过引导扇区加载入内存,然后将控制权交给它。
首先,我们先理清楚一个问题,是不是被引导扇区加载到内存的就是操作系统的内核呢,我们先看看一个操作系统从开机到开始运行要经过一个怎样的过程:引导->加载内核入内存->跳入保护模式->开始执行内核。
所以,很明显,在内核开始执行前,不仅仅要加载内核,还有跳入保护模式等等,而这些工作都由引导扇区来做,很有可能不止512字节,所以我们把这个过程交给叫做Loader的模块来做。引导扇区负责把Loader加载到内存,并把控制权交给它,然后其他工作都由Loader来做,而它就没有512字节的限制了。
FAT12
FAT12是DOS时代就开始使用的文件系统,直到现在还在使用。几乎所有的文件系统都会把磁盘划分为若干层次(扇区:磁盘上的最小数据单元;簇:一个或者多个扇区;分区:通常指整个文件系统)以方便组织和管理,所以我们将软盘做成FAT12格式,以方便Kernel的操作。
引导扇区是整个软盘的第0个扇区,在这个扇区中有一个很重要的数据结构叫做BPB(BIOS Parameter Block),引导扇区的格式如下图:
其中,名称以BPB开头的域输入BPB,以BS_开头的域只是引导扇区的一部分。以下是整个软盘的结构图:
接下来,我们试着把Loader复制到软盘上,并引导扇区找到并加载它。为简单起见,我们规定Loader只能放在根目录中,而根目录信息存放在FAT2后面的根目录区中。所以先研究根目录区。
根目录区位于第二个FAT表之后,开始的扇区号为19,它有若干个目录条目组成,条目最多有BPB_RootEntCnt个。由于根目录的大小依赖BPB_RootEntCnt的,所以长度不固定。
根目录区中的条目格式:
当我们寻找Loader时,只要发现文件名正确就认为它是我们要找的那一个文件。其中最重要的信息是DIR_FstClus,即文件开始簇号,它会告诉我们文件存放在磁盘的什么位置,从而让我们可以找到它。由于一簇只包含一个扇区,所以简化起见,下面都用扇区来替代簇。,需要注意的是,数据区的第一个簇号是2。所以,我们必须计算根目录区所占的扇区数才能知道数据区的第一个簇在哪里。假设根目录区总共有RootDirSectors个扇区,则有:
1 | RootDirSectors = ((BPB_RootEntCnt*32)+(BPB_BytsPerSec-1))/BPB_BytsPerSec |
有了以上公式,可以通过根目录区找到文件并看到内容,而FAT的作用在于,如果文件大于512字节,我们需要FAT表来找到所有的簇。
DOS可以识别的引导盘
既然引导扇区需要有BPB等头信息才能被微软识别,我们就先加上它,让程序开头变成下面的形式:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23jmp 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个字节
把生成的Boot.bin写入磁盘引导扇区,运行的效果没有变。说明我们现在的软盘已经能被DOS以及Linux识别了,我们已经可以方便地往上添加或删除文件了。
一个最简单的Loader
我们先写一个最小的,让其显示一个字符,然后进入死循环。
新建一个loader.asm,其代码如下:1
2
3
4
5
6
7
8
9org 0100h
mov ax, 0B800h
mov gs, ax
mov ah, 0Fh ; 0000: 黑底 1111: 白字
mov al, 'L'
mov [gs:((80 * 0 + 39) * 2)], ax ; 屏幕第 0 行, 第 39 列。
jmp $ ; 到此停住
我们将其编译,命令如下:1
nasm loader.asm -o loader.bin
为了以后扩展不出问题,我们将编译后的二进制文件放在某个段内偏移0x100的位置。
加载Loader如内存
要加载一个文件如内存,免不了要读软盘,这时就要用到BIOS中断int 13h。它的用法如下:
。
从上可知,中断需要的参数不是原来提到的从第0扇区开始的扇区号,而是柱面号、磁头号以及在当前柱面上的扇区号3个分量,所以需要我们自己来转换一下。对于1.44MB的软盘来说,总共有两面,每面80个磁道,每个磁道有18个扇区。下面的公式就是软盘容量的由来:1
2 x 80 x 18 x 512 =1.44MB
于是,磁头号、柱面号和起始扇区号可以用下图方法来计算:
下面,我们先写一个读软盘区的函数:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27push 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)
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
因为这段代码中用到了堆栈,要在程序开头初始化ss和esp1
2
3
4
5
6
7BaseOfStack equ 07c00h ; 堆栈基地址(栈底, 从这个位置向低地址生长)
....
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, BaseOfStack
读扇区的函数写好了,下面我们就开始在软盘中寻找Loader.bin1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61xor 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 ; / 如果读完表示没有找到 LOADER.BIN
dec word [wRootDirSizeForLoop] ; /
mov ax, BaseOfLoader
mov es, ax ; es <- BaseOfLoader
mov bx, OffsetOfLoader ; bx <- 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
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
jmp $ ; 没有找到 LOADER.BIN, 死循环在这里
LABEL_FILENAME_FOUND: ; 找到 LOADER.BIN 后便来到这里继续
jmp $ ; 代码暂时停在这里
这段代码就是遍历根目录区所有的扇区,将每一个扇区加载入内存,然后从中寻找文件名为Loader.bin的条目,直到找到为止。找到的那一刻,es:di是指向条目中字母N后面的那个字符。
接着我们编译出boot.bin1
nasm boot.asm -o boot.bin
,然后在bximage生成一个软盘映像,然后在Linux下输入命令:1
2
3
4
5nasm 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
向Loader交出控制权
上面代码调试通过后,我i门就已经成功的将Loader加载入内存了,接着我们加上一个跳转。开始执行Loader1
2
3
4jmp BaseOfLoader:OffsetOfLoader ; 这一句正式跳转到已加载到内
; 存中的 LOADER.BIN 的开始处,
; 开始执行 LOADER.BIN 的代码。
; Boot Sector 的使命到此结束。
最后的结果如下
保护下的“操作系统”
为了让自己的操作系统内核至少应该可以在Linux下用GCC编译链接,所以我们假设已经有了一个内核,Loader肯定要加载它乳内存,而且内核开始执行对的时候肯定已经在保护模式下了,所以Loader要做的只要有两件事:
- 加载内核如内存
- 跳入保护模式
将来的内核是在Linux下编译链接出的ELF格式文件,直接放进内存肯定不行,下一章就会开始研究ELF格式。