0%

【Linux技术分享】LoongArch64 原生 Wine 的 PE 编译与运行链路

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
2
3
4
5
6
7
8
9
10
clang --target=loongarch64-windows
│ ① 目标是否合法、wchar_t/size_t 是否符合 Windows
▼
.o(COFF,IMAGE_FILE_MACHINE=0x6264)
│ ② 节、重定位、CALL36、.pdata/.xdata 是否写得出
▼
lld-link → .exe(PE32+)
│ ③ 导入库是否解析出 __imp_xxx、异常表是否排序
▼
可执行

任何一层缺失,报错形态分别是「unknown target」「unsupported relocation」「undefined symbol __imp_*」。


二、交互图

1. 编译期:从源码到 PE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
源文件 (.c)
│ clang -target loongarch64-windows
│ ├─ TargetInfo:wchar_t=16bit、SizeType=unsigned long long、无 MSVC 名字修饰
│ └─ 后端:LP64 调用约定($a0-$a7 / $fa0-$fa7,返回 $a0/$fa0)
▼
COFF .obj
│ 节:.text/.data/.rdata/.pdata/.xdata
│ 重定位:CALL36 / PAGE32 / ADDR64 …
▼
lld-link
│ ├─ 识别 Machine=0x6264(LoongArch64)
│ ├─ .pdata 按 RVA 排序(异常分发查表要求)
│ └─ 解析 __imp_xxx(来自 Wine 构建产出的 dlls/*/lib*.a)
▼
PE32+ (.exe/.dll)

2. 运行期:从 PE 入口到进程退出

1
2
3
4
5
6
7
8
9
10
11
12
13
ntdll/loader.c  load_start_exe()
│ 映射 PE、重定位、解析导入
▼
mainCRTStartup (crt/crt0.S)
│ 保存 $ra → bl main → 恢复 $ra → jr $ra
▼
main()
│ 返回码在 $a0
▼
BaseThreadInitThunk
│ RtlExitUserThread( entry( arg ) )
▼
进程退出(退出码 = $a0)

3. 异常路径:Windows SEH 落到 Linux 信号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
用户态异常(除零/非法访问)
│
▼
内核对 LoongArch 投递 SIGSEGV/SIGFPE
│
▼
signal_loongarch64.c segv_handler()
│ sigcontext_to_context() ← 结构体需与内核 ucontext 对齐
▼
call_seh_handlers() ← 遍历 SEH 链,直到 StackBase
│
▼
RtlVirtualUnwind2() ← 读 .xdata 解码 unwind code、推进 sp/pc
│
▼
__C_specific_handler / handler 返回「继续执行」

三、代码与参数说明

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
2
// 只有 ELF 才需要处理 relaxation 相关的伪符号
if (!isCOFF()) { /* 原有 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    .text
.globl mainCRTStartup
.p2align 2
.seh_proc mainCRTStartup
mainCRTStartup:
addi.d $sp, $sp, -16
.seh_stackalloc 16
st.d $ra, $sp, 8
.seh_savereg $ra, 8
.seh_endprologue
bl main
ld.d $ra, $sp, 8
addi.d $sp, $sp, 16
jr $ra
.seh_endproc

三个要点:

  1. **bl 会覆盖 $ra**。如果写成 bl main; ret,ret(即 jirl $r0,$ra,0)会跳回 main 的下一条指令,形成自旋——实测 gdb 采样 PC 恒为 0x140001004。
  2. **返回码在 $a0**。BaseThreadInitThunk 的实现是 RtlExitUserThread( entry( arg ) ),只要 CRT 不破坏 $a0,退出码就能正确传出。
  3. .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
2
3
#4 free
#5 find_builtin_dll (loader.c:1243)
#6 load_start_exe (loader.c:1502)

修复方式是把两处常量(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 上构建。