1、本文的目标
本文将通过实际读取一个FAT32格式的U盘来简单了解和学习FAT32文件系统的格式。虽然目前windwos操作系统的主流文件系统格式是NTFS,但是FAT32由于其兼容性原因,还是有一定的学习价值。为了能做出一个窗体程序提供直观的感觉,本文的代码采用c#编写,对应的c++代码也会附上。
2、本文目录
2、什么是FAT32
FAT32是Windwos系统硬盘格式分区的一种。这种格式采用32位的文件分配表,使其对磁盘的管理能力大大增强,突破了FAT16对配一个分区的容量只有2GB的限制。虽然目前已被更优异的NTFS分区格式所取代[1]。其实说白了就是FAT表的每一项长度都是32位,所以叫做FAT32。至于每一项存放的内容是什么,下面的内容会慢慢进行分析。
FAT32 文件系统将逻辑盘的空间划分为三部分,依次是引导区 (BOOT区)、文件分配表区(FAT区)、数据区(DATA区)[2]。引导区和文件分配表区又合称为系统区。本文将简单学习引导区的内容,文件分配表区和数据区将在下一篇文章中学习。
3、引导区
引导区从第一扇区开始,保存了每个扇区的字节数,一个簇的扇区数,FAT表的起始位置,FAT表的个数以及FAT表的扇区数等信息。之后还留有若干保留扇区。首先我们先看一下如何从一个U盘当中读取引导区(这里只读取第一个扇区,接下来的读取方法相同)。为了直接读取磁盘的逻辑扇区,我们需要用到windows api当中的几个函数,分别是CreateFile(这里用来创建磁盘的句柄),ReadFile(这里用于读取磁盘扇区)。首先来看一下这两个函数的定义:
1 HANDLE WINAPI CreateFile( 2 _In_ LPCTSTR lpFileName, // 要打开的文件的名或设备名。 3 _In_ DWORD dwDesiredAccess, // 指定类型的访问对象。 4 _In_ DWORD dwShareMode, // 文件共享模式 5 _In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes, // 定义了文件的安全特性 6 _In_ DWORD dwCreationDisposition, 7 _In_ DWORD dwFlagsAndAttributes, 8 _In_opt_ HANDLE hTemplateFile // 返回句柄 9 );
这个函数在msdn上可以查到,这里需要说明一下的是,dwShareMode需要指定为FILE_SHARE_WRITE才能读取扇区的数据(这里是为什么我也不太清楚为什么,希望高人指教)。dwCreationDisposition需要制定为OPEN_EXISTING ,表示文件必须已经存在,由设备提出要求。函数如执行成功,则返回文件句柄。否则返回的句柄 = INVALID_HANDLE_VALUE表示出错,会设置GetLastError。具体失败原因可以查询ErrorCode。
第二个函数是ReadFile,其定义如下:
1 BOOL ReadFile( 2 HANDLE hFile, //文件的句柄 3 LPVOIDl pBuffer, //用于保存读入数据的一个缓冲区 4 DWORD nNumberOfBytesToRead, //要读入的字节数 5 LPDWORD lpNumberOfBytesRead,//指向实际读取字节数的指针 6 LPOVERLAPPED lpOverlapped 7 //如文件打开时指定了FILE_FLAG_OVERLAPPED,那么必须,用这个参 数引用一个特殊的结构。 8 //该结构定义了一次异步读取操作。否则,应将这个参数设为NULL 9 );
该函数用于读取文件,这里指的是读取U盘扇区。其中lpOverlapped设为NULL即可。需要特别注意的是,由于磁盘是以扇区为单位进行读写的,所以这里读取的字节数必须是512的倍数!其他参数注释写的很明白,这里就不再解释了。配合SetFilePointer函数可以读取指定起始位置上指定字节的数据。
首先先来看一下用c++是如何读取引导扇区的数据的。
1 // 笔者的U盘盘符为G 2 WCHAR szDiscFile[] = _T("\\\\.\\G:"); 3 // 打开设备句柄 4 HANDLE hDisc = CreateFile(szDiscFile, GENERIC_READ, FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL); 5 if (hDisc == INVALID_HANDLE_VALUE) 6 { 7 // 打开设备失败 8 return 0; 9 } 10 // 从第一个扇区起始位置开始读取 11 SetFilePointer(hDisc, 0, 0, FILE_BEGIN); 12 // 需要读取字节数 13 DWORD dwNumber2Read = 512; 14 // 实际读取的字节数 15 DWORD dwRealNumber; 16 // 分配缓冲区 17 char* buffer = new char[512]; 18 bool bRet = ReadFile(hDisc, buffer, dwNumber2Read, &dwRealNumber, NULL); 19 20 // 扫尾工作,释放缓冲区,关闭句柄 21 delete[] buffer; 22 CloseHandle(hDisc);
如果我们在21行处下各断点的话可以看到buffer在内存中的数据,这里为了方便,笔者直接把这512个字节的数据保存到了文件中,用二进制文件读取软件打开来看了下,部分数据如下图所示:
好了,接下来重点来了。首先,最开始的3各字节的数据分别是跳转指令与空指令,因为在汇编当中0xEB是跳转指令,0x58是跳转的地址,而0x90则是空指令。至于为什么要在这里放上一句跳转指令呢,这个还得从启动区开始讲起,为了节约篇幅,我就简单介绍一下:一般第一个扇区叫做启动区,cpu把扇区当中的数据当作指令来执行,当读取到EB 58 这个指令时,遍跳转到0x58这个地址并继续读取指令来执行,而0x58地址之后的内容通常都是载入操作系统的指令。如果希望知道详细内容的读者不妨去看一下《30天自制操作系统》这本书,第一天结尾部分有很详细的说明。总之这边的话FAT32规定这个3各字节的内容必须是EB 58 90,只要记住就行了(笑)。(如1L所说,EB 58 90 对应汇编代码即为JUMP 0x58; NOP;)。
而从0x03~0x0A这8个字节的数据表示OEM,这里即为“MSDOS5.0”。
我们把从0x000B开始的79个字节的数据叫做BPB(BIOS Paramter Block),关于BPB的详细说明请参见下表[5]:
偏移量 | 字节数 | 含义 | 值 |
0x00B | 2 | 每扇区字数 | 0x0200 |
0x00D | 1 | 每簇扇区数 | 0x08 |
0x00E | 2 | 保留扇区数 | 0x03F8 |
0x010 | 1 | FAT个数 | 0x02 |
0x011 | 2 | 根目录项数,FAT32以突破该限制,无效 | 0x0000 |
0x013 | 2 | 扇区总数,小于32M使用 | 0x0000 |
0x015 | 1 | 存储介质描述负 | 0x0F8 |
0x016 | 2 | 每FAT表占用扇区数 ,小于32M使用 | 0x0000 |
0x018 | 2 | 逻辑每磁道扇区数 | 0x003F |
0x01A | 2 | 逻辑磁头数 | 0x00FF |
0x01C | 4 | 系统隐含扇区数 | 0x00000080 |
0x020 | 4 | 扇区总数,大于32M使用 | 0x00784F80 |
0x024 | 4 | 每FAT表扇区数,大于32M使用 | 0x00001E04 |
0x028 | 2 | 标记 | 0x0000 |
0x02A | 2 | 版本 (通常为零) | 0x0000 |
0x02C | 4 | 根目录起始簇 | 0x00000002 |
0x030 | 2 | Boot占用扇区数 | 0x0001 |
0x032 | 2 | 备份引导扇区位置 | 0x0006 |
0x034 | 14 | 保留 | 14个字节的0x00 |
0x042 | 1 | 扩展引导标记 | 0x29 |
0x043 | 4 | 序列号 | 0x6A9C4125 |
0x047 | 10 | 卷标 | 转成字符即“NO NAME” |
0x052 | 8 | 文件系统 | 转成字符即“FAT32” |
我来解释一下其中的几个参数。首先是保留扇区数,它可以理解为是FAT表的起始位置。我们可以先来看一下下面这张图,有助于更好的理解保留扇区数的意思。
可以看到引导扇区后面紧跟这若干保留扇区,至于保留扇区的作用会在下一篇中分析,这里先跳过。而保留扇区的后面紧跟着的是FAT1和FAT2。所以FAT1表的起始地址是(引导扇区+保留扇区)*扇区字节数?不对。这里有个需要注意的地方是,保留扇区数这个参数其实已经包含了引导扇区了,所以在计算FAT1表位置的时候直接通过保留扇区数这个参数来计算偏移就行了。这一点需要特别注意。(这里笔者看了几篇文献的说法不一,有说需要加上保留扇区的,有说不用加的,可能是版本不一样的关系。但是笔者亲自实践之后发现是不需要加上的。)
至于FAT表留到下一篇再讲,这里说明一下为什么FAT会有两张。文件分配表区共保存了两个相同的文件分配表,因为文件所占用的存储空间(簇链)及空闲空间的管理都是通过FAT实现的,FAT如此重要,保存两个以便第一个损坏时,还有第二个可用[4]。这样FAT表个数这个参数也解释了。
为了用窗体程序直观的显示各个参数,接下来把上面的程序改写为c#。众所周知,c#调用系统函数大多都需要靠间接调用c的动态库来实现,这里的CreateFile和ReadFile也不例外。下面我们先编写一个FileReader类来提供这些系统api调用。部分代码如下,完整的FileReader点我下载
class FileReader { [System.Runtime.InteropServices.DllImport("kernel32", SetLastError = true, ThrowOnUnmappableChar = true, CharSet = System.Runtime.InteropServices.CharSet.Unicode)] static extern unsafe System.IntPtr CreateFile ( string FileName, // file name uint DesiredAccess, // access mode uint ShareMode, // share mode uint SecurityAttributes, // Security Attributes uint CreationDisposition, // how to create uint FlagsAndAttributes, // file attributes int hTemplateFile // handle to template file ); public bool Open(string FileName) { // open the existing file for reading handle = CreateFile ( FileName, GENERIC_READ, FILE_SHARE_WRITE, 0, OPEN_EXISTING, 0, 0 ); if (handle != System.IntPtr.Zero) { return true; } else { return false; } } }
由于本文的重点不是学习c#窗体编程,所以略过这部分,直接通过调用FileReader提供的系统API并编写窗体程序,效果如下图所示(本程序参考[3]),需要完整工程的读者可以点我下载,请我VS2012及以上打开。
好了,今天这篇文章就先写到这边。由于笔者才疏学浅,对于FAT32也是刚开始学习,如果有错误的地方欢迎批评指正。同时这也是我第一次发随笔,如果排版等方面有不妥的地方也欢迎提出。接下来会继续学习接下去的FAT表区和数据区并继续更新接下来的学习心得。
参考文献:
1、http://baike.baidu.com/view/45233.htm?fr=aladdin
2、FAT32文件格式 http://blog.csdn.net/shrekmu/article/details/5950414
3、读写U盘(FAT32)引导扇区 http://blog.csdn.net/zhanglei8893/article/details/5912903
4、FAT32文件系统格式详解 http://wenku.baidu.com/link?url=zrGv8nld-bc-7KT_TKbo2vWplaiIHhmJ9_ydRZBZdZ4zy8odQFwS6komz2gz1AHX36T_EN1CKZ_16d19upW9pDauno6zEmpw10wlTSTwcoi
5、基于U盘FAT32文件系统的分析 http://wenku.baidu.com/link?url=cIKgrwV66y4CoyuOEB1-OhjRY9tnXtIAoZuYEwDCjxbyRomSIiJgBAXGxq6LudfwuopUpYhiVd8TjxrBFoVyPs0NX3OqbnoWjyn4ZAx60Wi
FAT32文件系统学习(2) —— FAT表
1、题外话
在继续本文学习FAT32文件系统之前,先来插入一点别的话题。我们都知道U盘有一个属性是容量,就拿笔者的U盘为例,笔者手上的U盘是金士顿的DataTraveler G3 4GB的一个U盘。电脑上显示的容量如图1所示为3.75GB。那么这个3.75GB是怎么计算出来的呢?
我们先来回顾一下上一篇BPB参数当中的Sectors(扇区总数)这个参数,这一参数代表了这个U盘在出厂时的总扇区数,笔者手上这个是7884672个,可以从图2中看到。其中每个扇区为512 B,也就是说总共可以容纳4036952064 B 约为 3.76GB 的数据。但是这其中一部分是要用来存放FAT32文件系统的相关信息参数的,比如FAT表,BPB等。我们这边来算一下,首先需要减去1016个保留扇区,还有两个FAT表总共是7684 * 2 = 15368个扇区,所示还剩下的字节数为4036952064 B - ( 1016 + 15368 ) * 512 B = 4028563456 B 正好是图中显示的容量。所以可以得出结论,系统显示的U盘容量 = ( 总扇区数 - 保留扇区数 - FAT表扇区数 * FAT表个数 ) * 512 B。经计算可得实际的使用率是99.79%。所以相对与整个U盘来说,FAT32文件系统用于存储相关信息部分的损耗是很小的。
好了,接下来进入正题,继续学习FAT32文件系统的FAT表部分。
2、本文目录
3、FAT表的读取
首先FAT表一般来说有两张,另一张用于备份。两张表是前后紧挨在一起的,只要计算出了FAT1表的偏移之后加上FAT表的大小就可以得到FAT2表的偏移。FAT1表的偏移地址计算公式如下[4] :
FAT1表偏移 = 保留扇区数 * 每扇区字节数
由图2可知,在本例中,FAT1表的偏移 = 1016 * 512 B = 520192 = 0x7F000。同理:
FAT2表的偏移 = FAT1+FAT表的大小 = (保留扇区数 + FAT表扇区数) * 每扇区字节数
在本例中,FAT2表的偏移 = (1016 + 7684) * 512 B = 4454400 = 0x43F800。用上一篇中讲到的程序可以读取出两张FAT表的内容,一般情况下两张表的内容应该是完全一样的。笔者读取了第一张FAT表起始部分的内容,如图3所示:
4、FAT表项
在分析FAT表之前先来说明一下FAT的构成。FAT表即文件分配表(File Allocation Table)。FAT32文件表是由一个个表项组成的一张表,其中每一个表项由一个32位的二进制组成,其值对应了相应簇的使用情况,如2号表项对应了2号簇的使用情况,3号表项对应了3号簇的使用情况,依此类推。(但是第0和第1项例外,下面会有说明)。每个表项对应数值的含义如表1所示[2]:
表项数值 | 对应含义 |
0x00000000 | 空闲簇,即表示可用 |
0x00000001 | 保留簇 |
0x00000002 - 0x0FFFFFEF | 被占用的簇,其值指向下一个簇号 |
0x0FFFFFF0 - 0x0FFFFFF6 | 保留值 |
0x0FFFFFF7 | 坏簇 |
0x0FFFFFF8 - 0x0FFFFFFF | 文件最后一个簇 |
具体每一项填写的内容规则如下表所示:如果该簇是文件的最后一簇,填入的值为0x0FFFFFFF;如果该簇不是文件的最后一簇,则填入的值为该文件占用的下一簇号(所以我们可以看到在FAT32中文件是以簇链的形式保存起来的)。下面我们根据实际情况,图3来分析一下FAT表的含义。
FAT表第0项(0x00000000~0x00000003): 0x0FFFFF8
FAT表第1项(0x00000004~0x00000007): 0xFFFFFFFF
这两项不代表任何簇的使用情况,而是FAT表的表头,表征了介质描述,是固定值,所以0x00和0x01这两个簇号是不用的,簇号的下标从2开始。其中1号表项可能被用于记录脏标志,以说明文件系统没有被正常卸载或者磁盘表面存在错误。接下来
FAT表第2项(0x00000008~0x0000000B): 0x0FFFFFFF
第2项存储的是第2簇的使用情况,通常第2簇存储的是文件系统的根目录。虽然在FAT32文件系统中,根目录的位置不再硬性地固定,可以存储在分区内可寻址的任意簇内,不过通常根目录是最早建立的(格式化就生成了)目录表。所以,我们看到的情况基本上都是根目录首簇紧邻FAT2,占簇区顺序上的第1个簇(即2号簇)。同时,FAT32文件系统将根目录当做普通的数据文件来看,所有没有了目录项数的限制,在需要的时候可以分配空簇,存储更多的目录项[1]。
这一项的值为0x0FFFFFFF ,说明根目录占用且只占用了1个簇。
FAT表第3 ……
这里再穿插一点题外话,FAT32格式文件分配的最小单位是簇。也就是说你存储了一个实际大小1kB的文件,那么它占用的存储空间还是1簇(在这里换算成大小即为8*512B = 4KB)。笔者以一个实际的例子来说明一下:在U盘中放入一个8B大小的temp.txt文件,然后查看文件属性的时候发现其占用空间是4KB,和我们上面讲的理论符合。
图 4 temp.txt的大小和占用空间
看了下篇幅也差不多了,那么本文关于FAT表的部分到此结束。其实本来也没多少内容,笔者想到哪就扯到哪,胡扯了些其他的东西。剩下的数据区部分就留到下一篇当中再讲好了。同样的,本文当中有一些内容是笔者自己思考理解甚至推测出来的,如果有错误的地方欢迎指正,以免误人子弟了(笑)。
5、参考文献
1、FAT32文件系统的存储组织结构(一) http://blog.chinaunix.net/uid-26913704-id-3213948.html
2、FAT32 http://baike.baidu.com/view/45233.htm?fr=aladdin
3、基于U盘FAT32文件系统的分析 http://wenku.baidu.com/link?url=cIKgrwV66y4CoyuOEB1-OhjRY9tnXtIAoZuYEwDCjxbyRomSIiJgBAXGxq6LudfwuopUpYhiVd8TjxrBFoVyPs0NX3OqbnoWjyn4ZAx60Wi
4、FAT 32 文件格式 http://blog.csdn.net/shrekmu/article/details/5950414
FAT32文件系统学习(3) —— 数据区(DATA区)
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)这里高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。
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所示:
这个时候再去看一下FAT表的5号簇,计算方式在上一篇当中,结果如图6所示:
红色矩形框的位置就是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]:
- 参数解释
然后还是以一个实际的例子来说明,在根目录区读入一个长文件名目录项,如图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 |
第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) 根据短文件名目录项参数的意思,这个文件是一个子目录。其余参数读者可以根据上面提到的计算方法得出。
最后再来补上刚才的校验码计算方法:
1 int i, j = 0, chksum=0; 2 for (i = 11; i > 0; i--) 3 chksum = ((chksum & 1) ? 0x80 : 0) + (chksum >> 1) + 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
这样应该就可以了,好了,开始编码:
1 // 短文件名目录项数据结构 2 typedef struct ShortDirItem 3 { 4 char strFilename[8]; 5 char strExtension[3]; 6 char attribute; 7 char reserved; 8 char millisecond; 9 unsigned short createTime; 10 unsigned short createDate; 11 unsigned short accessDate; 12 unsigned short highWordCluster; 13 unsigned short updateTime; 14 unsigned short updateDate; 15 unsigned short lowWordCluster; 16 unsigned int filesize; 17 }ShortDirItem;
1 // 定位到FAT1表 2 SetFilePointer(hDisc, 1016*512, 0, FILE_BEGIN); 3 DWORD dwNumber2Read = 512; 4 // 实际读取的字节数 5 DWORD dwRealNumber; 6 // 分配缓冲区 7 char* buffer = new char[512]; 8 // 读取一个扇区的数据 9 BOOL bRet = ReadFile(hDisc, buffer, dwNumber2Read, &dwRealNumber, NULL); 10 // 把4第项改为 0x0FFFFFFF 11 buffer[12] = buffer[13] = buffer[14] = 0xFF; 12 buffer[15] = 0x0F; 13 // 写回FAT1 14 bRet = WriteFile(hDisc, buffer, dwNumber2Read, &dwRealNumber, NULL); 15 // 定位到FAT2表 16 SetFilePointer(hDisc, (1016+7684)*512, 0, FILE_BEGIN); 17 // 把4第项改为 0x0FFFFFFF 18 bRet = WriteFile(hDisc, buffer, dwNumber2Read, &dwRealNumber, NULL); 19 // 定位到根目录 20 SetFilePointer(hDisc, (1016+7684*2)*512, 0, FILE_BEGIN); 21 // 读取根目录扇区 22 bRet = ReadFile(hDisc, buffer, dwNumber2Read, &dwRealNumber, NULL); 23 // 准备数据 24 ShortDirItem* item = new ShortDirItem(); 25 strcpy(item->strFilename, "root"); 26 strcpy(item->strExtension, " "); 27 item->attribute = 0x10; 28 item->millisecond = 0x00; 29 item->createTime = 0x9249; 30 item->createDate = 0x4508; 31 item->accessDate = 0x4508; 32 item->highWordCluster = 0x0000; 33 item->updateTime = 0x9249; 34 item->updateDate = 0x4508; 35 item->lowWordCluster = 0x0004; 36 item->filesize = 0x00; 37 38 // 修改根目录数据 39 char* pData = (char*)item; 40 for (int i = 32; i < 64; ++i) 41 { 42 buffer[i] = *(pData++); 43 } 44 // 写回根目录 45 bRet = WriteFile(hDisc, buffer, dwNumber2Read, &dwRealNumber, NULL); 46 // 扫尾工作,释放缓冲区,关闭句柄 47 delete[] buffer; 48 delete item; 49 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