文章目录
抓屏技术之MirrorDriver镜像驱动实现
在XP和Win7系统下面,如果要对桌面进行录屏/抓屏,很多情况下使用的都是BitBlt
函数来实现的,例如一些屏幕录制软件,局域网教学软件,甚至远程工具类软件。
BitBlt
函数声明如下:
BOOL BitBlt(
HDC hdc,
int x,
int y,
int cx,
int cy,
HDC hdcSrc,
int x1,
int y1,
DWORD rop
);
一般来说,屏幕录制都是将桌面DC的内容到一个兼容DC中,然后从兼容DC中提取出位图。BitBlt
这里涉及到从DC中将整个位图拷贝出来,其实整体效率是比较差的。
本文我们来看一种基于XDDM驱动的MirrorDriver镜像驱动技术,可以直接在内核中高效的抓屏技术。当然这种技术在Win8以上的系统已经被新技术取代,如果在Win8以上的系统,可以使用更加简单的Desktop Duplicate技术。
1. 技术概览
在WinXp的系统下面,显示的引擎主要还是支持2D,但是随着游戏等领域的兴起,D3D成为了Windows的核心显示引擎,MirrorDriver是WinXp下面的技术,是一种驱动层面支持绘图的技术。Mirror驱动和普通的XDDM驱动类似,分为两部分实现:
- 负责图像渲染的部分,这部分一般是以DLL形式提供,运行于各自的会话空间。
- MiniPort驱动,运行在全局内核空间,一般提供硬件层面的显存等硬件信息。
XDDM驱动的架构如下:
下面分别来看一下各个部分的实现过程。
2. 小端口驱动
对于Mirror驱动,小端口驱动并不提供任何硬件信息(包括显存等),因此Mirror驱动的小端口实现非常简单,只需要提供最基础的框架接口即可:
ULONG
DriverEntry (
PVOID Context1,
PVOID Context2
)
{
VIDEO_HW_INITIALIZATION_DATA hwInitData;
ULONG initializationStatus;
// Zero out structure.
VideoPortZeroMemory(&hwInitData, sizeof(VIDEO_HW_INITIALIZATION_DATA));
// Specify sizes of structure and extension.
hwInitData.HwInitDataSize = sizeof(VIDEO_HW_INITIALIZATION_DATA);
// Set entry points.
hwInitData.HwFindAdapter = &MirrorFindAdapter;
hwInitData.HwInitialize = &MirrorInitialize;
hwInitData.HwStartIO = &MirrorStartIO;
hwInitData.HwResetHw = &MirrorResetHW;
hwInitData.HwInterrupt = &MirrorVidInterrupt;
hwInitData.HwGetPowerState = &MirrorGetPowerState;
hwInitData.HwSetPowerState = &MirrorSetPowerState;
hwInitData.HwGetVideoChildDescriptor = &MirrorGetChildDescriptor;
hwInitData.HwLegacyResourceList = NULL;
hwInitData.HwLegacyResourceCount = 0;
// no device extension necessary
hwInitData.HwDeviceExtensionSize = 0;
hwInitData.AdapterInterfaceType = 0;
initializationStatus = VideoPortInitialize(Context1,
Context2,
&hwInitData,
NULL);
return initializationStatus;
}
小端口驱动都是提供一个VideoPortInitialize
函数来支持IRP的回调函数,这个是Windows驱动开发中小端口驱动的基本框架;对于这些回调函数,不用做任何处理,提供一个空的即可。
3. 显示驱动
对于显示驱动并不是通过DriverEntry
作为入口,而是通过DrvEnableDriver
函数作为驱动的入口,这个函数声明如下:
BOOL DrvEnableDriver(
ULONG iEngineVersion,
ULONG cj,
DRVENABLEDATA *pded
);
一般情况,我们在DrvEnableDriver
函数中提供相关的回调接口给Win32K调用,一般回调函数设置如下:
static DRVFN gadrvfn[] =
{
{ INDEX_DrvEnablePDEV, (PFN) DrvEnablePDEV },
{ INDEX_DrvCompletePDEV, (PFN) DrvCompletePDEV },
{ INDEX_DrvDisablePDEV, (PFN) DrvDisablePDEV },
{ INDEX_DrvEnableSurface, (PFN) DrvEnableSurface },
{ INDEX_DrvDisableSurface, (PFN) DrvDisableSurface },
{ INDEX_DrvAssertMode, (PFN) DrvAssertMode },
{ INDEX_DrvNotify, (PFN) DrvNotify },
{ INDEX_DrvCreateDeviceBitmap, (PFN) DrvCreateDeviceBitmap },
{ INDEX_DrvDeleteDeviceBitmap, (PFN) DrvDeleteDeviceBitmap },
{ INDEX_DrvTextOut, (PFN) DrvTextOut },
{ INDEX_DrvBitBlt, (PFN) DrvBitBlt },
{ INDEX_DrvCopyBits, (PFN) DrvCopyBits },
{ INDEX_DrvStrokePath, (PFN) DrvStrokePath },
{ INDEX_DrvLineTo, (PFN) DrvLineTo },
{ INDEX_DrvFillPath, (PFN) DrvFillPath },
{ INDEX_DrvStrokeAndFillPath, (PFN) DrvStrokeAndFillPath },
{ INDEX_DrvStretchBlt, (PFN) DrvStretchBlt },
{ INDEX_DrvAlphaBlend, (PFN) DrvAlphaBlend },
{ INDEX_DrvTransparentBlt, (PFN) DrvTransparentBlt },
{ INDEX_DrvGradientFill, (PFN) DrvGradientFill },
{ INDEX_DrvPlgBlt, (PFN) DrvPlgBlt },
{ INDEX_DrvStretchBltROP, (PFN) DrvStretchBltROP },
#if (NTDDI_VERSION >= NTDDI_VISTA)
{ INDEX_DrvRenderHint, (PFN) DrvRenderHint },
#endif
{ INDEX_DrvEscape, (PFN) DrvEscape },
{INDEX_DrvSetPointerShape, (PFN)DrvSetPointerShape},
{INDEX_DrvMovePointer, (PFN)DrvMovePointer}
};
在上述回调函数中,有一些回调函数需要稍微介绍一些:
-
DrvEnableDriver
:是显示驱动的初始入口点,这个API入口参数有一个DRVENABLEDATA
结构,这个结构需要使用一个指向驱动入口的函数表来填充。 -
DrvDisableDriver
:当显示驱动卸载时调用这个函数。在这个函数中,可以执行一些必要的清理工作,来清理你在DrvEnableDriver
调用中创建的东西。 -
DrvGetModes
:这个API它用来查询设备支持的显示模式。 -
DrvEnablePDEV
: 一旦选定了一种显示模式,DrvEnablePDEV
这个API就会被调用,这个API的用途是允许显示驱动创建自己私有的上下文,这个上下文将会被传递给其他的显示入口点。 -
DrvCompletePDEV
:该函数是用在DrvEnablePDEV
调用之后,用来通知显示驱动,设备对象现在已经完成。 -
DrvDisablePDEV
:当PDEV
不再需要,且应该被销毁时,就调用这个API。 -
DrvEnableSurface
:这个API是用在PDEV完成以后,用来请求显示驱动创建一个表面,当创建一个表面的时候,可以有两种选择:- 可以创建一个由显示驱动管理的表面。
- 也可以创建一个由GDI管理的表面。
-
DrvDisableSurface
:调用这个API以注销在DrvEnableSurface
调用中创建的绘画表面。
对于整个显示驱动,其驱动函数的调用序列如下:
DrvEnableDriver
:加载驱动DrvGetModes
:获取缓冲尺寸,来存放所有支持的显示模式。DrvGetModes
:得到显示模式。DrvEnablePDEV
:通知显示驱动在DEVMODE
数据结构中初始化一个选中的模式,并且返回一个实例句柄。DrvCompletePDEV
:通知驱动,设备初始化已经完成。DrvEnableSurface
:得到驱动来提供一个绘画表面。- <GDI 调用>。
DrvDisableSurface
:删除绘画表面。DrvDisablePDEV
:删除实例结构。DrvDisableDriver
:卸载显示驱动。
3.1 关于绘图HOOK
我们在DrvEnableSurface
回调函数中,创建Surface的时候,一般需要对我们敢兴趣的绘图函数进行HOOK(也就是设置相关回调函数),一般我们在EngModifySurface
这个函数中完成,该函数声明如下:
ENGAPI BOOL EngModifySurface(
HSURF hsurf,
HDEV hdev,
FLONG flHooks,
FLONG flSurface,
DHSURF dhsurf,
VOID *pvScan0,
LONG lDelta,
VOID *pvReserved
);
我们通过设置flHooks
来标记哪些绘图操作是我们感兴趣的,对于HOOK标记有如下:
#define HOOK_BITBLT 0x00000001
#define HOOK_STRETCHBLT 0x00000002
#define HOOK_PLGBLT 0x00000004
#define HOOK_TEXTOUT 0x00000008
#define HOOK_PAINT 0x00000010 // Obsolete
#define HOOK_STROKEPATH 0x00000020
#define HOOK_FILLPATH 0x00000040
#define HOOK_STROKEANDFILLPATH 0x00000080
#define HOOK_LINETO 0x00000100
#define HOOK_COPYBITS 0x00000400
#define HOOK_MOVEPANNING 0x00000800 // Obsolete
#define HOOK_SYNCHRONIZE 0x00001000
#define HOOK_STRETCHBLTROP 0x00002000
#define HOOK_SYNCHRONIZEACCESS 0x00004000 // Obsolete
#define HOOK_TRANSPARENTBLT 0x00008000
#define HOOK_ALPHABLEND 0x00010000
#define HOOK_GRADIENTFILL 0x00020000
#if (NTDDI_VERSION < NTDDI_VISTA)
#define HOOK_FLAGS 0x0003b5ff
#else
#define HOOK_FLAGS 0x0003b5ef
#endif
3.2 关于剪切区
在Mirror驱动中,我们可以对改动区域(一般称作脏区域)进行获取,这个区域我们成为剪切区,例如针对DrvTextOut
函数声明如下:
BOOL DrvTextOut(
SURFOBJ *pso,
STROBJ *pstro,
FONTOBJ *pfo,
CLIPOBJ *pco,
RECTL *prclExtra,
RECTL *prclOpaque,
BRUSHOBJ *pboFore,
BRUSHOBJ *pboOpaque,
POINTL *pptlOrg,
MIX mix
);
在这个函数中存在FONTOBJ *pfo
表示我们的剪切区(也就是我们通常说的脏区域),我们通过如下清理来进行枚举:
do {
bMore = CLIPOBJ_bEnum(pco, sizeof(buffer), &buffer.c);
for (i = 0; i < buffer.c; i++) {
}
} while (bMore);
3.3 关于共享内存
我们在内核显示驱动中,可以通过MirrorDriver获取到绘图信息,但是如果用户层需要这个数据怎么获取呢?由于数据量太大,使用ExtEscape
来说是不太可行的;因此需要一种更加高效的拷贝数据的方法,这种方法就是共享内存。Mirror镜像驱动和用户层通信的共享内存一般存在两种方式:
- 第一种是通过
EngMapFile
以文件为背景,映射一个虚拟内存;然后在用户层通过CreateFileMapping
和MapViewOfFile
来实现内存的共享。 - 第二种是在内核中通过
ZwCreateSection
和MmMapViewInSystemSpace
来创建系统内存;然后用户层再通过ZwMapViewOfSection
来将内存映射到指定的进程中。
对于第二种方法,存在一个困难的点就是显示驱动是运行再会话空间中,不能调用系统层的相关函数,只能调用Win32k提供的EngXXX
函数,因此我们要借助MiniPort小端口驱动来实现,显示驱动通过EngDeviceIoControl
将请求转发给小端口驱动;让小端口驱动来完成共享内存的创建。
4. 实现效果
通过上述实现之后,安装硬件驱动,我们可以在设备管理器中看到显示器设备,如下:
通过Mirror镜像驱动,我们可以实现高效的截图操作,对桌面进行录屏操作,示例如下(右侧为录屏窗口的预览):
5. 关于AERO特效
开启Mirror驱动之后,会导致AERO特效的关闭,这个是为什么呢?因为AERO特效的实现是在DWM.EXE进程里面完成的,但是DWM的合成操作的图形绘制并不会经过Mirror驱动的回调函数,这是系统的特性。
因此我们开启Mirror的时候,就会导致DWM.EXE的AERO特效关闭。
6. Win10的兼容性
WIN7之后我们就使用的WDDM驱动,Win8之后微软使用了新的抓图技术来实现屏幕抓图;那么Win8之后是否还能使用Mirror镜像驱动呢?
在Win10系统中存在一个文件MirrorDrvCompat.dll,该DLL中大致存在如下的伪代码:
int CMitigationManagement::MitigationManagementThread(LPCRITICAL_SECTION lpCriticalSection)
{
while (TRUE)
{
CMitigationManagement::DuplicateDesktop(...);
Sleep(...);
}
}
CMitigationManagement::DuplicateDesktop
通过名字也可以发现是Win8之后的抓屏技术,其实在这个函数里面就是使用的Desktop Duplication API来抓屏,然将图像信息绘制到MirrorDriver驱动中去。
因此我们在Win10下面仍旧可以使用Mirror镜像驱动的,这里不得不佩服Windows系统兼容性的强大;但是我们不建议在Win10环境下面使用Mirror镜像驱动,应该使用更新的特性Desktop Duplication API来抓屏。
7. 应用
如果了解过远程相关的技术,那么一定听过一个(或者使用过)一个开源工具uvnc(全名是UltraVNC,地址为https://uvnc.com/),这个工具在Win7及其以下系统,高效的录屏方式就是MirrorDriver。
但是UltraVNC并没有将MirrorDriver录屏技术开源(甚至还进行授权和源码收费),后续文章中,我们将通过其公开的SDK,实现兼容https://uvnc.com/downloads/mirror-driver/85-mirror-driver-downloads.html的镜像驱动。