NTFS中的MFT,File Record和File Reference概念

本文介绍了NTFS文件系统中文件记录(FileRecord)的概念及其在$MFT中的存储方式。通过解释FileReference的作用,揭示了如何确定文件在$MFT中的位置,并探讨了文件删除后$MFT空间的管理。

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

在NTFS文件系统下,所有的数据都以“文件“的形式存放在磁盘上,包括NTFS用来记录普通文件信息的“原数据“(metadata),每个文件都在一个特殊的$MFT(Master File Table)文件中至少有一个“文件记录”(File Record),每个file record的大小是固定的1024字节。那么如何知道某个文件所在$MFT中的位置呢?这个就是由一个file reference来给出的。 附件中的程序,可以输出一个普通文件(包括目录)所在$MFT中的索引(MFT index number),以及$MFT的这个位置被重用的次数(MFT sequence number)。在同一个NTFS分区下,这两个32bit整数合起来(LONGLONG) 就构成了文件的唯一标示,在Win32中通常称为File Id,而在NTFS的术语就是File Reference. 当一个文件被删除时,他在$MFT中的file record被标为可用,因此如果有新的文件创建, NTFS就会重用这个位置的file record并且将这个file record的MFT sequence number加1 (当然MFT index number)还是不变的。 虽然一个file record被标为可用,但这1024字节只能作为$MFT中的一个file record来使用所以NTFS并不把它作为一个可分配的空间,这样当一个NTFS分区中文件的个数很多,而这些文件又被删除后,磁盘空间并没有显著的增加。(*) Windows自带的磁盘整理工具并不reclaim这部分空间, 而一些3rd-party磁盘整理工具可以“释放”这些$MFT中未使用的file record,但是一个file record只占用1024字节,估计效果不会太明显。

#include <QCoreApplication> #include <QDebug> #include <QFileInfo> #include <QDir> #include <Windows.h> #include <winioctl.h> #include <vector> #include <map> #pragma pack(push, 1) #define FILE_RECORD_MAGIC 0x454C4946 // 'FILE' typedef struct { DWORD magic; USHORT usa_offset; USHORT usa_count; ULONGLONG lsn; USHORT sequence_number; USHORT link_count; USHORT attrs_offset; USHORT flags; DWORD bytes_in_use; DWORD bytes_allocated; ULONGLONG base_file_record; USHORT next_attr_id; DWORD mft_record_number; } FILE_RECORD_HEADER; typedef struct { DWORD type; DWORD length; BYTE non_resident_flag; BYTE ; USHORT name_offset; USHORT flags; USHORT instance; union { struct { DWORD value_length; USHORT value_offset; BYTE reserved[2]; } resident_data; struct { ULONGLONG lowest_vcn; ULONGLONG highest_vcn; USHORT data_run_offset; USHORT compression_unit; DWORD reserved; ULONGLONG allocated_size; ULONGLONG data_size; ULONGLONG initialized_size; ULONGLONG compressed_size; } non_resident_data; }; } ATTRIBUTE_HEADER; typedef struct { FILETIME creation_time; FILETIME modification_time; FILETIME mft_change_time; FILETIME last_access_time; DWORD file_attributes; DWORD maximum_versions; DWORD version_number; DWORD class_id; DWORD owner_id; DWORD security_id; ULONGLONG quota_charged; ULONGLONG usn; } STANDARD_INFORMATION_ATTRIBUTE; typedef struct { ULONGLONG parent_directory; FILETIME creation_time; FILETIME modification_time; FILETIME mft_change_time; FILETIME last_access_time; ULONGLONG allocated_size; ULONGLONG data_size; DWORD file_attributes; DWORD packed_ea_size; BYTE name_length; BYTE name_type; WCHAR name[1]; } FILE_NAME_ATTRIBUTE; // 文件信息结构体 typedef struct { ULONGLONG mft_reference; QString filename; ULONGLONG parent_mft_reference; ULONGLONG file_size; ULONGLONG allocated_size; FILETIME creation_time; FILETIME modification_time; FILETIME last_access_time; DWORD file_attributes; bool is_directory; } FILE_INFO; #pragma pack(pop) // 检查卷是否为NTFS文件系统 bool isNTFSVolume(const QString& drivePath) { wchar_t volumeName[MAX_PATH]; wchar_t fileSystemName[MAX_PATH]; DWORD serialNumber, maxComponentLen, fileSystemFlags; if (GetVolumeInformationW( (LPCWSTR)drivePath.utf16(), volumeName, MAX_PATH, &serialNumber, &maxComponentLen, &fileSystemFlags, fileSystemName, MAX_PATH)) { return QString::fromWCharArray(fileSystemName).toUpper() == "NTFS"; } return false; } // 将FILETIME转换为可读字符串 QString fileTimeToString(const FILETIME& ft) { if (ft.dwLowDateTime == 0 && ft.dwHighDateTime == 0) { return "N/A"; } SYSTEMTIME st; FileTimeToSystemTime(&ft, &st); return QString("%1-%2-%3 %4:%5:%6") .arg(st.wYear, 4) .arg(st.wMonth, 2, 10, QLatin1Char('0')) .arg(st.wDay, 2, 10, QLatin1Char('0')) .arg(st.wHour, 2, 10, QLatin1Char('0')) .arg(st.wMinute, 2, 10, QLatin1Char('0')) .arg(st.wSecond, 2, 10, QLatin1Char('0')); } // 从MFT引用号中提取记录号(低48位) ULONGLONG extractMftRecordNumber(ULONGLONG mft_reference) { return mft_reference & 0x0000FFFFFFFFFFFF; } // 构建完整路径 QString buildFullPath(ULONGLONG mft_reference, const std::map<ULONGLONG, FILE_INFO>& mft_map, const QString& driveLetter) { std::vector<QString> pathComponents; ULONGLONG current_ref = extractMftRecordNumber(mft_reference); // 最大递归深度,防止循环引用 int max_depth = 100; while (current_ref != 0 && max_depth-- > 0) { auto it = mft_map.find(current_ref); if (it == mft_map.end()) { // 如果找不到记录,可能是根目录或系统保留记录 if (current_ref == 5) { // 根目录的记录号通常是5 break; } qDebug() << "找不到MFT记录:" << current_ref; break; } const FILE_INFO& info = it->second; // 跳过根目录的文件名(它通常是空字符串) if (current_ref != 5) { pathComponents.push_back(info.filename); } // 根目录的父引用是自身或0 ULONGLONG parent_ref = extractMftRecordNumber(info.parent_mft_reference); if (parent_ref == current_ref || parent_ref == 0 || current_ref == 5) { break; } current_ref = parent_ref; } // 构建路径 QString fullPath = driveLetter; for (auto it = pathComponents.rbegin(); it != pathComponents.rend(); ++it) { fullPath += "\\" + *it; } return fullPath; } bool parseMFTAndGetFileInfo(const QString& drivePath, std::vector<FILE_INFO>& fileInfoList, std::map<ULONGLONG, FILE_INFO>& mftMap) { QString volumePath = QString("\\\\.\\%1").arg(drivePath.left(2)); HANDLE hVolume = CreateFileW((LPCWSTR)volumePath.utf16(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hVolume == INVALID_HANDLE_VALUE) { qWarning() << "无法打开卷" << drivePath << "错误代码:" << GetLastError(); return false; } // 获取NTFS卷数据 NTFS_VOLUME_DATA_BUFFER ntfsInfo; DWORD bytesReturned; if (!DeviceIoControl(hVolume, FSCTL_GET_NTFS_VOLUME_DATA, NULL, 0, &ntfsInfo, sizeof(ntfsInfo), &bytesReturned, NULL)) { qWarning() << "无法获取NTFS卷数据. 错误代码:" << GetLastError(); CloseHandle(hVolume); return false; } // 计算MFT的起始字节偏移 ULONGLONG mftStartOffset = ntfsInfo.MftStartLcn.QuadPart * ntfsInfo.BytesPerCluster; LARGE_INTEGER li; li.QuadPart = mftStartOffset; if (!SetFilePointerEx(hVolume, li, NULL, FILE_BEGIN)) { qWarning() << "无法设置文件指针到MFT起始位置. 错误代码:" << GetLastError(); CloseHandle(hVolume); return false; } const DWORD recordSize = ntfsInfo.BytesPerFileRecordSegment; BYTE* recordBuffer = new BYTE[recordSize]; ULONGLONG totalRecords = ntfsInfo.MftValidDataLength.QuadPart / recordSize; qDebug() << "开始解析MFT,共" << totalRecords << "条记录..."; for (ULONGLONG i = 0; i < totalRecords; i++) { DWORD bytesRead; if (!ReadFile(hVolume, recordBuffer, recordSize, &bytesRead, NULL)) { qWarning() << "读取MFT记录失败. 错误代码:" << GetLastError(); break; } if (bytesRead != recordSize) { qWarning() << "读取的字节数不正确. 期望:" << recordSize << "实际:" << bytesRead; continue; } FILE_RECORD_HEADER* recordHeader = (FILE_RECORD_HEADER*)recordBuffer; if (recordHeader->magic != FILE_RECORD_MAGIC || !(recordHeader->flags & 0x01)) { continue; } FILE_INFO fileInfo = {}; fileInfo.mft_reference = i; // 解析属性 BYTE* attrOffset = recordBuffer + recordHeader->attrs_offset; while (attrOffset < recordBuffer + recordSize) { ATTRIBUTE_HEADER* attrHeader = (ATTRIBUTE_HEADER*)attrOffset; if (attrHeader->type == 0xFFFFFFFF) break; // 处理$FILE_NAME属性 if (attrHeader->type == 0x30 && !attrHeader->non_resident_flag) { BYTE* attrData = attrOffset + attrHeader->resident_data.value_offset; if (attrData + sizeof(FILE_NAME_ATTRIBUTE) > recordBuffer + recordSize) { break; // 属性数据越界 } FILE_NAME_ATTRIBUTE* fileNameAttr = (FILE_NAME_ATTRIBUTE*)attrData; if ((fileNameAttr->file_attributes & 0x10) != 0 || (fileNameAttr->file_attributes & 0x4) != 0 || (fileNameAttr->file_attributes & 0x6) != 0 || (fileNameAttr->file_attributes & 0x2020) != 0 || (fileNameAttr->file_attributes & 0x20000006) != 0 || (fileNameAttr->file_attributes & 0x10000000) != 0) { break; // 属性数据越界 } // 确保文件名长度不会导致越界 int name_len = fileNameAttr->name_length; qDebug() << "name_length" << name_len; if (attrData + sizeof(FILE_NAME_ATTRIBUTE) + name_len * sizeof(WCHAR) > recordBuffer + recordSize) { name_len = (recordBuffer + recordSize - attrData - sizeof(FILE_NAME_ATTRIBUTE)) / sizeof(WCHAR); } fileInfo.filename = QString::fromWCharArray(fileNameAttr->name, name_len); fileInfo.parent_mft_reference = fileNameAttr->parent_directory; fileInfo.allocated_size = fileNameAttr->allocated_size; fileInfo.file_size = fileNameAttr->data_size; fileInfo.file_attributes = fileNameAttr->file_attributes; fileInfo.creation_time = fileNameAttr->creation_time; fileInfo.modification_time = fileNameAttr->modification_time; fileInfo.last_access_time = fileNameAttr->last_access_time; // 检查是否是目录 fileInfo.is_directory = (fileNameAttr->file_attributes & 0x10) != 0; break; } // 移动到下一个属性 if (attrHeader->length == 0) break; attrOffset += attrHeader->length; if (attrOffset >= recordBuffer + recordSize) break; } if (!fileInfo.filename.isEmpty()) { fileInfoList.push_back(fileInfo); // 将文件信息添加到映射表中,键是MFT记录号 ULONGLONG record_num = extractMftRecordNumber(i); mftMap[record_num] = fileInfo; } if (fileInfoList.size() % 10000 == 0) { qDebug() << "已找到" << fileInfoList.size() << "个文件..."; } } delete[] recordBuffer; CloseHandle(hVolume); return true; } int main(int argc, char* argv[]) { QCoreApplication a(argc, argv); std::vector<FILE_INFO> fileInfoList; std::map<ULONGLONG, FILE_INFO> mftMap; // MFT记录号到文件信息的映射 QFileInfoList drives = QDir::drives(); for (const QFileInfo& drive : drives) { QString drivePath = drive.absoluteFilePath(); if (isNTFSVolume(drivePath)) { qDebug() << "解析NTFS卷:" << drivePath; if (parseMFTAndGetFileInfo(drivePath, fileInfoList, mftMap)) { qDebug() << "找到" << fileInfoList.size() << "个文件"; // 显示前几个文件的信息,包括完整路径 for (int i = 0; i < 1000 && i < fileInfoList.size(); i++) { const FILE_INFO& info = fileInfoList[i]; QString fullPath = buildFullPath(info.mft_reference, mftMap, drivePath.left(2)); qDebug() << "\n文件:" << info.filename; qDebug() << " 完整路径:" << fullPath; qDebug() << " MFT记录号:" << info.mft_reference; qDebug() << " 父目录MFT:" << info.parent_mft_reference; qDebug() << " 提取的父目录记录号:" << extractMftRecordNumber(info.parent_mft_reference); qDebug() << " 大小:" << info.file_size << "字节"; qDebug() << " 分配大小:" << info.allocated_size << "字节"; qDebug() << " 创建时间:" << fileTimeToString(info.creation_time); qDebug() << " 修改时间:" << fileTimeToString(info.modification_time); qDebug() << " 属性: 0x" << QString::number(info.file_attributes, 16); qDebug() << " 是否是目录:" << (info.is_directory ? "是" : "否"); } } break; } } return a.exec(); } 我想输出文件名与路径是完整的,而不短文件名,名字中间带~
最新发布
08-23
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值