如何为 Windows 开发虚拟磁盘






4.99/5 (95投票s)
本文面向 Windows 开发人员,介绍如何在 Windows 系统中创建虚拟磁盘。
目录
1. 引言
1.1 为什么要在用户模式下处理请求?
1.2 文章结构
1.3 使用的技术
1.4 项目结构
1.5 项目目录结构
2. Windows 和磁盘
3. 实现
3.1 第一阶段:初始化
3.2 第二阶段:磁盘挂载
3.3 第三阶段:处理请求
3.4 第四阶段:卸载
4. 如何构建此解决方案
4.1 测试
4.2 支持的 Windows 版本
5. 结论
引言
本文面向 Windows 开发人员,介绍如何在 Windows 系统中创建虚拟磁盘。
在 Windows 中,虚拟磁盘是通过内核模式驱动程序实现的。有关 Windows 磁盘驱动程序的必要信息已在“Windows 和磁盘”部分给出。
虚拟磁盘的实现也可以在广为人知的开源项目 FileDisk(http://www.acc.umu.se/~bosse/)中找到。本文提供的解决方案的主要区别在于,FileDisk 在内核模式下处理对虚拟磁盘的请求,而本文提出的解决方案则在用户模式下处理。
为什么要在用户模式下处理请求?
当已有用户模式代码用于提供数据源访问(可以是任何数据源,例如内存中的磁盘映像、远程磁盘或收银机)且难以将其移植到内核模式,或者根本没有源代码(例如,在执行网络访问或使用特定加密库时)时,这种方法很有用。
作为此类用户模式库的一个示例,我使用了SparseFile(一个文件容器,随着数据的累积,其大小会增加到最大值)。
文章结构
在“Windows 和磁盘”一节中,我们将讨论 Windows 与磁盘的交互方式;虚拟磁盘创建的可能方案;以及 虚拟磁盘驱动程序开发所需的必要信息。
在“实现”一节中,将考虑解决方案架构以及关键实现方面和我们磁盘生命周期的主要阶段。
“如何构建此解决方案”一节包含有关如何构建和测试项目的几句话。
使用的技术
- C++。整个项目(包括内核模式和用户模式部分)都使用 C++ 开发,并利用了异常处理。关于 C++ 在驱动程序开发中的使用存在许多相互矛盾的观点。以下是一篇关于此主题的好文章:http://www.osronline.com/article.cfm?article=490。
- CppLib(http://www.acc.umu.se/~bosse/)。这个优秀的库使得用 C++ 开发驱动程序成为可能。
- STLPort(http://www.stlport.org/)。标准库。
- BOOST。(https://boost.ac.cn/)。非常有用的库。
项目结构
该解决方案包含多个项目:
- CoreMntTest(用户模式,可执行文件)- 使用 CoreMnt_user 中的代码创建磁盘映像并进行挂载。
- CoreMnt_user(用户模式,库)- 接收来自 CoreMnt 驱动程序的虚拟磁盘请求并提供服务。
- UsrUtils(用户模式,库)- 包含与驱动程序交互的辅助代码,使用 DeviceIoControl。
- CoreMnt(内核模式,可执行文件)- 实现磁盘的 OS 要求;执行请求转换;将其发送到 CoreMnt_user 进行服务。
- drvUtils(内核模式,仅头文件库)- 内核模式的辅助代码,例如同步工具。
下图展示了项目之间的关系。
项目目录结构
.\bin - 包含二进制文件的文件夹
.\lib - 包含库文件的文件夹
.\obj - 包含对象文件的文件夹
.\src - 包含源文件的文件夹
|
|-> .\CoreMnt - 内核模式驱动程序。
|-> .\CoreMnt_user - 用户模式挂载库。
|-> .\CoreMntTest - 用户模式挂载测试。
|-> .\drvCppLib - 用于在 C++ 中开发驱动程序的内核库。
|-> .\drvUtils - 包含用于内核模式项目的工具的内核库。
|-> .\mnt_cmn - 项目之间共享的文件。
|-> .\STLPort - 包含移植到 Windows 驱动程序使用的 STLPort 4.6 的目录。
|-> .\usrUtils - 包含用于用户模式项目的工具的 Win32 库。
Windows 和磁盘
如果您熟悉 Windows 驱动程序开发,并且至少开发过一个最简单的驱动程序,那么您可以跳过本节。
对于其余的读者,我想说一切都很简单。Windows 向磁盘发送“写入”或“读取”请求。磁盘返回读取的数据或错误代码。仅此而已。
当然,会有一些细微之处,否则就不会是这样了。
让我们考虑一个简化版的磁盘请求处理方案。那么,当应用程序调用例如ReadFile
函数时会发生什么?首先,文件读取请求由文件系统驱动程序(例如 ntfs.sys)接收。该方案说明了此过程:
文件系统驱动程序会检测请求的文件在磁盘上的确切位置(偏移量),并形成读取磁盘请求。一个文件可能被分成多个部分,这些部分可能位于磁盘的不同位置。在这种情况下,将形成多个请求。正是这些请求将由我们的虚拟磁盘驱动程序从文件系统驱动程序接收。顺便说一句,虚拟磁盘也可以在文件系统级别实现,有关详细信息,请参阅文章 fs-filter-driver-tutorial.aspx。
本文将使用以下术语:
- IRP(I/O 请求包)是 Windows 内核中的一个结构,用于存储请求参数。例如,如果我们想从设备读取数据,我们需要指明请求类型、要读取数据的缓冲区、大小和偏移量。有一定保留地说,我们可以说IRP是对某个设备的请求。至少在本文中,当我们谈论IRP时,我们总是指请求。更多详细信息可以在这里找到:http://www.microsoft.com/whdc/driver/kernel/IRPs.mspx。
- STATUS_PENDING 是一个特殊的返回码,它通知请求发起者 IRP 目前无法处理,将在稍后处理。在这种情况下,有一个终止事件,设备将在完成请求处理时设置该事件。下面我们将考虑使用此返回码的代码。
- Device 是一个代表任何设备的 Windows 内核对象。它存储有关该设备的信息,例如其名称。它还包含DeviceExtension。
- DeviceExtension 是Device结构中的一个字段,设备创建者可以以自己的方式使用它。下面我们将考虑使用DeviceExtension的代码。
实现
解决方案本身是一个驱动程序(CoreMnt.sys)和一个应用程序(CoreMntTest.exe)。通用方案如下。
驱动程序提供磁盘挂载服务。应用程序创建数据源并使用该服务将其挂载为磁盘。驱动程序接收 IRP,在用户模式下处理它们并返回结果。驱动程序工作通用方案如下图所示。
应用程序(CoreMntTest.exe)处理来自 OS 的对虚拟磁盘的请求。结构图如下所示。
现在让我们按阶段查看源代码中的情况。
第一阶段:初始化
在此阶段,我们通过命令启动CoreMnt驱动程序:
c:\>sc start CoreMnt
我们应该在DriverEntry
中创建一个管理设备,作为用户模式下CoreMntTest的接入点。
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath) { ... NTSTATUS status; status = IoCreateDevice(DriverObject, // pointer on DriverObject 0, // additional size of memory &gDeviceName, // pointer to UNICODE_STRING FILE_DEVICE_NULL, // Device type 0, // Device characteristic FALSE, // "Exclusive" device &gDeviceObject); // pointer do device object if (status != STATUS_SUCCESS) return STATUS_FAILED_DRIVER_ENTRY; status = IoCreateSymbolicLink(&gSymbolicLinkName,&gDeviceName); if (status != STATUS_SUCCESS) return STATUS_FAILED_DRIVER_ENTRY;
下一步是注册驱动程序的请求处理程序。我们将为所有请求类型使用一个处理程序。
for (size_t i = 0; i <= IRP_MJ_MAXIMUM_FUNCTION; ++i) DriverObject->MajorFunction[i] = IrpHandler;
最后,让我们创建MountManager
。
gMountManager = new MountManager(DriverObject); return STATUS_SUCCESS; }
第二阶段:磁盘挂载
在此阶段,我们启动应用程序CoreMntTest.exe。它向驱动程序发送管理消息CORE_MNT_MOUNT_IOCTL
。
CORE_MNT_MOUNT_REQUEST request; request.totalLength = totalSize; request.mountPojnt = mountPoint; DWORD bytesWritten = 0; CORE_MNT_MOUNT_RESPONSE response; if(!m_coreControl.DeviceIoGet(CORE_MNT_MOUNT_IOCTL, &request, sizeof(request), &response, sizeof(response), &bytesWritten)) { throw std::exception(__FUNCTION__" DeviceIoGet failed.&); }
函数DispatchMount
反序列化请求参数并调用MountManager::Mount
。
if(inputBufferLength < sizeof(CORE_MNT_MOUNT_REQUEST) || outputBufferLength < sizeof(CORE_MNT_MOUNT_RESPONSE) ) { throw std::exception(__FUNCTION__" buffer size mismatch"); } DISK_PROPERTIES diskProperties; CORE_MNT_MOUNT_REQUEST * request = (CORE_MNT_MOUNT_REQUEST *)buffer; diskProperties.totalLength.QuadPart = request->totalLength; CORE_MNT_MOUNT_RESPONSE * response = (CORE_MNT_MOUNT_RESPONSE *)buffer; response->deviceId = gMountManager->Mount(&diskProperties);
在MountManager::Mount
中,我们创建一个MountedDisk
类对象并保存它。MountedDisk
包含LogicIrpDispatcher
。其构造函数创建磁盘设备。OS 将向此设备发送请求。
LogicIrpDispatcher::LogicIrpDispatcher(PDISK_PROPERTIES diskProperties, PDRIVER_OBJECT DriverObject, MountManager* mountManager) { ... //create device status = IoCreateDevice(DriverObject,sizeof(InThreadDeviceExtension), &deviceName,FILE_DEVICE_DISK, 0, FALSE,&deviceObject_); if (!NT_SUCCESS(status)) throw std::exception(__FUNCTION__" can't create device.");
设备创建后,我们必须初始化DeviceExtension
。我们希望使用它来存储设备标识符。因此,当我们收到 IRP 时,可以轻松找到相应的MountedDisk
。
InThreadDeviceExtension* devExt = (InThreadDeviceExtension*)deviceObject_->DeviceExtension; memset(devExt, 0, sizeof(InThreadDeviceExtension)); devExt->mountManager = mountManager; devExt->deviceId = diskProperties->deviceId;
因此,此时MountManager
已经创建了MountedDisk
的一个实例并将其保存在容器中。初始化阶段在用户模式下完成。为每个磁盘创建一个线程,并在该线程中处理所有请求。线程向驱动程序发送IOCTL RequestExchange
命令,然后进入请求等待模式。
while(true) { int type = 0; int size = 0; __int64 offset = 0; drvCtrl->RequestExchange(deviceId, lastType, lastStatus, lastSize, &dataBuf[0], dataBuf.size(), &type, &size, &offset); //do requested operation DispatchImageOperation(image, type, size, offset, &dataBuf[0], dataBuf.size(), &lastStatus); lastType = type; lastSize = size; }
性能提示:在一个线程中服务请求无疑是“瓶颈”。在实际项目中肯定会有线程池。
第三阶段:处理请求
因此,我们的虚拟磁盘已准备好处理请求。让我们跟踪请求处理的完整序列。一切都始于IrpHandler
函数,该函数由我们的驱动程序注册为 IRP 处理过程。在这里,我们从DeviceExtension
(在初始化阶段已保存)获取设备标识符,并将 IRP 传输给MountManager
。
NTSTATUS IrpHandler( IN PDEVICE_OBJECT fdo, IN PIRP pIrp ) { ... InThreadDeviceExtension* devExt = (InThreadDeviceExtension*)fdo->DeviceExtension; return gMountManager->DispatchIrp(devExt->deviceId, pIrp);
MountManager
接收 IRP,通过设备标识符查找相应的MountedDisk
,并将 IRP 重定向到它。下面的代码决定是立即处理此请求还是应在用户模式下处理。
NTSTATUS MountedDisk::DispatchIrp(PIRP irp) { IrpParam irpParam(0,0,0,0); irpDispatcher_.getIrpParam(irp, &irpParam); if(irpParam.type == directOperationEmpty) { ... irpDispatcher_.dispatch(irp); ... NTSTATUS status = irp->IoStatus.Status; IoCompleteRequest(irp, IO_NO_INCREMENT); return status; } IoMarkIrpPending( irp ); irpQueue_.push(irp); return STATUS_PENDING; }
决策很简单:如果是IRP_MJ_READ
或IRP_MJ_WRITE
,则应在用户模式下处理。驱动程序可以自行处理所有其他请求。例如IOCTL_DISK_GET_LENGTH_INFO
:我们的驱动程序知道磁盘大小,也知道磁盘大小不会改变。可以在LogicIrpDispatcher::dispatchIoctl
中找到 Windows 可能发送给磁盘的所有请求的完整列表。
服务此磁盘的线程从列表中选择请求。
void MountedDisk::RequestExchange(UINT32 lastType, UINT32 lastStatus, UINT32 lastSize, char* buf, UINT32 bufSize, UINT32 * type, UINT32 * length, UINT64 * offset) { ... NTSTATUS status = KeWaitForMultipleObjects(sizeof(eventsArray)/sizeof(PVOID), eventsArray, WaitAny, Executive, KernelMode, FALSE, NULL, 0); ... IrpParam irpParam(0,0,0,0); irpDispatcher_.getIrpParam(lastIrp_, &irpParam); *type = irpParam.type; *length = irpParam.size; *offset = irpParam.offset;
如果是IRP_MJ_WRITE
,则要写入的数据将被复制到缓冲区。然后,该缓冲区将被传递给用户模式代码。
if(*type != directOperationEmpty && opType2DirType(directOperationTypes(*type)) == directOperationWrite) { IrpParam irpParam(0,0,0,0); irpDispatcher_.getIrpParam(lastIrp_, &irpParam); if(irpParam.buffer) memcpy(buf, irpParam.buffer, *length);
从RequestExchange
函数返回后,我们将再次进入请求处理循环(DispatchImage
)。
while(true) { int type = 0; int size = 0; __int64 offset = 0; drvCtrl->RequestExchange(deviceId, lastType, lastStatus, lastSize, &dataBuf[0], dataBuf.size(), &type, &size, &offset); //do requested operation DispatchImageOperation(image, type, size, offset, &dataBuf[0], dataBuf.size(), &lastStatus); lastType = type; lastSize = size; }
变量 type、size、offset 现在包含要处理的新请求。这是DispatchImageOperation
函数的任务。
void DispatchImageOperation(IImage * image, int type, int size, __int64 in_offset, char* buf, int bufsize, int* status) { switch(type) { ... case directOperationRead: { image->Read((char*)buf, in_offset, size); *status = 0; break; } case directOperationWrite: { image->Write((const char*)buf, in_offset, size); *status = 0; break; }
请求被服务后,将再次调用RequestExchange
函数,线程将继续进入新的请求等待模式。
第四阶段:卸载
此阶段在用户模式下通过调用UnmountImage
函数开始。下面的代码检查磁盘当前是否正在使用。
void UnmountImage(int devId, wchar_t mountPoint, DriverControl * drvCtrl) { ... if (!DeviceIoControl(hVolume,FSCTL_LOCK_VOLUME,NULL, 0,NULL,0,&BytesReturned,NULL)) { throw std::exception("Unable to lock logical drive"); } else if (!DeviceIoControl(hVolume,FSCTL_DISMOUNT_VOLUME, NULL,0,NULL,0,&BytesReturned,NULL)) { throw std::exception("Unable to dismount logical drive"); } else if (!DeviceIoControl(hVolume,FSCTL_UNLOCK_VOLUME,NULL, 0,NULL,0,&BytesReturned,NULL)) { throw std::exception("Unable to unlock logical drive"); }
然后,我们销毁挂载点与我们的设备之间的连接。
if (UndefineLogicDrive(mountPoint)) throw std::exception("Unable to undefine logical drive");
然后,我们发送一条消息给系统中存储磁盘列表的所有组件(例如explorer.exe或任何其他文件管理器)。
SHChangeNotify(SHCNE_DRIVEREMOVED, SHCNF_PATH, root, NULL);
最后,我们通知驱动程序该设备可以被删除。
drvCtrl->Unmount(devId); }
MountManager::Unmount
只需从容器中删除相应的MountedDisk
,从而调用其析构函数。
MountedDisk::~MountedDisk() {
我们为主处理该磁盘的请求的线程设置停止事件。
stopEvent_.set();
我们终止所有尚未服务且当前在队列中的 IRP。
if(lastIrp_) CompleteLastIrp(STATUS_DEVICE_NOT_READY, 0); while(irpQueue_.pop(lastIrp_)) CompleteLastIrp(STATUS_DEVICE_NOT_READY, 0); }
正在MountedDisk::RequestExchange
中处于等待状态的请求处理线程会响应stopEvent_ set
并抛出异常。
NTSTATUS status = KeWaitForMultipleObjects(sizeof(eventsArray)/sizeof(PVOID), eventsArray, WaitAny, Executive, KernelMode, FALSE, NULL, 0); if(status != STATUS_SUCCESS) { throw std::exception("MountedDisk::RequestExchange - mount stop."); }
我们将在DispatchException
函数的 catch 块中捕获抛出的异常,并向用户模式返回STATUS_UNSUCCESSFUL
。
NTSTATUS DispatchExchange(PVOID buffer, ULONG inputBufferLength, ULONG outputBufferLength) { try { ... gMountManager->RequestExchange(request->deviceId, request->lastType, request->lastStatus, request->lastSize, request->data, request->dataSize, &response.type, &response.size, &response.offset); ... } catch(const std::exception & ex) { KdPrint((__FUNCTION__" %s\n", ex.what())); return STATUS_UNSUCCESSFUL; } }
返回的错误状态随后将被用户模式代码在DriverControl::RequestExchange
函数中处理,并且也会在其自身的处理中抛出异常。
void DriverControl::RequestExchange(int deviceId, int lastType, int lastStatus, int lastSize, char * data, int dataSize, int *type, int *size, __int64 * offset) { ... if(!m_coreControl.DeviceIoGet(CORE_MNT_EXCHANGE_IOCTL, &request, sizeof(request), &response, sizeof(response), &bytesWritten)) { throw std::exception(__FUNCTION__" DeviceIoGet failed."); } ... }
此异常随后将被SyncMountmanager::mountDispatchThread
中的 catch 块捕获。
void SyncMountManager::mountDispatchThread(void* pContext) { ... try { DispatchImage(dispContext->devId, image->GetMountPoint(), dispContext->image, dispContext->mountManager->GetDriverControl()); } catch(const std::exception& ex) { dispContext->mountManager->OnUnmount(dispContext->image, ex.what()); } ... }
这将导致请求处理线程终止,以及调用 IImage 析构函数。
如何构建此解决方案
- 安装 Windows Driver Developer Kit 2003。(http://www.microsoft.com/whdc/devtools/ddk/default.mspx)
- 将全局环境变量“BASEDIR”设置为已安装 DDK 的路径。
计算机 -> 属性 -> 高级 -> 环境变量 -> 系统变量 -> 新建
例如:BASEDIR -> c:\winddk\3790
- 下载并安装 boost(已测试 1.40 版本)。(https://boost.ac.cn/users/download/)
- 将全局环境变量“BOOST”设置为已安装 boost 的路径。(设置后需要重启计算机。)
- 使用 Visual Studio 2008 构建解决方案。
测试
- 按照上述说明构建解决方案。
- 将 CoreMnt.sys 复制到%windir%\system32\drivers。
- 使用命令在系统中注册驱动程序:
sc create CoreMnt type= kernel binPath= system32\drivers\CoreMnt.sys
- 使用命令启动驱动程序:
sc start CoreMnt
- 启动 CoreMntTest.exe。
如果一切顺利,CoreMntTest.exe 将显示以下消息:
Image was mounted. Press any key for unmount.
磁盘 Z 将出现在系统中。
现在我们可以格式化它了。
文件“tst_img”将出现在 CoreMntTest.exe 所在的目录中。
支持的 Windows 版本
- Windows XP SP2
结论
本文仅讨论了虚拟磁盘创建的可能方法之一。所述方法可称为逻辑虚拟磁盘。还可以提到以下方法:
- 文件系统虚拟磁盘。驱动程序接收
CreateFile
、ReadFile
、WriteFile
等请求。 - 物理虚拟磁盘。此方法在语义上与逻辑方法相似,但在驱动程序级别存在一些差异。对虚拟磁盘的请求具有不同的格式,磁盘设备应该是即插即用(PNP)的。
本文来自 Apriorit 网站