FAT32文件系統學習(3) —— 數據區(DATA區)
今天繼續學習FAT32文件系統的數據區部分(Data區)。其實這一篇應該是最有意思的,我們可以通過在U盤內放入一些文件,然后在程序中讀取出來;反過來也可以用程序在U盤內寫入一下數據,然后在windows下可以看到寫入的文件。這些筆者都會在這篇文章中演示(后來發現並沒有成功,不過筆者也找到相關的原因,詳見后來的更新部分吧:) )。同時,在寫這篇文章的時候筆者也發現了許多意想不到的規律。
1、本文目錄
2、讀取根目錄
兩張FAT之后的所有扇區都是數據區部分。我們再通過圖1來回顧一下整個FAT32文件系統的分布規則。
通常情況下根目錄都是位於數據區頭部的,前面也提到過,有文獻上說是因為一旦U盤格式化完畢之后,根目錄就創建好了。本着探究的精神,筆者嘗試格式化了一下U盤,發現其實並沒有創建根目錄。不過一旦有文件操作,馬上就會創建根目錄,因為這時整個數據區都是空的,所以自然是寫入數據區的頭部。到頭來其實道理是一樣的,也就是說根目錄一般情況下都是在數據區的頭部(第2簇)。
- 數據區偏移計算
經過前兩篇關於BPB和FAT部分學習之后,我們就可以計算出數據區頭部的偏移:
數據區偏移 = (保留扇區數 + FAT表扇區數 * FAT表個數(通常為2) + (起始簇號-2) * 每簇扇區數) * 每扇區字節數
筆者首先格式化了U盤,通過偏移讀取出了數據區的頭部,發現都是0x00。
- 題外話
這里又要插一些題外話了,筆者試着改了一下U盤的卷標,把它改名為“FAT”。然后還記得BPB當中有一個參數叫做卷標嗎?筆者看了下發現卷標這個參數還是“NO NAME”並沒有改變。這時筆者把數據區的頭部讀取了出來,如圖2所示:
原來被寫在了這里。最后經筆者的測試,卷標最長長度是11個字節,偏移從0x00~0x0A,而偏移0x0B處的值0x08值的意思就是卷標(關於此處值的意思相面還會詳細描述)。因為這個U盤其實還沒有寫入過任何數據,所以卷標才會顯示在數據區的開頭,但是如果換種情況呢,文件系統又是如何找到卷標的呢?筆者查閱了相關資料后發現,卷標一般都是寫在根目錄的下的,如果發現根目錄項的其中一項其0x0B偏移處的值為0x08那么讀取該項的前11個字節即為卷標。
3、短文件名目錄項
- 短文件名目錄項參數
好,回到正題。先來講一下理論的東西:目錄區是由一個個目錄項構成,類似於FAT表。其中每一個目錄項占用32個字節,可以是代表長文件名目錄項、文件目錄項、子目錄項等。對於短文件名格式的目錄項,其參數的含義如表1所示(不會畫這種表,從別處引用了一個)[1]:
- 參數解釋
用一個實際的例子來解釋一下這些參數的意思,首先創建一個短文件名文件,如“file1.txt”,讀取根目錄,如圖3所示:
先不管其他數據,我們來看一下紅色矩形框部分的數據,它就是一個短目錄項。我們來一個個對比表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