最近在尝试把 Rust 写的应用交叉编译到 RISC-V 的机器上。

目标机器是 RISC-V 架构,系统使用 buildrootlibc 为 musl。

看起来我们可以直接编译一个 musl 版本的程序,就能解决,而且 rust 支持 musl 的 target

标准的 C 库实现

1973 年汤普逊和里奇用 C 语言重写了 Unix

由于整个 Unix/Linux 都是建立在 C 之上,几乎所有的程序都依赖了 libc

在 Linux 下 libc 有四个实现:

glibc, musl libc, uClibc/uClibc-ng, Bionic C

  • glibc 基本上就是事实的标准
  • musl libc 容器或者嵌入式设备,静态编译用的比较多
  • uClibc/uClibc-nguClibc已经停止维护了, uClibc-ng是新的版本)主要用于无 MMU (Memory Management Unit) 的设备,也有非常少量了 Linux 用了 uClibc(比如旧版本的 OpenWRT 和一些特殊的硬件,新版本用了 musl libc
  • Bionic C 主要用于 Android,一般不把它算在 Linux 的生态里

如何判断是否为静态编译

file

这是一个比较高级的命令,通过很多因素判断,还是非常准确的

1
2
% file $(which gcc)
/usr/bin/gcc: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=d68af36149454ddf51e22ad93db29f583c51f70b, for GNU/Linux 4.4.0, stripped

在 Linux 下的可执行文件时 ELF (Executable and Linkable Format) 格式,ELF 是一种用于可执行文件、目标代码、共享库和核心转储(core dump)的标准文件格式

我们先来写一个最简单的程序验证一下

1
2
3
4
5
6
7
cat > hello.c << EOF
#include <stdio.h>
int main() {
printf("Hello, World!\r\n");
return 0;
}
EOF
1
2
3
4
5
% gcc hello.c -o c-hello-gcc-gnu
% ./c-hello-gcc-gnu
Hello, World!
% file -b c-hello-gcc-gnu
ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=6169fdfc99a343c23b9d349da437b313e09254e3, for GNU/Linux 4.4.0, not stripped

通过 file 命令,我们可以看到 dynamically linked 标识它是动态链接,interpreter /lib64/ld-linux-x86-64.so.2 是动态链接的加载器

我们需要先了解一下,动态链接的工作流程,执行会去通过 interpreter 字段记录的位置找 ld (动态链接装载器),然后由 ld 加载 Dynamic section 里面记录动态链接

ldd 命令

我们可以通过 ldd (List Dynamic Dependencies)来找动态链接

1
2
3
4
5
6
7
8
9
% gcc hello.c -o c-hello-gcc-gnu
% ./c-hello-gcc-gnu
Hello, World!
% file -b c-hello-gcc-gnu
ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=6169fdfc99a343c23b9d349da437b313e09254e3, for GNU/Linux 4.4.0, not stripped
% ldd c-hello-gcc-gnu
linux-vdso.so.1 (0x00007b07aeba6000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007b07ae999000)
/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007b07aeba8000)

加上静态编译参数,ldd 没有发现动态链接

1
2
3
4
5
6
7
% gcc -static hello.c -o c-hello-gcc-gnu-static
% ./c-hello-gcc-gnu-static
Hello, World!
% file -b c-hello-gcc-gnu-static
ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=532829a15c17e744e340e6ab081ace670137dfd2, for GNU/Linux 4.4.0, not stripped
% ldd c-hello-gcc-gnu-static
not a dynamic executable

当然 ldd 其实非常的不靠谱,比如就没法判断这个

1
2
3
4
5
% musl-gcc hello.c -o c-hello-gcc-musl
% file -b c-hello-gcc-musl
ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-x86_64.so.1, not stripped
% ldd c-hello-gcc-musl
./c-hello-gcc-musl: error while loading shared libraries: /usr/lib/libc.so: invalid ELF header

当然这是由于 ldd 的工作原理决定的,ldd 要去执行程序,执行到 ld 的位置停下,输出动态链接的部分,如果这个程序本身是完全静态,不依赖于 ldldd 是完全无效的,对于找不到 ld 和跨架构的情况也是完全不可用

1
2
$ cat $(which ldd) | wc -l
191

ldd 本身就是个 shell 脚本,添加了几个环境变量

直接执行 LD_TRACE_LOADED_OBJECTS=1 ./c-hello-gcc-gnu 效果是一样的

1
2
3
4
% LD_TRACE_LOADED_OBJECTS=1 ./c-hello-gcc-gnu
linux-vdso.so.1 (0x00007783dc6cf000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007783dc4c7000)
/lib64/ld-linux-x86-64.so.2 (0x00007783dc6d1000)

ldd 只用来判断当前系统可以执行的动态程序用了那些动态依赖

readelf

readelf 属于 binutils 提供的工具,可以读 ELF 格式里面的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
% readelf -h c-hello-gcc-gnu 
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Position-Independent Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x1040
Start of program headers: 64 (bytes into file)
Start of section headers: 13496 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 14
Size of section headers: 64 (bytes)
Number of section headers: 30
Section header string table index: 29

我们可以通过 Type: DYN (Position-Independent Executable file) 来判断是动态链接,但是这种判读也不准确

如果把 PIE 去掉,类型和静态链接完全一样,倒是可以判断是否有 PIE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
% gcc hello.c -o c-hello-gcc-gnu
% readelf -h c-hello-gcc-gnu | grep Type
Type: DYN (Position-Independent Executable file)

% gcc -no-pie hello.c -o c-hello-gcc-gnu-nopie
% readelf -h c-hello-gcc-gnu-nopie | grep Type
Type: EXEC (Executable file)

% gcc -static hello.c -o c-hello-gcc-gnu-static
% readelf -h c-hello-gcc-gnu-static | grep Type
Type: EXEC (Executable file)

% gcc -static-pie hello.c -o c-hello-gcc-gnu-static-pie
% readelf -h c-hello-gcc-gnu-static-pie | grep Type
Type: DYN (Position-Independent Executable file)

ELF 格式有两个 header,readelf -h 是看文件的 header,readelf -l 是看程序的 header

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
% readelf -l c-hello-gcc-gnu

Elf file type is DYN (Position-Independent Executable file)
Entry point 0x1040
There are 14 program headers, starting at offset 64

Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040
0x0000000000000310 0x0000000000000310 R 0x8
INTERP 0x00000000000003b4 0x00000000000003b4 0x00000000000003b4
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000640 0x0000000000000640 R 0x1000
LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000
0x0000000000000161 0x0000000000000161 R E 0x1000
LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000
0x00000000000000d4 0x00000000000000d4 R 0x1000
LOAD 0x0000000000002dd0 0x0000000000003dd0 0x0000000000003dd0
0x0000000000000248 0x0000000000000250 RW 0x1000
DYNAMIC 0x0000000000002de0 0x0000000000003de0 0x0000000000003de0
0x00000000000001e0 0x00000000000001e0 RW 0x8
NOTE 0x0000000000000350 0x0000000000000350 0x0000000000000350
0x0000000000000040 0x0000000000000040 R 0x8
NOTE 0x0000000000000390 0x0000000000000390 0x0000000000000390
0x0000000000000024 0x0000000000000024 R 0x4
NOTE 0x00000000000020b4 0x00000000000020b4 0x00000000000020b4
0x0000000000000020 0x0000000000000020 R 0x4
GNU_PROPERTY 0x0000000000000350 0x0000000000000350 0x0000000000000350
0x0000000000000040 0x0000000000000040 R 0x8
GNU_EH_FRAME 0x0000000000002014 0x0000000000002014 0x0000000000002014
0x0000000000000024 0x0000000000000024 R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x0000000000002dd0 0x0000000000003dd0 0x0000000000003dd0
0x0000000000000230 0x0000000000000230 R 0x1

Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .note.gnu.property .note.gnu.build-id .interp .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt
03 .init .plt .text .fini
04 .rodata .eh_frame_hdr .eh_frame .note.ABI-tag
05 .init_array .fini_array .dynamic .got .got.plt .data .bss
06 .dynamic
07 .note.gnu.property
08 .note.gnu.build-id
09 .note.ABI-tag
10 .note.gnu.property
11 .eh_frame_hdr
12
13 .init_array .fini_array .dynamic .got

我们可以通过 INTERP/lib64/ld-linux-x86-64.so.2 来判断它使用了动态链接,具体那些做了动态链接,我们可以使用 readelf -d 来看 Dynamic section 里面的内容

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
% readelf -d c-hello-gcc-gnu 

Dynamic section at offset 0x2de0 contains 26 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000c (INIT) 0x1000
0x000000000000000d (FINI) 0x1154
0x0000000000000019 (INIT_ARRAY) 0x3dd0
0x000000000000001b (INIT_ARRAYSZ) 8 (bytes)
0x000000000000001a (FINI_ARRAY) 0x3dd8
0x000000000000001c (FINI_ARRAYSZ) 8 (bytes)
0x000000006ffffef5 (GNU_HASH) 0x3d0
0x0000000000000005 (STRTAB) 0x498
0x0000000000000006 (SYMTAB) 0x3f0
0x000000000000000a (STRSZ) 141 (bytes)
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000015 (DEBUG) 0x0
0x0000000000000003 (PLTGOT) 0x3fe8
0x0000000000000002 (PLTRELSZ) 24 (bytes)
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0x628
0x0000000000000007 (RELA) 0x568
0x0000000000000008 (RELASZ) 192 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000006ffffffb (FLAGS_1) Flags: PIE
0x000000006ffffffe (VERNEED) 0x538
0x000000006fffffff (VERNEEDNUM) 1
0x000000006ffffff0 (VERSYM) 0x526
0x000000006ffffff9 (RELACOUNT) 3
0x0000000000000000 (NULL) 0x0

静态链接,没有 Dynamic section,所以里面没有内容,所以也可以通过这个来判断是否是动态链接?当然,这个不行,还有一种特殊情况

1
2
3
% readelf -d c-hello-gcc-gnu-static 

There is no dynamic section in this file.

PIE

PIE(Position-Independent Executable,位置无关可执行文件)是一种安全技术,它使可执行程序在内存中的加载地址随机化,从而增强系统的安全性。运行时由动态链接器配合操作系统随机化加载地址,目前很多编译器默认会开 PIE。

PIE 本质上是利用 ld 完成的随机加载,静态编译没有 ld ,需要把 ld 的 PIE 相关的功能也打包进去。

另外 PIE 的部分符号信息是写在 Dynamic section 里面的,所以,静态链接 PIE 会有 Dynamic section 里会有一些符号信息

-static-pie 在一起时,有一些天然的互斥,不过目前的编译器都可以做到

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
% gcc -static-pie hello.c -o c-hello-gcc-gnu-static-pie
% file -b c-hello-gcc-gnu-static-pie
ELF 64-bit LSB pie executable, x86-64, version 1 (GNU/Linux), static-pie linked, BuildID[sha1]=2a89b311b755433561c90ed642fdb44756e5063b, for GNU/Linux 4.4.0, not stripped
% readelf -d c-hello-gcc-gnu-static-pie

Dynamic section at offset 0xafd68 contains 22 entries:
Tag Type Name/Value
0x000000000000000c (INIT) 0x7000
0x000000000000000d (FINI) 0x82790
0x0000000000000019 (INIT_ARRAY) 0xabc60
0x000000000000001b (INIT_ARRAYSZ) 16 (bytes)
0x000000000000001a (FINI_ARRAY) 0xabc70
0x000000000000001c (FINI_ARRAYSZ) 16 (bytes)
0x000000006ffffef5 (GNU_HASH) 0x380
0x0000000000000005 (STRTAB) 0x3b8
0x0000000000000006 (SYMTAB) 0x3a0
0x000000000000000a (STRSZ) 1 (bytes)
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000015 (DEBUG) 0x0
0x0000000000000003 (PLTGOT) 0xaffe8
0x0000000000000002 (PLTRELSZ) 432 (bytes)
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0x6ae0
0x0000000000000007 (RELA) 0x3c0
0x0000000000000008 (RELASZ) 26400 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000006ffffffb (FLAGS_1) Flags: PIE
0x000000006ffffff9 (RELACOUNT) 1096
0x0000000000000000 (NULL) 0x0

patchelf

移花接木,使用 patchelf 工具去改变动态链接

你有没有想过,既然差距就在 glibcmuslldlibc 位置不同,那我能不能直接修改 ELF 的位置,来把原本是动态链接 glibc 的程序改成动态链接 musl

glibcmusl 只有核心部分兼容,对于只用标准 c 和 posix c 的程序是可以兼容的,但 musl力求精简和标准 glibc 兼容更好也更为庞大,把动态链接 musl 改成动态链接 glibc 的成功率更高一些,不过这种改法没什么实际的应用价值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
% gcc hello.c -o c-hello-gcc-gnu-elf
% readelf -l c-hello-gcc-gnu-elf | grep interpreter
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
% readelf -d c-hello-gcc-gnu-elf | grep NEEDED
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]

# Patch LD 的路径和 libc 的路径
% patchelf --set-interpreter /lib/ld-musl-x86_64.so.1 c-hello-gcc-gnu-elf
% patchelf --replace-needed libc.so.6 libc.so c-hello-gcc-gnu-elf

% ./c-hello-gcc-gnu-elf
Hello, World!
% readelf -l c-hello-gcc-gnu-elf | grep interpreter
[Requesting program interpreter: /lib/ld-musl-x86_64.so.1]
% readelf -d c-hello-gcc-gnu-elf | grep NEEDED
0x0000000000000001 (NEEDED) Shared library: [libc.so]

不过还是能看到好多有问题的地方的,但程序能跑(

1
2
3
4
5
6
7
8
9
10
11
12
13
% hexdump -C c-hello-gcc-gnu-elf
*
00005000 00 70 75 74 73 00 5f 5f 6c 69 62 63 5f 73 74 61 |.puts.__libc_sta|
00005010 72 74 5f 6d 61 69 6e 00 5f 5f 63 78 61 5f 66 69 |rt_main.__cxa_fi|
00005020 6e 61 6c 69 7a 65 00 6c 69 62 63 2e 73 6f 2e 36 |nalize.libc.so.6|
00005030 00 47 4c 49 42 43 5f 32 2e 32 2e 35 00 47 4c 49 |.GLIBC_2.2.5.GLI|
00005040 42 43 5f 32 2e 33 34 00 5f 49 54 4d 5f 64 65 72 |BC_2.34._ITM_der|
00005050 65 67 69 73 74 65 72 54 4d 43 6c 6f 6e 65 54 61 |egisterTMCloneTa|
00005060 62 6c 65 00 5f 5f 67 6d 6f 6e 5f 73 74 61 72 74 |ble.__gmon_start|
00005070 5f 5f 00 5f 49 54 4d 5f 72 65 67 69 73 74 65 72 |__._ITM_register|
00005080 54 4d 43 6c 6f 6e 65 54 61 62 6c 65 00 6c 69 62 |TMCloneTable.lib|
00005090 63 2e 73 6f 00 00 00 00 00 |c.so.....|
00005099

不要用 patchelf 来移植不同的 libc,本身 glibcmusl 就差距很大,glibc 功能非常庞大

Go 的编译

Golang 不依赖系统的 libc,通过两个变量直接交叉编译出其他平台的程序,非常的方便

1
GOOS=linux GOARCH=amd64 go build

CGO 的情况还是可能依赖 libc

1
2
3
4
5
6
7
8
9
cat > hello.go << EOF
package main

import "fmt"

func main() {
fmt.Println("hello world!")
}
EOF

CGO_ENABLED 主要是用来禁用 CGO 的,过于简单的完全不依赖 CGO

1
2
3
4
5
6
7
% CGO_ENABLED=0 go build -o go-hello-0 ./hello.go
% file -b go-hello-0
ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=b5b7d5fb91fa0881908a29fb762f71d1244b808a, with debug_info, not stripped

% CGO_ENABLED=1 go build -o go-hello-1 ./hello.go
% file -b go-hello-1
ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=6acc631714b8e9f1ece35c8a2c0cafeee876ae30, with debug_info, not stripped

比如我们使用 stdnet 包,默认会使用 CGO,我们禁用 CGO 来静态编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cat > lookup.go << EOF
package main

import (
"fmt"
"net"
)

func main() {
addrs, err := net.LookupHost("baidu.com")
if err != nil {
panic(err)
}
fmt.Println("Baidu IPs:", addrs)
}
EOF
1
2
3
4
5
6
7
8
9
10
11
12
% CGO_ENABLED=0 go build -o go-lookup-0 ./lookup.go
% file -b ./go-lookup-0
ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=0c269da87124d8d01065f131f3527b4e8a0044fe, with debug_info, not stripped

% CGO_ENABLED=1 go build -o go-lookup-1 ./lookup.go
% file -b ./go-lookup-1
ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=48cc551954f4de00d378dfac57065055a4f4a28c, with debug_info, not stripped
% ldd go-lookup-1
linux-vdso.so.1 (0x000078aa44203000)
libresolv.so.2 => /usr/lib/libresolv.so.2 (0x000078aa441db000)
libc.so.6 => /usr/lib/libc.so.6 (0x000078aa43fe9000)
/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x000078aa44205000)

Zig 的编译

Zig 是一门开源的、静态类型的、跨平台的系统编程语言,甚至集成了 c/c++ 的编译器。当然我们可以把它当成 c 的编译器来用

zig cc 会在 *-linux-gnu 编译成动态链接,在 *-linux-musl 编译成静态链接

1
2
3
% zig cc -target x86_64-linux-gnu hello.c -o c-hello-zig-gnu
% file -b c-hello-zig-gnu
ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.0.0, with debug_info, not stripped
1
2
3
% zig cc -target x86_64-linux-musl hello.c -o c-hello-zig-musl
% file -b c-hello-zig-musl
ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, with debug_info, not stripped

我们知道如果使用 gcc / clang 交叉编译时,每个平台都需要单独的交叉编译工具链

比如 arm64 需要 aarch64-linux-gnu-gc , Risc-V 需要 riscv64-linux-gnu-gcc

ziggo 一样可以编译所有支持的平台,非常的方便,不过 zig 还有内置 C 的编译工具链,交叉编译非常的方便

Rust 的编译

rust 自己交叉编译,但 Rust 是依赖 libc 的。rust 交叉编译仍然需要对应平台的 C 编译器

1
cargo new rust-hello

对于每个平台我们都需要单独安装对应的 C 的工具链,更确切的说是,rust 目前自己没有 ld。rust 自己的编译器只能编译到 .o 文件,需要其他的 ld 来生成对应平台的 ELF

zig 和 rust

rust 没法处理 ld 和 c 扩展的交叉编译问题,zig 有很好交叉编译工具链,集成 C 编译器

这时候我们可以考虑使用 rustzig 一起用

zig cc 是子命令,所以我们需要实现一个简单的包装器 zig.sh

1
2
#!/bin/sh
zig cc -target riscv64-linux-musl $@

我们可以直接指定环境变量使用这个 Linker

1
2
% CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_MUSL_LINKER="./zig.sh" \
cargo build --release --target riscv64gc-unknown-linux-musl

也可以修改一下编译配置 .cargo/config.toml

1
2
[target.riscv64gc-unknown-linux-musl]
linker = "./zig.sh"

很棒,完全的静态链接

1
2
file -b rust-hello
ELF 64-bit LSB pie executable, UCB RISC-V, RVC, double-float ABI, version 1 (SYSV), static-pie linked, stripped

这只是处理了 Linker,手动太麻烦了,而且有 C 扩展还需要其他配置

这里有个 cargo 的插件 cargo-zigbuild,帮你把这些都处理好了

1
2
3
4
% cargo zigbuild --release --target riscv64gc-unknown-linux-musl

% file -b rust-hello
ELF 64-bit LSB pie executable, UCB RISC-V, RVC, double-float ABI, version 1 (SYSV), static-pie linked, stripped

zig 虽然好用,兼容性也很好,但并不一定能处理全部的情况?

cross-rs

rust 交叉编译神器 cross-rs,使用 Docker 构建交叉编译环境,在容器里面配好了 C 的交叉编译工具链

但是 cross 目前不支持 riscv64gc-unknown-linux-musl 我们来加一个,这个 PR 已经合了 cross-rs#1664

顺便分享一下给 cross-rs 加新的 target 踩的坑。cross-rs 其实是个非常老的项目,目前有两套环境构建方法。一个是比较常规的编译安装 gcc,另一个用 crosstool-ng 来配环境

编译 musl 版本也用 gcc 来编译,但需要配置的东西比较多,官方提供了一个叫 musl-gcc 的 warp。当然 binutils 也 warp 过,musl 提供一个 make 脚本帮助你来编译 musl-gcc。但是 musl-cross-make 吸收了几个关于 binutils 的 patch,然后有 break change。这个 patch 非常庞大,很难迁移

这就导致 binutils 比较旧,它的 ld 没法适配新的 RISC-V 的格式。当然自己用可以直接去升级 binutils。那几个 patch 没有关于 RISC-V 相关的。推动这个 musl-cross-make 升级也很难,除非说服维护者放弃那几个 patch

最终还是选择和 crosstool-ng 来安装容器环境

静态编译 libc

Rust 的程序是依赖 libc 的,官方提供了 gnumusl 版本的编译器。glibcmusl 都是两种情况默认都是动态链接。对,musl 默认也是动态链接

我们需要传递一些 CRT (C runtime) 参数才能静态编译 libc

1
RUSTFLAGS="-C target-feature=+crt-static" cargo build --release

也可以在 .cargo/config.toml 里设置

1
2
[target.riscv64gc-unknown-linux-gnu]
rustflags = ["-C", "target-feature=+crt-static"]

我们可以编译出 glibc 静态编译的版本,也可以有 musl 静态编译的版本。实际对比体积几乎无区别。但是,可别忘了 glibc 是 LGPL 许可证,对于静态链接是有法律上的限制

体积对比

各工具链编译出的体积的对比参考

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
 16K c-hello-gcc-gnu
21K c-hello-gcc-gnu-elf
16K c-hello-gcc-gnu-nopie
16K c-hello-gcc-gnu-pie
760K c-hello-gcc-gnu-static
801K c-hello-gcc-gnu-static-pie
15K c-hello-gcc-musl
18K c-hello-gcc-musl-static
15K c-hello-gcc-musl-static-pie
7.9K c-hello-zig-gnu
9.4K c-hello-zig-gnu-aarch64
111K c-hello-zig-musl
1.8M c-hello-zig-musl-riscv64
2.2M go-hello-0
2.2M go-hello-1
3.0M go-lookup-0
3.1M go-lookup-1
423K riscv64-rust-hello-gcc-gnu
435K riscv64-rust-hello-gcc-gnu-elf
421K riscv64-rust-hello-gcc-musl
432K rust-hello-gcc-gnu
1.5M rust-hello-gcc-gnu-static
535K rust-hello-gcc-musl
535K rust-hello-gcc-musl-static
384K rust-hello-zig-gnu
393K rust-hello-zig-musl
24M rust-live777-gcc-gnu
25M rust-live777-gcc-gnu-elf
24M rust-live777-gcc-musl
30 sh-hello.sh

patchelf rust 应用

测试的 RISC-V 环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
% cat /etc/os-release
NAME=Buildroot
VERSION=-g1fcc2fd40
ID=buildroot
VERSION_ID=2023.11.2
PRETTY_NAME="Buildroot 2023.11.2"
% uname -a
Linux licheervnano-1d6b 5.10.4-tag- #15 PREEMPT Fri Dec 20 17:26:44 CST 2024 riscv64 GNU/Linux
% cat /etc/os-release
NAME=Buildroot
VERSION=-g1fcc2fd40
ID=buildroot
VERSION_ID=2023.11.2
PRETTY_NAME="Buildroot 2023.11.2"
% ls /lib/ld-musl-*.so.1
/lib/ld-musl-riscv64-sf.so.1 /lib/ld-musl-riscv64v0p7_xthead.so.1 /lib/ld-musl-riscv64xthead-sf.so.1
/lib/ld-musl-riscv64.so.1 /lib/ld-musl-riscv64v_xthead.so.1 /lib/ld-musl-riscv64xthead.so.1

patchelf rust 的 hello world,把动态链接 glibc 的改成动态链接 musl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
% cp target/riscv64gc-unknown-linux-gnu/release/build-static rust-hello-gcc-gnu-elf
% cp target/riscv64gc-unknown-linux-gnu/release/build-static rust-hello-gcc-gnu
% cp target/riscv64gc-unknown-linux-musl/release/build-static rust-hello-gcc-musl

% readelf -d rust-hello-gcc-gnu-elf | grep NEEDED
0x0000000000000001 (NEEDED) Shared library: [libgcc_s.so.1]
0x0000000000000001 (NEEDED) Shared library: [libc.so]
0x0000000000000001 (NEEDED) Shared library: [ld-linux-riscv64-lp64d.so.1]

% patchelf --set-interpreter /lib/ld-musl-riscv64.so.1 rust-hello-gcc-gnu-elf

% patchelf --replace-needed libc.so.6 libc.so rust-hello-gcc-gnu-elf
% patchelf --replace-needed ld-linux-riscv64-lp64d.so.1 ld-musl-riscv64.so.1 rust-hello-gcc-gnu-elf

% readelf -d rust-hello-gcc-gnu-elf | grep NEEDED
0x0000000000000001 (NEEDED) Shared library: [libgcc_s.so.1]
0x0000000000000001 (NEEDED) Shared library: [libc.so]
0x0000000000000001 (NEEDED) Shared library: [ld-musl-riscv64.so.1]

移花接木大法搞出来的 rust-hello-gcc-gnu-elf 是可以正常运行的

1
2
3
4
5
6
% ./rust-hello-gcc-gnu
-sh: ./rust-hello-gcc-gnu: not found
% ./rust-hello-gcc-gnu-elf
Hello, world!
% ./rust-hello-gcc-musl
Hello, world!

我们拿一个实际的项目试试,用 patchelf 来修改 glibc 版本的 live777 试试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
% cp target/riscv64gc-unknown-linux-musl/release/live777 rust-live777-gcc-musl
% cp target/riscv64gc-unknown-linux-gnu/release/live777 rust-live777-gcc-gnu
% cp target/riscv64gc-unknown-linux-gnu/release/live777 rust-live777-gcc-gnu-elf

% readelf -d rust-live777-gcc-gnu-elf | grep NEEDED
0x0000000000000001 (NEEDED) Shared library: [libgcc_s.so.1]
0x0000000000000001 (NEEDED) Shared library: [libm.so.6]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x0000000000000001 (NEEDED) Shared library: [ld-linux-riscv64-lp64d.so.1]

% patchelf --set-interpreter /lib/ld-musl-riscv64.so.1 rust-live777-gcc-gnu-elf

% patchelf --replace-needed libc.so.6 libc.so rust-live777-gcc-gnu-elf
% patchelf --replace-needed libm.so.6 libc.so rust-live777-gcc-gnu-elf
% patchelf --replace-needed ld-linux-riscv64-lp64d.so.1 ld-musl-riscv64.so.1 rust-live777-gcc-gnu-elf

% readelf -d rust-live777-gcc-gnu-elf | grep NEEDED
0x0000000000000001 (NEEDED) Shared library: [libgcc_s.so.1]
0x0000000000000001 (NEEDED) Shared library: [libc.so]
0x0000000000000001 (NEEDED) Shared library: [libc.so]
0x0000000000000001 (NEEDED) Shared library: [ld-musl-riscv64.so.1]

移花接木大法搞出来的 live777 不能正常运行的,看起来用了 glibc 专有的函数

1
2
3
4
5
6
7
8
9
10
11
12
% ./rust-live777-gcc-gnu
-sh: ./rust-live777-gcc-gnu: not found
% ./rust-live777-gcc-gnu-elf
Error relocating ./rust-live777-gcc-gnu-elf: gnu_get_libc_version: symbol not found
Error relocating ./rust-live777-gcc-gnu-elf: __res_init: symbol not found
Error relocating ./rust-live777-gcc-gnu-elf: __register_atfork: symbol not found
Error relocating ./rust-live777-gcc-gnu-elf: gnu_get_libc_version: symbol not found
Error relocating ./rust-live777-gcc-gnu-elf: __res_init: symbol not found
Error relocating ./rust-live777-gcc-gnu-elf: __register_atfork: symbol not found
% ./rust-live777-gcc-musl
2025-04-16T22:03:47.280021Z WARN ThreadId(01) live777: src/main.rs:25: set log level : info
2025-04-16T22:03:47.280936Z INFO ThreadId(01) live777: src/main.rs:31: Server listening on 0.0.0.0:7777

总结

  • 使用 file 命令判断是否为静态链接还是比较靠谱的
  • ldd 依赖 /lib/ld ,全静态链接不走 ld ,而且没法跨 libc 和架构
  • 纯 rust,默认 gnu 和 musl 的 libc 都是动态链接,需要 RUSTFLAGS="-C target-feature=+crt-static" 来编译静态链接
  • zig 默认参数,zig 默认 gnu 为动态链接,musl 为静态链接
  • zig 配合 rust 处理交叉编译非常好用
  • cross-rs 也很好用
  • 不要用 patchelf 来移植不同的 libc

Reference

计算机那些事(4)——ELF文件结构

计算机那些事(5)——链接、静态链接、动态链接

计算机那些事(6)——可执行文件的装载与运行

Executable and Linkable Format - Wikipedia

ldd (Unix) - Wikipedia

ldd 命令

记一次Rust静态编译

How to generate statically linked executables?

1721-crt-static - The Rust RFC Book