冰琥珀 发表于 2015-10-19 00:47:24

自己动手写操作系统系列之----------引导扇区编写

本帖最后由 冰琥珀 于 2015-10-19 13:38 编辑

        上一个帖子:http://www.ihonker.org/thread-6625-1-1.html
        上个帖子我们讲述了如何搭建开发环境,同时也写了个简单的"Hello, OS",这次我们来学学引导扇区的编写,确切的说应该是MBR(主引导记录)的编写,它是磁盘的第一个扇区,当我们按下电源键,BIOS完成一些初始化之后,就将MBR读取到0x7c00处,并将控制权转交给MBR来执行,由MBR来完成内核的加载。由于MBR最多只有512个字节(1个扇区),所以加载内核的工作不可能在MBR中完成,那么MBR又是干啥的呢。
        其实MBR只是在磁盘中找到Loader并将其读入内存,然后把控制权转交给Loader,让它来完成接下来的工作。所以MBR的工作就只是在磁盘中找到Loader。
        因为我们现在还没有文件系统,所以一切都只是用的软盘,后期我们有自己的文件系统了,我们会把软盘改为硬盘。在我们接下来的实验中,软盘都格式化为FAT12,所以我们要对FAT12文件系统有所了解。先来看看FAT12文件系统格式
       
        从上图可以看到,引导扇区之后就是FAT1和FAT2(FAT2是FAT1的备份),每个FAT表占9个扇区,FAT后紧跟着的是根目录区,大小在引导扇区的BPB中有设定,根目录区之后是数据区。我们这次要找到Loader,就要从根目录入手,先在根目录中找到Loader的文件名,然后在根据根目录结构中的簇号到FAT表中去找到相应的项,再根据FAT来读取Loader的数据,并将其加载到内存中。说了这么多,我们先看引导扇区的结构,如下图
       
        从上图可以看出,在引导扇区开始的3个字节是一个跳转指令,然后是OEM字符串,它必须是一个8字节长的字符串标,标识了格式化此磁盘的操作系统的名称和版本号
        接下来是每扇区字节数,一般情况下,这个值是512
        接着是每簇扇区数,在这里我们填1
        接着是保留扇区数,这里默认也为1
        接着是FAT表个数,这里有两个,分别是FAT1和FAT2,所以这个值为2
        然后是最大根目录数,用来记录根目录区有多少条目录项
        再下来是逻辑扇区总数,用来记录整个磁盘的扇区数
        接着是磁盘类型标识符,使用0f0h表示3.5寸高密码软盘,用0f8h来表示硬盘
        然后是每FAT扇区数,记录每个FAT表中有多少个扇区
        接着是每磁道扇区数,记录每个磁道中有多少个扇区
       然后是双字节长的磁头数,磁头数指的是磁盘面数,每面都有一个磁头,软盘都是2面的,所以在FAT12格式下此字段固定为2
        接下来是的位于偏移28处类型为双字(4B)长的隐藏扇区数,指的在引导扇区之前的隐藏扇区,在FAT12格式上此字段默认为0,即不隐藏任何扇区
      偏移32处的是类型为双字(4B)长的逻辑扇区总数,如果此分区或磁盘的逻辑扇区总数大于65536则用这个字段来表示逻辑扇区总数,否则设置此字段为0
        偏移36处的是物理驱动器号,类型是字节长,它与BIOS物理驱动器相关,在磁盘中断Int13h相关的操作中使用,第一个软盘驱动器设置为0,第一个硬盘驱动器设置为80h,第二个硬盘驱动器设置为81h,以此类推
        位于偏移37处的字节没有使用,保留并设置为0
        位于偏移38处的是扩展引导标识,类型是字节,操作系统用它来识别引导信息,值可以是28h或29h
        接下来的是位于偏移39处的卷标号,类型是双字(4B)长,在格式化磁盘时所产生的一个随机序号,有助于区分磁盘,可以为0
        然后是位于偏移43处的卷标,长度必须是11字节长(不足以空格20h填充)
        最后是位于偏移54处的是长度为8字节的文件系统类型标识符,不足8字节则以空格20h来填充
        接下来就是引导代码了,最后的两个字节要填入0xaa55,表示引导扇区结束
        引导记录结构介绍了之后,我们来看FAT表,相比之下,FAT表的结构就简单的多了,一句话描述就是每个FAT项(FATEntry)长度位12Bit,即一个半字节,FAT表中每3个字节存放了2个FATEntry,这里也不再贴出它的结构图了,下面看根目录的结构,如下图
       
        可以看到,在根目录中,每个目录项占32个字节,其中,前11个字节是文件名(文件名占8字节,拓展名占3字节),接下来是文件属性(占1个字节),然后是保留位(占10个字节),然后是写入时间和日期(各占2个字节,总共占4个字节),接下来是文件在FAT表中的簇号(占2字节),然后是文件大小(占4字节),总共32个字节。在上面的这些项中,我们不需要关心那么多,只要关注文件名和文件在FAT表中的簇号即可。首先通过文件名的对比来找到相应的目录项,其次再从目录项中读取处文件在FAT表中的簇号,然后根据这个簇号在FAT表中找出相应的项,再根据其来读取文件数据,现在我们就先写代码实现在根目录区中找到Loader,代码如下
        org 0x7c00
       
        jmp                LABEL_START                ;跳转       
        nop
        BS_OEMName                db                'FreeDos '                ;OEM string,8个字节
        BPB_BytePerSec        dw                512                                ;每扇区字节数
        BPB_SecPerClus        db                1                                ;每簇扇区数
        BPB_RsvdSecCnt        dw                1                                ;保留扇区数
        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_HideSec                dd                0                                ;隐藏扇区数
        BPB_TotSec32        dd                0                                ;如果BPB_TotSec16为0,这里记录扇区总数
        BS_DrvNum                db                0                                ;中断13的驱动器号
        BS_Reserved1        db                0                                ;未使用
        BS_BootSig                db                0x29                        ;扩展引导标记
        BS_VolID                dd                0                                ;卷序列号
        BS_VolLab                db                'Orange OS'        ;卷标,11个字节
        BS_FileSysType        db                'FAT12   '                ;文件系统类型

LABEL_START:
        mov ax, cs
        mov ds, ax
        mov es, ax
        mov ss, ax
        mov sp, BaseOfStack

        ; 清屏
        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

        ;显示Booting
        mov dh, 0
        call DispStr

        ;软驱复位
        xor ah, ah
        xor dl, dl
        int 0x13

        mov word , RootDirSecNum
LABEL_SEARCH_IN_ROOT_DIR_BEGIN:
        cmp word , 0
        jz        LABEL_SEARCH_IN_ROOT_DIR_END        ;这里跳转表示整个根目录都搜索完毕,没有找到Loader.bin
       
        dec word
        mov        ax, BaseOfLoader
        mov es, ax
        mov bx, OffsetOfLoader
        mov ax, word
        mov cl, 1
        call ReadSector                                ;读取扇区数据
        mov di, OffsetOfLoader
        cld
        mov dl, 0x10                                ;每个扇区中目录个数为16

LABEL_CMP_NEXT_FILE_NAME:
        mov si, LoaderFileName
        mov cl, 0x0b                                ;文件名长度为11个字节

LABEL_CMP_FILE_NAME:
        test cl, cl
        jz LABEL_FOUND_LOADER
        dec cl
        lodsb
        test al, al
        jz LABEL_CMP_NAME_END                ;文件名比较完毕
        cmp al,
        jz LABEL_GO_ON                ;从根目录中读取到的文件名字符和目标文件名字符相等,则继续比较下一个字符

        jmp LABEL_CMP_NAME_END

LABEL_GO_ON:
        inc di
        jmp LABEL_CMP_FILE_NAME

LABEL_CMP_NAME_END:
        dec dl
        jz LABEL_GO_TO_NEXT_SECTOR        ;16组目录都比较完毕,则读取下一个扇区
        and di, 0xffe0                                ;这里把di指针重置到当前目录的起始处
        add di, 0x20                                ;指针指向下一组目录(每组目录长度为32个字节)
        jmp        LABEL_CMP_NEXT_FILE_NAME        ;比较下一组文件名

LABEL_GO_TO_NEXT_SECTOR:
        inc word
        jmp LABEL_SEARCH_IN_ROOT_DIR_BEGIN

LABEL_SEARCH_IN_ROOT_DIR_END:        ;没找到LOADER.BIN
        mov dh, 2
        call DispStr

        jmp $

LABEL_FOUND_LOADER:                                ;找到LOADER.BIN文件
        jmp $

ReadSector:
        push bp
        mov bp, sp
        sub sp, 2
        mov byte , cl
        push bx
        mov bl,                        
        div bl                                                ;计算出当前扇区所处的磁道
        inc ah                                                ;除得的商放在al,余数放在ah(al中放的是磁道号,ah中放的是扇区号)
        mov cl, ah
        mov dh, al
        shr al, 1                                        ;磁道号除以2得到磁头号
        mov ch, al
        and ch, 1
        pop bx
        mov dl,
.GoOnReading:
        mov ah, 2                                        ;读扇区
        mov al, byte                 ;要读的扇区数
        int 0x13
        jc .GoOnReading                                ;读扇区失败则继续读
        add sp, 2
        pop bp,
        ret

DispStr:
        mov ax, MessageLength
        mul dh
        add ax, BootMessage
        mov bp, ax
        mov ax, ds
        mov es, ax
        mov cx, MessageLength
        mov ax, 0x1301
        mov bx, 0x0007
        mov dl, 0
        int 0x10
        ret

        BaseOfStack                equ                0x7c00
        BaseOfLoader        equ                0x9000
        OffsetOfLoader        equ                0x100
        RootDirSecNum        equ                19                        ;根目录起始扇区号=FAT扇区数(2个) + 引导扇区数
        wRootSecCnt                dw                14                        ;根目录所占的扇区数
        wRootCurSecNum        dw                0
        wFileNameCnt        db                0
        LoaderFileName        db                'LOADERBIN', 0        ;LOADER.BIN (文件名)
        MessageLength        equ                9
        BootMessage:        db                "Booting"
        Message1:                db                "Ready.   "
        Message2:                db                "No LOADER"
        times        510-($-$$) db 0
        dw                0xaa55

        代码看着有点长,我们一点一点来分析。这段代码因为是写在引导扇区的,所以要按照引导扇区的格式来写。在上面我们已经看到了引导扇区最开始的3个字节是一个跳转指令,所以在程序的开头我们写入
        jmp                LABEL_START
        这里只占两个字节,剩余的一个字节用nop填充,接下来就是一堆的数据,具体的上面有介绍,我们直接看跳转到的代码
        mov ax, cs
        mov ds, ax
        mov es, ax
        mov ss, ax
        mov sp, BaseOfStack

        ; 清屏
        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

        ;显示Booting
        mov dh, 0
        call DispStr

        ;软驱复位
        xor ah, ah
        xor dl, dl
        int 0x13
        这里就是一些初始化的工作,设置段寄存器,设置栈,清屏,显示字符串,复位软盘等,真正的算法在下面。我们先来整理思路,要在根目录中找到Loader,那个我们就先要确定根目录的起始偏移,前面我们有介绍过,根目录位于FAT表之后,每个FAT表占9个扇区,两个FAT表就占18个扇区,再加上一个引导扇区,所以根目录的起始扇区是19,既然要在根目录中查找,那就要有个范围,我们已经得到了起始值为19,根目录总的扇区数=根目录文件数最大值 * 32 / 512,这里得出的结果是14,所以我们就从19号扇区开始找,最大找满14个扇区,代码如下
        mov word , RootDirSecNum                ;RootDirSecNum存放的是根目录的起始扇区号       
LABEL_SEARCH_IN_ROOT_DIR_BEGIN:
        cmp word , 0                                                ;wRootSecCnt里存放的是根目录总的扇区数,为0表示根目录所有的扇区都已经查找了
        jz        LABEL_SEARCH_IN_ROOT_DIR_END                ;这里跳转表示整个根目录都搜索完毕,没有找到Loader.bin
       
        上面的代码只是检测根目录是否已经查找完毕,如果查找完毕就跳转到后面显示没找到,没找到后的代码如下
        LABEL_SEARCH_IN_ROOT_DIR_END:        ;没找到LOADER.BIN
        mov dh, 2        ;2表示显示第三组字符串(BootMessage开始的3组字符串),即No LOADER
        call DispStr

        jmp $                :程序停在这
        如果没找到,我们就让程序先停在这,下面我们看根目录查找没有结束的情况,如果没有结束,我们就要读取数据,一次读取一个扇区,代码如下
        dec word
        mov        ax, BaseOfLoader
        mov es, ax
        mov bx, OffsetOfLoader
        mov ax, word
        mov cl, 1
        call ReadSector                                ;读取扇区数据
        上面先是进行一些设置,比如要读取哪个扇区,读取的扇区数,读取到哪个位置等。数据读取出来了之后,我们就要查找Loader的文件名了,这里我们要找的文件名是"LOADERBIN",查找的方法就是取出一个目录项,然后一个字节一个字节的比较文件名,在上面我们介绍过目录项的结构,开始的11个字节就是文件名,所以我们只比较11个字节,如果全部相等,表示找到,否则取出下一个目录项进行比较,代码如下
        mov di, OffsetOfLoader               
        cld
        mov dl, 0x10                                ;每个扇区中目录个数为16

LABEL_CMP_NEXT_FILE_NAME:
        mov si, LoaderFileName
        mov cl, 0x0b                                ;文件名长度为11个字节

LABEL_CMP_FILE_NAME:
        test cl, cl
        jz LABEL_FOUND_LOADER
        dec cl
        lodsb
        test al, al
        jz LABEL_CMP_NAME_END                ;文件名比较完毕
        cmp al,
        jz LABEL_GO_ON                ;从根目录中读取到的文件名字符和目标文件名字符相等,则继续比较下一个字符

        jmp LABEL_CMP_NAME_END

LABEL_GO_ON:
        inc di
        jmp LABEL_CMP_FILE_NAME
        中存放的是从根目录中读取到的数据,si中存放的是目标文件名,这里用lodsb从si中取出一个字节代al中,然后用个字节和中的进行比较,如果相等就比较下一个(jz LABEL_GO_ON),否则就表示这个目录项比较结束,程序跳转到LABEL_CMP_NAME_END,如果全部比较完成后,发现都相等则跳转到LABEL_FOUND_LOADER,找到后我们先让程序停在那,我们现在看如果当前目录项不是我们要找的,又怎么处理,代码如下
LABEL_CMP_NAME_END:
        dec dl                                        ;目录项数减1
        jz LABEL_GO_TO_NEXT_SECTOR        ;16组目录都比较完毕,则读取下一个扇区
        and di, 0xffe0                                ;这里把di指针重置到当前目录的起始处
        add di, 0x20                                ;指针指向下一组目录(每组目录长度为32个字节)
        jmp        LABEL_CMP_NEXT_FILE_NAME        ;比较下一组文件名

LABEL_GO_TO_NEXT_SECTOR:
        inc word
        jmp LABEL_SEARCH_IN_ROOT_DIR_BEGIN
        如果当前目录项不是我们要找的,那么我们就跳转到下一个,这时dl中记录的当前扇区目录项的总数要减1,如果dl为0,表示当前扇区的所有目录项都查找完毕,那么我们就要去读取下一个扇区(jz LABEL_GO_TO_NEXT_SECTOR),否则就跳到下一个目录项进行比较,依次循环。
        到了这里,我们这个帖子的目标也达到了,就是在根目录中查找Loader的文件名,至于找到之后该咋办,或者没找到又该怎么处理,我们下个帖子再作处理。

ayang 发表于 2015-10-19 06:32:07

支持中国红客联盟(ihonker.org)

小龙 发表于 2015-10-20 07:59:33

感谢楼主的分享~

小圈圈 发表于 2015-10-21 10:06:43

流弊流弊O(∩_∩)O~

wanmznh 发表于 2015-10-21 21:36:39

还是不错的哦,顶了

HUC-参谋长 发表于 2015-10-22 02:14:35

支持中国红客联盟(ihonker.org)

r00tc4 发表于 2015-10-22 05:55:15

还是不错的哦,顶了

arctic 发表于 2015-10-22 15:14:53

wtsqq123 发表于 2015-10-23 11:30:03

若冰 发表于 2015-10-24 07:29:05

学习学习技术,加油!
页: [1]
查看完整版本: 自己动手写操作系统系列之----------引导扇区编写