diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..227ce24 --- /dev/null +++ b/Makefile @@ -0,0 +1,51 @@ +################################################## +# Makefile +################################################## + +BOOT:=boot.asm +LDR:=loader.asm +BOOT_BIN:=$(subst .asm,.bin,$(BOOT)) +LDR_BIN:=$(subst .asm,.bin,$(LDR)) + +.PHONY : everything + +everything : $(BOOT_BIN) $(LDR_BIN) + @dd if=/dev/zero of=a.img bs=512 count=2880 + @mkfs -t vfat a.img + @dd if=$(BOOT_BIN) of=a.img bs=512 count=1 conv=notrunc + @ mount -o loop a.img /mnt + @ cp $(LDR_BIN) /mnt -v + @ umount /mnt + @ qemu-system-x86_64 -boot order=c -drive file=a.img,format=raw + +clean : + @rm -f $(BOOT_BIN) $(LDR_BIN) + +$(BOOT_BIN) : $(BOOT) + @nasm $< -o $@ + +$(LDR_BIN) : $(LDR) + @nasm $< -o $@ + + +run: + @qemu-system-i386 \ + -boot order=c \ + -drive file=a.img,format=raw \ + +gdb: + @qemu-system-i386 \ + -boot order=c \ + -drive file=a.img,format=raw \ + -S -s + +monitor: + @gdb \ + -ex 'set architecture i8086' \ + -ex 'target remote localhost:1234' \ + -ex 'b *0x9400' + +bin: + @nasm boot.asm -o boot.bin + @nasm loader.asm -o loader.bin + diff --git a/boot.asm b/boot.asm new file mode 100644 index 0000000..1b0f9a2 --- /dev/null +++ b/boot.asm @@ -0,0 +1,323 @@ + org 07c00h ; Boot 状态, Bios 将把 Boot Sector 加载到 0:7C00h 处并开始执行 + +; 下面这部分不仅是boot的内容,也是Fat12文件系统的引导部分 +; boot需要文件系统的加持(可以没有,但是如果这么干的话boot可扩展性为0,磁盘内容位置稍微变动就不能用了)才能正确的将loader的内容导入到内存里执行 +;================================================================================================ + ; Fat12规定前3字节是跳转代码,后面再是文件系统信息 + ; 如果是短跳转(2字节,指令机器码以0xEB开头),就需要后面加一个nop填充到3字节 + ; 如果是长跳转(3字节,指令机器码以0xE9开头),就不需要添加nop了 + jmp Main ; Start to boot. + ; nop ; 这个 nop 不可少 + + ; 下面是 FAT12 磁盘的头 + ; 正常情况下,boot是要对磁盘头的数据进行解析的 + ; 但是出于简单考虑,直接将磁盘头硬编码进来,如果要分析可就太麻烦了,汇编本来就看的头大,还搞那么多未知元 + ; 里面很多信息实际上在boot里用不上,请各位别对里面的参数纠结太多,用到了再查也不迟 + 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 80h ; 中断 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个字节 + + ; 文件系统信息存放完毕后后面的内容就可以自由调整了,撒花! + +; 原先Orange的代码是按照 主函数->常量->变量->子函数的顺序给出,逻辑很乱 +; 这次尝试用C语言风格按照 常量->变量->子函数->主函数的顺序给出,更加符合正常的编程逻辑 +;============================================================================ +;常量 +;================================================================================================ +; boot的内存模型实际上比较简单(区间左闭右开) +; (0x500~0x7c00) 栈 +; (0x7c00~0x7e00) 引导扇区 +; (0x90000~0x90400) 缓冲区,GetNextCluster函数会用到它 +; (0x90400~?) 加载区,loader代码会加载到这里 +BaseOfStack equ 07c00h ; Boot状态下堆栈基地址(栈底, 从这个位置向低地址生长) +BaseOfLoader equ 09000h ; LOADER.BIN 被加载到的位置 ---- 段地址 +OffsetOfLoader equ 0400h ; LOADER.BIN 被加载到的位置 ---- 偏移地址 + +; 这部分请看手册 +RootDirSectors equ 14 ; +SectorNoOfRootDirectory equ 19 +SectorNoOfFAT1 equ 1 +DeltaSectorNo equ 31 +;================================================================================================ + +;============================================================================ +;变量 +;---------------------------------------------------------------------------- +LeftRootDirSectors dw RootDirSectors ; 还未搜索的根目录扇区数 +RootDirSectorNow dw SectorNoOfRootDirectory ; 目前正在搜索的根目录扇区 +BufferPacket times 010h db 0 ; ReadSector函数会用到的,用于向int 13h中断的一个缓冲区 + +;============================================================================ +;字符串 +;---------------------------------------------------------------------------- +LoaderFileName db "LOADER BIN", 0 ; LOADER.BIN 的文件名(为什么中间有空格请RTFM) +; 为简化代码, 下面每个字符串的长度均为 MessageLength +MessageLength equ 9 +BootMessage: db "Booting " ; 9字节, 不够则用空格补齐. 序号 0 +Message1 db "Ready. " ; 9字节, 不够则用空格补齐. 序号 1 +Message2 db "Read Fail" ; 9字节, 不够则用空格补齐. 序号 2 +Message3 db "No Loader" ; 9字节, 不够则用空格补齐. 序号 3 +;============================================================================ +; 汇编并不像高级语言一样规范,寄存器忘保存,调用子函数后发现值变了可太痛苦了 +; 所以为了减少这份痛苦,这里的所有函数都保证函数除了返回值寄存器其余的主要寄存器都有保护现场 +; 保证调用之后不用担心寄存器值变了 + +;---------------------------------------------------------------------------- +; 函数名: DispStr +;---------------------------------------------------------------------------- +; 作用: +; 显示一个字符串, 函数开始时 dh 中应该是字符串序号(从0开始) +DispStr: + push bp + mov bp, sp + pusha + push es + + mov ax, MessageLength + mul dh + add ax, BootMessage + mov bp, ax + mov ax, ds + mov es, ax ; ES:BP = 串地址 + mov cx, MessageLength ; CX = 串长度 + mov ax, 01301h ; AH = 13, AL = 01h + mov bx, 0007h ; 页号为0(BH = 0) 黑底白字(BL = 07h) + mov dl, 0 + int 10h + + pop es + popa + pop bp + ret + +;---------------------------------------------------------------------------- +; 函数名: DispDot +;---------------------------------------------------------------------------- +; 作用: +; 打印一个点 +DispDot: + push bp + mov bp, sp + pusha + + mov ah, 0Eh ; `. 每读一个扇区就在 "Booting " 后面 + mov al, '.' ; | 打一个点, 形成这样的效果: + mov bl, 0Fh ; | Booting ...... + int 10h ; / + + popa + pop bp + ret + +;---------------------------------------------------------------------------- +; 函数名: ReadSector +;---------------------------------------------------------------------------- +; 作用: +; 将磁盘的数据读入到内存中 +; ax: 从哪个扇区开始 +; cx: 读入多少个扇区 +; (es:bx): 读入的缓冲区的起始地址 +; +; 中断调用传入的参数规范请参考本节实验指导书的实验参考LBA部分 +ReadSector: + push bp + mov bp, sp + pusha + + mov si, BufferPacket ; ds:si 指向的是BufferPacket的首地址 + mov word [si + 0], 010h ; buffer_packet_size + mov word [si + 2], cx ; sectors + mov word [si + 4], bx ; buffer-offset + mov word [si + 6], es ; buffer-segment + mov word [si + 8], ax ; start_sectors + + mov dl, [BS_DrvNum] ; 驱动号 + mov ah, 42h ; 扩展读 + int 13h + jc .ReadFail ; 读取失败,简单考虑就默认bios坏了 + + popa + pop bp + ret + +.ReadFail: + mov dh, 2 + call DispStr + jmp $ ; 如果cf位置1,就意味着读入错误,这个时候建议直接开摆 + +;---------------------------------------------------------------------------- +; 函数名: GetNextCluster +;---------------------------------------------------------------------------- +; 作用: +; ax存放的是当前的簇(cluster)号,根据当前的簇号在fat表里查找,找到下一个簇的簇号,并将返回值存放在ax +GetNextCluster: + push bp + mov bp, sp + pusha + + mov bx, 3 ; 一个FAT项长度为1.5字节 + mul bx + mov bx, 2 ; ax = floor(clus_number * 1.5) + div bx ; 这个时候ax里面放着的是FAT项基地址相对于FAT表开头的字节偏移量 + ; 如果clus_number为奇数,则dx为1,否则为0 + push dx ; 临时保存奇数标识信息 + mov dx, 0 ; 下面除法要用到 + mov bx, [BPB_BytsPerSec] + div bx ; dx:ax / BPB_BytsPerSec + ; ax <- 商 (基地址在FAT表的第几个扇区) + ; dx <- 余数 (基地址在扇区内的偏移) + mov bx, 0 ; bx <- 0 于是, es:bx = BaseOfLoader:0 + add ax, SectorNoOfFAT1 ; 此句之后的 ax 就是FAT项所在的扇区号 + mov cx, 2 ; 读取FAT项所在的扇区, 一次读两个, 避免在边界 + call ReadSector ; 发生错误, 因为一个FAT项可能跨越两个扇区 + + mov bx, dx ; 将偏移量搬回bx + mov ax, [es:bx] + pop bx ; 取回奇数标识信息 + cmp bx, 0 ; 如果是第奇数个FAT项还得右移四位 + jz EvenCluster ; 可能是微软(FAT是微软创建的)第一个亲儿子的原因,有它的历史局限性 + shr ax, 4 ; 当时的磁盘很脆弱,经常容易写坏,所以需要两张FAT表备份,而且人们能够制作的存储设备的容量很小 +EvenCluster: + and ax, 0FFFh ; 读完需要与一下,因为高位是未定义的,防止ax值有误 + mov word [bp - 2], ax ; 这里用了一个技巧,这样在popa的时候ax也顺便更新了 + + popa + pop bp + ret + +;---------------------------------------------------------------------------- +; 函数名: StringCmp +;---------------------------------------------------------------------------- +; 作用: +; 比较 ds:si 和 es:di 处的字符串(比较长度为11,仅为loader.bin所用) +; 如果两个字符串相等ax返回1,否则ax返回0 +StringCmp: + push bp + mov bp, sp + pusha + + mov cx, 11 ; 比较长度为11 + cld ; 清位保险一下 +.STARTCMP: + lodsb ; ds:si -> al + cmp al, byte [es:di] + jnz .DIFFERENT + inc di + dec cx + cmp cx, 0 + jz .SAME + jmp .STARTCMP +.DIFFERENT: + mov word [bp - 2], 0 ; 这里用了一个技巧,这样在popa的时候ax也顺便更新了 + jmp .ENDCMP +.SAME: + mov word [bp - 2], 1 ; 下一步就是ENDCMP了,就懒得jump了 +.ENDCMP: + popa + pop bp + ret +;---------------------------------------------------------------------------- +; 这里就是真正的boot的处理函数了,boot实际上只做了一件事,将loader从磁盘里搬到内存指定位置 +; 如何将loader搬到内存中就需要文件系统里面的信息的帮助 +; 通过扫描根目录区中所有可能的目录项找到loader.bin对应的目录项 +; 然后根据目录项信息读入loader.bin的文件内容 +Main: + mov ax, cs ; cs <- 0 + mov ds, ax ; ds <- 0 + mov ss, ax ; ss <- 0 + mov ax, BaseOfLoader + mov es, ax ; es <- BaseOfLoader + mov sp, BaseOfStack ; 这几个段寄存器在Main里都不会变了 + + ; 清屏 + 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 ; 显示字符串 + + mov ah, 0 ; ┓ + mov dl, [BS_DrvNum] ; ┣ 硬盘复位 + int 13h ; ┛ + +; 下面在 A 盘的根目录寻找 LOADER.BIN +FindLoaderInRootDir: + mov ax, [RootDirSectorNow]; ax <- 现在正在搜索的扇区号 + mov bx, OffsetOfLoader ; es:bx = BaseOfLoader:OffsetOfLoader + mov cx, 1 + call ReadSector + + mov si, LoaderFileName ; ds:si -> "LOADER BIN" + mov di, OffsetOfLoader ; es:di -> BaseOfLoader:400h = BaseOfLoader*10h+400h + mov dx, 10h ; 32(目录项大小) * 16(dx) = 512(BPB_BytsPerSec) + +CompareFilename: + call StringCmp + cmp ax, 1 + jz LoaderFound ; ax == 1 -> 比对成了 + dec dx + cmp dx, 0 + jz GotoNextRootDirSector ; 该扇区的所有目录项都探索完了,去探索下一个扇区 + add di, 20h ; 32 -> 目录项大小 + jmp CompareFilename + +GotoNextRootDirSector: + inc word [RootDirSectorNow] ; 改变正在搜索的扇区号 + dec word [LeftRootDirSectors] ; ┓ + cmp word [LeftRootDirSectors], 0 ; ┣ 判断根目录区是不是已经读完 + jz NoLoader ; ┛ 如果读完表示没有找到 LOADER.BIN,就直接开摆 + jmp FindLoaderInRootDir + +NoLoader: + mov dh, 3 + call DispStr + jmp $ + +LoaderFound: ; 找到 LOADER.BIN 后便来到这里继续 + add di, 01Ah ; 0x1a = 28 这个 28 在目录项里偏移量对应的数据是起始簇号(RTFM) + mov dx, word [es:di] ; 起始簇号占2字节,读入到dx里 + mov bx, OffsetOfLoader ; es:bx = BaseOfLoader:OffsetOfLoader + +LoadLoader: + call DispDot + mov ax, dx ; ax <- 数据区簇号 + add ax, DeltaSectorNo ; 数据区的簇号需要加上一个偏移量才能得到真正的扇区号 + mov cx, 1 ; 一个簇就仅有一个扇区 + call ReadSector + mov ax, dx ; ax <- 数据区簇号(在之前ax = 数据区簇号+偏移量) + call GetNextCluster ; 根据数据区簇号获取文件下一个簇的簇号 + mov dx, ax ; dx <- 下一个簇的簇号 + cmp dx, 0FFFh ; 判断是否读完了(根据文档理论上dx只要在0xFF8~0xFFF都行,但是这里直接偷懒只判断0xFFF) + jz LoadFinished + add bx, [BPB_BytsPerSec] ; 别忘了更新bx,否则你会发现文件发生复写的情况(指来回更新BaseOfLoader:OffsetOfLoader ~ BaseOfLoader:OffsetOfLoader+0x200) + jmp LoadLoader +LoadFinished: + mov dh, 1 ; "Ready." + call DispStr ; 显示字符串 + ; 这一句正式跳转到已加载到内 + ; 存中的 LOADER.BIN 的开始处, + ; 开始执行 LOADER.BIN 的代码。 + ; Boot Sector 的使命到此结束 + jmp BaseOfLoader:OffsetOfLoader + +times 510-($-$$) db 0 ; 填充剩下的空间,使生成的二进制代码恰好为512字节 +dw 0xaa55 ; 结束标志 diff --git a/i386-32bit.xml b/i386-32bit.xml new file mode 100644 index 0000000..872fcea --- /dev/null +++ b/i386-32bit.xml @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/loader.asm b/loader.asm new file mode 100644 index 0000000..d3afa13 --- /dev/null +++ b/loader.asm @@ -0,0 +1,285 @@ +org 0400h + ; + + mov ax, cs + mov ds, ax + mov es, ax + call DispStr1 ; 调用显示字符串例程 + jmp $ ; 无限循环 + +DispStr1: + mov ax, STR + mov bp, ax ; ES:BP = 串地址 + mov cx, 28 ; CX = 串长度 + mov ax, 01301h ; AH = 13, AL = 01h + mov bx, 000fh ; 页号为0(BH = 0) + mov dx, 01326 + int 10h ; 10h 号中断 + ret + + STR: db "this is zhangjianlong's boot"; + + + + + + + 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 80h ; 中断 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个字节 + +; (0x500~0x7c00) 栈 +; (0x7c00~0x7e00) 引导扇区 +; (0x90000~0x90400) 缓冲区,GetNextCluster函数会用到它 +; (0x90400~?) 加载区,loader代码会加载到这里 +BaseOfStack equ 07c00h ; Boot状态下堆栈基地址(栈底, 从这个位置向低地址生长) +BaseOfLoader equ 09000h ; LOADER.BIN 被加载到的位置 ---- 段地址 +OffsetOfLoader equ 0400h ; LOADER.BIN 被加载到的位置 ---- 偏移地址 + +; 这部分请看手册 +RootDirSectors equ 14 ; +SectorNoOfRootDirectory equ 19 +SectorNoOfFAT1 equ 1 +DeltaSectorNo equ 31 +;================================================================================================ + +;============================================================================ +;变量 +;---------------------------------------------------------------------------- +LeftRootDirSectors dw RootDirSectors ; 还未搜索的根目录扇区数 +RootDirSectorNow dw SectorNoOfRootDirectory ; 目前正在搜索的根目录扇区 +BufferPacket times 010h db 0 ; ReadSector函数会用到的,用于向int 13h中断的一个缓冲区 + +;============================================================================ +;字符串 +;---------------------------------------------------------------------------- +LoaderFileName db "LOADER BIN", 0 ; LOADER.BIN 的文件名(为什么中间有空格请RTFM) +; 为简化代码, 下面每个字符串的长度均为 MessageLength +MessageLength equ 9 +BootMessage: db "Booting " ; 9字节, 不够则用空格补齐. 序号 0 +Message1 db "Ready. " ; 9字节, 不够则用空格补齐. 序号 1 +Message2 db "Read Fail" ; 9字节, 不够则用空格补齐. 序号 2 +Message3 db "No Loader" ; 9字节, 不够则用空格补齐. 序号 3 +;============================================================================ +; 汇编并不像高级语言一样规范,寄存器忘保存,调用子函数后发现值变了可太痛苦了 +; 所以为了减少这份痛苦,这里的所有函数都保证函数除了返回值寄存器其余的主要寄存器都有保护现场 +; 保证调用之后不用担心寄存器值变了 + +;---------------------------------------------------------------------------- +; 函数名: DispStr +;---------------------------------------------------------------------------- +; 作用: +; 显示一个字符串, 函数开始时 dh 中应该是字符串序号(从0开始) +DispStr: + push bp + mov bp, sp + pusha + push es + + mov ax, MessageLength + mul dh + add ax, BootMessage + mov bp, ax + mov ax, ds + mov es, ax ; ES:BP = 串地址 + mov cx, MessageLength ; CX = 串长度 + mov ax, 01301h ; AH = 13, AL = 01h + mov bx, 0007h ; 页号为0(BH = 0) 黑底白字(BL = 07h) + mov dl, 0 + int 10h + + pop es + popa + pop bp + ret + +;---------------------------------------------------------------------------- +; 函数名: ReadSector +;---------------------------------------------------------------------------- +; 作用: +; 将磁盘的数据读入到内存中 +; ax: 从哪个扇区开始 +; cx: 读入多少个扇区 +; (es:bx): 读入的缓冲区的起始地址 +; +; 中断调用传入的参数规范请参考本节实验指导书的实验参考LBA部分 +ReadSector: + push bp + mov bp, sp + pusha + + mov si, BufferPacket ; ds:si 指向的是BufferPacket的首地址 + mov word [si + 0], 010h ; buffer_packet_size + mov word [si + 2], cx ; sectors + mov word [si + 4], bx ; buffer-offset + mov word [si + 6], es ; buffer-segment + mov word [si + 8], ax ; start_sectors + + mov dl, [BS_DrvNum] ; 驱动号 + mov ah, 42h ; 扩展读 + int 13h + jc .ReadFail ; 读取失败,简单考虑就默认bios坏了 + + popa + pop bp + ret + +.ReadFail: + mov dh, 2 + call DispStr + jmp $ ; 如果cf位置1,就意味着读入错误,这个时候建议直接开摆 + +;---------------------------------------------------------------------------- +; 函数名: GetNextCluster +;---------------------------------------------------------------------------- +; 作用: +; ax存放的是当前的簇(cluster)号,根据当前的簇号在fat表里查找,找到下一个簇的簇号,并将返回值存放在ax +GetNextCluster: + push bp + mov bp, sp + pusha + + mov bx, 3 ; 一个FAT项长度为1.5字节 + mul bx + mov bx, 2 ; ax = floor(clus_number * 1.5) + div bx ; 这个时候ax里面放着的是FAT项基地址相对于FAT表开头的字节偏移量 + ; 如果clus_number为奇数,则dx为1,否则为0 + push dx ; 临时保存奇数标识信息 + mov dx, 0 ; 下面除法要用到 + mov bx, [BPB_BytsPerSec] + div bx ; dx:ax / BPB_BytsPerSec + ; ax <- 商 (基地址在FAT表的第几个扇区) + ; dx <- 余数 (基地址在扇区内的偏移) + mov bx, 0 ; bx <- 0 于是, es:bx = BaseOfLoader:0 + add ax, SectorNoOfFAT1 ; 此句之后的 ax 就是FAT项所在的扇区号 + mov cx, 2 ; 读取FAT项所在的扇区, 一次读两个, 避免在边界 + call ReadSector ; 发生错误, 因为一个FAT项可能跨越两个扇区 + + mov bx, dx ; 将偏移量搬回bx + mov ax, [es:bx] + pop bx ; 取回奇数标识信息 + cmp bx, 0 ; 如果是第奇数个FAT项还得右移四位 + jz EvenCluster ; 可能是微软(FAT是微软创建的)第一个亲儿子的原因,有它的历史局限性 + shr ax, 4 ; 当时的磁盘很脆弱,经常容易写坏,所以需要两张FAT表备份,而且人们能够制作的存储设备的容量很小 +EvenCluster: + and ax, 0FFFh ; 读完需要与一下,因为高位是未定义的,防止ax值有误 + mov word [bp - 2], ax ; 这里用了一个技巧,这样在popa的时候ax也顺便更新了 + + popa + pop bp + ret + +;---------------------------------------------------------------------------- +; 函数名: StringCmp +;---------------------------------------------------------------------------- +; 作用: +; 比较 ds:si 和 es:di 处的字符串(比较长度为11,仅为loader.bin所用) +; 如果两个字符串相等ax返回1,否则ax返回0 +StringCmp: + push bp + mov bp, sp + pusha + + mov cx, 11 ; 比较长度为11 + cld ; 清位保险一下 +.STARTCMP: + lodsb ; ds:si -> al + cmp al, byte [es:di] + jnz .DIFFERENT + inc di + dec cx + cmp cx, 0 + jz .SAME + jmp .STARTCMP +.DIFFERENT: + mov word [bp - 2], 0 ; 这里用了一个技巧,这样在popa的时候ax也顺便更新了 + jmp .ENDCMP +.SAME: + mov word [bp - 2], 1 ; 下一步就是ENDCMP了,就懒得jump了 +.ENDCMP: + popa + pop bp + ret +;---------------------------------------------------------------------------- +; 这里就是真正的boot的处理函数了,boot实际上只做了一件事,将loader从磁盘里搬到内存指定位置 +; 如何将loader搬到内存中就需要文件系统里面的信息的帮助 +; 通过扫描根目录区中所有可能的目录项找到loader.bin对应的目录项 +; 然后根据目录项信息读入loader.bin的文件内容 +Main: + mov ax, cs ; cs <- 0 + mov ds, ax ; ds <- 0 + mov ss, ax ; ss <- 0 + mov ax, BaseOfLoader + mov es, ax ; es <- BaseOfLoader + mov sp, BaseOfStack ; 这几个段寄存器在Main里都不会变了 + + mov ah, 0 ; ┓ + mov dl, [BS_DrvNum] ; ┣ 硬盘复位 + int 13h ; ┛ + +; 下面在 A 盘的根目录寻找 LOADER.BIN +FindLoaderInRootDir: + mov ax, [RootDirSectorNow]; ax <- 现在正在搜索的扇区号 + mov bx, OffsetOfLoader ; es:bx = BaseOfLoader:OffsetOfLoader + mov cx, 1 + call ReadSector + + mov si, LoaderFileName ; ds:si -> "LOADER BIN" + mov di, OffsetOfLoader ; es:di -> BaseOfLoader:400h = BaseOfLoader*10h+400h + mov dx, 10h ; 32(目录项大小) * 16(dx) = 512(BPB_BytsPerSec) + +CompareFilename: + call StringCmp + cmp ax, 1 + jz LoaderFound ; ax == 1 -> 比对成了 + dec dx + cmp dx, 0 + jz GotoNextRootDirSector ; 该扇区的所有目录项都探索完了,去探索下一个扇区 + add di, 20h ; 32 -> 目录项大小 + jmp CompareFilename + +GotoNextRootDirSector: + inc word [RootDirSectorNow] ; 改变正在搜索的扇区号 + dec word [LeftRootDirSectors] ; ┓ + cmp word [LeftRootDirSectors], 0 ; ┣ 判断根目录区是不是已经读完 + jz NoLoader ; ┛ 如果读完表示没有找到 LOADER.BIN,就直接开摆 + jmp FindLoaderInRootDir + +NoLoader: + mov dh, 3 + call DispStr + jmp $ + +LoaderFound: ; 找到 LOADER.BIN 后便来到这里继续 + add di, 01Ah ; 0x1a = 28 这个 28 在目录项里偏移量对应的数据是起始簇号(RTFM) + mov dx, word [es:di] ; 起始簇号占2字节,读入到dx里 + mov bx, OffsetOfLoader ; es:bx = BaseOfLoader:OffsetOfLoader + +LoadLoader: + mov ax, dx ; ax <- 数据区簇号 + add ax, DeltaSectorNo ; 数据区的簇号需要加上一个偏移量才能得到真正的扇区号 + mov cx, 1 ; 一个簇就仅有一个扇区 + mov bp, ax ; ES:BP = 串地址 + mov cx, 10 ; CX = 串长度 + mov ax, 01301h ; AH = 13, AL = 01h + mov bx, 00f9h ; 页号为0(BH = 0) + mov dx, 01530 + int 10h ; 10h 号中断 + + jmp $ \ No newline at end of file diff --git a/target.xml b/target.xml new file mode 100644 index 0000000..71daae8 --- /dev/null +++ b/target.xml @@ -0,0 +1 @@ +i8086 diff --git "a/\345\256\236\351\252\214\344\272\214-v0.3.md" "b/\345\256\236\351\252\214\344\272\214-v0.3.md" new file mode 100644 index 0000000..395dd5f --- /dev/null +++ "b/\345\256\236\351\252\214\344\272\214-v0.3.md" @@ -0,0 +1,149 @@ +
+ 实验二 加载loader +
+ +
+ 谷建华 +
+ +
+ 2022-09-20 v0.3 +
+ +#### 实验目的 + +1. 学习从boot加载loader的过程. +2. 学习FAT12文件系统. + +#### 实验预习内容 + +1. FAT12的基本格式. +2. BIOS读硬盘扇区的调用的使用方法. +3. 硬盘LBA的地址编码. + +#### 实验内容以及步骤 + +**2022年试点班需要做1、2两个实验,第3个实验想做就做不强求** + +1. 编译构建硬盘启动盘,并挂载到虚拟机上,观察并证实执行流交给了loader + (1) 修改`loader.asm`,使之能够在虚拟机终端中显示`This is {学生自己的名字的拼音}’s boot`. + (2) 编译`boot.asm`和`loader.asm`,生成相应的二进制文件. + (3) 通过`dd`和`mkfs`命令制作一个文件镜像盘,挂载到`/mnt`文件夹,将loader二进制文件写入文件镜像后将boot写入第0个扇区. + (4) 将文件镜像盘作为硬盘启动盘挂载到虚拟机,运行虚拟机观察并记录现象. +2. 观察并记录文件所使用的簇号 + (1) 修改loader文件,能够像boot一样寻找loader.bin. + (2) 编写打印功能将簇号打印在终端里. + (3) 制作硬盘启动盘,运行虚拟机观察并记录现象. + (4) 使用`dd`命令创建一个大小为4KB,名字为`aA1.txt`的文件并写入`a.img`,修改loader寻找`aA1.txt`并打印簇号,运行虚拟机观察并记录现象(此项可能较难,好好利用gdb调试和`xxd a.img | less`查看镜像数据). +3. 修复损坏的镜像文件(自我提高内容,自己想做就做,不用写进报告,**禁止内卷**) + (1) 我们准备了一个未知的损坏了的镜像文件,其文件系统为FAT12.它的第一张FAT表受到了部分损坏,根目录区也被未知数据覆盖,但万幸的是其余部分均未损坏,你需要根据根据剩下的信息恢复出文件系统中的数据. + (2) 此实验很磨时间,很难,做的同学需要好好对照FAT的官方文档进行学习,需要编写一个C程序仔细分析其中的关键信息并一步步恢复,验证你恢复是否成功可以将它用`mount`挂载并查看里面的数据,里面有我们准备的彩蛋. +4. 结合参考代码,请尝试自己重写boot代码,完成系统引导和加载loader的实验. +#### 实验总结 + +1. 在boot阶段都完成了哪些主要功能?这些功能是如何实现的?如何在引导盘中查找文件loader.bin的? +2. 为什么FAT12在寻找下一个簇的时候要连续读两个扇区,请通过画图的方式画出边界的几种情况. +3. 如果把引导盘的格式换成FAT32,查找文件loader.bin的过程是什么? + +#### 实验参考 + +##### 从boot到loader + +在上一节成功制作了硬盘启动盘后,实际上512字节远远不能满足我们的需求,这么点随便写点啥就超出去了,在进入真正的内核kernel之前,要做的东西可不少,加载解析内核文件,进入保护模式,向bios询问设备信息(比如剩余内存,设备树)等等,所以需要一个程序loader作为中间缓冲,它不受文件大小512字节限制. + +在有了loader之后boot的任务一下子就轻松了很多了,它只需要加载loader,并将执行流交给loader即可,仅完成一项工作512字节还是能够应付的. + +假设我们有了loader程序,我们怎么进行加载?要知道在第一个实验里我们的硬盘就只有512字节大,往哪里放loader?一个简单的想法就是直接将loader直接接在boot后面,但是这种方式可扩展性极低,无法存放多个文件,文件信息无法很好的管理.为了解决了这个问题需要一个在磁盘上的持久化的数据结构管理文件信息,上世纪七八十年代微软这帮工程师整出了FAT文件系统,FAT12是第一代,如果有兴趣可以在[这里](https://www.bilibili.com/video/BV1oZ4y1t7ce)听听FAT的历史由来. + +利用FAT文件系统可以有效管理文件,boot作为FAT文件系统的一部分放进了磁盘第0个扇区,然后根据第0个扇区的文件系统信息通过bios中断(分CHS和LBA两种模式,预实验简化了读入方式为LBA)读取磁盘其余扇区,完成loader的加载. + +##### 系统镜像文件的创建 + +在编写了boot和loader两个文件后就需要准备一个镜像文件存放,其中boot写入镜像文件的第0个扇区,loader放进文件系统中存放,根据boot里面的信息规范需要创建一个1.44MB大的镜像,通过`dd`命令创建一个1.44MB大的镜像文件. + +```shell +# if (input file) 读取的文件,这里读取的文件是zero抽象文件,这个文件会输出无尽的0 +# of (output file) 输出的文件,这里输出的地方是a.img镜像文件 +# bs 一次读写的数据字节数 +# count 重复多少次读写 +dd if=/dev/zero of=a.img bs=512 count=2880 +# 在命令结束后可以用stat检查a.img的大小确认 +``` + +在创建好镜像文件后,它现在还不是一个FAT文件系统,这个可以通过`file`命令检查,创建文件系统linux里面有一个命令叫`mkfs`(make filesystem). + +```shell +mkfs.vfat a.img +``` + +`vfat`指定文件系统类型,尽管FAT系列有很多,但是根据官方文档,`mkfs`根据镜像文件的大小判断该是什么类型,这里会默认生成`FAT12`文件系统,可以通过`file`命令检查. + +在创建完文件系统后就该考虑怎么把loader放进去了,镜像文件可以通过`mount`命令进行挂载,挂载到一个文件夹之后,所有对镜像文件的操作可以抽象成对该文件夹的操作.所以可以通过`cp`命令将loader写入镜像文件. + +```shell +# mount要求sudo权限 +sudo mount a.img /mnt -o loop #挂载到根目录的mnt文件夹,-o loop可以暂时不去理会 +sudo cp ./loader.bin /mnt #将loader拷贝过去后文件镜像里就写入了loader的数据 +sudo umount /mnt #解除挂载,可能会失败,如果没成功需要多执行几次 +``` + +将loader写入文件镜像后最后一步就要将boot写入第0个扇区. + +```shell +# conv=notrunc 如果这句命令不加上的话a.img又变回512字节大小了 +# no truncate 不会在写文件前让a.img强制置回0字节 +dd if=boot.bin of=a.img bs=512 count=1 conv=notrunc +``` + +这样`a.img`就可以当做硬盘启动盘,放进qemu虚拟机模拟运行了. + +```shell +qemu-system-i386 \ +-boot order=c \ +-drive file=a.img,format=raw +``` + +##### 磁盘扇区的读写 + +在boot加载loader的过程中读取磁盘是靠着13h中断命令,通过中断让bios将磁盘中的数据读到内存中,读取方式有CHS(C:柱面,H:磁头,S:扇区)和LBA(Logical Block Addressing)两种.Orange教材用的是软盘CHS读取方式,读取一个扇区需要经过复杂的运算(柱面、磁道、扇区都要计算).这次使用了更简单的LBA读取方式,它只需要读入的逻辑扇区号就可以直接将磁盘上的文件读入,只需要传入一个结构体指针就可以进行读取. + +LBA读入方式使用的是bios的int 13h扩展读功能,扩展读功能的标识号ah=0x42,LBA最核心的是要往里面传入一个结构体参数,写成C语言的struct类似长这样: + +```c +struct buffer_packet { + short buffer_packet_size; + short sectors; + char *buffer; + long long start_sectors; + long long *l_buffer; +}; +``` + ++ `buffer_packet_size`:描述这个结构体的字节数(值为0x10或0x18,区别在于最后一个`l_buffer`是否启用).2字节 ++ `sectors`:从`start_sectors`开始需要连续读入的扇区数.2字节 ++ `buffer`:如果结构体大小为0x10,这个参数启用,描述接受磁盘数据的缓冲区地址.4字节,其高2字节为段寄存器值,其低2字节为偏移量 ++ `start_sectors`:磁盘中第几个逻辑扇区开始读入.8字节 ++ `l_buffer`:如果结构体大小为0x18,这个参数启用,描述接受磁盘数据的缓冲区地址.8字节. + +调用int 13h中断时的寄存器参数设置如下: + ++ ah=0x42 ++ dl=驱动号(模拟器里为硬盘镜像,驱动号为0x80) ++ ds:si=指向`buffer_packet`结构体的指针地址 + +通过调用int 13h将数据从持久化设备(磁盘)中读入到内存中,这样可以完成数据的加载. +##### 修复文件系统 + +修复文件系统说简单也简单,说难也难,如果不熟悉文件系统的话学习成本会巨高.在之前的实验,我们使用的是文件系统功能的子集,而修复文件系统需要知道它的全集,要不然从哪里修复都不知道,博客基本上是无法把所有东西讲清楚,所以**RTFM**是最有效的方法. + +当你脑中有一个设计知道怎么去做的时候,你又会发现第二个难题:你的实现能力跟不上了.在你们大学的前几年,可能说自己做了几个项目,但是实际情况上是用高级语言调用几个API就可以完成,难度在于建模,以及怎么跟数据打交道,并没有对底层有过太多的交互. + +大部分情况下我们都是在做计算型的编程,只要运算出来输出就完事了,而这次你需要独自面对系统,python这种高级语言可能会给你帮倒忙,因为它不太适合处理这种情况.你所能做的就只有拿着C/Cpp,以及一堆由glibc(比如printf,puts这些常用函数)包装好的API与内核进行交互. + +虽然同样是API,glibc的API相当的底层,需要`man`命令好好阅读相关函数调用的细节,不要惧怕英文.靠自己查博客绝对会把自己查晕,实现半天发现函数干的跟自己想的不一样 ~~(我曾经相信csdn)~~. + +在你们实现中会遇到另一个可怕的事情就是:struct结构体,如果你直接实现你会惊喜地发现可能结构体跟你想的不那么一致,~~不是语言不行,是你不懂语言~~. + +在实现时推荐大家使用一种防御性编程`assert(expr)`,就是当expr表达式的值为False时程序会及时奔溃并告诉你奔溃的位置.如果不想让自己的程序出太多离谱的错还要花大量时间尝试,就应该使用这种方式,在各种你觉得可能会出问题的地方加上一个`assert`. + +如果你通过一步步翻文档,一步步独立调错终于把镜像文件给恢复正确,你就能够看到我们精心准备的**彩蛋**,这给你的成就感会相当的大,如果会读手册,并有一定的实现能力你会发现你能干更多事,那些看上去很牛逼的玩意实际上你也能够实现. \ No newline at end of file