- qrenderdoc.exe / renderdoccmd.exe:工具自己,要回放 .rdc,不能再钩自己的 GPU
- 你的游戏 / UE:目标进程,要挂钩子抓帧
这些部分的内容写了多少?如果要改的话要改大概多少行代码?
这部分逻辑其实很少,核心代码约 20 行以内,
CMakeLists.txt 是 CMake 构建系统的核心输入文件,
项目的"构建蓝图"----> tell --->"CMaker"
makefile是在 Linux 编译c或者c++代码的时候的一种脚本文件,但是每一个功能都要写一个makefile文件,这样如果这个工程很大,而且相关性比较强的话,makefile的书写就会变得相对繁琐,
更简单的解释就是cmake是为了生成makefile而存在,这样我们就不需要再去写makefile了,只需要写简单的CMakeLists.txt即可。CMakeLists.txt是什么-CSDN博客


Qt 6 已徹底從 qmake 轉向 CMake,並表示未來將停止 qmake 開發;Google 的 Android NDK 也推薦以 CMake 取代傳統 Android.mk。
这个项目的 CMakeLists.txt 并不是为了在所有平台上使用,Windows 被明确排除了:
if(WIN32)
message(FATAL_ERROR "CMake is not needed on Windows, just open and build renderdoc.sln")
endif()
位置:CMakeLists.txt (line 253)(E:/college researcher/renderdoc-1.x/CMakeLists.txt:253)
源文件选择也按平台区分,见 renderdoc/CMakeLists.txt (line 293)(E:/college researcher/renderdoc-1.x/renderdoc/CMakeLists.txt:293)

同一个
renderdoc.dll,分别加载进不同进程;工具进程有 marker,游戏进程没有 marker。
但不是两份不同版本的 DLL,而是同一个 DLL 文件在两个进程中各自有一份独立的加载实例和运行状态。
为什么两个进程都需要 renderdoc.dll
renderdoc.dll 同时包含两大类能力:
| 所在进程 | 使用的能力 | DLL 应采取的模式 |
|---|---|---|
qrenderdoc.exe / renderdoccmd.exe |
解析 .rdc、创建 replay device、重放 GPU 命令 |
Replay 模式,不安装捕获钩子 |
| 游戏 / UE | 拦截 D3D/Vulkan API、记录资源和命令、生成 .rdc |
Capture 模式,安装钩子 |
例如,qrenderdoc.exe 会调用:
RENDERDOC_InitialiseReplay(env, coreargs);
见 qrenderdoc.cpp (line 607)(E:/college researcher/renderdoc-1.x/qrenderdoc/Code/qrenderdoc.cpp:607)。
这个函数的实现位于 renderdoc.dll 中。因此 GUI 本身虽然只是前端,但真正解析和重放 .rdc 的核心代码仍然来自 renderdoc.dll。
游戏进程则不是为了调用 replay API,而是由 RenderDoc 注入 renderdoc.dll,让 DLL 拦截 D3D11、D3D12、Vulkan 等 API。
空函数为什么能够区分
宏展开后:
extern "C" __declspec(dllexport) void __cdecl renderdoc__replay__marker()
{
}
关键不是函数执行了什么,而是这个名字被放进了 EXE 的 PE 导出表:
renderdoc__replay__marker
renderdoc.dll 加载时,在 DllMain 中执行:
if(LibraryHooks::Detect("renderdoc__replay__marker"))
{
RenderDoc::Inst().SetReplayApp(true);
// 不注册捕获钩子
}
else
{
RenderDoc::Inst().Initialise();
LibraryHooks::RegisterHooks();
}
见 win32_libentry.cpp (line 52)(E:/college researcher/renderdoc-1.x/renderdoc/os/win32/win32_libentry.cpp:52)。
具体到两个进程qrenderdoc.exe和renderdoccmd.exe
qrenderdoc.exe 中写了:
REPLAY_PROGRAM_MARKER()
见 qrenderdoc.cpp (line 151)(E:/college researcher/renderdoc-1.x/qrenderdoc/Code/qrenderdoc.cpp:151)。
因此:
qrenderdoc.exe 导出 marker
↓
Windows 加载 renderdoc.dll
↓
renderdoc.dll 扫描当前进程模块
↓
在 qrenderdoc.exe 的导出表中找到 marker
↓
设置 ReplayApp = true,不调用 RegisterHooks()
renderdoccmd.exe 同样导出了它,见 renderdoccmd.cpp (line 1462)(E:/college researcher/renderdoc-1.x/renderdoccmd/renderdoccmd.cpp:1462)。
普通游戏或 UE 程序没有写这个宏:
游戏 EXE 没有导出 marker
↓
renderdoc.dll 被注入
↓
扫描不到 marker
↓
调用 LibraryHooks::RegisterHooks()
↓
开始拦截图形 API
严格来说,用导出变量也能实现类似效果;空函数只是一种跨编译器、链接器和平台都比较方便的 marker 形式。
marker 必须在 renderdoc.dll 加载前,存在于主 EXE或者某个已经加载的 DLL 中,见 renderdoc_replay.h (line 47)(E:/college researcher/renderdoc-1.x/renderdoc/api/replay/renderdoc_replay.h:47)。否则 DllMain 第一次检测时看不到它,就会误走捕获模式。
Shim:一个尽量薄的 dll(renderdocshim64.dll),用在 Global Hook------「以后启动的、路径匹配的进程都抓」。它几乎不干别的:打开一块命名共享内存,看路径对不对,对就 LoadLibrary 真正的 renderdoc.dll。
对应命令是 renderdoccmd globalhook。从界面里点「全局挂钩某 exe」才走这条路。普通「从 RenderDoc 启动游戏」是直接注入 renderdoc.dll,可以完全不用到 Shim。
https://zhuanlan.zhihu.com/p/2020093825977168718

文章要的是 改名并对齐,不是把 Replay Marker 删掉。
删掉会怎样
这个空函数是 dll 认「我在工具里」的唯一默认开关。
• 从 qrenderdoc.exe / renderdoccmd.exe 拿掉导出:Detect() 找不到符号 → 把工具当成游戏 → 给界面装 GPU hook → UI 崩(文章里写的就是这种崩法)
• 从 dll 里 拿掉这次检查:要么永远当游戏钩(工具自己也崩),要么永远不钩(游戏也抓不成)
所以 Marker 在架构里必须还在,只是不必叫 renderdoc__replay__marker。
文章具体在说什么
它举的是:
dll 去找 rendertest__replay__marker,exe 还在导出 renderdoc__replay__marker → 对不上 → 崩
会同时修改 CMake 和现有 Visual Studio 工程,因为你明确要用当前工程编译;只改 CMake 会导致直接从 .sln/.vcxproj 构建时仍产出旧名。
renderdocshim64.dll 确实是一个"哨兵"性质的轻型 DLL,它的作用不是直接抓帧,而是负责快速判断"这个进程我该不该管",以及"该管的话,把真正干活的模块带进来"。
RenderDoc 的 Global Hook 是怎麼來的?
它不是對「已經在跑的某個進程內部全部無差別注入」,而是:
系統級別(全域)地讓「之後新啟動的所有進程」都先載入一個極小的 shim DLL,然後再由這個 shim 決定要不要真正注入 RenderDoc。
官方文件寫得很清楚:
RenderDoc implements this behaviour by modifying the AppInit_DLLs registry key to reference RenderDoc's dlls.
When you've entered a path, or filename... this option will then insert a global hook that causes every new process created to load a very small shim dll.
The shim dll will load, create a thread that checks to see if the process matches the path or filename specified, and then unload. If the process matches it will also inject RenderDoc...
global的技術細節簡述
- 修改登錄檔 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows 底下的 AppInit_DLLs(以及相關的 LoadAppInit_DLLs)。
- 作用範圍 之後任何會載入 user32.dll 的新進程(幾乎所有有 GUI 的程式),都會在啟動早期被系統強制載入 RenderDoc 的 shim DLL(設計得非常精簡,只依賴 kernel32.dll)。
- 過濾機制 shim 進去後會開一個 thread,檢查目前進程的路徑/檔名是否符合你指定的 match 字串。
- 符合 → 真正注入完整的 renderdoc.dll,開始 hook 圖形 API。
- 不符合 → 立刻卸載自己。
- 為什麼叫 Global 因為它是透過系統機制影響「所有新進程」,而不是只針對你指定的某個進程做注入。這就是「全域」的意思。
为什么需要这个"薄"的 Shim?
这是为了在"捕获所有进程"和"系统稳定性"之间取得平衡。如果要求 RenderDoc 对之后启动的每一个进程都注入完整的 renderdoc.dll,会有两个问题:
- 性能与资源浪费:绝大部分进程(比如后台服务、无关应用)根本不需要被调试,注入完整的捕获模块会白白消耗内存并拖慢所有程序的启动速度。
- 干扰与风险增加:对不相关的进程强行注入图形调试模块,可能导致这些程序行为异常甚至崩溃,尤其在系统级场景下不可接受。
因此,Shim 的设计初衷就是"尽量薄、尽量安全",它的职责边界很清晰。
它的三项动作,我结合 RenderDoc 架构展开一下:
- 打开命名共享内存:这是与 RenderDoc 主程序(qrenderdoc)通信的通道。主程序会把用户配置的"需要捕获的进程路径匹配规则"写入这块共享内存,Shim 启动后通过它来获取过滤条件,整个过程非常轻量。
- 比对进程路径:Shim 检查自身所在进程的可执行文件路径是否符合共享内存中的规则。只有匹配上,它才会认为自己"接管"了这个目标;否则,它的生命周期就到此为止,直接静默退出,不影响进程正常运行。
- 加载真正的 renderdoc.dll:一旦匹配成功,Shim 会调用 LoadLibrary 加载完整的 renderdoc.dll。这个真正的模块随即开始发挥作用------挂接图形 API(如 D3D11/D3D12/Vulkan)、捕获资源状态、记录命令调用流等。
在使用 RenderDoc 的 "Enable Global Hook" 功能时,体验到的"启动前勾选、重启目标程序后生效"的操作,就是这套逻辑的体现。Shim 通过系统注入机制提前进入到目标进程,在程序执行任何图形 API 调用之前完成对 renderdoc.dll 的加载,从而确保从初始化阶段就能被完整捕获。
注意事項(官方也強調很危險)
- 需要管理員權限。
- 如果開了 Secure Boot,AppInit_DLLs 會被系統禁用,這個功能就失效。
只改 CMakeLists.txt,但直接用已經生成好的 .sln / .vcxproj 去建置,通常還是會產出舊名字。
為什麼會這樣?
CMake 的工作流程是:
- 你寫 CMakeLists.txt(定義 target 名稱、OUTPUT_NAME、RUNTIME_OUTPUT_NAME 等)
- 執行 cmake(或用 CMake GUI / VS 的 CMake 整合)→ 生成 .sln 和 .vcxproj
- 之後 Visual Studio 或 msbuild 是直接讀這些已經生成好的專案檔來編譯
一旦 .vcxproj 生成完畢,裡面的專案名稱、輸出檔名、目標名稱等都已經被寫死成 XML 了。 之後你再去改 CMakeLists.txt,已經生成的專案檔不會自動更新。
跨位数转发、全局 Hook 的 32/64 shim、工具子进程排除、崩溃处理和 UI 更新辅助流程现在都指向 CTWResearcher*。接下来处理 Vulkan layer 的公开层标识和资源元数据,这是防止它与原版 Vulkan layer 注册冲突的必要部分。
一项需要补齐:根 CMake 的 DLL 基名是跨平台共享配置,因此 POSIX/Android 的 Replay Marker 检测也必须使用同一个新符号,否则非 Windows 构建会失去"工具不自钩"语义。
两处会影响构建/安装的残留:Android Vulkan 包装层仍使用旧导出函数名,而 64 位安装器仍复制旧 Shim 名。我会修正这两处;其余搜索到的旧名目前只是注释或错误提示文本,
检查 Visual Studio 项目之间的 .lib 链接名。改变 DLL 输出名后,命令行程序、UI 和 Python 模块若仍硬编码链接 renderdoc.lib,会在链接阶段失败;这正是静态文本替换容易漏掉的部分。
链接名已同步到 Qt 工程。现在执行 x64 Release 构建;该构建会覆盖 DLL、命令行程序、Shim 和 GUI,能直接验证新的输出名、符号引用以及生成后的 Vulkan 层元数据。
核心 DLL 生成后再用 dumpbin 核验导出 Replay Marker。
定位到真正的 GUI 命名冲突:qrenderdoc_local.vcxproj 会错误地把同目录的 CTWResearcher.lib 当成自身链接输出名,因此 CTWResearcher.exe 与 DLL 的导入库基名冲突。修复方式是将 DLL 的导入库显式命名为 CTWResearcherCapture.lib,对外 DLL 文件仍保持你指定的 CTWResearcher.dll。
Visual Studio 生成的 renderdoc.json 仍保留旧 layer 名、旧 DLL 路径和旧启用变量,而导出函数已经改名,Vulkan 捕获会因此失败。
你的电脑是 64 位,并不限制你编译 32 位程序。原因是:
- 64 位 Windows 可以运行 32 位程序;
- Visual Studio 安装了 Win32 编译工具链;
- 编译器可以通过目标平台参数生成 x86 机器码;
- 生成的程序只决定"目标 CPU 架构",不取决于编译器所在电脑本身必须是对应架构。
renderdoc prompts
注意目錄名字,但這一步只是改了名字,還沒改完,之後還有renderdoc\os\win32\win32_process.cpp,這個文件裡的renderdoc*和internal*要改
下面这段可以直接复制到新的对话中,作为复现说明。
```text
工作目录:
E:\college researcher\renderdoc-1.x
目标:
将 Windows + UE 使用的 RenderDoc 工具整体改名为 CTWResearcher,同时保持 .rdc 文件格式、公共 RENDERDOC_* API 和 Python 模块兼容。
固定名称:
CTWResearcher.exe GUI
CTWResearcherCmd.exe 命令行、崩溃处理
CTWResearcher.dll 注入和捕获 DLL
CTWResearcherShim32.dll 32 位全局 Hook Shim
CTWResearcherShim64.dll 64 位全局 Hook Shim
ctwresearcher__replay__marker 工具自身 Replay Marker
CTWResearcherGlobalHookData32/64
CTWRESEARCHER_CRASHHANDLE
VK_LAYER_CTWRESEARCHER_Capture
ENABLE_VULKAN_CTWRESEARCHER_CAPTURE
改名原则:
1. 工具自身必须识别为 Replay App,不能钩自己的 GPU。
2. 游戏和 UE 进程没有 Replay Marker,必须正常注册 GPU hooks。
3. 不修改公共 RENDERDOC_* API。
4. 不修改 .rdc 文件格式。
5. 不修改 Python 模块名 renderdoc/qrenderdoc。
6. 不使用 CTWResearcherCapture.dll 作为 DLL 名,DLL 名固定为 CTWResearcher.dll。
7. Windows + UE 是本轮重点,跨平台发布不是重点。
已修改的核心位置:
1. 根 CMakeLists.txt
将:
set(RDOC_BASE_NAME "renderdoc")
改为:
set(RDOC_BASE_NAME "CTWResearcher")
2. renderdoc/CMakeLists.txt
为 Windows DLL 增加:
if(WIN32 AND NOT INTERNAL_SELF_CAPTURE)
set_target_properties(renderdoc PROPERTIES OUTPUT_NAME "CTWResearcher")
endif()
3. renderdoccmd/CMakeLists.txt
Windows 下将命令行程序输出为:
CTWResearcherCmd.exe
4. qrenderdoc/qrenderdoc.pro
将 GUI 输出目标改为:
TARGET = CTWResearcher
5. Visual Studio 工程
修改:
renderdoc/renderdoc.vcxproj
qrenderdoc/qrenderdoc_local.vcxproj
renderdoccmd/renderdoccmd.vcxproj
renderdocshim/renderdocshim.vcxproj
输出目标改为:
CTWResearcher
CTWResearcherCmd
CTWResearcherShim32
CTWResearcherShim64
renderdoc.vcxproj 的 RDOC_BASE_NAME 改为:
RDOC_BASE_NAME=CTWResearcher
6. Replay Marker
文件:
renderdoc/api/replay/renderdoc_replay.h
将导出符号改为:
ctwresearcher__replay__marker
文件:
renderdoc/os/win32/win32_libentry.cpp
renderdoc/os/posix/posix_libentry.cpp
检测:
LibraryHooks::Detect("ctwresearcher__replay__marker")
带这个符号的 GUI 和 Cmd 会进入 Replay App 路径,不执行:
LibraryHooks::RegisterHooks();
7. Shim 和共享内存
文件:
renderdocshim/renderdocshim.h
改为:
CTWResearcherGlobalHookData32
CTWResearcherGlobalHookData64
CTWResearcherShim32.dll
CTWResearcherShim64.dll
同时修改:
renderdoc/os/win32/win32_process.cpp
确保跨位数注入时寻找:
CTWResearcherCmd.exe
CTWResearcherShim32.dll
CTWResearcherShim64.dll
8. 崩溃处理
文件:
renderdoc/core/crash_handler.h
renderdoccmd/renderdoccmd_win32.cpp
renderdoc/core/core.cpp
改为使用:
CTWRESEARCHER_CRASHHANDLE
CTWResearcherCmd.exe
CTWResearcher.exe
并确保 CTWResearcherCmd 不重复创建崩溃处理服务。
9. Windows 工具路径
修改:
renderdoc/os/win32/win32_process.cpp
renderdoc/os/win32/win32_stringio.cpp
renderdoc/os/win32/sys_win32_hooks.cpp
renderdoccmd/renderdoccmd_win32.cpp
qrenderdoc/renderdocui_stub.cpp
qrenderdoc/Windows/Dialogs/UpdateDialog.cpp
所有工具启动、注入、跨位数转发和自注入排除逻辑改为 CTWResearcher 名称。
10. Vulkan layer
文件:
renderdoc/common/globalconfig.h
renderdoc/driver/vulkan/vk_layer.cpp
renderdoc/driver/vulkan/vk_layer_android.cpp
renderdoc/driver/vulkan/renderdoc.json
renderdoc/renderdoc.vcxproj
改为:
VK_LAYER_CTWRESEARCHER_Capture
ENABLE_VULKAN_CTWRESEARCHER_CAPTURE
DISABLE_VULKAN_CTWRESEARCHER_CAPTURE_版本号
JSON 中的 DLL 路径必须是:
.\\CTWResearcher.dll
Visual Studio 的 JSON 生成规则必须同步修改,否则生成的 JSON 会混用旧 layer 名和新导出函数。
11. Windows 资源信息
修改:
renderdoc/data/renderdoc.rc
qrenderdoc/Resources/qrenderdoc.rc
renderdoccmd/renderdoccmd.rc
修改文件描述、内部名称、原始文件名和产品名为 CTWResearcher 系列。
12. 安装器和打包脚本
修改:
util/installer/Installer32.wxs
util/installer/Installer64.wxs
util/buildscripts/scripts/compile_win32.sh
util/buildscripts/scripts/make_package_win32.sh
文件名改为:
CTWResearcher.dll
CTWResearcher.exe
CTWResearcherCmd.exe
CTWResearcherShim32.dll
CTWResearcherShim64.dll
CTWResearcher.json
13. 导入库冲突处理
GUI 目标也是 CTWResearcher.exe,而 DLL 的默认导入库也可能叫 CTWResearcher.lib,导致链接冲突。
因此:
renderdoc/renderdoc.vcxproj
增加:
<ImportLibrary>$(OutDir)CTWResearcherCapture.lib</ImportLibrary>
qrenderdoc/qrenderdoc.pro 使用:
LIBS += $$DESTDIR/CTWResearcherCapture.lib
注意:
这是导入库名称,不是 DLL 名。
最终 DLL 仍然是:
CTWResearcher.dll
构建环境:
Visual Studio 2022
MSBuild 17.14.40
本机为 64 位 Windows,但可以同时编译 x64 和 Win32。
x64 Release 构建:
MSBuild renderdoc.sln /m /t:Build /p:Configuration=Release /p:Platform=x64
x64 输出:
x64\Release\CTWResearcher.exe
x64\Release\CTWResearcherCmd.exe
x64\Release\CTWResearcher.dll
x64\Release\CTWResearcherShim64.dll
x64\Release\CTWResearcher.json
Win32 Release 构建:
MSBuild renderdoc.sln /m /t:Build /p:Configuration=Release /p:Platform=Win32
Win32 输出:
Win32\Release\CTWResearcher.exe
Win32\Release\CTWResearcherCmd.exe
Win32\Release\CTWResearcher.dll
Win32\Release\CTWResearcherShim32.dll
Win32\Release\CTWResearcher.json
构建验证:
1. 检查两个工具都导出:
ctwresearcher__replay__marker
2. 检查 GUI 导入:
CTWResearcher.dll
3. 检查 Vulkan JSON:
VK_LAYER_CTWRESEARCHER_Capture
.\\CTWResearcher.dll
ENABLE_VULKAN_CTWRESEARCHER_CAPTURE
4. 运行:
x64\Release\CTWResearcherCmd.exe --help
5. 第一次运行 Vulkan 捕获时,在提示注册 Vulkan layer 的对话框中点击 Yes。
6. 之后用 CTWResearcher.exe 测试:
- 打开 .rdc 回放;
- 启动或附加 64 位 UE;
- D3D11/D3D12 抓帧;
- Vulkan 抓帧;
- 确认 CTWResearcher.exe 和 CTWResearcherCmd.exe 不会钩自己的 GPU。
重要注意:
1. 只编译 x64 时,只能得到 64 位工具。
2. 如果要支持 32 位目标程序,必须另外编译 Win32。
3. 64 位电脑可以编译 32 位程序,只要安装了 Visual Studio 的 x86/Win32 工具链。
4. x64 和 Win32 的 DLL 不能混用。
5. Vulkan layer 注册通常是一次性操作,路径改变后需要重新注册。
6. 不要全局替换所有 renderdoc 字符串,否则会破坏公共 API、Python 模块和 .rdc 兼容性。
7. 生成文件夹中如果出现旧的 renderdoc.dll 或旧 JSON,应清理或确认不会被旧版本优先加载。
```
这套流程的核心结果是:
```text
CTWResearcher.exe
有 ctwresearcher__replay__marker
不注册 GPU hooks
用于回放 .rdc
CTWResearcherCmd.exe
有 ctwresearcher__replay__marker
不注册 GPU hooks
用于命令行、注入和崩溃处理
CTWResearcher.dll
注入 UE 或其他目标进程
目标进程没有 Replay Marker
正常注册 GPU hooks
```
此前已经成功验证 x64 Release 的 DLL、GUI、Cmd、Shim 和 Vulkan JSON。实际 UE D3D12/Vulkan 抓帧仍需要在目标机器上继续测试。
"两端同步"完成第一组 INTERNAL_* 注入接口改名:
- DLL 导出定义改为 CTW_*
- win32_process.cpp 的远程注入查找同步修改
- 本进程 GetProcAddress 同步修改
- Shim 的函数指针和 GetProcAddress 同步修改
- 修正 Shim 原先查找 INTERNAL_SetLogFile、但 DLL 实际导出 INTERNAL_SetCaptureFile 的不一致
当前还没有编译,因为第二组 RENDERDOC_* 公共导出接口尚未改完。它涉及约 50 个函数,下一步会逐个按功能边界命名,并同步所有调用端,最后统一编译验证。
接下来就是改 RENDERDOC_* 这一组,但这里比 INTERNAL_* 大很多:当前 renderdoc_replay.h 和实现中大约有 60 个真正的 DLL 导出函数,且 GUI/Cmd 大多是直接链接调用,不是像 Shim 那样用 GetProcAddress。
所以第二组会同步改:
- 导出声明和实现;
- GUI/Cmd 的调用名称;
- renderdoc_replay.h 等接口头文件;
- RENDERDOC_GetAPI 这类目标程序会主动查找的入口;
- 相关导出表和工程链接引用。
exe shim里要来找,dll导出表要表示我就是要被找来用的,所以改的要是"两端同步",
renderdoc也要知道自己調用的是誰,這個剛好被利用了,所以反抓幀才可以輕鬆的識別出來,
默認的renderdoc必然要暴露自己是誰,這是最直白的問題,必須和自己的renderdoc.dll的導出表相互聯繫上
平时你改 foo 成 bar,调用处编译器会一起改,对不上就编不过。
这 9 个函数不是那样。它们被 __declspec(dllexport) 挂出去,对面用 字符串 找:
挂牌的人: 我叫 INTERNAL_GetTargetControlIdent
找人的人: GetProcAddress(..., "INTERNAL_GetTargetControlIdent")
编译器不检查这两边是不是同一个词。你改一边,另一边还是旧词,能编过,跑起来才挂。
renderdoc.dll注入♂到目標之后,renderdoc的exe 并不是直接写 C++ 调用,而是把名字当钥匙递进去:
InjectFunctionCall(hProcess, loc, "INTERNAL_SetCaptureFile", ...
InjectFunctionCall(hProcess, loc, "INTERNAL_SetDebugLogFile", ...
InjectFunctionCall(hProcess, loc, "INTERNAL_SetCaptureOptions", ...
InjectFunctionCall(hProcess, loc, "INTERNAL_GetTargetControlIdent", ...
InjectFunctionCall 拿到这串字以后,做的是:
HMODULE renderdoc_local = GetModuleHandleA(STRINGIZE(RDOC_BASE_NAME) ".dll");
uintptr_t func_local = (uintptr_t)GetProcAddress(renderdoc_local, funcName);
uintptr_t func_remote = func_local + renderdoc_remote - (uintptr_t)renderdoc_local;
翻译成人话:
-
在 本机这份 CTWResearcher.dll 里,按名字找函数
-
算出它相对 DLL 基址的偏移
-
用同样偏移,去 游戏进程里那份 DLL 上调同一个函数
对 PE 导出表来说,改 INTERNAL_* 和 RENDERDOC_* 就够了。 你那份 DLL 的 70 个导出,除掉已经改过的 VK_LAYER_CTWRESEARCHER_*,剩下就是这两组。没有第三组导出前缀。
MSVC 开着 RTTI 指的是编译器选项 /GR(Enable Run-Time Type Information),不是 MSBuild 本身。
- MSBuild 是构建系统(负责调度编译、链接等)。
- RTTI 是传给 cl.exe 的编译器开关。
能不能关?
可以关,用 /GR-。
官方说明:
- 默认是开的(/GR)。
- 如果你的代码没用 dynamic_cast 和 typeid,关掉可以减小映像大小(.rdata 段会小一点)。
五个 Env 为什么拆开:InjectFunctionCall 一次只能送一块连续内存,不好直接传「一串环境修改」。所以先塞进目标进程里的临时结构 tempEnvMod,攒齐再 Commit。
捕获器不是一堆平行函数,是三条进门、汇到同一个单例:
门A 外部植入 CTW.exe 把 DLL 打进游戏 → INTERNAL_* 立刻给它配参数、要端口
门B 进程内自报 游戏自己 GetProcAddress → RENDERDOC_GetAPI 拿到函数表
门C 全局挂钩 Shim 随进程加载 → 再转去门A 那套配置
↓
进程内捕获器实例(挂钩 D3D/Vulkan、记命令)
↓
Target Control(听一个 ident/端口,给 UI 连)
↓
写出捕获文件 → Replay 打开回放

Hook → OS 级全局挂钩(不针对特定 exe)
InjectFunctionCall 是什么感觉?
- 通过某种跨进程通道(通常是共享内存 + 事件,或 named pipe),把「我要调用哪个函数、参数是什么」打包成一块内存发过去。
- 目标进程里的 DLL 收到后,在自己的线程里执行对应的函数。
改了导出名之后,Shim 里写死的那些字符串(GetProcAddress(hModule, "RENDERDOC_xxx"))也要一起改,否则指针会拿到 NULL。
- GetModuleHandle("xxx.dll"):查看当前进程里是否已经加载了某个模块,返回它的句柄。
- 和 LoadLibrary 不同,它不会去加载,只是查询。
在注入/Shim 场景里常见用法是:
C++
HMODULE h = GetModuleHandleA("renderdoc.dll"); // 或你改名后的名字
if (h) {
auto fn = (SomeFunc)GetProcAddress(h, "RENDERDOC_GetAPI");
...
}
你改了 DLL 文件名或导出名,这里的字符串也必须同步改。
多个版本的renderdoc都写同一个文件夹,怎么可能?而且放的位置还不同
多个版本、装在不同盘,却写同一个 C 盘文件夹------因为代码从来没问"我在哪",只问"这个用户的 Temp/AppData 在哪",再拼上写死的 RenderDoc。要按工具分开放,得改这两处字符串(以及 Temp 捕获目录),不是换 exe 路径就会跟着变。
strings 扫描生成的 PE 文件,检查是否残留旧的 renderdoc、RENDERDOC_、INTERNAL_ 字符串。
它不是"运行时机制",只是二进制检查工具。最可靠的导出验证仍是:
dumpbin /exports x64\Release\CTWResearcher.dll
strings 用来查残留,dumpbin 用来确认实际导出表。


extern "C" RENDERDOC_API uint32_t RENDERDOC_CC CTW_Replay
_NumVerticesPerPrimitive(Topology topology);
-
extern "C" 這是 C++ 關鍵字,用來告訴編譯器: 「這個函式請用 C 的命名規則和呼叫慣例 來產生符號,不要做 C++ 的 name mangling。」
沒有它的話,C++ 編譯器會把函式名變成類似 ?NumVerticesPerPrimitive@@YAIW4Topology@@@Z 這種亂碼,其他語言或用 GetProcAddress 就找不到了。
-
RENDERDOC_API 和 RENDERDOC_CC 這兩個是宏,通常定義成:
- RENDERDOC_API → __declspec(dllexport) 或 __declspec(dllimport)(視編譯/使用端而定)
- RENDERDOC_CC → __cdecl 或 __stdcall(呼叫約定)
所以展開後大概會變成:
C++extern "C" __declspec(dllexport) uint32_t __cdecl CTW_Replay_NumVerticesPerPrimitive(Topology topology);
寫「可被其他語言或動態載入」的 C API 時的標準寫法:
- 用 extern "C" 保證符號名乾淨、穩定
- 用宏控制「這個函式要不要匯出」以及「用哪種呼叫約定」
- 這樣同一個標頭檔可以同時給 DLL 自己編譯(export)和給外部程式使用(import)

launcher 能掐连接,是因为你抓的是一个马上就会退出的进程,捕获从来不是"整棵进程树强制锁死"。
OpenGL 的上下文 一次只能绑在一条线程上。线程 A 画着,线程 B 再调 GL,必须先 MakeCurrent 把上下文切过去。RenderDoc 把这种"没显式标注释、但线程已经换了"记成 ImplicitThreadSwitch:
case GLChunk::ImplicitThreadSwitch:
{
m_ImplicitThreadSwitches++;
...
}
回放扫完整帧后,切过 超过 2 次 就弹出你看到的那条:
if(m_ImplicitThreadSwitches > 2)
{
AddDebugMessage(..., "%d implicit thread switches detected. ...");
}
Blender 的 viewport 会在多线程里提交 GL(绘图、更新、部分后台上传)。这一帧里 RenderDoc 数到了 13 次 这种隐式切线程。
它的意思是:
• 能抓、能打开,不是坏文件
• GL 多线程提交不是 RenderDoc 的主路径,每次切换都要重记一份上下文,捕获和回放都慢、也更容易对不齐
• 不影响你验证"CTW 能抓 Blender"

上面不是"启动器比较特殊所以走 handshake 这条日志",是注入成功了才会写 handshake。
下面也不是"因为它叫 Endfield 所以写 inject",是注入失败,日志停在 inject。
Blender 能抓,是因为 blender.exe 让 DLL 留在了模块表里。Endfield 没让它留下。
能不能启动另一个完全同名的自己,然后立刻死掉?OS 允许吗
允许,而且不需要什么额外权限。 这是普通 CreateProcess。
Windows 认的是 PID + 地址空间,不是"名字全世界只能有一个"。可以同时有:
Launcher.exe PID 23484
Launcher.exe PID 30001
自己 CreateProcess("Launcher.exe")、换命令行再跑一遍自己、先起 Endfield.exe 再退出,都是常规启动器写法,用户权限就够。Steam、很多游戏壳都这样。不涉及提权,也不是"骗过 OS"。


boot.config 里有:
gfx-enable-gfx-jobs=1
gfx-enable-native-gfx-jobs=1
这是 Unity 把渲染工作丢到 Job 系统里,和 Blender 那条「多线程提交」是同一类现象,只是这里走的是 Unity/DX/Vulkan,不是 GL。
PlatformProcess.exe --- 平台助手
• QtWebEngineProcess.exe / CefView\CefViewWing.exe --- 内嵌浏览器
• UnityCrashHandler64.exe --- 崩溃处理
Endfield.exe 只有 0.79MB,就是 Unity 官方那种 stub:启动后加载旁边的 UnityPlayer.dll 和 Endfield_Data。
https://zhuanlan.zhihu.com/p/353043910
只要把同名的 d3d11.dll 丟在 exe 旁邊,程式就會優先載入你的版本。這是 Windows 的設計,不是 RenderDoc 特有的。
「FAKER d3d11.dll」做法(Proxy DLL)
這是另一套完全不同的技術,不是 RenderDoc 官方製造的。
它的本質是:
- 你自己寫一個假的 d3d11.dll(或 d3d12.dll)
- 把這個假 DLL 放在程式目錄,讓程式優先載入它
- 假 DLL 再去載入真正的系統 d3d11.dll,把所有呼叫轉發過去
- 同時在假 DLL 裡主動 LoadLibrary("renderdoc.dll")
這招的目的是:繞過「不允許外部 DLL 注入」的保護。 因為遊戲/模擬器是自己用 LoadLibrary("d3d11.dll") 把你的假 DLL 載進來的,反作弊比較難直接判定這是「惡意注入」。
RenderDoc 的日志本来就分两个前缀:
RDOC=CTWResearcher.dll(核心 DLL)的日志。QTRD=CTWResearcher.exe(Qt GUI)的日志。

1114 就是 ERROR_DLL_INIT_FAILED,微软定义是:
A dynamic link library (DLL) initialization routine failed.
这是确定结论。它同时排除了前面所有环节:路径没错、位数匹配、依赖齐全、权限足够,否则会分别是 2 / 193 / 126 / 5。失败只发生在最后一步,也就是 LoadLibraryW 调用 DllMain(DLL_PROCESS_ATTACH) 时返回了 FALSE。
对应代码就在 win32_libentry.cpp (line 75)(E:/college_researcher/renderdoc-1.x/renderdoc/os/win32/win32_libentry.cpp:75):
BOOL APIENTRY DllMain(...)
{
if(ul_reason_for_call == DLL_PROCESS_ATTACH)
{
BOOL ret = add_hooks(); // 返回 FALSE 才会导致 1114
SetLastError(0);
return ret;
}
return TRUE;
}
而 add_hooks() (line 34)(E:/college_researcher/renderdoc-1.x/renderdoc/os/win32/win32_libentry.cpp:34) 里只有两处复杂调用:
RenderDoc::Inst().Initialise();
...
LibraryHooks::RegisterHooks();
return TRUE;
它本身没有显式 return FALSE,所以是 Initialise() 或 RegisterHooks() 在目标进程里抛了 C++ 异常,异常穿过 DllMain,最终被 LoadLibraryW 报告为 1114。
要精确定位是 Initialise 还是 RegisterHooks,需要在 add_hooks() 里加异常捕获和 OutputDebugString。
从 Windows 规范看,根因方向也很明确:DllMain 是在 loader lock 下执行的,微软要求它尽量简单,不应在里面做 LoadLibrary、复杂初始化等。RenderDoc 在 DLL_PROCESS_ATTACH 里直接跑了 Initialise() 和 RegisterHooks(),后者还会去 LoadLibrary 图形 API DLL 并安装钩子,这一步在目标进程里失败,就返回了 1114。
新 DLL 已生成并覆盖:
现在 add_hooks() 会捕获异常并输出是哪一步失败。这里有个关键点要注意:这条调试信息不在 renderdoc.log 里 ,因为它是目标进程内部的 OutputDebugString,需要用工具看:
- 下载/运行 Sysinternals DebugView(以管理员运行)。
- 菜单
Capture → Capture Global Win32勾上。 - 保持 DebugView 开着,重新 Launch 你的目标程序。
SYSTEM 账户(LocalSystem)是 Windows 中权限最高的账户,它拥有对几乎所有系统资源的访问权。但这只是软件层面的授权 ,它决定了你能打开哪些文件、修改哪些注册表项,但不能让你执行特权指令。
关键点:SYSTEM 账户的代码仍然运行在 Ring 3,它访问硬件时依然要通过内核的 NtDeviceIoControlFile 等系统调用。
为什么驱动级权限如此强大?
1. 无限制的内存访问
驱动运行在 Ring 0,可以 直接读写任意物理内存地址,不受虚拟地址空间隔离的限制
杀软和 EDR 的核心防御机制(如 ObRegisterCallbacks、MiniFilter 文件过滤)都运行在 Ring 0。如果恶意代码只停留在 Ring 3,那么杀软可以轻松拦截;但一旦恶意代码进入 Ring 0,它就可以直接篡改或清除这些内核回调,让杀软"失明"
为什么驱动能"为所欲为"?
从内核设计哲学来看,驱动被视为"操作系统的一部分",它被假定为 可信代码。Windows 内核不检查驱动代码的意图,只检查它是否符合驱动规范(如 WDM/KMDF)。一旦驱动加载成功,它就被赋予了与内核同等的信任级别。
LdrLoadDll 是 ntdll 层的导出函数,它负责整个 DLL 加载流程的调度。如果目标进程在 ntdll 层就拦截了 LdrLoadDll(比如通过 Hook 或内核回调),那么你的 DLL 可能 根本不会被加载到进程地址空间,更别提执行 DllMain 了。
LdrLoadDll 是模块加载的必经之路,所以它成为安全攻防的焦点
Windows 模块加载机制的"总开关"。它的主要职责包括:
- 定位 DLL 文件 :根据++搜索路径(如应用程序目录、系统目录、PATH 变量)++找到要加载的 DLL 文件。
- ++解析 PE 结构++:读取 DLL 文件的 PE 头,解析导入表、导出表等关键数据。
- 映射到内存 :将 DLL 的代码和数据映射到调用进程的地址空间。
- 执行初始化 :调用 ++DLL 的入口点(DllMain),完成必要的初始化++工作。
"函数头被改写"就是常说的 inline hook ,它是让注入失败的一种手段。目标进程如果想让所有注入都失败,最省事的做法是在加载器 LdrLoadDll 这条"公共管道"上装一个阀门。
ntdll.dll 全称 NT Layer DLL ,是 Windows 操作系统最核心的用户态系统组件,也可以理解为 Windows 内核在用户态的 "官方代理 / 接口层"。
它是所有 Windows 用户态程序的底层基石:任何用户态代码都不能直接调用内核功能,必须通过 ntdll 作为中转网关,才能进入内核态执行系统调用。对应你之前的 "权力结构" 模型,它就是 用户态的最高权力中心,再往上就是内核本身。
LdrLoadDll 是 Windows 操作系统内部一个非常核心的底层 API,它位于 ntdll.dll 中,是所有 DLL 加载操作的最终执行者。无论是你调用 LoadLibrary、LoadLibraryEx,还是系统在进程启动时自动加载依赖的 DLL,最终都会走到 LdrLoadDll 这个函数。
虽然理论上无底洞,但实际开发中,不同角色会选择不同的"停靠点":
- 应用开发者:停在 LoadLibrary 即可,不需要关心底层
- 安全工程师:停在 LdrLoadDll 挂钩,因为这是模块加载监控的最佳切入点
- 内核开发者:停在 NtMapViewOfSection,研究内存管理
- 逆向工程师:停在 LdrpLoadDll,分析加载逻辑
renderdoc 现在其实已经用了"抢时间"策略:CreateProcess(CREATE_SUSPENDED),在目标进程 main 还没跑、反注入逻辑还没装的时候注入。这已经是注入方能做到的最早时机之一。
不碰 LdrLoadDll,只碰底下的 NtMapViewOfSection,而那里没被 hook。
目标进程没有内核 hook,手++动映射要解决的是"谁把 mapper 送进去、在哪执行、怎么把 DLL 搬进去"。++
注入器(CTWResearcherCmd / win32_process.cpp 的 InjectDLL())
远程线程不再 call LoadLibraryW,改成把 mapper stub + CTWResearcher.dll 的字节写进目标。mapper stub
这是你新写的一小段代码,用 CreateRemoteThread 丢进目标里跑。它不是目标"原生"的 DLL,是你注入进去的 mini loader。
几乎必须拿掉的
按你这个产品形态,上一份名单里大部分都不能当拦截策略用。不是技术做不到,是一开就会误伤,客服和卸载会先把游戏打死。
这类游戏要同时兼容:Steam/渠道 overlay、Discord、NVIDIA/AMD 录屏、Xbox 游戏栏、OBS、输入法、加速器、手柄软件、各种名字随机的"助手/盒子/直播伴侣"。用户态看起来都像"来路不明的 DLL + 远程线程 + 私有可执行内存"。强检测分不清 Capture 工具和这些东西。
这些不能当默认拦截,更不能闪退、弹"检测到非法程序"、禁止进游戏。
内核驱动 / 内核回调 / ETW-TI
"打个游戏先装驱动"本身就会劝退。再叠加不同 Windows 版本、笔记本厂商驱动、安全软件互殴、偶发蓝屏,卸载是立刻的。非竞技、要铺很多渠道的游戏,基本不会为了防 RenderDoc 上内核。
ACG(Arbitrary Code Guard,任意代码保护)
overlay、录屏、部分输入法、驱动辅助都会在进程里生成或映射可执行内存。一开,轻则 overlay 挂了,重则游戏起不来。
- 内存不能同时具有写入(W)和执行(X)属性:这意味着进程无法申请一块既可写又可执行的内。
- 现有代码不能被修改 :已标记为可执行的代码页不能被更改为可写状态
开启了 ACG 保护的进程,就不能再通过 VirtualProtect、VirtualAlloc 等 API 获得 PAGE_EXECUTE_READWRITE 的内存。这直接切断了攻击者"写入 Shellcode → 修改内存属性 → 执行"的经典利用路径。
ACG 通常与 CIG(代码完整性保护) 配合使用:
- CIG:只允许加载经过微软签名的二进制文件,阻止未签名或不可信模块注入
CIG(只允许签名映像当代码)
等于只让微软签名的代码进进程。Steam、Discord、显卡面板、国内渠道盒子全死。和"用户电脑上什么都有"完全相反。
PPL 保护进程
所有正规 overlay 注入都会失败。游戏栏、好友、录屏、性能叠加层一起没。
按"模块不在 PEB / 私有可执行内存 / 匿名内存里有 PE 头"直接杀
这正是 overlay、注入式录屏、部分加速器和助手的正常长相。误报会非常稳,而且对方名字还千奇百怪,没法靠名单救。
按"线程起始地址不在任何已知模块里"直接杀
远程线程是 overlay 的常规打法。Steam、Discord、NVIDIA 都这么进。杀线程 = 杀叠加层。
instrumentation callback 当拦截
性能和兼容性都不适合这种要跑在各种破烂电脑上的游戏。安全软件、沙箱、老驱动都可能被它搞抽风。
拦一切远程线程 / 拦一切未知 DLL
和上面同一件事。用户不会写成"我被注入了",只会写成"好友没了、FPS 显示没了、录不了屏、输入法候选框没了"。
对这种"要伺候海量杂牌软件、又不是竞技反外挂"的游戏,能挡住手动映射的策略,和会让用户卸载的策略,基本是同一批。
这类游戏实际还能留的
只剩误伤面窄、能讲清楚、失败了游戏还能玩的东西:
• 已知工具黑名单:renderdoc.dll、Nsight、PIX 这类固定名字。你们把 DLL 改名成 CTWResearcher.dll,这条就失效了。这是产品选择,不是没想到改名。
• 只拦加载器、不扫内存:就是现在的 LdrLoadDll hook。只对"走系统加载、且名字撞名单"的生效。overlay 多数仍能过。
• 游戏自己的文件校验:防改客户端资源,不管用户电脑上别的软件。
• 发现了也不杀进程:顶多日志、关闭捕获接口、功能降级。比弹"外挂"少一个差评。
全量内存扫描 / 匿名线程拦截 -> 能发现手动映射,但误伤海量第三方
内存读写执行权限强隔离 -> 能阻断手动映射,但破坏叠加层/录屏
代码完整性保护 -> 能挡未签名 DLL,但兼容性极差
為什麼檢查代碼完整性會導致兼容性差?
代码完整性保护(Code Integrity)导致兼容性差,核心原因在于它从根本上改变了系统对"合法代码"的判定标准,而这个标准与现实中大量第三方软件的运行机制存在冲突。
判定标准过于严苛
代码完整性保护要求所有加载到内存中的可执行代码都必须通过签名验证。这意味着:
- 不仅仅是 DLL 文件本身需要签名,所有依赖的库、插件、驱动都必须有有效签名
- 签名必须是可信根证书链上的,自签名或测试签名都会被拒绝
- 代码在加载后不能被修改,任何运行时补丁(hotpatch)都会触发完整性校验失败
大量商业软件和游戏使用了过期的代码签名证书,或者干脆没有签名。在普通环境下它们运行正常,但在代码完整性保护下会被直接拒绝加载。
现代软件普遍使用 JIT(即时编译) 、脚本引擎 (如 Lua、Python)或运行时代码生成 技术。这些技术会在内存中动态创建可执行代码,而这些代码没有签名,也无法预先签名。代码完整性保护会将其视为"未授权代码"而拦截。
很多商业软件使用加壳工具(如 Themida、VMProtect)保护自身代码。这些壳会在运行时解密并重新映射代码段,导致内存中的代码与磁盘上的原始文件不一致,触发完整性校验失败。
游戏加速器、录屏软件、输入法、翻译工具等常驻型辅助软件,普遍通过 DLL 注入或 Hook 机制工作。这些操作会修改目标进程的内存布局,在代码完整性保护下会被视为"篡改"而阻止。
代码完整性保护的设计初衷是构建一个"白名单"环境,但 Windows 生态本质上是一个"黑名单"环境------默认允许所有代码运行,只有确认恶意才阻止。