软盘读取与启动区
从今天开始制作一个操作系统,今天完成最基本的功能---输出hello world。
首先还是采用软盘启动,制作一个软盘镜像,然后用虚拟机装载。
现在软盘都淘汰了,为什么不用U盘或者硬盘镜像呢? 因为软盘是基础,软盘1.44MB足够了,格式简单又清楚,U盘/硬盘只不过是软盘格式的进一步拓展。
制作软盘镜像的时候,我们第一步就是创建并格式化一个软盘镜像,软盘一般为FAT12格式, FAT12是DOS时代就开始使用的文件系统(File System),直到现在仍然在软盘上使用。 FAT12软盘的被格式化后为:有两个磁头,每个磁头80个柱面(磁道),每个柱面有18个扇区,每个扇区512个字节空间。
- 磁头(Heads):每张磁片的正反两面各有一个磁头,一个磁头对应一张磁片的一个面。因此,用第几磁头就可以表示数据在哪个磁面。
- 柱面(Cylinder):所有磁片中半径相同的同心磁道构成“柱面",意思是这一系列的磁道垂直叠在一起,就形成一个柱面的形状。简单地理解,柱面数=磁道数。
- 扇区(Sector):将磁道划分为若干个小的区段,就是扇区。虽然很小,但实际是一个扇子的形状,故称为扇区。每个扇区的容量为512字节。
知道了磁头数、柱面数、扇区数,就可以很容易地确定数据保存在软盘的哪个位置。也很容易确定软盘的容量,其计算公式是:
硬盘容量=磁头数2×柱面数80×扇区数18×512字节=1474560字节
因此这么一张软盘的容量就是1.44MB,我们可以看作有2880个元素的数组,每个元素512字节。
计算机之所以能加载操作系统,其秘密就在于第0扇区这512字节上。 计算机在读取磁盘的时候,首先从最初的一个扇区开始读盘, 如果这512字节的最后两个字节的内容分别是55和AA(0xAA55,低字节在前,高字节在后)的话, 那么计算机就会认为这个扇区的内容是启动程序,并开始从头执行这个程序。 BIOS在启动时会将这个第0扇区512字节内容读取到内存的0x7C00H~0x7DFFH处,然后跳转到0x7C00H处开始执行指令, 操作系统即用此来达到引导系统的目的,而这个第0扇区就称为引导扇区(也称为启动区), 第0扇区的512字节内容就称为主引导记录。
随后的第1扇区以及之后的位置,我们就可以用来放置我们的操作系统的代码。
注意: 标识FAT12文件系统是在第0扇区(即引导扇区)处还存储着一个特定的数据结构, 此结构有固定的格式,在操作系统将此磁盘格式化时自动生成,具体数据结构如下表所示:
名称 | 开始字节 | 长度 | 内容 | 参考值 |
---|---|---|---|---|
BS_jmpBOOT | 0 | 3 | 一个短跳转指令 | jmp short LABEL_STARTnop |
BS_OEMName | 3 | 8 | 厂商名 | 'ZGH' |
BPB_BytesPerSec | 11 | 2 | 每扇区字节数(Bytes/Sector) | 0x200 |
BPB_SecPerClus | 13 | 1 | 每簇扇区数(Sector/Cluster) | 0x1 |
BPB_ResvdSecCnt | 14 | 2 | Boot记录占用多少扇区 | 0x1 |
BPB_NumFATs | 16 | 1 | 共有多少FAT表 | 0x2 |
BPB_RootEntCnt | 17 | 2 | 根目录区文件最大数 | 0xE0 |
BPB_TotSec16 | 19 | 2 | 扇区总数 | 0xB40 |
BPB_Media | 21 | 1 | 介质描述符 | 0xF0 |
BPB_FATSz16 | 22 | 2 | 每个FAT表所占扇区数 | 0x9 |
BPB_SecPerTrk | 24 | 2 | 每磁道扇区数(Sector/track) | 0x12 |
BPB_NumHeads | 26 | 2 | 磁头数(面数) | 0x2 |
BPB_HiddSec | 28 | 4 | 隐藏扇区数 | 0 |
BPB_TotSec32 | 32 | 4 | 如果BPB_TotSec16=0,则由这里给出扇区数 | 0 |
BS_DrvNum | 36 | 1 | INT 13H的驱动器号 | 0 |
BS_Reserved1 | 37 | 1 | 保留,未使用 | 0 |
BS_BootSig | 38 | 1 | 扩展引导标记(29h) | 0x29 |
BS_VolID | 39 | 4 | 卷序列号 | 0 |
BS_VolLab | 43 | 11 | 卷标 | 'ZGH' |
BS_FileSysType | 54 | 8 | 文件系统类型 | 'FAT12' |
引导代码 | 62 | 448 | 引导代码及其他数据 | 引导代码(剩余空间用0填充) |
结束标志0xAA55 | 510 | 2 | 第510字节为0x55,第511字节为0xAA | 0xAA55 |
因此当CPU跳转到内存的0x7C00H处继续执行指令时,该位置的指令是一个短跳转指令,从而你可以在这里写跳转到你指定的位置继续执行。
0x7C00这个地址来自Intel的第一代个人电脑芯片8088,以后的CPU为了保持兼容,一直使用这个地址。 当时,搭配的操作系统是86-DOS。这个操作系统需要的内存最少是32KB。我们知道,内存地址从0x0000开始编号,32KB的内存就是0x0000~0x7FFF。 8088芯片本身需要占用0x0000~0x03FF,用来保存各种中断处理程序的储存位置。所以,内存只剩下0x0400~0x7FFF可以使用。 为了把尽量多的连续内存留给操作系统,主引导记录就被放到了内存地址的尾部。 由于一个扇区是512字节,主引导记录本身也会产生数据,需要另外留出512字节保存。 所以,它的预留位置就变成了:
0x7FFF - 512 - 512 + 1 = 0x7C00
,0x7C00就是这样来的。
根据启动时的内存结构,0x7C00~0x7DFF 用于载入主引导记录作为引导区, 而0x7E00~0x7FFF用于引导区运行时栈,因此从0x8000开始之后的内存都是空闲。
然而引导区的容量实在太少了(512字节还被固定格式数据用去不少),所以我们需要在其他地方实现启动操作系统(比如说将实现启动操作系统的脚本放到第1扇区), 而主引导区里的逻辑就仅仅只是读取软盘所有数据,拷贝在内存从0x8000开始之后的位置(也叫启动程序加载器,IPL),然后跳转到实现启动操作系统的脚本位置 (启动操作系统脚本,也叫BOOT)。
注意: 第0扇区的内容既然已经加载到内存的0x7C00~0x7DFF位置,那么我们就不用再复制一遍第0扇区的内容, 因此,我们把第1扇区及之后的内容拷贝到内存从0x8000开始之后的位置。拷贝完毕后直接跳转到0x8000。
好,大致理论都讲完了,那么代码该怎么写呢?我用的是nasm汇编代码,下面是引导扇区的内容。
; IPL.asm
; 引导扇区的内容
ORG 0x7c00
JMP entry
; 标准FAT12格式软盘专用的代码 Stand FAT12 format floppy code
DB 0x90
DB "HARIBOTE" ; 启动扇区名称(8字节)
DW 512 ; 每个扇区(sector)大小(必须512字节)
DB 1 ; 簇(cluster)大小(必须为1个扇区)
DW 1 ; FAT起始位置(一般为第一个扇区)
DB 2 ; FAT个数(必须为2)
DW 224 ; 根目录大小(一般为224项)
DW 2880 ; 该磁盘大小(必须为2880扇区1440*1024/512)
DB 0xf0 ; 磁盘类型(必须为0xf0)
DW 9 ; FAT的长度(必须为9扇区)
DW 18 ; 一个磁道(track)有几个扇区(必须为18)
DW 2 ; 磁头数(必须为2)
DD 0 ; 不使用分区,必须是0
DD 2880 ; 重写一次磁盘大小
DB 0,0,0x29 ; 意义不明(固定)
DD 0xffffffff ; (可能是)卷标号码
DB "HARIBOTEOS " ; 磁盘的名称(必须为11字,不足填空格)
DB "FAT12 " ; 磁盘格式名称(必须为8字,不足填空格)
TIMES 18 db 0 ; 先空出18字节
; 拷贝软盘所有内容
entry:
; 初始化
MOV AX,0
MOV SS,ax
MOV SP,0x7c00
MOV DS,ax
; 设置参数
MOV AX,0x0800
MOV ES,AX ; 内存基址
MOV CH,0 ; 柱面0
MOV DH,0 ; 磁头0
MOV CL,2 ; 扇区2
readloop:
MOV SI,0 ; 记录失败次数寄存器
retry:
MOV AH,0x02 ; 读入磁盘
MOV AL,1 ; 1个扇区
MOV BX,0
MOV DL,0x00 ; A驱动器,一般就1个驱动器
INT 0x13 ; 调用磁盘BIOS,进行复制
JNC next ; 没出错则跳转到next,进行剩下的复制
ADD SI,1 ; 出错了,往SI加1
CMP SI,5 ; 比较SI与5
JAE error ; SI >= 5 跳转到error
MOV AH,0x00 ; 否则继续重新尝试,重置寄存器
MOV DL,0x00 ; A驱动器
INT 0x13 ; 重置驱动器
JMP retry ; 从头开始尝试
next:
MOV AX,ES
ADD AX,0x20
MOV ES,AX ; 把内存地址后移0x200(512/16十六进制转换)
ADD CL,1 ; 往CL里面加1
CMP CL,18 ; 比较CL与18,18个扇面
JBE readloop ; CL <= 18 跳转到readloop,继续装载,扇区
MOV CL,1
ADD DH,1
CMP DH,2
JB readloop ; DH < 2 跳转到readloop,DH是磁头数,2个
MOV DH,0
ADD CH,1
CMP CH,39 ; 读80个柱面会出错,因为16位系统只能支配1MB的内存
JB readloop
; 读取完毕,跳转到boot执行
;jmp error
JMP 0x8000
; 出错显示部分
error:
MOV SI,error_msg
JMP putloop
success:
mov si, success_msg
jmp putloop
putloop:
MOV AL,[SI]
ADD SI,1 ; 给SI加1
CMP AL,0
JE fin
MOV AH,0x0e ; 显示一个文字
MOV BX,15 ; 指定字符颜色
INT 0x10 ; 调用显卡BIOS
JMP putloop
fin:
HLT ; 让CPU停止,等待指令
JMP fin ; 无限循环
error_msg:
DB 0x0a, 0x0a ; 换行两次
DB "zipl error"
DB 0x0a ; 换行
DB 0
success_msg:
DB 0x0a, 0x0a ; 换行两次
DB "zipl success"
DB 0x0a ; 换行
DB 0
times 510-($-$$) db 0
DB 0x55, 0xaa