ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

x64dbg源码深度解析:调试器原理与插件开发实战指南

x64dbg源码深度解析:调试器原理与插件开发实战指南 简介x64dbg 是适用于 Windows 的开源二进制调试器功能与 OllyDbgOD类似服务逆向工程、脱壳与恶意软件分析场景可对没有源代码的可执行程序进行动态调试和插件扩展。压缩包共 1826 个文件大小约 4.66MB内容包括 cpp/h 核心源码、asm 汇编示例、cmake/bat 构建脚本以及 md 文档、png 截图、ui 界面文件等目录层次清楚便于按模块阅读。预览中的 avx512、sse、mmx、fpu 汇编片段显示其底层指令覆盖较全适合关注调试器内部实现、反汇编引擎和插件 API 的读者研读。当前已有 57 人浏览学习对于想基于真实项目学习调试器开发或进行二次改造的逆向爱好者而言这是一份体积紧凑但信息密度较高的开源资源可节省自行检索源码结构的时间。1. 把 x64dbg 源码看透不只是 OD 的替代品更是理解调试器底层的活教材在逆向和脱壳这个圈子里x64dbg 早就不需要太多介绍了。如果你还在用 OllyDbgOD跟 64 位程序较劲你会越来越难受OD 对 x64 的支持几乎停在十年前动不动就崩、一调试就卡遇到反调试直接傻眼。x64dbg 的出现正好补上了这块短板它不只是界面长得像 OD更重要的是它开源——整个调试引擎、反汇编模块、插件 SDK 全部裸露在你面前。这意味着你不仅能拿它当工具用还能翻开源码看明白断点、单步、内存断点、异常分发这些机制到底是怎么实现的甚至可以改源码定制出属于自己的调试器。这篇文章适合两类人一是想从 OD 迁到 x64dbg 的逆向从业者二是想深入理解调试器实现原理、或者打算做插件开发的源码爱好者。2. 源码架构拆解拿到手先分清六个模块别一头扎进 GUI 里出不来2.1 顶层目录与模块边界gui、dbg、bridge 各管什么x64dbg 的源码包解压之后第一眼看到的是几十个文件夹如果没有一个整体视角很容易迷失在细节里。我一般拿到源码会先看根目录下的src结构这里的边界划分其实非常清晰。核心就三块gui是 Qt 写的界面层dbg是调试引擎本体bridge是两者之间的通信桥梁。先说dbg目录这是整个项目的心脏。它负责所有跟调试目标进程直接打交道的逻辑设置断点、处理异常、读写内存、管理线程栈、反汇编分析、符号解析、模块枚举这些东西全部在dbg里完成。它不关心界面长什么样只暴露接口给上层调用。gui目录则是你看到的那个窗口程序反汇编视图、寄存器面板、堆栈窗口、内存 dump 视图全部由 Qt 绘制。它通过bridge目录里定义的命令接口跟dbg通信比如你按 F9 继续运行界面层只是发出一条命令字符串真正的SetBPX、StepOver这些操作发生在dbg层。这种桥接模式让前后端完全解耦你调试的时候偶尔发现界面卡住基本能判断是dbg线程在做耗时操作而不是 GUI 本身的问题。还有几个相对独立的子模块值得留意pluginsdk是给第三方插件开发者用的头文件和接口定义lzma、zlib这些是内置的第三方库用于解压和压缩x64dbg和x32dbg各自的启动入口也在src底下。建议初读源码时把精力集中在dbg目录GUI 部分等需要改界面时再看也不迟。2.2 调试循环Debug Loop的核心机制WaitForDebugEvent 与命令队列调试器最底层的引擎其实是一个循环这个循环的基础是 Windows 提供的调试 API。x64dbg 的dbg模块里有一个核心函数负责处理这个循环它的骨架用伪代码表达大致是下面这个样子// 调试主循环的简化示意源码位于 dbg/debugger.cpp while (g_bRunLoop) { DEBUG_EVENT de { 0 }; // 1. 阻塞等待调试事件timeout 设为 INFINITE if (!WaitForDebugEvent(de, INFINITE)) { // 出错处理GetLastError() continue; } // 2. 统一交给事件处理器分发 bool bHandled HandleDebugEvent(de); // 3. 判断状态是否处于暂停态、是否有用户命令待处理 if (g_bPause) { PrintDbgView(Debugger paused.\n); // 暂停时处理用户输入的命令队列 ProcessCommandQueue(); } // 4. 恢复目标进程继续运行 ContinueDebugEvent(de.dwProcessId, de.dwThreadId, bHandled ? DBG_CONTINUE : DBG_EXCEPTION_NOT_HANDLED); }这段循环是整个调试器能“动”起来的核心逻辑。WaitForDebugEvent是 Windows 调试 API 的基石它会把目标进程里发生的所有关键事件——断点触发、异常抛出、线程创建、模块加载、进程退出——塞进一个DEBUG_EVENT结构体里交给调试器。x64dbg 拿到事件后就调用HandleDebugEvent做分发比如收到EXCEPTION_BREAKPOINT就知道是 int3 断点命中收到LOAD_DLL_DEBUG_EVENT就触发模块加载通知。参数的关注点在于ContinueDebugEvent的第三个参数对断点异常返回DBG_CONTINUE对非调试器关心的异常返回DBG_EXCEPTION_NOT_HANDLED把决定权交还给系统。这个细节极其重要如果你在自定义调试器里把每个异常都吞掉目标进程的错误处理逻辑会全部失效脱壳时壳的异常反调试逻辑也有可能失灵。2.3 断点管理的完整生命周期从 SetBPX 到内存断点、硬件断点的分派断点是调试器的灵魂x64dbg 的断点管理在dbg模块里做得非常系统。它把断点分成三大类软件断点int3、硬件断点DR0-DR3 寄存器、内存断点页面权限触发。每种断点的生命周期都走同一条路径设置→触发→处理→移除。// dbg/breakpoint.cpp 中的断点设置核心逻辑简化 bool SetBreakpoint(BP_TYPE type, duint addr, BPNOTIFY notify) { // 根据类型分派 switch (type) { case BP_NORMAL: { // 软件断点 // 读原字节保存后用 0xCC 覆盖 unsigned char original 0; MemRead(addr, original, 1); gc_bpdata[addr].original original; unsigned char int3 0xCC; MemWrite(addr, int3, 1); FlushInstructionCache(); // 关键不刷新指令缓存会拿到旧指令 break; } case BP_HARDWARE: { // 硬件断点数量有限x64 下最多 4 个 // 需要从 DR0-DR3 里找一个空闲槽位 int slot FindFreeHardwareSlot(); SetHardwareBreakpoint(slot, addr, 1 /* size */, 0 /* execute */); break; } case BP_MEMORY: { // 内存断点原理修改页面属性为 PAGE_GUARD DWORD oldProtect 0; VirtualProtectEx(hProcess, (LPVOID)addr, 1, PAGE_EXECUTE_READ | PAGE_GUARD, oldProtect); break; } } return true; }这里边最容易翻车的是软件断点的FlushInstructionCache。我在自研调试器的时候漏过这一步结果断点设置后首次命中正常第二次就出现“断点飞掉”的诡异现象——原因就是 CPU 的指令缓存里还留着旧字节。MemRead和MemWrite是dbg层封装好的进程内存读写接口背后是ReadProcessMemory和WriteProcessMemory。硬件断点只有 4 个槽位是硬限制这是 x64 CPU 架构决定的。x64dbg 源码里FindFreeHardwareSlot会遍历当前线程上下文里的调试寄存器找到闲置的 DR 寄存器就写入。它的局限在于 DR0-DR3 只能存地址长度和条件要靠 DR7 控制。内存断点则是利用PAGE_GUARD页面属性实现“访问即触发”它的代价是性能极差不适合在频繁访问的内存区域下断点否则程序会慢如蜗牛。3. 从源码编译出你自己的 x64dbg环境准备与完整构建流程3.1 构建环境清单Qt 版本、编译器选型与几个易错依赖想玩转源码第一步是先把它构建出来跑起来。x64dbg 的构建文档在BUILD.md里写得很清楚但历史上的坑不少。我根据自己在多台机器上编译的经验把环境整理成了一张表组件推荐版本注意点Visual Studio2019 或 2022必须勾选“使用 C 的桌面开发”工作负载Qt5.15.x开源版msvc2019_64 套件CMake3.16 以上建议用 VS 自带的或单独安装最新版Windows SDK10.0.19041安装 VS 时默认带上即可Python3.x可选编译脚本增强工具需要编译前要把Qt的bin目录加到PATH环境变量里否则qmake和windeployqt找不到。还有一个经常让人卡半天的依赖x64dbg 源码里用到了一个名为DeviceIOctl类型的 DDK 头如果在windows.h和winioctl.h的包含顺序上出问题会报一堆莫名其妙的宏重定义错误。我一般会先把WIN32_LEAN_AND_MEAN定义好减少无关头文件的干扰。3.2 分步构建用 CMake 生成工程并用 VS 编译三个需要手动调整的开关构建流程本身不复杂但有几个开关不看源码根本不知道含义。我用命令行方式做完整构建步骤是固定的# 1. 拉取源码如果你拿到的源码包已经解压跳过这一步 git clone --recursive https://github.com/x64dbg/x64dbg.git cd x64dbg # 2. 创建构建目录并配置 mkdir build cd build # 这行命令是关键-DQT_DIR 指向 Qt 的安装路径 cmake .. -G Visual Studio 16 2019 \ -A x64 \ -DQT_DIRC:/Qt/5.15.2/msvc2019_64 \ -DBUILD_DEMOOFF \ -DENABLE_LTOOFF # 3. 编译 release 版本 cmake --build . --config Release --parallel 8-DBUILD_DEMO是官方文档里存在的一个演示项目开关默认关闭即可。ENABLE_LTO是链接时代码优化实测打开后编译时间会显著变长但生成的二进制体积和速度优化并不明显我建议关掉加速构建。构建完成后输出目录会生成x32\x32dbg.exe和x64\x64dbg.exe两个可执行文件。编译完成只是第一步运行前还要把 Qt 的运行库拷贝到可执行文件旁边。官方构建脚本里有这步操作你手动编译时容易漏# 4. 部署 Qt 依赖到可执行文件目录 windeployqt --release --no-translations build/x64/Release/x64dbg.exe windeployqt --release --no-translations build/x32/Release/x32dbg.exe3.3 构建后自检用命令行参数启动验证调试器能附加到进程构建产物能不能用最直接的验证方式是跑一次实际的调试流程。我会先用自带命令行模式做冒烟测试不点 GUI直接命令行加载目标程序# x64dbg 支持命令行启动并执行脚本 x64dbg.exe -open C:\test\sample.exe -run bp MessageBoxW; g上面这条命令的意思很明确打开sample.exe设置MessageBoxW断点然后继续运行直到断点命中。如果调试器能正常停在MessageBoxW入口处说明驱动层、断点引擎和命令解析全部工作正常。如果这一步没反应多半是调试引擎初始化失败或者符号加载路径有问题要在 GUI 里看Log标签页的输出。我自己的习惯是跑完冒烟测试后再用 x64dbg 附加到notepad.exe上玩一轮附加调试比启动调试更容易暴露权限问题和反调试冲突值得养成这个习惯。4. 自己写一个插件把 x64dbg 的插件 SDK 用起来扩展右键菜单和命令4.1 插件 SDK 的核心接口plugin_init、plugin_command 与导出符号表x64dbg 的生态之所以强就是因为插件 API 设计得简单直白。你只需要写一个 DLL导出几个特定名字的函数调试器启动时就会自动加载。pluginsdk目录里的plugin.h和plugin_common.h是全部你需要包含的头文件。插件的入口约定非常固定。核心就三个回调函数// plugin.cpp —— 一个最小可编译的 x64dbg 插件骨架 #include plugin.h // 插件初始化加载时被调用一次 extern C __declspec(dllexport) void plugin_init() { _plugin_registercommand(MyDumpCmd, my_dump_command, true); _plugin_registercommand(MyInfoCmd, my_info_command, true); } // 插件卸载时的清理 extern C __declspec(dllexport) void plugin_stop() { _plugin_unregistercommand(MyDumpCmd); _plugin_unregistercommand(MyInfoCmd); } // 自定义命令的实际执行函数 static bool my_dump_command(int argc, char** argv) { if (argc 2) { _plugin_logputs(用法: MyDumpCmd [表达式]); return false; } duint addr _expression_eval(argv[1]); _plugin_logprintf(当前地址: %p\n, (void*)addr); return true; } static bool my_info_command(int argc, char** argv) { _plugin_logputs(MyInfoCmd 执行成功); return true; }这份骨架里的_plugin_registercommand是关键 API它把命令名跟函数绑定起来。_expression_eval是 sdk 里提供的表达式求值函数可以直接使用用户输入的寄存器名、地址表达式比如MyDumpCmd eip就拿到了当前指令指针。_plugin_logprintf负责把输出打到 x64dbg 的日志窗口。插件编译出来后就是一个 DLL放到x64dbg可执行文件所在目录下的plugins子目录即可注意x64dbg.exe加载plugins\x64下的x32dbg.exe加载plugins\x32下的别放错目录。放错位置是最常见的“插件未加载”原因。4.2 实战扩展给反汇编右键菜单加一个“显示调用方信息”的功能命令行插件只是入门真正常用的是给反汇编界面加右键菜单项。x64dbg 的 SDK 提供了菜单接口可以在指定菜单位置插入条目。// 在 plugin_init() 中追加菜单注册 extern C __declspec(dllexport) void plugin_init() { // 注册命令 _plugin_registercommand(Callers, cmd_callers, true); // 注册菜单在反汇编右键菜单里追加一组入口 int hMenu _plugin_menuaddentry(plugin_handle, MENU_ENTRY_CALLERS, 查看调用方); // 绑定菜单回调 callback_register(hMenu, menu_callback); } // 菜单点击后的回调 static void menu_callback(CBTYPE cbType, PLUG_CB_MENUENTRY* info) { if (info-hEntry MENU_ENTRY_CALLERS) { // 获取当前光标处的地址 duint cur _gui_disasmgetselection(); _plugin_logprintf(正在分析调用方: %p\n, (void*)cur); // 在这里调用你自己的分析逻辑枚举引用该地址的指令 analyze_callers(cur); } }这段代码里的_gui_disasmgetselection是 GUI 接口返回反汇编窗口当前选中的地址。菜单 ID 需要自己声明一个常量控制好每个菜单项的唯一性。analyze_callers是我自己写的函数内部可以用_reg_getcontext获取寄存器或者用_dbg_getfunction获取所在函数范围再遍历指令找 CALL 引用。实际用下来这个功能非常实用定位某个关键 API 时右键一点就能看到所有调用点省去了手动切到Reference窗口的功夫。写插件并不需要改调试器核心代码但这套 SDK 的接口设计本身就值得细读。4.3 插件的构建配置与调试技巧VS 工程设置、日志输出定位加载失败插件 DLL 的构建配置有几个必须注意的设置否则编出来的 DLL 根本加载不了。对x64dbg.exe来说配置平台必须选x64x32dbg.exe则对应x86。运行库要用多线程 DLL/MD不能选静态链接/MT否则 CRT 冲突会让插件注入直接失败。比较隐蔽的一个坑是“导出符号被裁剪”。如果你在 VS 里建 DLL 工程后没有把上面那些函数标注__declspec(dllexport)链接器默认不会导出它们x64dbg 加载时发现没有plugin_init导出就直接跳过。排查方法是用dumpbin /exports MyPlugin.dll查看导出表确认函数名是plugin_init、plugin_stop、plugin_cb这些如果名字被修饰成了?_plugin_init...检查工程是否定义了__cdecl导出约定。插件加载失败时的日志定位重点是 x64dbg 的日志窗口第一行有没有Plugin loaded信息。如果日志窗口里连插件加载事件都没出现要么是 DLL 的依赖库缺失要么是导出表不对。我遇到过一次插件 DLL 依赖了一个不存在的第三方库x64dbg 加载时直接跳过日志里只有一句 “Failed to load plugin” 的提示用Dependency Walker查依赖才定位到问题。5. 避坑指南x64dbg 源码使用中最常踩的 5 个坑5.1 x64dbg 调试时显示“无法启动调试器”现象点启动后日志窗口直接报错目标程序没有启动。 原因最常见的是目标程序路径权限不足或者调试器本身没有管理员权限——Vista 以后附加到受保护进程需要管理员权限普通权限会被拒绝。 解决用管理员身份运行 x64dbg。如果还不行检查杀毒软件是否拦截了调试器创建调试进程的行为Windows Defender 的“基于虚拟化的安全性”也偶尔会干扰调试器附加。5.2 x64dbg 的“单步执行”不走点一次 F8 后程序直接跑飞现象在某个代码区域按下 F8 单步本应进入下一行指令结果调试器直接变成“运行中”再也停不下来。 原因这条路径上有反调试代码。反调试技术里有一种手法是把断点寄存器和调试寄存器清空或者用NtContinue跳转到特殊位置。如果你的断点被壳顺带清除了单步就会失效。 解决先用hide debugger插件比如ScyllaHide隐藏调试器痕迹再尝试单步。还是不行的话改用硬件断点或内存断点避开指令级检测。5.3 软件断点下多了之后调试速度明显变慢现象程序里下的断点超过几十个之后操作开始卡顿。 原因软件断点每命中一次就要经历“恢复原字节→单步→重新下断点”的完整周期断点多了这个开销会累积。 解决核心逻辑只保留 2-3 个关键断点多用条件断点替代逐个单步。条件断点在 x64dbg 里可以直接在断点对话框里写表达式比如eax 0x1234等于在断点命中后才做条件判断省掉大量无意义的暂停。5.4 附加进程后寄存器窗口全是 0代码也看不到现象附加到正在运行的进程时寄存器面板显示的全是零反汇编窗口也没有有效内容。 原因进程的线程上下文没同步。附加调试跟启动调试不同它需要把当前所有线程的上下文拉到界面如果主线程停在哪一个不稳定的内核态寄存器显示就可能错乱。 解决在 x64dbg 的命令行执行pause强制暂停所有线程再刷新视图。或者干脆先在 Windows 的任务管理器里把进程“挂起”再附加。5.5 自己编译的 x64dbg 一打开就闪退现象编译成功但双击x64dbg.exe就闪退连窗口都看不到。 原因十之八九是 Qt 插件依赖缺失或者是编译配置里开的某些优化导致不兼容。内部 Qt 插件路径和编译时设置的路径不一致也会引发闪退。 解决直接复制 Qt 安装目录里的plugins文件夹到 x64dbg 可执行文件旁边保证platforms\qwindows.dll能找到。然后用命令行方式启动x64dbg.exe -d或查看 Windows 事件查看器的崩溃日志定位是哪一行代码触发崩溃。6. 进阶玩法把 x64dbg 当“半自动化调试器”用命令脚本与条件断点的组合技巧调试脱壳程序时全程手动操作非常低效x64dbg 的命令脚本能力可以帮你把常规流程自动化。我最常用的方式是把“下断 记录 继续”这套循环写成一个脚本在命令行里直接跑。举一个实际场景脱一个简单的 UPX 壳需要定位 OEP原始入口点。UPX 壳的典型特征是末尾有一个jmp跳到原始入口。这个跳转的特征是pushad保存寄存器解压结束后popad加jmp指令。你可以用脚本自动搜索这一段特征// oep_find.txt —— 一段 x64dbg 脚本自动找 UPX OEP 特征 // 创建脚本文件后用 x64dbg 的命令行 script 命令执行 var target 0x401000; var end 0x406000; loop: // 在范围 [target, end) 中搜索 popad 指令0x61 find target, end, 61 cmp $result, 0; je not_found // 检查 popad 之后是否有 jmp 指令0xE9 相对跳转 find $result1, $result5, E9 cmp $res, 0; je no_jmp // 如果找到打印地址并暂停 log OEP 候选: $result1 pause no_jmp: set target, $result1 jmp loop not_found: log 未找到 OEP这段脚本用到了 x64dbg 脚本引擎里的find命令它的参数包括开始地址、结束地址和要搜索的字节序列。log命令把信息输出到日志窗口pause暂停调试让脚本停止。用这种方法可以快速从壳的末尾代码区筛出popad jmp组合。条件断点是第二个高价值技巧。手动给一个 API 下断点时把它设成“只在参数等于某个值时才停”// 命令行断点写法MessageBoxW 断点仅当 4 个参数里的第一个参数是 0 时才停 bp MessageBoxW, rdx 0 // 也可以配合 [esp..] 或寄存器写复杂条件 bp CreateFileW, [rsp0x28] ! 0这里rdx在 x64 调用约定下是第四个参数。如果断点条件不满足调试器不会暂停代码继续跑性能损耗极小。对高频调用的 API 来说不用条件断点做好筛选的话手点“继续”会点到你怀疑人生。最后分享一个我自己的习惯用 x64dbg 的x64dbg.ini做初始配置把常用脚本路径加到“初始化脚本”选项里每次启动自动执行。我还在脚本里默认做了几件事清空日志、关闭自动更新、设定hide debugger的默认配置。从那以后我每次拿到新版本或新机器都强制走一遍“编译 → 冒烟测试 → 跑初始化脚本 → 附加真实程序验证”这套流程确保不会有环境差异带来的奇怪问题。希望这篇笔记能帮你在 x64dbg 的源码和插件开发路上少踩几个坑。本文还有配套的精品资源点击获取
返回列表