chuganghong

本篇目标

  1. 介绍引导扇区。
  2. 介绍软盘结构(FAT12)。
  3. 用汇编代码把加载器读取到内存中。
  4. 用汇编代码把内核加载器读取到内存中。

简略流程

计算机启动的简略流程如下:

BIOS对应的中文术语是“基本输入输出系统”。计算机启动时,首先运行的便是BIOS

BIOS是计算机厂商预置在计算机硬件中的一种软件,它会完成一些操作。我们只需知道,它会从内存地址0x7c00处读取引导扇区,就足够了。引导扇区的作用是从软盘中读取加载器。

我们把”引导扇区“叫做boot,把引导扇区的源码文件命名为boot.asmboot恰好占用一个扇区,因此,boot所在的扇区被称为“引导扇区”。我们就把“加载器”叫做loader吧,把加载器的源码文件命名为loader.asm

我们将使用nasm编写bootloader

软盘

软盘和硬盘一样,是一种存储介质,但目前已经很少使用。我们将使用bochs创建虚拟软盘。

软盘使用FAT12文件系统。我们写好loader后,会把它存储到软盘中。

从软盘的第一个字节开始存储还是从0x7c00开始存储?

先介绍一下软盘的数据结构分布图。

我们使用的软盘是1.44M软盘。这种软盘有80个磁道,每个磁道有18个扇区。软盘有两个盘面,因此这种软盘的容量是:

$软盘容量 = 80 * 18 * 512 * 2 / 512 = 2879个扇区≈1.44M$。

int 13h

我们使用BIOS的中断int 13h从软盘中读取数据到内存中。先看下图了解一下中断int 13h的用法。

什么是BIOS中断?我们不必纠缠这个概念,先从我们熟悉的高级语言的角度理解int 13h

int 13h理解成一个函数,把这个函数命名为ReadSectorFromFloppy

这个函数的声明是:void ReadSectorFromFloppy(int ah, int al, int ch, int cl, int dh, int dl, char *dest)。除最后一个参数char *dest外,这个函数的参数对应上图中的同名寄存器。char *dest对应上图中的es:bx

ReadSectorFromFloppy的作用能简化为:把数据从src复制到dest指定的内存地址处。只不过,src不是通过一个参数传递给函数,而是通过一系列参数传递给函数。再说得明确一些,一系列参数联合起来告诉了函数src是多少。

怎么调用ReadSectorFromFloppy?很简单,按照函数声明传递参数给它就行。对int 13h的使用也是如此,将每个寄存器需要的值填入对应的寄存器,然后,使用int 13h。使用int 13h的伪代码如下:

ReadSector:
		mov ah, 02h
		mov al, 要读的扇区数
		mov ch, 磁道号
		mov cl, 起始扇区号
		mov dh, 磁头号
		mov dl, 驱动器号(0表示A盘)
		mov bx, 目标数据比如loader在内存中的位置
		
		int 13h

我们在后续的开发过程中,还会多次遇到对一些端口的读写操作。和这里的BIOS中断的类似,都能用高级语言中的函数来理解。没什么神秘的,传递一些参数,然后执行某种操作,从某个指定的地址获取数据。仅此而已。

上面说得轻描淡写,大家可能会以为我们只要三四分钟就能从软盘中读取loader了。真这么顺利吗?让我们来试试。

  1. ah,只需往这个寄存器中填充02h
  2. al,每次读取一个扇区,往al中填充01h
  3. ch,磁道号是多少?未知。cldhdlbx中应该填充什么值?全是未知。
  4. 再看一次上面的说明,dl中应该填充0hbx的值也好确定。

除了四个参数未知,还需要知道调用ReadSector几次才能读完全部loader数据。让我们带着这些疑问去多了解一下软盘。

数据分布

一张1.44M的软盘中存储的数据的结构如下图所示。

补充说明一下这张图:

  1. 第0个扇区是引导扇区。
  2. 第1个扇区到第18个扇区是FAT区域。FAT区域中存储两个完全相同的FAT表,分别是FAT1和FAT2。它们互为备份。
  3. 从第19个扇区开始,存储根目录。根目录的占用的扇区数量是多少,取决于在软盘中存储多少个文件。
  4. 根目录区域之后的所有扇区都存储数据区。数据区的初始扇区号、一共占用多少个扇区,都是未知数。

猜猜看,loader存储在软盘数据结构的哪个区域?

显而易见,loader被存储在数据区。

补充说明一点。每个扇区只会存储一个文件的数据,绝对不会存储两个文件的数据。例如,文件A的大小是510字节,从扇区号为N的扇区的最开始那个字节存储文件A。N号扇区存储完A文件后,还剩下2个字节。这个时候,如果往软盘中存储文件B,文件B的大小是2个字节,文件B也不会使用N号扇区的剩下的2个字节来存储,而是会重新使用一个完全没有使用过的扇区来存储文件B。软盘、硬盘都是如此。

继续回到我们的主题。

根目录

查询根目录

根目录的大小由软盘能存储的最大文件数量决定。但是,根目录的大小一定是整数个扇区。

根目录由若干个根目录项组成。根目录项是一段32个bit的存储空间。在根目录项中,最有用的是"文件名"和“文件名对应的文件在数据区中的第一个扇区的扇区号”。

先看一段伪代码。它非常清楚地说明了根目录的作用:根据文件名找到文件对应的根目录项,从目标根目录项中找到文件在数据区的第一个扇区的扇区号。

int get_1st_sector_by_filename(filename){
  		start_address;	// 根目录的初始地址
  		count;	// 根目录项的数量
  		for(i = 0; i < count; i++){
        	if(start_address.文件名 == filename){
            	return start_address.文件在数据区的第一个扇区的扇区号
          }
        	start_address += 32;
      }
  		return -1;		// 不存在文件名是filename的文件
}

在后面,我们会使用nasm实现和伪代码思路相同的汇编函数。

根目录项结构

如果用C语言为根目录项建立一个struct,将会是下面这样的。

struct root_directory_entry{
  	char[11] DIR_Name;
  	char	DIR_Attr;
  	char[10] DIR_Reserved;
  	short	DIR_WrtTime;
  	short	DIR_WrtDate;
  	short DIR_FstClus;
  	int DIR_FileSize;
};

FAT表

单链表

先说结论:一个文件对应的所有FAT表项构成一个单链表。

什么是FAT表项?它们怎么构成一个单链表?请继续往下看。

从根目录中找到目标文件在数据区的第一个扇区的扇区号,就知道从哪个扇区读取数据了。

可是,根目录只提供了目标文件的第一个扇区的扇区号。如果目标文件需要两个以上扇区存储呢?如何知道第二个扇区、第三个扇区、第N个扇区的扇区号呢?FAT表会提供这些信息。

回忆一下软盘的数据分布图,图中有FAT1FAT2,它们都是FAT表,我们只需从一个FAT表中获取数据,就选择FAT1吧。

FAT1的大小是9个扇区,512*9个字节。每12个bit构成一个FAT表项,FAT表项的值有两重含义:

  1. 下一个FAT表项的编号。
  2. 文件的下一个扇区的扇区号。

举例说明。软盘中存储着文件名为CG的文件。CG的大小是514个字节。从根目录中查询到CG在数据区的第一个扇区的扇区号是4。

4除了是扇区号,还是FAT表项的编号。注意这句话,非常重要。

FAT1中找到编号为4的FAT表项,这个FAT表项的值是5,那么,5既是下一个FAT表项的编号,又是下一个扇区号。也就是说,CG对应的FAT表项是第4个FAT表项、第5个FAT表项;在数据区占用的扇区是第4个扇区、第5个扇区。

根据当前FAT表项找到下一个FAT表项,这其实就是一个单链表。和单链表一样,FAT表项构成的单链表也有一个尾结点。识别伪结点的方法是判断FAT表项的值是否大于等于FAT_Entry_ValueFAT_Entry_Value是一个具体的值,等我们写代码时再看看它是多少。

FAT表项

本小节的目的是弄清楚:根据FAT表项的编号计算FAT表项的值。

先看一下FAT表项图。

每个FAT表项占用12个bit,计算机读取数据的最小单位是1个字节8个bit。为了每次都读取到完整的FAT表项,需要一次读取2个字节16个bit。16个bit只能存储一个FAT表项。怎么存储?只有图2-2的两种情况。

每次读取FAT表项,都会读取两个字节,而这两个字节的低12位和高12位都可能是FAT表项。要想获取FAT表项,首先要读取两个字节,然后要判断FAT表项存储在低12位还是高12位。这对我来说,是一个有点费劲的问题,我会写得详细一些。

用具体例子来寻找判断FAT表项存储位置的方法。FAT表存储在初始地址为$512$字节的存储空间中。

编号 读取地址(字节) 占用(bit) 实际读取(bit) 注释 实际读取(字节)
0 1 8~~19 8~~23 20~~23是其他表项数据,低12位是本表项数据 1~~2
1 2 20~~31 16~~31 16~~19是其他表项数据,高12位是本表项数据 2~~3
2 3 32~~43 32~~47 44~~47是其他表项数据,低12位是本表项数据 3~~4

对上面表格的补充说明:

  1. 单位为bit的列中的数值应该加上基数:$512 * 8$。
  2. 单位为字节的列中的数值应该加上基数:511。
  3. 因版本需要,没有把基数写到表格的列中。

观察表格中的“编号”列和“注释"列,能得到下面的结论:

  1. 当FAT项的编号是奇数时,FAT表项存储在2个字节的高12位。
  2. 当FAT项的编号是偶数时,FAT表项存储在2个字节的低12位。

知道了FAT表项编号N,怎么计算存储FAT表项的存储空间的字节偏移量?计算公式是:$N * 12 / 8$。例如,编号为2的FAT表项的存储空间的初始地址是:$2 * 12/8 = 3 $。

最后,再看一张图。

定位FAT项在软盘中的位置,需要确定两个值:

  1. FAT项在软盘中的扇区偏移量。
  2. FAT项在软盘中的某个扇区中的字节偏移量。

结合上面的示意图来解释。扇区偏移量是N,字节偏移量是M。读取N+1号扇区后,从N+1号扇区的第M字节开始读取两个字节,目标FAT项就存储在这两个字节中。

获取FAT项的值

方法一

现在,可以给出查询FAT表项的值的伪代码了。

int get_fat_entry_value(fat_entry_no){
  	// fat_entry_no是FAT项的编号。
  	remainder = fat_entry_no % 2;
  	// sector_number是要读取的扇区数量
  	sector_number = 2;
  	// sector_offset 是FAT项存储示意图中的N。
  	sector_offset = fat_entry_no * 3 / 2 / 512;
  	// bit_offset 是FAT项存储示意图中的M。
  	bit_offset = fat_entry_no * 12 / 8;
  	// 读取偏移量是sector_offset两个扇区
  	// sectors是FAT项存储示意图中的N+1号扇区和没有画出来的N+2号扇区。
  	sectors = read_sector(sector_offset, sector_number);
    two_byte_value = sector + bit_offset;
    fat_entry_value = remainder == 0 ? (two_byte_value && 0x0FFF) : (two_byte_value && 0xFFF0);
  	return fat_entry_value;
}
方法二

在上面的伪代码中,检查FAT项的字节偏移量是不是整数个字节的方法是根据FAT项的编号识别。除了这种方法,还有第二种方法。这个方法如下所述:

  1. 一个FAT项占用12个bit,也就是1.5个字节,所以FAT项的字节偏移量等于FAT项的编号乘以1.5
  2. 为了避免不同类型的数据之间进行运算,可以采用乘以1.5的等价运算:先乘以3,再除以2。
    1. 商是字节偏移量。
    2. 余数是bit偏移量。根据余数是否为0判断FAT项的字节偏移量是不是整数个字节。

在后面会讲到的GetFATEntry函数中,使用的是第二种方法。理解不了这个函数的代码的时候,记得回头看看这里的讲解。

草稿
  1. 编号为0的FAT表项,读取地址是1,占用(1*8)~~19个bit,实际读取的空间是第823个bit(第1~~2个字节)。~~

  2. 编号为1的FAT表项,读取地址是2,占用20~~31个bit,实际读取的空间是第1631个bit(第2~~3个字节)。~~

  3. 编号为2的FAT表项,读取地址是4,占用32~~43个bit,实际读取的空间是第3247个bit(第4~~5个字节)。~~

  1. 编号为0的FAT表项,读取地址是1,占用(1*8)~~19个bit,实际读取的空间是第1~~2个字节(第8~~23个bit),字节偏移量是$(8/1=1)$个字节。
  2. 编号为1的FAT表项,读取地址是2,占用20~~31个bit,实际读取的空间是第2~~3个字节(第16~~31个bit),字节偏移量是$(20/8=2)$个字节。
  3. 编号为2的FAT表项,读取地址是4,占用32~~43个bit,实际读取的空间是第4~~5个字节(第32~~47个bit),字节偏移量是$(32/8=4)$个字节。

boot

boot要实现的功能是:

  1. 从根目录中找到目标文件在FAT表中的FAT项。
  2. FAT项包含目标文件的数据存储在哪个扇区。
  3. 使用BIOS中断int 13h读取目标扇区的数据。

直接看代码吧。代码比较长,但是不要被吓到,也不要烦躁,我们一起来看看。

代码解读

泛读

; 计算机启动后,会检查有没有存储设备例如软盘、硬盘等。如果有,会选择一种设备例如软盘,
; 从软盘的引导扇区中读取数据并且复制到内存地址为0x7c00的那段内存空间。
; 也就是说,这个指令的作用是,让BIOS把boot存储到内存地址是0x7c00的内存空间中。
; 然后,BIOS执行结束后,会从0x7c00处开始执行。
; 为什么是0x7c00?这涉及到古老的计算机历史。我以为这种知识不重要,不知道也不影响我们继续开发操作系统。
; 因此,不深究这个问题。
org 0x7c00

	; 跳转到LABEL_START为开头的那块代码。
	jmp	LABEL_START
	; 空指令。
	nop

	; 下面是 FAT12 磁盘的头,叫做"BPB"。
	; 必须有这段指令,BIOS才会把存储设备中的这个扇区识别为引导扇区。
	; 也不必深究,我们写操作系统时,照搬这段即可。
  BS_OEMName      DB 'YOUR--OS'   ; 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 'YOUR--OS.02'; 卷标, 必须 11 个字节
  BS_FileSysType  DB 'FAT12   '   ; 文件系统类型, 必须 8个字节

LABEL_START:
	; many code
	

; 引导器最多只有510个字节,如果存储完实现功能的指令后还不够510个字节,就用0填充剩余的存储空间。
times	510 - ($ - $$)	db	0
; 0xAA55是一个魔数。BIOS读取存储设备的第一个扇区后,会检查扇区的最后两个字节是不是`0xAA55`。
; 如果不是,BIOS认为这个扇区不是引导扇区;如果是,BIOS认为这个扇区是引导扇区。
dw	0xAA55

这段代码中的FAT12的磁盘头和扇区的最后两个字节0xAA55一起构成了引导扇区的标志。没有这两个标志,BIOS就认为这个扇区不是引导扇区。

BPB:BIOS参数块(BIOS Parameter Block)。

ReadSector

泛读

先回顾一下前面给出的伪代码。

ReadSector:
		mov ah, 02h
		mov al, 要读的扇区数
		mov ch, 磁道号
		mov cl, 起始扇区号
		mov dh, 磁头号
		mov dl, 驱动器号(0表示A盘)
		mov bx, 目标数据比如loader在内存中的位置
		
		int 13h

下面的代码中:

  1. ah的值通过mov ah, 02h ; 读软盘设置成02h

  2. al的值通过两条语句设置。

    1. mov byte [bp-2], cl
      mov al, [bp-2]
      
    2. 也就是说,al中的值是cl中的值。cl的值应该在调用ReadSector前设置了值。

  3. ch的值通过下面的语句设置。

    1. mov ch, al
      shr ch, 1	; ch 是柱面号
      
    2. 不能一眼看出这两条语句的含义。先搁置。

  4. cl的值通过下面的语句设置。

    1. inc ah
      mov cl, ah
      
    2. 也不能一眼看出这两条语句的含义。先搁置。

  5. dh的值通过下面的语句设置。

    1. mov dh, al
      and dh, 1	; dh 是磁头号
      
    2. 也不能一眼看出这两条语句的含义。先搁置。

  6. dl的值通过mov dl, 0 ; 驱动器号,0表示A盘

  7. bx有关系的语句是:

    1. push bx
      pop bx
      
    2. bx的值应该是在调用ReadSector前设置的。

难点

经过上面的仔细分析,发现了三个疑问,分别是clchdh的值。它们分别是:起始扇区号、柱面号和磁头号。先给出这三个值的计算公式:

理解这个计算公式前,复习一次1.44M式软盘的知识。

  1. 软盘有80个磁道。每个磁道有18个扇区。
  2. 软盘有两个盘面,有两个磁头,每个盘面有一个磁头。盘面号和磁头号分别是:0号、1号。
  3. 两个盘面上的对应的一对磁道组成一个柱面。每个柱面包含两个磁道。柱面号的初始值是0。
  4. 每个磁道的扇区的扇区号的初始值是1,不是0。

再理解公式。

  1. $磁道号 = 扇区号/每磁道扇区数$
  2. $初始扇区号 = 扇区号sector_no \mod\ 每磁道扇区数 + 1$
    1. 扇区号除以每磁道扇区数的余数是填充若干个磁道后剩余的扇区数量M。
    2. 换句话说,这些扇区是位于第N磁道的前M个扇区,扇区号为sector_no的扇区是第N磁道的第M个扇区。
    3. 第M个扇区在第N磁道的扇区号是多少?
    4. 要回答这个问题,先补充两个知识点:sector_no是扇区在软盘中的扇区号,初始值是0;M是扇区在磁道中的扇区号,初始值是1。
    5. 因此,在磁道中,偏移量是0个扇区的扇区的扇区号是(1 + 0);偏移量是1个扇区的扇区的扇区号是(1 + 1);偏移量是2个扇区的扇区的扇区号是(1 + 2);由此归纳出,偏移量是M个扇区的扇区的扇区号是(1+M)。
    6. 这就是公式中起始扇区号 = R + 1的由来。
  3. 柱面号 = 磁道号 / 2。很容易理解。每个柱面有两个磁道,0号磁道在0号柱面,1号磁道在0号柱面;2号磁道在1号柱面,3号磁道在1号柱面。
  4. 磁头号 = 磁道号 & 1
    1. 磁道号是奇数,这个磁道的由0号磁头处理;磁道号是偶数,这个磁道由1号磁头处理。
    2. 奇数的最低位bit的值总是1,偶数的最低位bit的值总是0。因此,只需判断磁道号的最低位bit是0还是1就能判断出这个磁道号是奇数还是偶数。
再读代码

我初次看这块知识时花了不少时间,所以,我要再重复写几句。

使用ReadSector读取数据时,直接提供的参数只有从FAT12的根目录和FAT中查询出来的扇区号sector_no。这个扇区号的初始值是0。

ReadSector函数中把sector_no代入上面的公式计算出来的在磁道中的扇区号的初始值是1。

彻底扫清所有障碍之后,让我们再次直面开始的那个难题吧:起始扇区号、柱面号和磁头号是多少?

  1. sector_no存储在ax中。
  2. SectorNumberOfTrack的值是18。在文末的全部代码中将会看到为这个变量赋值的语句。
  3. div bl ; 商在al中,余数在ah中
    1. nasmdiv指令,进行除法计算,被除数存储在ax中,除数存储在bl中,商存储在al中,余数存储在ah中。
    2. 根据公式,$柱面号(ch) = 扇区号(ax)/每个磁道包含的扇区的数量(SectorNumberOfTrack)/2$。
    3. $在磁道中的起始扇区号(cl) = 扇区号(ax) % 每个磁道包含的扇区的数量(SectorNumberOfTrack) + 1$。
    4. $磁头号(dh) = 扇区号(ax) / 每个磁道包含的扇区的数量(SectorNumberOfTrack) & 1$。

结合上面冗长的分析看下面加了注释的代码,应该不会有太多疑问。

; 读取扇区
ReadSector:
	push ax
	push bp
	push bx
	mov bp, sp
	sub esp, 2
	mov byte [bp-2], cl
	
	; ax 存储在软盘中的扇区号
	mov bl, SectorNumberOfTrack	; 一个磁道包含的扇区数
	div bl	; 商在al中,余数在ah中
	mov ch, al
	shr ch, 1	; ch 是柱面号
	mov dh, al
	and dh, 1	; dh 是磁头号
	mov dl, 0	; 驱动器号,0表示A盘
	inc ah
	mov cl, ah
	mov al, [bp-2]
	add esp, 2
	mov ah, 02h	; 读软盘
	pop bx
	
	int 13h

	pop bp
	pop ax
	ret

GetFATEntry

汇编指令div和mul

[]()

代码

FATEntryIsInt	equ 0		; FAT项的字节偏移量是不是整数个字节:0,不是;1,是。
BytesOfSector	equ	512	; 每个扇区包含的字节数量
; 根据FAT项的编号获取这个FAT项的值
GetFATEntry:
	; 用FAT项的编号计算出这个FAT项的字节偏移量 start
	; 复位软驱时会修改ax的值,先把它存储到栈中。
	push ax
	; 复位软驱
	mov ah, 00h
	mov dl, 0
	int 13h
	
	; 还原被复位软驱而修改的ax中的值。
	pop ax	
	; 下面的操作,实现 ax * 3 / 2。
	mov dx, 0
	mov bx, 3
	mul bx
	mov bx, 2
	div bx
	; 用FAT项的编号计算出这个FAT项的字节偏移量 end
	; div bx操作会把余数存储在dx中,商存储在ax中。
	; dx是bit偏移量,ax是字节偏移量。
	mov [FATEntryIsInt], dx
	; 用字节偏移量计算出扇区偏移量 start
	mov dx, 0
	; and ax, 0000000011111111b  ; 不知道这句的意图是啥,忘记得太快了!
	; mov dword ax, al ; 错误用法
	; mov cx, [BytesOfSector]
	mov cx, 512
	; div cx操作计算FAT项的扇区偏移量,存储在ax中,dx中存储的是字节偏移量。
	div cx
	; push dx
	add ax, SectorNumberOfFAT1	; ax 是在FAT1区域的偏移。要把它转化为在软盘中的扇区号,需加上FAT1对软盘的偏移量。
	; 用字节偏移量计算出扇区偏移量 end
	
	; 读两个扇区。
	mov cl, 2 
	mov bx, 0
	push es
	; dx的值可能会在call ReadSector改变,所以先存储到栈中。
	push dx
	push ax
	mov ax, BaseOfFATEntry
	; ReadSector把两个扇区的数据读取到BaseOfFATEntry:bx处。
	; bx是什么?bx是0。
	mov es, ax
	pop ax
	; 用扇区偏移量计算出在某柱面某磁道的扇区偏移量,可以直接调用ReadSector
	call ReadSector
	; 恢复dx的值,此时,dx中的值是FAT项读取到的两个扇区中的字节偏移量。
	pop dx
	add bx, dx
	;[es:bx]是FAT项的初始位置,从这个位置开始,复制2个字节到ax中。
	mov ax, [es:bx]
	pop es
	; 根据FAT项偏移量是否占用整数个字节来计算FAT项的值。
	; 若偏移量是整数个字节,ax的低12位是FAT项;反之,ax的高12位是FAT项。
	cmp byte [FATEntryIsInt], 0
	jz FATEntry_Is_Int
	; 获取ax的高12位。
	shr ax, 4	
FATEntry_Is_Int:
	; 获取ax的低12位。
	and ax, 0x0FFF
	ret

结尾

本篇介绍了:

  1. 计算机启动的极简流程。
  2. 1.44M软盘的结构。
  3. boot.的代码解释。

需要结合上一篇文章《写操作系统之搭建开发环境》才知道怎么运行boot中的代码。由于本文篇幅有点长,将在下篇《写操作系统之开发引导器》中讲解运行boot中的代码的方法。

祝一切顺利!

boot代码全文

org 0x7c00

	jmp	LABEL_START
	nop

	; 下面是 FAT12 磁盘的头
  BS_OEMName      DB 'YOUR--OS'   ; 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 'YOUR--OS.02'; 卷标, 必须 11 个字节
  BS_FileSysType  DB 'FAT12   '   ; 文件系统类型, 必须 8个字节

LABEL_START:
	; 0B800h是显存地址,gs存储显存地址。
	mov ax,	0B800h
	mov gs,	ax
	; 把es设置成BaseOfLoader。
	mov ax, BaseOfLoader
	mov es, ax

	; 复位软驱
	mov  ah, 00h
	mov  dl, 0
	int 13h
	; FirstSectorOfRootDirectory的值是19,是根目录在软盘中的扇区号,也是扇区偏移量。
	mov ax,	FirstSectorOfRootDirectory
	mov cl, 1
	
	; OffSetOfLoader是存储loader的内存空间的初始地址。
	mov bx, OffSetOfLoader
	; 读取第19号扇区,存储到内存空间的初始地址是OffSetOfLoader的这片内存中。
	call ReadSector
	; 在根目录中检查3个目录项,这是人为规定,假设根目录中只有3个目录项。
	mov cx, 3
	
	; 执行这条指令后,[es:di]存储的就是根目录的第一个根目录项。文件名位于根目录项的最开始的11个字节。
	mov di, OffSetOfLoader
; 遍历根目录
SEARCH_FILE_IN_ROOT_DIRECTORY:
	cmp cx, 0
	; 没有找到目标文件,跳转到FILE_NOT_FOUND开头的那段代码。
	jz FILE_NOT_FOUND
	push cx
	; LoaderBinFileName是目标文件即loader的文件名的初始地址。
	mov si, LoaderBinFileName
	; LoaderBinFileNameLength是目标文件的文件名的长度。
	mov cx, LoaderBinFileNameLength
	mov dx, 0
	mov bx, (80 * 18 + 40) * 2
; 开始检查当前目录项中存储的文件名是否和目标文件的文件名相同。方法是:检测每个字符是否相同。
COMPARE_FILENAME:
	; 从[es:si]中读一个字符复制到al中。
	lodsb
	;从根目录项的文件名中取一个字符和从LoaderBinFileName中获取的对应位置的字符进行比较。
	;当二者不相等时,跳转到FILENAME_DIFFIERENT代码块执行。
	cmp al, byte [es:di]
	jnz FILENAME_DIFFIERENT
	; cx是文件名的长度。
	; 比较文件名函数结束的条件有两个:一个是对比完了所有字符;一个是发现了不相同的字符。
	dec cx
	; 将di加1,逐个对比LoaderBinFileName和根目录项中的文件名。
	; 将dx加1,统计已经比较过的字符的个数。
	inc di
	inc dx

	; 当已经统计完了所有字符,并且所有字符都相同时,说明当前根目录项就是要目标文件的根目录项,跳转到FILE_FOUND块执行。
	cmp dx, LoaderBinFileNameLength
	jz FILE_FOUND
	; 继续对比下一个字符。
	jmp COMPARE_FILENAME		
FILENAME_DIFFIERENT:
	mov al, 'D'
  mov ah, 0Ah
  mov [gs:(80 * 24 + 40) *2], ax


	pop cx		; 在循环中,cx会自动减少吗?
	cmp cx, 0
	dec cx
	jz FILE_NOT_FOUND
	; 低5位设置为0,其余位数保持原状。回到正在遍历的根目录项的初始位置。
	and di, 0xFFE0	
	add di, 32	; 增加一个根目录项的大小
	jmp SEARCH_FILE_IN_ROOT_DIRECTORY
FILE_FOUND:
	mov al, 'S'
	mov ah, 0Ah
	mov [gs:(80 * 24 + 35) *2], ax
	; 修改段地址和偏移量后,获取的第一个簇号错了 
	; 获取文件的第一个簇的簇号
	and di, 0xFFE0  ; 低5位设置为0,其余位数保持原状。回到正在遍历的根目录项的初始位置; 获取文件的第一个簇的簇号
	; 文件的第一个簇号(可以理解为扇区号)在根目录项中的字节偏移量是0x1A。
	add di, 0x1A
	mov si, di
	mov ax, BaseOfLoader
	push ds
	mov ds, ax
	; 把[ds:si]处的数据复制到ax中。也就是说,ax中存储着目标文件的第一个扇区的扇区号,同时也是这个文件的第一个FAT项的编号。
	lodsw
	pop ds	
	push ax
	; 将会把从软盘中读取到的数据复制到[es:bx]开始的内存空间。
	mov bx, OffSetOfLoader
	; 获取到文件的第一个簇号后,开始读取文件
READ_FILE:
	push bx
	
	; 簇号就是FAT项的编号,同时也是文件块在数据区的扇区号。
	; 用簇号计算出目标扇区在软盘中的的扇区号。
	add ax, 19
	add ax, 14
	; 为什么要减去2?因为0号FAT项、1号FAT项不表示记录任何扇区信息,从2号FAT项开始记录数据区的扇区。
	; 第2号FAT项记录数据区的第0号扇区。
	sub ax, 2
		
	; 读取一个扇区的数据 start
	; add ax, SectorNumberOfFAT1
	mov cl, 1
	pop bx	
	call ReadSector
	;;xchg bx, bx
	; 读取一个扇区到[es:bx]后,把下一个扇区读取到[es:bx+512]开始的内存。
  add bx, 512
	; 读取一个扇区的数据 end
	
	;jmp READ_FILE_OVER
		
	; 执行pop后,ax中存储的是目标文件的第一个FAT项的编号。
	; GetFATEntry能根据这个FAT项的编号获取这个FAT项的值,也就是下一个FAT项的编号。
	pop ax
	push bx
	call GetFATEntry
	pop bx
	push ax
	;ax >= 0xFF8时,当前扇区是文件的最后一个扇区。
	cmp ax, 0xFF8
	; 注意了,ax >= 0xFF8 时跳转,使用jc 而不是jz。昨天,一定是在这里弄错了,导致浪费几个小时调试。
	;jz READ_FILE_OVER	
	;jc READ_FILE_OVER	
	jnb READ_FILE_OVER	
	
	jmp READ_FILE
	
FILE_NOT_FOUND:
        mov al, 'N'
        mov ah, 0Ah
        mov [gs:(80 * 24 + 36) *2], ax
	jmp OVER

READ_FILE_OVER:
	
	; 簇号就是FAT项的编号,同时也是文件块在数据区的扇区号。
	; 用簇号计算出目标扇区在软盘中的的扇区号。
	add ax, 19
	add ax, 14
	sub ax, 2

	; 读取一个扇区的数据 start
	mov cl, 1

	mov al, 'O'
	mov ah, 0Ah
	mov [gs:(80 * 24 + 33) * 2], ax
	
	; 跳转到loader执行loader中的指令。
	jmp BaseOfLoader:OffSetOfLoader	
	jmp OVER

OVER:

	jmp $

BootMessage:	db	"Hello,World OS!"
;BootMessageLength:	db	$ - BootMessage
; 长度,需要使用 equ 
BootMessageLength	equ	$ - BootMessage

FirstSectorOfRootDirectory	equ	19
SectorNumberOfTrack	equ	18
SectorNumberOfFAT1	equ	1

LoaderBinFileName:	db	"LOADER  BIN"
LoaderBinFileNameLength	equ	$ - LoaderBinFileName	; 中间两个空格

FATEntryIsInt	equ 0		; FAT项的字节偏移量是不是整数个字节:0,不是;1,是。
BytesOfSector	equ	512	; 每个扇区包含的字节数量
; 根据FAT项的编号获取这个FAT项的值
GetFATEntry:
	; 用FAT项的编号计算出这个FAT项的字节偏移量 start
	; mov cx, 3
	; mul cx
	; mov cx, 2
	;div cx		; 商在al中,余数在ah中	; 
	push ax
	MOV ah, 00h
	mov dl, 0
	int 13h
	
	pop ax	
	mov dx, 0
	mov bx, 3
	mul bx
	mov bx, 2
	div bx
	; 用FAT项的编号计算出这个FAT项的字节偏移量 end
	mov [FATEntryIsInt], dx
	; 用字节偏移量计算出扇区偏移量 start
	mov dx, 0
	; and ax, 0000000011111111b  ; 不知道这句的意图是啥,忘记得太快了!
	; mov dword ax, al ; 错误用法
	; mov cx, [BytesOfSector]
	mov cx, 512
	div cx
	; push dx
	add ax, SectorNumberOfFAT1	; ax 是在FAT1区域的偏移。要把它转化为在软盘中的扇区号,需加上FAT1对软盘的偏移量。
	; mov ah, 00h

	; 用字节偏移量计算出扇区偏移量 end
	mov cl, 2 
	mov bx, 0
	push es
	push dx
	push ax
	mov ax, BaseOfFATEntry
	mov es, ax
	pop ax
	; 用扇区偏移量计算出在某柱面某磁道的扇区偏移量,可以直接调用ReadSector
	call ReadSector
	pop dx
	add bx, dx
	mov ax, [es:bx]
	pop es
	; 根据FAT项偏移量是否占用整数个字节来计算FAT项的值
	cmp byte [FATEntryIsInt], 0
	jz FATEntry_Is_Int
	shr ax, 4	
FATEntry_Is_Int:
	and ax, 0x0FFF
	ret

; 读取扇区
ReadSector:
	push ax
	push bp
	push bx
	mov bp, sp
	sub esp, 2
	mov byte [bp-2], cl
	
	; ax 存储在软盘中的扇区号
	mov bl, SectorNumberOfTrack	; 一个磁道包含的扇区数
	div bl	; 商在al中,余数在ah中
	mov ch, al
	shr ch, 1	; ch 是柱面号
	mov dh, al
	and dh, 1	; dh 是磁头号
	mov dl, 0	; 驱动器号,0表示A盘
	inc ah
	mov cl, ah
	;add cl, 1	; cl 是起始扇区号
	; pop al		; al 是要读的扇区数量
	mov al, [bp-2]
	add esp, 2
	mov ah, 02h	; 读软盘
	pop bx
	
	int 13h

	pop bp
	pop ax
	ret	

BaseOfLoader	equ	0x9000
OffSetOfLoader	equ	0x100
BaseOfFATEntry	equ	0x1000


times	510 - ($ - $$)	db	0
dw	0xAA55

相关文章: