保护模式预备知识--书中boot.asm代码详细注释
一、FAT12软盘格式
软盘格式如图1:
每个扇区是512字节,512B*2880=1.44MB

图 1 软盘(1.44MB,FAT12)
1、引导扇区占512字节,开启后加载这里的512个字节代码,不能把操作系统写在里面,因为太小了。
2、FAT1,FAT2两者都是一样的,各占9个扇区,如下图。之所以从00000200开始是因为引导扇区占了512字节。


由簇号寻找在表中的FAT项,由于第0个和第1个FAT项始终不用,所以最小的簇号为2,簇号为2对应的FAT项为FFF,簇号为3对应的FAT项为008,FAT项的值代表的是文件下一个簇号,但如果值大于或等于0xFF8,则表示当前簇已经是本文件的最后一个簇。如果值为0xFF7,表示它是一个坏簇。
3、根目录区,存放着根目录条目,每个条目占32个字节,如下图:

由每个条目就能知道文件的名字和条目对应的开始簇号。扇区号=簇号-2,根目录区的扇区大小如下图

RootDirSectors=(224*32+512-1)/ 512=14,共224个根目录条目,每个长度是32字节,每个扇区为512字节。
4、数据区
其实起始扇区为19+14+簇号-2
二、int 13h中断

对于1.44MB的软盘来讲,总共有两面(磁头号0和1),每面有80个柱面(0-79),每个柱面有18个扇区。软盘的容量的由来:2×80×18×512=1.44MB,扇区号是这样分配的,0柱面,0磁头是第一个扇区,0柱面,1磁头是第二个扇区,1柱面,0磁头是第三个扇区,1柱面,1磁头是第四个扇区。

三、boot.asm代码详解如下:
根目录读取一个扇区到内存0900h:0100位置,遍历此扇区的16个根目录,看是否有LOADER BIN,如果没找到,再读取一个扇区到内存0900h:0100位置,循环刚才的动作,直到14个扇区全部查找完毕;如果找到了,那么取该目录的开始簇号,①根据开始簇号取得他在数据区的扇区号然后读入内存0900h:0100处;然后根据开始簇号取得它在FAT1中的扇区号,然后读入内存08FF:0000处,一般读两个扇区;根据偏移计算FAT项的值,如果为FFF则结束,如果为008,转到①继续执行。0900:0000-0900:0100为堆栈区域。内存分配图如下:

代码如下:
;%define_BOOT_DEBUG_; 做 Boot Sector 时一定将此行注释掉!将此行打开后用 nasm Boot.asm -o Boot.com 做成一个.COM文件易于调试%ifdef_BOOT_DEBUG_org 0100h; 调试状态, 做成 .COM 文件, 可调试%elseorg 07c00h; Boot 状态, Bios 将把 Boot Sector 加载到 0:7C00 处并开始执行%endif;================================================================================================%ifdef_BOOT_DEBUG_BaseOfStackequ0100h; 调试状态下堆栈基地址(栈底, 从这个位置向低地址生长)%elseBaseOfStackequ07c00h; Boot状态下堆栈基地址(栈底, 从这个位置向低地址生长)%endif;equ为伪指令,编译时候就变成对应的代码,本身不占用空间BaseOfLoaderequ09000h; LOADER.BIN 被加载到的位置 ---- 段地址OffsetOfLoaderequ0100h; LOADER.BIN 被加载到的位置 ---- 偏移地址RootDirSectorsequ14; 根目录占用14个扇区 根据BPB_RootEntCnt计算出来的SectorNoOfRootDirectoryequ19; 根目录的第一个扇区号,每个根目录项占32个字节SectorNoOfFAT1equ1; FAT1 的第一个扇区号 = BPB_RsvdSecCntDeltaSectorNoequ17; DeltaSectorNo = BPB_RsvdSecCnt + (BPB_NumFATs * FATSz) - 2; 文件的开始Sector号 = DirEntry中的开始Sector号 + 根目录占用Sector数目 + DeltaSectorNo;================================================================================================jmp short LABEL_START; Start to boot.nop; 这个 nop 不可少 ;一个磁道有18个扇区,一个扇区有512字节; 下面是 FAT12 磁盘的头BS_OEMNameDB 'ForrestY'; OEM String, 必须 8 个字节BPB_BytsPerSecDW 512; 每扇区字节数 ;重要BPB_SecPerClusDB 1; 每簇多少扇区BPB_RsvdSecCntDW 1; Boot 记录占用多少扇区 ;所以FAT的第一个扇区为1BPB_NumFATsDB 2; 共有多少 FAT 表BPB_RootEntCntDW 224; 根目录文件数最大值 ;计算扇区个数时候用到BPB_TotSec16DW 2880; 逻辑扇区总数BPB_MediaDB 0xF0; 媒体描述符BPB_FATSz16DW 9; 每FAT扇区数BPB_SecPerTrkDW 18; 每磁道扇区数 ;重要BPB_NumHeadsDW 2; 磁头数(面数) ;由扇区求柱面,磁头,扇区时候用到BPB_HiddSecDD 0; 隐藏扇区数BPB_TotSec32DD 0; 如果 wTotalSectorCount 是 0 由这个值记录扇区数BS_DrvNumDB 0; 中断 13 的驱动器号 ;后来赋值给dlBS_Reserved1DB 0; 未使用BS_BootSigDB 29h; 扩展引导标记 (29h)BS_VolIDDD 0; 卷序列号BS_VolLabDB 'OrangeS0.02'; 卷标, 必须 11 个字节BS_FileSysTypeDB 'FAT12 '; 文件系统类型, 必须 8个字节 LABEL_START:movax, cs ;cs=0000hmovds, axmoves, axmovss, axmovsp, BaseOfStack;上面的地方没有代码,可以自由存数据; 清屏movax, 0600h; AH = 6, AL = 0hmovbx, 0700h; 黑底白字(BL = 07h)movcx, 0; 左上角: (0, 0)movdx, 0184fh; 右下角: (80, 50)int10h; int 10hmovdh, 0; "Booting "callDispStr;int 10号暂时不考虑,功能显示字符串,因为有很多种方式xorah, ah; ┓xordl, dl; ┣ 软驱复位int13h; ┛; 下面在 A 盘的根目录寻找 LOADER.BINmovword [wSectorNo], SectorNoOfRootDirectory ;19LABEL_SEARCH_IN_ROOT_DIR_BEGIN:cmpword [wRootDirSizeForLoop], 0; ┓ 14jzLABEL_NO_LOADERBIN; ┣ 判断根目录区是不是已经读完decword [wRootDirSizeForLoop]; ┛ 如果读完表示没有找到 LOADER.BINmovax, BaseOfLoader ;09000hmoves, ax; es <- BaseOfLoadermovbx, OffsetOfLoader; 0100h bx <- OffsetOfLoader于是, es:bx = BaseOfLoader:OffsetOfLoadermovax, [wSectorNo]; ax <- Root Directory 中的某 Sector 号movcl, 1callReadSectormovsi, LoaderFileName; ds:si -> "LOADER BIN"movdi, OffsetOfLoader; es:di -> BaseOfLoader:0100,,正好指向根目录项的文件名属性cldmovdx, 10h ;因为一个扇区最多有512/32=16个根目录LABEL_SEARCH_FOR_LOADERBIN:cmpdx, 0; 循环次数控制,jzLABEL_GOTO_NEXT_SECTOR_IN_ROOT_DIR;如果已经读完这个扇区的所有根目录,就跳到下一个扇区。decdx; 就跳到这个扇区的下一个根目录movcx, 11 ;因为根目录的DIR_Name有11个字节LABEL_CMP_FILENAME:cmpcx, 0jzLABEL_FILENAME_FOUND; 如果比较了 11 个字符都相等, 表示找到 deccxlodsb; ds:si -> alcmpal, byte [es:di]jzLABEL_GO_ONjmpLABEL_DIFFERENT; 只要发现不一样的字符就表明本 DirectoryEntry 不是我们要找的 LOADER.BINLABEL_GO_ON:incdijmpLABEL_CMP_FILENAME;继续循环LABEL_DIFFERENT:anddi, 0FFE0h; di &= E0 为了让它指向本条目开头adddi, 20h; di += 20h 下一个目录条目 20h=32个字节movsi, LoaderFileNamejmpLABEL_SEARCH_FOR_LOADERBIN; LABEL_GOTO_NEXT_SECTOR_IN_ROOT_DIR:addword [wSectorNo], 1jmpLABEL_SEARCH_IN_ROOT_DIR_BEGINLABEL_NO_LOADERBIN:movdh, 2; "No LOADER."callDispStr; 显示字符串%ifdef_BOOT_DEBUG_movax, 4c00h; ┓int21h; ┛没有找到 LOADER.BIN, 回到 DOS%elsejmp$; 没有找到 LOADER.BIN, 死循环在这里%endifLABEL_FILENAME_FOUND:; 找到 LOADER.BIN 后便来到这里继续movax, RootDirSectorsanddi, 0FFE0h; di -> 当前条目的开始adddi, 01Ah; 取得此条目对应的开始簇号的偏移movcx, word [es:di] ;2pushcx; 保存此 Sector 在 FAT 中的序号addcx, ax ;2+14addcx, DeltaSectorNo; 19+14+簇号-2movax, BaseOfLoadermoves, ax; movbx, OffsetOfLoader; movax, cx; ax=34LABEL_GOON_LOADING_FILE:pushax; `.pushbx; |movah, 0Eh; | 每读一个扇区就在 "Booting " 后面moval, '.'; | 打一个点, 形成这样的效果:movbl, 0Fh; | Booting ......int10h; |popbx; |popax; /movcl, 1 ;扇区号为34,读一个扇区,到09000h:0100hcallReadSectorpopax; 取出此 Sector 在 FAT 中的序号callGetFATEntry ;或者FAT项的值cmpax, 0FFFhjzLABEL_FILE_LOADEDpushax; 保存 Sector 在 FAT 中的序号movdx, RootDirSectorsaddax, dx ;ax+14addax, DeltaSectorNo ;ax+14+17addbx, [BPB_BytsPerSec];0100h+200h,又读取一个扇区jmpLABEL_GOON_LOADING_FILELABEL_FILE_LOADED:movdh, 1; "Ready."callDispStr; 显示字符串; *****************************************************************************************************jmpBaseOfLoader:OffsetOfLoader; 这一句正式跳转到已加载到内; 存中的 LOADER.BIN 的开始处,; 开始执行 LOADER.BIN 的代码。; Boot Sector 的使命到此结束; *****************************************************************************************************;============================================================================;变量;----------------------------------------wRootDirSizeForLoop:dwRootDirSectors; 因为要用到这个存储单元,不是单一的用14这个数字,Root Directory 占用的扇区数, 在循环中会递减至零.wSectorNo:dw0; 要读取的扇区号bOdd:db0; 奇数还是偶数;============================================================================;字符串;----------------------------------------LoaderFileName:db"LOADER BIN", 0; LOADER.BIN 之文件名; 为简化代码, 下面每个字符串的长度均为 MessageLengthMessageLengthequ9BootMessage:db"Booting "; 9字节, 不够则用空格补齐. 序号 0Message1:db"Ready. "; 9字节, 不够则用空格补齐. 序号 1Message2:db"No LOADER"; 9字节, 不够则用空格补齐. 序号 2;============================================================================;----------------------------------------; 函数名: DispStr;----------------------------------------; 作用:;显示一个字符串, 函数开始时 dh 中应该是字符串序号(0-based)DispStr:movax, MessageLengthmuldhaddax, BootMessagemovbp, ax; ┓movax, ds; ┣ ES:BP = 串地址moves, ax; ┛movcx, MessageLength; CX = 串长度movax, 01301h; AH = 13, AL = 01hmovbx, 0007h; 页号为0(BH = 0) 黑底白字(BL = 07h)movdl, 0int10h; int 10hret;----------------------------------------; 函数名: ReadSector;----------------------------------------; 作用:;从第 ax 个 Sector 开始, 将 cl 个 Sector 读入 es:bx 中ReadSector:; -----------------------------------; 怎样由扇区号求扇区在磁盘中的位置 (扇区号 -> 柱面号, 起始扇区, 磁头号); -----------------------------------; 设扇区号为 x; ┌ 柱面号 = y >> 1; x ┌ 商 y ┤; -------------- => ┤ └ 磁头号 = y & 1; 每磁道扇区数 │; └ 余 z => 起始扇区号 = z + 1pushbp ;因为要用到bpmovbp, sp ;subesp, 2; 辟出两个字节的堆栈区域保存要读的扇区数: byte [bp-2]movbyte [bp-2], cl ;大家奇怪为什么不直接push呢,因为如果直接push,那么则不能直接取出来数据pushbx; 保存 bx,因为要做除数movbl, [BPB_SecPerTrk]; bl: 除数为18divbl; y 在 al 中, z 在 ah 中incah; z ++movcl, ah; cl <- 起始扇区号movdh, al; dh <- yshral, 1; y >> 1 (其实是 y/BPB_NumHeads, 这里BPB_NumHeads=2)movch, al; ch <- 柱面号anddh, 1; dh & 1 = 磁头号popbx; 恢复 bx; 至此, "柱面号, 起始扇区, 磁头号" 全部得到 ^^^^^^^^^^^^^^^^^^^^^^^^movdl, [BS_DrvNum]; 驱动器号 (0 表示 A 盘).GoOnReading:movah, 2; 读moval, byte [bp-2]; 读 al 个扇区int13hjc.GoOnReading; 如果读取错误 CF 会被置为 1, 这时就不停地读, 直到正确为止addesp, 2 ;由于前面辟出了两个字节,保存要读的扇区数popbpret;----------------------------------------; 函数名: GetFATEntry;----------------------------------------; 作用:;找到序号为 ax 的 Sector 在 FAT 中的条目, 结果放在 ax 中;需要注意的是, 中间需要读 FAT 的扇区到 es:bx 处, 所以函数一开始保存了 es 和 bxGetFATEntry:pushespushbxpushaxmovax, BaseOfLoader; `.subax, 0100h; | 在 BaseOfLoader 后面留出 4K 空间用于存放 FAT;9000-0100=8F00,90000和8F000之前差4Kmoves, ax; /popax ;ax=2movbyte [bOdd], 0movbx, 3mulbx; ax=6;movbx, 2divbx;ax=3(商);dx=0(余数),本质是ax*1.5,为了求偏移cmpdx, 0jzLABEL_EVENmovbyte [bOdd], 1 ;如果为1,说明是奇数LABEL_EVEN:;偶数; 现在 ax 中是 FATEntry 在 FAT 中的偏移量,下面来; 计算 FATEntry 在哪个扇区中(FAT占用不止一个扇区)xordx, dxmovbx, [BPB_BytsPerSec] :512divbx ; dx:ax / BPB_BytsPerSec ; ax <- 商 (FATEntry 所在的扇区相对于 FAT 的扇区号) 结果是0号扇区 ; dx <- 余数 (FATEntry 在扇区内的偏移) 结果是3号偏移pushdxmovbx, 0 ; bx <- 0 于是, es:bx = (BaseOfLoader - 100):00addax, SectorNoOfFAT1 ; 引导扇区占一个扇区,此句之后的 ax 就是 FATEntry 所在的扇区号movcl, 2callReadSector ; 读取 FATEntry 所在的扇区, 一次读两个, 避免在边界 ; 发生错误, 因为一个 FATEntry 可能跨越两个扇区popdxaddbx, dx ;偏移为3movax, [es:bx] ; 如前面的图所说,ax=8FFFhcmpbyte [bOdd], 1jnzLABEL_EVEN_2shrax, 4 ;如果偏移为4,上步得到ax=008Fh,右移后ax=0008h,与后还为0008hLABEL_EVEN_2:andax, 0FFFh ;不等于跳到此处,ax=0FFFhLABEL_GET_FAT_ENRY_OK:popbxpopesret;----------------------------------------times 510-($-$$)db0; 填充剩下的空间,使生成的二进制代码恰好为512字节dw 0xaa55; 结束标志