上篇文章为了吸引读者(其实也没吸引几个人),先介绍了怎么让自己的系统镜像跑起来
VictorYXL:30天自制操作系统-初版镜像和启动zhuanlan.zhihu.com这篇回归正题,介绍这个镜像的实现。
系统引导扇区
一个操作系统,想要被BIOS找到,依靠的就是引导扇区。所以操作系统的第一步就是实现引导扇区,上一篇的镜像其实就是一个引导扇区+空的系统,这篇的重点就是怎么实现引导扇区。
关于汇编语言,书中用的工具是作者自己写的nask工具,我这里还是用通用的nasm给大家介绍。实现引导的code在这里,是我第一版代码的整理,以后的代码也都放在这里。
VictorYXL/MyOSRefinegithub.com![a43196eb4b44d7468961c66cf1b887b6.png](https://i-blog.csdnimg.cn/blog_migrate/ceaca6ebfddbd45aa889860737ab3e7f.png)
2.13.03下测试可以运行,和作者代码差距不大,整个代码包含四段,前三段是512个字节的引导扇区,最后是引导扇区以外的内容。
引导扇区的实现
第一段指明程序加载的位置。
ORG 0x7c00
ORG命令指明将整个程序加载到内存的0x7c00处,0x7c00是人为规定的地址,不能更改。
第二段标准FAT12格式软盘的结构
为了方便大家理解,我加了一些中文注释
; Format of floppy disk in fat12
JMP entry ; JMP跳转指令,跳转到目标内存地址
; 注意1 这列的跳转指的是计算机执行时候的跳转,而不是汇编的时候跳转
; 注意2 entry表示entry代码段在内存中的起始位置
DB 0x90 ; DB,DD和DW分别是向镜像中写单字节,双字节和四字节
DB "MyOS IPL" ; 写入启动区名字,必须是8字节
DW 512 ; 启动扇区大小512字节
DB 1 ; 1个簇大小是1个扇区
DW 1 ; FAT从第一个扇区启动
DB 2 ; FAT个数为2个
DW 224 ; 根目录大小224
DW 2880 ; 磁盘包含2880个扇区
DB 0xf0 ; 磁盘种类必须为0xf0
DW 9 ; FAT长度为9扇区
DW 18 ; 1个磁道18个扇区
DW 2 ; 2个磁头
DD 0 ; 不使用分区
DD 2880 ; 同上,磁盘大小
DB 0, 0, 0x29 ; 无意义,固定这么写
DD 0xffffffff ; 无意义,固定这么写
DB "MyOS " ; 磁盘名,11字节
DB "FAT12 " ; 磁盘格式名,8字节
RESB 18 ; 空18个字节,填充0x00
这里的代码基本都是写死的,如果对磁头磁道扇区这些概念不熟悉,后面在读写磁盘的时候也会讲到,其他的细节都是基于fat12格式软盘的信息,不必细究。
第三段是核心代码,调用中断显示Hello, world
初始化寄存器
这里先简单介绍下寄存器,对此比较了解的同学可以跳过。寄存器是CPU里的存储电路,存储CPU需要处理的数据和处理的结果,这里使用的是16位寄存器,主要的是以下几个(图来自书本)。
![77d2c7e69802ecbc0ea57a214b5b2bbb.png](https://i-blog.csdnimg.cn/blog_migrate/dfaa7a0b6963c821eb99b9757093d8a2.png)
![0fd85bcb110fb9a68c35d06ed451f1e1.png](https://i-blog.csdnimg.cn/blog_migrate/126a23ff0e5dda127e8624aaeef9f719.png)
其中基础寄存器的前四种可以拆分为高位和低位单独处理,如AX包含AL(低位)和AH(高位)。在32位电脑中用EAX、ECX等表示32为寄存器,我们这里使用16位寄存器就够了,对各个寄存器作用这里只做简单介绍,更多细节不清楚的同学自己百度下吧。
; Boot kernel
entry:
; Init register
MOV AX, 0
MOV SS, AX
MOV SP, 0x7c00
MOV DS, AX
MOV ES, AX
MOV SI, msg
SS:SP是指向栈顶的单元,这里通过AX赋值成0:0x7c00,指向我们这段代码本身,并且初始化DS和ES为0,最后把msg的地址赋给SI,和entry一样,msg也是代码段的起始位置,。
接下来进入循环打印的阶段
; Loop: print string with interrupt
print:
MOV AL, [SI]
ADD SI, 1
CMP AL, 0
JE end
MOV AH, 0x0e
MOV BX, 15
INT 0x10
JMP print
这里的核心就是打印SI的内容,[SI]表示SI内存中的值,理解成C语言的指针,SI和[SI]的关系大概就是就是p和*p的关系。把SI指向的字符赋给AX的低位,SI自身再向后移动一位,接下来是判断,CMP和JE实现,如果CMP的两个值相同则跳转到JE的内容,这里就是判断如果读到了0字符则进入end。后面是调用0x10中断显示字符,调用的格式如下:
![ae5926fba13c0e76c22ae05d094605ec.png](https://i-blog.csdnimg.cn/blog_migrate/ed3bc57009852098b4d39a9e6dc59c28.png)
中断结束再跳回到开头继续打印。
打印结束
; Wait
end:
HLT
JMP end
这段是打印结束的等待,HLT是操作系统交出CPU控制权等待外部信号,我们这里不做任何处理,表示程序结束。
打印内容
; msg
msg:
DB 0x0a, 0x0a, 0x0a
DB "Hello,world!"
DB 0x0a
DB 0
这段很好理解, 0x0a是换行,中间是要打印的字符串,最后的0是结束标志,对应前面循环打印的结束。
引导扇区结尾
; Boot end
TIMES 510 - ($ - $$) DB 0
DB 0x55, 0xaa
引导区最后一段是引导扇区结束,引导扇区结束一定要以0x55, 0xaa结束,所以前面补充在510字节前补充0。TIMES的重复执行,格式是TIMES 循环次数 执行内容,$ 和 $$分别表示当前地址和段首地址,所以就是到当前职位到510字节前全部写0,最后写上0x55, 0xaa两个字节结束。
第四段是引导扇区意外的内容
; Output outside booting section
DB 0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00
RESB 4600
DB 0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00
RESB 1468432
满足2880*512个字节就好,内容不重要,反正也不会被执行。
执行汇编
最后用nasm将这段汇编翻译成机器码。
nasm IPL.nsm -o MyOS.img
如果对汇编不是很熟悉,在写这段汇编的时候,一定要想明白自己是在填充镜像的内容,还是在模拟镜像的运行,否则很容易陷入结构上的混乱,是在不行就多写几次。
写的比较随意,如有疏漏欢迎指正。
TIMES 0x510 - ($ - $$) DB 0
处有失误,应该是
TIMES 510 - ($ - $$) DB 0
感谢 @qiujian2008 的指正。