Skip to content

12 · 常用字节码详解(下) ​

接上篇(变量、常量、调用、赋值)。这一章覆盖控制流、执行流栈、类型转换、 结构与容器、委托,最后给一张全 token 索引(UE 5.8)。

本章以 UE 5.8 为准

取值与注释出自 CCB-TEAM 私有镜像的 Epic 官方 UE5 源码(release 分支,ENGINE 5.8.0)的 CoreUObject/Public/UObject/Script.h;运行时的处理函数名出自同版本的 ScriptCore.cpp。


五、控制流 ​

操作码token操作数渲染注释原文
EX_Jump0x06uint CodeOffsetgoto L_XXXXGoto a local address in code.
EX_JumpIfNot0x07uint CodeOffset、KismetExpression BooleanExpressionif (!cond) goto L_XXXXGoto if not expression.
EX_Return0x04KismetExpression ReturnExpressionreturn / return XReturn from function.
EX_ComputedJump0x4EKismetExpression CodeOffsetExpression// switch (...)Goto a local address in code, specified by an integer value.
EX_SwitchValue0x69见下RenderSwitch(s)无
EX_Nothing0x0B无空No operation.
EX_NothingInt320x0Cint32(调试用)空No operation with an int32 argument (useful for debugging script disassembly)
EX_EndOfScript0x53无空Last byte in script code
EX_Assert0x09ushort LineNumber、bool DebugMode、KismetExpression AssertExpressionassert(...)Assertion.
EX_Skip0x18uint CodeOffset、KismetExpression SkipExpression—Skippable expression.
EX_SkipOffsetConst0x5B无—A CodeSizeSkipOffset constant

EX_JumpIfNot 是 if 的全部 ​

注意它的语义是「条件不成立就跳」。所以 if (C) { body } 编译出来是:

EX_JumpIfNot { BooleanExpression: C, CodeOffset: <body 之后> }
<body>

反编译器要还原成 if (C),得把条件取反——这就是为什么本站反编译器里有一堆 分支极性判定逻辑(f==t && inner.Follow==t → 守卫式 if (!cond) break; 之类)。

(极性判定规则出自 KismetDecompiler README)

运行时的实现印证了这个顺序(UE 5.8 ScriptCore.cpp):

cpp
DEFINE_FUNCTION(UObject::execJumpIfNot)
{
    CheckRunaway();
    // Get code offset.
    CodeSkipSizeType Offset = Stack.ReadCodeSkipCount();

    uint8 TempValue = 0;
    Stack.Step(Stack.Object, &TempValue);      // ★ 递归求值条件表达式
    const bool NewBoolValue = (TempValue != 0);
    // Jump if false.
    if (!NewBoolValue) { ... }
}

EX_ComputedJump 是块边界 ​

它渲染成一句注释 // switch (...)——因为它的目标是运行时算出来的整数偏移, 静态渲染不出具体 case。但它在结构上极其重要:

  • ubergraph 的分发入口就是 ComputedJump(EntryPoint);
  • 如果解析器不把它当作基本块边界,分发体的每个 case 都切不出来,执行流栈会一路下溢。

本站踩过这个坑,实测数字见第 03 章。运行时的实现(UE 5.8):

cpp
DEFINE_FUNCTION(UObject::execComputedJump)
{
    CheckRunaway();
    // Get the jump offset expression
    int32 ComputedOffset = 0;
    Stack.Step( Stack.Object, &ComputedOffset );
    check((ComputedOffset < Stack.Node->Script.Num()) && (ComputedOffset >= 0));
    // Jump to the new offset
    Stack.Code = &Stack.Node->Script[ComputedOffset];
}

那句 check(...) 说明 EntryPoint 就是 Script 数组里的字节下标。

EX_SwitchValue · 0x69 的七个操作数 ​

  • KismetExpression CaseIndexValueTerm、uint NextOffset、KismetExpression CaseTerm、 uint EndGotoOffset、KismetExpression IndexTerm、KismetExpression DefaultTerm、 FKismetSwitchCase[] Cases

它是唯一自带「跳转表」结构的操作码:Cases 是 case 数组,IndexTerm 是被 switch 的值, DefaultTerm 是 default 分支。UE 5.8 里它没有注释——只能从实现里读语义。

渲染成空字符串的操作码,仍然要算偏移

EX_Nothing、EX_NothingInt32、EX_EndOfScript、EX_Breakpoint、EX_Tracepoint、 EX_WireTracepoint、EX_InstrumentationEvent、EX_DeprecatedOp4A 在反编译产物里什么都不输出。

但它们占的字节一个都不能少算——GetSize 错一个字节,后面所有 CodeOffset 全错位。 「不输出」不等于「不存在」。


六、执行流栈 ​

这三个操作码是全套字节码里最独特的设计:

操作码token操作数渲染注释原文
EX_PushExecutionFlow0x4Cuint PushingAddress空push an address on to the execution flow stack for future execution when a EX_PopExecutionFlow is executed. Execution continues on normally and doesn't change to the pushed address.
EX_PopExecutionFlow0x4D无空continue execution at the last address previously pushed onto the execution flow stack.
EX_PopExecutionFlowIfNot0x4FKismetExpression BooleanExpressionif (!cond) breakcontinue execution at the last address previously pushed onto the execution flow stack, if the condition is not true.

三个必须记住的点:

  1. Push 不改变执行顺序。它只是「记下这个地址,以后要用」,然后继续往下跑—— 这和 x86 的 call 完全不同(call 会跳过去)。
  2. Pop 才是跳转,跳回最后一次 push 的地址。所以这是一个后进先出的栈。
  3. Push/Pop 在反编译产物里渲染成空——你在伪代码里看不到它们, 但它们是蓝图里「顺序执行节点」「延迟逻辑」的实现基础。

运行时的承载就是 FFrame::FlowStack(UE 5.8 Stack.h):

cpp
/** The execution flow stack for compiled Kismet code */
FlowStackType FlowStack;    // typedef TArray<CodeSkipSizeType, TInlineAllocator<8>>

本站反编译器的做法是先用 FlowStackResolver 把这一对解析成静态跳转, 再交给后面的 CFG 构建,而不是在渲染阶段硬猜语义。

(出自 KismetDecompiler README)

直译路线的处理方式不同

本站直译模拟器不走「解析成静态跳转」这条路——它用一个真的 Stack<int> 加 switch 直接承载这个执行流栈,因为压栈的目标地址是编译期常量。 两条路线都成立,区别是可读性 vs 保真度。


七、类型转换 ​

EX_Cast · 0x38 — 基本类型转换 ​

注意:UE4 里这个取值叫 EX_PrimitiveCast,UE 5.8 已改名为 EX_Cast(语义注释未变)。

  • 操作数:ECastToken ConversionType、KismetExpression Target
  • 注释原文:A casting operator which reads the type as the subsequent byte
  • 运行时处理函数:execCast

注释里那句 「reads the type as the subsequent byte」——ConversionType 是紧跟 token 的一个字节。

本站反编译器对它的处理(出自 README):

  • 数值隐式转换 token 3/4 → 渲染成 (float) / (double)(已用 UHT 签名交叉验证);
  • 类型检查语义 → 渲染成 x != None。

EX_CastBase 家族 — 五种对象/接口转换 ​

基类两个字段:FPackageIndex ClassPtr、KismetExpression Target;渲染统一为 Cast<T>(target)。

操作码token运行时处理函数注释原文
EX_DynamicCast0x2EexecDynamicCastSafe dynamic class casting.
EX_MetaCast0x13execMetaCastMetaclass cast.
EX_ObjToInterfaceCast0x52execInterfaceCastConverting an object reference to native interface variable
EX_InterfaceToObjCast0x55execInterfaceToObjectConverting an interface variable reference to an object
EX_CrossInterfaceCast0x54execInterfaceToInterfaceConverting an interface variable reference to native interface variable

(处理函数名出自 UE 5.8 ScriptCore.cpp 的 IMPLEMENT_VM_FUNCTION 注册表,见 13 章)

EX_DynamicCast 是蓝图里 Cast To XXX 节点的实现,也是反编译产物里出现频率最高的转换。 它渲染成 Cast<BP_HandCard>(self) 这种形式。


八、结构与容器 ​

成对出现的常量块 ​

开始token结束token说明
EX_StructConst0x2FEX_EndStructConst0x30An arbitrary UStruct constant / End of UStruct constant
EX_SetArray0x31EX_EndArray0x32Set the value of arbitrary array
EX_SetSet0x39EX_EndSet0x3ASet 字面量(UE5 新增)
EX_SetMap0x3BEX_EndMap0x3CMap 字面量(UE5 新增)
EX_SetConst0x3DEX_EndSetConst0x3ESet 常量(UE5 新增)
EX_MapConst0x3FEX_EndMapConst0x40Map 常量(UE5 新增)
EX_ArrayConst0x65EX_EndArrayConst0x66无注释

EX_StructConst 的操作数是 FPackageIndex Struct(结构类型)+ int StructSize(这个常量占多少字节), 后面跟各字段的表达式,直到 EX_EndStructConst。

为什么需要 StructSize

因为结构常量的内容长度不固定——不同结构字段数不同。 StructSize 让解析器能跳过不认识的结构常量。这也是唯一一处「长度写在前面」的设计。

成员访问 ​

操作码token操作数渲染注释原文
EX_StructMemberContext0x42KismetPropertyPointer StructMemberExpression、KismetExpression StructExpression{StructExpression}.{成员}Context expression to address a property within a struct
EX_ArrayGetByRef0x6BKismetExpression ArrayVariable、KismetExpression ArrayIndex{ArrayVariable}[{ArrayIndex}]无
EX_PropertyConst0x33KismetPropertyPointer Property属性路径FProperty constant.
EX_FieldPathConst0x6DKismetExpression(泛型 Value)—无
EX_BitFieldConst0x11KismetPropertyPointer Property、byte Value{属性}:{值}assign to a single bit, defined by an FProperty
EX_Vector3fConst0x41float X、float Y、float Z(x, y, z)A float vector constant.

EX_StructMemberContext 是**「一个指针 + 一个表达式」**的组合:指针说「取哪个字段」, 表达式说「从哪个结构上取」。见第 09 章。


九、委托 ​

操作码token操作数渲染注释原文
EX_AddMulticastDelegate0x5CKismetExpression Delegate、KismetExpression DelegateToAdd{Delegate}.Add({DelegateToAdd})Adds a delegate to a multicast delegate's targets
EX_RemoveMulticastDelegate0x62同上{Delegate}.Remove({DelegateToAdd})Remove a delegate from a multicast delegate's targets
EX_ClearMulticastDelegate0x5DKismetExpression DelegateToClear{DelegateToClear}.Clear()Clears all delegates in a multicast target
EX_CallMulticastDelegate0x63KismetExpression Delegate(+ StackNode / Parameters){Delegate}.{函数}({参数})Call multicast delegate
EX_BindDelegate0x61FName FunctionName、KismetExpression Delegate、KismetExpression ObjectTerm{Delegate}.BindUFunction({ObjectTerm}, "{名字}")bind object and name to delegate
EX_InstanceDelegate0x4BFName FunctionName函数名const reference to a delegate or normal function object

委托相关的赋值走 EX_LetDelegate / EX_LetMulticastDelegate(见上篇)。


十、调试、事务内存与杂项 ​

操作码token说明
EX_Breakpoint0x50Breakpoint. Only observed in the editor, otherwise it behaves like EX_Nothing.
EX_Tracepoint0x5ETrace point. Only observed in the editor, otherwise it behaves like EX_Nothing.
EX_WireTracepoint0x5A同上(蓝图连线上的调试点)
EX_InstrumentationEvent0x6AInstrumentation event;操作数 EScriptInstrumentationType EventType、FName EventName
EX_DeprecatedOp4A0x4A已废弃的占位值

UE5 新增:AutoRTFM 事务内存 ​

操作码token注释原文
EX_AutoRtfmTransact0x70AutoRTFM: run following code in a transaction
EX_AutoRtfmStopTransact0x71AutoRTFM: if in a transaction, abort or break, otherwise no operation
EX_AutoRtfmAbortIfNot0x72AutoRTFM: evaluate bool condition, abort transaction on false
EX_AutoRtfmAbort0x73AutoRTFM: abort transaction

AutoRTFM(Automatic Runtime Transactional Function Memory)是 UE5 引入的事务内存机制—— 这四个操作码就是它在字节码层面的体现。UE4 的解析器完全不知道它们的存在。

上面这些调试类操作码在打包后的游戏里通常不出现(编辑器专用),但解析器仍要能跳过它们。


附:全 token 索引(UE 5.8) ​

按取值排序的完整列表,取自 UE 5.8 Script.h。「详解」列指向本章或上篇的小节; 标「未详解」的是实践中很少遇到的。

token操作码详解
0x00EX_LocalVariable上篇 · 变量引用
0x01EX_InstanceVariable上篇 · 变量引用
0x02EX_DefaultVariable上篇 · 变量引用
0x04EX_Return本章 · 控制流
0x06EX_Jump本章 · 控制流
0x07EX_JumpIfNot本章 · 控制流
0x09EX_Assert本章 · 控制流
0x0BEX_Nothing本章 · 控制流
0x0CEX_NothingInt32 ★本章 · 控制流
0x0FEX_Let上篇 · 赋值
0x11EX_BitFieldConst ★本章 · 成员访问
0x12EX_ClassContext上篇 · 上下文调用
0x13EX_MetaCast本章 · 类型转换
0x14EX_LetBool上篇 · 赋值
0x15EX_EndParmValue上篇 · 终止符
0x16EX_EndFunctionParms上篇 · 终止符
0x17EX_Self上篇 · 变量引用
0x18EX_Skip本章 · 控制流
0x19EX_Context上篇 · 上下文调用
0x1AEX_Context_FailSilent上篇 · 上下文调用
0x1BEX_VirtualFunction上篇 · 函数调用
0x1CEX_FinalFunction上篇 · 函数调用
0x1DEX_IntConst上篇 · 常量
0x1EEX_FloatConst上篇 · 常量
0x1FEX_StringConst上篇 · 常量
0x20EX_ObjectConst上篇 · 常量
0x21EX_NameConst上篇 · 常量
0x22EX_RotationConst上篇 · 常量
0x23EX_VectorConst上篇 · 常量
0x24EX_ByteConst上篇 · 常量
0x25EX_IntZero上篇 · 常量
0x26EX_IntOne上篇 · 常量
0x27EX_True上篇 · 常量
0x28EX_False上篇 · 常量
0x29EX_TextConst上篇 · 常量
0x2AEX_NoObject上篇 · 常量
0x2BEX_TransformConst上篇 · 常量
0x2CEX_IntConstByte上篇 · 常量
0x2DEX_NoInterface上篇 · 常量
0x2EEX_DynamicCast本章 · 类型转换
0x2FEX_StructConst本章 · 结构与容器
0x30EX_EndStructConst本章 · 结构与容器
0x31EX_SetArray本章 · 结构与容器
0x32EX_EndArray本章 · 结构与容器
0x33EX_PropertyConst ★本章 · 成员访问
0x34EX_UnicodeStringConst上篇 · 常量
0x35EX_Int64Const上篇 · 常量
0x36EX_UInt64Const上篇 · 常量
0x37EX_DoubleConst ★上篇 · 常量
0x38EX_Cast本章 · 类型转换
0x39EX_SetSet ★本章 · 结构与容器
0x3AEX_EndSet ★本章 · 结构与容器
0x3BEX_SetMap ★本章 · 结构与容器
0x3CEX_EndMap ★本章 · 结构与容器
0x3DEX_SetConst ★本章 · 结构与容器
0x3EEX_EndSetConst ★本章 · 结构与容器
0x3FEX_MapConst ★本章 · 结构与容器
0x40EX_EndMapConst ★本章 · 结构与容器
0x41EX_Vector3fConst ★本章 · 成员访问
0x42EX_StructMemberContext本章 · 成员访问
0x43EX_LetMulticastDelegate上篇 · 赋值
0x44EX_LetDelegate上篇 · 赋值
0x45EX_LocalVirtualFunction ★上篇 · 函数调用
0x46EX_LocalFinalFunction ★上篇 · 函数调用
0x48EX_LocalOutVariable上篇 · 变量引用
0x4AEX_DeprecatedOp4A未详解(已废弃)
0x4BEX_InstanceDelegate本章 · 委托
0x4CEX_PushExecutionFlow本章 · 执行流栈
0x4DEX_PopExecutionFlow本章 · 执行流栈
0x4EEX_ComputedJump本章 · 控制流
0x4FEX_PopExecutionFlowIfNot本章 · 执行流栈
0x50EX_Breakpoint本章 · 调试与杂项
0x51EX_InterfaceContext上篇 · 上下文调用
0x52EX_ObjToInterfaceCast本章 · 类型转换
0x53EX_EndOfScript本章 · 控制流
0x54EX_CrossInterfaceCast本章 · 类型转换
0x55EX_InterfaceToObjCast本章 · 类型转换
0x5AEX_WireTracepoint本章 · 调试与杂项
0x5BEX_SkipOffsetConst本章 · 控制流
0x5CEX_AddMulticastDelegate本章 · 委托
0x5DEX_ClearMulticastDelegate本章 · 委托
0x5EEX_Tracepoint本章 · 调试与杂项
0x5FEX_LetObj上篇 · 赋值
0x60EX_LetWeakObjPtr上篇 · 赋值
0x61EX_BindDelegate本章 · 委托
0x62EX_RemoveMulticastDelegate本章 · 委托
0x63EX_CallMulticastDelegate本章 · 委托
0x64EX_LetValueOnPersistentFrame上篇 · 赋值
0x65EX_ArrayConst本章 · 结构与容器
0x66EX_EndArrayConst本章 · 结构与容器
0x67EX_SoftObjectConst上篇 · 常量
0x68EX_CallMath上篇 · 函数调用
0x69EX_SwitchValue本章 · 控制流
0x6AEX_InstrumentationEvent本章 · 调试与杂项
0x6BEX_ArrayGetByRef本章 · 成员访问
0x6CEX_ClassSparseDataVariable ★上篇 · 变量引用
0x6DEX_FieldPathConst ★本章 · 成员访问
0x70EX_AutoRtfmTransact ★本章 · 事务内存
0x71EX_AutoRtfmStopTransact ★本章 · 事务内存
0x72EX_AutoRtfmAbortIfNot ★本章 · 事务内存
0x73EX_AutoRtfmAbort ★本章 · 事务内存
0xFFEX_Max枚举上界,非实际操作码

标 ★ 的是 UE4 表里没有的取值(共 22 个)。

空档在哪:0x6E–0x6F 与 0x74–0xFE

引擎自己用到了 0x6D,0x70–0x73 被 AutoRTFM 占用。所以厂商能塞自定义指令的空间是这两段—— 鸣潮的 EX_6E/EX_6F 正好落在前一段,Borderlands 4 的 EX_FD/EX_FE 在后一段, 见 第 05 章 · 罕见之处。

相关 ​