对硬盘的操作一般有:读,写,查询,分区,格式化。在Linux系统对硬盘进行自定义开发是比较容易的,因为在Linux系统,所有的设备都可以当做文件来处理。但是在万恶的Windows上就没有那么的便利了,简直就是人间地狱。最近在Linux上做了个硬盘的自定义分区硬盘,自定义格式化硬盘的程序,需要将它们移植到Windows上,遇到了很多的问题,记录下来留作备忘,也可以给后来者做个参考。
(一)基本概念
先看一个磁盘容量的计算公式:
磁盘容量=磁头数(盘面号) * 柱面数 * 扇区数 * 扇区大小
也就是说,如果需要计算出一个磁盘大的大小,我们需要先知道磁头数柱面数扇区数等信息。基本概念如下:
1) 磁头号:磁头号也叫盘面(Side)号,硬盘有数个盘片,每盘片两个面,每个面一个磁头 。
2) 磁道:磁盘在格式化时被划分成许多同心圆,这些同心圆轨迹叫做磁道(Track)
3) 柱面:所有盘面上的同一磁道构成一个圆柱,通常称做柱面(Cylinder)
4) 扇区: 操作系统以扇区(Sector)形式将信息存储在硬盘上,每个扇区包括512个字节的数据和一些其他信息
5) 簇:将物理相邻的若干个扇区称为了一个簇。
(二)磁盘信息查询
在Windows系统,硬盘操作需要通过驱动才能进行,Windows系统提供了函数DeviceIoControl 用来查询和设置设备信息。下面是一个简单得获取磁盘容量的实例:
#include<Windows.h>
#include<stdio.h>
#define DISK_NAME "\\\\.\\PhysicalDrive2"
int GetDiskSize(void)
{
HANDLE handle;
int l_s32Ret = 0;
long long l_s64Offset = 0;
long l_s64OffsetH = 0;
long long l_s64Ret = 0;
unsigned long l_u64Type = 0;
DISK_GEOMETRY DiskGeometry;
DWORD junk;
handle = CreateFile(
TEXT(DISK_NAME),
GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
0,
NULL);
if (handle == INVALID_HANDLE_VALUE)
{
fprintf(stderr, "open device Error: %ld\n", GetLastError());
CloseHandle(handle);
return -1;
}
l_s32Ret = DeviceIoControl(handle, // device to be queried
IOCTL_DISK_GET_DRIVE_GEOMETRY, // operation to perform
NULL, 0, // no input buffer
&DiskGeometry, sizeof(DISK_GEOMETRY), // output buffer
&junk, // # bytes returned
(LPOVERLAPPED)NULL); // synchronous I/O
if (l_s32Ret)
{
l_s64Ret = DiskGeometry.Cylinders.QuadPart * DiskGeometry.TracksPerCylinder
* DiskGeometry.SectorsPerTrack * DiskGeometry.BytesPerSector;
printf("l_s64Ret = %lld l_s64Ret = %d G \n", l_s64Ret, l_s64Ret / (1024 * 1024 * 1024));
}
else
{
fprintf(stderr, "DeviceIoControl Error: %ld\n", GetLastError());
}
CloseHandle(handle);
}
int main(void)
{
GetDiskSize();
getchar();
return 0;
}
在Windows打开一个物理存储设备,需要使用目录:\\\\.\\PhysicalDrive,后面的数值就是在电脑中的物流设备号,系统从0开始分配。我这里打开的是一个SD卡,在我电脑中给它分的设备号是2,因为我电脑中有接两个硬盘,0,1 分配给了硬盘。上面代码执行结果如下:
在Windows中不能直接获取到当前总共有多少个物理存储设备,比如,系统并没有接口可以直接知道当前有多少个存储设备接入到了电脑中。需要开发者自己依次打开各物理设备判断是否存在,如果不能打开就表示设备不在。如果你要知道Windows上哪些逻辑分区是属于哪个分区,同样也需要自己依次去查询。下面的程序分别是通过磁盘物理通道和逻辑通道获取分区信息。所谓逻辑分区也就是我们电脑中可以看到的C盘,D盘,E盘等等。
#include<Windows.h>
#include<stdio.h>
#define DISK_NAME "\\\\.\\PhysicalDrive2"
#define DISK_PATH_LEN 128
/**根据物理磁盘号获取磁盘信息**/
//IOCTL_DISK_GET_DRIVE_GEOMETRY_EX
/**取磁盘的详细信息(包括柱面、磁道、扇区等统计信息)**/
/******************************************************************************
* Function: get the disk's drive geometry information
* input: diskPath, disk path name
* output: pdg,disk geometry information
* return: Succeed, 0-
* Fail, -1
******************************************************************************/
BOOL GetDriveGeometry(unsigned char Disknum, DISK_GEOMETRY *pdg)
{
HANDLE hDevice; // handle to the drive to be examined
BOOL bResult; // results flag
DWORD junk; // discard results
ULONGLONG DiskSize; // size of the drive, in bytes
char diskPath[64] = { 0 };
sprintf_s(diskPath, "\\\\.\\PhysicalDrive%d", Disknum);
//printf("Partition path = %s \n", diskPath);
hDevice = CreateFile(TEXT(diskPath), // drive
0, // no access to the drive
FILE_SHARE_READ | // share mode
FILE_SHARE_WRITE,
NULL, // default security attributes
OPEN_EXISTING, // disposition
0, // file attributes
NULL); // do not copy file attributes
if (hDevice == INVALID_HANDLE_VALUE) // cannot open the drive
{
return (FALSE);
}
bResult = DeviceIoControl(hDevice, // device to be queried
IOCTL_DISK_GET_DRIVE_GEOMETRY, // operation to perform
NULL, 0, // no input buffer
pdg, sizeof(*pdg), // output buffer
&junk, // # bytes returned
(LPOVERLAPPED)NULL); // synchronous I/O
if (bResult)
{
printf("Cylinders = %I64d\n", pdg->Cylinders);
printf("Tracks/cylinder = %ld\n", (ULONG)pdg->TracksPerCylinder);
printf("Sectors/track = %ld\n", (ULONG)pdg->SectorsPerTrack);
printf("Bytes/sector = %ld\n", (ULONG)pdg->BytesPerSector);
DiskSize = pdg->Cylinders.QuadPart * (ULONG)pdg->TracksPerCylinder *
(ULONG)pdg->SectorsPerTrack * (ULONG)pdg->BytesPerSector;
printf("Disk size = %I64d (Bytes) = %I64d (Gb)\n", DiskSize,
DiskSize / (1024 * 1024 * 1024));
}
else
{
printf("GetDriveGeometry failed. Error %ld.\n", GetLastError());
}
CloseHandle(hDevice);
return (bResult);
}
/**根据逻辑分区获取分区信息**/
//IOCTL_DISK_GET_PARTITION_INFO_EX
DWORD DiskGetPartitionInfoEX(CHAR letter)
{
HANDLE hDevice; // handle to the drive to be examined
BOOL result; // results flag
DWORD readed; // discard results
PARTITION_INFORMATION_EX pInformation;
CHAR path[DISK_PATH_LEN];
sprintf_s(path, "\\\\.\\%c:", letter);
printf("Partition path = %s \n", path);
hDevice = CreateFile(path, // drive to open
GENERIC_READ | GENERIC_WRITE, // access to the drive
FILE_SHARE_READ | FILE_SHARE_WRITE, //share mode
NULL, // default security attributes
OPEN_EXISTING, // disposition
0, // file attributes
NULL); // do not copy file attribute
if (hDevice == INVALID_HANDLE_VALUE) // cannot open the drive
{
fprintf(stderr, "CreateFile() Error: %ld\n", GetLastError());
return DWORD(-1);
}
result = DeviceIoControl(
hDevice, // handle to device
IOCTL_DISK_GET_PARTITION_INFO_EX, // dwIoControlCode
NULL, // lpInBuffer
0, // nInBufferSize
&pInformation, // output buffer
sizeof(pInformation), // size of output buffer
&readed, // number of bytes returned
NULL // OVERLAPPED structure
);
printf("buffer len = %d readlen = %zd \n", sizeof(pInformation), readed);
if (!result) // fail
{
fprintf(stderr, "IOCTL_STORAGE_GET_DEVICE_NUMBER Error: %ld\n", GetLastError());
(void)CloseHandle(hDevice);
return (DWORD)-1;
}
printf("PartitionStyle = %d \n", pInformation.PartitionStyle);
printf("StartingOffset = 0x%x \n", pInformation.StartingOffset.QuadPart);
printf("PartitionLength = 0x%x\n = %d G\n", pInformation.PartitionLength.QuadPart, (pInformation.PartitionLength.QuadPart) / (1024 * 1024 * 1024));
printf("PartitionNumber = %d \n", pInformation.PartitionNumber);
printf("RewritePartition = %ld \n", pInformation.RewritePartition);
(void)CloseHandle(hDevice);
return 0;
}
int main(void)
{
DISK_GEOMETRY pdg; // disk drive geometry structure
BOOL bResult; // generic results flag
bResult = GetDriveGeometry(0, &pdg);
printf("\n\n\n");
DiskGetPartitionInfoEX('D');
getchar();
return 0;
}
执行结果如下,打开的是物理设备0,也就是我电脑中的一个硬盘。然后是通过逻辑分区号D,查询了我电脑D盘的信息。
如果要通过分区号去查询磁盘的物理设备号等信息,可以使用下面的程序:
#include<Windows.h>
#include<stdio.h>
#define DISK_NAME "\\\\.\\PhysicalDrive2"
#define DISK_PATH_LEN 64
/********************************************************
Function: DiskGetPartitionInfoEX
Description: Query the physical disk number based on the partition number
Input: letter
OutPut: none
Return: 0 success
none 0 error
Others:
Author: Caibiao Lee
Date: 2018-05-03
*********************************************************/
DWORD GetDiskPhyChnFromPartitionLetter(CHAR letter)
{
HANDLE hDevice; // handle to the drive to be examined
BOOL result; // results flag
DWORD readed; // discard results
STORAGE_DEVICE_NUMBER number; //use this to get disk numbers
CHAR path[DISK_PATH_LEN];
sprintf_s(path, "\\\\.\\%c:", letter);
hDevice = CreateFile(path, // drive to open
GENERIC_READ | GENERIC_WRITE, // access to the drive
FILE_SHARE_READ | FILE_SHARE_WRITE, //share mode
NULL, // default security attributes
OPEN_EXISTING, // disposition
0, // file attributes
NULL); // do not copy file attribute
if (hDevice == INVALID_HANDLE_VALUE) // cannot open the drive
{
fprintf(stderr, "CreateFile() Error: %ld\n", GetLastError());
return DWORD(-1);
}
result = DeviceIoControl(
hDevice, // handle to device
IOCTL_STORAGE_GET_DEVICE_NUMBER, // dwIoControlCode
NULL, // lpInBuffer
0, // nInBufferSize
&number, // output buffer
sizeof(number), // size of output buffer
&readed, // number of bytes returned
NULL // OVERLAPPED structure
);
if (!result) // fail
{
fprintf(stderr, "IOCTL_STORAGE_GET_DEVICE_NUMBER Error: %ld\n", GetLastError());
(void)CloseHandle(hDevice);
return (DWORD)-1;
}
/**#define FILE_DEVICE_DISK 0x00000007
#define FILE_DEVICE_CD_ROM 0x00000002
**/
printf("DeviceType = %d DeviceNumber = %d PartitionNumber = %d\n\n",
number.DeviceType, number.DeviceNumber, number.PartitionNumber);
(void)CloseHandle(hDevice);
return number.DeviceNumber;
}
执行结果如下图,第一个是设备类型,我们可以通过这个判断是是属于哪种存储设备,比如,U盘,硬盘,CD等。DeviceNumber 是物理设备号,PartitionNumber是表示该物理设备总共有多少个分区。
如果要通过DeviceIoControl查询更多的磁盘系统信息,可以参考官方资料:DeviceIoControl function
(三)磁盘位置偏移
磁盘位置偏移,在Windows系统,它有提供两个函数来实现,SetFilePointer 和SetFilePointerEx。对于习惯了Linux编程的人来说,这两个接口都是非常的不友好,函数详细内容可见官方文档。
DWORD WINAPI SetFilePointer(
_In_ HANDLE hFile,
_In_ LONG lDistanceToMove,
_Inout_opt_ PLONG lpDistanceToMoveHigh,
_In_ DWORD dwMoveMethod
);
lDistanceToMove 参数是需要偏移位置的低32位,lpDistanceToMoveHigh是需要偏移位置的高32位的地址。对于最大地址不大于4G的情况,可以直接这样使用,高位的地址就直接赋值为空。
res = SetFilePointer(handle, offset, NULL, FILE_BEGIN);
这样的接口函数,简直就是想砸电脑,又是低位值,又是高位地址。SetFilePointerEx这个函数相对比较好用些
BOOL WINAPI SetFilePointerEx(
_In_ HANDLE hFile,
_In_ LARGE_INTEGER liDistanceToMove,
_Out_opt_ PLARGE_INTEGER lpNewFilePointer,
_In_ DWORD dwMoveMethod
);
liDistanceToMove 是需要偏移到的位置,lpNewFilePointer这个是用来接收接收偏移到位置的地址,不知道这个值具体有什么作用。对于大容量存储设备,比如几T大小的硬盘,可以这样进行偏移:
int SetFilePoint(void)
{
HANDLE handle;
unsigned long long l_u64Ret = 0;
unsigned long long l_u64In = 0;
long l_s64Offset = 0;
long l_s64OffsetH = 0;
long long l_s64Ret = 0;
LARGE_INTEGER liDistanceToMove = { 0 };
LARGE_INTEGER lNewFilePointer = { 0 };
PLARGE_INTEGER lpNewFilePointer = &lNewFilePointer;
handle = CreateFile(
TEXT(DISK_NAME),
GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
0,
NULL);
if (handle == INVALID_HANDLE_VALUE)
{
fprintf(stderr, "open device Error: %ld\n", GetLastError());
CloseHandle(handle);
return -1;
}
liDistanceToMove.QuadPart = 5500105249280;
l_u64Ret = SetFilePointerEx(handle, liDistanceToMove, lpNewFilePointer, FILE_BEGIN);
if (l_u64Ret)
{
printf("Set Point l_s32Ret = %lld \n", l_u64Ret);
if (NULL != lpNewFilePointer)
{
printf("Set Point l_s64Offset = %lld \n", lpNewFilePointer->QuadPart);
}
else
{
printf("Set Point l_s64Offset = %lld \n");
}
}
else
{
fprintf(stderr, "%s %d set file pointer Error: %ld\n", __FILE__, __LINE__, GetLastError());
}
CloseHandle(handle);
}
注意:对于dwMoveMethod,与我们文件操作一样,它有三个标志,用来表示相对于开始位置,当前位置和结束位置。这里需要注意的是,在对于存储设备的操作中,偏移并不能直接偏移到设备的结尾处,偏移到先对与结尾为0的位置,实际在执行的时候会报错,不知道为什么。
(四)磁盘数据直接读写
在Linux系统我们操作一个存储设备,可以向操作一个文件一样直接操作,比如我要修改第二扇区的第5个字节,在Linux系统我们可以直接打开设备,将设备描述符偏移到第二扇区的第五个字节,直接写一个字节的数据就可以修改该位置的数据了。但是,对于Windows系统则不是这样了,如果要直接对存储设备进行读写操作,它是以扇区为单位进行操作的。还是修改第二扇区的第5个字节的数据,我首先需要将第二扇区的所有数据都读取出来缓存,然后在缓存中修改第二扇区的第5个字节的数据,然后再将缓存的数据都写回到第二扇区。我是刚接触Windows系统编程,不知道有没有其便捷的方式写入数据,总觉得在Windows下做设备文件编程,迟早是要被气疯的。下面是一个直接读写数据的实例:
#include<Windows.h>
#include<stdio.h>
#define DISK_NAME "\\\\.\\PhysicalDrive2"
#define DISK_PATH_LEN 64
int SetFilePoint(HANDLE handle, long offset)
{
int res;
res = SetFilePointer(handle, offset, NULL, FILE_BEGIN);
if (INVALID_SET_FILE_POINTER == res)
{
fprintf(stderr, "set file pointer Error: %ld\n", GetLastError());
CloseHandle(handle);
return -1;
}
else
{
printf("set file point = %d \n", res);
}
return 0;
}
int SetFilePointEx(HANDLE handle, LARGE_INTEGER liDistanceToMove)
{
unsigned long long l_u64Ret = 0;
LARGE_INTEGER lNewFilePointer = { 0 };
PLARGE_INTEGER lpNewFilePointer = &lNewFilePointer;
l_u64Ret = SetFilePointerEx(handle, liDistanceToMove, lpNewFilePointer, FILE_BEGIN);
if (l_u64Ret)
{
printf("Set Point l_s32Ret = %lld \n", l_u64Ret);
if (NULL != lpNewFilePointer)
{
printf("Set Point l_s64Offset = %lld \n", lpNewFilePointer->QuadPart);
}
else
{
printf("Set Point l_s64Offset = %lld \n");
}
}
else
{
fprintf(stderr, "%s %d set file pointer Error: %ld\n", __FILE__, __LINE__, GetLastError());
}
return 0;
}
int ReadDataFromDevice(void)
{
HANDLE handle;
int res = 0;
unsigned char arru8Buffer[1024] = { 0 };
unsigned long size = 0;
unsigned long offset = 0;
unsigned long readsize = 0;
handle = CreateFile(
TEXT(DISK_NAME),
GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
0,
NULL);
if (handle == INVALID_HANDLE_VALUE)
{
fprintf(stderr, "open device Error: %ld\n", GetLastError());
return -1;
}
offset = 512;
SetFilePoint(handle, offset);
size = 512;
res = ReadFile(handle, arru8Buffer, size, &readsize, NULL);
if (0 != res)
{
if (size == readsize)
{
for (unsigned int i = 0; i < readsize; )
{
printf("0x%x ", arru8Buffer[i++]);
if (0 == i % 16)
{
printf("\n");
}
}
}
}
else
{
fprintf(stderr, "Read data Error: %ld\n", GetLastError());
}
CloseHandle(handle);
return 0;
}
int WriteDataToDevice(void)
{
HANDLE handle;
int res = 0;
unsigned char arru8Buffer[1024] = { 0 };
unsigned long size = 0;
unsigned long offset = 0;
unsigned long writesize = 0;
offset = 512 ;
writesize = 36;
handle = CreateFile(
TEXT(DISK_NAME),
GENERIC_READ| GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
0,
NULL);
if (handle == INVALID_HANDLE_VALUE)
{
fprintf(stderr, "open device Error: %ld\n", GetLastError());
CloseHandle(handle);
return -1;
}
res = SetFilePoint(handle, offset);
res = WriteFile(handle, arru8Buffer, 512, &writesize, NULL);
if (0 == res)
{
fprintf(stderr, "Write File Error: %ld\n", GetLastError());
CloseHandle(handle);
return -1;
}
else
{
}
CloseHandle(handle);
return 0;
}
int main(void)
{
ReadDataFromDevice();
WriteDataToDevice();
ReadDataFromDevice();
getchar();
return 0;
}
上面代码操作的物理设备2,是我的一个SD卡。注意:不要对自己系统的硬盘低扇区进行写入操作,低扇区是一些分区信息,写错了就得重新分区格式化了,如果是系统盘,那就得重新安装系统了。
上面我是先将整个第二扇区的数据读取出来并打印显示,然后再将第二扇区的开始36个字节写为0。从下面的结果图可以看出,它实际上是把整个扇区都擦除了。
注意,关于数据的直接读取,读取的最小单位是一个扇区,我电脑是512字节。如果读取的数据长度不是512的整数倍,读取接口会报错,错误码为87。
(五)注意事项
(1)对硬盘的所有操作都需要管理员权限,因此需要将自己的IDE设置配置合适的权限,不然运行会提示错误码5。VS设置方法可以参考我之前博客:VS2017中设置程序以管理员身份运行
(2)在对存储设备位置进行偏移的时候,不能相对于结尾做偏移,不知道为什么。
(3)直接数据读写的时候,是以扇区的形式对数据进行读写,如果只写几个字节数据,系统并不会只修改这几个字节数据,而是怎个扇区擦除然后再写入需要写入的几个字节数据。就算你先把句柄偏移到需要写入的位置,实际写入的时候它也是按扇区对齐的,不一定是从你偏移到的位置读写。
官方文档链接:
国内对于Windows磁盘操作的资料比较少比较乱,建议多看官方文档,不然还是比较容易掉坑里。
如果有时间,后面在写 存储设备的分区和格式化操作