国王是AI?程序员的都市传说(上)|游戏逆向笔记001

Roy*l M**代码逻辑逆向分析

Posted by lxraa on November 30, 2025 · 6 mins read

📖 本文属于系列:游戏逆向笔记

查看系列目录 | 关于我

【游戏逆向笔记】 是 lxraa 的技术分享系列,记录”我”习得老爹的黑客技艺后,在游戏逆向领域的探索笔记,基于真实游戏逆向分析过程的半虚构日记式写作。
(友情提示)读完之后,你可能一时半会儿都不太想单纯「打游戏」了。


一、老张的都市传说

认识老张是在一年前的一个下午。

那天我出门溜达,路过一家咖啡馆,门口挂着横幅:”游戏AI的未来——技术沙龙”。

我本来对这种活动没兴趣,但看到门口桌子上摆着免费的咖啡和点心,就走了进去。

会场里坐了三四十人,台上有个西装革履的嘉宾正在讲PPT,满屏都是”深度学习”、”强化学习”、”神经网络”这种听起来很唬人的词。

台下一片昏昏欲睡。

我端了杯咖啡,拿了两块曲奇饼,打算找个角落坐着混一会儿就走。

咖啡馆技术沙龙
咖啡馆里的技术沙龙

结果刚坐下,旁边一个戴黑框眼镜的中年程序员突然凑过来,压低声音说:

“讲得都是屁话。真正的AI不在PPT里,在Roy*l M**的国王手里。”

我愣了一下,以为他在跟别人说话,没搭理,继续吃饼干。

结果他转过头盯着我:”你玩Roy*l M**吗?”

“玩啊,卡在64关三天了。”我随口回了一句。

他眼睛一亮,像是找到了知音:”那你有没有发现,螺旋桨棋子(propeller)很聪明?”

“还行吧,确实挺智能的。”我敷衍道,心想这人怎么回事。

“不是’还行’,是’太聪明了’。”他压低声音,神神秘秘地说,”我怀疑那个国王是真的AI。”

我笑了:”大哥,这就是个三消游戏,能有多智能?”

“你不懂。”他摇摇头,从口袋里掏出一张名片塞给我,”我叫张伟,做了十年游戏。Roy*l M**的螺旋桨棋子,不是规则引擎能做出来的效果。”

我看了眼名片:xx游戏,资深客户端开发工程师。

他说完又凑近了点,滔滔不绝地讲起他对Roy*l M**螺旋桨棋子的各种观察和猜测。

我本来想找个借口溜走,但他说得太投入了,根本不给我插话的机会。

就这样,我端着咖啡,硬生生听了他半个小时的”都市传说”。

散场的时候,他还意犹未尽:”加个微信吧,我还有很多东西想跟你讨论。”

我心想,赶紧应付完走人。

就扫了他的码,也没多说什么。


加完之后,两个月没联系过。

直到某天,我在一个游戏技术论坛上刷帖,看到一个题目很唬人的帖子。

楼主 ID 叫”老张不老”,头像一看,就是那天那个程序员。

他发了个很长的分析贴,从玩家角度推测某个热门手游用了什么”黑科技”,写得有理有据,下面一堆人点赞。

我看完笑了,直接回帖:用逆向分析验证了一遍,你想多了,就是普通的实现。

还把关键代码截图贴了上去。

老张当时估计挺尴尬的,但他没删帖,反而私信我:”你会逆向?”

“会一点。”

“能不能帮我看看别的?”

我想了想,反正闲着也是闲着,就答应了。

一来二去,我们就熟了。

老张这人,技术上确实有两把刷子,但有个毛病:特别喜欢脑补,总觉得游戏公司在背后搞什么骚操作。

不过他在游戏公司工作,经常能接触到一些很有意思的场景和案例。

我对这些挺感兴趣的——毕竟家里蹲能接触到的素材有限,能从一线开发者那里听到实际遇到的问题,还能自己动手验证,这对我来说挺有吸引力的。

所以每次他提出什么新的”推测”,我就帮他用逆向分析验证一下。

大多数时候,结果都是:”想多了。”

他也不生气,下次遇到新的”疑点”,还是会来找我。

直到一个月前。

任务名称:Roy*l M** 螺旋桨棋子算法验证
起因:老张的都市传说
目标:证明国王不是AI
预计耗时:两周

二、凌晨两点的电话

某三消游戏截图

图:某三消游戏

那天晚上我正吃着泡面,电脑上放着视频。

微信突然弹出来,老张发了条语音:

“在吗?”

我看了眼时间,凌晨2:17。

“在,怎么了?”我回了条消息。

十秒后,语音电话打过来。

“我验证了。”他的声音有点兴奋,”Roy*l M**的国王,真的是AI。”

“大哥,你半夜不睡觉研究这个?”我把手机夹在肩膀上,左手按着键盘快进视频,右手扒拉着泡面。

“你听我说完。”他语速很快,”我Hook了螺旋桨棋子的目标选择函数,跟踪了它的决策过程。你知道我发现了什么吗?”

“发现它很聪明?”

“不止!”他声音提高了八度,”它的决策路径,不是简单的’找分数最高的位置’,它会……它会计算后续的连锁反应!”

“等等等等。”我放下手机,切换到免提,”老张,我现在给你上一课。社会主义核心价值观第一条——唯物主义。”

电话那头沉默了两秒。

“什么意思?”

“意思是,”我清了清嗓子,切换到”教育模式”,”游戏公司不是搞科研的,他们是做生意的。你知道做一个真正的AI要多少钱吗?”

“可是……”

“没有可是。”我继续说,”Roy*l M**是个三消游戏,不是自动驾驶,不是医疗诊断,更不是什么AlphaGo。它需要的智能,用规则引擎加权重表就够了。”

“规则引擎做不到这个效果!”

“做得到。”我很肯定,”给每个棋子类型打个分,冰块80分,木箱50分,目标棋子10000分,然后遍历棋盘,找分数最高的位置,螺旋桨棋子飞过去。就这么简单。”

“但是它……”

“而且啊,老张。”我打断他,”你做了十年游戏,难道不知道AI这个词有多值钱吗?真要是用了什么黑科技,游戏公司早就拿出来吹了。你看Roy*l M**的宣传里提过’AI’吗?”

电话那头又沉默了。

我以为他被说服了,正准备挂电话,他突然问:

“那你解释一下,为什么它能找到那种’特别刁钻’的路径?”

我手指停在屏幕上。

“什么路径?”

“就是那种……“他顿了顿,像是在组织语言,”你知道吗,有时候螺旋桨棋子会选一个看起来’很蠢’的目标,但消除之后,棋子下落,触发连锁,最后反而是最优解。”

“那可能只是你运气好吧。”

“不是运气。”他的声音很平静,”我专门观察过好几局,每次它都能找到那种’不直观’的最优路径。你自己玩的时候有没有这种感觉?明明觉得应该打A,结果它打了B,然后你发现B确实更好。”

我沉默了两秒。

还真有。

上次玩64关,螺旋桨棋子飞向了一个角落里的普通方块,我当时还以为是bug,结果那一下触发了连锁反应,直接通关了。

“那可能是……“我想找个解释,但话卡在喉咙里。

“你知道要实现这个,需要什么吗?”老张继续说,”需要模拟消除后的棋盘状态,需要对每个可能的未来状态打分,需要算法来筛选最优路径。这不是简单的if-else,这是……”

“搜索算法。”我替他说完,”预判,评估,剪枝。”

“对。”他说,”所以我说它是AI。不是深度学习那种新AI,是……”

“经典AI。”我说,”下棋那一代的技术。”

电话两端都沉默了。

半晌,我说:”你别发癫了,睡觉去吧。”

“你不验证一下?”

“验证什么?”我嘴硬,”就算它用了搜索算法,也只能证明程序员写得好,不能证明’国王是AI’。”

“那你验证完了告诉我,”电话那头传来键盘敲击声,”到底是国王聪明,还是程序员聪明。”

我没回话,直接挂了电话。

但盯着64关的界面,我心里确实有点不踏实。

老张是神神叨叨,但他说的那个现象,我确实见过——

螺旋桨棋子有时候选择的目标,真的很”反直觉”。 但事后看,又确实是最优解。

这要是真的有搜索算法……

那成本也太高了吧?

一个三消游戏,至于吗?

除非……它真的需要这么”智能”?

三、老爹的硬盘

第二天晚上,我还是没忍住,从柜子深处翻出了老爹的那块旧硬盘。

3.5英寸机械硬盘,IDE接口,外壳生了锈,标签都磨得看不清了。我从阁楼那台老爷机上拆下来的,当时还费了半天劲焊转接头。

接上电脑,硬盘转动的声音有点响,但还能读。我打开那个叫notes/unity-reverse/的文件夹,翻到一个很早以前的文档:

## Unity IL2CPP逆向基础

Unity5之后引入IL2CPP,把C#代码先编译成C++,再编译成机器码。
对逆向来说,难度提高了十倍。

核心机制:
1. global-metadata.dat 包含完整的类型系统
   - 命名空间、类名、方法签名、字段定义
   - 这部分可以完整还原

2. libil2cpp.so 是最终的二进制代码
   - 所有函数体都变成了ARM/x86汇编
   - 没有符号,只有地址

3. 两者配合才能分析
   - metadata告诉你"有什么"
   - so文件告诉你"怎么做"

基础工具:
- Il2CppDumper:自动提取类型信息
- IDA Pro:静态分析so文件
- 需要把两者关联起来

难点:
函数体完全丢失,必须从汇编逆推C#逻辑。
一个300行的函数,手动分析要3-5天。
一个复杂系统,少说要一个月。

——某个通宵写下的笔记

我盯着最后那句话。

“一个复杂系统,少说要一个月。”

老爹的笔记讲的是Unity IL2CPP的基础知识。

Roy*l M**用的也是IL2CPP。老张说螺旋桨棋子”很聪明”,那背后一定有一套复杂的决策系统。

如果真像老张说的那样,有”连锁预判”、”状态模拟”,那代码复杂度……

按老爹笔记里说的,一个月可能都不够。

但我还是想试试。

我打开微信,给老张发了条消息:

“发你的测试数据过来。”

他秒回:”你要验证了?”

“嗯。”我敲字,”但先说好,我是为了证明你错了。”

“好。”他秒回,”我等你结果。”

五分钟后,他发来一个压缩包,里面是他Hook的日志文件,还有一个简短的说明文档。

我解压,打开日志,开始看。

然后我愣住了。

四、踩点:Unity IL2CPP的迷宫

老张的日志文件很详细。

他Hook了一个叫ExplodeTargetFinder.FindForExploder()的函数,记录了每次螺旋桨棋子选择目标时的完整调用栈。

老张的完整调试日志

日志节选:

============================================================
[调用栈] ExplodeTargetFinder.FindForExploder()
  └─ ExplodeTargetFinder.FillScores()

  [高分格子Top5]:
    位置[4:0]: 456976分
    位置[5:0]: 456976分
    位置[6:0]: 456976分
    位置[7:0]: 456976分
    位置[8:0]: 456976分

  └─ ExplodeTargetFinder.FindSingleTarget()
  └─ [重定向] 目标已重定向
[FindForExploder] 目标查找完成
============================================================

我看到一连串的函数名:

ExplodeTargetFinder.FindSingleTarget()
ExplodeTargetFinder.FillScores()
ExplodeTargetMediator.CalculateScore()
CellModel.GetExplodeScore()
StaticItemModel.GetExplodeScore()
ItemModel.GetExplodeScore()
...

还有更多。

光是计算一个位置的分数,就调用了十几个类。

但这还不是最诡异的。

最诡异的是,老张在日志里标记了几个”反直觉决策”的案例:

案例一:底部收集关卡

  • 棋盘顶部有一个三层冰块,分数:2400
  • 棋盘底部有一个普通木箱,分数:50
  • 螺旋桨棋子选择:底部的木箱
  • 原因:底部收集关卡,越靠下权重越高

案例二:收集青蛙关卡

  • 位置A:三层障碍物,基础分数:850
  • 位置B:一只青蛙,基础分数:200
  • 螺旋桨棋子选择:青蛙
  • 原因:青蛙是关卡目标,有4.4倍权重加成,实际分数:880+

案例三:重定向机制

  • 位置C:高分障碍物,分数1500+
  • 位置D:土壤最后一层,分数300
  • 螺旋桨棋子选择:土壤
  • 原因:策略级重定向,通关条件优先

案例四:实际Hook日志

从老张的日志中可以看到,一次螺旋桨棋子触发会计算整个棋盘的分数,并选择最优位置。许多格子的分数都是456976分(青蛙权重),但最终的选择还会经过重定向机制调整。

我盯着这些案例。这不是简单的分数排序。

我关掉日志,开始标准流程。


标准流程:

# 解包
java -jar apktool.jar d royxxx_mxxxx.apk -o apk/

# IL2CPP元数据提取
./Il2CppDumper.exe `
  apk\lib\arm64-v8a\libil2cpp.so `
  apk\assets\bin\Data\Managed\Metadata\global-metadata.dat

29秒后,工具吐出一堆文件:

  • dump.cs:所有类定义
  • script.json:IDA脚本元数据
  • DummyDll/:C# DLL骨架

il2cppdumper导出文件

图:Il2CppDumper导出的文件

我用ILSpyCMD反编译Assembly-CSharp.dll

ilspycmd -p -o re-code/ DummyDll/Assembly-CSharp.dll

三分钟后,re-code/目录里出现了完整的C#工程。

我打开编辑器,搜索”ExplodeTargetFinder”。

找到了。

一个1200多行的类,几十个方法,函数体全是空的——IL2CPP不保留实现。

但这就够了。

类定义、方法签名、字段类型,这些信息足够我定位到IDA里的对应汇编代码。

ExplodeTargetFinder类定义

图:ExplodeTargetFinder类的反编译代码


我打开IDA Pro,拖进libil2cpp.so

107MB的二进制文件,IDA开始分析。进度条缓慢爬升,我去泡了杯咖啡。

20分钟后,分析完成。

我点击菜单:

File → Script File → ida_with_struct_py3.py

对话框弹出,我选择:

  • 头文件:il2cpp.h
  • 元数据:script.json

点击确定。

IDA开始导入符号。状态栏显示:”Processing symbols…”

这一步需要一整天。

我抬头看了眼时钟,凌晨1点。


第二天晚上,我回到电脑前。

IDA状态栏显示:”Ready”。

我按Shift+F12打开字符串窗口,搜索”ExplodeTargetFinder”。找到了——一个函数名。

双击,跳转。

ARM64汇编代码铺满屏幕:

ExplodeTargetFinder__FindSingleTarget:
    SUB     SP, SP, #0x80
    STP     X29, X30, [SP,#0x70]
    ADD     X29, SP, #0x70
    STR     X0, [SP,#0x68]
    STR     X1, [SP,#0x60]
    ...

300多行汇编。

IDA Pro分析界面

图:IDA Pro 中导入元数据脚本的界面

我往下滚动,又看了几个方法:

FindSingleTarget() - 300多行汇编 FillScores() - 200多行汇编 CalculateScore() - 400多行汇编 TryRedirect() - 150多行汇编 …

还有数十个棋子类,每个都有自己的GetExplodeScore()方法。

还有十几个Helper类,每个都有复杂的影响因子计算逻辑。

我粗略估算了一下:

如果要完整还原螺旋桨棋子的决策逻辑,至少需要还原:

  • 1个核心类(1200行)
  • 数十个棋子类(每个几百行)
  • 十几个Helper类(每个几百行)

总共接近2万行C#代码。

每一行都需要从ARM64汇编逆推。

我盯着屏幕,心里开始打退堂鼓。

按老爹笔记里说的,手动逆向一个300行的函数,需要3-5天。

2万行代码,就算我每天8小时全职投入,也需要1年

而且这还不是简单的代码翻译。

螺旋桨棋子的权重计算涉及大量条件判断、配置读取、动态调用,汇编代码复杂度是普通函数的5-10倍。

更要命的是,老张说他要验证的是”反直觉决策”——这意味着我不能只还原代码结构,还必须保证逻辑精度

一个权重系数搞错了,整个分析就前功尽弃。

我揉了揉太阳穴。这活儿根本不可能完成。

我关掉IDA,打开浏览器,搜索”Unity IL2CPP 自动化逆向工具”。

出来的要么是收费商业工具,要么是学术界的研究项目,精度都达不到我的要求。

我靠在椅子上,盯着天花板发呆。

五、破门:AI辅助分析

这时,老爹端着茶杯路过。

他扫了眼我的屏幕,看到IDA里的汇编代码,还有我浏览器里搜索的关键词。

停顿了两秒,丢下三个字:

“试试AI。”

然后端着茶杯走了。

我愣在原地。

老爹从来不会多说一句废话。他能开口,说明这事儿有门。

AI……

我一直在用AI写脚本、补全代码,但从来没想过——

它能不能逆向汇编?

不过在尝试之前,我先推导了一下可行性。

IL2CPP的编译流程是:C# → IL字节码 → C++代码 → 机器码。关键点在于,IL2CPP为了支持反射和调试,会在编译后保留完整的符号信息——类名、函数签名、字段类型、泛型参数,这些都在global-metadata.dat里。

这意味着,从汇编还原到C#,丢失的信息并不多:

  • 类型系统?保留了。
  • 函数签名?保留了。
  • 变量名?局部变量确实丢了,但有类型信息就能推断。

真正需要还原的,只有代码控制流——把jmpcmpcall这些汇编指令,翻译回ifforreturn

理论上可行。

值得一试。


我打开编辑器,新建了一个测试文件。

把IDA反编译出来的伪代码和C#类定义一起丢进去,输入指令:

“根据这段伪代码和类定义,还原C#函数体。”

等了一会儿。

代码出来了。

还原前(Il2CppDumper导出的空函数体):

ExplodeTargetFinder.cs - 还原前

关键代码节选:

public void FindForExploder(GuidedExploderItemModel exploder)
{
}

private IExplodeTarget FindSingleTarget(GuidedExploderItemModel guidedExploder)
{
    return (IExplodeTarget) null;
}

private void FillScores(GuidedExploderType guidedExploderType, bool canExploderSpreadJelly)
{
}

还原后(AI辅助还原的完整实现):

ExplodeTargetFinder_re.cs - 还原后完整代码(3306行)

关键函数实现示例:

public void FindForExploder(GuidedExploderItemModel exploder)
{
    if (exploder == null) return;

    var explodeData = exploder.TargetExplodeData;
    var trigger = explodeData.trigger;
    var guidedExploderType = TriggerExtensions.AsGuidedExploderType(trigger);

    // 根据不同的trigger类型执行不同的查找策略
    switch (trigger)
    {
        case Trigger.TntPropeller:
            target = FindPowerCubeOrSoilTargetForWinCondition(explodeData);
            if (target == null)
                target = FindAreaTarget(explodeData, canSpreadJelly);
            break;
        case Trigger.VerticalPropellerRocket:
            target = FindColumnTarget(explodeData, guidedExploderType, canSpreadJelly);
            break;
        // ... 更多策略
    }

    exploder.TargetFound(target);
}

我对照着IDA的汇编代码,逐行检查。

函数调用顺序,对。 条件判断逻辑,对。 返回值类型,对。

精度……85%以上。

“还真行。”


我开始搭建AI还原工作流。

第一步,在Cursor里加载ILSpy导出的C#骨架工程。Cursor的RAG会给AI提供完整的上下文。

第二步,配置IDA Pro MCP。装了个开源插件,让Cursor能直接读取IDA数据库里的伪代码,不用再复制粘贴。

第三步,批量还原函数体。在Cursor里输入指令,AI读取IDA伪代码,结合工程上下文,输出C#代码。我负责检查和修正错误。

第四步,处理编译器生成类。C#的yield returnasync/await这些语法糖,编译后会生成状态机类。所有函数还原完之后,用AI把这些状态机类还原回语法糖形式。

这套流程跑通之后,我有了汇编代码->C#代码的能力,而不需要理解抽象的汇编代码。


但还原代码只是第一步。

拿到81个类、16个系统、228行权重计算逻辑之后,我面对的是另一个问题:这些代码想干什么?

技术实现里充斥着各种字段名、常量、状态机,但策划的设计意图被埋在了代码噪声里。我需要把这些C#代码翻译成人话——或者说,翻译成老张能看懂的业务文档。

老爹笔记里有句话:“逆向不只是还原代码,更是还原设计者的思维。”

我开始搭建第二套AI工作流:从C#代码还原业务逻辑。

第一步,模块拆分。把81个类按照真实设计意图重新组织——不是按代码目录结构,而是按业务功能划分。比如”单棋子权重计算”“组合奖励权重”“全局调控因子”。这一步需要反复和AI确认理解,因为一旦模块边界划错,后面全是错的。

第二步,子任务拆分。每个模块拆成多个上下文独立的小任务。比如”单棋子权重”模块拆成:”基础权重规则”“优先级系统”“特殊状态加成”。关键是让每个子任务的上下文足够小,AI能一次性处理完。

第三步,细节填充。让AI读代码,过滤掉技术噪声(类型转换、状态机逻辑、异常处理),用流程图、数学公式、表格这些人类友好的形式输出。AI在这一步会发现一些有趣的细节——比如某个优先级权重是676,恰好是低优先级权重26的26倍。

第四步,逻辑关联。把各个文档之间的关联关系标注清楚,用超链接连接起来。这样看文档的人能快速跳转,理解整体架构。


静态逆向只是揭开了冰山一角。

代码可以还原,但它真的按照我理解的方式运行吗?

要回答这些问题,我需要测试。


→ 未完待续


阶段产出


代码会说真话。

本文为基于真实技术分析的虚构故事,文中截图、图片如有展示,均作技术示意,与具体厂商和项目无直接指向关系。

未经授权 禁止转载。

版权所有 © lxraa 2025