简介x64dbg 源码包是一款面向 Windows 的开源二进制调试器完整工程专为恶意软件分析、逆向工程与软件脱壳而设计它提供丰富的调试功能和全面的插件系统适合需要深入底层调试、二次开发或定制调试能力的开发者与安全研究人员。压缩包约 4.66MB共 1826 个文件核心代码以 C/C 头文件.h、.cpp、.c为主辅以 Markdown/reStructuredText 文档、Qt 界面文件.ui、汇编模块.asm以及 CMake/Bat 构建脚本便于阅读源码、编译工程和二次修改。包内还包含 AVX512、SSE、MMX、FPU 等架构相关的汇编示例以及用于构建、生成文档和安装钩子的辅助脚本可帮助读者快速搭建开发环境、生成帮助文档并从中理解调试器的底层指令处理与用户界面设计思路。目前已有 57 人学习/下载适合作为学习调试器实现、扩展插件能力的参考也可在此基础上进行个性化功能开发。1. 从 OllyDbg 换到 x64dbg为什么这套源码值得自己啃一遍打开 x64dbg 源码第一眼你会看到熟悉的 OllyDbg 式布局左侧反汇编、右侧寄存器、底部一条命令输入框。很多老逆向习惯把它当成“OD 的 64 位升级版”但真正翻过代码的人会发现x64dbg 把调试器最吃劲的三块——调试循环、断点管理、命令交互——做成了边界清晰的三个工程。读这套源码能直接回答为什么断点命中后指令显示会“变味”、附加进程时发生了什么、插件 API 不够用该去哪里改。适合的人群是有具体诉求的从业者要扩展调试器功能、想做自己的分析工具或想把 Windows 调试机制吃到透。这套源码能落地的价值在于它的行为大多能在 dbg 工程里找到对应处理函数不用隔着黑匣子瞎猜。下面按“拉源码编译 → 拆核心模块 → 走命令链路 → 记录翻车现场 → 写最小插件”的顺序走一遍。先说清楚这里说的 OD 就是 OllyDbg调试器圈子里一直这么叫。2. 拉取 x64dbg 源码并在本地编译工具链与三层项目结构2.1 为什么要拆成 gui、dbg、bridge 三个工程x64dbg 源码顶层分三块gui 是 Qt 界面工程dbg 是调试核心bridge 是连接二者的中间层。dbg 工程不依赖 Qt它只调用 Windows 调试 API 和少量第三方库负责附加进程、处理调试事件、读写内存、管理断点gui 工程不直接操作目标进程所有操作都通过 bridge 桥接函数转给 dbg。我第一次自己写调试器时把界面逻辑和调试逻辑塞进一个类改个按钮要翻半天代码后来才理解分层的好处调试核心可以脱离界面单独验证比如喂一个异常事件直接看断点表的反应。bridge 这层不是单纯转发它还扛着数据类型转换和线程安全。dbg 的调试循环跑在独立线程gui 跑在主线程两边共享状态都靠 bridge 的锁和消息机制同步。所以你想加一个“读取当前 EIP 附近内存”的功能正确路径是gui 调 bridge 的 DbgGetRegDump → bridge 转发到 dbg 的寄存器缓存 → 结果再返回来。第一次读代码时先把这条链路画出来不然函数之间套来套去很容易绕晕。三个工程的编译顺序也有讲究先编第三方依赖再编 dbg再编 bridge最后编 gui。实际用 CMake 一条命令就能跑通因为仓库里把依赖关系声明好了。这里的依赖指的是 capstone 这类反汇编库和 Qt下面编译步骤会看到它们在 CMake 里怎么被找到。另外x64dbg 同时维护 32 位和 64 位两套产物共用同一份核心代码只是前缀和位数不同。2.2 用 Visual Studio 和 CMake 在本地跑通的最小编译步骤编译 x64dbg 需要三样东西Visual Studio2019 或 2022 都行C 桌面开发工作负载必须勾上、Qt 开源版我一般用 5.15 的 msvc2019_64 包6.x 也可以、一个可靠的 git 客户端。官方仓库在 github 的 x64dbg 组织下克隆时一定要加 --recursive否则子模块里的反汇编引擎等依赖是空的编译到一半报缺头文件血泪经验。git clone --recursive https://github.com/x64dbg/x64dbg.git cd x64dbg mkdir build cd build cmake -G Visual Studio 17 2022 -A x64 -DQT_DIRC:\Qt\5.15.2\msvc2019_64 .. cmake --build . --config Release --target x64dbg第一行克隆整个仓库和子模块子模块里有 capstone反汇编解析、lz4压缩等第三方库。第二到四行建立独立 build 目录源码目录不被编译产物污染以后看 git 改动记录也清爽。cmake 这行核心是指定生成器为 VS2022、架构 x64用 -DQT_DIR 告诉 CMake Qt 装在哪最后的两个点指源码根目录。最后一行只构建 x64dbg 目标想顺手编 32 位壳就改成 --target x32dbg两套壳共用同一套核心代码。如果 CMake 报找不到 Qt5 或 Qt6Config.cmake九成是 -DQT_DIR 指错了目录。Qt 安装后同一个版本会有 msvc2019_64、mingw_64 等多个子目录要指到带 msvc 的那个不是 Qt 根目录。如果报缺头文件先别怀疑代码跑一次git submodule update --init --recursive把子模块补全再说。还有一类报错是“找不到 MSVC 编译器”那是 VS 安装时没勾 C 桌面开发重装一下组件即可。2.3 编译成功后跑起来之前先做这几件事编译完成后exe 和 dll 分散在 build 下的 Release 子目录。直接把 x64dbg.exe 单独拎出来双击十有八九起不来因为插件和配置结构要求按固定目录摆放。我在本地一般把 build 下的 Release 输出整个当运行根目录确认里面同时存在 x64dbg.exe、x32dbg.exe、plugins 目录和必要的依赖 dll缺哪个就手动复制过去。第一次启动前我会先用命令行在根目录跑一次 x64dbg.exe看有没有弹缺失 DLL 的报错再进图形界面。首次启动时 x64dbg 会生成 x32dbg.ini 和 x64dbg.ini。改了字体、颜色重启后丢失多半是 ini 写入被拦截或者当前用户对运行目录没有写权限。开发自用可以暂时不管配置但排查插件加载问题时要学会看日志View → Log 窗口会逐条记录插件加载成功还是失败这是所有插件问题的第一入口。很多人找不到日志窗口直接去菜单里翻插件列表反而绕远了。插件目录在运行时按“exe 同级目录下的 plugins”找32 位壳找 x32 后缀的 dll64 位壳找 x64 后缀的。有的新手把 64 位插件扔进 32 位壳目录日志里一个错误都没有菜单里就是不出东西这个现象背后的原因到避坑章节展开。现在先把编译跑通进入核心模块拆解。3. 调试器骨架拆解从进程附加到第一个断点命中3.1 调试循环一个线程怎么等完整个进程的一生所有 Windows 调试器的地基是同一组调试 APIDebugActiveProcess 发起附加WaitForDebugEvent 阻塞等待调试事件事件处理完用 ContinueDebugEvent 让进程继续跑。x64dbg 的 dbg 工程里有一个线程专门跑这个循环事件进来后按类型分发。下面这段是从核心逻辑里提炼出的骨架真实代码还要处理模块加载、线程创建等补充逻辑但主线就是这样的。// 调试循环主线程的伪代码只保留主干 for (;;) { DEBUG_EVENT evt {0}; if (!WaitForDebugEvent(evt, INFINITE)) { break; // 句柄无效或目标进程已退出 } DWORD continueStatus DBG_EXCEPTION_NOT_HANDLED; switch (evt.dwDebugEventCode) { case EXCEPTION_DEBUG_EVENT: // 先把异常交给上层分发决定吞掉还是转交程序 continueStatus HandleException(evt); break; case CREATE_PROCESS_DEBUG_EVENT: // 记录主模块路径、进程句柄、入口地址 OnCreateProcess(evt); break; case EXIT_PROCESS_DEBUG_EVENT: OnExitProcess(evt); break; case LOAD_DLL_DEBUG_EVENT: case UNLOAD_DLL_DEBUG_EVENT: // 模块加载卸载刷新模块窗口和符号表 OnModuleEvent(evt); break; case OUTPUT_DEBUG_STRING_EVENT: OnOutputDebugString(evt); break; } ContinueDebugEvent(evt.dwProcessId, evt.dwThreadId, continueStatus); }这个循环最关键的变量是 continueStatus。断点异常EXCEPTION_BREAKPOINT和单步异常EXCEPTION_SINGLE_STEP是调试器自己设的要回传 DBG_CONTINUE告诉系统“这个异常我接手了”非法访问ACCESS_VIOLATION这类目标进程的真实异常如果打算交给程序自己的异常处理就得回 DBG_EXCEPTION_NOT_HANDLED。新手改这个循环最容易犯的错是统一回 DBG_CONTINUE结果目标进程内部 try/catch 永远接不到异常表现成“程序不崩溃但行为诡异”。x64dbg 在这一层之上还做了个简化每个用户操作F7 单步、F9 运行最终都变成往调试线程投一个任务任务执行完再通过 bridge 通知界面刷新。这样 UI 线程不会卡在 WaitForDebugEvent 上但也带来一个常见观感按了 F9 界面还停留在暂停状态过一会儿才跑起来那是因为调试线程还没完成上一次事件处理。读代码时看到 GUI 和 dbg 之间的消息投递不要惊讶这是异步调试的正常节奏。3.2 断点管理软件断点、硬件断点与内存断点各有各的账本x64dbg 同时支持三类断点。软件断点原理是在目标地址写入一个 0xCCINT3CPU 执行到这里触发 EXCEPTION_BREAKPOINT调试器收到后把 EIP 回退一个字节再显示原始指令。dbg 工程里每个软件断点占一条记录存地址、原字节、所属模块、是否临时删除时要写回原字节。这里有个很直观的坑直接 ReadProcessMemory 读断点地址会读到 0xCC而不是原始指令所以 x64dbg 专门做了带断点还原的内存读取接口避坑章节再展开。硬件断点走另一条路CPU 的 DR0~DR3 寄存器存地址DR7 控制启停和访问宽度。硬件断点不用改内存适合只读区域或不好写的位置缺点是总共只有 4 个寄存器而且每个线程切换上下文都要重新铺一遍。内存断点则是把目标页设置成 PAGE_GUARD访问时触发异常x64dbg 里同一时刻通常只允许一个内存断点因为守卫页机制不支持多个共存。三者的触发路径差异很大所以 dbg 工程里是分开存储、分开处理的。对应到命令层就是 bp 家族bp 是软件断点bph 是硬件断点bpm 是内存断点查断点用 bl删断点用 bc。初学调试器开发的人总想把三种断点揉进一个结构实际维护下来发现还是分开清爽因为命中逻辑和恢复逻辑完全不同。x64dbg 的实现就是一个很好的参考样例照着它拆避免给自己挖坑。3.3 反汇编窗口背后capstone 与指令缓存反汇编窗口每刷新一屏背后都有一套完整流程读当前地址附近内存交给反汇编引擎逐条解码再按地址范围显示。x64dbg 用的是 capstone跨架构反汇编框架比直接用系统的反汇编函数方便在能统一处理 x86/x64 指令长度解析。源码里还做了一层缓存同一段地址在一个暂停周期内不会重复解码避免拖动滚动条时卡顿。缓存失效的时机很关键。命中断点、单步执行、修改内存后都必须清缓存否则反汇编窗口显示的还是旧指令。很多人在这个点翻车代码逻辑改对了界面不刷新折腾半天才发现根本没触发缓存失效通知。x64dbg 的做法是通过 bridge 发一个刷新消息gui 收到后重新抓内存、重新解码顺序不能反。反汇编之外x64dbg 还分析了跳转关系、调用目标和字符串引用这些附加数据挂在指令记录的边上。想加一种“显示所有全局引用”的窗口入口就在这个分析数据层而不是去改绘制函数。把这三层结构理解透你会明白改 x64dbg 通常是“改 dbg 的数据结构 加 gui 的表单项”而不是在绘图代码里堆条件分支。4. 把“bp CreateFileW”这条路走通命令系统与表达式求值4.1 命令系统命令表、参数解析与注册入口x64dbg 的命令行是它的半条命。命令框里输入的内容不是临时脚本而是走一套完整分发命令名查表 → 参数数量检查 → 参数解析 → 回调执行 → 界面刷新。任何一条命令背后都是一个函数指针命令表在 dbg 工程里初始化插件也能通过 _plugin_registercommand 往里塞新命令。所以读懂命令系统等于同时读懂了内建命令和插件命令的运行机制。分发过程最容易被忽略的是“参数解析”这一步。表达式求值器会在解析参数时被调用所以命令参数可以是寄存器名eip、地址表达式esp0x14、模块符号kernel32.CreateFileW或内置函数。命令系统本身不做这个解析它把字符串原样交给求值器求值器返回一个 duint 值。这个设计让命令层非常薄插件命令写起来也沾光不用自己写“字符串转地址”的工具函数。4.2 表达式求值器寄存器、内存引用与内置函数表达式求值器是 x64dbg 命令行最有价值的部分。一个表达式可以是变量、寄存器、内存引用、算术运算的组合比如 [esp0x4] 的意思是“读 esp4 这个地址处的内存”。这里的方括号是取内存相当于很多脚本语言里的解引用。求值器还能认内置函数常见的有 mod.entry 取模块入口disasm(addr) 反汇编一条指令readdword(addr) 读 4 字节内存。懂了这个再看命令就通透了。执行 bp [eip1]命令先求值表达式 [eip1] 得到地址再对这个地址下断点条件判断时两边的表达式也是同一套求值器在算。这些能力在你自己的插件命令里都能直接用入口是 DbgEval 这个 bridge 函数。我第一次在插件里调用 DbgEval 时有点不习惯觉得它返回的不过是个整数后来才意识到调试器场景下地址本来就是数值求值器把“符号变成地址”这件事做掉了极大省事。4.3 从字符串到下断点bp kernel32.CreateFileW 的完整路径现在把一条最常用的命令拆开看bp kernel32.CreateFileW。命令解析器先取出命令名 bp调用对应断点命令处理函数参数是 kernel32.CreateFileW 这个字符串。它不会直接被写进内存中间隔了几层处理。第一层表达式求值器认出“模块.导出名”格式去当前进程的模块列表找 kernel32没加载就去加载模块再从导出表查 CreateFileW查到返回模块基址加偏移后的绝对地址。这一步用到的是 dbg 工程里的符号服务把“kernel32.CreateFileW”变成 0x7FF 开头的一串数字。第二层断点命令拿到地址检查断点表里是否已存在没有就调用软件断点设置函数写 0xCC、保存原字节并标记活动。第三层通知界面刷新反汇编窗口同时把这条断点写进日志。// 断点设置函数的简版逻辑路线参考 dbg 工程 bool SetBreakpoint(duint addr) { if (IsSoftBreakpoint(addr)) return false; // 重复下断直接拒绝 BYTE orig; if (!DbgMemRead(addr, orig, 1)) return false; // 地址不可读比如内存尚未提交 DbgMemWrite(addr, 0xCC, 1); // 写入 INT3 指令 SoftBpMap[addr].origByte orig; SoftBpMap[addr].active true; GuiUpdateDisasm(); // 通知界面刷新反汇编 return true; }这段流程最值得学的是顺序先检查可读性再写字节再保存原字节。很多初写调试器的人在不可读的地址上下断导致写入失败但断点表里多了个废记录后续所有操作都背着这个错误账本跑。x64dbg 写入前做 DbgMemRead失败直接返回 false上层向用户报“地址不可读”而不是让调试循环里多出一个永远触发的幽灵断点。条件断点 bpcnd 的原理也在这层命令解析时把表达式存进断点记录命中异常后求值器先判表达式真假为假就不暂停为真才停下来。理解了这个链路再复杂的命令组合都能拆回“命令名 参数表达式”两个部分来看。5. 调试器开发路上的常见问题五条踩坑记录5.1 附加进程后界面显示已暂停目标进程却在跑现象用 File → Attach 附加进程暂停按钮亮了但目标进程该动还在动按 F9 也没有恢复感。原因附加流程里调试线程等待的是第一个调试事件。多线程程序在附加瞬间主线程可能还没进入调试暂停点界面收到的状态和线程实际执行状态错位。另一种常见版本是根本没附加成功目标进程被保护钩子挡住句柄没拿到。解决先看调试器日志有没有附加成功记录没有就换管理员身份运行调试器。改源码的话重点检查附加后是否向所有线程同步派发了一次暂停信号x64dbg 靠这个让所有线程停在同一个同步点。5.2 软件断点命中后反汇编窗口的指令对不上现象在函数入口下 F2 断点命中后看到的不是 push rbp 这类函数头而是一排 CC CC。原因反汇编窗口读内存时走了后门绕过了断点掩码。dbg 工程的内存读取接口会把软件断点占用的地址还原成原始字节但插件里直接调用 ReadProcessMemory 的话读到的就是断点指令本身。解决读目标进程内存一律走 bridge 的 DbgMemRead 或等价接口不要自己直接持有进程句柄去读。看源码时注意 DbgMemRead 内部会先查软断点表再决定返回原字节还是真实字节。这个坑属于看懂了断点存储却没看懂内存读取。5.3 插件加载失败日志里却什么错误都没有现象把编译好的 dll 放进 plugins 目录重启 x64dbg插件菜单空空如也日志窗口也没有报错。原因多数是插件位数或导出符号不对。x64dbg 加载插件时会检查 dll 是否导出了 pluginit并要求插件位数和调试器壳一致。64 位编译器编出来的 dll 放进 x32dbg 目录自然静默失败。解决先用 dumpbin /exports 看 dll 有没有导出 pluginit再用 Dependencies 工具看依赖的运行时是否缺失最后确认放对了目录。日志里插件加载那一行的返回值含义源码的 PluginLoad 函数里都有记录认真看一次就不会再瞎猜。5.4 64 位场景下地址莫名被“截断”成 32 位现象用 x64dbg 调试 64 位程序有的窗口地址显示正常但表达式求值返回的地址高位变成 0后续按这个地址读内存全是错的。原因在 64 位场景里任何地址和句柄都不该用 unsigned long 或 uint32_t 存。x64dbg 的桥接类型 duint 在 64 位下是 64 位宽但部分旧接口历史包袱用 DWORD 传参高位自然丢失。解决自己写插件统一用 duint不要用 unsigned long。读源码时遇到地址经过 DWORD 转换的地方基本判为历史接口新代码都走 duint。切不可为了对齐某个旧接口把 duint 强转成 uint32_t那是给自己埋雷。5.5 硬件断点一多目标进程慢得像死机现象同时下三个以上硬件断点后目标进程运行速度急剧下降单步也卡CPU 占用很高。原因硬件断点触发会产生调试异常进入调试循环。如果断点下在热路径上比如 memcpy 内部每次访问都触发一次异常再叠加多线程场景下每个线程恢复时都要重铺 DR 寄存器线程越多开销越大。解决热路径优先用软件断点硬件断点留给观察内存访问的场景。下断前先想这个地址会不会被高频访问真的有需求时用 bph 的读写类型限定避免读、写、执行三种模式全开。硬件断点总共只有 4 个用满不如用精。6. 把 x64dbg 改造成自己要的样子写一个打印 EIP 的最小插件6.1 插件入口与事件回调的两种姿势x64dbg 的插件机制非常直接编一个 DLL导出固定名字的函数主程序加载时认这些函数。最关键的入口是 pluginit它返回插件版本、SDK 版本和插件名主程序认完这个才把插件视为合法。事件回调有两种拿法一种是插件导出 plugprepl 等固定名字的函数x64dbg 在每次暂停时主动调用另一种是通过 SDK 显式注册回调关注指定类型的事件。自己写插件用第一种最省事功能够用。6.2 一个最小插件每次暂停时打印当前 EIP/RIP下面这个插件很小但把插件加载、命令注册、寄存器读取、日志输出全串起来了。它注册一条命令 logeip输入后打印当前指令指针同时每次调试暂停时自动打印一次。32 位和 64 位都能编译。#include pluginsdk/_plugins.h #include pluginsdk/bridgemain.h #ifdef _WIN64 #define CURRENT_IP regs.regs.cip #else #define CURRENT_IP regs.regs.eip #endif static bool cmdLogEip(int argc, char* argv[]) { REGDUMP regs; if (DbgGetRegDump(regs)) { dprintf([eip_logger] current ip %llX\n, CURRENT_IP); } return true; } // x64dbg 每次暂停时都会回调这个导出函数 extern C __declspec(dllexport) void plugprepl(void) { REGDUMP regs; if (DbgGetRegDump(regs)) { dprintf([eip_logger] paused at %llX\n, CURRENT_IP); } } extern C __declspec(dllexport) bool pluginit(PLUG_INITSTRUCT* initStruct) { initStruct-pluginVersion 1; initStruct-sdkVersion PLUG_SDKVERSION; strcpy(initStruct-pluginName, eip_logger); _plugin_registercommand(initStruct-pluginHandle, logeip, cmdLogEip, true); return true; }REGDUMP 是寄存器快照结构DbgGetRegDump 是 bridge 提供的读寄存器接口dprintf 是写调试器日志窗口的快捷函数。CIP 是 x64dbg 对当前指令指针的统称64 位下对应 RIP32 位下对应 EIP用宏区分两套构建。plugprepl 的签名必须保持这样导出名字错一个字插件就静默加载失败这正是第 5 章里讲的坑。PLUG_SDKVERSION 是 SDK 头文件里定义的版本宏直接赋给 initStruct 让主程序做版本兼容检查。6.3 验证插件工作的三个步骤编译时把 dll 的位数和使用的 x64dbg 壳对应好放进正确的 plugins 目录。重启调试器日志里出现 eip_logger 插件名说明加载成功。随便打开一个程序停在入口日志里应出现 paused at 那行命令行输入 logeip能看到当前指令地址。把命令输出和回调输出对照一次就能确认 gui → bridge → dbg 的数据链路是通的。最后留一个习惯建议改 x64dbg 源码之前先把“gui → bridge → dbg → Windows API”这条调用链用笔写下来再动手。我见过太多人一头扎进 gui 里找断点逻辑翻很久才发现逻辑在 dbg 工程。自己第一次改内存窗口刷新逻辑时也绕了弯路后来老老实实画链路图十分钟就定位了。这个习惯比记住任何具体接口都值钱希望帮到你。本文还有配套的精品资源点击获取