当前位置:首页 > 逆向工程 > 正文

x64dbg 入门:断点、调用栈,以及「谁在改这个地址」

约 5356 字 · 预计阅读 14 分钟
一句话总结:Cheat Engine 能告诉你「血量存在 0x1A2C4F80」,但它不会告诉你「哪条指令在减这个值」。x64dbg 就是接棒的调试器,这篇从附加进程、下三种断点讲到读调用栈回溯,把「谁访问了这个地址」这条最常用的链路走通。

上一篇讲的 CE 用法停在一个尴尬的位置:你手上有了地址,也有了稳定的指针链,但下一个问题没答案——这个值被哪里改的?谁在每次扣血时执行?想改逻辑、想找真正的偏移入口,光看内存是不够的,得看正在执行的指令。

调试器就是干这个的。x64dbg 是 Windows 上目前用得最多的免费开源调试器(GitHub 上的 x64dbg/x64dbg,GPLv3),带 x32dbg 和 x64dbg 两个版本。本篇示例仍在虚构靶场《星陨 Online》上跑,偏移继续编。没看过前两篇的话:CE 入门、ReClass 结构还原。

前置条件

界面:只看四个窗口

打开的默认布局是这样的:

窗口 看什么 实际用途
反汇编(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 链和返回地址往回走,就有了栈回溯。

读的时候关注两件事:

  1. 栈上每层的「返回地址」对应哪条 call。双击栈帧会跳到调用点,往上翻两行常能看到「为什么调」的判断逻辑
  2. 参数。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 脚本,把「附加→下断→跑→抓寄存器→记录」这套流程自动化,遇到需要跑几百次才能定位的问题时特别有用。

前者更通用,后者更省手。

参考资料

目录导航
  • 前置条件
  • 界面:只看四个窗口
  • 断点:三种,各有各的适用面
  • 「谁访问了这个地址」:完整链路
  • 读调用栈,比读汇编更早出结论
  • 条件断点:把噪声过滤掉
  • 坑清单
  • 边界与下一步
    • 参考资料
  • 相关文章:

  • 反调试手段识别:调试点运行就退出,到底是谁在拦你2026-10-26 09:09:39
  • 用 ReClass 还原游戏结构体:从内存碎片到可直接用的偏移表2026-10-26 09:09:39
  • Cheat Engine 入门实战:从扫描到指针链,自己动手定位游戏数据2026-10-26 09:09:39
  • 发表评论

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