OS lab2 读取FAT12文件系统类型的镜像文件的二进制格式

本文介绍了FAT12文件系统的结构,包括引导扇区、FAT表、根目录的细节,以及如何读取和解析FAT12镜像文件。同时,讨论了C++和nasm联合编译的问题,以及在Linux下创建和操作FAT12镜像文件的方法。此外,还涉及了实模式和保护模式、选择子、描述符、GDT和LDT在内存管理中的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

操作系统课程的第二个实验是写一个main文件(c/cpp),读取一个FAT12文件系统格式的.img文件,并响应用户输入,然后进行输出。一个特殊要求是print函数不能调用库函数,需要用nasm写一个自定义的输出函数,实现gcc+nasm的联合编译。就网上的许多资料和学长的代码,写一个小总结。


镜像文件/映像文件

把外存上的数据和存储地址信息存储在一个文件内。镜像文件中包含许多信息,除了数据本身还包含系统文件、引导文件、分区表信息等,这样镜像文件就可以包含一个分区甚至是一块硬盘的所有信息。

FAT12文件系统

结构:引导扇区——>FAT1——>FAT2——>根目录区——>数据区

引导扇区

FAT12文件系统的引导扇区通常有512字节。这512字节包含了FAT文件系统的BIOS参数块(BPB)信息和启动代码,以及分区的引导记录或分区引导扇区(MBR)。其中BPB信息占据了前面的部分,并且它们的长度是固定的。而剩余的部分可以根据需要进行自定义编写,例如用于加载操作系统其他模块等。

BPB

FAT12文件系统的BPB是指BIOS参数块(BIOS Parameter Block),包含了FAT12分区的相关信息,如每个簇的扇区数目、保留扇区数、FAT表的数量等。BPB通常位于FAT12文件系统的引导扇区,并且其长度为25个字节。

以下是FAT12文件系统BPB中各个字段的详细说明:

  • BytesPerSector:每个扇区的字节数,占用2个字节。

  • SectorsPerCluster:每个簇所包含的扇区数目,占用1个字节。

  • ReservedSectors:保留扇区的数量,占用2个字节。

  • NumberOfFATs: FAT表的数量,占用1个字节。

  • RootEntries:根目录可以容纳的最大文件数,占用2个字节。

  • TotalSectors:分区中的总扇区数,如果这个值为0,则使用TotalSectors16字段的值,占用2个字节。

  • Media:介质描述符,占用1个字节。

  • SectorsPerFAT:每张FAT表所占的扇区数目,占用2个字节。

  • SectorsPerTrack:每个磁道所包含的扇区数目,占用2个字节。

  • Heads:磁头的数量,占用2个字节。

  • HiddenSectors:隐藏扇区的数量,占用4个字节。

因此,FAT12文件系统的BPB共计25个字节。

FAT表

详见书籍《ORANGE'S:一个操作系统的实现》4.1节

主要是为了找到一个大于512字节文件的所有的簇,每一个簇号处的值表示下一个簇所在的位置......这样找到这个文件内容所占有的所有的簇,再根据这些簇号到数据区去寻找相应的数据

根目录

在FAT12文件系统中,根目录位于分区的开头,通常紧跟在引导扇区和BIOS参数块后面,是一个固定大小的区域,用于存储文件和子目录信息。它的位置和大小在BPB中定义。

对于FAT12文件系统来说,根目录部分有以下特点:

1. 根目录所占用的簇数在BPB中有限制,一般情况下不会超过32个簇(也就是16KB),因此根目录最大容量为512个条目,每个条目占据32字节。这意味着,在FAT12文件系统中,根目录的大小始终固定为512字节的整数倍。

2. 每个根目录条目包含了文件或目录的元数据,如文件名、文件属性、时间戳和起始簇号等。文件名可以包含8个字符的文件名和3个字符的扩展名,总计11个字符。由于根目录条目长度固定为32字节,因此如果文件名长度小于11个字符,则剩余空间需要用空格填充。

也就是说,根目录存储了文件名、文件属性(是文件还是目录)、保留位、最后一次写入时间、最后一次写入日期、文件开始的簇号(如果文件大于512B可以根据这个簇号可以去FAT表查找这个文件所占据的所有簇号,再根据收集到的簇号到数据区查找)、文件大小


OK,知道了FAT12文件系统的原理,可以开始写代码了。参考学长代码:

FAT12文件系统镜像查看工具linux下的实现(nasm、g++联合编译_linux系统下如何制作fat12镜像文件_richardzzzZ的博客-CSDN博客

下面记录一下我对代码中一些疑惑的地方

CODE_QUESTION

BPB偏移11个字节

//BPB从偏移11个字节处开始

check = fseek(fat12, 11, SEEK_SET);

BPB(BIOS Parameter Block,BIOS参数块)是磁盘分区的元数据之一,它存储了分区的各种信息,如文件系统类型、分区大小、扇区大小等。

BPB 的结构和位置是由 FAT 文件系统的设计决定的。在 FAT 文件系统中,BPB 是位于启动扇区(Boot Sector)中的一个重要部分。启动扇区是指硬盘第一个扇区,用于引导计算机操作系统并加载文件系统。由于启动扇区始终是固定大小的,因此 BPB 的位置也是固定的。在 FAT12 和 FAT16 文件系统中,BPB 通常位于启动扇区的前面,而在 FAT32 文件系统中,则位于后面。

在 FAT12 和 FAT16 中,BPB 的长度为25个字节。其中,前11个字节被用作 Boot-Jump Instruction。Boot-jump指令是一个可选的汇编指令,在MS-DOS和Windows启动时用于跳转到启动区代码的执行点。因此,BPB 的实际信息从偏移量为11的位置开始。

FILE

`FILE` 是C标准库中用于表示文件流的结构体类型。它定义在 `stdio.h` 头文件中

其中包含了一些字段,这些字段记录了文件流的状态和位置等信息。

在使用C标准库中的函数对文件进行操作时,需要将打开的文件指针传递给这些函数。文件指针是指向 `FILE` 结构体的指针,通常使用 `fopen` 函数打开文件后获得。例如,`fread`、`fwrite`、`fgets` 和 `fputs` 等函数可以用于读写文件。

fseek函数详解

`fseek` 是C标准库提供的文件操作函数之一,用于设置文件指针的位置。它的函数原型如下:
c复制代码int fseek(FILE *stream, long offset, int whence);

其中,stream 是一个文件指针,表示需要设置位置的文件;offset 是参考位置,表示要设置的偏移量;whence 表示参考位置是从哪里开始计算的,可能有以下 3 种值:

- SEEK_SET:表示从文件的起始位置开始计算,偏移量为 offset;
- SEEK_CUR:表示从当前位置开始计算,偏移量为 offset;
- SEEK_END:表示从文件末尾位置开始计算,偏移量为 offset。

函数返回值为 0 表示成功,非零表示出现错误或者到达文件结尾等异常情况。

通过使用 `fseek` 函数,我们可以随时调整文件指针的位置,以方便进行文件的读写操作。例如,在读取文件时,读入了一部分数据,然后需要跳过一些内容才能继续读取,就可以使用 `fseek` 函数跳过指定的字节数,实现文件指针的快速定位。在写文件时,也可以使用 `fseek` 函数将文件指针定位到特定位置来执行插入、删除等操作。

下面是一个使用 `fseek` 函数修改文件指针的例子:

#include <stdio.h>

int main() {
    FILE *fp;
    char *filename = "test.txt";
    fp = fopen(filename, "r");
    if (fp == NULL) {
        printf("Failed to open file %s\n", filename);
    } else {
        printf("File %s opened successfully.\n", filename);
        // 将文件指针定位到文件末尾
        fseek(fp, 0L, SEEK_END);
        // 获取文件大小
        long size = ftell(fp);
        printf("The file size is %ld bytes.\n", size);
        fclose(fp);
    }
    return 0;
}

上面的代码中,先以只读方式打开名为 `"test.txt"` 的文件,并使用 `fseek` 函数将文件指针移到文件末尾位置。然后使用 `ftell` 函数获取当前文件指针的位置,即文件大小,并输出文件大小。最后关闭文件指针。

fread()函数

fread 是C标准库提供的文件操作函数之一,用于从文件中读取数据。它的函数原型如下:

size_t fread(void *ptr, size_t size, size_t count, FILE *stream);

其中,ptr 是一个指向内存块的指针,用于存储读取到的数据;size 是每个数据块的大小;count 是需要读取的数据块数量;stream 是一个文件指针,表示要读取的文件。

函数返回值是实际读取的数据块数量,如果出现错误或者到达了文件末尾,则返回值可能小于 count

通过使用 fread 函数,我们可以从文件中读取二进制数据,例如图片、音频、视频等文件,也可以读取文本文件等纯文本文件。在使用 fread 函数时,需要确保 ptr 指向的内存空间足够大,并且文件指针指向正确的位置。

下面是一个使用 fread 函数读取二进制文件的例子:

#include <stdio.h>
​
int main() {
    FILE *fp;
    char *filename = "test.bin";
    fp = fopen(filename, "rb");
    if (fp == NULL) {
        printf("Failed to open file %s\n", filename);
    } else {
        printf("File %s opened successfully.\n", filename);
        // 读取 10 个 int 型数据
        int buf[10];
        int cnt = fread(buf, sizeof(int), 10, fp);
        printf("%d integers read from file.\n", cnt);
        fclose(fp);
    }
    return 0;
}

上面的代码中,先以二进制读取方式打开名为 "test.bin" 的文件,并使用 fread 函数将其中的 10 个 int 型数据读入到数组 buf 中。最后输出实际读取的数据块数量,并关闭文件指针。

u32  BPB_TotSec32

if (bpb_ptr->BPB_FATSz16 != 0) {
		FATSz = bpb_ptr->BPB_FATSz16;
	}
	else {
		FATSz = bpb_ptr->BPB_TotSec32;
	}

BPB_TotSec32(Total Sectors)表示整个分区中的总扇区数,也就是文件系统占用的磁盘空间大小。如果BPB_FATSz16(FAT Size 16)为0,则该值表示FAT表所占用的扇区数,否则该值不起作用。在FAT32中,由于FAT表较大,无法用16位的值来表示,因此需要使用32位来表示FAT表的大小和簇号等信息。

如何判断文件属性

在FAT12文件系统中,可以通过根目录条目下文件名的属性位来判断一个条目是文件还是目录。具体来说,如果属性位的第4个比特位设置为1,则表示该条目是一个子目录(也就是一个目录);否则,它是一个文件。

根据文件名属性位的值,操作系统可以区分文件和目录,并决定采取何种方式读取或处理这些条目。对于目录条目,可以进一步访问子目录,并查找其中包含的其他文件或子目录。而对于文件条目,则需要按照固定长度从指定位置读取相应的数据块。通过这种方式,操作系统可以在FAT12文件系统中正确地管理和操作文件与目录。

(rootEntry_ptr->DIR_Attr & 0x10) == 0
rootEntry_ptr->DIR_Attr表示该条目的文件名
若上述表达式为真,说明该条目是一个文件

 数据区起始处

int dataBase = BytsPerSec * (RsvdSecCnt + FATSz * NumFATs + (RootEntCnt * 32 + BytsPerSec - 1) / BytsPerSec);//算的是数据区的起始字节处

= 每个扇区字节数 * (引导扇区数 + FAT扇区数 + 根目录扇区数)
其中根目录扇区数 = (根目录区条目数 * 32 + 每个扇区字节数-1) / 每个扇区字节数
其中+ 每个扇区字节数-1是为了保证此公式在根目录区无法填满整数个扇区时仍然成立

簇号

在FAT12文件系统中,簇号小于0xFF8表示该簇是一个可用的数据簇,可以被用来存储文件的数据。当读取文件内容时,操作系统会根据文件的FAT表中记录的簇号来定位文件的数据簇,然后读取该簇中的数据。如果簇号小于0xFF8,那么该簇就是一个有效的数据簇,可以被用来存储文件的数据。如果簇号大于等于0xFF8,那么该簇就是一个特殊的簇号,用于表示文件的结束或者空闲簇的开始。

根据当前簇号获取下一个要读的簇号

在FAT12文件系统中,每个簇的FAT表项占用12位,其中前4位为保留位,后12位用来存储下一个数据簇的簇号。因此,要获取下一个要读取的簇号,需要先找到当前簇的FAT表项,然后解析出其中存储的下一个簇号。

具体来说,假设当前簇的簇号为n,那么可以通过以下步骤获取下一个要读取的簇号:

1. 计算当前簇的FAT表项的偏移量。偏移量可以通过以下公式计算:offset = 1.5 * n。
2. 读取FAT表中偏移量为offset的两个字节,其中包含了下一个簇号的信息。
3. 根据FAT表项的结构,解析出下一个簇号。具体来说,如果偏移量为偶数,则下一个簇号为FAT表项的低12位;如果偏移量为奇数,则下一个簇号为FAT表项的高4位和低8位组成的12位。

需要注意的是,如果下一个簇号为0xFF7,那么表示当前簇是文件的最后一个簇;如果下一个簇号为0xFF8或者更大,那么表示当前簇是文件的结束或者空闲簇的开始,不应该再继续读取下一个簇。

fread()是小端存储

这决定了读簇号的时候,需要获取高位还是低位

存储数据

需要malloc一个空间存储读到的一个个字节,同时指向这个空间的指针也得移动,使得字节按顺序存储下去。这时候需要两个指针指向这个空间,一个用于移动位置,一个用于使用完毕空间以后free空间:

char* str = (char*)malloc(SecPerClus * BytsPerSec);    //暂存从簇中读出的数据
char* content = str;
...
check = fread(content, 1, SecPerClus * BytsPerSec, fat12);
...
for (int i = 0; i < count; i++) {//读取赋值
            *p = content[i];
            p++;
        }
free(str);

split() 

 split(input, input_list, ' '); 
for (auto it = input_list.begin(); it != input_list.end();) 
{//删除数组中空格产生的空位置 
if (*it == "") 
{ it = input_list.erase(it); } 
else { it++; } 
}//为什么需要去除空位置

 一开始我疑惑split不是已经分割了吗,为什么还需要去除空格。原来是用法不同

这段代码的目的是从输入的字符串input中分离出不包含空格的单词,并将它们存储到input_list这个容器中。由于split操作会生成一些空单词(比如"hello world"会被分成"hello"和""和"world"),因此需要去除空位置以避免影响后续对input_list的处理。具体来说,这可以确保:

1. 程序不会错误地将空单词作为有效单词进行处理;
2. 可以减少程序需要处理的空元素数量,从而提高效率。

注意区别split()和str.split()的用法

\033[31m

\033[31m 是一个控制台输出的格式控制符号,用于设置输出文本的颜色。它是一种ASCII转义序列,由以下几部分组成:

\033:表示转义序列的开始部分,后面的字符将被解释为控制序列;
[:表示控制序列的开始部分;
31:表示要设置的颜色代码,这里的31表示红色,默认值是0(表示黑色);
m:表示控制序列的结束部分。
因此,使用\033[31m可以将输出文本的颜色设置为红色,例如:

cout << "\033[31mThis text will be printed in red.\033[0m" << endl;
在这个例子中,将输出"This text will be printed in red.",并将其设置为红色。需要注意的是,在输出颜色后,应该使用"\033[0m"重置颜色设置,否则后续输出文本也会保持红色,直到另一个颜色设置被应用为止。


好啦~~~代码解释部分终于结束了!接下来需要实现gcc+nasm的联合编译

编译

Makefile

网上有很多学习资料  w >_< w

error

在编译时遇到了一些报错:

报错:

@ubuntu:~/HM/lab2$ make
nasm -f elf32 my_print.asm
g++ -m32 main.cpp my_print.o -o main
make: g++: Command not found
make: *** [Makefile:3: Main] Error 127

需要安装g++:

sudo apt-get update
sudo apt-get install g++
​

报错:

@ubuntu:~/HM/lab2$ make
nasm -f elf32 my_print.asm
g++ -m32 main.cpp my_print.o -o main
In file included from main.cpp:1:
/usr/include/stdio.h:27:10: fatal error: bits/libc-header-start.h: No such file or directory
   27 | #include <bits/libc-header-start.h>
      |          ^~~~~~~~~~~~~~~~~~~~~~~~~~
compilation terminated.
make: *** [Makefile:3: Main] Error 1
​

需要安装g++:

sudo apt-get install g++-multilib

报错:

fatal error: bits/libc-header-start.h: No such file or directory

需要安装gcc:

sudo apt-get install gcc-multilib

重装系统是真的写的想毁灭世界!!!烦死了!(猴哥附体)

(在编译之前应该先创建一个镜像文件)

创建FAT12镜像文件并向其中写入文件

  1. 使用 dd 命令创建一个指定大小的空文件,例如:

    复制代码$ dd if=/dev/zero of=my_fat12_image.img bs=1M count=10

    这将创建一个名为 my_fat12_image.img 的 10MB 空文件。

  2. 使用 mkfs.fat 命令在该文件上创建一个 FAT12 文件系统,例如:

    复制代码$ sudo mkfs.fat -F 12 my_fat12_image.img
  3. 检查并记录该 .img 文件所使用的块大小(通常为 512B):

    复制代码$ sudo parted my_fat12_image.img unit B print
  4. 创建一个目录并将 FAT12 文件系统镜像文件挂载到该目录中:

    复制代码$ mkdir my_mount_point
    $ sudo mount -o loop,offset=$((block_size*start_block)) my_fat12_image.img my_mount_point

    其中,block_sizestart_block 分别是镜像文件系统的块大小和其在 .img 文件中的起始块。

  5. 现在,您可以向该目录添加文件、复制文件等来写入 FAT12 文件系统:复制代码$ cp your_file fat12_dir/

  6. 写入完成后,卸载镜像文件系统:

    复制代码$ sudo umount my_mount_point

请注意,在创建和使用 FAT12 文件系统时,请确保遵循该文件系统的限制,如文件名长度、文件大小等。否则,可能会导致写入操作失败或文件无法正常读取。

提问

经典之“教的和作业写的和提问的不是一样东西”,诶嘿,虽然上课不讲,但是作业布置下去你们得做吧,这样怎么不算学到了该学的呢......原地爆炸ing!!

1. 什么是实模式,什么是保护模式?

实模式:基地址 + 偏移量可以直接获得物理地址的模式
缺点:非常不安全
保护模式:不能直接拿到物理地址
需要进行地址转换
80286 开始,是现代操作系统的主要模式

2. 什么是选择子?

选择子共 16 位,放在段选择寄存器里
2 位表示请求特权级
3 位表示选择 GDT 还是 LDT 方式
13 位表示在描述符表中的偏移
故描述符表的项数最多是 2 13 次方


3. 什么是描述符?

1. 保护模式下引入描述符来描述各种数据段

2. 所有的描述符均为8个字节,由第5个字节说明描述符的类型

3. 类型不同,描述符的结构也有所不同。

描述符是GDT和LDT表中的一个数据结构项,用于向处理器提供有关一个段的位置和大小信息以及访问控制的状态信息。 通常由编译器,链接器,加载器,或操作系统或执行体,但不由应用程序创建。 每个段描述符的长度是8字节,含有3个主要字段:段基地址、段限长和段属性。


4. 什么是GDT,什么是LDT?

GDT :全局描述符表,是全局唯一的。
存放一些公用的描述符,和包含 各进程局部描述符表 首地址的描述符。
LDT :局部描述符表,每个进程都可以有一个。
存放本进程内使用的描述符。


5. 请分别说明GDTR和LDTR的结构。

GDTR 48 位寄存器,高 32 位放置 GDT 首地址,低 16 位放置 GDT 限长
限长决定了可寻址的大小,注意低 16 位放的不是选择子
LDTR 16 位寄存器,放置一个特殊的选择子,用于查找当前进程的 LDT 首地址。


6. 请说明GDT直接查找物理地址的具体步骤。

1. 给出段选择子(放在段选择寄存器里 )+ 偏移量
2. 若选择了GDT方式,则从GDTR获取GDT首地址,用段选择子中的13位做偏  移,拿到GDT中的描述符
3. 如果合法且有权限,用描述符中的段首地址加上 1. 中的偏移量找到物理地址 ,   寻址结束


7. 请说明通过LDT查找物理地址的具体步骤。

1. 给出段选择子(放在段选择寄存器中 )+ 偏移量
2. 若选择了LDT方式,则从GDTR获取GDT首地址,用LDTR中的偏移量做偏移,拿到GDT中的描述符1
3. 从描述符1中获取LDT首地址,用段选择子中的13位做偏移,拿到LDT中的描述符2
4. 如果合法且有权限,用描述符2中的段首地址加上 1. 中的偏移量找到物理地址。寻址结束


8. 根目录区大小一定么?扇区号是多少?为什么?

不⼀定,需要根据根目录项的最大个数计算

19 号扇区,前面有 1 个引导扇区,2 个占 9 扇区的文件配置表

- 1(引导扇区) + 9(FAT1) + 9(FAT2) = 19


9. 数据区第一个簇号是多少?为什么?

2 号

在1.44M软盘上,FAT前三个字节的值是固定的0xF0、0xFF、0xFF,用于表示这是一个应用在1.44M软盘上的FAT12文件系统。(前三字节对应序号为0和1的FAT表项)

本来序号为0和1的FAT表项应该对应于簇0和簇1,但是由于这两个表项被设置成了固定值,簇0和簇1就没有存在的意义了,所以数据区就起始于簇**2**


10. FAT表的作用?

记录硬盘中有关文件如何被分散存储在不同扇区的信息

每12位是一个FAT项(FATEntry),FAT 项的值代表文件的下⼀个簇号

11. 解释静态链接的过程。

1. 空间和地址分配
2. 符号解析和重定位
   1. 符号解析主要使用elf里面的**符号表节**来完成
   2. 重定位由两部分组成:
      1. 重定位节和符号定义
      2. 重定位节中的符号引用


12. 解释动态链接的过程。

1. **动态链接器自举**

动态链接器是一个不依赖其他共享对象的共享对象,需要完成自举。

2. **装载共享对象**

将可执行文件和链接器自身的符号合并成为全局符号表,开始寻找依赖对象。加载对象的过程可以看做图的遍历过程;新的共享对象加载进来后,其符号将合并入全局符号表;加载完毕后,全局符号表将包含进程动态链接所需全部符号。

3. **重定位和初始化**

链接器遍历可执行文件和共享对象的重定位表,将它们GOT/PLT中每个需要重定位的位置进行修正。完成重定位后,链接器执行.init段的代码,进行共享对象特有的初始化过程(例如C++里全局对象的构造函数)。

4. **转交控制权**

完成所有工作,将控制权转交给程序的入口开始执行。


13. 静态链接相关PPT中为什么使用ld链接⽽不是gcc?

gcc是工具链,除了具备基本的c文件编译功能外,还把其它工具的功能也集成了进来,比如as的汇编功能,ld的链接功能。所以gcc可以通过-Wl, option,将option传给链接器ld

所以使用ld时只链接声明的库,可以避免gcc进行glibc等额外内容的链接


14. linux下可执行文件的虚拟地址空间默认从哪里开始分配。

0x8048000


实验相关内容
1. BPB指定字段的含义
2. 如何进⼊子目录并输出(说明方法调用)
3. 如何获得指定文件的内容,即如何获得数据区的内容(比如使用指针等)
4. 如何进行C代码和汇编之间的参数传递和返回值传递
5. 汇编代码中对I/O的处理方式,说明指定寄存器所存值的含义

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值