ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

yuzu金手指功能完整指南:3步编写你的第一个Switch内存修改代码

yuzu金手指功能完整指南:3步编写你的第一个Switch内存修改代码

yuzu金手指功能完整指南:3步编写你的第一个Switch内存修改代码

【免费下载链接】yuzu项目地址: https://gitcode.com/GitHub_Trending/yuz/yuzu

yuzu 是目前最活跃的开源 Nintendo Switch 模拟器项目之一,其内置的**金手指(cheats)**功能允许玩家在不修改游戏文件的前提下,按固定频率读写游戏进程的内存,实现无限生命、解锁道具、跳过检查等玩法。本文以 yuzu 源码为准,带你从"文件放哪里"讲到"指令怎么编",最终给出安全使用的完整建议。

先说收益:没有金手指 vs 有了金手指

在了解实现细节之前,先看这个功能实际解决什么问题。

场景无金手指有金手指
反复死亡的教学关反复读档、重打一条 StoreStatic 代码把生命值字段锁满
想按特定按键解锁功能依赖游戏内设定或改版键控条件块,按住 L 键才生效
需要批量初始化数据结构手动修改二进制文件循环指令 + 寄存器寻址一次写完全阵
想确认某内存字段当前取值用外部工具逐帧猜测条件块比较 + DebugLog 输出到日志

💡提示:金手指的本质不是"修改游戏",而是在游戏运行期间以每秒固定频率向游戏内存注入指令。它和游戏本体文件互不干扰,随时可以取消勾选停用。

原理拆解:yuzu 的金手指引擎如何运行

yuzu 的金手指系统复刻了 Switch 固件中的 DMNT 作弊虚拟机(DmntCheatVm)。整个链路可以概括为:启动时解析文本 → 拼成指令流 → 每帧定时执行 → 通过内存回调读写游戏进程。

四个关键模块:

  • 文件定位与解析:src/core/file_sys/patch_manager.cpp 中的CreateCheatList()扫描 mod 目录下的cheats子目录,读取以 build_id 命名的.txt文件
  • 文本解析:src/core/memory/cheat_engine.cpp 中TextCheatParser::Parse()把方括号节和十六进制行转成CheatEntry结构
  • 虚拟机:src/core/memory/dmnt_cheat_vm.cpp 负责逐条解码并执行操作码
  • 定时调度:CheatEngine::Initialize()在 src/core/memory/cheat_engine.cpp 中注册一个周期事件,常量CHEAT_ENGINE_NS对应12 次/秒的执行频率

执行细节上有几个容易忽视但很关键的点:

  1. 虚拟机拥有16 个 64 位寄存器(NumRegisters = 0x10),全部指令共享,且每次执行前清零,所以状态不会跨帧残留
  2. 所有启用的作弊项会被拼接成一条连续指令流,单次程序上限 1024 条操作码(MaximumProgramOpcodeCount = 0x400)
  3. 键控条件块在执行瞬间读取hid服务的按键状态(HidKeysDown()),所以"按住 L 键触发"是真实可控的
  4. 所有内存访问都经过IsAddressInRange()校验,只能落在 Main NSO、堆、别名区、ASLR 区四块区域之内,越界写入会被直接丢弃并记录日志

核心概念速查表:

名称作用常见场景
StoreStatic把固定值写入相对地址锁满血、改金币
BeginConditional / EndConditional条件块,比较内存值后决定是否执行块内指令仅在血量低于阈值时回血
BeginKeypressConditional按键条件块,按住指定掩码的按键才执行手动触发的无敌模式
ControlLoop以寄存器为计数器的循环批量写入数组
LoadRegisterMemory从内存读入寄存器先读再改、动态寻址
MemoryAccessType内存区域标识(MainNso/Heap/Alias/Aslr)选择正确的基址偏移

快速上手:3步导入你的第一个金手指文件

第 1 步:确认游戏的 build_id

金手指文件不是按 title_id 命名的,而是按游戏可执行文件(NSO)的 build_id匹配。yuzu 在启动游戏加载 NSO 时(src/core/loader/nso.cpp 第 196 行)会取出nso_header.build_id传给CreateCheatList()。你可以在启动日志里找到对应行,文件名取 build_id 的前 16 个十六进制字符。

第 2 步:创建目录与文件

yuzu 的 mod 加载根目录是load/(见 src/common/fs/fs_paths.h 中LOAD_DIR "load"定义),游戏 mod 的约定结构为:

load/<title_id>/<任意mod名>/cheats/<build_id前16位>.txt

例如某游戏的 title_id 为0100000000010000,build_id 前 16 位为A1B2C3D4E5F60718,则文件路径是:

load/0100000000010000/MyCheat/cheats/A1B2C3D4E5F60718.txt

注意cheats目录名不区分大小写(代码中用FindSubdirectoryCaseless查找),但 build_id 文件名会先尝试大写再尝试小写,两种都支持。

第 3 步:写入最简金手指代码

文件是纯文本。解析器(TextCheatParser::Parse)只认两种节标记:{名称}(始终启用)和[名称](可开关),其余内容必须是无空白的 8 位十六进制操作码。一条 8 字节写入的代码长这样:

[锁定满血] 07000000 00010A4C 000003E8

白话解释:第一行07000000 00010A4C是 StoreStatic 指令——首位0是操作码类型,7表示位宽 8 字节,0表示 Main NSO 区,0表示不叠加寄存器偏移;第二行000003E8是要写入的值 1000。第三行地址00010A4C拼在地址字段里。整个节会被翻译成"每 1/12 秒向主 NSO 相对地址 0x00010A4C 写入 8 字节 0x3E8"。

✅自检清单:

  • 文件名 = build_id 前 16 位(区分大小写各试一次)
  • 节名用[名称]或{名称}包裹,不能为空
  • 每行操作码恰好 8 个十六进制字符,不含空格
  • 单个节最多 256 条操作码(CheatDefinition::opcodes数组大小0x100)

进阶技巧:条件触发、键控与批量写入

基础写入之外,虚拟机还支持组合逻辑。以下三个用法都来自 src/core/memory/dmnt_cheat_vm.cpp 中实际实现的解码路径。

编写按键触发的条件代码

BeginKeypressConditional 指令只需一个 32 位按钮掩码,掩码定义在 src/hid_core/hid_types.h 的NpadButton枚举中(A=0x1、B=0x2、X=0x4、Y=0x8、L=0x40、R=0x80、+ =0x400、- =0x800)。执行时虚拟机把掩码与当前按下的按键做与运算,不完全命中就整块跳过。

[按住L键无敌] 80000040 04000000 0000A1B2 000000FF 20000000

白话解释:80000040中首位8是键控条件操作码,000040是 L 键掩码;块内是一条 4 字节写入,把某地址写成 0xFF;20000000是 EndConditionalBlock 收尾。只有按住 L 键的那个 1/12 秒窗口内,写入才会发生。

编写内存值条件代码

BeginConditionalBlock 指令把"内存里某个值"和常量做比较,比较类型编码在指令字段里:GT=1、GE=2、LT=3、LE=4、EQ=5、NE=6。指令格式(见 dmnt_cheat_vm.cpp 中的位域注释)为1 T M C 00 AA,T 位宽、M 内存区、C 比较类型、AA 是地址高 8 位,地址低 32 位与比较值各占后续字。

[血量低于1时回满] 18050000 0000A1B2 00000064 07000000 0000A1B2 000003E8 20000000

白话解释:条件块从 Main NSO 偏移 0xA1B2 读 8 字节,与 0x64(100)比较,只有"小于"成立时才执行块内的满值写入。这样写出来的效果是"低于阈值才回血",而不是一帧无条件锁死。

用循环和寄存器批量写内存

ControlLoop 以某个寄存器为计数器:300R0000启动循环并写入迭代次数,310R0000结束时递减,未归零则跳回循环头。配合 LoadRegisterStatic(把常量载入寄存器)、LoadRegisterMemory(从内存读入寄存器)、StoreRegisterToAddress(把寄存器值写到寄存器指向的地址)可以构造出"遍历结构体数组逐项初始化"这样的复杂逻辑。

[批量初始化16项] 40000000 00000010 00000000 30010000 00000010 64010000 00000064 31010000

白话解释:先把 0x10(16)载入寄存器 0;以寄存器 1 计数执行 16 次循环;每次循环向寄存器 0 指向的地址写入 4 字节 0x64 并自动递增指针。注意寄存器 0 初值来自 LoadRegisterStatic 的第二个字,实际编写时应先把它指向数组首地址。

💡提示:所有寄存器在每次 VM 执行前都会清零(ResetState()),因此跨帧保持状态要靠"静态寄存器"读写指令(ReadWriteStaticRegister),不要假设普通寄存器的值会留存到下一帧。

注意事项:常见错误与规避方法

⚠️注意:金手指代码写错不会"没事",它可能让游戏进入异常状态。以下按出现频率排序。

症状原因建议
代码完全无效文件名与 build_id 不匹配,或cheats目录层级不对查看启动日志中No cheats file found提示,确认前 16 位大小写
日志刷invalid address相对地址落在四块合法区域之外参考 cheat_engine.cpp 中IsAddressInRange的四区域划分,核对偏移是否相对 Main NSO 基址
游戏崩溃写入了代码段或关键结构体指针先用条件块读值确认,再小范围写入测试
更新后代码失效游戏更新导致 build_id 变化为新 build_id 复制一份金手指文件,重新核对偏移
效果不稳定把"按住"当成了"点按"键控掩码读的是持续按住状态(GetNpadButton按压位),设计触发逻辑时考虑重复执行

其他风险点:

  • 存档污染:把数值写进会被存盘序列化的字段,存档里就是脏数据。测试新代码前先备份存档
  • 在线游玩:多人环境使用内存修改可能导致匹配异常或封禁,单机场景再启用
  • 数量上限:所有启用项拼接后超过 1024 条操作码会导致整份作弊列表加载失败(LoadProgram返回 false),精简不用的项
  • 性能:12 次/秒的固定开销可忽略,但块内指令过多或条件块频繁跳转会放大 CPU 占用,保持代码短小

相关资源与最佳实践

想继续深入,从这些源码目录入手:

  • 引擎总控与文件解析:src/core/memory/cheat_engine.cpp、src/core/memory/dmnt_cheat_types.h
  • 虚拟机指令集与位域注释:src/core/memory/dmnt_cheat_vm.h(每个操作码格式都带// 0TMR00AA这样的注释,是最好的"指令手册")
  • 文件加载链路:src/core/file_sys/patch_manager.cpp 的CreateCheatList()、src/core/loader/nso.cpp 的注册入口
  • 按键掩码定义:src/hid_core/hid_types.h 的NpadButton枚举
  • mod 目录约定:src/common/fs/fs_paths.h

最佳实践回顾:从一条 StoreStatic 写起,用条件块验证地址取值,确认无误后再加键控触发或循环——这正是本文开头"先收益、再原理、后操作"的完整闭环。金手指的终极价值在于理解:读 dmnt_cheat_vm.cpp 里每一段位域注释,你就掌握了一套完整的 Switch 内存修改语言。

如需本地构建 yuzu 源码,仓库地址为 https://gitcode.com/GitHub_Trending/yuz/yuzu ,克隆后按仓库自带 CMake 流程配置即可。

【免费下载链接】yuzu项目地址: https://gitcode.com/GitHub_Trending/yuz/yuzu

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进