這里討論的都是win7以上平臺的WDDM模型的顯卡驅(qū)動,而不是WINXP之前的XPDM模型的顯卡驅(qū)動。
實際上沒有"顯卡過濾驅(qū)動"一說, windows 10 1607之前的平臺也從來沒有支持過顯卡過濾驅(qū)動這一框架。
而有些應用又不得不獲取顯卡底層數(shù)據(jù)。
比如擴展windows桌面,雖然顯卡本身支持連接多個顯示器,能做到擴展桌面的效果,
但是這局限于HDMI,Display-Port,VGA等接口線,
而如果想采用其他接口,比如通過USB,通過網(wǎng)線通訊等更廣泛和廉價的接口,
(比如USB顯卡,把顯卡的顯示通過USB接口線傳輸?shù)狡渌O備上等。)
除了顯卡硬件本身支持外(顯然現(xiàn)在的顯卡都不支持網(wǎng)線,USB接口等),就只能采用其他途徑來解決問題。
要解決這個問題:
一種辦法是開發(fā)虛擬顯卡,讓虛擬顯卡和真實顯卡一起工作,虛擬顯卡擴展windows桌面到網(wǎng)線或USB,
這種辦法以后會逐步講解。
還有一種辦法就是本文即將講述的:給顯卡掛載一個過濾驅(qū)動,攔截和偽造顯卡各種請求,
從而給顯卡虛擬出一個額外的顯示終端(可以簡單理解成虛擬顯示器,下同)。
而這個所謂的過濾驅(qū)動,其實不是windows提供的標準框架,而是采用類似黑客的HOOK技術(shù),
HOOK某些回調(diào)函數(shù),改寫這些回調(diào)函數(shù)的行為,從而達到目的。
還有一種應用,云桌面的問題:
在一臺物理服務器上,開辟10幾個虛擬機系統(tǒng),當然每個虛擬機系統(tǒng)可以使用虛擬顯卡,這對一般辦公使用沒有大問題,
但是如果要求更高性能的顯示效果,就不得不使用其他辦法,
比如 vGPU 方式,物理宿主機上的硬件顯卡虛擬出多個顯卡給虛擬機使用,性能跟使用真實硬件差不多。
還有一種更廉價的辦法:顯卡透傳。
說的這么專業(yè),其實就是給物理宿主機器插上多個真實顯卡,讓每塊顯卡分配給每個虛擬機獨占享用。
這樣虛擬機的顯示性能跟插上一塊真實顯卡沒什么區(qū)別。
有了這么一塊透傳的顯卡,在虛擬機安裝的也是對應廠商的硬件驅(qū)動,接著的問題就是如何采集顯示數(shù)據(jù)。
當然我們在WIN10平臺有DXGI采集,或者Indirect DIsplay Driver,(下面會簡單講解),
可是在win7平臺,只能使用mirror驅(qū)動,
可是mirror驅(qū)動的局限性,不能利用硬件加速,透傳顯卡的性能沒能充分壓榨。
而且一般的顯卡還有一個特性,一般情況下,得給它插上顯示器,顯卡驅(qū)動才能處于正常的工作狀態(tài),才能設置各種分辨率等。
如果我們給物理宿主機上的10多個顯卡都插上10幾個顯示器,這種場景該是多么的壯觀!
于是我們給顯卡驅(qū)動掛載過濾驅(qū)動,讓它虛擬出一個顯示終端,這樣就不用插真實的顯示器,顯卡的驅(qū)動也能正常工作了。
而且還能從過濾驅(qū)動獲取到顯示的圖像數(shù)據(jù)。
當然從 windows 1607 以后的版本,支持一種 Indirect DIsplay Driver,這個是得到微軟官方支持的框架,
能在現(xiàn)有的顯卡上生成一個額外的顯示終端,這個額外的顯示終端不再是真實顯卡上的HDMI,DIsplay-Port等接口終端,
而是可以其他接口比如USB接口。具體使用什么接口,由Indirect DIsplay Driver開發(fā)者決定。
也許是微軟已經(jīng)意識到上面的這些需求問題,才在最新的系統(tǒng)中增加了這樣的功能。
更具體的描述請看如下鏈接:
https://docs.microsoft.com/en-us/windows-hardware/drivers/display/indirect-display-driver-model-overview
我們再看看 Indirect DIsplay Driver, Indirect Display Driver是 UMD驅(qū)動,就是屬于應用層驅(qū)動。
從windows10 1607以上的操作系統(tǒng)的 \windows\system32\drivers 驅(qū)動目錄下,我們可以找到一個叫 IndirectKmd.sys 的驅(qū)動文件。
這個驅(qū)動文件就是 Indirect Display Driver 對應的內(nèi)核驅(qū)動,再打開這個文件的屬性,會看到文件說明描述為:
“Indirect displays kernel-mode filter driver” 。很顯然,這個就是被微軟官方新增加的真正意義上顯卡過濾驅(qū)動。
我們不能直接使用IndirectKmd.sys,只能通過 Indirect Display Drive框架間接使用。
Indirect Display Driver運行的時候, IndirectKmd.sys 會被掛載到真實顯卡驅(qū)動上去,從而達到虛擬出額外顯示終端的效果。
而在windows 7,windows 8 ,windows10 1607以前的版本,這樣的平臺,很不幸,沒有這樣的東西。
于是我們得自己制造一個“顯卡過濾驅(qū)動”。
開始之前,我們先來看看顯卡驅(qū)動開發(fā)的大致流程。
先看下圖,這圖摘自MSDN文檔:
看這個圖估計還是不明白什么意思.
其實我們開發(fā)windows應用層用戶界面的程序的時候,多少都會跟圖形庫打交道。
估計大家最熟悉的就是GDI圖形庫,而游戲開發(fā)人員最熟悉應該就是DirectX和OpenGL圖像庫。
GDI,DirectDX,OpenGL這三種圖形庫就是Windows為我們提供的基礎圖形庫。
GDI是為了兼容以前的老系統(tǒng),同時為了兼容目前絕大部分普通界面的應用程序,其實一般人用用GDI基本就足夠了。
GDI通過調(diào)用驅(qū)動的 win32k.sys 來模擬畫圖,熟悉mirror鏡像驅(qū)動開發(fā)的都應該知道,
win32k.sys導出的一大堆的Eng*前綴的函數(shù),就是利用CPU模擬畫圖的函數(shù)。
GDI也可以利用顯卡硬件加速,DRIVER_INITIALIZATION_DATA結(jié)構(gòu)導出的DxgkDdiRenderKm 回調(diào)函數(shù)就是在顯卡里加速GDI的。
DirectX和OpenGL圖形庫都需要在應用層提供用戶模式的驅(qū)動,
目的其實就是在應用層渲染圖像,這樣大量的圖像相關(guān)計算全在用戶層完成。
最終的圖像都會通過系統(tǒng)的dxgkrnl.sys驅(qū)動,提交給內(nèi)核模式的顯卡驅(qū)動去管理和分配。
內(nèi)核模式的驅(qū)動主要功能就是對資源的管理分配,顯存和內(nèi)存之間的DMA數(shù)據(jù)傳輸, GPU管理等。
除了GDI加速外,所有復雜圖形渲染加速,都交給應用層驅(qū)動去完成。
可能這里會有一個疑問:
比如有三個窗口模式的程序,一個是DX開發(fā)的游戲,另一個是OpenGL開發(fā)的游戲,還有一個是GDI做的普通界面程序。
然后另外一個程序利用 GDI庫的 BitBlt 抓取桌面屏幕,居然把三個窗口所有圖像都截取到了。
三個互不相關(guān)的圖形庫,如何融合到一起并且被BitBlt抓取到所有圖像?
其實WDDM是個混合模型,也就是三種不同圖形庫的程序生成的圖像,最終會到 dwm.exe桌面窗口管理器混合。
混合成統(tǒng)一的圖像輸出。這樣不管是哪種圖形庫開發(fā)的程序,最終都合并到一起了。
BitBlt函數(shù)最終獲取到混合之后的圖像數(shù)據(jù)。
當然,DirectX屏幕獨占模式的程序,則是另外一回事,這也是絕大部分抓屏軟件無能為力的事。
我們這里只簡單了解內(nèi)核模式的驅(qū)動的大致流程:
在DriverEntry驅(qū)動入口函數(shù)里,
初始化 DRIVER_INITIALIZATION_DATA 數(shù)據(jù)結(jié)構(gòu),
然后調(diào)用 DxgkInitialize 函數(shù)注冊,初始化就完成了,就這么簡單,然而真的簡單嗎?
當你打開 DRIVER_INITIALIZATION_DATA 數(shù)據(jù)結(jié)構(gòu)的聲明,看到至少七八十個的回調(diào)函數(shù),
希望你不會立馬暈厥,當然如果真的暈厥了得馬上就醫(yī)。
而且WDDM模型,從WIN7 的1.1版本開始,到WIN8 的1.2和1.3版本,再到 WIN10 的 2.0, 2.1, 2.2, 2.3版本,
每個版本都會朝里邊塞上更多的回調(diào)函數(shù),真虧微軟的開發(fā)者們能想的出來。
WDDM模型依然是符合WDM規(guī)范的miniport小端口驅(qū)動,有電源管理,PNP即插即用等。
查看DRIVER_INITIALIZATION_DATA 里邊的回調(diào)函數(shù),大致可以把他們分成三大類。
一,普通的WDM驅(qū)動的回調(diào)函數(shù),比如 DxgkDdiAddDevice, DxgkDdiStartDevice等,
一看就明白對應的是 WDM的AddDevice函數(shù)和 IRP_MN_START_DEVICE 請求。
二,DDI函數(shù),就是圖像相關(guān)的,比如圖像資源分配,畫鼠標形狀等。
三,VIDPN函數(shù),用于管理VIDPN。 我們要模擬出一個額外顯示終端,就必須改造VIDPN相關(guān)函數(shù)。
現(xiàn)在關(guān)鍵的問題,如何開發(fā)我們的 “顯卡過濾驅(qū)動” ?
所有顯卡,不管是Intel,NvidIA,ATI等,
他們驅(qū)動的DriverEntry入口函數(shù)都會調(diào)用 DxgkInitialize 注冊 DRIVER_INITIALIZATION_DATA 數(shù)據(jù)結(jié)構(gòu),
我們只要獲取DRIVER_INITIALIZATION_DATA 數(shù)據(jù)結(jié)構(gòu),就完全掌握他們實現(xiàn)的所有回調(diào)函數(shù)。
可能首先想到的就是掛鉤 DxgkInitialize 系統(tǒng)函數(shù),可是非常可惜,DxgkInitialize 不是某個系統(tǒng)模塊導出的接口函數(shù),
而是被靜態(tài)編譯進程序,于是只好逆向工程,分析DxgkInitialize 函數(shù)的調(diào)用過程,從而找到破解規(guī)律。
WDDM出來很長時間,早就已經(jīng)有人找到規(guī)律了,這里也就不再羅嗦,有興趣可以自己去逆向DxgkInitialize 函數(shù)。
DxgkInitialize 會在內(nèi)部調(diào)用ZwLoadDriver 加載 dxgkrnl.sys系統(tǒng)模塊,
dxgkrnl.sys的服務在注冊表固定為 \REGISTRY\MACHINE\SYSTEM\CURRENTCONTROLSET\SERVICES\DXGKrnl 。
這個就是Microsoft DirectX graphics kernel subsystem,用于管理顯卡的miniport小端口驅(qū)動,
同時跟應用層的DirectX,OpenGL等通訊。
而dxgkrnle.sys與顯卡小端口驅(qū)動之間,基本上是通過互相提供回調(diào)函數(shù)來通訊的。舉個例子,
顯卡調(diào)用 DxgkInitialize 注冊DRIVER_INITIALIZATION_DATA ,幾十個或者上百個回調(diào)函數(shù)就注冊到dxgkrnl.sys中,
同時在 DxgkDdiStartDevice回調(diào)函數(shù)的參數(shù),提供一個 PDXGKRNL_INTERFACE參數(shù),
這個參數(shù)就是dxgkrnl.sys提供給顯卡驅(qū)動的回調(diào)函數(shù)集合,用于顯卡驅(qū)動調(diào)用dxgkrnl.sys提供的回調(diào)函數(shù)。
dxgkrnl.sys和顯卡驅(qū)動,就是通過這種機制,互相調(diào)用對方提供的回調(diào)函數(shù),從而達到通訊的目的。
DxgkInitialize 成功加載dxgkrnl.sys之后,就需要獲取dxgkrnl.sys生成的一個設備,設備名為 \Device\Dxgkrnl
因為一會要根據(jù)\Device\Dxgkrnl 這個設備,獲取到dxgkrnl.sys提供的一個回調(diào)函數(shù),這個回調(diào)函數(shù)的功能是把
DRIVER_INITIALIZATION_DATA 數(shù)據(jù)結(jié)構(gòu)注冊到dxgkrnl.sys中。
整個流程的大致代碼如下:
UNICODE_STRING drvPath;
UNICODE_STRING drvName;
RtlInitUnicodeString(&drvPath, L"\\REGISTRY\\MACHINE\\SYSTEM\\CURRENTCONTROLSET\\SERVICES\\DXGKrnl");
RtlInitUnicodeString(&drvName, L"\\Device\\Dxgkrnl");
//加載dxgkrnl.sys驅(qū)動
status = ZwLoadDriver(&drvPath);
///獲取 \Device\Dxgkrnl 設備對象
status = IoGetDeviceObjectPointer(&drvName, FILE_ALL_ACCESS, &dxgkrnl_fileobj, &dxgkrnl_pdoDevice);
PIRP pIrp = IoBuildDeviceIoControlRequest(
IOCTL_VIDEO_DDI_FUNC_REGISTER, //0x23003F , dxgkrnl.sys 導出注冊函數(shù)
dxgkrnl_pdoDevice,
NULL,
0,
&dxgkrnl_dpiInit, //獲取回調(diào)函數(shù)地址
sizeof(PDXGKRNL_DPIINITIALIZE),
TRUE, // IRP_MJ_INTERNAL_DEVICE_CONTROL
&evt,
&ioStatus);
status = IoCallDriver( dxgkrnl_pdoDevice, pIrp);
其中 IOCTL_VIDEO_DDI_FUNC_REGISTER 是個IOCTL,跟Device\Dxgkrnl通訊碼固定為 0x23003F,定義如下:
CTL_CODE( FILE_DEVICE_VIDEO, 0xF, METHOD_NEITHER, FILE_ANY_ACCESS )
dxgkrnl_dpiInit是回調(diào)函數(shù)地址,用于注冊 DRIVER_INITIALIZATION_DATA 數(shù)據(jù)結(jié)構(gòu)到dxgkrnl.sys中,定義如下:
typedef __checkReturn NTSTATUS
DXGKRNL_DPIINITIALIZE(
PDRIVER_OBJECT DriverObject,
PUNICODE_STRING RegistryPath,
DRIVER_INITIALIZATION_DATA* DriverInitData
);
typedef DXGKRNL_DPIINITIALIZE* PDXGKRNL_DPIINITIALIZE;
PDXGKRNL_DPIINITIALIZE dxgkrnl_dpiInit;
成功獲取到 dxgkrnl_dpiInit地址后,調(diào)用 dxgkrnl_dpiInit(DriverObject,RegistryPath,DriverInitData)
就把 DRIVER_INITIALIZATION_DATA數(shù)據(jù)結(jié)構(gòu)注冊到dxgkrnl.sys中了。
所有顯卡驅(qū)動必須調(diào)用DxgkInitialize,而DxgkInitialize是按照上面流程注冊DRIVER_INITIALIZATION_DATA的。
于是我們找到了獲取顯卡 DRIVER_INITIALIZATION_DATA數(shù)據(jù)結(jié)構(gòu)的一個辦法:
給 \Device\Dxgkrnl 設備對象掛載一個FiDO過濾設備,這樣 IOCTL_VIDEO_DDI_FUNC_REGISTER 獲取注冊回調(diào)函數(shù)地址就能截獲到。
然后在 IOCTL_VIDEO_DDI_FUNC_REGISTER 請求中,使用我們自己函數(shù)替換dxgknrl.sys提供的函數(shù)地址,
然后在我們的函數(shù)中,截獲到DRIVER_INITIALIZATION_DATA數(shù)據(jù)結(jié)構(gòu),同時再次調(diào)用dxgkrnl.sys提供的函數(shù),實現(xiàn)真正的注冊。
大致流程如下:
1,開發(fā)一個非常普通的NT過濾式驅(qū)動,在DriverEntry函數(shù)中,提供 IRP_MJ_XXX函數(shù)地址,
for (UCHAR i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; ++i) {
DriverObject->MajorFunction[i] = commonDispatch;
}
然后完成如下2,3兩個步驟:
2, 首先,按照上面代碼的步驟,加載dxgkrnl.sys驅(qū)動,獲取 \Device\Dxgkrnl設備對象,調(diào)用IOCTL_VIDEO_DDI_FUNC_REGISTER,
獲取到 dxgkrnl_dpiInit,dxgkrnl.sys提供的注冊回調(diào)函數(shù)地址,并且保存起來。
3,然后IoCreateDevice 一個過濾設備,調(diào)用 IoAttachDeviceToDeviceStack 掛載到 \Device\Dxgkrnl設備對象上。
4,commonDispatch派遣函數(shù)中,如下實現(xiàn):
static NTSTATUS commonDispatch(PDEVICE_OBJECT devObj, PIRP irp)
{
PIO_STACK_LOCATION irpStack = IoGetCurrentIrpStackLocation(irp);
switch (irpStack->MajorFunction)
{
case IRP_MJ_CREATE:
break;
case IRP_MJ_CLEANUP:
break;
case IRP_MJ_CLOSE:
break;
case IRP_MJ_INTERNAL_DEVICE_CONTROL:
if (irpStack->Parameters.DeviceIoControl.IoControlCode == IOCTL_VIDEO_DDI_FUNC_REGISTER) {
///////顯卡驅(qū)動在DxgkInitialize函數(shù)中調(diào)用 IOCTL獲取dxgkrnl.sys的注冊回調(diào)函數(shù),我們hook此處,獲取到顯卡驅(qū)動提供的所有DDI函數(shù)
irp->IoStatus.Information = 0;
irp->IoStatus.Status = STATUS_SUCCESS;
///把我們的回調(diào)函數(shù)返回給顯卡驅(qū)動.
if (irp->UserBuffer) {
///
irp->IoStatus.Information = sizeof(PDXGKRNL_DPIINITIALIZE);
*((PDXGKRNL_DPIINITIALIZE*)irp->UserBuffer) = DpiInitialize;
}
/////
IoCompleteRequest(irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
///
}
break;
}
////
return call_lower_driver(irp);
}
其中 DpiInitialize 函數(shù)就是我們提供的一個新的注冊函數(shù),DpiInitialize如下實現(xiàn):
NTSTATUS DpiInitialize(
PDRIVER_OBJECT DriverObject,
PUNICODE_STRING RegistryPath,
DRIVER_INITIALIZATION_DATA* DriverInitData)
{
NTSTATUS status = STATUS_SUCCESS;
static BOOLEAN is_hooked = FALSE;
////
UNICODE_STRING vm_str; RtlInitUnicodeString(&vm_str, L"\\Driver\\vm3dmp_loader"); // Vmware 3D
UNICODE_STRING igfx_str; RtlInitUnicodeString(&igfx_str, L"\\Driver\\igfx"); // Intel Graphics
if ( !is_hooked &&
(RtlEqualUnicodeString(&vm_str, &DriverObject->DriverName, TRUE) || RtlEqualUnicodeString(&igfx_str, &DriverObject->DriverName, TRUE) )//vmware里的虛擬顯卡或者Intel顯卡
)
{//這里只HOOK第一個顯卡
is_hooked = TRUE;
///
//這里復制需要注意:
// DRIVER_INITIALIZATION_DATA結(jié)構(gòu)定義,WDDM1.1 到 WDDM2.3 每個都會有不同定義,這里是WDK7下編譯,因此只copy WDDM1.1的部分。
RtlCopyMemory(&wf->orgDpiFunc, DriverInitData, sizeof(DRIVER_INITIALIZATION_DATA));
////replace some function
DriverInitData->DxgkDdiAddDevice = DxgkDdiAddDevice;
DriverInitData->DxgkDdiRemoveDevice = DxgkDdiRemoveDevice;
DriverInitData->DxgkDdiStartDevice = DxgkDdiStartDevice;
DriverInitData->DxgkDdiStopDevice = DxgkDdiStopDevice;
DriverInitData->DxgkDdiQueryChildRelations = DxgkDdiQueryChildRelations;
DriverInitData->DxgkDdiQueryChildStatus = DxgkDdiQueryChildStatus;
DriverInitData->DxgkDdiQueryDeviceDescriptor = DxgkDdiQueryDeviceDescriptor;
DriverInitData->DxgkDdiEnumVidPnCofuncModality = DxgkDdiEnumVidPnCofuncModality; ////
DriverInitData->DxgkDdiIsSupportedVidPn = DxgkDdiIsSupportedVidPn;
DriverInitData->DxgkDdiCommitVidPn = DxgkDdiCommitVidPn;
DriverInitData->DxgkDdiSetVidPnSourceVisibility = DxgkDdiSetVidPnSourceVisibility;
DriverInitData->DxgkDdiSetVidPnSourceAddress = DxgkDdiSetVidPnSourceAddress;
// DriverInitData->DxgkDdiPresent = DxgkDdiPresent;
/////
}
///替換了某些函數(shù)后,接著調(diào)用 dxgkrnl.sys 回調(diào)函數(shù)注冊
return wf->dxgkrnl_dpiInit(DriverObject, RegistryPath, DriverInitData);
}
如上代碼,我只HOOK兩類顯卡,vmware虛擬顯卡和Intel顯卡,因為我所有電腦中,就只有Intel集成顯卡。
代碼中,找到我們需要hook的顯卡驅(qū)動,然后首先保存 DRIVER_INITIALIZATION_DATA結(jié)構(gòu),接著把DRIVER_INITIALIZATION_DATA
的某些函數(shù)替換成我們的函數(shù),這樣,我們就修改某些回調(diào)函數(shù),從而達到虛擬出一個新顯示器的目的。
這里還有一個問題,如何調(diào)用我們的“顯卡過濾驅(qū)動”,以及以怎樣的順序調(diào)用?
我們的驅(qū)動就是個再簡單不過的NT過濾驅(qū)動,直接使用WIN32 API函數(shù)CreateService創(chuàng)建服務,然后啟動服務就可以了。
可是DxgkInitialize 函數(shù)是在顯卡驅(qū)動的 DriverEntry里被調(diào)用的,一般顯卡驅(qū)動在電腦啟動的時候,就被加載了,
之后不會再次調(diào)用DxgkInitialize 。這樣即使我們的驅(qū)動運行了,也無法攔截到 IOCTL_VIDEO_DDI_FUNC_REGISTER注冊請求。
因此必須在顯卡驅(qū)動加載之前,加載我們的過濾驅(qū)動。這需要了解windows啟動的驅(qū)動加載過程。
直接查看注冊表的服務表項:\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services,隨便找一個驅(qū)動服務,
比如Dxgkrnl服務,看到有Start,還有Group,Start決定啟動時間,
如果是0,則在BOOT階段就啟動,這是個非常早的階段,只加載了windows的核心,很多都還沒加載起來
如果是 1, 在System Init階段啟動,這個階段加載了大部分,一般顯卡和DxGkrnl都是在這個階段啟動起來的。
如果是 2,則在系統(tǒng)GUI加載,進入登錄界面之后,被加載。
如果是3則手動加載,4是禁止加載
如果Start都設置1 ,如何決定處于這階段的驅(qū)動,哪些先加載,哪些后加載,Group幫助解決這問題,
Group是字符串,用于把驅(qū)動歸類到某個加載組,每個Group的加載順序根據(jù)
\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\ServiceGroupOrder 提供的順序來決定,
其中 Dxgknrl.sys屬于 Video Init組,而一般顯卡屬于 Video組,Video Init組先于Video加載。
因此,我們在創(chuàng)建服務的時候,設置Start=1,同時組為Video Init, 然后重啟系統(tǒng),這樣我們的驅(qū)動基本都先于顯卡驅(qū)動被加載。
當然可能還有其他情況,需要自己去測試,找到最好辦法,不過至少在我的WIN7系統(tǒng),Intel顯卡是這種規(guī)律。其他的沒試過。
到目前為止,我們的”顯卡過濾驅(qū)動“已經(jīng)能正常運行并且HOOK了顯卡的回調(diào)函數(shù),接下來,就該干實際的事情了。
如何虛擬出一個顯示終端出來,這是我們首先需要解決的問題。
有了虛擬顯示終端,才能進一步截取到輸出到這顯示終端的圖像數(shù)據(jù)。
這需要我們首先熟悉VIDPN,VIDPN英文全名 Video Present Network,視頻展現(xiàn)網(wǎng)絡,
這是一個非常抽象的概念,可能你看MSDN上的文檔,看了半天也不明白是什么意思。
VIDPN是連接顯卡的Source和Target的路徑管理集合,而Source和Target又是什么東西?
Source是顯卡的顯示源,簡單的說,就是顯示表面Surface, 桌面圖像都會畫到這個表面,
現(xiàn)在的顯卡一般都有兩個或以上的Source,一個是主顯示表面,也就是我們大部分時間看到的一個windows桌面,
另外的有些是作為擴展桌面的顯示源Source。
比如某塊顯卡有兩個Source,編號分別0和1,Source0作為主顯示桌面,Source1可以作為擴展顯示桌面。
當然也不是這么硬性規(guī)定,主要看顯卡廠商自己怎么決定。
接下來就是顯示接口的問題,比如使用VGA接口,HDMI,Display-Port接口等。
這個顯示接口,就是Target,也就是顯示源最后都會朝某個或某幾個顯示接口輸出最終的圖像數(shù)據(jù)。
而VIDPN就是用來管理哪些Source朝哪些Target輸出圖像數(shù)據(jù)的一個抽象的管理概念。
Source和Target都有自己的模式集合,所謂模式,就是類似分辨率是多少,顏色深度(32還是16位,當然現(xiàn)在都是32位的了)等。
所有這些模式組成Source和Target的模式集合。
如下圖,摘自MSDN:
顯卡有兩個顯示源,Source1和Source2,Source1朝DVI的Target輸出圖像,
Source2朝HD15和S-Video輸出圖像,并且這兩個圖像是一樣的,屬于Clone,
而Source1和Source2之間是擴展方式,一個是主桌面,另一個則作為擴展桌面。
Source1,2 和DVI,HD15和S-Video之間的連線,就是VIDPN的管理方式,構(gòu)成Topology拓撲結(jié)構(gòu)。
Source是受顯卡本身的設計決定的,也就是我們的“顯卡過濾驅(qū)動”不能隨意添加Source,
比如某塊倒霉的顯卡,只有一個Source,我們就只能用來做主桌面輸出,不能實現(xiàn)擴展桌面功能了。
但是這樣的拓撲結(jié)構(gòu)中,Target是可以隨意添加的,
這也是我們的”顯卡過濾驅(qū)動“能成功虛擬出一個新的顯示終端,以及WIN10 以后平臺的Indirect Display Driver的基礎。
當然添加了一個新的Target,就必須重新設置整個拓撲結(jié)構(gòu),否則就會出問題。
這也是我們的“顯卡過濾驅(qū)動”該解決的問題。
首先,如何添加一個新的Target?
我們HOOK DxgkDdiStartDevice 函數(shù)。此函數(shù)原型為:
NTSTATUS DxgkDdiStartDevice(
IN PVOID MiniportDeviceContext,
IN PDXGK_START_INFO DxgkStartInfo,
IN PDXGKRNL_INTERFACE DxgkInterface,
OUT PULONG NumberOfVideoPresentSources,
OUT PULONG NumberOfChildren);
最后兩個參數(shù)就是分別返回 Source個數(shù),Target個數(shù)。
首先調(diào)用原始的 DxgkDdiStartDevice,然后增加 最后一個參數(shù)就可以了,如下:
status = wf->orgDpiFunc.DxgkDdiStartDevice(MiniportDeviceContext, DxgkStartInfo, DxgkInterface,
NumberOfVideoPresentSources, NumberOfChildren);
wf->vidpn_source_count = *NumberOfVideoPresentSources;
wf->vidpn_target_count = *NumberOfChildren + 1;
*NumberOfChildren = wf->vidpn_target_count; ///增加一個新的Target
接著HOOK DxgkDdiQueryChildRelations,在此函數(shù)中,主要給這個新添加的Target設置ID值。如下:
status = wf->orgDpiFunc.DxgkDdiQueryChildRelations(pvMiniportDeviceContext, pChildRelations, ChildRelationsSize);
PDXGK_CHILD_DESCRIPTOR target = &pChildRelations[wf->vidpn_target_count - 1];
....
target->ChildUid = VIDPN_CHILD_UID;// 正確的做法是遍歷pChildRelations的CHildUid,
然后設置一個不存在的Uid給這個新的target,這里為了簡單,直接定義一個 VIDPN_CHILD_UID宏。
HOOK DxgkDdiQueryChildStatus 和 DxgkDdiQueryDeviceDescriptor 函數(shù),遇到 ChildUid是VIDPN_CHILD_UID的,
都需要自己處理,不再傳遞給原始驅(qū)動。
HOOK了這些函數(shù),好像是完事了,其實更大頭的還在VIDPN的處理。
首先HOOK DxgkDdiEnumVidPnCofuncModality,給dxgkrnl.sys報告我們的新Target的模式集合以及Topology連線規(guī)則。
具體做法是在DxgkDdiEnumVidPnCofuncModality函數(shù)中,枚舉VIDPN的所有路徑,
如果遇到 路徑的終端是VIDPN_CHILD_UID,也就是我們自己的新添加的Target,則設置模式集合。
新增了一個Target,我們必須讓真實的顯卡驅(qū)動不知道有這么一個Target的存在,否則顯卡驅(qū)動會出錯,甚至可能會藍屏。
前面說過的,顯卡驅(qū)動和dxgknrl.sys之間通過回調(diào)函數(shù)來通訊,跟VIDPN相關(guān)的是 在 DxgkDdiStartDevice 回調(diào)函數(shù)的參數(shù)提供的
PDXGKRNL_INTERFACE接口里邊的 DxgkCbQueryVidPnInterface 函數(shù),
這個函數(shù)用于查詢VIDP的Topology接口,這個Topology接口提供的又是一堆的接口函數(shù)。
大致的HOOK的 DxgkDdiStartDevice 代碼如下:
static NTSTATUS DxgkDdiStartDevice(
IN PVOID MiniportDeviceContext,
IN PDXGK_START_INFO DxgkStartInfo,
IN PDXGKRNL_INTERFACE DxgkInterface,
OUT PULONG NumberOfVideoPresentSources,
OUT PULONG NumberOfChildren)
{
NTSTATUS status = STATUS_SUCCESS;
////WDDM1.1 到 WDDM2.3 每個都會有不同定義,這里是WDK7下編譯,因此只copy WDDM1.1的部分。
wf->DxgkInterface = *DxgkInterface; /// save interface function,用于VIDPN設置
///////替換原來的接口
DxgkInterface->DxgkCbQueryVidPnInterface = DxgkCbQueryVidPnInterface;
//////
status = wf->orgDpiFunc.DxgkDdiStartDevice(MiniportDeviceContext, DxgkStartInfo, DxgkInterface, NumberOfVideoPresentSources, NumberOfChildren);
////
DxgkInterface->DxgkCbQueryVidPnInterface = wf->DxgkInterface.DxgkCbQueryVidPnInterface;
///
DPT("Hook: DxgkDdiStartDevice status=0x%X.\n", status ); ///
if (NT_SUCCESS(status)) {
DPT("org: DxgkDdiStartDevice, NumberOfVideoPresentSources=%d, NumberOfChildren=%d\n", *NumberOfVideoPresentSources, *NumberOfChildren);
//// 分別增加 1,增加 source 和 target
wf->vidpn_source_count = *NumberOfVideoPresentSources; // +1;
wf->vidpn_target_count = *NumberOfChildren + 1;
//////
*NumberOfVideoPresentSources = wf->vidpn_source_count;
*NumberOfChildren = wf->vidpn_target_count;
////
}
////
return status;
}
而在 我們自己的HOOK的 DxgkCbQueryVidPnInterface 函數(shù)中:
首先調(diào)用原始的DxgkCbQueryVidPnInterface函數(shù),獲取到 DXGK_VIDPN_INTERFACE 接口函數(shù)集合。
這個接口里邊大部分是模式管理相關(guān),有個 pfnGetTopology 接口,是我們需要HOOK,因為在里邊才能屏蔽我們新的Target。
HOOK了這個接口之后,發(fā)現(xiàn)pfnGetTopology 被調(diào)用的時候,會獲取到 DXGK_VIDPNTOPOLOGY_INTERFACE 接口,
里邊又是一大堆的回調(diào)函數(shù),在 DXGK_VIDPNTOPOLOGY_INTERFACE這個接口中,我們主要HOOK里邊的
pfnGetNumPaths, pfnGetNumPathsFromSource,pfnEnumPathTargetsFromSource,pfnAcquireFirstPathInfo,pfnAcquireNextPathInfo
等函數(shù),這些都是跟我們新添加的Target 路徑相關(guān)的 ,在這些函數(shù)中,屏蔽掉我們新添加的UID是VIDPN_CHILD_UID的 Target 。
非常繁瑣,這里也就不羅嗦了,我目前發(fā)現(xiàn)的主要是上面的函數(shù),如果還有其他沒有HOOK的,有興趣的而且又知道的請?zhí)岢鰜怼?/p>
解決了VIDPN的問題,我們的“顯卡過濾驅(qū)動”運行之后,終于能在電腦中看到我們新增加的這塊顯示器了,
下面是效果圖:
這個圖可能有點看不明白,
大屏幕是臺式機,裝的win7系統(tǒng),被作為擴展屏幕,
而筆記本電腦上和大屏幕上的瀏覽器里邊xdisp_virt(xdisp_virt是網(wǎng)頁方式遠程控制,詳細查閱前面的文章)顯示的是主桌面。
————————————————
版權(quán)聲明:本文為CSDN博主「雨中風華」的原創(chuàng)文章,遵循 CC 4.0 BY-SA 版權(quán)協(xié)議,轉(zhuǎn)載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/fanxiushu/article/details/82731673