问题描述
在开发 ChinaDNS-NG 2.0 的过程中,遇到一个 MIPS 软浮点目标的链接器错误,zig 版本是 0.10.1。
1
2
3
4
5
6
$ zig build clean-all && zig build -Dtarget=mips-linux-musl -Dcpu=mips32+soft_float --verbose
...
error: ld.lld: /root/chinadns-ng/zig-cache/o/8a51af63d061459702b0b7df54c14aa6/crti.o: floating point ABI '-mdouble-float' is incompatible with target floating point ABI '-msoft-float'
error: ld.lld: /root/chinadns-ng/zig-cache/o/90b58999351700493837db929be7e2bb/libc.a(/root/chinadns-ng/zig-cache/o/ab26fe2cec5f22f6ca91dc1fa8c110b6/restore.o): floating point ABI '-mdouble-float' is incompatible with target floating point ABI '-msoft-float'
error: ld.lld: /root/chinadns-ng/zig-cache/o/b52ce46b6c877a6adf627c116590f665/crtn.o: floating point ABI '-mdouble-float' is incompatible with target floating point ABI '-msoft-float'
error: chinadns-ng:mips-linux-musl:mips32+soft_float...
大概意思是:在链接时,不能混用 软浮点ABI、硬浮点ABI 的 object。
lld 提示的这几个 object,是 musl libc 里面的几个汇编源文件产生的。
相关 issue
https://github.com/ziglang/zig/issues/11829
zig 在编译 .s、.S 汇编源文件时,没有给汇编器传递 -msoft-float
标志,导致相关 obj 用的是硬浮点。
相关源码
https://github.com/ziglang/zig/blob/0.10.1/src/Compilation.zig#L4457
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.assembly => {
// The Clang assembler does not accept the list of CPU features like the
// compiler frontend does. Therefore we must hard-code the -m flags for
// all CPU features here.
switch (target.cpu.arch) {
.riscv32, .riscv64 => {
// ...
},
else => {
// TODO
},
}
if (target_util.clangAssemblerSupportsMcpuArg(target)) {
if (target.cpu.model.llvm_name) |llvm_name| {
try argv.append(try std.fmt.allocPrint(arena, "-mcpu={s}", .{llvm_name}));
}
}
}
注释说的很明白了,C/C++ 编译器前端 允许通过 -Xclang -target-feature -Xclang +/-CPU特征
参数来传递 CPU 特征信息,但 汇编器 不接受这种参数,因此需要手动硬编码相关 -m 编译标志。
从源码能看出,zig 0.10.1 只处理了 riscv32/64 目标的 -m 标志(奇怪的是没看到 x86、arm 目标的特殊处理,可能是不需要?),mips 的没有处理。
查看上面那个 issue,可以看到作者在今年 4 月 19 号合并了修复此问题的一个 PR,让我们看看 PR 的修改:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
switch (target.cpu.arch) {
.riscv32, .riscv64 => {
// ...
},
// 这一段就是 PR 新增的内容,处理了软浮点
.mips, .mipsel, .mips64, .mips64el => {
if (target.cpu.model.llvm_name) |llvm_name| {
try argv.append(try std.fmt.allocPrint(arena, "-march={s}", .{llvm_name}));
}
if (std.Target.mips.featureSetHas(target.cpu.features, .soft_float)) {
try argv.append("-msoft-float");
}
},
else => {
// TODO
},
}
解决方法
升级到最新的 zig 版本。
其他方法
但对我来说,升级 zig 不可行,因为 zig 0.10.1 之后的版本还不支持 async 异步功能,这是由于自托管编译器的开发导致的,因为要处理的东西很多,所以 async 功能暂时被搁置了。
自托管编译器在 0.10.0 版本首次亮相,0.10.1 是 0.10.0 的错误修复版本,本身没有功能性修改。在 0.10 中,默认启用自托管编译器,但允许使用 -fstage1
切换回 Bootstrap 编译器,这样就能愉快的使用 async 了。
但是 0.11 版本已经删除了 Bootstrap 编译器相关源码,因此不支持 -fstage1
参数了。
首先想到的是从源码构建 zig 0.10.1,并打上 PR 补丁,但因为 Bootstrap 编译器是 C++ 写的,还有 Clang、LLVM 等一堆库,还开了 LTO 优化,尝试了好几次都因为 OOM 编译失败了,即使是 32G 内存也没能顶住,遂放弃。
然后我就换了个思路,看看能否在不重新编译 zig 的前提下,让其支持 mips 软浮点目标的正确编译。
看了下 zig 源码,发现 zig 在编译 C、C++、汇编 等源文件时,是通过调用 zig 自己的 clang 子命令来实现的。
那问题来了,zig 在运行时是如何找到 zig 自己的可执行文件路径的呢?这里只考虑 Linux,答案很简单,那就是读取 /proc/self/exe
软链接文件。
如果可以通过某种手段,让 zig 在运行时读到的 zig_exe_path 是指向我自己的一个程序,然后在这个程序中针对 clang 子命令的参数做特殊处理,如果是 mips 软浮点目标,并且是汇编源文件,那么就追加 -msoft-float
等标志,然后再调用真正的 zig clang 命令,问题就解决了。
没错,这就是我这篇笔记的主要目的,下面说下具体的操作过程。
修补zig可执行文件
要让 zig 读取到的 zig_exe_path 不同,有两种办法:
- 如果 zig 是动态链接的:可以通过 LD_PRELOAD 环境变量来钩住 zig 对
readlink
C 库函数的调用,如果发现参数是/proc/self/exe
,则返回特定的路径,否则调用原始的 C 库函数。 - 如果 zig 是静态链接的:我只能想到两种办法,一种方法是使用 ptrace,监视 readlink 相关系统调用,检测到
/proc/self/exe
参数时,返回特定的路径,就像 LD_PRELOAD 方式那样;另一种方法就是将 zig 可执行文件中的"/proc/self/exe"
字符串文字替换为其他路径(为确保不会出问题,新字符串长度要与原字符串相同),在新路径上建立软链接,指向我们的一个包装程序。
因为我的 zig 可执行文件是静态链接到 musl 的,而又因为 ptrace 方式太复杂,所以我选择第二种方法。
1
2
3
4
5
6
7
8
9
10
cd /opt/zig-linux-x86_64-0.10.1
# 复制一份,避免修改原文件
cp -af zig zig_
# 新路径随意,确保字符串长度相同就行
sed -i 's@/proc/self/exe@/opt/zig123456@g' zig_
# 建立软链接,指向我们的包装程序 (这里我用 zig_.sh)
ln -snf $PWD/zig_.sh /opt/zig123456
编写包装脚本zig_.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/bin/bash
argv=("$@")
cmd="${argv[0]}"
file="${argv[1]}"
log() {
# 注意,必须重定向到一个文件,不能打印到 stdout,防止破坏 zig 内部逻辑
echo "$@" >>/root/chinadns-ng/log.txt
}
#log zig_ "${argv[@]}"
# 如果是目标是mips,并且编译的是汇编源文件,那么就追加参数
if [ "$MIPS_M_ARCH" ] && [ "$cmd" = clang ] && [[ "$file" == *.s || "$file" == *.S || "$file" == *.sx ]] && [[ "${argv[*]}" == *" -target mips"* ]]; then
argv+=("-march=$MIPS_M_ARCH") # -march参数,指定微架构级别
((MIPS_SOFT_FP)) && argv+=("-msoft-float") # 如果是软浮点,则追加-msoft-float
log zig_ "${argv[@]}" # 打印一下,验证替换结果是否正确
fi
# 调用真正的 zig 程序,注意是 zig_,修补过的二进制文件
exec zig_ "${argv[@]}"
创建其他软链接
1
2
3
4
5
6
7
8
cd /opt/zig-linux-x86_64-0.10.1
# 把 zig_ 添加到 PATH
ln -snf $PWD/zig_ /usr/local/bin/zig_
# 把 zig_.sh 添加到 PATH
# 这里我就使用 zig 这个名字了
ln -snf $PWD/zig_.sh /usr/local/bin/zig
验证一下
还是用开头的那个命令,测试下,是否正常编译、链接、运行:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
$ zig build clean-all && MIPS_M_ARCH=mips32 MIPS_SOFT_FP=1 zig build -Dtarget=mips-linux-musl -Dcpu=mips32+soft_float --verbose
$ file zig-out/bin/chinadns-ng:mips-linux-musl:mips32+soft_float
zig-out/bin/chinadns-ng:mips-linux-musl:mips32+soft_float: ELF 32-bit MSB executable, MIPS, MIPS32 version 1 (SYSV), statically linked, stripped
$ readelf -A zig-out/bin/chinadns-ng:mips-linux-musl:mips32+soft_float
MIPS ABI Flags Version: 0
ISA: MIPS32
GPR size: 32
CPR1 size: 0
CPR2 size: 0
FP ABI: Soft float
ISA Extension: None
ASEs:
None
FLAGS 1: 00000001
FLAGS 2: 00000000
Static GOT:
Canonical gp value: 000597a0
Reserved entries:
Address Access Value
000517b0 -32752(gp) 00000000
000517b4 -32748(gp) 80000000
Local entries:
Address Access Value
000517b8 -32744(gp) 00041574
000517bc -32740(gp) 0002b184
000517c0 -32736(gp) 00041548
000517c4 -32732(gp) 0003179c
$ qemu-mips-static -cpu 4Kc zig-out/bin/chinadns-ng:mips-linux-musl:mips32+soft_float -V
ChinaDNS-NG 2023.10.28 | openssl-3.2.0 | target:mips-linux-musl | cpu:mips32+soft_float | mode:fast | <https://github.com/zfl9/chinadns-ng>