一个约 1.07 GB 的单 exe 启动器:双击即玩,不解压、零驱动、零管理员、零外部依赖, 直接运行 Minecraft 26.2-NeoForge_26.2.0.45-beta(离线单机)。
artifacts\SingleFileMc.exe = ~3.4 MiB NativeAOT 宿主 + ~1.06 GB 尾部 Store zip 容器。
核心是纯用户态 ntdll hook 虚拟文件系统(Z:\ 虚拟根、假句柄表、假 SEC_IMAGE 内存加载
jvm.dll)+ JNI 进程内 JVM + 容器 mmap 手动解析 zip。
SingleFileMc 把整套 Minecraft + NeoForge + JDK 数据树(约 1.06 GB)打进启动器 exe 的尾部
zip 容器里。运行时不做任何解压:宿主通过 mmap 直接把 exe 尾部的 zip 当作数据源,再用
ntdll hook 把 JVM 与游戏对文件系统的所有访问引导到一个虚拟的 Z:\ 根上,由容器按偏移
直接提供文件内容。没有驱动、没有提权、没有临时解压目录、没有真实盘符。
关键特性:
- 单文件交付:只分发
SingleFileMc.exe一个文件。 - 零解压:容器内条目全部 Store(不压缩),运行时 mmap 按偏移直读,不落地。
- 零驱动 / 零管理员:全部是纯用户态 ntdll hook,无内核组件、无需 UAC。
- 零外部依赖:NativeAOT 发布 +
sfmc_hooks_static.lib静态链接,exe 之外没有任何 dll。 - 离线单机:无微软账号登录;用户名取自 exe 文件名(默认
SingleFileMc.exe显示 "SingleFileMc",重命名 exe 即可改名)。 - 存档持久:gameDir 是真实可写目录,存档 / 配置 / mods 持久保存。
环境基线:Windows 11 26100,.NET 10,x64。
- 双击
artifacts\SingleFileMc.exe(或在artifacts\目录内执行)。 - 进程会先启动 JVM,然后拉起 Minecraft,等待主菜单窗口出现。
- 关闭游戏窗口后进程退出(详见已知限制与设计取舍)。
启动后生成:
artifacts\game\:gameDir(真实、可写、持久)。saves\存档、mods\、config\、resourcepacks\都在这。mods 放artifacts\game\mods\即可加载。artifacts\logs\:游戏日志。- natives 虚拟化:JVM natives 全部位于虚拟
Z:\cache\natives\(内存,不落盘), 真实artifacts\game\cache零 natives。
| 变量 | 取值 | 作用 |
|---|---|---|
SFMC_VERBOSE_HOOKS |
1 / true |
开启 ntdll hook 全量日志(默认关闭,避免刷屏) |
双击 SingleFileMc.exe
│
├─ 1. Container.Init() 最早期 mmap 自身 exe,尾部扫描 EOCD,解析中央目录,
│ Store 校验,建虚拟目录表(不落盘)
├─ 2. JIT-safety 预热 在第一个 detour 安装前编译全部热路径
│ (回退/启动链/PE 镜像布局/JNI delegate)
├─ 3. FakeFileSystem.Init() 安装 19 个 ntdll hook(MinHook.NET 原生 detour)
├─ 4. JNI_CreateJavaVM 进程内创建 JVM;jvm.dll 优先真实磁盘加载,
│ 缺失时经假 SEC_IMAGE 从容器内存加载
└─ 5. McLaunch.Run() 解析版本 json → 构建 Z:\ 类路径 → natives 虚拟化到
Z:\cache\natives → gameDir 就绪 → Client.main(String[])
→ 等待主菜单
- 宿主最早期
CreateFileMappingW + MapViewOfFile把整个 exe 映射进内存(只读)。 - 从文件尾回看最多 64 KB + 22 B 扫描 EOCD(
0x06054b50),校验 commentLen 与文件尾 距离一致才接受。 - 手动解析中央目录,建立条目表。zip 顶层只有两棵:
openjdk/与minecraft/(打包时由Minecraft/数据树映射,见构建)。 - Store 校验:任何条目
method != 0或compSize != uncompSize都视为容器损坏, 打印错误并以退出码 100 拒绝启动。运行时不做任何解压,全部按偏移直读。 - 损坏到无法解析时以退出码 101 退出。
19 个 hook,分五组:
| 组 | Hook |
|---|---|
| 文件 | NtCreateFile NtOpenFile NtReadFile NtClose NtQueryInformationFile NtQueryAttributesFile NtQueryFullAttributesFile NtQueryVolumeInformationFile NtSetInformationFile |
| 映射 | NtCreateSection NtMapViewOfSection NtUnmapViewOfSection NtQuerySection |
| 目录枚举 | NtQueryDirectoryFile NtQueryDirectoryFileEx |
| 句柄 | NtDuplicateObject |
| 写入与锁 | NtWriteFile NtLockFile NtUnlockFile |
(NtDuplicateObject 保留在句柄组;写入与锁组为 PHASE18 新增,服务虚拟 natives 区。)
关键机制:
Z:\虚拟根:JVM 与游戏的路径访问被改写到Z:\openjdk\...、Z:\minecraft\..., 由容器条目表直接服务;无容器时回退 exe 旁的磁盘别名(仅调试用)。- 假句柄表:
NtCreateFile/NtOpenFile对容器内文件与虚拟 natives 文件返回伪造句柄 (文件0x5100xxxx、section0x52000000|n),真实句柄一律放行到原 trampoline。 - 虚拟可写区(PHASE18):仅
Z:\cache\natives\子树可写,natives 提取与运行期 JNA/LWJGL/Netty 写入全走内存虚拟文件表(经NtWriteFilehook),其余Z:\保持只读; 可写句柄拒读、只读句柄拒写(句柄级 + 文件级互斥)。 - 假
SEC_IMAGE:NtCreateSection/NtMapViewOfSection对容器内 PE 文件 (如jvm.dll)做纯托管 PE32+ 解析 + 手工镜像布局,内存里按节加载,不落盘。 - JNI 进程内 JVM:加载
jvm.dll→JNI_CreateJavaVM→ 在宿主进程内直接跑 Minecraft。类路径全部是Z:\...虚拟路径,经 hook 由容器提供字节。 - native 守卫 stub(
native_hooks/,C):每个 hook 有一个Stub_*前置分流, 只在命中假句柄 /Z:\路径时才进托管,GC 内部的 ntdll 调用不经过托管 hook;Suppress计数防重入。
- 宿主以
dotnet publish -c Release -r win-x64(<PublishAot>true</PublishAot>)发布为 纯原生单 exe,运行时零 JIT。 - native 守卫库以
sfmc_hooks_static.lib通过<DirectPInvoke Include="__Internal"><NativeLibrary>静态链接进 exe,LibraryImport("__Internal")符号由链接器直接 解析,运行时不再 LoadLibrary。这是"零外部依赖"的根基。
- JIT 调试模式(
Buildtarget)则使用sfmc_hooks_shared.dll动态加载。 - MinHook 用嵌入的修改后 MinHook.NET(新增
CreateHook(IntPtr, IntPtr)原生 detour 重载),见third_party/Minhook.NET/(BSD-3)。
| 码 | 含义 |
|---|---|
0 |
检测到游戏窗口(主菜单在渲染;watchdog 激活路径) |
3 |
游戏自行退出(当前实际路径,见限制) |
42 |
watchdog 超时(180 s 未检测到窗口) |
100 |
容器含非 Store 条目,拒绝启动 |
101 |
容器 zip 结构损坏 / 解析失败 |
- .NET 10 SDK(csproj 目标
net10.0,NativeAOT publish) - cmake(PATH 中,或 VS 内置 CMake)
- Visual Studio 构建工具(MSVC,编译 native_hooks 与 AOT link)
| Target | 作用 |
|---|---|
Build |
dotnet build -p:PublishAot=false 主工程,JIT 调试链路,不参与交付 |
Native |
cmake --build native_hooks/build 产 sfmc_hooks_shared.dll |
Pack |
Minecraft/ 数据树 → artifacts\container.zip(全 Store 不压缩) |
Publish |
dotnet publish -c Release -r win-x64 产 NativeAOT 单 exe |
Append |
最终交付:Publish + Pack 后,把 zip 追加到 AOT exe 尾部 |
主命令:
# 完整交付:Publish AOT + Pack 容器 + Append 尾部拼接 -> artifacts\SingleFileMc.exe
.\build.ps1 Append
# 仅 JIT 调试构建(容器验证链路)
.\build.ps1 Build
# 仅编译 native 守卫库
.\build.ps1 Native
# 帮助
.\build.ps1 --help首次构建顺序(native 静态库是 AOT 链接的硬依赖):
cmake -S native_hooks -B native_hooks/build # 先配置一次(生成 sfmc_hooks_static.lib 目标)
.\build.ps1 Native
.\build.ps1 Append源数据树 SingleFileMc\Minecraft\:
.minecraft\… -> zip 顶层 minecraft\…
<jdk顶层>\… -> zip 顶层 openjdk\… (jdk 顶层动态发现:含 bin/server/jvm.dll)
PCL\ 与 Plain Craft Launcher 2.exe 等 -> 排除
容器 zip 顶层只有 openjdk/ 与 minecraft/,未知顶层目录直接报错,防止静默丢数据。
SingleFileMc/
├─ build.ps1 # NUKE 构建引导脚本
├─ LICENSE # GNU GPL v3(主工程 + native 守卫库)
├─ SingleFileMc.slnx # 解决方案
├─ SingleFileMc/ # 宿主主工程(net10.0 / NativeAOT)
│ ├─ Program.cs # 入口:容器 Init -> 预热 -> hook 安装 -> JNI -> MC 启动
│ ├─ Container.cs # 尾部 zip 容器:mmap + EOCD + 手动解析 + Store 校验
│ ├─ FakeFileSystem.cs # ntdll hook VFS:Z:\ + 假句柄 + 假 SEC_IMAGE + 19 hooks
│ ├─ McLaunch.cs # 启动链:版本 json -> 类路径 -> natives 虚拟化 -> Client.main
│ ├─ Minecraft/ # 游戏数据树(打包进容器;.minecraft/ 与 jdk-25.0.4.7-hotspot/)
│ └─ TestFS/ # VFS 相关测试
├─ native_hooks/ # C 守卫 stub 库(CMake)
│ └─ src/ntdll_hooks.c # Stub_* 前置分流 + Suppress + SetCallbacks 绑定
├─ build/ # NUKE 构建管线(Build.cs)
├─ third_party/Minhook.NET/ # 嵌入的修改后 MinHook.NET(BSD-3,含 LICENSE)
├─ artifacts/ # 交付物 + gameDir
│ ├─ SingleFileMc.exe # 最终交付物(AOT 宿主 + 尾部 zip)
│ ├─ container.zip # 打包中间产物
│ └─ game/ # gameDir(存档 / mods / config 持久)
├─ .omo/docs/PHASE*.md # 阶段演进文档(见下)
├─ tools/ # spike 验证工程(spike-jvm / spike-secimage 等)
├─ docs/spike-results.md # spike 结论汇总
└─ logs/ # 运行日志
- gameDir 是真实目录:存档/配置持久保存是特性;但容器内打包的 mods 不会自动进入
gameDir,要装 mod 请直接放进
artifacts\game\mods\。 - natives 已完全虚拟化(PHASE18):natives 提取与运行期 JNA/LWJGL/Netty 写入全走虚拟
Z:\cache\natives\内存区(提取直写 + 运行期经NtWriteFilehook;LoadLibrary 从虚拟区 经假 SEC_IMAGE 加载),真实game\cache零 natives、%TEMP%零残留。仅该子树可写, 其余Z:\只读;可写句柄拒读、只读句柄拒写(句柄级 + 文件级互斥)。 - JNA
File.createTempFile在虚拟区不可用(PHASE18 已知限制):MS JDK 25 的临时文件 创建链绕过部分 detour,仅造成 SystemReport/OSHI 路径的 log4j WARN(装饰性,非致命), 游戏照常启动到主菜单。 - 离线单机:Realms 登录、微软账号、多人联机登录均不可用;离线身份由 exe 文件名决定。
- watchdog 当前被注释(
McLaunch.cs中t.Start()被注释):处于手动测试模式, 进程持续到玩家关闭游戏窗口,之后以退出码 3 结束;窗口检测 / 超时路径(0 / 42) 逻辑保留,恢复后即可自动退出。 - 固定 x64 / Windows:无跨平台、无 32 位支持。
- 体积即成本:全 Store 不压缩是"零解压"的前提,代价是容器大小 ≈ 原始数据树。
- 主工程(
SingleFileMc/宿主)+ native 守卫库(native_hooks/):GNU GPL v3, 全文见根目录LICENSE。 - 嵌入的第三方库保持各自许可:MinHook.NET(
third_party/Minhook.NET/)为 BSD-3, 许可证保留于third_party/Minhook.NET/LICENSE(BSD-3 与 GPL-3.0 兼容,GPL 项目可包含 BSD 组件)。
分发 / 修改本项目请遵守 GPL-3.0 条款,参见 GNU General Public License v3.0。