news 2026/10/3 22:35:01

如何看懂 Madeira 中 Wine 的 PE/Unix 分离架构:ntdll、kernel32 与 wineserver 协作全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
如何看懂 Madeira 中 Wine 的 PE/Unix 分离架构:ntdll、kernel32 与 wineserver 协作全解析

如何看懂 Madeira 中 Wine 的 PE/Unix 分离架构:ntdll、kernel32 与 wineserver 协作全解析

【免费下载链接】MadeiraRun x86-64 Windows PC games on jailed iOS via FEX-Emu + Wine + DXMT项目地址: https://gitcode.com/GitHub_Trending/mad/Madeira

Madeira 是一个无需越狱即可在 iPhone 上运行 x86-64 Windows PC 游戏的开源项目,其核心秘诀在于 Wine 的PE/Unix 分离架构:Windows 那一侧(PE 侧)的 ntdll、kernel32 等 DLL 以"纯翻译"方式运行,而真正干活的内核层(Unix 侧)以线程形式塞进同一个 iOS 进程。本文带你从零看懂这套 ntdll、kernel32 与 wineserver 三方协作的完整设计。

一、为什么必须"分家":iOS 的三条铁律

普通 Wine 跑在 Linux 上时,wineserver是一个独立进程,Windows 程序是真正的子进程。但 iOS 把这条路全堵死了:

iOS 限制对 Wine 的影响Madeira 的对策
App 不能启动其他程序wineserver 不能独立成进程wineserver 改造成一个线程(WineServerBridge.h)
沙盒内 Unix Domain Socket 不可靠ntdll 与 wineserver 之间没法走常规 socket用socketpair()造一根"内存电话线"
所有"Windows 进程"其实是伪进程多个游戏进程共享一个 Mach 任务每个"进程"只是一组线程 + 各自的 TEB

一句话概括:PE 侧负责"像 Windows",Unix 侧负责"干 Unix 的活",wineserver 负责"当裁判"。

二、架构总览:谁住在哪一侧

┌──────────────────────── 单个 iOS App 进程 ────────────────────────┐ │ │ │ PE 侧(Windows API 层) Unix 侧(真正的实现) │ │ ┌────────────────────────┐ ┌─────────────────────────────┐ │ │ │ kernel32.dll (Win32 API)│ │ ntdll 的 unix 实现 │ │ │ │ ntdll.dll(内核边界) │─────▶│ virtual_ios / env_ios 等 │ │ │ │ user32 / gdi32 / ... │ │ wineserver 线程(事件循环) │ │ │ │ 由 FEX 实时翻译成 ARM64 │ │ 全部是原生 ARM64 代码 │ │ │ └────────────────────────┘ └─────────────────────────────┘ │ │ ▲ ▲ │ │ └── FEX-Emu JIT 池(RX/RW 双映射)──┘ │ └────────────────────────────────────────────────────────────────────┘
  • PE 侧:ntdll.dll、kernel32.dll 这些是真正的 Windows PE 二进制,放在 App 包的 arm64ec-windows/ 和 aarch64-windows/ 两个"DLL 农场"里。游戏的 x86-64 代码和这些 DLL 一起由 FEX-Emu 边跑边翻译成 ARM64(接口见 FEXBridge.h)。
  • Unix 侧:ntdll 里那部分"必须接触操作系统"的代码被单独实现为 C 代码(虚拟内存、环境变量、进程/线程),原生 ARM64 运行,不经过 FEX——这是分离架构最大的性能红利。
  • wineserver:Wine 的"中央调度员",负责对象所有权、跨进程消息、注册表等。在 Madeira 里它是一个被改名wineserver_main()后跑在线程上的静态库。

三、启动顺序:一场精心编排的接力赛

读懂这套架构,最好的方式是按时间线看一次冷启动。全部逻辑集中在 WineProcessBridge.m 和 WineServerBridge.m 两个文件中:

步骤动作为什么必须这样
1️⃣解压前缀模板(prefix template),铺好注册表wineserver 的init_registry()启动毫秒级就跑,若system.reg还没落盘,它会用空注册表覆盖模板里 17479 个键——所以播种必须先于服务器(madeira_seed_prefix_if_needed)
2️⃣创建 wineserver 线程,跑wineserver_main(--foreground)日志重定向到madeira-log.txt;fatal_error被重写为pthread_exit——原来会exit(1)杀掉整个 App
3️⃣创建socketpair(),一端注入 wineserver,另一端写入环境变量WINESERVERSOCKET绕过 iOS 上坏掉的 Unix socketaccept():ntdll 的server_connect()检查这个变量,直接拿到"已连接"的 fd
4️⃣创建 Wine 进程线程,读 PE 头判断目标位数madeira_pe_machine()从磁盘读MZ → e_lfanew → PE\0\0里的 Machine 字段,决定选哪个 DLL 农场
5️⃣把农场里的 DLL软链接进drive_c/windows/system32符号链接指向 App bundle,重装后路径变了,链接会自动重建(自愈)
6️⃣调用__wine_main(argc, argv),外面套一层setjmpntdll 的 iOS 出口 shim 用 longjmp 回到这里,实现"干净的进程退出"而不是exit()

其中第 3 步是整个架构里最"妙"的一笔:

// 摘自 WineProcessBridge.m:用 socketpair 替代传统 socket 连接 int pair[2]; socketpair(AF_UNIX, SOCK_STREAM, 0, pair); setenv("WINESERVERSOCKET", fd_str, 1); // ntdll 侧:直接拿 fd wineserver_inject_client_fd(pair[0]); // 服务器侧:事件循环捡走它

ntdll 拿到WINESERVERSOCKET后就不走server_connect(),直接在这根"电话线"上发第一个create_process请求——于是 PE 侧的加载器和 Unix 侧的 wineserver 在同一个进程里完成了握手。

四、ntdll:站在悬崖边的"双面人"

ntdll 是 Wine 里最特殊的模块,因为它正好骑在 PE/Unix 分界线上:

  • PE 侧的 ntdll.dll:导出NtCreateFile、NtWaitForSingleObject等Nt*/Zw*系统调用,是 kernel32 之下、系统之上的唯一入口。游戏调 kernel32 → kernel32 调 ntdll。
  • Unix 侧的实现:真正的活儿(mmap、线程、信号量)由静态链入的build/ntdll-unix/*_ios.c完成,对外只暴露__wine_main()这个入口(见 WineProcessBridge.m 第 285 行附近的声明)。

这种"一个模块、两种人格"的设计正是分离架构的精髓:PE 侧保持原汁原味的 Windows 调用链,Unix 侧随时可以针对 iOS 内核(XNU/Mach)定制优化,两边只在 ntdll 这一条缝上通信。

五、kernel32 与 DLL 农场:Windows 的"目录重定向"

kernel32.dll 提供CreateProcess、LoadLibrary、ReadFile这些经典 Win32 API。Madeira 里它和同门的 user32、gdi32 等一起住在 App 包里,靠软链接农场摆上"Windows 的货架":

农场目录内容何时使用
arm64ec-windows/ARM64EC 混合模式系统 DLL(如madeira_d3d12.dll)真游戏(x86-64 目标)
aarch64-windows/纯 ARM64 原生 Windows 模块ARM64 原生测试程序
i386-windows/32 位 WoW64 模块32 位游戏
x86_64-vcruntime/微软真版 VC++ 运行时覆盖 Wine 的 stub 实现

启动时wine_process_thread()会把选定农场的全部 DLL 链接进system32,同时维护sysx64/sysaa64两个镜像农场,让"64 位启动器拉起 32 位子进程"这类混合场景也能按 Win32 路径正确解析到对应架构的二进制。

💡 小提示:为什么 ntdll 在"系统 DLL"里地位如此特殊?因为它是每个进程的第一个被加载的模块,加载器本身也用它。谁先有、谁加载谁——这就是为什么 ntdll 必须同时存在于 PE 侧和 Unix 侧。

六、wineserver:为什么必须跑在最高优先级线程上

wineserver 是 Wine 的"系统服务",PE 侧每个对象等待、消息泵调用都要和它往返一次。这里有一个很实在的工程教训(WineServerBridge.m 中有完整注释):

  • 早期版本:wineserver 线程被降优先级,"免得挤占主线程"。
  • 后果:游戏线程在每个 55ms 帧里大部分时间停在read_reply_data等回复——教科书级的优先级反转。
  • 修正:改用QOS_CLASS_USER_INTERACTIVE,让服务器线程至少和客户一样"热"。

此外还有两处关键的"保命"改造:

  1. fatal_error()被重定义为pthread_exit(NULL)——wineserver 崩了只死一个线程,App 还活着;
  2. 游戏退出后必须主动wineserver_stop()并 join 线程,否则空转的服务器会因"CPU 占用过高"被 iOS 杀掉。

七、WoW64 彩蛋:32 位游戏如何各占 4GB

32 位 x86 程序在 ARM64 上跑面临一个根本矛盾:XNU 给每个进程 4GB 的__PAGEZERO,而经典 WoW64 假设"32 位指针就是真实地址"。Madeira 的解法是guest window(详见 docs/WOW64.md):

  • 每个 32 位伪进程独占一段 4GB 对齐的宿主区间[B, B+4GB),x86 代码看到的地址a实际落在B + a;
  • Wine 自己的wow64.dll以原生 aarch64 运行(不翻译),只有游戏自身的 x86 代码交给 FEX 的 WOW64 模块xtajit.dll;
  • 基准B通过私有NtQueryInformationProcess查询下发——又是一个 ntdll 跨界的例子;
  • 不跑 32 位程序时什么都不预留(lazy windows),小内存设备也不会白白损失地址空间。

八、退出路径:用 longjmp 模拟"进程死亡"

iOS 上一个线程不能调用exit()(会杀死整个 App)。ntdll 的 iOS shim 给每个"进程"线程准备了线程局部的jmp_buf:

  • 游戏进程调用ExitProcess→ Unix 侧longjmp回到 WineProcessBridge.m 里__wine_main()外层的setjmp;
  • 若是 NTSTATUS 错误(0xC...),ntdll 还会回调wine_launched_process_did_exit(),把崩溃码上报给 App 前端展示;
  • 主线程退出后清掉 pthread TSD 里的 TEB 镜像,避免线程销毁时触发 Objective-C 析构踩雷。

至此,一次完整的"进程生命周期"——启动、运行、退出——全部在单进程、多线程、无子进程的约束下闭环了。

九、总结:分离架构的收益与代价

维度PE/Unix 分离带来的收益代价
性能内核路径(内存、线程、同步)走原生 ARM64,零翻译开销ntdll 必须维护两套实现
兼容性PE 侧保持标准 Wine DLL,社区修复可直接复用所有跨边界通信都要自己设计(socketpair、NtQuery 私有类)
可移植性iOS 定制只集中在 Unix 侧文件单进程多"进程"模型带来优先级反转、TSD 等微妙问题
可维护性日志统一进madeira-log.txt,崩溃可归因到线程调试复杂度显著高于原生 Wine

如果你想继续深入,建议按这个顺序读源码:先看 WineServerBridge.m(最短,线程化 wineserver 的全貌),再看 WineProcessBridge.m 的wine_process_thread()(DLL 农场 + 启动编排),最后配合 docs/WOW64.md 理解 32 位路径。整个架构没有一行"魔法",每一条别扭的设计(socketpair、longjmp、软链接农场)背后都对应着 iOS 的一条具体限制——这正是读懂 PE/Unix 分离架构的钥匙。

【免费下载链接】MadeiraRun x86-64 Windows PC games on jailed iOS via FEX-Emu + Wine + DXMT项目地址: https://gitcode.com/GitHub_Trending/mad/Madeira

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 22:34:20

merge 为什么有时只是移动指针,有时会多一个 commit

merge 为什么有时只是移动指针,有时会多一个 commit 前面讲 checkout 和 reset 时,我们一直围绕几样东西看: HEAD:当前站在哪里。分支引用:refs/heads/master 这种名字指向哪个 commit。index:下一次提交准…

作者头像 李华
网站建设 2026/10/3 22:23:02

企业AI Agent定制:把万元当元来比,错在哪里?

一家企业的经营分析岗让助手比较两个区域上半年的费用。助手很快给出结论,说A区比B区高出三成。同事看着数字觉得不对,回去翻原始报表才发现,A区的单位是万元,B区是元,两个数被直接放在一起比了。数字都是从系统里取出…

作者头像 李华
网站建设 2026/10/3 22:17:12

ai编程工具接入TaoToken:统一Key与API通道的配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 22:16:24

MetaERP 的 AP 模块在跨法人结算里,不是“A 公司付外部供应商”那种普通应付,而是把“集团内一个法人欠另一个法人”也当成应付对象来处理,并且和 AR、资金、合并报表打通。核心思路是:内部交

MetaERP 的 AP 模块在跨法人结算里,不是“A 公司付外部供应商”那种普通应付,而是把“集团内一个法人欠另一个法人”也当成应付对象来处理,并且和 AR、资金、合并报表打通。核心思路是:内部交易有唯一源头 → 双边自动派生应收/应…

作者头像 李华