2020301918-os/实验二-v0.3.md
2022-09-20 20:02:21 +08:00

10 KiB
Raw Permalink Blame History

实验二 加载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.asmloader.asm,生成相应的二进制文件. (3) 通过ddmkfs命令制作一个文件镜像盘,挂载到/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是第一代,如果有兴趣可以在这里听听FAT的历史由来.

利用FAT文件系统可以有效管理文件,boot作为FAT文件系统的一部分放进了磁盘第0个扇区,然后根据第0个扇区的文件系统信息通过bios中断分CHS和LBA两种模式,预实验简化了读入方式为LBA读取磁盘其余扇区,完成loader的加载.

系统镜像文件的创建

在编写了boot和loader两个文件后就需要准备一个镜像文件存放,其中boot写入镜像文件的第0个扇区,loader放进文件系统中存放,根据boot里面的信息规范需要创建一个1.44MB大的镜像,通过dd命令创建一个1.44MB大的镜像文件.

# 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里面有一个命令叫mkfsmake filesystem.

mkfs.vfat a.img

vfat指定文件系统类型,尽管FAT系列有很多,但是根据官方文档,mkfs根据镜像文件的大小判断该是什么类型,这里会默认生成FAT12文件系统,可以通过file命令检查.

在创建完文件系统后就该考虑怎么把loader放进去了,镜像文件可以通过mount命令进行挂载,挂载到一个文件夹之后,所有对镜像文件的操作可以抽象成对该文件夹的操作.所以可以通过cp命令将loader写入镜像文件.

# 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个扇区.

# 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虚拟机模拟运行了.

qemu-system-i386               \
-boot order=c                   \
-drive file=a.img,format=raw
磁盘扇区的读写

在boot加载loader的过程中读取磁盘是靠着13h中断命令,通过中断让bios将磁盘中的数据读到内存中,读取方式有CHSC:柱面,H:磁头,S:扇区和LBALogical Block Addressing两种.Orange教材用的是软盘CHS读取方式,读取一个扇区需要经过复杂的运算(柱面、磁道、扇区都要计算).这次使用了更简单的LBA读取方式,它只需要读入的逻辑扇区号就可以直接将磁盘上的文件读入,只需要传入一个结构体指针就可以进行读取.

LBA读入方式使用的是bios的int 13h扩展读功能,扩展读功能的标识号ah=0x42,LBA最核心的是要往里面传入一个结构体参数,写成C语言的struct类似长这样

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.

如果你通过一步步翻文档,一步步独立调错终于把镜像文件给恢复正确,你就能够看到我们精心准备的彩蛋,这给你的成就感会相当的大,如果会读手册,并有一定的实现能力你会发现你能干更多事,那些看上去很牛逼的玩意实际上你也能够实现.