实验要求

实现一个FAT12镜像查看工具。本次实验重点在于:熟悉掌握FAT12文件系统、gcc+nasm联合编译,了解实模式与保护模式的基本内容。

用C/C++和nasm编写一个FAT12镜像查看工具,读取一个.img格式的文件并响应用户输入。

功能列表

  1. 运行程序后,读取FAT12镜像文件,并提示用户输入指令
  2. 用户输入ls 路径,输出根目录及其子目录的文件和目录列表。
  3. 首先输出路径名,加一个冒号:,换行,再输出文件和目录列表;
  4. 使用红色(\033[31m)颜色输出目录的文件名,不添加特殊颜色输出文件的文件名。
  5. 当用户不添加任何选项执行ls命令时,每个文件/目录项之前用两个空格隔开
  6. 当用户添加-l为参数时,
  7. 在路径名后,冒号前,另输出此目录下直接子目录和直接子文件的数目,两个数字之间用空格连接。此两个数字不添加特殊颜色
  8. 每个文件/目录占据一行,在输出文件/目录名后,空一格,之后:
  9. 若项为目录,输出此目录下直接子目录和直接子文件的数目,两个数字之间用空格连接。此两个数字不添加特殊颜色
  10. 不输出.和…目录的子目录、子文件数目
  11. 若项为文件,输出文件的大小
  12. 对于-l参数用户可以在命令任何位置、设置任意多次-l参数,但只能设置一次文件名
  13. 直接子目录不计算.和…
  14. 当用户给出不支持的命令参数时,报错
  15. 当用户不设定路径时,默认路径为镜像文件根目录
  16. 用户输入cat 文件名,输出路径对应文件的内容, 若路径不存在或不是一个普通文件则给出提示,提示内容不严格限定,但必须体现出错误所在。
  17. 用户输入exit, 退出程序。

实现思路

读取指令略去,先简单记录一下如何查找目录及文件:

FAT12 根据路径字符串查找目录及文件

对于字符串将其按/分割,根据需求处理...的问题。然后调用searchTargetDir(target, des),函数会搜索并修改target的值,获得在数据区的位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
// 19-32号扇区为根目录文件项
//从根目录文件项起始位开始向后寻找目录
int searchTargetDir(int target, string des)
{
while (true)
{
if (file[target] == 0)
{
break;
}
int flag = 0;
for (int i = 0; i < 11; i++) // 0-10B是名字,文件的话,8-10B是后缀
{
if (!isTrue(file[target + i]))
{
target += 32; //每个目录项为32B,跳到下一项
flag = -1;
break;
}
}
if (flag == -1)
{
continue;
}
if (file[target + 11] == 0x10) // 11B为属性项:15——长目录项,可跳过;16:文件夹;32:文件
{
string m = "";
for (int i = 0; i < 11; i++)
{
if (file[target + i] != 0x20)
{ //不是文件则为路径
m = m + char(file[target + i]);
}
}
if (m == des)
{ // 33号扇区往后为数据区,26-27B表示在数据区的簇号
return 512 * 33 + (file[target + 26] - 2) * 512; // 数据区起始地址对应编号为2
}
}
target += 32;
}
return -1;
}

int searchTargetFile(int target, string name)
{
while (true)
{
if (file[target] == 0)
{
break;
}
int flag = 0;
for (int i = 0; i < 11; i++)
{
if (!isTrue(file[target + i]))
{
target += 32;
flag = -1;
break;
}
}
if (flag == -1)
{
continue;
}
if (file[target + 11] == 0x20) // 11B属性,0x20代表文件
{
string m = "";
for (int i = 0; i < 8; i++) // 0-7B名字
{
if (file[target + i] != 0x20)
m = m + char(file[target + i]);
}
if (file[target + 8] != 0x20)
{
m = m + ".";
}
for (int i = 8; i < 11; i++) // 8-10B:.后缀
{
if (file[target + i] != 0x20)
m = m + char(file[target + i]);
}
if (m == name)
{
return 512 * 33 + (file[target + 26] - 2) * 512;
}
}
target += 32;
}
return -1;
}

实现cat:读取文件内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int cluster = target / 512 - 33 + 2; //计算簇数,从33号扇区开始,起始对应编号为2
while (true)
{
int i = 0;
string str = "";
for (i = 0; file[target + i] != 0 && i < 512; i++)
{
char c = (char)file[target + i];
str = str + c;
}
printSentence(str, true);
if (i != 512)
{ //文件结束
break;
}
if (cluster % 2 == 0)
{ // FAT12: 12位代表一个FAT项,2个FAT占3个字节
int fat12 = 512 + cluster / 2 * 3;
cluster = ((file[fat12 + 1] & 0x0F) << 8) | file[fat12]; //左移8位(低位补零),计算簇数
}
else
{
int fat12 = 513 + cluster / 2 * 3;
cluster = (file[fat12 + 1] << 4) | (file[fat12] >> 4 & 0x0F);
}
target = 512 * 33 + (cluster - 2) * 512; //重新计算tgt
}

cluster = target / 512 - 33 + 2;计算簇数,从33号扇区开始,起始对应编号为2,在一个簇读取完毕时,需要到下一个簇区,由于FAT12中12位代表一个FAT项,2个FAT占3个字节,故需要分奇偶来处理。

问答题

课件相关

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

实模式使用基地址加偏移量的方式就可以直接拿到物理地址的模式。

保护模式是不能直接拿到物理地址的模式,需要进行地址转换。

2.什么是选择子?

选择子总共16位,存放在段选择寄存器中,低2位表示请求特权级,第3位表示选择GDT方式还是LDT方式,高13位表示在描述符表中的偏移。

3.什么是描述符?

保护模式下引入描述符来描述各种数据段,所有的描述符均为8个字节(0-7),由第5个字节说明描述符的类型。类型不同,描述符的结构也有所不同。

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.根目录区大小一定吗?扇区号是多少?为什么?

不一定,根目录区位于第二个FAT表之后,开始的扇区号为19,它由若干个目录条目(Directory Entry)组成,条目最多有BPB_RootEntCnt个。由于根目录区的大小是依赖于BPB_RootEntCnt的,所以长度不固定,根目录区中的每一个条目占用32字节。(BPB_RootEntCnt即根目录文件数(条目数)最大值)

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

需要根据根目录大小RootDirSectors计算,数据区DataSectors=RootDirSectors+引导扇区(1)+FAT1(9)+FAT2(9),一般值是33。

数据区的第一个簇是2,因为1.44M的软盘上,FAT前三个字节的值是固定的0xF0、0xFF、0xFF用于表示这是一个应用在1.44M软盘上的FAT12文件系统。本来序号为0和1的FAT表项应该对应簇0和簇1,但是这两个表项都被设置为固定值,簇0和簇1没有存在意义。

10.FAT表的作用?

文件分配表被划分为紧密排列的若干个表项,每个表项都与数据区中的一个簇相对应,而且表项的序号也是与簇号一一对应。

11.解释静态链接的过程

静态链接是指在编译阶段直接把静态库加入到可执行文件中,这样可能导致可执行文件比较大。

  1. 空间与地址的分配:每个.o文件都有自己的段属性,比如.text和.data等,链接的第一步就是将这些段属性合并在一起。

  2. 符号解析和重定位:重定位(修正地址)、重定位表(相对地址修正)

12.解释动态链接的过程

动态链接是指链接阶段仅仅加入一些描述信息,而程序执行时再从系统中把相应动态库加载到内存中去。

  1. 动态链接器自举

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

  2. 装载共享对象

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

  3. 重定位和初始化

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

  4. 转交控制权

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

13.静态链接为什么使用ld链接而不是gcc

gcc工具链包含很多工具,其中用于链接的是ld,ld是binutils工具集的底层部件,所以工作在汇编级别。

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

0x08048000

实验相关问题

1.BPB指定字段的含义

名称 偏移 长度 内容
BS_jmpBoot 0 3 一个短跳转指令
BS_OEMName 3 8 厂商名
BPB_BytsPerSec 11 2 每扇区字节数
BPB_SecPerClus 13 1 每簇扇区数
BPB_RsvdSecCnt 14 2 Boot记录占用多少扇区
BPB_NumFATs 16 1 共有多少FAT表
BPB_RootEntCnt 17 2 根目录文件数最大值
BPB_TotSec16 19 2 扇区总数
BPB_Media 21 1 介质描述符
BPB_FATSz16 22 2 每FAT扇区数
BPB_SecPerTrk 24 2 每磁道扇区数
BPB_NumHeads 26 2 磁头数(面数)
BPB_HiddSec 28 4 隐藏扇区数
BPB_TotSecc32 32 4 若BPB_TotSec16为0,由这个值记录扇区
BS_DrvNum 36 1 中断13的驱动器号
BS_Reservedl 37 1 未使用
BS_BootSig 38 1 扩展引导标记
BS_VolID 39 4 卷序列号
BS_VolLab 43 11 卷标
BS_FileSysType 54 8 文件系统类型
引导代码及其他 62 448 引导代码、数据、其他填充字符
结束标志 510 2 0xAA55

2.如何进行C代码和汇编之间的参数传递和返回值传递?

参数传递,可以通过栈esp完成,C调用时的函数参数会被放置在esp中,然后汇编从esp+4开始,逐一拿出参数。

汇编语言的返回值,放置在eax中进行返回。