一个操作系统的实现(4)


走进保护模式

引导扇区突破512个字节的限制

代码逻辑:

jmp 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个字节
LABEL_START:
	mov ax, cs
	mov ds, ax
	mov es, ax
	Call DispStr ; 调用显示字符串例程
	jmp $ ; 无限循环
DispStr:
	mov ax, BootMessage
	mov bp, ax ; ES:BP = 串地址
	mov cx, 16 ; CX = 串长度
	mov ax, 01301h ; AH = 13,  AL = 01h
	mov bx, 000ch ; 页号为0(BH = 0) 黑底红字(BL = 0Ch,高亮)
	mov dl, 0
	int 10h ; int 10h
	ret
BootMessage: db "Hello, OS world!"
times 510-($-$$) db 0 ; 填充剩下的空间,使生成的二进制代码恰好为512字节
dw 0xaa55 ; 结束标志

指令调试:

nasm boot.asm -o boot.bin
dd if=boot.bin if=a.img bs=512 count=1 conv=notrunc
bochs

引导扇区突破512个字节的限制分析:

一个操作系统从开机到开始运行,大致经历“引导→加载内核入内存→跳入保护模式→开始执行内核”这样一个过程。也就是说,在内核开始执行之前不但要加载内核,而且还有准备保护模式等一系列工作,如果全都交给引导扇区来做,512字节很可能是不够用的,所以,不妨把这个过程交给另外的模块来完成,我们把这个模块叫做Loader。引导扇区负责把Loader加载入内存并且把控制权交给它,其他工作放心地交给Loader来做,因为它没有512字节的限制,将会灵活得多。

在这里,为了操作方便,可以把软盘做成FAT12格式,这样对 Loader以及今后的Kernel(内核)的操作将会非常简单易行。

FAT12格式:

FAT12是DOS时代就开始使用的文件系统(File System),直到现在仍然在软盘上使用。几乎所有的文件系统都会把磁盘划分为若干层次以方便组织和管理,这些层次包括:

  • 扇区(Sector):磁盘上的最小数据单元。
  • 簇(Cluster):一个或多个扇区。
  • 分区(Partition):通常指整个文件系统。

将工作分给loader,加载loader进入内存并运行

最简单的Loader

org 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 $ ; 到此停住

这段代码可以被编译成.COM文件直接在DOS下执行,效果是在屏幕第一行中央出现字符“L”,然后进入死循环。

编译:

nasm loader.asm -o loader.bin

需要注意的是,虽然编译出的二进制代码加载到内存的任意位置都可以正确执行,但我们要扩展它,为了将来的执行不会出现问题,要保证把它放入某个段内偏移0x100的位置。

代码及分析:

	;调用中断int 13h,实现软驱复位
	xor ah, ah
	xor dl, dl
	int 13h
	;下面在A盘的根目录中寻找LOADER.BIN
	;wSectorNo表示要读取的扇区号,SectorNoOfRootDirectory
	;表示根目录区的开始扇区号=19
	mov word[wSectorNo], SectorNoOfRootDirectory
LABEL_SEARCH_IN_ROOT_DIR_BEGIN:
	;wRootDirSizeForLoop=RootDirSectors,表示根目录占用的扇区数,即表示要读取的扇区数;也就是
	;最外部循环中的控制变量(相当于i)。
	;判断根目录区所有扇区是不是已经读取完毕,如果读完表示没有找到LOADER.BIN,
	;跳入到LABEL_NO_LOADERBIN,否则,减1。
	cmp word[wRootDirSizeForLoop], 0
	jz LABEL_NO_LOADERBIN
	dec word[wRootDirSizeForLoop]
	;为ReadSector函数准备参数,从第ax个Sector开始读,将cl个Sector读入es:bx中
	mov ax, BaseOfLoader
	mov es, ax ;es<-BaseOfLoader
	mov bx, OffsetOfLoader ;bx<-OffsetOfLoader,于是es:bx=BaseOfLoader:OffsetOfLoader
	mov ax, [wSectorNo] ;ax<-Root Directory中的某Sector号,表示要读取的扇区号
	mov cl, 1 ;cl表示要读取扇区个数=1
	call ReadSector
	;调用ReadSector函数之后,es:bx将存储该扇区数据。
	mov si, LoaderFileName ;ds:si->"LOADER  BIN"
	mov di, OffsetOfLoader ;es:di->BaseOfLoader:OffsetOfLoader=es:bx
	;即es:di指向存储的该扇区数据
	cld
	;一个扇区是512个字节,一个根目录项占32个字节,故512/32=16,因此需要比较16个根目录项的文件名,
	;故赋值dx=16,由dx来控制循环次数
	mov dx, 10h
LABEL_SEARCH_FOR_LOADERBIN:
	;判断dx是否为0,0意味着这个扇区内的所有根目录项进行比较完毕,然后跳入到下一个扇区,继续进行比较,
	;dx=0,则跳入LABEL_GOTO_NEXT_SECTOR_IN_ROOT_DIR;否则,dx--
	cmp dx, 0
	jz LABEL_GOTO_NEXT_SECTOR_IN_ROOT_DIR
	dec dx
	;一个根目录项的文件名占用11个字节,故必须对其每个字节与"LOADER  BIN"一一对比
	;故赋值cx=11,由cx来控制循环次数
	mov cx, 11
LABEL_CMP_FILENAME:
	cmp cx, 0
	jz LABEL_FILENAME_FOUND ;如果cx=0,意味着11个字符都相等,表示找到,跳转到LABEL_FILENAME_FOUND
	dec cx ;否则,cx--
	lodsb ;ds:si->al,ds:si指向的是字符串"LOADER  BIN"
	cmp al, byte[es:di] ;进行一个字符的比较,如果相等,则比较下一个字符,
	jz LABEL_GO_ON ;跳入到LABEL_GO_ON
	jmp LABEL_DIFFERENT ;只要发现有一个不相等的字符就表明本Directory Entry不是我们要
	;找的LOADER.BIN,跳转到LABEL_DIFFERENT,进如下一个Directory Entry比较。
	LABEL_GO_ON:
	inc di ;将di++,进行一个字符的比较。
	jmp LABEL_CMP_FILENAME ;跳转到LABEL_CMP_FILENAME,继续进行文件名比较。
	LABEL_DIFFERENT:
	;di&=E0是为了让它指向本条目开头,di初始化为某个条目开头,
	;在比较过程中,会将它不断增1,当失败之后,必须进行重新初始化
	;因为一个条目占用32个字节,故and di,0FFE0h   add di, 20h
	;之后,di就指向了下一个条目
	and di, 0FFE0h
	add di, 20h
	;重新初始化si,使其指向"LOADER  BIN"的开始位置
	mov si, LoaderFileName
	jmp LABEL_SEARCH_FOR_LOADERBIN ;跳转到LABEL_SEARCH_FOR_LOADERBIN
	LABEL_GOTO_NEXT_SECTOR_IN_ROOT_DIR:
	add word[wSectorNo], 1 ;将要读取的扇区号+1,进行下一个扇区的比较
	jmp LABEL_SEARCH_IN_ROOT_DIR_BEGIN ;跳转到LABEL_SEARCH_IN_ROOT_DIR_BEGIN,开始一个扇区的比较
	;如果最后没有找到"LOADER   BIN",则显示“NO LOADER”字符串来表示。
LABEL_NO_LOADERBIN:
	mov dh, 2 ;"NO LOADER"
	call DispStr ;显示字符串
	%ifdef _BOOT_DEBUG_
	mov dh, 2 ;"NO LOADER"
	call DispStr ;显示字符串
	mov ax, 4C00h
	int 21h ;没有找到LOADER.BIN,返回到DOS
	%else
	jmp $ ;没有找到LOADER.BIN,死循环在这里
	%endif
	;如果找到"LOADER   BIN",则跳转到LABEL_FILENAME_FOUNT,然后进行第二步骤,从
	;Directory Entry中读取文件在数据区的开始簇号。
LABEL_FILENAME_FOUND:

代码流程图:

现在已经有了Loader.bin的起始扇区号,我们需要用这个扇区号来做两件事情:一件是把起始扇区装入内存,另一件则是通过它找到FAT中的项,从而找到Loader占用的其余所有扇区。

然后我们就根据扇区号去FAT表中找到相应的项。要写一个函数GetFATEntry,函数的输入就是扇区号(ax),输出则是其对应的FAT项的值(ax)。

我们知道了扇区号x,然后我们去FAT1中寻找x所对应的FATEntry,我们已经知道一个FAT项占1.5个字节。所以我们用x * 3/2=y………z,y为商(偏移量)(字节),相对于FAT1的初始位置的偏移量;Z为余数(0或者1),是判断FATEntry是奇数还是偶数,0表示偶数,1表示奇数。然后我们让y/512=m………n,m为商,n为余数,此时m为FATEntry所在的相对扇区,n为在此扇区内的偏移量(字节)。因为FAT1表前面还有1个引导扇区,所以FATEntry所在的实际扇区号为m+1。然后读取m+1和m+2两个扇区,然后在偏移n个字节处,取出FATEntry,相当于读取两个字节。此时再利用z,如果z为0的话,此FAT项为前一个字节和后一个字节的后4位,如果z为1的话,此FATEntry取前一个字节的前4位和后一个字节。

实现GetFATEntry函数

;-----------------------------------------------------
;函数名:GetFATEntry
;-----------------------------------------------------
;作用: 找到序号为ax的Sector在FAT中的条目,结果放在ax中,需要注意的是,中间需要读FAT的扇区es:bx处,
; 所以函数一开始保存了es和bx
GetFATEntry:
	push es
	push bx
	push ax
	;在BaseOfLoader后面留出4K空间用于存放FAT
	mov ax, BaseOfLoader
	sub ax,0100h
	mov es, ax ;此时es-> (BaseOfLoader - 100h)
	pop ax ;ax存储的是要读取的扇区号
	mov byte[bOdd], 0
	;ax是要读取的扇区号,如何获得该扇区号在FAT1中的FATEntry
	;因为每个FATEntry占有1个半字节,所以计算ax*3/2,找到该FATEntry所在FAT1中的偏移量
	mov bx, 3
	mul bx
	mov bx, 2
	div bx
	;ax*3/2=ax...dx,商为ax表示该FATEntry在FAT1中的偏移量,dx的值为(0或者1),
	;0表示该FATEntry为偶数,1表示该FATEntry为奇数,
	cmp dx, 0
	jz LABEL_EVEN
	;我们使用byte[bOdd]来保存dx的值,也就是该FATEntry是奇数项还是偶数项。
	mov byte[bOdd], 1
LABEL_EVEN:
	xor dx, dx
	;此时ax中保存着FATEntry在FAT1中的偏移量,下面来计算FATEntry
	;在哪个个扇区中(FAT1占用9个扇区)。
	;ax/BPB_BytsPerSec=ax/512=ax...dx,商ax存储着该FATEntry所在FAT1表的第几个扇区,
	;余数dx保存着该FATEntry在该扇区内的偏移量。
	mov bx, [BPB_BytsPerSec]
	div bx
	push dx ;将dx存储在堆栈中.
	mov bx, 0 ;es:bx=(BaseOfLoader-100):00=(BaseOfLoader-100h)*10h
	;我们知道ax是FATEntry所在FAT1中的相对扇区,而FATEntry所在的实际扇区,需要加上
	;FAT1表的开始扇区号,即加1,之后ax就是FATEntry所在的实际扇区
	add ax, SectorNoOfFAT1
	mov cl, 2
	;读取FATEntry所在的扇区,一次读2个,避免在边界发生错误,
	;因为一个FATEntry可能跨越两个扇区
	call ReadSector
	;从堆栈中弹出dx,FATEntry所在扇区的偏移量,将其与bx相加,此时es:bx指向的是该FATEntry所占用
	;的两个字节空间
	pop dx
	add bx, dx
	;读取该FATEntry
	mov ax, [es:bx]
	;下面是对bOdd进行判断,如果其为0,则表示FATEntry为偶数,此时需要取byte1和byte2的后4位,
	;由于在80x86下,从内存中读取数据之后,byte2在前,byte1在后。
	;所以当FATEntry为偶数时,需要将ax&0FFF,将byte2的前4位置0.
	;反之,如果bOdd为1,则表示FATEntry为奇数,此时需要取得byte1中的前4位和byte2.
	;所以,需要将ax右移4位,将byte1的后四位移除。
	cmp byte[bOdd], 1
	jnz LABEL_EVEN_2
	shr ax, 4
LABEL_EVEN_2:
	and ax, 0FFFh
	;此时ax存储的是FATEntry的值
	LABEL_GET_FAT_ENTRY_OK:
	pop bx
	pop es
	ret

下面开始加载Loader.bin进入内存。

首先从根目录区中的Loader.bin的条目,获取文件的起始扇区号,然后加上BPB_RsrvSecCnt+BPB_FATSz162-2+RootDirSectors=1+(92)+14-2=31,,其中DeltaSectorNo=BPB_RsrvSecCnt+BPB_FATSz16 * 2-2=17。得到的结果才是文件的实际的起始扇区。获得起始扇区后,我们就可以调用ReadSector来读取扇区了。然后从FAT1表中获取FATEntry的值,判断是否为0FFFh,如果是,结束加载;如果不为0FFFh,意味着该文件没有读取完成,需要读取下一个扇区,此时的FATEntry的值,就是下一个扇区号,再将其转换为实际扇区号,再进行读取。

LABEL_FILENAME_FOUND: ;找到了LOADER.BIN后便来到这里继续
	mov ax, RootDirSectors ;根目录区所占用扇区数=14
	and di, 0FFE0h ;di->当前Directory Entry的开始位置
	add di, 01Ah ;di->此条目对应的开始簇号,DIR_FstClus
	mov cx, word[es:di] ;将开始簇号存储在寄存器cx中
	push cx ;将cx入栈
	;实现cx+RootDirSectors+DeltaSectorNo之后,此时cx保存着文件的实际开始扇区号,
	;即数据区内的扇区
	add cx, ax
	add cx, DeltaSectorNo
	mov ax, BaseOfLoader
	mov es, ax
	mov bx, OffsetOfLoader ;es:bx=BaseOfLoader:OffsetOfLoader
	mov ax, cx ;ax表示要读取的扇区号
LABEL_GOON_LOADING_FILE:
	push ax
	push bx
	mov ah, 0Eh
	mov al, '.'
	mov bl, 0Fh
	int 10h
	pop bx
	pop ax
	;每读一个扇区就在”Booting   “后面打一个点,形成这样的效果:Booting......
	;继续为ReadSector函数的参数做准备,cl=1,表示要读取一个扇区
	mov cl, 1
	call ReadSector
	pop ax ;读完一个扇区之后,然后重新读取此Sector在FAT中的序号
	call GetFATEntry
	cmp ax, 0FFFh
	jz LABEL_FILE_LOADED
	;如果读取的FAT值为FFF,表示该扇区为该文件的最后一个扇区,
	;因此结束加载,也就是加载成功
	;如果读取的FAT表中的值不是FFF,则表示还有扇区,故保存下一个扇区序号
	push ax
	mov dx, RootDirSectors
	add ax, dx
	add ax, DeltaSectorNo
	add bx, [BPB_BytsPerSec]
	;为call ReadSector的参数做准备,es:bx表示要缓存的地址,
	;ax表示要读取的扇区号=DirEntry中的开始Sector号+根目录占用Sector数目+DeltaSectorNo
	;进入下一次循环。
	jmp LABEL_GOON_LOADING_FILE
LABEL_FILE_LOADED:
	mov dh, 1 ;"Ready."
	call DispStr ;显示字符串

执行结果:

将控制权交给loader

关键代码及分析

`SectorNoOfFAT1 equ 1 ; FAT1 的第一个扇区号 = BPB_RsvdSecCnt
...
;---------------------------------------------------------------------------
; 函数名: GetFATEntry
;---------------------------------------------------------------------------
;作用:
;找到序号为ax的 Sector 在 FAT 中的条目, 结果放在 ax 中
;需要注意的是,中间需要读FAT的扇区到es:bx 处,所以函数一开始保存了es和bx
GetFATEntry:
	push es
	push bx
	push ax
	mov ax, BaseOfLoader; `.
	sub ax, 0100h ;  | 在 BaseOfLoader 后面留出 4K 空间用于存放 FAT
	mov es, ax ; /
	pop ax
	mov byte [bOdd], 0
	mov bx, 3
	mul bx ; dx:ax = ax * 3
	mov bx, 2
	div bx ; dx:ax / 2  ==>  ax <- 商, dx <- 余数
	cmp dx, 0
	jz LABEL_EVEN
	mov byte [bOdd], 1
LABEL_EVEN:;偶数
	; 现在 ax 中是 FATEntry 在 FAT 中的偏移量,下面来
	; 计算 FATEntry 在哪个扇区中(FAT占用不止一个扇区)
	xor dx, dx
	mov bx, [BPB_BytsPerSec]
	div bx ; dx:ax / BPB_BytsPerSec
	   ;  ax <- 商 (FATEntry 所在的扇区相对于 FAT 的扇区号)
	   ;  dx <- 余数 (FATEntry 在扇区内的偏移)。
	push dx
	mov bx, 0 ; bx <- 0 于是, es:bx = (BaseOfLoader - 100):00
	add ax, SectorNoOfFAT1 ; 此句之后的 ax 就是 FATEntry 所在的扇区号
	mov cl, 2
	call ReadSector ; 读取 FATEntry 所在的扇区, 一次读两个, 避免在边界
	   ; 发生错误, 因为一个 FATEntry 可能跨越两个扇区
	pop dx
	add bx, dx
	mov ax, [es:bx]
	cmp byte [bOdd], 1
	jnz LABEL_EVEN_2
	shr ax, 4
LABEL_EVEN_2:
	and ax, 0FFFh
	LABEL_GET_FAT_ENRY_OK:
	pop bx
	pop es
	ret`

加载Loader

LABEL_FILENAME_FOUND: ; 找到 LOADER.BIN 后便来到这里继续
	mov ax, RootDirSectors
	and di, 0FFE0h ; di -> 当前条目的开始
	add di, 01Ah ; di -> 首 Sector
	mov cx, word [es:di]
	push cx ; 保存此 Sector 在 FAT 中的序号
	add cx, ax
	add cx, DeltaSectorNo ; cl <- LOADER.BIN的起始扇区号(0-based)
	mov ax, BaseOfLoader
	mov es, ax ; es <- BaseOfLoader
	mov bx, OffsetOfLoader ; bx <- OffsetOfLoader
	mov ax, cx ; ax <- Sector 号
LABEL_GOON_LOADING_FILE:
	push ax ; `.
	push bx ;  |
	mov ah, 0Eh ;  | 每读一个扇区就在 "Booting  " 后面
	mov al, '.' ;  | 打一个点, 形成这样的效果:
	mov bl, 0Fh ;  | Booting ......
	int 10h    ;  |
	pop bx    ;  |
	pop ax    ; /
	mov cl, 1
	call ReadSector
	pop ax ; 取出此 Sector 在 FAT 中的序号
	call GetFATEntry
	cmp ax, 0FFFh
	jz LABEL_FILE_LOADED
	push ax ; 保存 Sector 在 FAT 中的序号
	mov dx, RootDirSectors
	add ax, dx
	add ax, DeltaSectorNo
	add bx, [BPB_BytsPerSec]
	jmp LABEL_GOON_LOADING_FILE
LABEL_FILE_LOADED:

跳入Loader

jmp BaseOfLoader:OffsetOfLoader ; 这一句正式跳转到已加载到内
; 存中的 LOADER.BIN 的开始处,
; 开始执行 LOADER.BIN 的代码。
; Boot Sector 的使命到此结束。

清屏并显示一个字符串

; 清屏
mov ax, 0600h ; AH = 6,  AL = 0h
mov bx, 0700h ; 黑底白字(BL = 07h)
mov cx, 0 ; 左上角: (0, 0)
mov dx, 0184fh ; 右下角: (80, 50)
int 10h ; int 10h
mov dh, 0 ; "Booting  "
call DispStr ; 显示字符串

跳入Loader之前显示字符串

ov dh, 1 ; "Ready."
call DispStr ; 显示字符串

编译调试:

nasm boot.asm -o boot.bin
nasm 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

结果:


文章作者: Aiaa
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Aiaa !
  目录