Skip to content

03 · 反编译 ​

反编译的目标是可读性,代价是有损。这一章讲清楚:反编译要经过哪几层处理、每一层会丢什么、 以及怎么判断产物能不能信。

引擎自己就有反汇编器 ​

Epic 提供了一个 FKismetBytecodeDisassembler,头文件里的自我描述是:

cpp
/**
 * Kismet bytecode disassembler; Can be used to create a human readable version
 * of Kismet bytecode for a specified structure or class.
 */
class FKismetBytecodeDisassembler
{
    // ...
    UNREALED_API void DisassembleStructure(UFunction* Source);

(UE4 里的位置:Editor/UnrealEd/Public/ScriptDisassembler.h。 ⚠️ 在 UE 5.8 的源码树里这个路径已不存在——多个候选路径均返回 404,可能被移动或移除; 需要时请在自己有权限的副本里检索 FKismetBytecodeDisassembler)

两个信息值得注意:

  1. 它住在 UnrealEd 模块里——只有编辑器构建才有,打包后的游戏里没有。所以你不能指望在目标游戏里 「调用引擎自带的反汇编器」把蓝图打出来。
  2. 它的产物是「human readable version」,面向人读,不是可复用的数据结构。

这两点解释了为什么生态里需要第三方工具:要的是数据,不是文本。

反编译的四层处理 ​

以本站 KismetDecompiler 的流水线为例(出自 README):

ScriptBytecode
   │  FlowStackResolver      把 EX_PushExecutionFlow / EX_PopExecutionFlow 解析成静态跳转
   ▼
BuildStatements → SplitBlocks → BuildEdges → MarkReachable(+DCE) → Compact
   │  FunctionSignatures     从 LoadedProperties 取真实签名(参数名/类型/in-out/局部变量)
   ▼
ComputeDominators / ComputePostDominators / FindLoops
   ▼
EmitRegion                 支配树 + 后支配 + 自然循环 → if / if-else / while / do-while / switch
   │  EmitPendingGotoTargets  无法结构化处才落 goto + label
   ▼
ReadabilityPasses          文本级等价改写
   ▼
SemanticOptimizer(--opt)  值流内联 + 常量折叠 + 命名 + 循环还原

逐层看,每一层都在做「等价改写」,而每一次等价改写都是一次可能出错的推断:

层做什么会丢什么
表达式渲染AST → 字符串:变量路径、函数名、常量、类型内层表达式可能退化成类型名(需靠 RawValue 递归展开避免)
控制流结构化支配树 + 后支配 + 自然循环 → if/else/while结构化不了的地方降级成 goto + label;本站指标是 17/19 函数零 goto
反 DispatchExecuteUbergraph 还原成 switch (EntryPoint),case 标注来源事件函数跨段 goto 以保真形式保留,不强行结构化
可读性改写折叠空 if、else if 合并、for 循环还原、去死代码等文本级改写一旦规则写错,会静默改变语义
语义优化临时量值流内联、常量折叠、命名简化原本两次独立调用可能看起来像一次

有损性:一次真实事故 ​

这是本站最有价值的一条实测记录,来自直译模拟器 01:

反编译器的 EX_ComputedJump 被当成普通顺序执行语句,没有当作基本块边界。 而 ubergraph 的分发入口恰恰就是 ComputedJump(EntryPoint) 起手的—— 于是分发体从缺,执行流栈一路下溢,产物里遍布 [stack=?] 占位。

修法是在 FlowStackResolver 里给 Block 加 Dispatch 标记、在 EX_ComputedJump 与分发入口处切块, 并让 BuildEdges 把 EX_ComputedJump 当作 Term.Halt。补丁前后的实测(1906 张卡):

指标补丁前补丁后
效果调用点763210102(+32%)
空壳处理函数1028(35.8%)826(28.8%)
BP_Logic 产物299174 B / 5324 行363415 B / 5639 行
BP_Logic 空 case20

(出自 直译模拟器 01 · 为什么是 AST,不是伪代码)

这张表的真正含义

补丁前的产物是「能读的」——它没有报错、没有崩溃,只是少了 32% 的调用点。 如果你拿它当参考去理解逻辑,你会得到一个自信的错误结论。

所以:反编译产物的正确性,不能靠「看起来对不对」判断。

一个至今没修的残留缺陷 ​

同一条记录里还留了一个二级 bug:do { X(); } while (X()); 会把条件里的调用重复渲染一次, 370 个文件里共 531 处。对直译没影响(直译走 AST,不经过这条渲染路径), 但如果你拿伪代码做参考,要知道这个位置会多一次调用。

(出处同上)

怎么判断产物能不能信 ​

综合上面的经验,给产物分级:

用途可信度说明
批量扫描找特征(某个函数名出现在哪些蓝图里)✅ 可用名称解析与调用点存在性一般不受结构化影响
人工阅读理解逻辑⚠️ 需交叉验证结构性缺陷会让分支/调用缺失,且不报错
作为机器翻译 / 直译的输入❌ 不可用应该直接消费 AST;伪代码是有损渲染,边界情况丢信息
作为改写后回编译的基础❌ 不可用往返编译必须走 AST ↔ 文本 ↔ AST,不能经过伪代码

(第一、三、四行的依据分别来自工具用法与直译模拟器 01 的结论; 第二行为经验做法。)

三个可操作的验证手段 ​

  1. 看反汇编视图。 本站工具提供 --disasm [名字子串],输出「字节偏移 / 操作码 / 渲染结果 / 跳转目标」 四列——渲染结果可疑时,回去看操作码。
  2. 看 CFG。 --flowdebug / --cfg 把执行流栈解析与基本块切分过程打出来。 边界切错的典型症状就是「块数明显偏少」「执行流栈下溢」。
  3. 换一个锚点交叉验证。 比如统计「某个函数被调用的次数」,用 AST 遍历算一遍, 再和伪代码文本 grep 一遍——两个数字不一致时,先怀疑伪代码。

(工具参数出自 KismetDecompiler README)

反编译与直译的分工 ​

反编译直译
输入ASTAST(不是伪代码)
产物给人读的伪代码给机器执行的目标语言代码
可读性目标明确放弃(产物 42 万行全是 L_0A23: 和 goto)
正确性要求够读就行必须严格等价
本站实例KismetDecompilerKardsSim

本站直译项目对这件事有一句总结:「能拿到 AST 就别看文本。文本是给眼睛的,AST 是给机器的。」

相关 ​