Windows桌面實現之七(DirectX HOOK 方式截取特殊的全屏程序之一)

                                                                            by fanxiushu 2019-04-16 轉載或引用請註明原始作者。
因爲間隔的時間較長,爲了方便查閱,下面是以前的六篇文章地址鏈接:
https://blog.csdn.net/fanxiushu/article/details/73269286 Windows遠程桌面實現之一 (抓屏技術總覽 MirrorDriver,DXGI,GDI)
https://blog.csdn.net/fanxiushu/article/details/76039801 Windows遠程桌面實現之二(抓屏技術之MirrorDriver鏡像驅動開發)
https://blog.csdn.net/fanxiushu/article/details/77013158 Windows遠程桌面實現之三(電腦內部聲音採集,錄音採集,攝像頭視頻採集)
https://blog.csdn.net/fanxiushu/article/details/78869719 Windows遠程桌面實現之四(在現代瀏覽器中通過普通頁面訪問遠程桌面)
https://blog.csdn.net/fanxiushu/article/details/80996391 Windows遠程桌面實現之五(FFMPEG實現桌面屏幕RTSP,RTMP推流及本地保存)
https://blog.csdn.net/fanxiushu/article/details/81905680 Windows遠程桌面實現之六(新版本框架更新,以及網頁HTML5音頻採集通訊

這裏還有一篇文章,是以HOOK 顯卡的WDDM驅動的各類回調函數的辦法來採集數據:
https://blog.csdn.net/fanxiushu/article/details/82731673  
WIN7以上系統WDDM虛擬顯卡開發(WDDM Filter/Hook Driver 顯卡過濾驅動開發之一)
這並不是一個值得推薦的辦法。

對應的程序請到GITHUB下載最新版本:
https://github.com/fanxiushu/xdisp_virt


前三篇都是介紹如何採集各種數據,尤其是windows的桌面圖像數據,其中GDI最通用,也最簡單,其次是mirror和DXGI,
但 DXGI(其實是 DXGI Desktop Duplication API,爲了簡單就叫DXGI )只適合win8以上系統。

但有類特殊的程序,就是獨佔方式的全屏3D程序,這裏不單是全屏遊戲,還可能是其他獨佔方式的全屏程序,比如某些視頻播放軟件。
但是各種全屏獨佔程序中,以遊戲最多。因此下面介紹中多以全屏遊戲作爲測試主要對象。
全屏獨佔模式,就是程序代替windows的桌面管理器,接管整個顯示器的顯示,
程序產生的圖像數據不再經過桌面管理器的混合,而是程序直接朝顯示器輸出。
很早前的DDRAW圖形組件就是做這種事的,從DirectX8開始取消了DDraw,但是因爲使用DDRAW的老程序很多,
因此ddraw一直都存在,並且也兼容各種windows系統,包括最新的WIN10,當然越新的系統對DDRAW的功能大部分都是軟件模擬。
DirectX從8 ,到9, 到10 ,再到11,各個版本中,對全屏獨佔方式都不曾改變。
不過最新的WIN10中,不知道是從1803還是1809開始,對全屏獨佔方式似乎做了很大的弱化,DirectX12似乎對全屏獨佔模式只是模擬。
不管怎樣,在WIN7系統中,因爲全屏獨佔模式的存在,給截取windows桌面圖像帶來很大麻煩。
比如最典型的,WIN7中的 Windows Media Center 程序,我對這個win7自帶的程序其實並不瞭解,因爲平時不使用它,
不過用它來測試全屏抓屏效果,倒也不錯。

其實熟悉DirectX的,通過分析MediaCenter加載的DLL庫,很容易看出來,
它實際上使用 Direct3D9, 不光是MediaCenter,很多程序都是使用D3D9開發的,比如迅雷影音等視頻播放軟件,
因爲DX9可以支持WINXP到WIN7到WIN10. 也就是兼容性好,當WINXP徹底淘汰之後,DX9的接替者就是DX11 。
當Media Center程序進入全屏之後,便開啓了全屏獨佔,它接替了windows桌面管理器,
這個時候windows桌面管理器是失效的。
我們再用  GetDC(GetDeskTopWIndow())  ( 或者GetDC(NULL)  )
獲取到就只是黑屏或者就是程序進入全屏前那一剎那windows桌面的內容。
也就是GDI抓屏在這種情況下是無法工作的,
再來看看mirror驅動方式抓屏,在第二篇文章中就曾講過mirror的原理,它是誕生於XP時代,主要是2D圖像DDI繪圖指令。
而WIN7以上是3D繪圖指令,全屏DirectX程序更是清一色的Direct3D加速指令,
mirror是適用於XP時代的XPDM顯卡驅動模型,WIN7以上是WDDM模型,mirror之所以還能運行在WIN7,WIN8,WIN10 ,
主要是WIN7以後的系統爲了保持兼容性而模擬的。
WDDM驅動中有個 DxgkddiPresent回調函數,這個函數就是把後臺緩存的圖像數據呈現到顯示終端,
類似於2D圖像指令中的 BitBlt 的功能。
Present這個函數不管是DirectX圖形庫,還是應用層的WDDM顯示驅動,還是WDDM內核驅動,都會多次出現。
我們這篇文章講的HOOK方式截屏,也是對這個Present函數入手,下面會詳細講解。
可以這麼推測,WIN7在模擬mirror驅動的時候,並沒有從Present方面去模擬,還是遵循WINXP原來的工作方式,
因此WIN7中的mirror無法解決全屏獨佔程序抓屏問題。
但這種情況在WIN10有所改變,因爲我使用 ”極品飛車17“ 等全屏獨佔遊戲在WIN10(1809版本)平臺下,
使用DXGI和mirror都能正確截屏全屏遊戲(當然GDI還是無法全屏截屏),因此可以推測,WIN10中的mirror模擬得更徹底,
基本上是從Present中模擬mirror的DrvBitBlt的2D繪圖指令。

再來看看windows桌面管理器,這究竟是個什麼東西。
他是windows中的一個桌面管理集合,應該包括多個系統組件,其中我們能看得到的就是一個叫 dwm.exe 程序。
這個dwm.exe程序在WIN7似乎可有可無,但是開啓AERO效果必須有dwm.exe。
win7中不存在dwm.exe並不意味着桌面管理器就不運行了,只是切換到了XP以前那種桌面管理模式而已。
而到WIN8以上系統,dwm.exe必須存在,沒它整個windows桌面就掛了。
這也是windows開發中的一進步,同時更加穩固了他們對桌面管理的概念。
WIN7有幾種桌面樣式:
1,經典樣式,就是窗口呈現3維效果的樣子,這是我最常用的,因爲看起來簡潔清爽。
2,WIN7 Basic,
3,AERO,半透明毛玻璃效果,這是我感覺最花裏胡哨的界面,不大常用,而且很佔資源。
其中前兩種不需要dmw.exe程序都能正常使用,AERO必須要開啓dwm.exe。
我們再來簡單看看桌面管理器都做些什麼核心工作,
windows中一切都是窗口化的,各種窗口,什麼頂層窗口,子窗口,隱藏窗口等等,他們的祖宗都是桌面窗口。
窗口包括外框(frame)和客戶區(client area),窗口外框由桌面管理器負責繪製,客戶區則是程序負責繪製。
我們在程序中使用 GDI, DirectX,OpenGL圖形庫(還有目前被吹噓的新vulkan圖形庫)在客戶區繪製出各種圖形,
然後桌面管理器負責混合我們各種程序繪製的圖像,判定哪些被遮擋不能顯示,哪些可以顯示。
然後混合了之後最後輸出到顯示終端。
這也是我們在普通截屏的時候,使用GetDC(NULL)就能截取整個電腦屏幕,因爲所有程序都是在窗口管理器中進行混合的,
使用GetDC自然就能截屏。
當然,這裏邊也有些另類的,比如某個使用DDRAW窗口程序,而且恰巧顯卡也支持DDRAW硬件加速,
於是DDRAW畫圖繞過了窗口管理器混合,直接朝顯示器輸出。
(這是我使用VLC播放DirectDraw方式輸出的視頻的時候,非全屏,在Intel集成顯卡和一塊N卡測試得出的不同結果:
使用GDI抓屏,WIN7系統,N卡抓不到VLC播放的圖像,VLC播放窗口是黑的,在Intel集成顯卡上卻能看到,可見N卡支持DDRAW硬件加速)
這裏似乎有些矛盾,既然不是全屏獨佔模式的窗口程序,桌面管理器還是有效的,
那他還是應該老老實實的混合圖像,GetDC就應該抓取到所有圖像,可是DDRAW有些特別。
總感覺那個時候搞得有些混亂的。畢竟那個時候顯卡性能不強,得想各種辦法來提升顯示性能。

在WINXP以及以前的系統,在WIN7沒有開啓AERO效果的情況下,窗口混合都是實時混合的,什麼意思呢?
就是應用程序在窗口客戶區畫圖,繪製圖形交給桌面管理器,
桌面管理器立馬根據這個窗口所處的位置,決定哪些區域該顯示,哪些不該顯示。
然後立馬顯示到顯示終端上。不會保留圖像備份,這在當時顯卡性能不強,顯存很小的情況下,這麼做是明智的。
不過這樣帶來一個問題,比如我們想單獨截取某個窗口的圖像,
如果這個窗口被遮擋了,就截取不到被遮擋的部分,因爲這部分圖像數據都不存在了。

而到了WIN7開啓AERO特效,WIN8,WIN10以後的系統,系統內核爲每個窗口客戶區,在顯存裏專門開闢了一塊顯存地址,
也叫離屏表面(off-screen),微軟也把它稱爲重定向表面。
程序在窗口客戶區不管是使用GDI,DirectX,OpenGL等畫圖,都會畫到這塊離屏表面上。
然後窗口管理器,從這些離屏表面混合圖像,然後統一朝顯示終端輸出。
這是dwm.exe主要做的事情,也是不能強行關閉dwm.exe原因。
其中WIN7的AERO特性的做法有些特別,它混合圖像之後,可能是做些透明方面的處理,
然後使用DirectX朝顯示終端輸出,也就是它把整個桌面混合圖像再使用DirectX朝顯示器輸出。
這很浪費資源。

既然WIN7的AERO特性是如此做的,
我們顯然在這種情況下,直接掛鉤WDM.exe程序,攔截它的DirectX函數,就能截取整個桌面圖像了。
這也是比GDI更高效的截圖辦法,當然只適合WIN7,而且是開啓AERO特效的情況下。

而WIN8,WIN10中的dwm.exe輸出最終桌面圖像的時候,並沒有使用DirectX,可能是更底層的驅動方式輸出或交給其他系統組件完成的。
並且從WIN8以上系統,CreateWindowEx創建窗口的時候,提供了一個叫 WS_EX_NOREDIRECTIONBITMAP 的 擴展Style。
前面說過了,每個窗口客戶區域,在顯存都會對應一個重定向表面,所有繪圖都會畫到這個表面上。這是WIN8以上普通窗口的做法。
可是需求總是很多,有些程序不想這麼做,不想要重定向表面。
因爲它想自己創建和管理這個表面,讓桌面管理器從這個程序自己新創建的表面取圖像來混合,
於是CreateWindowEx的時候,設置WS_EX_NOREDIRECTIONBITMAP 告訴系統,我不需要重定向表面。
最典型的就是WIN10上的UWP程序,它的窗口風格都是清一色的 WS_EX_NOREDIRECTIONBITMAP。
它通常配合DirectX一起使用,比如IDXGIFactory2 中的 CreateSwapChainForComposition,就是創建一個複合交換鏈,
再配合DirectComposition API或者XAML一起在 UWP 窗口某個區域繪圖,因爲不是畫到重定向表面,而是保存到DirectX自己的內部緩存。
桌面管理器直接從DirectX中取圖像。
這也引出一個問題,就是這種窗口,我們是無法通過GetDC(HWND)截取到窗口內容的,
所以UWP程序,無法通過傳統的GDI截取窗口圖像的。

介紹了上面的知識,我們再來看看如何HOOK DirectX等圖形庫並且截圖,既然提到是HOOK,
自然免不了要把我們的dll注入到其他進程裏,不注入如何能HOOK,不HOOK如何能截取到程序的繪圖操作。
最常用也最正規的無非就兩種:
1,調用SetWindowHookEx,設置消息鉤子,比如WH_GETMESSAGE,
只要對方的程序是調用了user32.dll建立了消息循環
都會自動被注入。
有窗口的程序都遵循這個規律的,圖形庫沒窗口是畫不出來的,
因此使用WH_GETMESSAGE鉤子,幾乎都能注入所有調用圖形庫的程序。當然UWP是個例外。
2,CreateRemoteThread,在指定進程中創建一個線程,在此線程中調用LoadLibrary加載dll,這個方法同樣適用於UWP程序。

具體如何操作,這裏也就不再贅述了,可查閱其他相關資料。

之後是如何HOOK DirectX,我們需要熟悉DirectX圖形庫,
未完待續。。。
下圖是採用HOOK DirectX辦法,截取全屏MediaCenter的效果圖,請關注

https://github.com/fanxiushu/xdisp_virt 上的xdisp_virt,稍後會把這部分功能發佈上去。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章