System

软盘读取与启动区

从今天开始制作一个操作系统,今天完成最基本的功能---输出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_jmpBOOT03一个短跳转指令jmp short LABEL_STARTnop
BS_OEMName38厂商名'ZGH'
BPB_BytesPerSec112每扇区字节数(Bytes/Sector)0x200
BPB_SecPerClus131每簇扇区数(Sector/Cluster)0x1
BPB_ResvdSecCnt142Boot记录占用多少扇区0x1
BPB_NumFATs161共有多少FAT表0x2
BPB_RootEntCnt172根目录区文件最大数0xE0
BPB_TotSec16192扇区总数0xB40
BPB_Media211介质描述符0xF0
BPB_FATSz16222每个FAT表所占扇区数0x9
BPB_SecPerTrk242每磁道扇区数(Sector/track)0x12
BPB_NumHeads262磁头数(面数)0x2
BPB_HiddSec284隐藏扇区数0
BPB_TotSec32324如果BPB_TotSec16=0,则由这里给出扇区数0
BS_DrvNum361INT 13H的驱动器号0
BS_Reserved1371保留,未使用0
BS_BootSig381扩展引导标记(29h)0x29
BS_VolID394卷序列号0
BS_VolLab4311卷标'ZGH'
BS_FileSysType548文件系统类型'FAT12'
引导代码62448引导代码及其他数据引导代码(剩余空间用0填充)
结束标志0xAA555102第510字节为0x55,第511字节为0xAA0xAA55

因此当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