LoongArch64 原生 Wine 的 PE 编译与运行链路
本文说明在 LoongArch64 上不依赖任何 x86 翻译层运行 Windows 程序的实现路径:从 clang 产出 LoongArch 机器码的 COFF 目标文件开始,到 lld-link 链接出 PE、原生 Wine 加载执行、异常展开,再到两类典型卡死的定位方法。文章分四个部分:先总述为什么必须自建三层能力,再给出编译期与运行期的交互图,然后逐段说明关键代码与参数,最后总结踩过的坑与当前边界。
一、总述
1. 目标与前提
常见方案(如 latx、box64、Hangover)是在 LoongArch 上模拟 x86,让 32/64 位 x86 版 Wine 跑起来。本文走的是另一条路:让 Wine 本身以 LoongArch 编译,PE 文件里直接就是 LoongArch 机器码,运行时没有指令翻译开销。
要做到这一点,必须先补齐三层上游不具备的能力:
| 层次 | 上游状态 | 本次补齐 |
|---|---|---|
| LLVM 目标层 | 只能为 LoongArch 生成 ELF | MC 层 COFF 写出、lld-link 支持、导入 thunk、.pdata 排序、PE unwind 编码、Windows 目标信息 |
| CRT 层 | 无 loongarch64 版 mingw CRT | 手写 mainCRTStartup,保存 $ra 并携带 .seh_* 展开信息 |
| Wine 层 | 无 LoongArch 架构后端 | 信号/上下文、栈展开、__C_specific_handler、PE 加载与运行期修复 |
2. 一个硬约束
Wine 的 unix 侧(ntdll/unix、wineserver、图形驱动)是本机 ELF,必须链接龙的 glibc 与 X11。
因此 Wine 只能在 loongarch64 机器上构建,x86 机器最多只能产出编译器工具链(clang/lld 本身是跨平台的,但产物跑不了)。
3. 编译期的三个“卡点”顺序
1 | clang --target=loongarch64-windows |
任何一层缺失,报错形态分别是「unknown target」「unsupported relocation」「undefined symbol __imp_*」。
二、交互图
1. 编译期:从源码到 PE
1 | 源文件 (.c) |
2. 运行期:从 PE 入口到进程退出
1 | ntdll/loader.c load_start_exe() |
3. 异常路径:Windows SEH 落到 Linux 信号
1 | 用户态异常(除零/非法访问) |
三、代码与参数说明
3.1 clang 侧:Windows 目标与 COFF 写出
① 目标信息(clang/lib/Basic/Targets/LoongArch.h)
Windows 目标与 Linux 目标的差异集中在三处:
| 项 | Linux(上游) | Windows(新增) |
|---|---|---|
wchar_t |
4 字节(int) |
2 字节(unsigned short),与 MSVC 一致 |
size_t |
unsigned long |
采用 MSVC 风格,避免 C++ 头文件类型冲突 |
| 名字修饰 | Itanium | 不做 MSVC 修饰,否则 winebuild 生成的导出名对不上 |
② 目标文件写出(llvm/lib/MC/WinCOFFObjectWriter.cpp)
LoongArch 后端原有的链路松弛(relaxation)分支会假设 ELF 语义,直接开 COFF 会在空符号上崩溃,因此加了格式判断守卫:
1 | // 只有 ELF 才需要处理 relaxation 相关的伪符号 |
重定位映射补了 CALL36(LoongArch 的 36 位调用重定位),并在 MCAssembler 中支持 COFF 的隐式加数。
③ .pdata/.xdata(llvm/lib/MC/MCWin64EH.cpp)
PE 异常依赖 .pdata 数组,每项 8 字节(BeginAddress / UnwindInfoAddress),必须按 RVA 升序,否则 RtlLookupFunctionEntry 二分查找失败。lld/COFF/Writer.cpp 中对 LoongArch 做了与 ARM64 相同的排序处理。
Win64EH::LoongArchUnwindEmitter 负责编码:
| unwind code | 含义 | 本次实现 |
|---|---|---|
UOP_StackOffset |
addi.d $sp,$sp,-N |
支持 |
UOP_SaveR64 |
st.d $r, $sp, off |
支持 |
UOP_End |
序言结束 | 支持 |
| epilogue | 收尾指令不在序言内 | 记录 epilogue 范围,供展开器识别 |
当前只为无栈调整的叶子函数与带 .seh_* 标注的序言生成条目;没有标注的手写汇编帧不生成,这也埋下了后文「自旋」的伏笔。
3.2 CRT 侧:mainCRTStartup 与 $ra
crt/crt0.S 全文只有十几行,但它决定了进程能否退出:
1 | .text |
三个要点:
- **
bl会覆盖$ra**。如果写成bl main; ret,ret(即jirl $r0,$ra,0)会跳回main的下一条指令,形成自旋——实测gdb采样 PC 恒为0x140001004。 - **返回码在
$a0**。BaseThreadInitThunk的实现是RtlExitUserThread( entry( arg ) ),只要 CRT 不破坏$a0,退出码就能正确传出。 .seh_*不是装饰。异常从main内部穿过这一帧时,展开器要靠它恢复$ra与sp;缺失会导致展开链不前进。
3.3 Wine 侧:从信号到展开
① signal_loongarch64.c:上下文转换
内核 ucontext_t 里的 sigcontext 布局与 Wine 的 CONTEXT 不同,转换函数按字段逐个搬运,两层结构体的偏移必须与内核头一致(等价于 x86 的 ucontext_i.h)。这里一旦错位,表现是寄存器随机化、sp 变成非法值。
② unwind.c:RtlVirtualUnwind2
展开器按 .pdata 找到 RUNTIME_FUNCTION,再解码 .xdata 里的 unwind code:
1 | if (!context->Gpr[1]) return STATUS_BAD_FUNCTION_TABLE; /* 叶子帧,$ra 无意义 */ |
context->Gpr[1] 即 $ra。叶子函数在 .xdata 中被标记为无 $ra 槽位,展开时不再恢复返回地址;如果错误地把它当成非叶子函数,就会反复恢复到同一个 pc,表现为 +unwind 日志中 pc/sp 完全不变地重复打印。
③ loader.c:一个长度算术引发的崩溃
Wine 11.16 新增的 find_builtin_dll() 用固定长度估算路径缓冲区:
1 | maxlen = strlen(bindir) + strlen(pe_dir) + strlen(name) + sizeof("/aarch64-windows"); |
/aarch64-windows 是上游最长的架构目录名(17 字节),而 /loongarch64-windows 是 20 字节。prepend 越界写 3 字节,恰好写坏堆块头,几帧之后在 free() 处报:
1 | #4 free |
修复方式是把两处常量(PE 目录、unix 目录)改成 loongarch64-* 的实际长度。
这类问题的特征是崩溃点与根因相距很远,只能靠 backtrace 定位。
④ libs/compiler-rt:128 位 long double
LoongArch 上 long double 是 128 位,ffmpeg 的 avsscanf.c 会引用 __floatunsitf、__multf3、__addtf3。
Wine 自带的 compiler-rt 子集只包含整数 builtins,因此链接期报未定义。
补法是引入 addtf3.c、multf3.c、divtf3.c、floatunsitf.c 等一组源文件及 fp_*.inc 依赖,并加入 libs/compiler-rt/Makefile.in。
四、两类卡死与定位方法
| 现象 | 根因 | 定位手段 | 修复 |
|---|---|---|---|
| 所有程序退出即卡死,CPU 100% | bl main; ret 使 $ra 自指 |
gdb 附上后采样 PC,恒为同一地址 |
保存/恢复 $ra |
printf 输出即卡死 |
缺少 -D_UCRT,FILE 布局与 ucrtbase 不匹配,抛 c0000008 后展开死循环 |
WINEDEBUG=+relay,+seh,+unwind,看到 pc/sp 不变的重复 RtlVirtualUnwind2 |
编译加 -D_UCRT,并给 CRT 帧补 .seh_* |
wineboot 报 double free |
loader.c 路径缓冲区长度不足 |
gdb -batch -ex run -ex bt |
按实际架构目录名计算长度 |
一条经验:PE 侧的问题先用 +relay 看最后一条返回的调用,unix 侧的问题先用 backtrace 看崩溃点前后三帧。两者混在一起时,先判断异常是否跨过了没有展开信息的汇编帧。
五、总结
1. 整条链路一句话
clang -target loongarch64-windows 产出机器码为 LoongArch 的 COFF,lld-link 按 PE 规则链接并排序 .pdata,原生 Wine 加载后由 mainCRTStartup 进入 main,返回码经 $a0 交给 RtlExitUserThread;运行期异常经 Linux 信号转为 Windows 上下文,再由 .xdata 驱动逐帧展开。
2. 三层职责
| 层 | 代表文件 | 职责 |
|---|---|---|
| LLVM | WinCOFFObjectWriter.cpp、MCWin64EH.cpp、lld/COFF/* |
把 LoongArch 指令写成 COFF,并按 PE 规则链接、排序异常表 |
| CRT | crt/crt0.S |
提供入口、维护 $ra、携带展开信息 |
| Wine | signal_loongarch64.c、unwind.c、loader.c |
信号与上下文转换、栈展开、PE 加载与运行期修复 |
3. 已验证与未完成
已验证:wineboot 正常、wine cmd /c ver 输出 Microsoft Windows 10.0.19045、notepad(721×512)与 winecfg(Wine 设置 477×608)均映射真实窗口;
控制台程序退出码与 printf 输出正确。
未完成:后台服务(services/winedevice/rpcss/plugplay)空闲时各占满一个核;dlls/rsaenh 链接 libtomcrypt.a 报重定位错误;C++ 异常与 MSVC C++ ABI 不可用;
32 位 / WoW64 链路尚未开始。
4. 复现
完整的补丁、脚本与验证步骤见工程 loongarch-wine-kit
(LLVM 13 个提交、Wine 10.14/11.16 两套补丁、5 个构建脚本)。
新机器上按 README 的顺序执行即可,唯一硬约束是 Wine 必须在 loongarch64 上构建。