译自:http://www.brokenthorn.com/Resources/OSDev5.html
第5 章:引导加载器3
by Mike, 2009
本系列文章旨在向您展示并说明如何从头开发一个操作系统。
请注意:本章计划在近期更新以修正错误并提供更多相关信息。
介绍
欢迎!
在前一章里,我们了解了处理器的不同模式,以及一些BIOS 中断。我们也了解了在实模式中段:偏移的寻址方式,并深入解释了实模式。我们也扩展了我们的引导加载器,我们增加了谜一样的OEM 参数块,并增加了打印字符串的功能。
在本章里,我们会看看不同的“环”,它表明了应用程序和系统程序的不同。
我们也会看到一段引导和多段引导,以及他们的优缺点。
最后,我们了解一下BIOS INT 0x13 , OEM 参数块, 及读、加载、执行一个程序。 这个程序将是我们的第二段引导加载器 。我们的第二段引导加载器 将设置32 位环境,并为C 内核 的加载做好准备。
准备好了吗?
汇编语言的环
在汇编语言中,你可能听说过“环0 程序”,“这是一个环3 程序”的说法。在操作系统开发中理解不同的环(是什么)是有用的。
环——理论
什么是环呢?在汇编语言中,环是系统中保护与控制的层次。有4 个环:环0 ,环1 ,环2 ,环3 。
环0 对系统所有部分有绝对控制权,而环3 只有很少的控制权。软件的环值越小,控制权越大(保护越少)。
环不只只是一个概念——它是处理器的体系结构。
当计算机启动,当引导加载器运行时,处理器工作在环0 ,大多数的应用程序,比如DOS 应用程序,运行在环3 。而操作系统工作在环0 ,比一般的应用程序有更大的控制权。
切换环
因为环是处理器体系结构的一部分,当处理器状态改变时就可能发生环的切换,在以下情况下,它可能改变
我们在后面会讨论异常处理及 SYSCALL 和 SYSENTER 指令。
多段引导加载器
单段引导加载器
引导加载器、引导扇,只能有512 字节。如果引导加载器比512 字节少,它直接执行内核的话,它就是一个单段引导加载器
问题是它的大小。512 字节太小了,很难在一个16 位的引导加载器中设置、加载并执行一个32 位的内核。这还不包含错误控制代码。这些代码要包括: GDT, IDT, A20, PMode, 查找并加载 32 位 kernel, 执行 kernel, 和错误控制 。在512 字节中放下全部的这些东西,不大可能。所以单段引导加载器必须加载并执行一个16 位的内核。
因此大多数的引导加载器都是多段引导加载器。
多段引导加载器
一个多段引导加载器包括一个512 字节的引导加载器(单段引导加载器),这个加载器仅仅加载并执行另一个引导加载器——第二段引导加载器。第二段引导加载器往往是16 位的,但包含更多的代码。它能够加载并执行一个32 位的内核。
这是因为引导加载器的512 字节限制。当引导加载器加载了第二段引导加载器所需的全部扇区后,第二段引导加载器就没有了大小的限制。这样他更容易完成加载内核的工作。
我们将使用二段(2 Stage )引导加载器。
从磁盘加载扇区
因为引导加载器被限制到512 字节,不可能做太多的工作。正如前一节中所说,我们使用二段引导加载器,也就是说,引导加载器加载并执行我们的第二段程序——内核加载器。
如果你想,可以在第二段加载器中放置一个“选择操作系统”和“高级选项”的菜单 J ,做吧,我知道你想法要一个
BIOS INT 0x13 的 0 号功能 – 复位软盘驱动器
BIOS 0x13 中的用于磁盘访问。你可以使用INT 0x13 的0 号功能复位软盘驱动器。什么意思?无论软盘控制器在读什么位置,它会立即返回磁盘的第一个扇区。
INT 0x13/AH=0x0 – 磁盘:复位软盘驱动器
AH = 0x0
DL = 待复位驱动器
返回值:
AH = 状态码
CF ( 进位标志) 成功为0 ,失败为1
这是一个完整的例子。它重设软盘驱动器,如果出错会再试一次:
.Reset:
mov ah, 0 ; 复位磁盘
mov dl, 0 ; drive 0 是软驱
int 0x13 ; BIOS 调用
jc .Reset ; 如果进位标志 (CF) 为1 ,表示有错误发生,再试一次。
为什么这个中断很重要?在读扇区之前,我们要保证从0 扇区开始。我们不知道软盘控制从哪读,这不好,这使得每次启动都不一样。复位磁盘到0 扇区会使我们每次从同一个扇区开始。
BIOS INT 0x13 的0x02 号功能 – 读扇区
INT 0x13/AH=0x02 – 磁盘 : 往内存中读扇区
AH = 0x02
AL = 要读入的扇区数
CH = 柱面号的低8 位
CL = 扇区号(Bits 0-5). Bits 6-7 只对硬盘有效
DH = 磁头号
DL = 驱动器号 ( 对于硬盘第7 位为1)
ES:BX = 保存读到扇区的缓冲区
返回值:
AH = 状态码
AL = 读到的扇区数
CF = 失败为1 ,成功为0
有不少要考虑的,一些很简单,另一些要详加说明。
CH = 柱面号的低8 位
什么是柱面(Cylinder ) ? 柱面是(具有相同半径的)一组磁道,为了理解它,看下图:
看看上面的图:
这对我们有什么用?柱面数表示单个磁盘的磁道数,对于软盘,它表示要读的磁道 。
每磁道18 个扇区,在软盘上有63 个磁道。
如果柱面号比63 大,软盘控制器会发送一个异常,因为扇区不存在。因为不存在错误控制代码,CPU 会产生另一个异常,最终导致一个三重错误。
CL = 扇区号(Bits 0-5). Bits 6-7 只对硬盘有效
读取的第一个扇区号。要记住:每磁道只有18 个是扇区,该值只能在0 到17 之间,否则就增大当前磁道的值,以确保扇区号设置为你需要读取的那个扇区。
如果该值大于18 ,软盘控制器会因为扇区不存在而产生一个异常。由于没有错误控制,CUP 会产生另外一个错误异常,这最终导致一个三重错误。
DH = 磁头号
记住对于某些软盘有两个磁头,或面。对于他们磁头0 是正面,磁头1 是反面。所以我们从磁头0 开始读 。
如果该值大于2 ,软盘控制器会因为扇区不存在而产生一个异常。由于没有错误控制,CUP 会产生另外一个错误异常,这最终导致一个三重错误。
DL = 驱动器号 ( 对于硬盘第7 位为1)
ES:BX = 保存读到扇区的缓冲区
什么是驱动器号呢?它是一个代表驱动器是数字。驱动器号0 总用于软驱 。驱动器号1 常由于 5-1/4" 软盘驱动器。
因为我们的代码在软盘上,我们要从软驱读数据,所以要读的驱动器号为0 。
ES:BX 存储读取目标地址的段: 偏移,注意,基地址表示起始地址。
都知道了,我们来读一个扇区。
读取并执行一个扇区
从磁盘读一个扇区,首先复位驱动器,就像刚刚见到的那样:
.Reset:
mov ah, 0 ; reset floppy disk function
mov dl, 0 ; drive 0 is floppy drive
int 0x13 ; call BIOS
jc .Reset ; If Carry Flag (CF) is set, there was an error. Try resetting again
mov ax, 0x1000 ; we are going to read sector to into address 0x1000:0
mov es, ax
xor bx, bx
.Read:
mov ah, 0x02 ; function 2
mov al, 1 ; read 1 sector
mov ch, 1 ; we are reading the second sector past us, so its still on track 1
mov cl, 2 ; sector to read (The second sector)
mov dh, 0 ; head number
mov dl, 0 ; drive number. Remember Drive 0 is floppy drive.
int 0x13 ; call BIOS - Read the sector
jc .Read ; Error, so try again
jmp 0x1000:0x0 ; jump to execute the sector!
注意:如果在读取扇区时发生了错误,并且你还要跳到那里去执行。无论读取是否成功,CPU 都会执行指令。这往往意味着CPU 要么执行一些非法的或是未知的指令,超出内存之外,这些都会导致三重错误。
上面的代码读取并执行一原始扇区,这对我们来说没有意义。首先,现在我们对PartCopy 的配置只能复制512 字节数据, 那么我们在哪里以及怎么创建一个原始扇区呢?
另外,我们也不能通过“文件名”来代表这个扇区,因为它不存在,它只是一个原始扇区。
最后,我们现在的引导加载器设置成了FAT12 文件系统。Windows 会试图从扇区2 和扇区3 读取一张表( 文件分配表File Allocation Tables ) 。但是对于一个原始扇区,这张表是不存在的,这时Windows 会得到一个垃圾值(如果这是那张表的话)。结果呢,当我们使用Windows 读软盘时,会发现文件或文件夹有一个损坏的文件名和巨大的尺寸(你将在1.44MB 的软盘上有一个2.5GB 的文件吗?我见过 J )。
当然,我们也需要这样读取一个扇区 。在我们读取之前,我们要确定文件在磁盘上起始扇区,扇区数、基地址 等等。这是从磁盘上读取文件的基础。
我们接下来看看这个。
FAT12 文件系统导航
OEM 参数块 – 详细
在前面的文章中我们在代码里重复了一大段难看的表,是什么呢?
bpbBytesPerSector: DW 512
bpbSectorsPerCluster: DB 1
bpbReservedSectors: DW 1
bpbNumberOfFATs: DB 2
bpbRootEntries: DW 224
bpbTotalSectors: DW 2880
bpbMedia: DB 0xF0
bpbSectorsPerFAT: DW 9
bpbSectorsPerTrack: DW 18
bpbHeadsPerCylinder: DW 2
bpbHiddenSectors: DD 0
bpbTotalSectorsBig: DD 0
bsDriveNumber: DB 0
bsUnused: DB 0
bsExtBootSignature: DB 0x29
bsSerialNumber: DD 0xa0a1a2a3
bsVolumeLabel: DB "MOS FLOPPY "
bsFileSystem: DB "FAT12 "
大多数很简单,让我们详细发现一下:
bpbBytesPerSector: DW 512
bpbSectorsPerCluster: DB 1
bpbBytesPerSector 指示每扇区的字节数。必须是2 的幂。对于通常的软盘,它是512 字节。
bpbSectorsPerCluster 指示每一簇 有多少个扇,这里我们希望每簇有扇。
bpbReservedSectors: DW 1
bpbNumberOfFATs: DB 2
保留扇 是指不包括在FAT12 中是扇区数,比如,某个扇区不包含再根目录 里。这里,保存有引导加载器的引导扇不包括在根目录中,所以bpbReservedSectors 是 1 。
这也表示保留是扇区(我们的引导加载器)不包含在文件分配表中。
bpbNumberOfFATs 表示文件分配表的数目。FAT12 文件系统总有两个FAT 。
通常,你需要创建文件分配表,但是使用VFD , 我们可使用Windows/VFD 通过格式化来创建这张表。
注意:在创建、删除文件或文件夹时Windows/VFD 也会修改这些表。
bpbRootEntries: DW 224
bpbTotalSectors: DW 2880
对于软盘,在根目录中最多有224 个文件夹。同样,记住,一张软盘有2,880 个扇区。
bpbMedia: DB 0xF0
bpbSectorsPerFAT: DW 9
媒体描述字节 (bpbMedia) 包括磁盘的一些信息。这是该字节的位模式:
0xF0 = 11110000 (二进制)。这表示:这是一个单面的,每FAT9 扇区,80 个磁道,可移除的磁盘 。看看bpbSectorsPerFAT 我们会发现它确实是9 。
bpbSectorsPerTrack: DW 18
bpbHeadsPerCylinder: DW 2
前一章中说:每磁道18 个扇区。
bpbHeadsPerCylinder 表示每柱面有两个磁头
bpbHiddenSectors: DD 0
这表示物理磁盘或卷开始之前有多少个扇。
bpbTotalSectorsBig: DD 0
bsDriveNumber: DB 0
记得软盘驱动器号是0 吗?
bsUnused: DB 0
bsExtBootSignature: DB 0x29
引导标志表示BIOS 参数块(BIOS Parameter Block (OEM 表) )的版本,它的值可以是:
我们用0x29 ,即我们使用的版本。
bsSerialNumber: DD 0xa0a1a2a3
bsVolumeLabel: DB "MOS FLOPPY "
bsFileSystem: DB "FAT12 "
序列号被格式化它的工具赋值,不同的软盘有不同的序列号,没有哪两序列号是相同的。
Microsoft, PC, 和 DR-DOS 产生的序列号是基于时间和日期的:
Low 16 bits = ((seconds + month) << 8) + (hundredths + day_of_month)
High 16 bits = (hours << 8) + minutes + year
因为序列号会被重写,我们可以放置任何我们想要的值,这没有关系。
卷标,是磁盘的标签,有些操作系统在这里显示它的名字。注意:这个字符串必须11 字节,不能长,也不能短 。
文件系统字符串具有相同的目的,没什么特别 。注意:这个字符串必须8 字节,不能长,也不能短 。
演示
Wow, 好多内容, huh? 下面是我为这一章开发的引导加载器,它把所有的东西都放在了一起。
请注意:这个演示不会向它看起来那么工作。 它仅仅用于演示目的,并且在现阶段,它不可构建,我计划在本系列的下一次修订中使其成为一个可构建的程序 。
;*********************************************
; Boot1.asm
; - A Simple Bootloader
;
; Operating Systems Development Tutorial
;*********************************************
bits 16 ; 我们处在16位模式
org 0x7c00 ; 我们已被BIOS加载到了0x7C00
start: jmp loader ; 跳过 OEM块
;*************************************************;
; OEM 参数块 / BIOS 参数块
;*************************************************;
TIMES 0Bh-$+start DB 0
bpbBytesPerSector: DW 512
bpbSectorsPerCluster: DB 1
bpbReservedSectors: DW 1
bpbNumberOfFATs: DB 2
bpbRootEntries: DW 224
bpbTotalSectors: DW 2880
bpbMedia: DB 0xF0
bpbSectorsPerFAT: DW 9
bpbSectorsPerTrack: DW 18
bpbHeadsPerCylinder: DW 2
bpbHiddenSectors: DD 0
bpbTotalSectorsBig: DD 0
bsDriveNumber: DB 0
bsUnused: DB 0
bsExtBootSignature: DB 0x29
bsSerialNumber: DD 0xa0a1a2a3
bsVolumeLabel: DB "MOS FLOPPY "
bsFileSystem: DB "FAT12 "
;***************************************
; 打印字符串
; DS=>SI: 0终止的字符串
;***************************************
Print:
lodsb ; 从SI加载下一个字符到AL
or al, al ; AL=0?
jz PrintDone ; 是,0终止,跳出
mov ah, 0eh ; 不是,打印字符
int 10h
jmp Print ; 重复,直到到达结尾
PrintDone:
ret ; 完成返回
;*************************************************;
; 引导加载器入口点
;*************************************************;
loader:
.Reset:
mov ah, 0 ; 重设软盘驱动器
mov dl, 0 ; drive 0 是软盘驱动器
int 0x13 ; BIOS调用
jc .Reset ; 如果进位标志 (CF)为1,则有错误发生,再试一次
mov ax, 0x1000 ; 我们准备将扇区读入到地址0x1000:0处
mov es, ax
xor bx, bx
mov ah, 0x02 ; 读扇区
mov al, 1 ; 读1个扇区
mov ch, 1 ; 我们读第2个扇区,在1磁道
mov cl, 2 ; 要读的扇区 (第2扇区)
mov dh, 0 ; 磁头号
mov dl, 0 ; 驱动器号,0是软驱
int 0x13 ; 调用BIOS -读扇区
jmp 0x1000:0x0 ; 跳转以执行扇区!
times 510 - ($-$$) db 0 ; 我们需要512字节,用0填充
dw 0xAA55 ; 引导标志
;第1扇区结束,第2扇区开始 ---------------------------------
org 0x1000 ; 这个扇区会被引导加载器加载到0x1000:0
cli ; 仅仅使系统停机
hlt
总结
我们很详细的了解了如何读取磁盘,及BIOS 参数块(BPB) 的内容。我们甚至开发了一个将他们组合起来的例子。
我们也了解了汇编语言的不同环,并知道操作系统在环0 ,这不同于大多数程序,这运行我们执行一些应用程序不能执行的特权指令。
现在,我们了解了所有查找和加载我们的第二段加载器的全部知识!我们会在下一章中,学习FAT12 的全部,并加载我们的第二段加载器!我等不及了! J 下次见。