-1.写在开始之前
虽然网上此类教程云集,虽然此类书籍很多,但是!
这些书籍有很多地方讲得不够细致(主要是代码有缺漏),有些对代码的更改甚至在书中了无痕迹。
而这才是我开启这篇教程的原因。
这篇教程之中,只要照着所有的操作做了一遍,以您 OIer
的水平,应当能够写出完整的操作系统!
不过,与其说这篇文章是个教程,倒不如说是一个学习笔记和我自身编程经验的记录。
0.开发环境配置
如果您使用的是 Linux
,我们只需要输入下面一行命令即可完成开发环境的配置:
sudo apt-get install nasm build-essential qemu-system-x86
如果您使用的 Linux
中不含有 apt
系列包管理器,请使用您系统中的包管理器。
如果您使用的是 Linux
,但您的系统内没有包管理器,那么您可以去 nasm 官网、 gcc 官网和 qemu 官网下载源码,然后 configure -> make -> sudo make install。
如果您使用的是 Windows
,请去以下地方获取所需要的工具:
交叉编译的gcc(请下载i686-elf-tools-windows.zip)
bochs(其实我们只需要其中的 bximage.exe )(如果是 32 位电脑请下载 2.5 以前的版本)
edimg(这个就是《30天自制操作系统》的写盘工具)
如果您使用的是 macOS
,那么请注意,系统内置的 gcc
会把文件编译成 Mach-O
格式,请通过 Homebrew
下载交叉编译器:
brew install i386-elf-binutils
brew install i386-elf-gcc
然后我们还需要去往 nasm 官网获取可执行文件,并执行:
brew install qemu
以获取 qemu。
在安装完之后,如果您使用的是 Windows
,请确保它们的路径位于 PATH 下!
除此之外便没什么重点了,不过,对于下文给出的工具名称默认以 Windows
为准,若您使用 Linux
,请去掉工具前缀,若您使用 macOS
,请将工具前缀中的 i686
改为 i386
!!!
对了,如果您使用的是 Linux
或 macOS
,请确保您在 dd
命令的后面加入 conv=notrunc
!!!
那么,开发环境配置正式结束,征程开始!
1.第一个引导扇区
所谓引导扇区,其实就是一段可执行的代码而已,不过加入了一个小限制:编译后的总字节数不能超过 512,同时扇区最后两个字节必须是 55 AA 。
虽然现在看来这个限制并不怎样,但一到后面再回过头来,您将会发现这是一个非常恶心的限制。不过没关系,对于现在的我们来说,这个限制并不大。
那么我们的目标就是用 BIOS 中断 int 10h
往屏幕上输出信息,具体用法如下:
AH=13h:输出信息
BH=页码(一般可以置0)
BL=属性(当al=0或1)
(DH, DL):行和列
ES:BP:字符串地址
AL=输出方式
AL=0:仅含显示字符,字符属性(颜色等)位于 BL 中。显示后,光标位置不变。
AL=1:同 AL=0,但显示后光标位置改变。
AL=2:字符串中含有显示字符和显示属性。显示后,光标位置不变。
AL=3:同 AL=2,但显示后光标位置改变。
那么此次我们要使用的就是 AH=13h AL=01h
的显示方法,即显示字符串后光标移动。
知道怎么显示字符串,代码也就好写了:
代码 1-1 最简单的引导扇区(boot.asm)
org 07c00h ; 告诉编译器程序将装载至0x7c00处
mov ax, cs
mov ds, ax
mov es, ax ; 将ds es设置为cs的值(因为此时字符串存在代码段内)
call DispStr ; 显示字符函数
jmp $ ; 死循环
DispStr:
mov ax, BootMessage
mov bp, ax ; es前面设置过了,所以此处的bp就是串地址
mov cx, 16 ; 字符串长度
mov ax, 01301h ; 显示模式
mov bx, 000ch ; 显示属性
mov dl, 0 ; 显示坐标(这里只设置列因为行固定是0)
int 10h ; 显示
ret
BootMessage: db "Hello, OS world!"
times 510 - ($ - $$) db 0
db 0x55, 0xaa ; 确保最后两个字节是0x55AA
程序写好了,我们该怎么运行呢?首先编译一下:
nasm boot.asm -o boot.bin
对于 Linux
和 macOS
用户而言,只需要下面两行命令就可以完成软盘映像的创建与写入:
dd if=/dev/zero of=a.img bs=512 count=2880
dd if=boot.bin of=a.img bs=512 count=1 conv=notrunc
如果您使用的是 Windows
,那么请执行 bximage
。下面是使用 bximage
创建软盘映像的实例:
> bximage
========================================================================
bximage
Disk Image Creation Tool for Bochs
$Id: bximage.c,v 1.34 2009/04/14 09:45:22 sshwarts Exp $
========================================================================
Do you want to create a floppy disk image or a hard disk image?
Please type hd or fd. [hd] fd
Choose the size of floppy disk image to create, in megabytes.
Please type 0.16, 0.18, 0.32, 0.36, 0.72, 1.2, 1.44, 1.68, 1.72, or 2.88. [1.44]
I will create a floppy image with
heads=2
sectors per track=18
total sectors=2880
total bytes=1474560
What should I name the image? [a.img]
Writing: [] Done.
I wrote 1474560 bytes to a.img.
The following line should appear in your bochsrc:
floppya: image="a.img", status=inserted
(The line is stored in your windows clipborad, use CTRL-V to paste)
Press any key to continue
>
硬盘镜像制作完成之后,我们再执行一条写入命令:
dd if=boot.bin of=a.img bs=512 count=1
注意,Windows
下的 dd
不支持 conv
选项。
另外,如果您的 boot.bin
被报毒 KillMBR
,请不要惊慌,因为它就是一个 MBR
,因此被判为覆盖 MBR
的病毒非常正常,默认不做操作即可。
无论是上述哪种情况,在制作完成之后,直接执行一行命令来执行:
qemu-system-i386 -fda a.img
如果您的执行结果如下图,那么恭喜您,您的引导扇区成功执行了!
(图 1-1 运行结果)
无论您使用的是哪种虚拟机,只要左上角出现 Hello, OS world!
就算是成功。
2.FAT12
文件系统
前面我们花了极大的篇幅来写一个极简引导扇区的实现,但是本节相比之下就要短很多了,我们要在我们的引导扇区中加入 FAT12
文件系统头,这样后续我们写入 Loader
和 Kernel
就要方便很多了。
FAT12
文件系统的具体结构如下图所示(实在懒得打列表了,干脆搬了一张网图):
如诸位所见,FAT12
文件系统头占用了汇编程序开头的 64 个字节。注意,对于 org
指令来说,它并不在所编译得出的机器码之列,因此被称为伪指令。
那么我们就依照此结构写入一下这些结构吧:
代码 2-1 FAT12
文件系统头(boot.asm)
org 07c00h ; 告诉编译器程序将装载至0x7c00处
jmp short LABEL_START
nop ; BS_JMPBoot 由于要三个字节而jmp到LABEL_START只有两个字节 所以加一个nop
BS_OEMName db 'tutorial' ; 固定的8个字节
BPB_BytsPerSec dw 512 ; 每扇区固定512个字节
BPB_SecPerClus db 1 ; 每簇固定1个扇区
BPB_RsvdSecCnt dw 1 ; MBR固定占用1个扇区
BPB_NumFATs db 2 ; FAT12 文件系统固定2个 FAT 表
BPB_RootEntCnt dw 224 ; FAT12 文件系统中根目录最大224个文件
BPB_TotSec16 dw 2880 ; 1.44MB磁盘固定2880个扇区
BPB_Media db 0xF0 ; 介质描述符,固定为0xF0
BPB_FATSz16 dw 9 ; 一个FAT表所占的扇区数,FAT12 文件系统固定为9个扇区
BPB_SecPerTrk dw 18 ; 每磁道扇区数,固定为18
BPB_NumHeads dw 2 ; 磁头数,bximage 的输出告诉我们是2个
BPB_HiddSec dd 0 ; 隐藏扇区数,没有
BPB_TotSec32 dd 0 ; 若之前的 BPB_TotSec16 处没有记录扇区数,则由此记录,如果记录了,这里直接置0即可
BS_DrvNum db 0 ; int 13h 调用时所读取的驱动器号,由于只挂在一个软盘所以是0
BS_Reserved1 db 0 ; 未使用,预留
BS_BootSig db 29h ; 扩展引导标记
BS_VolID dd 0 ; 卷序列号,由于只挂载一个软盘所以为0
BS_VolLab db 'OS-tutorial' ; 卷标,11个字节
BS_FileSysType db 'FAT12 ' ; 由于是 FAT12 文件系统,所以写入 FAT12 后补齐8个字节
LABEL_START: ; 后面就是正常的引导代码
mov ax, cs
mov ds, ax
mov es, ax ; 将ds es设置为cs的值(因为此时字符串存在代码段内)
call DispStr ; 显示字符函数
jmp $ ; 死循环
DispStr:
mov ax, BootMessage
mov bp, ax ; es前面设置过了,所以此处的bp就是串地址
mov cx, 16 ; 字符串长度
mov ax, 01301h ; 显示模式
mov bx, 000ch ; 显示属性
mov dl, 0 ; 显示坐标(这里只设置列因为行固定是0)
int 10h ; 显示
ret
BootMessage: db "Hello, OS world!"
times 510 - ($ - $$) db 0
db 0x55, 0xaa ; 确保最后两个字节是0x55AA
按上文的方法编译运行,结果仍应如图 1-1 所示。虽然显示结果没有变化,但此时的软盘已经拥有了 FAT12
文件系统。
3.查找 Loader
总是困在小小的引导扇区之中,也不是长久之计,毕竟只有 446 个字节能给我们自由支配,而保护模式的栈动不动就 512 字节,一个引导扇区完全盛不下。所以我们有必要进入一个跳板模块,并在其中进行初始化工作,再进入内核。
这时候又该有人问了:
啊所以为什么不直接进内核呢?
emmm,事实上也有这种系统(比如haribote
),但这样的一个缺点就是你的内核文件结构必须很简单甚至根本没有结构才行。
所以我们还是老老实实地跳入 Loader
再进内核吧,不过话说回来,我们现在连一个正经八百的 Loader
都还没有,不着急,我们马上创建一个:
代码 3-1 极简 Loader
(loader.asm)
org 0100h
mov ax, 0B800h
mov gs, ax ; 将gs设置为0xB800,即文本模式下的显存地址
mov ah, 0Fh ; 显示属性,此处指白色
mov al, 'L' ; 待显示的字符
mov [gs:((80 * 0 + 39) * 2)], ax ; 直接写入显存
jmp $ ; 卡死在此处
这个 Loader
的作用很简单,只是在屏幕第一行的正中央显示一个白色的 “L” 。
现在最主要的问题就是我们应该怎样寻找 Loader
呢?
这个很简单,在根目录区中是一个一个一个32字节的文件结构,其中就包含文件名,我们在根目录区中查找即可。
依照 FAT12 文件系统,根目录区排在 FAT 表和引导扇区后面,因此它的起始扇区是 BPB_RsvdSecCnt + BPB_NumFATs * BPB_FATSz16 = 19 号扇区;它的结束位置则是 19 + BPB_RootEntCnt * 32 / BPB_BytsPerSec = 33。
于是我们的思路便有了:从第 19 号扇区开始,依次读取根目录区,并在其中查找 LOADER BIN
(loader.bin写入之后的文件名)。如果已经读到第 34 扇区而仍然没有找到 LOADER BIN
,那么就默认该磁盘内不存在 loader
。
那么我们该怎么读取磁盘呢?这又是个问题,不过,int 13h
已经给我们提供了这个功能:
AH=02h:读取磁盘
AL:待读取扇区数
CH:起始扇区所在的柱面
DH:起始扇区所在的磁头
CL:起始扇区在柱面内的编号
DL:驱动器号
ES:BX:读入缓冲区的地址
返回值:
FLAGS.CF=0:操作成功,AH=0,AL=成功读入的扇区总数
FLAGS.CF=1:操作失败,AH 存放错误编码
错误编码我们并不需要,只需要保证 FLAGS.CF
的值为 0
就可以了。对此,我们可以执行一个 jc
跳转命令,它的作用是当 FLAGS.CF
为 1
时跳转。
思路有了,读盘功能也有了,我们就开始写程序吧。首先在 DispStr
函数的后面加入一个读取扇区的函数 ReadSector
,它的作用是从第 ax
号扇区开始,读取 cl
个扇区至 es:bx
: