FAT32文件系统学习(3) —— 数据区(DATA区)
今天继续学习FAT32文件系统的数据区部分(Data区)。其实这一篇应该是最有意思的,我们可以通过在U盘内放入一些文件,然后在程序中读取出来;反过来也可以用程序在U盘内写入一下数据,然后在windows下可以看到写入的文件。这些笔者都会在这篇文章中演示(后来发现并没有成功,不过笔者也找到相关的原因,详见后来的更新部分吧:) )。同时,在写这篇文章的时候笔者也发现了许多意想不到的规律。
1、本文目录
2、读取根目录
两张FAT之后的所有扇区都是数据区部分。我们再通过图1来回顾一下整个FAT32文件系统的分布规则。
图 1 FAT32文件系统分布图
通常情况下根目录都是位于数据区头部的,前面也提到过,有文献上说是因为一旦U盘格式化完毕之后,根目录就创建好了。本着探究的精神,笔者尝试格式化了一下U盘,发现其实并没有创建根目录。不过一旦有文件操作,马上就会创建根目录,因为这时整个数据区都是空的,所以自然是写入数据区的头部。到头来其实道理是一样的,也就是说根目录一般情况下都是在数据区的头部(第2簇)。
- 数据区偏移计算
经过前两篇关于BPB和FAT部分学习之后,我们就可以计算出数据区头部的偏移:
数据区偏移 = (保留扇区数 + FAT表扇区数 * FAT表个数(通常为2) + (起始簇号-2) * 每簇扇区数) * 每扇区字节数
笔者首先格式化了U盘,通过偏移读取出了数据区的头部,发现都是0x00。
- 题外话
这里又要插一些题外话了,笔者试着改了一下U盘的卷标,把它改名为“FAT”。然后还记得BPB当中有一个参数叫做卷标吗?笔者看了下发现卷标这个参数还是“NO NAME”并没有改变。这时笔者把数据区的头部读取了出来,如图2所示:
图2 卷标
原来被写在了这里。最后经笔者的测试,卷标最长长度是11个字节,偏移从0x00~0x0A,而偏移0x0B处的值0x08值的意思就是卷标(关于此处值的意思相面还会详细描述)。因为这个U盘其实还没有写入过任何数据,所以卷标才会显示在数据区的开头,但是如果换种情况呢,文件系统又是如何找到卷标的呢?笔者查阅了相关资料后发现,卷标一般都是写在根目录的下的,如果发现根目录项的其中一项其0x0B偏移处的值为0x08那么读取该项的前11个字节即为卷标。
3、短文件名目录项
- 短文件名目录项参数
好,回到正题。先来讲一下理论的东西:目录区是由一个个目录项构成,类似于FAT表。其中每一个目录项占用32个字节,可以是代表长文件名目录项、文件目录项、子目录项等。对于短文件名格式的目录项,其参数的含义如表1所示(不会画这种表,从别处引用了一个)[1]:
表1 FAT32短文件名目录项参数表
- 参数解释
用一个实际的例子来解释一下这些参数的意思,首先创建一个短文件名文件,如“file1.txt”,读取根目录,如图3所示:
图3 file1.txt 短文件名目录项
先不管其他数据,我们来看一下红色矩形框部分的数据,它就是一个短目录项。我们来一个个对比表1的参数进行说明:
字节偏移 | 参数含义 | 值 |
0x00~0x07 | 文件名 |
对应字符串“FILE1” |
0x08~0x0A | 后缀名 |
对应字符串“TXT” |
0x0B | 属性字节 |
0x20 = 00100000(2进制) 表示归档 |
0x0C | 系统保留 | 无 |
0x0D | 创建时间的10毫秒位 | 88,即0x88 * 10ms = 1360ms(10进制) |
0x0E~0x0F | 文件创建时间 |
0x785C = (0111100001011100)(2进制) 即为 15:02:57(注释1) |
0x10~0x11 | 文件创建日期 |
0x4508 = (0100010100001000)(2进制) 即为 2014/8/8(注释2) |
0x12~0x13 | 文件最后访问日期 |
0x4508 = (0100010100001000)(2 进制) 即为 2014/8/8 算法参考创建日期 |
0x14~0x15 | 文件起始簇号高16位 |
0x0000,可以用来计算出文件实际内容的偏移值, 这个放到后面单独计算。 |
0x16~0x17 | 文件最近修改时间 |
0x7869 = (0111100001101001)(2进制) 即为 15:03:18 算法参考创建时间 |
0x18~0x19 | 文件最近修改日期 |
0x4508 = (0100010100001000)(2进制) 即为 2014/8/8 算法参考创建日期 |
0x1A~0x1B | 文件起始簇号低16位 | 0x0005 |
0x1C~0x0F | 文件长度 | 0x0000000C = 12 |
表2 file1.txt 参数解释
注释1:01111 000010 11100
1)这里高5位代表小时,由于2^5 = 32,足够表示24小时,这边01111(2进制) = 15(10进制);
2)次6位代表分钟,同理2^6 = 64,足够表示60分钟,这边000010(2进制) = 2;
3)低5位表示秒的1/2, 计算结果需要加上毫秒位上的进位,这边11100(2进制) = 28(10进制),所以秒数 = 28*2 = 56,再加上毫秒上的进位1所以结果为57。
注释2:0100010 1000 01000
1)这里高7位代表从1980年开始的年数,笔者计算了下可以到2108年,总之还有90多年可以使用,这边0100010(2进制) = 34,所以年份 = 1980+34 = 2014;
2)次4位代表月份,2^4=16,可以表示12个月份,这边 1000(2进制) = 8(10进制);
3)低5位代表日期,2^5 = 32,可以表示28~31天,这边 01000(2进制) = 8(10进制)。
这样除了文件起始簇号字段,其他字段的意思和计算方法都弄清楚了。下面来看一下文件起始簇号,首先根据高低各16位,计算出完整的文件起始簇号 = 0x00000005 ,文件起始地址偏移的计算:
文件起始地址 = (保留扇区数 + FAT表扇区数 * FAT表个数(2) + (文件起始簇号-2)*每簇扇区数)*每扇区字节数
本例中计算结果为0x4010,然后到这个地址读取内容并切入到磁盘文件中(详细操作参考第一篇文章),如图4所示,windows下打开内容如图5所示:
图4 图5 文件内容
这个时候再去看一下FAT表的5号簇,计算方式在上一篇当中,结果如图6所示:
图 6 FAT表5号表项
红色矩形框的位置就是5号簇的位置,可以看到值0x0FFFFFFF,意思就是文件在这一簇结束了。 (具体不同数值的含义详见上一篇)。如果这里文件大小超过1簇,那么这个值应该是下一簇的簇号,继续读取下一簇的内容即可。虽然我们知道了文件占用的空间是1簇,但是怎么知道文件具体的大小呢?再回过头来看上面的短文件目录项,最后一个属性是文件长度,上面已经计算得到为12,“Hello World!”的长度正好是12 :)。
至此短文件目录项应该已经分析的差不多了。
4、长文件名目录项
- 长文件名目录项参数
下面是长文件名目录项,笔者思考了好久该怎么把它讲清楚,毕竟理解是一回事,讲清楚就是另一回事了。
在讲长文件目录项之前先来说一下FAT32的一个很重要的特性,支持长文件名。长文件名也是记录在目录项当中的,区别与短目录项的是,前者可能会占据好几个目录项。为了兼容低版本的OS或程序能正确读取长文件名文件,系统自动为所有长文件名文件创建了一个对应的短文件名,使对应数据既可以用长文件名寻址,也可以用短文件名寻址。不支持长文件名的OS或程序会忽略它认为不合法的长文件名字短,而支持长文件名的OS或程序则会以长文件名为显式项来记录和编辑,并隐藏起短文件名[2]。
当创建一个长文件名文件时,系统会自动加上对应的短文件名,其原则如下:
(1)、取长文件名的前6个字符加上"~1"形成短文件名,扩展名不变。
(2)、如果已存在这个文件名,则符号"~"后的数字递增,直到5。
那么系统是如何判断当前目录项是短文件名目录项呢还是长文件名目录项,这里关键是看目录项的第12个字节的值,如果为0x0F时则系统认为是长目录项。而如果是旧版本的系统看到第12个字节是0x0F则认为是异常而忽略掉。这里可以回过头去看一下短文件名目录项,第12个字节是文件属性字节,0x0F即为全1是无效的,所以系统认为是异常。系统将长文件名以13个字符为单位进行切割,每一组占据一个目录项。所以可能一个文件需要多个目录项,这时长文件名的各个目录项按倒序排列在目录表中,以防与其他文件名混淆。
这样讲可能还是很抽象,先来看一下长文件名目录项的参数,如表3所示[1]:
表3 长文件名目录项参数表
- 参数解释
然后还是以一个实际的例子来说明,在根目录区读入一个长文件名目录项,如图7所示:
图7 长文件名目录项
图中选定部分即为多个长文件名目录项。我们来慢慢分析。系统在读入一个目录项的时候首先查看它的第12个字节,发现是0x0F,所以认为这是一个长文件名目录项。我们来看长文件名目录项的参数,如表4所示:
偏移 | 字段含义 | 值 |
0x00 | 属性字节位 | 0x42 = (01000010)(2进制)(注释1) |
0x01~0x0A | 10个字节的Unicode码 | 即字符串”ename” (注释2) |
0x0B | 长文件名目录项 |
0x0F前面已经讲过 |
0x0C | 系统保留 | 无 |
0x0D | 校验值 | 这个等整个文件名读取完再讲 |
0x0E~0x19 | 12字节Unicode | 即字符串"Test" |
0x1A~0x1B | 文件起始簇号 | 常置0 |
0x1C~0x1F | 4字节Unicode |
0xFFFFFFFF 如果文件名已经结束的话则全部为0xFF |
表4 长文件名目录项参数解释
注释1:01000010
第7位为1,说明是文件最后一个目录项目,
低5位为顺序 0010(2进制) = 2(10进制),说明这是第2个长目录项,且是最后一个目录项。即为这个长文件名占用了两个目录项。
注释2:Unicode 百度百科Unicode 点我详细解释
这边有3个Unicode区,加起来正好是26个字节即13个Unicode码,所以这就是为什么上面讲的以13个字符为单位切割。因为这是第2个目录项,所以后面应该还有第1个目录项,继续分析下一个目录项其余参数同上,看一下3个Unicode分别是“LongL” “engthF” “il”而0x00的属性字节是01,说明这是第一个。至此这个长文件名读取完毕了。按照倒序(这里也解释了前面说的倒序的意思)的顺序拼接起来的话就是”LongLengthFilename”——这就是这个文件的文件名。
下面再来看一下下一个目录项,长文件名目录项后面还会跟一个短文件名目录项,这个目录项记录了除文件名以外的这个文件的信息,而文件名部分则用上面提到的短文件名目录项替换。所以读取方法和短文件名目录项是一样的,这里只看一下文件属性字节,偏移为0x0B,值为0x10=(00010000) 根据短文件名目录项参数的意思,这个文件是一个子目录。其余参数读者可以根据上面提到的计算方法得出。
最后再来补上刚才的校验码计算方法:
int i, j = , chksum=;
for (i = ; i > ; i--)
chksum = ((chksum & ) ? 0x80 : ) + (chksum >> ) + shortname[j++];
其中shortname即长文件名目录项对应的短文件名,所以这个校验码需要等到读完短文件名目录项之后才可以计算。这一段程序是笔者从网上摘来的,还没有时间验证一下。
5、U盘写入文件夹
这样关于数据区的部分差不多就讲完了。
最后在做一点有趣的事情,尝试向磁盘的扇区中写入一些数据,然后看是否会生成这个文件。为了方便起见,这里直接在根目录创建一个文件夹好了,
文件夹的名字叫做root,
创建时间日期2014/8/8 18:18:18
访问日期 2014/8/8
最近修改时间日期 2014/8/8 18:18:18
起始簇低16位 04 00
起始簇高16位 00 00
文件长度 0
同时修改2个FAT表第4项为0x0FFFFFFF
这样应该就可以了,好了,开始编码:
// 短文件名目录项数据结构
typedef struct ShortDirItem
{
char strFilename[];
char strExtension[];
char attribute;
char reserved;
char millisecond;
unsigned short createTime;
unsigned short createDate;
unsigned short accessDate;
unsigned short highWordCluster;
unsigned short updateTime;
unsigned short updateDate;
unsigned short lowWordCluster;
unsigned int filesize;
}ShortDirItem;
// 定位到FAT1表
SetFilePointer(hDisc, *, , FILE_BEGIN);
DWORD dwNumber2Read = ;
// 实际读取的字节数
DWORD dwRealNumber;
// 分配缓冲区
char* buffer = new char[];
// 读取一个扇区的数据
BOOL bRet = ReadFile(hDisc, buffer, dwNumber2Read, &dwRealNumber, NULL);
// 把4第项改为 0x0FFFFFFF
buffer[] = buffer[] = buffer[] = 0xFF;
buffer[] = 0x0F;
// 写回FAT1
bRet = WriteFile(hDisc, buffer, dwNumber2Read, &dwRealNumber, NULL);
// 定位到FAT2表
SetFilePointer(hDisc, (+)*, , FILE_BEGIN);
// 把4第项改为 0x0FFFFFFF
bRet = WriteFile(hDisc, buffer, dwNumber2Read, &dwRealNumber, NULL);
// 定位到根目录
SetFilePointer(hDisc, (+*)*, , FILE_BEGIN);
// 读取根目录扇区
bRet = ReadFile(hDisc, buffer, dwNumber2Read, &dwRealNumber, NULL);
// 准备数据
ShortDirItem* item = new ShortDirItem();
strcpy(item->strFilename, "root");
strcpy(item->strExtension, " ");
item->attribute = 0x10;
item->millisecond = 0x00;
item->createTime = 0x9249;
item->createDate = 0x4508;
item->accessDate = 0x4508;
item->highWordCluster = 0x0000;
item->updateTime = 0x9249;
item->updateDate = 0x4508;
item->lowWordCluster = 0x0004;
item->filesize = 0x00; // 修改根目录数据
char* pData = (char*)item;
for (int i = ; i < ; ++i)
{
buffer[i] = *(pData++);
}
// 写回根目录
bRet = WriteFile(hDisc, buffer, dwNumber2Read, &dwRealNumber, NULL);
// 扫尾工作,释放缓冲区,关闭句柄
delete[] buffer;
delete item;
CloseHandle(hDisc);
但是笔者发现在win7 (准确的说是win7、vista、win8,XP下获取管理员权限即可执行)下调用WriteFile函数无法将数据写入U盘,可能是由于系统保护措施的关系,由于时间关系,笔者也没去深究。
-----------------------------------------------------------2014/8/9-----------------------------------------------------------------
后来笔者专门去查找了相关资料,总的来说原因确实是因为系统保护措施的关系导致WriteFile函数操作的失败,具体的解释如下:
首先是msdn上的解释 http://msdn.microsoft.com/en-us/library/windows/hardware/ff551353(v=vs.85).aspx 大概意思是说在win7和vista上加入了一些新的特性,为了能够更好得保护系统,如果应用程序没有独占的权限就直接对装有文件系统的存储设备进行写入操作的话,这个操作是会被拒绝的。笔者上面的程序通过GetLastError()函数得到的ErroeCode=5,意思也确实是拒绝访问。那么到底要如何写入呢,msdn上给出了以下几种情况:
Write operations on a DASD volume handle will succeed if the file system is not mounted, or if:
The sectors being written to are the boot sectors.
The sectors being written to reside outside file system space.
The file system has been locked implicitly by requesting exclusive write access.
The file system has been locked explicitly by sending down a lock/dismount request.
The write request has been flagged by a kernel-mode driver that indicates that this check should be bypassed. The flag is called SL_FORCE_DIRECT_WRITE and it is in the IrpSp->flags field. This flag is checked by both the file system and storage drivers.
这里比较方便的做法可以采用第4种,即显示地发送一个锁定驱动的请求,然后再尝试写入。具体做法参考这个帖子22L吧,笔者打算去尝试一下,成功的话再来更新结果。 http://bbs.csdn.net/topics/390731448?page=1
-------------------------------------------------------------------------------------------------------------------------------------
好了,看了下篇幅这篇文章也差不多可以结束了。FAT32文件系统其实差不多也都学习完了,为了巩固学习内容,笔者打算接下去根据前面所学的知识,并去了解一下windows快速格式化FAT32的机制,尝试自己格式化U盘,还可以根据FAT32的原理尝试删除数据的恢复等,总之还是有很多事情可以做的。
最后的最后,如果文章当中有任何错误或者遗漏指出,欢迎指出,谢谢。
6、参考文献
1、FAT32系统中长文件名的存储 http://blog.csdn.net/yanpingsz/article/details/5597893
2、FAT32文件系统的存储组织结构(一) http://blog.chinaunix.net/uid-26913704-id-3213948.html
3、Blocking Direct Write Operations to Volumes and Disks http://msdn.microsoft.com/en-us/library/windows/hardware/ff551353(v=vs.85).aspx
4、vc 直接写物理磁盘,writefile 失败 错误返回5 拒绝访问 http://bbs.csdn.net/topics/390731448?page=1