简介x64dbg源码包是一套面向Windows平台的完整开源二进制调试器工程与OllyDbg属同类工具适用于逆向分析、恶意软件排查和脱壳进阶。压缩包体积不到5MB共含一千八百余个文件主体为C架构三百余个头文件与近三百个源文件承载调试器逻辑少量汇编文件覆盖x86/x64指令及SSE、MMX、FPU等扩展文档部分拥有五百余个Markdown与近百个RST文件从编译部署到插件开发均有说明另有三百余张图片、数十个Qt界面文件、工程配置和批处理脚本共同构成可视化交互与自动构建体系。目前已有58人学习下载。借助源码可完整观察反汇编引擎、断点管理、单步跟踪和插件系统等核心机制理解Windows调试器的工作原理理清各模块之间的调用关系结合构建脚本还能自行编译定制版本对深入研究调试器实现、逆向分析与脱壳实践具有直接参考价值。1. 把 x64dbg 源码装进自己的工具箱OD 用户迁移调试器的一条低门槛路做过脱壳的人手里基本都有一版 OllyDbg但真正拿它调试 x64 程序时才知道有多难受解析不全、断点不稳、反调试一绕就崩。x64dbg 源码这个开源调试器正好是这类需求里最稳的替代路线——它把调试器界面、调试内核和插件 SDK 全部开放既能当作 OD 的平替日常使用也能改源码、写插件做成自己的逆向调试工具。这份资源适合两类人一类是从 OD 迁移过来、需要 x64 调试能力的逆向从业者另一类是正在做课程设计或工具开发、想把调试循环和命令系统改成自己版本的工程师。下面从源码结构、编译、核心机制到踩坑点按能复现的顺序拆完。2. 为什么选 x64dbg与 OD 的差异、源码结构、Qt 与调试核心的边界2.1 为什么放弃 OllyDbg 转向 x64dbgx64 支持、反调试与二次开发自由度先给结论OllyDbg 在设计初期就没把 x64 当作主线即使后来出了 2.xx64 模块的断点、反调试绕过的能力也明显不如 x86 那边成熟而 x64dbg 从架构上就是 x64 优先x86 只是兼容。对脱壳场景x64dbg 还内置了一套反调试对抗的插件生态常用做法是把 ScyllaHide 这类插件挂进去处理 IsDebuggerPresent 和 NtQueryInformationProcess 检测这在 OD 上要折腾很久。再看二次开发自由度。OD 的插件接口是公开的但源码一直没有完整开源想改断点触发器、改命令解析逻辑只能停留在插件层面x64dbg 则从 GUI 到调试核心全部开放命令系统、表达式求值器、断点管理器、脚本引擎都在源码里。换句话说OD 能做的事它基本都能做OD 做不到的源码级修改它也能做。下面是 OD 与 x64dbg 的对比选型时可以直接套用对比项OllyDbgx64dbgx64 调试支持弱常有误判x64 优先x86 兼容源码未完整开源完全开源插件 SDK有但文档散有 pluginsdk接口稳定反调试对抗需额外插件且易崩插件生态成熟表达式简单算术支持寄存器、模块、引号内符号、运算这一段的重点是如果你只做 x86 小样本OD 还能用一旦要调试 x64 程序、要处理反调试、要改调试器行为x64dbg 源码是迁移成本最低的起点。2.2 源码目录拆解断点、表达式、插件代码分别在哪拿到源码包之后第一步不是急着堆编译环境而是先看一眼目录知道改什么去哪儿。x64dbg 源码的顶层结构非常清晰常见布局是 src 下一等分的核心模块目录作用你会动它的场景src/dbg调试内核断点、表达式、命令、寄存器状态都在这加命令、改断点逻辑、调反调试src/guiQt 界面反汇编视图、寄存器视图、栈视图改显示、加面板、调快捷键src/bridgeGUI 与内核之间的桥接层数据结构定义给两者之间传新数据src/pluginsdk插件开发用的头文件与接口定义写插件、编译第三方插件src/plugins官方示例插件脚本、命令、菜单的模板抄代码、复制成自己的插件如果沿着一条命令走比如命令行里敲 bp kernel32.VirtualAlloc它会先被 GUI 层接收走 bridge 传给 src/dbg 的命令处理器命令处理器调用断点管理接口把 VirtualAlloc 的地址转换成实际地址再在目标进程里写 0xCC。整个过程里表达式求值器负责把kernel32.VirtualAlloc这种符号字符串解析成数值断点管理器负责记录断点列表和命中行为二者都在 src/dbg 下。所以当你想给调试器加一条新命令核心改的是 src/dbg 里的命令表而不是 GUI。2.3 Qt 界面与调试内核的边界改控件不动核心的写法这是新手最容易犯的错想给反汇编窗口加一个右键菜单直接在 GUI 里找到DisassemblyView::contextMenuEvent就开始写结果发现拿不到当前调试状态。原因是 GUI 和调试内核跑在不同线程GUI 只能通过 bridge 里的接口发请求不能直接读写调试器的全局状态。我一般会遵循一个边界原则凡是展示层的事只改 src/gui凡是行为层的事只改 src/dbg两者之间要传新数据先在 src/bridge 里加结构体定义再在两边各自实现收发。这样做的好处是编译时不会出现「GUI 改了一行调试内核整条链接失败」的情况。对脱壳这种重操作场景大部分实际需求都用命令和插件解决GUI 最好保持原样等核心逻辑稳定了再去美化界面。这个边界意识在后面的避坑部分还会再碰到一次因为它就是二次开发里最常见的崩溃来源。3. 把源码跑起来编译环境、子模块与 CMake 构建参数3.1 前置环境VS、Qt 与 Git 的版本搭配编译 x64dbg 需要四样东西Visual Studio、Qt、Git、CMake。从实际编译经验来看最省事的是 VS2019 或 VS2022 搭配 Qt 5.15.x 的 msvc 预编译套件。这里强调一点Qt 的下载包有很多编译器变体必须选和 VS 一致的比如 VS2022 就用 msvc2019_64 或 msvc2022_64千万不要用 MinGW 变体否则后面 CMake 阶段就会因为编译器不匹配翻车。组件推荐版本作用Visual Studio2019 / 2022装 C 桌面开发编译器和链接器Qt5.15.x选择 msvc 编译器套件GUI 界面运行库CMake3.16 以上工程生成和构建Git任意较新版本拉取源码和子模块之所以特别强调 Qt 5 而不是 Qt 6是因为源码里很多控件写法是按 Qt 5 的行为写的Qt 6 不是不能用但会出现一些兼容性改动要额外花时间修。对一份要复现的资源来说稳定比新版本号重要。还有一个细节安装 VS 时记得勾选「使用 C 的桌面开发」工作负载只装一个 VS 本体没有 C 工具链cmake 阶段会直接失败。3.2 克隆与子模块更新三行命令带出整个依赖x64dbg 源码不是一个单仓库它把反汇编引擎、解压库、调试帮助库拆成了子模块所以克隆命令必须带 --recursive否则拉下来的源码是缺件儿的。下面是常见做法git clone --recursive https://github.com/x64dbg/x64dbg.git x64dbg_src cd x64dbg_src git submodule update --init --recursive第一条命令把主仓库和所有子模块一次性拉下来第三条命令用于补全如果中途网络断了导致某个子模块目录为空执行它可以把缺的子模块重新初始化。注意--recursive是递归拉取不只会把 BeaEngine、capstone 这些顶层依赖拉下来还会继续拉它们自己的依赖。如果你的源码包已经先把仓库打包好了第一步可以跳过但要确认子模块目录里面有实际文件而不是一个写着 commit 哈希的指针文件。怎么看进入子模块目录看有没有 CMakeLists.txt、源文件这些内容如果只有空目录结构说明子模块没有初始化还是要执行第三条命令。3.3 CMake 构建与工程生成参数对照源码根目录有 CMakeLists.txt构建流程是先 cmake 生成 Visual Studio 工程再编译。我常用的命令如下mkdir build cd build cmake .. -G Visual Studio 17 2022 -A x64 -DCMAKE_PREFIX_PATHC:/Qt/5.15.2/msvc2019_64 cmake --build . --config Release --parallel 4第二行里-G指定生成器VS2022 对应的是 Visual Studio 17 2022VS2019 对应 Visual Studio 16 2019-A x64指生成 64 位工程这也是 x64dbg 主程序所在的目标平台CMAKE_PREFIX_PATH指向 Qt 安装目录下具体的一套编译器套件目录CMake 会从那里找 Qt5Widgets、Qt5Core 等组件。第三行用--config Release编译 Release 版本--parallel 4表示用 4 个进程并行编译机器核多可以改成 8。如果 cmake 阶段报找不到 Qt优先检查CMAKE_PREFIX_PATH是否写到了msvc2019_64这一级而不是 Qt 根目录。写到根目录的话CMake 能找到 Qt 但可能匹配到别的编译器套件编译时依然会错。这个参数是我每次重建环境都会反复确认的一行写错它浪费的时间比写代码还多。3.4 首次运行前的目录检查编译结束后默认产物在 build/bin/x64 下。在运行之前花一分钟确认几个文件x64dbg.exe、x64dbg.dll、Qt5Core.dll、Qt5Widgets.dll。前两个是编译产物后两个如果不在输出目录里就把 Qt 安装目录下的 bin 目录加进系统 PATH或者直接复制过去。这一步不做启动时会出现找不到 Qt 动态库的报错第一次跑的人经常以为是自己编译失败了其实是运行库没到齐。还要检查有没有 plugins 目录没有就手动建一个。x64dbg 会把插件放在与主程序同一级的一个目录里后面写插件时要放进去。确认完这几点双击 x64dbg.exe能打开并附加一个测试进程就算编译链路通了。顺便说一句第一次启动会问你配置文件的保存位置保持默认即可这个配置目录后面调试插件和脚本文件时还会用到。4. 核心机制解读断点、表达式、命令系统与插件接口4.1 命令系统与表达式求值器命令行不只是「调试输入框」x64dbg 的命令行承担了 OD 命令框的职责但它更接近一个完整的表达式解析器。你在命令行里敲dump eip它会先把eip解析成当前指令地址再打开 dump 窗口敲bp kernel32.VirtualAlloc它会先把kernel32.VirtualAlloc解析成模块导出表里的实际地址再在对应位置下断。这个解析链路是表达式求值器做的它支持寄存器名、模块名、引号包起来的符号、十六进制常量以及运算符组合。实际用的时候几个最频繁的表达式长这样表达式写法含义eip / rip当前指令地址[eip]从当前指令地址读取一个指针值kernel32.VirtualAlloc解析模块导出符号为地址MyModule.dll0x1234模块基址加偏移由于表达式求值器在源码里是独立模块你在二次开发时可以直接复用它自己写一个插件命令接收用户输入后交给表达式解析得到数值再传给断点或内存接口。这样一来你的插件里不需要自己实现一套符号解析省掉了很多重复劳动。这也是 x64dbg 二次开发比 OD 插件省力的关键点之一。4.2 断点管理实现要点软件断点、硬件断点与日志断点断点是脱壳时的主战场。x64dbg 的断点管理分三类对应不同场景软件断点把内存里的指令字节替换成 0xCCINT3程序执行到该地址时触发中断。优点是数量不限缺点是被校验代码检测到修改所以脱壳时常配合 ScyllaHide 绕检测。硬件断点使用 CPU 调试寄存器 DR0-DR3 实现不修改指令字节能避开代码校验但最多同时下 4 个。内存断点基于内存页的访问权限在数据被读写的时候触发适合观察内存访问但性能开销大不适合频繁访问的区域。在 GUI 里右键断点列表可以切换类型也可以给断点挂条件。条件表达式用的还是刚才那个求值器比如想只在某个 API 的返回值为 0 时中断就在断点编辑框里写一条类似eax0的表达式命中时求值器先算一次不成立就继续跑。这对还原壳的调用流程非常有用先下 API 断点再用条件过滤掉中间大量无意义的调用。从源码角度看断点记录的布局、命中回调、条件判断都在 src/dbg 的断点管理模块里插件可以通过 SDK 提供的断点接口读取断点列表和状态。这一层吃透之后写自动化脚本时就清楚该在哪里插入自己的逻辑。4.3 插件接口plug_init 到菜单挂接的最短路径插件的入口不是常用的 WinMain 或 DllMain而是几个固定导出的函数其中最重要是 plug_init。它会在插件加载后被调试器调用框架代码在 src/pluginsdk 里定义。你在插件里要做的第一件事是从 initStruct 拿到插件句柄然后用它注册命令或菜单项。流程如下加载插件 → 调用 plug_init → 插件注册命令、菜单、表达式函数 → 调试器进入正常工作卸载或退出时调用 plug_stop释放资源。需要注意插件接口是 C 风格的导出函数即使你写 C 也要用 PLUG_EXPORT 声明成 extern C否则名字修饰会导致加载器认不出入口。这个点很多第一次写 x64dbg 插件的人都会翻车编译出来看着是 dll加载器却说你没有导出 plug_init。4.4 一个最小插件骨架注册自定义命令直接给一个能通过编译的最小骨架我通常拿它当所有插件的起点#include pluginsdk/plugin.h #include pluginsdk/_plugins.h static int hPlugin; static bool cbMySample(int argc, char* argv[]) { dprintf([mysample] command called, argc%d, argv[0]%s\n, argc, argv[0]); return true; } PLUG_EXPORT bool plug_init(PLUG_INITSTRUCT* initStruct) { hPlugin initStruct-pluginHandle; _plugin_registercommand(hPlugin, mysample, cbMySample, true); return true; } PLUG_EXPORT bool plug_stop() { _plugin_unregistercommand(hPlugin, mysample); return true; }逻辑说明plug_init 优先执行取出插件句柄_plugin_registercommand把mysample注册成调试器命令行里可用的命令用户在命令行敲mysample回调 cbMySample 被触发dprintf 把参数打印到日志窗口plug_stop 里反注册命令避免退出时留下悬空回调。参数说明_plugin_registercommand的第三个参数是命令回调的函数指针第四个参数 true 表示这个命令只在调试暂停状态可用。如果你的命令需要在程序运行时就生效比如自动附加和设置硬件断点要把它改成 false否则运行状态下敲命令会被调试器拒绝。这个细节在写自动化插件时特别容易忽略。5. 避坑指南编译、运行与二次开发的五个常见问题这一章是血泪经验每一条都是踩过之后才记进 check list 的。按「现象 → 原因 → 解决」写遇到可以直接对照。5.1 编译期子模块目录为空现象git clone 完成目录里 BeaEngine、capstone 对应的子模块文件夹是空的编译时爆一堆找不到头文件。原因克隆时没带--recursive或者拉取过程中子模块初始化失败主仓库下来了嵌套的依赖没下来。解决回到源码根目录执行git submodule update --init --recursive等子模块全部拉完再继续编译。如果子模块更新反复中断换一个更稳定的时间段重试不要跳过它直接 build后面反汇编引擎缺失会让整个编译失败。验证方法也很简单进到子模块目录里能看到源文件和 CMakeLists.txt 就说明有内容如果只有一个空壳目录那还是没拉全。注意判断子模块是否完整不要只看目录存在要看里面有实际源码文件。常见做法是看文件数和 CMakeLists.txt 是否都在。5.2 Qt 配置错位编译期找不到库、运行期缺 DLL现象一cmake 阶段报错Could not find a package configuration file ... Qt5WidgetsConfig.cmake或者编译头文件时报cannot open QtCore/qglobal.h。原因CMAKE_PREFIX_PATH写到了 Qt 根目录或 Qt 套件和 VS 版本不匹配CMake 找到了 Qt 但选错了编译器套件。现象二编译成功双击 x64dbg.exe 报错缺少 Qt5Core.dll 或 Qt5Widgets.dll。原因编译产物目录只放了工程自身的输出Qt 运行库没有被自动复制进来PATH 里也没有 Qt 的 bin 目录。解决两件事一起做。第一把CMAKE_PREFIX_PATH精确到具体的 msvc 套件目录比如C:/Qt/5.15.2/msvc2019_64第二把 Qt 的 bin 目录里Qt5Core.dll、Qt5Widgets.dll、Qt5Gui.dll复制到产物目录或者直接加进 PATH。我一般会写一个小批处理每次编译完自动复制set QTDIRC:\Qt\5.15.2\msvc2019_64 xcopy /d /y %QTDIR%\bin\Qt5*.dll build\bin\x64\这个批处理只复制比目标新的 DLL不会每次全量拷贝。环境里同时装多个 Qt 版本时CMake 可能优先捡到旧版本所以最好在 cmake 命令里显式指定路径别把 root 层级整个塞进 PATH。5.3 运行期反调试对抗不足导致目标崩溃现象附加一个带反调试的程序x64dbg 刚附加成功就被目标检测到目标主动崩溃或退出。原因目标程序通过IsDebuggerPresent、NtQueryInformationProcess或时间差检测调试器存在x64dbg 默认没有对这些做隐藏。解决在插件目录放好 x64dbg 配套的反调试插件常见做法是 ScyllaHide并在插件设置里勾选目标可能用到的检测项。实际脱壳时我一般先把反调试绕过打开再附加进程不然下完断点也白下。如果目标还有自校验还需要配合硬件断点而不是软件断点因为软件断点改字节容易被完整性检查发现。这个思路在 OD 时代就有效在 x64dbg 里只是实现得更顺。5.4 二次开发插件加载失败与改内核失控现象一自己按 SDK 模板编译好的插件copy 到插件目录后列表里看不到或者加载成功后一敲命令就崩溃。原因插件放错目录、位数与主程序不一致或者入口函数没有按 C 风格导出导致加载器虽然把 DLL 载入了但找不到 plug_init。解决确认扩展名是 .dll且放到了 x64dbg.exe 的指定插件目录下编译 x64dbg 对应位数的插件别用 32 位配置生成 64 位插件检查导出函数是否是PLUG_EXPORT bool plug_init(...)的原型用 dumpbin 看一下导出表dumpbin /exports myplugin.dll如果导出表里没有 plug_init说明名字修饰问题没处理好加extern C重新编译。还有一种常见坑是把插件调试代码放在 plug_init 里跑大量 GUI 操作干扰了加载流程至少在 init 阶段先只做注册其他逻辑放到命令回调里。现象二改了 src/dbg 里的断点处理逻辑调试目标时偶发 Access Violation。原因调试内核在子线程里处理事件你在界面线程直接读写了内核的数据结构没有走 bridge。解决回到 2.3 节的边界原则把内核改动限制在事件下发链路里需要与 GUI 通信就通过 bridge 消息。如果你只是想要「断点命中后多打印一条记录」优先用插件 SDK 在命令回调里做别直接改内核这是性价比最高的方案。改内核之前先想清楚一个问题这个功能是否只能在内核层实现大多数时候答案都是否定的。6. 进阶技巧把脱壳断点做成一条命令脱壳调试最烦的其实不是某个断点不会下而是每次都要重复敲一串 API 断点漏掉一个就得从头再来。x64dbg 支持把命令行操作保存成脚本文件也可以按 4.4 节的方式注册成自定义命令。下面是一个典型的脱壳断点组合放在脚本文件里bp kernel32.VirtualProtect bp kernel32.VirtualAlloc bp kernel32.LoadLibraryW bp kernel32.GetProcAddress run在脚本窗口里加载执行程序会依次对这几个 API 下断然后跑起来。VirtualProtect/VirtualAlloc 是壳申请和修改内存权限时几乎必调的LoadLibraryW/GetProcAddress 是导入表还原时绕不开的路口。断点命中后先看日志窗口里的调用栈判断是壳的 API 还是程序自己的调用再用条件断点过滤高频调用。如果你已经在做自己的调试器插件更好的做法是把这套断点组合注册成一条命令在回调里用调试器 SDK 提供的执行入口逐条下发命令名称就叫unpack。从那以后我每次编译完新版本都会强制跑一遍这个脚本验证断点和表达式求值器没有被改崩省掉了大量黑匣子式的排错时间。社区里基于这套命令和脚本接口衍生出的自动化玩法比如把 x64dbg 接进大模型工具链的 MCP 方案底层依赖的也还是这份源码里最基础的命令和插件能力。这算是我从 x64dbg 源码上收获最大的一条习惯先用脚本把坐标固定下来再动手改核心逻辑希望帮到你。本文还有配套的精品资源点击获取