一句话总结:Cheat Engine 能告诉你「血量存在 0x1A2C4F80」,但它不会告诉你「哪条指令在减这个值」。x64dbg 就是接棒的调试器,这篇从附加进程、下三种断点讲到读调用栈回溯,把「谁访问了这个地址」这条最常用的链路走通。
上一篇讲的 CE 用法停在一个尴尬的位置:你手上有了地址,也有了稳定的指针链,但下一个问题没答案——这个值被哪里改的?谁在每次扣血时执行?想改逻辑、想找真正的偏移入口,光看内存是不够的,得看正在执行的指令。
调试器就是干这个的。x64dbg 是 Windows 上目前用得最多的免费开源调试器(GitHub 上的 x64dbg/x64dbg,GPLv3),带 x32dbg 和 x64dbg 两个版本。本篇示例仍在虚构靶场《星陨 Online》上跑,偏移继续编。没看过前两篇的话:CE 入门、ReClass 结构还原。
前置条件
- x64dbg:官网下载压缩包解压即用,不需要安装。启动的是
x96dbg.exe,它让你选 x32dbg 还是 x64dbg——选错了会附加失败,因为 32 位进程不能被 64 位调试器附加,反过来也一样 - 靶场或自己的练习程序:本篇用虚构靶场。真实在线游戏别碰,理由见反作弊那篇
- 符号无关:这类目标没有 pdb,你会看到一堆
sub_140001A20这种名字。这是正常的,靠交叉引用和调用栈来推,不靠名字 - 一个心理准备:x64dbg 的界面不算友好,第一次打开五个窗口全是数字。但真正要用的就四个
界面:只看四个窗口
打开的默认布局是这样的:
| 窗口 | 看什么 | 实际用途 |
|---|---|---|
| 反汇编(CPU) | 指令流,左边地址右边汇编 | 主战场,90% 时间在这 |
| 寄存器 | RAX/RCX… 加标志位 | 判断条件跳转走哪边 |
| 堆栈 | RSP 往上的返回地址与参数 | 回溯调用链的原料 |
| 内存转储(Dump) | 跟随某个地址看数据 | 验证「这里确实是血量」 |
Dump 窗口的用法:反汇编里选中一个 lea rax, [rbx+138],右键 → 在转储中跟随,Dump 窗口就跳过去。这一步能验证寄存器加偏移算出来的到底是不是血量。我第一次做的时候跳过去发现是一串 ASCII,说明偏移算错了一层,比盲改代码快多了。
断点:三种,各有各的适用面
断点是调试器的核心动作。x64dbg 给的三种断点不是「随便选一个」,而是对应不同问题。
软件断点(F2):把目标指令的第一个字节临时改成 0xCC(int3)。执行到就断。适用面最广,F2 在光标所在行按下即可,再按一次取消。
代价是它修改了内存:0xCC 写入的是程序代码段。有些程序会校验自己的代码校验和,一改就崩;另外它只对「真正执行到这条指令」有效,如果这段代码已经被内联或 JIT 重写,你会断在别处。
硬件断点:用 CPU 的调试寄存器(DR0–DR3),不修改目标内存。右键地址 → 断点 → 硬件断点,可选执行 / 写入 / 读取。最长处是能断在「读取」上,软件断点做不到这件事。缺点是只有 4 个槽位,用完就得清。
内存断点:给某个页加保护属性(PAGE_GUARD),任何访问都触发异常。右键 Dump 窗口的目标地址 → 断点 → 内存断点 → 访问 / 写入。粒度是页(4KB)而不是字节,所以一个 4KB 页里任何访问都会命中,容易断在无关指令上。但它对「谁改了这个值」这个问题的命中率最高。
命令行也能下,速度更快:
bp 140001A20 ; 软件断点
bph 140001A20, r ; 硬件断点,r=读 w=写 x=执行
bpm 1A2C4F80, w ; 内存断点,w=写入
bl ; 列出所有断点
bc ; 清掉全部
语法在不同 snapshot 上略有差异,用之前先看命令窗口的自动补全提示,别照抄。
图 1:上半是三种断点的原理与代价,下半是「谁访问了这个地址」的六步
「谁访问了这个地址」:完整链路
这是新手最想学的动作,也是调试器最值钱的用法。目标:找到扣血那条指令。
第一步,拿到地址。 从 CE 那边来,靶场里血量地址是 0x1A2C4F80(虚构)。
第二步,附加进程。 文件 → 附加 → 选 StarFall.exe。注意附加会让进程暂停,游戏画面会卡住——这不是崩溃。
第三步,下内存断点。 在命令窗口敲 bpm 1A2C4F80, w。只断「写入」,因为我们要找的是改血量的指令,读血量的是绘制代码,断在那边没用。
第四步,跑起来。 F9 让进程继续,然后回游戏里挨一下打。断点命中,进程暂停,反汇编窗口跳到刚执行完写操作的那条指令附近。
第五步,读现场。 这时候看三个东西:
- 当前这条指令是什么(比如
mov [rbx+138], eax) rbx是多少(寄存器窗口),rbx+138是不是等于0x1A2C4F80- 调用栈里上一层是谁
对上之后,你就找到了「扣血」这个动作的落点。往上翻几行通常能看到 sub eax, 14 之类的减法,那个 14 就是伤害值。
第六步,换软断点固化。 内存断点会反复命中,很烦。找到落点后清掉内存断点(bc),在那一行按 F2 下软件断点,然后重新跑一次验证。这样每次扣血断一次,不会因为别的东西访问同一页而被吵醒。
读调用栈,比读汇编更早出结论
按 F9 断下来之后,很多人第一反应是往下读汇编,一行行猜。效率更高的做法是先看调用栈窗口,它能直接告诉你「这个函数是谁调的」。
调用栈是这么来的:call 指令会把返回地址压进栈,函数开头再 push rbp; mov rbp, rsp 建立栈帧。调试器顺着 RBP 链和返回地址往回走,就有了栈回溯。
读的时候关注两件事:
- 栈上每层的「返回地址」对应哪条
call。双击栈帧会跳到调用点,往上翻两行常能看到「为什么调」的判断逻辑 - 参数。x64 调用约定是前四个参数走 RCX/RDX/R8/R9,后面的压栈。如果断点条件取决于某个参数,看寄存器比看栈快
有个坑要说清楚:目标如果开了优化(Release 编译)或者是 JIT 生成的代码,栈回溯经常断掉,出现一堆「返回到未知模块」。这时候要么换条件断点硬碰,要么就接受部分栈帧缺失,别在这上面死磕。
条件断点:把噪声过滤掉
一次扣血可能断十几次(护甲计算、buff 结算、UI 更新都会写同一个字段)。条件断点是省时间的关键。
在断点列表里双击某个断点,可以填条件表达式。常用写法:
; 只在扣血大于 20 时断
dword(1A2C4F80) < 80
; 只在 rbx 指向本地玩家时断
rbx == 3A4F2000
; 命中 100 次后自动删掉断点(适合循环定位)
$breakpointcounter > 100
我对条件断点的看法是:先无条件断,看清一次现场的寄存器值,再回来加条件。一上来就写复杂条件,往往因为对寄存器含义判断错误,导致断点永远不命中,然后你开始怀疑调试器坏了。
坑清单
坑一:附加之后游戏卡死,以为崩了。 附加默认暂停进程。断下来的时候游戏必然停,这不是 bug。按 F9 才继续。
坑二:32/64 位选错。 用 x64dbg 附加 32 位进程会直接报错;反过来 x32dbg 附加 64 位进程有时不报错但行为异常。任务管理器看「平台」列确认一下,两秒钟的事。
坑三:软件断点把程序打崩。 写 0xCC 到代码段,遇到自校验就完蛋。换硬件断点或内存断点绕开。
坑四:内存断点断在完全无关的指令上。 4KB 页粒度,同一页里别的变量被访问也会命中。看断点命中时的地址是不是你关心的那个,不是就继续跑。
坑五:地址每次都变。 开了 ASLR 之后每次启动基址不同。要在模块基址 + 偏移的形式下工作,而不是记绝对地址。x64dbg 里的写法是 StarFall.exe+1A2C4F80,模块名会自动解析基址。
坑六:硬件断点用完不清理。 四个槽位看着够,实际上下几个就满了而且不提示,之后下的断点静默失效。养成 bl 看列表的习惯。
边界与下一步
这篇讲的是在你自己能完全控制的目标上做动态调试。这套技能的正经用途挺多:分析崩溃转储、查第三方库到底改了什么、研究编译器优化后的代码形态、逆向自己的遗留程序。对在线游戏用同类手法的后果在反作弊那篇里说过,不重复。
下一步有两个方向可以走:一是把动态和静态接起来,用 IDA 或 Ghidra 打开同一个模块,把 x64dbg 里找到的地址对过去看伪代码,比自己读汇编快几倍;二是学写 x64dbg 脚本,把「附加→下断→跑→抓寄存器→记录」这套流程自动化,遇到需要跑几百次才能定位的问题时特别有用。
前者更通用,后者更省手。
参考资料
- x64dbg 官方仓库与文档
- x64dbg 命令参考,bp / bph / bpm 语法以对应 snapshot 版本为准
- Microsoft x64 调用约定说明,前四参数走 RCX/RDX/R8/R9
- 参考资料

发表评论
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。