走进保护模式
引导扇区突破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
结果: