windows提供了以下api来向系统注册一个函数,当有volume增删(比如U盘插拔、新建分区)的时候,通知应用程序:
HDEVNOTIFY WINAPI RegisterDeviceNotification(
__in HANDLE hRecipient,// 可以是窗口句柄或者服务句柄
__in LPVOID NotificationFilter,
__in DWORD Flags // 制定hRecipient是窗口句柄,还是服务句柄
);
如果采用窗口句柄,根据msdn的文档,注册之后,系统会在volume增删的时候,向注册的窗口发送WM_DEVICECHANGE消息,只需要在窗口的消息循环中处理该消息就可以判断出U盘中的卷:
case WM_DEVICECHANGE:
if(wParam == DBT_DEVICEARRIVAL) //设备激活
{
PDEV_BROADCAST_HDR lpdb = (PDEV_BROADCAST_HDR)lParam;
if(lpdb->dbch_devicetype == DBT_DEVTYP_VOLUME)
{
PDEV_BROADCAST_VOLUME lpdbv = (PDEV_BROADCAST_VOLUME)lpdb;
// 下面可以进一步找到装载的卷名,如E:/
}
}
else if(wParam == DBT_DEVICEREMOVECOMPLETE)
{
PDEV_BROADCAST_HDR lpdb = (PDEV_BROADCAST_HDR)lParam;
if(lpdb->dbch_devicetype == DBT_DEVTYP_VOLUME)
{
PDEV_BROADCAST_VOLUME lpdbv = (PDEV_BROADCAST_VOLUME)lpdb;
// 下面可以进一步找到卸载卷名,如E:/
}
}
上面的代码经过验证是可行的。
但是窗口程序需要user登陆系统后才能执行,所以一般还是应该采用service来实现监控。但是如果采用在调用RegisterDeviceNotification的时候传入服务句柄,应该采取不同的处理方式,这点似乎与文档不一致。见下面代码的描述:
case SERVICE_CONTROL_DEVICEEVENT:
if(dwEventType == DBT_DEVICEARRIVAL)
{
PDEV_BROADCAST_HDR pHdr = (PDEV_BROADCAST_HDR)lpEventData;
switch(pHdr -> dbch_devicetype)
{
case DBT_DEVTYP_VOLUME:
{
// 本来应该在这里提取卷名称,但是实际上代码永远不会运行到这里
break;
}
case DBT_DEVTYP_DEVICEINTERFACE:
{
// 卷加载的时候,代码总是运行到这里
// pS->dbcc_name是形如这样的字符串://?/STORAGE#Volume#1&30a96598&0&Signature5C2864E7Offset4000Length67FC000#{53f5630d-b6bf-11d0-94f2-00a0c91efb8b}
PDEV_BROADCAST_DEVICEINTERFACE pS = (PDEV_BROADCAST_DEVICEINTERFACE)pHdr;
WriteLog(std::wstring(pS->dbcc_name));
// 处理代码
}
}
return NO_ERROR;
break;
}
}
研究了半天,最终还是在pS->dbcc_name上找到了突破口。
一个卷设备(比如E盘)在windows内核中是一个volume对象,对象名是/Device/HardDiskVolumeN,N为序号。但是用户态程序是不能直接通过/Device/HardDiskVolumeN对象名来打开volume对象的,必须通过三个SymbolLink来打开:
SymbolLink1形如//?/E:
SymbolLink2形如//?/Volume{GUID}
SymbolLink3形如//?/STORAGE#Volume#1&30a96598&0&Signature5C2864E7Offset4000Length67FC000#{GUID}
呵呵,发现了吧,pS->dbcc_name就是这里的SymbolLink3。
因此可以在UserMode的代码中用CreateFile打开pS->dbcc_name,获得一个Handle;
然后写一个driver,在driver的IOControl中,通过一个Handle查询对象的名字,也即是/Device/HardDiskVolumeN的字符串;
然后UserMode的代码通过DeviceIOControl调用Driver,调用的输入参数就是前面打开的Handle,这样就可以在UserMode中得到卷的名字了。
driver的通过Handle查询ObjectName的简化代码如下:
ObReferenceObjectByHandle(handle, 0, NULL, KernelMode, &pObj1, NULL)
unsigned char nameInfo1[ 512 ];
OBJECT_NAME_INFORMATION * pNameInfo1 = (OBJECT_NAME_INFORMATION *) (nameInfo1);
ULONG length;
NTSTATUS status = ObQueryNameString(pObj1, pNameInfo1, sizeof(nameInfo1) - sizeof(OBJECT_NAME_INFORMATION), &length);
ObDereferenceObject(pObj1);
很绕吧,可惜目前只找到了这个九曲十八弯的方法。
PS:
UserMode下的APPI函数GetVolumePathNamesForVolumeNameW是用不上的,它只能把SymbolLink2的字符串转换成盘符;如果传入SymbolLink3的字符串返回失败。最终在纯用户态下把SymbolLink3转换成volume的名字以失败告终 @@