戰(zhàn):用Detours庫(kù)截獲EndScene實(shí)現(xiàn)游戲鉤子)
簡(jiǎn)介面向游戲開(kāi)發(fā)者的API鉤子技術(shù)資料包專(zhuān)注于截獲DirectX接口調(diào)用可用于性能分析、調(diào)試、作弊檢測(cè)及游戲模組制作等場(chǎng)景。壓縮包共12個(gè)文件以C源代碼和頭文件為主涵蓋鉤子函數(shù)聲明、工程配置、導(dǎo)入庫(kù)與預(yù)編譯頭文件整體僅71KB輕量且結(jié)構(gòu)清晰便于快速查閱。文件類(lèi)型方面三個(gè)C源文件實(shí)現(xiàn)鉤子的設(shè)置與移除邏輯四個(gè)頭文件提供接口聲明工程文件與組件定義文件用于構(gòu)建和管理項(xiàng)目核心的Detours庫(kù)則簡(jiǎn)化了底層掛鉤操作。技術(shù)實(shí)現(xiàn)上資源基于微軟Detours庫(kù)通過(guò)字節(jié)碼注入方式掛鉤DirectX函數(shù)并演示了從初始化、設(shè)置鉤子到退出解除鉤子的完整流程。讀者可參考ReplaceApi等代碼學(xué)習(xí)如何聲明鉤子函數(shù)、調(diào)用DetourAttach綁定原接口從而在游戲運(yùn)行時(shí)動(dòng)態(tài)改變渲染效果、捕獲輸入事件或記錄調(diào)用日志為后續(xù)二次開(kāi)發(fā)打下基礎(chǔ)。該資料已有五百四十人學(xué)習(xí)適合具備基礎(chǔ)C知識(shí)、希望深入圖形底層機(jī)制的游戲開(kāi)發(fā)者和模組作者參考能夠幫助快速理解攔截原理并搭建自己的擴(kuò)展框架。1. APIHOOK鉤子截獲DirectX API游戲開(kāi)發(fā)里的手術(shù)刀還是定時(shí)炸彈想在游戲運(yùn)行時(shí)不改源碼就插入自己的代碼APIHOOK鉤子截獲DirectX API函數(shù)調(diào)用是繞不開(kāi)的技術(shù)路線。游戲開(kāi)發(fā)里主要用它干三件事性能分析、功能調(diào)試、做MOD或者反外掛檢測(cè)。你拿到的這個(gè)壓縮包是一個(gè)完整的C實(shí)現(xiàn)樣例——用微軟Detours庫(kù)掛住DirectX API里面有源碼、頭文件、Detours依賴(lài)庫(kù)和Visual Studio工程文件照著編一遍再看懂ReplaceApi和HookApi這對(duì)文件就能把它遷移到自己的游戲或工具里。它不是玩具demo而是接近實(shí)戰(zhàn)的一套鉤子框架。適合三類(lèi)人想給老游戲加功能但沒(méi)源碼的開(kāi)發(fā)者、研究游戲渲染和輸入管線的學(xué)習(xí)者、做引擎底層的工程師。2. 從壓縮包到能跑的工程文件清單、VS版本遷移與Detours庫(kù)匹配2.1 壓縮包里每一份文件是干什么的打開(kāi)壓縮包第一眼看到的是一堆后綴各異的文件。先別急著編譯花兩分鐘把每份文件歸位后面能少踩一半的坑。這個(gè)包的結(jié)構(gòu)其實(shí)很經(jīng)典是VS6時(shí)代的標(biāo)準(zhǔn)插件工程布局放現(xiàn)在依然能看懂。文件類(lèi)型作用StdAfx.h / StdAfx.cpp預(yù)編譯頭集中收錄公共頭文件和宏首次編譯后生成.pch加速后續(xù)編譯HookApi.dsw / HookApi.dspVS6工程文件dsw是工作區(qū)dsp是項(xiàng)目文件代表這份代碼的原始編譯環(huán)境HookApi.def模塊定義文件聲明DLL導(dǎo)出函數(shù)控制導(dǎo)出符號(hào)名和序號(hào)HookApi.h / HookApi.cpp鉤子實(shí)現(xiàn)安裝和移除鉤子的核心C源碼ReplaceApi.h / ReplaceApi.cpp替換實(shí)現(xiàn)定義替代函數(shù)體處理被攔截后要執(zhí)行的自定義邏輯detours.h / detours.libDetours庫(kù)微軟研究院提供的API掛鉤庫(kù)負(fù)責(zé)字節(jié)碼級(jí)的函數(shù)跳轉(zhuǎn)與恢復(fù)HookApi.bbs中間產(chǎn)物VS6生成的瀏覽信息文件編譯殘留對(duì)功能沒(méi)有影響這里面最值得先讀的是ReplaceApi.cpp和HookApi.cpp。HookApi.cpp負(fù)責(zé)“掛”ReplaceApi.cpp負(fù)責(zé)“替”兩者配合才是完整的一套鉤子。StdAfx.h里通常還有調(diào)試開(kāi)關(guān)或者通用宏后面編譯報(bào)錯(cuò)時(shí)經(jīng)常要先回來(lái)看它。2.2 老工程在新版Visual Studio里怎么打開(kāi)原始工程是VS6時(shí)代的.dsw/.dsp格式。我一般不建議直接雙擊.dsw讓新版VS轉(zhuǎn)換因?yàn)檗D(zhuǎn)換過(guò)程中經(jīng)常出幺蛾子——字符集設(shè)置、平臺(tái)工具集、依賴(lài)庫(kù)路徑全亂。更穩(wěn)的做法是新建一個(gè)工程把源碼文件手動(dòng)拖進(jìn)去配置路徑重來(lái)一遍。# 推薦操作順序Windows下在工程根目錄執(zhí)行 mkdir build cd build # 1. 打開(kāi)VS開(kāi)發(fā)者命令行確認(rèn)cl.exe可用 # 2. 在VS里新建一個(gè)Win32項(xiàng)目或空項(xiàng)目 # 然后把以下文件加入工程 # StdAfx.h / StdAfx.cpp / HookApi.h / HookApi.cpp # ReplaceApi.h / ReplaceApi.cpp # 3. 把Detours的頭文件和庫(kù)拷貝到工程目錄方便引用 copy ..\detours.h . copy ..\detours.lib .這段腳本不是自動(dòng)編譯命令而是操作清單。為什么新建項(xiàng)目而不是直接轉(zhuǎn)換因?yàn)?dsw轉(zhuǎn)換后配置項(xiàng)容易丟尤其“配置類(lèi)型”和“附加依賴(lài)項(xiàng)”與其去猜不如重建。參數(shù)上需要注意工程類(lèi)型選“動(dòng)態(tài)鏈接庫(kù)(DLL)”因?yàn)橐雁^子注入游戲進(jìn)程最后生成的一定是DLL而不是exe。字符集這塊是個(gè)經(jīng)典老坑。VS6時(shí)代默認(rèn)多字節(jié)字符集新版VS默認(rèn)Unicode。如果代碼里的char*去接API返回的寬字符串編譯報(bào)錯(cuò)是必然的。我的習(xí)慣是屬性→常規(guī)→字符集改成“使用多字節(jié)字符集”讓新編譯器去適配老代碼而不是反過(guò)來(lái)改源碼。2.3 Detours庫(kù)版本匹配與依賴(lài)路徑detours.h和detours.lib必須匹配且必須與目標(biāo)程序位數(shù)一致。VS6的老工程默認(rèn)是32位如果你的目標(biāo)游戲是32位——那個(gè)年代的PC游戲基本沒(méi)有64位——就用32位Detours如果要鉤64位程序就得切換平臺(tái)工具集到x64并換成64位的detours.lib。提示編譯報(bào)錯(cuò)“無(wú)法解析的外部符號(hào)__imp_DetourAttach”十有八九是lib版本和工程平臺(tái)不匹配先查平臺(tái)再查路徑不要一上來(lái)就懷疑源碼。我見(jiàn)過(guò)最離譜的情況是把64位的detours.lib硬塞進(jìn)32位工程鏈接器直接報(bào)一堆無(wú)法解析的外部符號(hào)。排查了半天才發(fā)現(xiàn)是庫(kù)版本問(wèn)題。所以第一次編譯失敗時(shí)先看三件事平臺(tái)是不是x86detours.lib是不是對(duì)應(yīng)的x86版本路徑是否真的被加進(jìn)了“附加庫(kù)目錄”。這三項(xiàng)檢查完絕大多數(shù)編譯問(wèn)題都能定位。還有一個(gè)容易被忽略的點(diǎn)detours.h在某些老版本里依賴(lài)windows.h的特定宏如果項(xiàng)目設(shè)置了強(qiáng)制預(yù)編譯頭包含順序?qū)戝e(cuò)也會(huì)編譯失敗標(biāo)準(zhǔn)順序是StdAfx.h在最前。3. 核心實(shí)現(xiàn)用DetourAttach替換DirectX API函數(shù)的完整套路3.1 DirectX掛鉤的特殊性函數(shù)藏在虛函數(shù)表里如果一個(gè)API是普通DLL導(dǎo)出函數(shù)比如MessageBoxADetours掛起來(lái)很簡(jiǎn)單直接在導(dǎo)出地址上下鉤子就行。但DirectX完全不同——Direct3D9的接口是COM風(fēng)格真正的方法如EndScene、Present不是從DLL導(dǎo)出的而是存在接口對(duì)象的虛函數(shù)表vtable里。這意味著你要先拿到接口指針再?gòu)奶摵瘮?shù)表里取出目標(biāo)槽位的函數(shù)地址最后才能把這個(gè)地址交給DetourAttach。這也解釋了為什么很多人用Detours鉤不了DirectX他們以為直接對(duì)d3d9.dll的導(dǎo)出表下鉤子就能掛上實(shí)際上d3d9.dll里導(dǎo)出的是Direct3DCreate9這種工廠函數(shù)而EndScene的實(shí)現(xiàn)在設(shè)備的虛表里。常見(jiàn)做法是先調(diào)用Direct3DCreate9創(chuàng)建一個(gè)設(shè)備用來(lái)獲取地址或者在游戲運(yùn)行過(guò)程中從現(xiàn)有設(shè)備指針出發(fā)直接讀虛表。后一種更實(shí)用因?yàn)檫M(jìn)游戲后設(shè)備早就建好了。3.2 定義原函數(shù)類(lèi)型、替換函數(shù)與Trampoline指針下面這段是標(biāo)準(zhǔn)掛鉤骨架Windows下用C寫(xiě)目標(biāo)用D3D9最常見(jiàn)的掛鉤點(diǎn)EndScene做例子// 以Direct3D9的EndScene為例這是幀渲染結(jié)束前的最后一站 // 聲明原函數(shù)指針類(lèi)型調(diào)用約定必須和COM接口保持一致 typedef HRESULT (WINAPI* EndSceneFn)(LPDIRECT3DDEVICE9); // Trampoline指針保存被替換前的真實(shí)函數(shù)地址 EndSceneFn RealEndScene nullptr; // 替換函數(shù)游戲每幀渲染結(jié)束前都會(huì)走到這里 HRESULT WINAPI HookEndScene(LPDIRECT3DDEVICE9 pDevice) { // 先調(diào)用原始函數(shù)保證渲染流程不被破壞 HRESULT hr RealEndScene(pDevice); // 在這里插入自己的邏輯畫(huà)FPS、寫(xiě)日志、改渲染參數(shù)…… // 注意此代碼在渲染線程執(zhí)行耗時(shí)操作要克制 return hr; }邏輯說(shuō)明RealEndScene是一個(gè)函數(shù)指針變量保存的是原始EndScene入口地址。Detours庫(kù)會(huì)把原函數(shù)入口處的幾條指令改寫(xiě)成跳轉(zhuǎn)指令跳到HookEndScene同時(shí)把被覆蓋的指令搬到一個(gè)私有區(qū)域也就是Trampoline。所以鉤子函數(shù)里必須先調(diào)RealEndScene再執(zhí)行自己的邏輯順序反了會(huì)讓整個(gè)渲染鏈路斷裂。參數(shù)說(shuō)明WINAPI代表__stdcall調(diào)用約定LPDIRECT3DDEVICE9是D3D9設(shè)備接口指針。EndScene接收一個(gè)設(shè)備指針后續(xù)所有繪制調(diào)用都靠這個(gè)指針發(fā)起。3.3 安裝鉤子的Transaction從Begin到CommitDetours要求安裝和卸載都放在事務(wù)里不能簡(jiǎn)單粗暴地直接附加。下面這段是完整安裝流程#include detours.h // 從設(shè)備虛函數(shù)表里取出EndScene的槽位地址 // 注意不同DirectX SDK版本里索引可能不同這里以常見(jiàn)版本為例 void* GetEndSceneAddress(LPDIRECT3DDEVICE9 pDevice) { // COM對(duì)象首地址指向虛表指針虛表指針再指向函數(shù)指針數(shù)組 void*** vtable reinterpret_castvoid***(pDevice); return vtable[42]; // EndScene在IDirect3DDevice9 vtable中的索引 } // 安裝鉤子 void InstallHook(LPDIRECT3DDEVICE9 pDevice) { void* endSceneAddr GetEndSceneAddress(pDevice); if (!endSceneAddr) return; // 1. 開(kāi)始一個(gè)Detours事務(wù) DetourTransactionBegin(); // 2. 把當(dāng)前線程加入事務(wù) // 多線程場(chǎng)景要把所有可能執(zhí)行目標(biāo)函數(shù)的線程都加進(jìn)來(lái) DetourUpdateThread(GetCurrentThread()); // 3. 把真實(shí)地址關(guān)聯(lián)到Trampoline同時(shí)把替換函數(shù)掛上去 // 注意DetourAttach的參數(shù)順序Trampoline指針在前鉤子函數(shù)在后 DetourAttach((PVOID)RealEndScene, HookEndScene); // 4. 提交事務(wù)此時(shí)才真正改寫(xiě)目標(biāo)函數(shù)入口處的指令 if (DetourTransactionCommit() ! NO_ERROR) { // 事務(wù)失敗回滾所有修改 DetourTransactionAbort(); } }這段代碼有幾個(gè)關(guān)鍵點(diǎn)。第一GetEndSceneAddress用了三級(jí)指針的reinterpret_cast因?yàn)镃OM對(duì)象布局是“對(duì)象首字節(jié)→虛表指針→函數(shù)指針數(shù)組”取地址必須解引用兩次。第二vtable[42]是經(jīng)驗(yàn)值——在主流D3D9 SDK里EndScene排在第42個(gè)虛函數(shù)但如果目標(biāo)游戲用的SDK版本不同這個(gè)索引會(huì)漂移后面避坑章節(jié)會(huì)細(xì)說(shuō)。第三DetourTransactionBegin和DetourTransactionCommit必須成對(duì)Commit失敗要立刻Abort否則內(nèi)存里留著半截改動(dòng)進(jìn)程幾乎必然崩潰。3.4 卸載鉤子的對(duì)稱(chēng)操作卸載是安裝的鏡像操作很多人在這里翻車(chē)是因?yàn)閰?shù)寫(xiě)反了或者多線程場(chǎng)景下在線程還停在鉤子函數(shù)里時(shí)強(qiáng)行卸載。void UninstallHook() { if (RealEndScene nullptr) return; // 開(kāi)始卸載事務(wù) DetourTransactionBegin(); DetourUpdateThread(GetCurrentThread()); // 參數(shù)順序與Attach完全一致Trampoline指針在前鉤子函數(shù)在后 DetourDetach((PVOID)RealEndScene, HookEndScene); if (DetourTransactionCommit() ! NO_ERROR) { DetourTransactionAbort(); } }邏輯說(shuō)明DetourDetach把原函數(shù)入口處的指令恢復(fù)原樣并把Trampoline指針歸位。參數(shù)寫(xiě)反的直接后果是Detours找不到要恢復(fù)的函數(shù)卸載后原函數(shù)指令變成一堆亂跳轉(zhuǎn)游戲在下一幀就崩。我的血淚經(jīng)驗(yàn)是Attach和Detach永遠(yuǎn)放在同一份代碼里對(duì)稱(chēng)寫(xiě)復(fù)制粘貼時(shí)不要打亂順序。另外一個(gè)習(xí)慣是如果程序退出時(shí)不需要再調(diào)用原函數(shù)干脆不寫(xiě)卸載邏輯讓進(jìn)程退出時(shí)操作系統(tǒng)回收DLL反而比強(qiáng)制Detach更安全。但這只適用于進(jìn)程即將結(jié)束的場(chǎng)景如果是熱卸載插件卸載邏輯就必須嚴(yán)謹(jǐn)。4. 避坑記錄DirectX鉤子最容易翻車(chē)的五個(gè)瞬間以下五條都是實(shí)際項(xiàng)目里踩過(guò)或者幫別人排查過(guò)的屬于血淚經(jīng)驗(yàn)。第一次跑就全綠是運(yùn)氣好翻車(chē)了按現(xiàn)象對(duì)號(hào)入座即可。4.1 現(xiàn)象一鉤子裝上后目標(biāo)函數(shù)紋絲不動(dòng)原因最常見(jiàn)的是拿到的函數(shù)地址根本不對(duì)。直接掛在d3d9.dll的導(dǎo)出表上或者虛表索引寫(xiě)錯(cuò)DetourAttach掛在一個(gè)無(wú)關(guān)函數(shù)上自然沒(méi)有任何效果。 解決在調(diào)試器里打印真實(shí)地址。用Visual Studio的Watch窗口觀察pDevice指向的虛表數(shù)清楚目標(biāo)函數(shù)在第幾個(gè)槽位。不要憑記憶硬編碼索引索引一定要實(shí)測(cè)確認(rèn)。4.2 現(xiàn)象二一掛上去游戲立刻訪問(wèn)沖突崩潰原因函數(shù)簽名不匹配。聲明用的是HRESULT (WINAPI*)(LPDIRECT3DDEVICE9)但真實(shí)函數(shù)可能是不同的調(diào)用約定或參數(shù)個(gè)數(shù)導(dǎo)致棧不平衡返回時(shí)跳錯(cuò)地址。 解決用typedef嚴(yán)格匹配真實(shí)簽名再檢查一遍調(diào)用約定。還有一個(gè)隱蔽問(wèn)題如果替換函數(shù)里用了與設(shè)備無(wú)關(guān)的全局狀態(tài)要考慮線程安全——EndScene在渲染線程執(zhí)行如果統(tǒng)計(jì)變量在另一個(gè)UI線程被讀寫(xiě)需要加鎖或用原子操作否則調(diào)試時(shí)會(huì)看到隨機(jī)崩潰。4.3 現(xiàn)象三編譯時(shí)找不到DetourAttach符號(hào)原因detours.lib沒(méi)有真正鏈接。常見(jiàn)的是detours.h放進(jìn)包含目錄了但lib沒(méi)加入附加庫(kù)目錄或者lib文件在但路徑?jīng)]配。 解決工程屬性→鏈接器→常規(guī)→附加庫(kù)目錄填lib所在路徑鏈接器→輸入→附加依賴(lài)項(xiàng)寫(xiě)detours.lib。改完看編譯輸出確認(rèn)“無(wú)法解析的外部符號(hào)”是否消失。4.4 現(xiàn)象四卸載鉤子的瞬間游戲崩潰原因卸載時(shí)還有線程停在HookEndScene里或者DetourDetach參數(shù)寫(xiě)反。Detours卸載需要線程安全某線程正在執(zhí)行鉤子函數(shù)時(shí)強(qiáng)行恢復(fù)指令等于地板突然抽掉。 解決卸載前先暫停所有可能執(zhí)行渲染代碼的線程如果做不到就在進(jìn)程退出路徑上只做Detach不重啟線程。更務(wù)實(shí)的選擇像2.2節(jié)說(shuō)的那樣很多鉤子工程干脆不寫(xiě)卸載邏輯等進(jìn)程退出讓系統(tǒng)回收。4.5 現(xiàn)象五同一個(gè)游戲今天能鉤住明天鉤不住原因目標(biāo)游戲更新了DirectX版本或者改用了不同路徑加載D3D9設(shè)備虛表索引漂移。另一個(gè)隱蔽原因是游戲自己也在用Detours或MinHook掛鉤多個(gè)庫(kù)在同一函數(shù)上疊加后裝的把先裝的覆蓋了。 解決啟動(dòng)時(shí)動(dòng)態(tài)探測(cè)虛表而不是寫(xiě)死索引對(duì)疊加掛鉤場(chǎng)景啟動(dòng)順序要保持一致。多鉤子疊加時(shí)的鏈?zhǔn)焦芾碓谧詈笠徽抡归_(kāi)這里記住一個(gè)原則自己的鉤子盡量晚安裝、早卸載把沖突概率降到最低。現(xiàn)象排查優(yōu)先級(jí)最可能原因鉤子不生效地址→索引→事務(wù)返回值虛表索引寫(xiě)錯(cuò)崩潰簽名→線程→狀態(tài)污染函數(shù)簽名不匹配編譯失敗lib路徑→位數(shù)→包含順序detours.lib未鏈接卸載崩潰線程安全→對(duì)稱(chēng)性參數(shù)寫(xiě)反或線程未暫停時(shí)好時(shí)壞版本→疊加沖突虛表索引漂移5. 落地應(yīng)用從函數(shù)截獲到幀率統(tǒng)計(jì)、MOD制作與作弊檢測(cè)5.1 性能分析在EndScene里掛一個(gè)FPS計(jì)數(shù)器鉤子裝好后的第一個(gè)需求基本是顯示幀率。做法是維護(hù)幀計(jì)數(shù)和時(shí)間戳在HookEndScene里累加每秒算一次FPS。注意不要在渲染熱路徑里做文件讀寫(xiě)或字符串格式化這些高頻調(diào)用會(huì)拖垮幀率。// 幀率統(tǒng)計(jì)在HookEndScene里調(diào)用 void CountFPS(LPDIRECT3DDEVICE9 pDevice) { static int frameCount 0; static double lastTime 0.0; static float fps 0.0f; double now GetTickCount64() / 1000.0; // 單位秒 frameCount; if (now - lastTime 1.0) { fps frameCount / (now - lastTime); frameCount 0; lastTime now; } // 把fps輸出到日志或用ID3DXFont繪制到屏幕上 // 這里只做統(tǒng)計(jì)繪制調(diào)用放在EndScene的其他分支里 }邏輯說(shuō)明GetTickCount64拿毫秒時(shí)間戳除以1000轉(zhuǎn)成秒lastTime初始為0第一次調(diào)用會(huì)直接跳過(guò)顯示分支。參數(shù)說(shuō)明如果幀率波動(dòng)劇烈可以改成滑動(dòng)窗口平均取最近60幀的均值。對(duì)于性能分析還可以在調(diào)用RealEndScene前后各插一個(gè)高精度計(jì)時(shí)器相減得到單幀渲染耗時(shí)這個(gè)數(shù)據(jù)對(duì)定位渲染瓶頸非常有用。注意GetTickCount64在32位老系統(tǒng)上不可用老代碼里常用timeGetTime或QueryPerformanceCounter替代。5.2 MOD制作在EndScene里畫(huà)自定義UI或替換渲染效果很多游戲MOD的本質(zhì)就是在渲染管線末端插入自己的繪制調(diào)用。HookEndScene拿到pDevice之后所有D3D設(shè)備方法都能用——畫(huà)FPS文本、繪制矩形、修改光照參數(shù)甚至打斷整個(gè)場(chǎng)景的渲染提交再自己重畫(huà)一遍。需要特別提醒的是D3D9的繪制狀態(tài)是全局的你插入的繪制會(huì)污染游戲的渲染狀態(tài)所以繪制前要保存狀態(tài)SetRenderState、SetTexture等繪制后要恢復(fù)。常見(jiàn)的做法是在鉤子函數(shù)里調(diào)用BeginScene/EndScene包裹自定義繪制并顯式保存和恢復(fù)所有被修改的狀態(tài)塊。5.3 反外掛與作弊檢測(cè)鉤子的另一面在反外掛場(chǎng)景鉤子的用途是記錄敏感API的調(diào)用頻率、調(diào)用棧哈希、參數(shù)特征用于識(shí)別外掛行為。舉例一個(gè)正常游戲進(jìn)程每秒不會(huì)調(diào)用幾千次SetCursorPos如果檢測(cè)到這種異常模式就標(biāo)記為可疑。但這里有一個(gè)明顯的邊界如果你自己也在用鉤子做反外掛那你也可能被別的反外掛系統(tǒng)當(dāng)成作弊者。多套鉤子疊加時(shí)穩(wěn)定性極難保證這也是很多競(jìng)技游戲直接拒絕染指鉤子技術(shù)的原因。做反外掛檢測(cè)時(shí)需要克制只記錄統(tǒng)計(jì)特征不要修改任何函數(shù)行為保持hook的“透明性”。5.4 什么時(shí)候不該用鉤子DirectX 12時(shí)代渲染架構(gòu)大幅改變命令列表機(jī)制讓傳統(tǒng)APIHOOK變得極為脆弱很少有人還在D3D12上做EndScene級(jí)掛鉤。如果目標(biāo)游戲是D3D12或Vulkan老老實(shí)實(shí)改源碼或者用官方調(diào)試層比逆向鉤子靠譜得多。這也是這份壓縮包的價(jià)值邊界它是研究D3D9/D3D11及更老DirectX游戲的利器不是萬(wàn)能工具。具體來(lái)說(shuō)D3D9時(shí)代的游戲普遍運(yùn)行在兼容模式鉤子介入相對(duì)穩(wěn)定D3D11的Present函數(shù)仍然可以?huà)斓舆t和同步問(wèn)題更多到了D3D12掛鉤成本已經(jīng)高到不劃算。6. 進(jìn)階技巧多鉤子共存、動(dòng)態(tài)卸載與鉤子生效驗(yàn)證6.1 三步驗(yàn)證法鉤子真的掛上了嗎裝完鉤子先別急著寫(xiě)功能先驗(yàn)證。最直接的辦法是在替換函數(shù)里OutputDebugString一行日志用DebugView觀察輸出頻率。如果日志沒(méi)輸出先查地址再查DetourAttach返回值。第二步是確認(rèn)原功能沒(méi)被破壞游戲畫(huà)面是否正常交互是否流暢有沒(méi)有明顯的幀率暴跌。第三步才是加自己的邏輯并逐步確認(rèn)每次改動(dòng)的影響。很多乍看是鉤子導(dǎo)致的問(wèn)題最后定位到是函數(shù)簽名或線程同步的問(wèn)題所以驗(yàn)證順序決定了排查效率。6.2 多鉤子共存與動(dòng)態(tài)卸載的鏈?zhǔn)揭?guī)則多個(gè)鉤子掛同一個(gè)函數(shù)時(shí)Detours按棧式鏈管理后掛的在頂層先執(zhí)行依次穿透到最底層每一層都必須調(diào)用自己的Trampoline才能把鏈條傳下去。如果你忘調(diào)了下層鉤子全部失效。動(dòng)態(tài)卸載注意不要在鉤子函數(shù)執(zhí)行過(guò)程中卸載當(dāng)前線程正站在被改寫(xiě)指令的路徑上恢復(fù)指令的瞬間就是崩潰的瞬間。// 多鉤子鏈的正確協(xié)作示例 // 假設(shè)另一個(gè)模塊在EndScene上也掛了鉤 HRESULT WINAPI HookEndScene_My(LPDIRECT3DDEVICE9 pDevice) { // 這里先執(zhí)行自己的邏輯 MyFrameLogic(pDevice); // 必須調(diào)用RealEndScene才能把鏈條傳給下一層鉤子 return RealEndScene(pDevice); }邏輯說(shuō)明如果RealEndScene已經(jīng)被上層鉤子重寫(xiě)那調(diào)用它會(huì)進(jìn)入上層鉤子的替換函數(shù)如果上層忘了調(diào)用Trampoline鏈條就斷了。參數(shù)說(shuō)明在多鉤子環(huán)境下你的RealEndScene最終指向的不一定是D3D9原始函數(shù)而是鏈中相鄰下一鉤子的替換函數(shù)。所以不要在初始化時(shí)緩存返回的指針以為它是“最終真實(shí)地址”每次調(diào)用時(shí)動(dòng)態(tài)穿透才是正確姿勢(shì)。很多人在老游戲上加MOD鉤子、代理DLL、注入器全上鏈條特別長(zhǎng)。我的習(xí)慣是先卸載自己再卸載別人先暫停線程再恢復(fù)指令整個(gè)過(guò)程按序執(zhí)行不要圖省事。從那以后我每次做鉤子工程都強(qiáng)制走一遍驗(yàn)證流程——裝上鉤子先確認(rèn)輸出日志再確認(rèn)原功能沒(méi)被破壞最后才加自己的邏輯。這個(gè)習(xí)慣幫我避開(kāi)了一大半的崩潰現(xiàn)場(chǎng)。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取