溺的文档
Dumper-7 · 第 2 篇 / 共 4 篇

Dumper-7 FName 名称系统

2026-06-30 · 阅读 3

本文是 Dumper-7 偏移推断算法 的姊妹篇。 偏移推断文档讲解了"如何在内存中定位 FName 字段并检测其布局", 本文则讲解"如何把 FName 中的整数索引还原成可读字符串"——这是生成 SDK 中所有类名、成员名、函数名的前提。

目录

  1. 概述:FName 与 NameArray 的关系
  2. FName 结构与四种变体
  3. 两种名称存储后端
  4. GNames / FNamePool 的自动定位算法
  5. 名称表的初始化与索引函数
  6. FNameEntry 的字符串解析
  7. FName → 字符串的三种转换策略
  8. PostInit:动态修正块偏移位数
  9. 版本兼容性总结
  10. 关键函数与数据结构索引

1. 概述:FName 与 NameArray 的关系

1.1 为什么需要 NameArray

在 Unreal Engine 中,所有的名称(对象名、类名、属性名、函数名、枚举值名……)都不是以字符串形式直接存储在对象里的,而是被**池化(interned)**为一个全局名称表中的索引。FName 只保存这个索引(以及可选的数字后缀),真正的字符串存放在全局表中:

对象 A 的名字 "PlayerController"
        │
        ▼
   FName { ComparisonIndex = 12345, Number = 0 }
        │
        │  通过全局名称表查询
        ▼
   全局名称表[12345] ──► "PlayerController"

Dumper-7 要生成 SDK,必须把每个对象、属性、函数的 FName 翻译回字符串。NameArray 模块就是负责"在运行时进程内存中找到这个全局名称表,并提供索引 → 字符串的查询能力" 的核心组件。

1.2 与 OffsetFinder 的分工

模块 职责 产物
OffsetFinder::InitFNameSettings() 检测 FName布局(大小、是否有 Number 字段、是否大小写保留) Off::FName::NumberOff::InSDK::Name::FNameSizebUseCasePreservingNamebUseOutlineNumberName
NameArray 定位全局名称表并实现索引 → 字符串的解码 Off::InSDK::NameArray::GNamesByIndex 索引函数、FNameEntry::GetStr 解码器
FName::Init_Windows() 选择 FName → 字符串的转换策略(引擎函数 vs 直接查表) FName::ToStr 函数指针

三者协作的顺序在 Generator::InitEngineCore() 中体现:

ObjectArray::Init();                          // 先找到 GObjects
CALL_PLATFORM_SPECIFIC_FUNCTION(FName::Init); // FName::Init_Windows():选择转换策略,必要时初始化 NameArray
Off::Init();                                  // 其中调用 InitFNameSettings() 检测 FName 布局

核心要点FName::InitNameArray 是"按需"协作的——如果引擎导出了 AppendString 函数,Dumper 优先调用引擎自己的函数把 FName 转成字符串,根本不需要手动遍历名称表;只有当找不到引擎函数时,才退回到 NameArray 的手动查表方案。


2. FName 结构与四种变体

FName 在不同的编译宏与引擎版本下有四种布局。它们决定了 Off::FName::NumberFNameSize 的取值,也决定了名称表里是否额外存放 Number。

变体 触发宏 内存大小 布局
默认 (无) 8 字节 { int32 ComparisonIndex; uint32 Number; }
大小写保留 WITH_CASE_PRESERVING_NAME 16 字节 { int32 ComparisonIndex; int32 DisplayIndex; uint32 Number; ... }
外联编号 FNAME_OUTLINE_NUMBER 4 字节 { int32 ComparisonIndex; },Number 移入 FNameEntry
大小写保留 + 外联编号 两者皆有 8 字节 { int32 ComparisonIndex; int32 DisplayIndex; }

对应的内部标志(定义于 Settings.hSettings::Internal 命名空间):

/* 使用 FNamePool(UE4.23+)而非 TNameEntryArray */
inline bool bUseNamePool = false;

/* 使用大小写保留 FName,FName 中多出 int32 DisplayIndex */
inline bool bUseCasePreservingName = false;

/* 使用 FNameOutlineNumber,把 Number 从 FName 移入 FNamePool 中的 FNameEntry */
inline bool bUseOutlineNumberName = false;

这些标志由 OffsetFinder::InitFNameSettings() 自动推断,推断算法详见 Dumper-7 偏移推断算法


3. 两种名称存储后端

UE 在 4.23 版本前后更换了名称表的底层数据结构。Dumper-7 通过 Settings::Internal::bUseNamePool 区分两者,并为每一种实现独立的索引函数。

3.1 TNameEntryArray(GNames,UE < 4.23)

旧版的全局名称表是一个"分块指针数组 + 计数字段"的结构:

GNames 指向的内存(TNameEntryArray)
┌────────────────────────────────────────┐
│ +0x00 : Chunk[0]  (FNameEntry**)        │ ┐
│ +0x08 : Chunk[1]  (FNameEntry**)        │ │ 一段连续的块指针
│  ...                                    │ │ (每块最多 0x4000 个条目)
│ +N*8  : Chunk[N]  (FNameEntry**)        │ ┘
├────────────────────────────────────────┤
│ +X    : NumElements (int32)             │ 总名称数
│ +X+4  : MaxChunkIndex / NumChunks(int32)│ 已用块数
└────────────────────────────────────────┘

索引计算(每块固定 0x4000 = 16384 个条目):

const int32 ChunkIdx = ComparisonIndex / 0x4000;   // 第几块
const int32 InChunk  = ComparisonIndex % 0x4000;   // 块内第几个
return reinterpret_cast<void***>(NamesArray)[ChunkIdx][InChunk];

3.2 FNamePool(UE4.23+)

新版的 FNamePool 把字符串数据直接连续存放在块内(而不是块里存指针再指向条目),并用"块索引 + 块内字节偏移"来寻址:

FNamePool 实例
┌────────────────────────────────────────┐
│ +MaxChunkIndex : int32  (已用块数 - 1)   │  ← 由扫描确定
│ +ByteCursor    : int32  (当前块写入游标) │
│  ...                                    │
│ +0x10(ChunksStart) : uint8* Block[0]    │ ┐ 块指针数组
│ +0x18              : uint8* Block[1]    │ │ 每个块指向一段
│  ...                                    │ ┘ 连续的 FNameEntry 数据
└────────────────────────────────────────┘

每个块内部:
[FNameEntry][FNameEntry][FNameEntry]...   ← 以 NameEntryStride 对齐

索引计算(块大小由 FNameBlockOffsetBits 位决定,条目按 NameEntryStride 对齐):

const int32 ChunkIdx      = ComparisonIndex >> NamePoolBlockOffsetBits;
const int32 InChunkOffset = (ComparisonIndex & ((1 << NamePoolBlockOffsetBits) - 1)) * NameEntryStride;
return Block[ChunkIdx] + InChunkOffset;

关键差异:旧版块大小固定为 0x4000,新版块大小是 1 << FNameBlockOffsetBits,且条目以字节为单位寻址(乘以 NameEntryStride)。FNameBlockOffsetBitsNameEntryStride 都需要在运行时探测——见第 5、8 章。


4. GNames / FNamePool 的自动定位算法

定位全局名称表是整个系统的难点:它是一个全局变量,没有符号,且新旧版本的引用方式完全不同。Dumper-7 对两种后端分别实现了基于"已知字符串引用 + 汇编特征 + 导入函数验证"的多重启发式定位。

4.1 TryFindNameArray_Windows(定位 GNames)

定位思路:FName::StaticInit() 中一定会引用字符串 "ByteProperty",并在其附近调用 EnterCriticalSection。顺着这个锚点就能找到 GNames 或返回它的 GetNames() 函数。

1. 在全部节区中查找字符串 "ByteProperty" 的引用地址
       │
2. 从该地址向上回扫,寻找 call 指令(机器码 0xFF.../0xE8)
       │  解析其相对调用目标
       │
3. 若调用目标 == EnterCriticalSection 的导入地址
   ──► 确认这里是 FName 的静态初始化代码
       │
4. 检查紧随其后的指令:
       ├─ 0xE8 (相对 call) ──► 这是 FName::GetNames() 函数
       │                       需进一步进入函数体找 mov reg,[GNames]
       └─ 0x48 0x8B (mov)  ──► GNames 已被内联引用,直接解析 RIP 相对地址
       │
5. 解析出地址,保存到 Off::InSDK::NameArray::GNames

关键在于通过 Architecture_x86_64::Resolve32BitRelativeCall / Resolve32BitRelativeMove 把相对寻址的机器码还原为绝对地址,并用 Platform::GetAddressOfImportedFunctionFromAnyModule 取得 EnterCriticalSection 的真实地址来做交叉验证。

4.2 TryFindNamePool_Windows(定位 FNamePool)

定位思路:FNamePool 是一个全局单例,它的构造函数会被 lea rcx, [rel FNamePool]; call FNamePool::FNamePool() 这样调用,而构造函数内部会调用 InitializeSRWLock 并引用字符串 "ByteProperty"

1. 特征码扫描 "48 8D 0D ? ? ? ? E8"
        = lea rcx, [rel ?]    (? = FNamePool 实例地址)
          call ?              (? = FNamePool 构造函数)
        │
2. 解析 call 目标 → 候选构造函数地址
        │
3. 在构造函数体内 0x50 字节范围内搜索 "FF 15"(call [rel import])
   解析其目标,验证 == InitializeSRWLock 或 RtlInitializeSRWLock 的导入地址
        │
4. 在构造函数 0x2A0 字节范围内验证存在字符串 "ByteProperty" 的引用
   (防止误匹配到其它含 SRWLock 的构造函数)
        │
5. 解析步骤 1 中 lea 的 RIP 相对地址 → FNamePool 实例地址
   保存到 Off::InSDK::NameArray::GNames

为什么用两层验证(SRWLock + ByteProperty):仅靠 lea + call 特征会匹配到大量无关的构造函数;InitializeSRWLock 调用把候选缩小到"带锁的单例构造";"ByteProperty" 字符串则进一步确认这就是名称池——因为引擎在名称池初始化时会预先注册一批内建属性名。

这两个查找函数声明在 NameArray.h:54-55,实现位于 NameArray.cpp(分别在第 364、420 行附近)。它们都属于平台相关函数,通过 CALL_PLATFORM_SPECIFIC_FUNCTION 宏分发。


5. 名称表的初始化与索引函数

定位到名称表地址后,还需要确定它内部各字段的偏移,并绑定正确的索引函数 ByIndex。这由 InitializeNameArray / InitializeNamePool 完成。

5.1 InitializeNameArray(旧版)

NameArray::InitializeNameArray(NameArray.cpp:144)通过"先一串有效指针、随后出现计数字段"的模式自动确定 NumElementsMaxChunkIndex 的偏移:

// 思路:块指针区都是有效指针;指针区结束后紧跟两个 int32(NumElements、NumChunks)
// 当某处读出的 int32 恰好等于"前面统计到的有效块指针个数",即认定找到了计数字段
for (int i = 0; i < 0x800; i += sizeof(void*))
{
    uint8_t* SomePtr = *reinterpret_cast<uint8_t**>(NameArray + i);

    if (SomePtr == 0)                       ZeroQWordCount++;          // 进入计数区
    else if (ZeroQWordCount == 0)           ValidPtrCount++;          // 仍在指针区
    else /* 指针区已结束 */
    {
        int32 NumElements = *(int32*)(NameArray + i);
        int32 NumChunks   = *(int32*)(NameArray + i + 4);

        if (NumChunks == ValidPtrCount)     // 找到了!
        {
            Off::NameArray::NumElements   = i;
            Off::NameArray::MaxChunkIndex = i + 4;

            ByIndex = [](void* NamesArray, int32 ComparisonIndex, int32 /*unused*/) -> void*
            {
                const int32 ChunkIdx = ComparisonIndex / 0x4000;
                const int32 InChunk  = ComparisonIndex % 0x4000;
                if (ComparisonIndex > NameArray::GetNumElements())
                    return nullptr;
                return reinterpret_cast<void***>(NamesArray)[ChunkIdx][InChunk];
            };
            return true;
        }
    }
}

5.2 InitializeNamePool(新版)

NameArray::InitializeNamePool(NameArray.cpp:195)固定了 ChunksStart = 0x10ByteCursor = 0x4,然后扫描确定 MaxChunkIndex 的偏移——判据是"某个 int32 字段的值恰好等于实际非空块指针的个数减一":

for (int i = 0; i < 0x20; i += 4)
{
    const int32 PossibleMaxChunkIdx = *(int32*)(NamePool + i);
    if (PossibleMaxChunkIdx <= 0 || PossibleMaxChunkIdx > 0x10000)
        continue;

    // 统计从 i+8 开始连续非空的块指针个数(连续 0x500 个空指针即认为结束)
    int32 NotNullptrCount = /* ... 扫描 ... */;

    if (PossibleMaxChunkIdx == (NotNullptrCount - 1))   // 匹配!
    {
        Off::NameArray::MaxChunkIndex = i;
        Off::NameArray::ByteCursor    = i + 4;
        break;
    }
}

随后还会用两个魔数字符串做最终校验,确保第 0 块确实是名称数据:

constexpr uint64 CoreUObjAsUint64 = 0x6A624F5565726F43; // 小端 "/Script/CoreUObject" 的片段
constexpr uint32 NoneAsUint32     = 0x656E6F4E;         // 小端 "None"

随后 FNameEntry::Init() 被调用,传入第 0 块指针,自动探测条目内字符串的偏移与长度位移量(见下一章),并据此确定 NameEntryStride,最终绑定 NamePool 版本的 ByIndex 索引函数(使用 FNameBlockOffsetBitsNameEntryStride)。

ByIndex 是一个统一的函数指针,签名为:

static inline void* (*ByIndex)(void* NamesArray, int32 ComparisonIndex, int32 NamePoolBlockOffsetBits) = nullptr;

旧版实现忽略第三个参数并硬编码 0x4000,新版实现使用第三个参数做移位——这样上层 NameArray::GetNameEntry() 无需关心后端差异。


6. FNameEntry 的字符串解析

FNameEntry::Init()(NameArray.cpp:38)负责自动探测条目布局,并生成一个解码 lambda GetStr 缓存起来。整个解析有两个分支。

6.1 FNamePool 版本:探测长度位移量

新版 FNameEntry 的头部是一个 uint16,其中混合了 bIsWide 标志位与名称长度——但长度占据的位数在不同版本不同。Dumper-7 用一个绝妙的办法探测:找到已知的 "ByteProperty" 条目(长度恰为 0xC = 12),把它的头部不断右移,直到值等于 12,移位次数就是长度字段的偏移位数。

constexpr uint16 BytePropertyStrLen = 0xC;   // "ByteProperty" 长度
uint16 BytePropertyHeader = *(uint16*)(AssumedBytePropertyEntry + HeaderOffset);

while (BytePropertyHeader != BytePropertyStrLen && FNameEntryLengthShiftCount < 16)
{
    FNameEntryLengthShiftCount++;
    BytePropertyHeader >>= 1;
}

确定位移量后,解码 lambda 如下:

GetStr = [](uint8* NameEntry) -> std::wstring
{
    const uint16 HeaderWithoutNumber = *(uint16*)(NameEntry + HeaderOffset);
    const int32 NameLen = HeaderWithoutNumber >> FNameEntry::FNameEntryLengthShiftCount;

    if (NameLen == 0)   // 特殊情况:哈希条目(外联编号),见 6.3
    { /* ... */ }

    if (HeaderWithoutNumber & NameWideMask)  // 最低位 = bIsWide
        return std::wstring((const wchar_t*)(NameEntry + StringOffset), NameLen);

    return UtfN::StringToWString(std::string((const char*)(NameEntry + StringOffset), NameLen));
};

其中 HeaderOffset = (StringOffset == 6 ? 4 : 0)——这是为了兼容大小写保留模式下条目头部多出的 2 字节对齐。

6.2 TNameEntryArray 版本:扫描定位字段

旧版没有紧凑的头部,需要分别扫描"字符串偏移"与"索引偏移":

// 用 None(索引 0)定位字符串字段:找到内容为 'enoN'(小端 "None")的偏移
for (int i = 0; i < 0x20; i++)
    if (*(uint32*)(FNameEntryNone + i) == 'enoN') { StringOffset = i; break; }

// 用索引 3、8 两个条目定位 Index 字段:(值 >> 1) 恰好等于 3 和 8
// (最低位是 bIsWide 掩码,所以右移 1 位才是真正的索引值)
for (int i = 0; i < 0x20; i++)
    if ((*(uint32*)(Entry3 + i) >> 1) == 0x3 && (*(uint32*)(Entry8 + i) >> 1) == 0x8)
        { IndexOffset = i; break; }

解码 lambda:

GetStr = [](uint8* NameEntry) -> std::wstring
{
    const int32 NameIdx = *(int32*)(NameEntry + IndexOffset);
    const void* NameString = NameEntry + StringOffset;

    if (NameIdx & NameWideMask)   // 最低位 = bIsWide
        return std::wstring((const wchar_t*)NameString);

    return UtfN::StringToWString<std::string>((const char*)NameString);
};

6.3 特殊情况:哈希条目(外联编号)

FNAME_OUTLINE_NUMBER 模式下,带数字后缀的名字(如 Actor_3)不会单独存一个条目,而是存一个长度为 0 的"哈希条目",它指向真正的基名条目并携带一个 Number:

if (NameLen == 0)
{
    const int32 EntryIdOffset = StringOffset + ((StringOffset == 6) * 2);
    const int32 NextEntryIndex = *(int32*)(NameEntry + EntryIdOffset);
    const int32 Number         = *(int32*)(NameEntry + EntryIdOffset + sizeof(int32));

    if (Number > 0)   // 递归取基名,并拼接 "_数字"(注意减 1)
        return NameArray::GetNameEntry(NextEntryIndex).GetWString() + L'_' + std::to_wstring(Number - 1);

    return NameArray::GetNameEntry(NextEntryIndex).GetWString();
}

为什么 Number 要减 1:UE 内部约定 Number == 0 表示"无后缀",Number == 1 对应显示的 _0。因此把内部 Number 转成显示后缀时需要 Number - 1


7. FName → 字符串的三种转换策略

FName::Init_Windows()(UnrealTypes.cpp)在初始化时会按优先级尝试三种策略,并把最终选定的方案绑定到 FName::ToStr 函数指针。后续所有 FName::ToWString() / ToRawWString() 都通过这个指针完成。

7.1 策略一:调用引擎的 AppendString(最优)

如果能在二进制中定位到引擎的 FName::AppendString(FString&) 函数,就直接借用它——这是最稳健的,因为它就是引擎自己用的实现。

thread_local FFreableString TempString(1024);
AppendString(Name, TempString);          // 引擎函数把 FName 写入 FString
std::wstring Result = TempString.ToWString();

定位方式:通过字符串 "ForwardShadingQuality_" 等已知引用,配合若干 x64 汇编特征找到调用点,再用 Resolve32BitRelativeCall 解析出函数地址,保存到 Off::InSDK::Name::AppendNameToString

7.2 策略二:内联的 GetNameEntry + GetName(次优)

某些编译配置把 AppendString 内联了,此时通过特征码识别"取条目 + 取名字"的两次调用序列:

mov  ecx, [...]          ; 加载 ComparisonIndex
call FName::GetComparisonNameEntry
lea  rdx, [...]          ; 目标 FString
mov  rcx, rax
call FNameEntry::GetName

识别后分别绑定 GetNameEntryFromNameAppendString 两个函数指针。

7.3 策略三:GNames 直接查表(兜底)

当上面两种引擎函数都找不到时,退回到手动查表——这正是第 4~6 章构建的能力:

ToStr = [](const void* Name) -> std::wstring
{
    if (!Settings::Internal::bUseOutlineNumberName)
    {
        const uint32 Number = FName(Name).GetNumber();
        if (Number > 0)
            return NameArray::GetNameEntry(Name).GetWString() + L'_' + std::to_wstring(Number - 1);
    }
    return NameArray::GetNameEntry(Name).GetWString();
};

7.4 三种策略对应的枚举

FName::EOffsetOverrideType(UnrealTypes.h)允许使用者手动指定使用哪种策略(在 Generator::InitEngineCore() 里有注释掉的手动覆盖示例):

enum class EOffsetOverrideType
{
    AppendString,  // 强制使用引擎的 AppendString 函数
    ToString,      // 使用 FName::ToString(仅在找不到 AppendString 时)
    GNames         // 强制走 GNames / FNamePool 手动查表
};
┌─────────────── FName::ToStr 选择决策 ───────────────┐
│                                                     │
│  找到 AppendString 函数?  ──是──► 策略一 AppendString │
│         │否                                          │
│  识别到内联 GetName 序列?  ──是──► 策略二 内联调用    │
│         │否                                          │
│  初始化 NameArray 成功?   ──是──► 策略三 直接查表    │
│         │否                                          │
│         ▼                                            │
│      报错 / 需手动覆盖                                │
└─────────────────────────────────────────────────────┘

8. PostInit:动态修正块偏移位数

FNamePool 的块大小(即每块容纳多少个名称)在不同游戏可能不同。FNameBlockOffsetBits 默认值为 0x10(见 NameArray.h:39),但实际值需要根据真实数据推断。

NameArray::PostInit()(NameArray.cpp:636)的做法是:反向遍历 GObjects,找到 ComparisonIndex 最大的那个对象,逐步增大 FNameBlockOffsetBits 直到它算出的块索引与已知块数自洽

void NameArray::PostInit()
{
    if (GNames && Settings::Internal::bUseNamePool)
    {
        NameArray::FNameBlockOffsetBits = 0xE;   // 从 14 位(块大小 16384)起试

        for (int i = ObjectArray::Num(); i >= 0; i--)
        {
            const int32 ObjNameChunkIdx = Obj.GetFName().GetCompIdx() >> NameArray::FNameBlockOffsetBits;

            if (ObjNameChunkIdx > CurrentBlock)
                NameArray::FNameBlockOffsetBits++;   // 块索引越界,说明位数不够,增大
            if (ObjNameChunkIdx == CurrentBlock)
                break;
        }

        Off::InSDK::NameArray::FNamePoolBlockOffsetBits = NameArray::FNameBlockOffsetBits;
    }
}

这个偏移位数最终会写入生成的 SDK,使运行期 SDK 能用同样的公式索引名称池。


9. 版本兼容性总结

NameArray 系统通过若干运行时探测,实现了对 UE4.15 ~ UE5.4+ 的统一支持:

版本特征 检测方式 影响
GNames vs FNamePool bUseNamePool(由布局检测得出) 选择 InitializeNameArrayInitializeNamePool,绑定不同 ByIndex
FNameEntry 头部位数 "ByteProperty"(长 12)移位探测 确定 FNameEntryLengthShiftCount
字符串/索引字段偏移 "None"、索引 3/8 扫描 确定 StringOffsetIndexOffset(旧版)
宽字符 vs 窄字符 头部/索引最低位 bIsWide 选择 wchar_tchar 解码
外联编号(Outline Number) bUseOutlineNumberName + 长度为 0 的哈希条目 名字后缀从 FNameEntry 递归取得
大小写保留 bUseCasePreservingName HeaderOffset 偏移 2 字节对齐
块大小(NamePool) PostInit 反向遍历对象推断 确定 FNameBlockOffsetBits

10. 关键函数与数据结构索引

10.1 关键函数

函数 位置 职责
NameArray::TryFindNameArray_Windows() NameArray.cpp:364 定位旧版 GNames 全局变量
NameArray::TryFindNamePool_Windows() NameArray.cpp:420 定位新版 FNamePool 单例
NameArray::InitializeNameArray() NameArray.cpp:144 确定 TNameEntryArray 字段偏移并绑定 ByIndex
NameArray::InitializeNamePool() NameArray.cpp:195 确定 FNamePool 字段偏移并绑定 ByIndex
NameArray::TryInit() NameArray.cpp:504 / 558 顶层初始化入口(自动 / 手动覆盖两个重载)
NameArray::PostInit() NameArray.cpp:636 推断 FNameBlockOffsetBits
NameArray::GetNameEntry() NameArray.h:71-72 FName 或索引取得 FNameEntry
FNameEntry::Init() NameArray.cpp:38 探测条目布局,生成 GetStr 解码器
FNameEntry::GetWString() NameArray.cpp:17 调用 GetStr 返回宽字符串
FName::Init_Windows() UnrealTypes.cpp 选择 FName → 字符串转换策略
FName::ToWString() UnrealTypes.cpp 通过 ToStr 完成转换

10.2 关键全局状态

变量 位置 含义
NameArray::GNames NameArray.cpp:10 名称表基址(GNames 或 FNamePool 实例)
NameArray::FNameBlockOffsetBits NameArray.h:39 NamePool 块大小的位数(默认 0x10,PostInit 修正)
NameArray::NameEntryStride NameArray.h:44 NamePool 中条目的字节步长
NameArray::ByIndex NameArray.h:46 索引 → 条目地址的后端无关函数指针
FNameEntry::FNameEntryLengthShiftCount NameArray.h:14 头部长度字段的右移位数
FNameEntry::GetStr NameArray.h:16 条目 → 宽字符串的解码器
FName::ToStr UnrealTypes.h FName → 宽字符串的转换器

文档版本: 1.0 最后更新: 2026-06-30 基于 Dumper-7 源码分析(NameArray.cpp / NameArray.h / UnrealTypes.cpp / UnrealTypes.h)

评论

  • 还没有评论,来说点什么吧。

无需注册或登录,填个昵称即可评论。