C3 交叉编译 AArch64

· 9594 字

先分清编译与链接

在 x86_64 开发机上构建 ARM64 Linux 程序,看起来像是一件需要特殊编译器的事。实际上,C3 已经内置了 linux-aarch64 目标,真正麻烦的是链接阶段。

整个流程可以拆成四步:

C3 源码
  -> C3 / LLVM 生成 AArch64 目标文件
  -> 链接 AArch64 的 crt、libc、libgcc
  -> 生成 AArch64 Linux ELF

生成目标文件只需要 C3 和 LLVM。要生成最终可执行文件,开发机还必须准备目标架构的 C 运行库、启动文件和 GCC 运行时。普通 Linux 程序和裸机程序的需求不同,下文讨论的是前者。

先确认当前编译器支持这个目标:

c3c --list-targets

输出中应当包含 linux-aarch64。本文的完整流程基于 C3 0.8.5_1 验证;其他版本的路径和参数应以本机安装结果为准。

开发环境准备

Linux

在 Arch Linux 或 Manjaro 上安装交叉工具链:

sudo pacman -S aarch64-linux-gnu-gcc aarch64-linux-gnu-glibc

Debian 和 Ubuntu 使用:

sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu libc6-dev-arm64-cross file

Fedora 使用:

sudo dnf install gcc-aarch64-linux-gnu glibc-aarch64-linux-gnu

安装完成后,先找到关键文件的位置。crt1.o 和 libc.so.6 通常位于 /usr/aarch64-linux-gnu/lib,而 crtbegin.o 与 libgcc.a 的位置会随发行版和 GCC 版本变化:

find /usr/lib -path '*aarch64*' -name crtbegin.o 2>/dev/null

Arch 和 Manjaro 通常使用 /usr/lib/gcc/aarch64-linux-gnu/<version>,Debian 和 Ubuntu 则使用 /usr/lib/gcc-cross/aarch64-linux-gnu/<version>。

Windows:优先使用 WSL2

在管理员 PowerShell 中安装 Ubuntu:

wsl --install -d Ubuntu

进入 WSL 后安装交叉工具链和 Linux 版 C3:

sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu libc6-dev-arm64-cross file
curl -fsSL https://raw.githubusercontent.com/c3lang/c3c/refs/heads/master/install/install.sh | bash

重新打开终端后确认安装:

c3c --version
find /usr/lib/gcc-cross -path '*aarch64*' -name crtbegin.o

Windows 项目通常位于 /mnt/<盘符>/...。进入对应目录后,构建流程与普通 Linux 开发机一致。这样既能使用 Linux 文件语义,也能减少 Windows 解压工具破坏 sysroot 软链接的问题。

Windows:纯原生交叉编译

如果必须继续使用 Windows 版 c3c.exe,就需要手动准备一个 AArch64 Linux sysroot。可以选择 ARM 官方工具链中的 aarch64-none-linux-gnu,也可以使用 Bootlin 提供的整套工具链。

ARM 官方工具链通常把关键文件放在:

<toolchain>\aarch64-none-linux-gnu\libc\usr\lib64
<toolchain>\lib\gcc\aarch64-none-linux-gnu\<version>

Bootlin 工具链通常把关键文件放在:

<toolchain>\aarch64-buildroot-linux-gnu\sysroot\usr\lib
<toolchain>\lib\gcc\aarch64-buildroot-linux-gnu\<version>

目录布局会随工具链版本变化,不要只凭文档猜路径。解压后可以用 PowerShell 统一确认:

Get-ChildItem -Recurse C:\sysroots\aarch64-linux-gnu `
  -Include crt1.o,libc.so.6,crtbegin.o,libgcc.a

最重要的一点是确认选择的是 Linux 工具链。arm-none-eabi 面向裸机,Mingw-w64 的 AArch64 工具链通常面向 Windows,它们都不能替代 Linux glibc sysroot。

glibc 必须匹配目标机

交叉编译能否成功,和产物能否在开发板上运行,是两个不同的问题。编译器、sysroot 和目标机上的 glibc 是三个独立组件,版本关系必须在动手配置前确认。

先在 ARM64 开发板上执行:

uname -m
ldd --version
cat /etc/os-release

例如 Debian 11 bullseye 的 glibc 是 2.31。开发机若使用要求 GLIBC_2.34 或 GLIBC_2.35 的新工具链,即使链接成功,产物也可能在板上报错:

version `GLIBC_2.34' not found

选择的基本原则是:sysroot 中的 glibc 不高于目标机上的 glibc。使用完全相同的版本最省心,稍旧且兼容的版本也可以。--sysroot 只改变链接器查找文件的路径,不会降低 ELF 对 glibc 符号版本的依赖。

Bootlin 已经组合好 glibc、GCC 运行时和 sysroot,适合从零搭建纯 Windows 环境。Debian 11 可以选择:

aarch64--glibc--stable-2020.08-1.tar.bz2

文件名里的 stable 只表示发布渠道,不代表兼容所有旧开发板。下载前仍要确认该批次对应的 glibc 版本。

Manjaro 和 Arch 的软件仓库通常只保留最新版交叉工具链。如果目标板使用 glibc 2.31,可以从 Arch Linux Archive 取得同期版本,但不建议覆盖系统包:

mkdir -p .toolchains/aarch64-glibc-2.31

curl -fL \
  https://archive.archlinux.org/packages/a/aarch64-linux-gnu-glibc/aarch64-linux-gnu-glibc-2.31-1-any.pkg.tar.zst \
  -o /tmp/aarch64-glibc-2.31.pkg.tar.zst
curl -fL \
  https://archive.archlinux.org/packages/a/aarch64-linux-gnu-gcc/aarch64-linux-gnu-gcc-9.3.0-1-x86_64.pkg.tar.zst \
  -o /tmp/aarch64-gcc-9.3.0.pkg.tar.zst

分别把 glibc 和 GCC 的链接文件解压到项目内的 .toolchains/aarch64-glibc-2.31。glibc 包可以整体解开,GCC 包只需要 crtbegin*、crtend*、libgcc* 和 libgcc_s* 等链接所需文件。

bsdtar -xf /tmp/aarch64-glibc-2.31.pkg.tar.zst \
  -C .toolchains/aarch64-glibc-2.31
bsdtar -xf /tmp/aarch64-gcc-9.3.0.pkg.tar.zst \
  -C .toolchains/aarch64-glibc-2.31 \
  'usr/lib/gcc/aarch64-linux-gnu/9.3.0/crtbegin*.o' \
  'usr/lib/gcc/aarch64-linux-gnu/9.3.0/crtend*.o' \
  'usr/lib/gcc/aarch64-linux-gnu/9.3.0/libgcc*' \
  'usr/aarch64-linux-gnu/lib64/libgcc_s*'

.toolchains/ 应加入 .gitignore。glibc 和 GCC 尽量来自相近时期:只替换旧 glibc,却继续使用依赖新 glibc 的新版 libgcc_s.so.1,仍然可能失败。

三个链接配置

C3 的 Linux 交叉链接主要涉及三个配置,它们不能互相替代。

配置负责的内容示例
linux-crtcrt1.o、crti.o、crtn.o、libc.so、libc.so.6/usr/aarch64-linux-gnu/lib
linux-crtbegincrtbegin.o、crtend.o、libgcc.a/usr/lib/gcc-cross/aarch64-linux-gnu/<version>
link-args原样传给 lld 的额外参数--sysroot=/usr/aarch64-linux-gnu

linux-crt 解决程序如何启动、标准 C 库从哪里来;linux-crtbegin 补全 GCC 的初始化、清理和底层运行时。link-args 则直接交给 lld,常用于补充 sysroot 或其他搜索路径。

为什么需要 sysroot?很多工具链里的 libc.so 并不是普通共享库,而是 GNU ld 链接脚本。脚本里可能写着:

GROUP ( /lib/libc.so.6 /lib/libc_nonshared.a
        AS_NEEDED ( /lib/ld-linux-aarch64.so.1 ) )

这些 /lib/... 是目标系统中的路径。没有 sysroot 时,lld 会尝试从 x86_64 宿主机上寻找它们,并报出 cannot open /lib/ld-linux-aarch64.so.1。正确做法是让这些绝对路径映射到解压后的 AArch64 目录,而不是复制运行库到宿主机根目录。

Linux 与 WSL 配置

在项目根目录的 project.json 中加入:

{
  "targets": {
    "hello-aarch64": {
      "type": "executable",
      "name": "00_hello_world",
      "target": "linux-aarch64",
      "linux-crt": "/usr/aarch64-linux-gnu/lib",
      "linux-crtbegin": "/usr/lib/gcc-cross/aarch64-linux-gnu/<version>",
      "link-args": [
        "--sysroot=/usr/aarch64-linux-gnu"
      ],
      "output": "build/aarch64"
    }
  }
}

把 <version> 替换成 find 命令查到的真实目录。Arch 和 Manjaro 需要把 gcc-cross 路径改成实际的 /usr/lib/gcc/...。

构建时指定目标名:

c3c build hello-aarch64

如果不修改项目配置,也可以在命令行临时覆盖:

c3c build 00_hello_world \
  --target linux-aarch64 \
  --linux-crt /usr/aarch64-linux-gnu/lib \
  --linux-crtbegin /usr/lib/gcc-cross/aarch64-linux-gnu/<version> \
  -z --sysroot=/usr/aarch64-linux-gnu

排查路径问题时,可以打印 C3 实际传入的链接参数:

c3c build 00_hello_world --print-linking

Windows 原生配置

使用 ARM 官方工具链时,--sysroot 指向 libc 根目录,linux-crt 则指向其中的 usr/lib64:

{
  "targets": {
    "hello-aarch64": {
      "type": "executable",
      "name": "00_hello_world",
      "target": "linux-aarch64",
      "linux-crt": "C:/sysroots/aarch64-linux-gnu/aarch64-none-linux-gnu/libc/usr/lib64",
      "linux-crtbegin": "C:/sysroots/aarch64-linux-gnu/lib/gcc/aarch64-none-linux-gnu/15.3.1",
      "link-args": [
        "--sysroot=C:/sysroots/aarch64-linux-gnu/aarch64-none-linux-gnu/libc"
      ],
      "output": "build/aarch64"
    }
  }
}

这里的 15.3.1 只是路径结构示例。JSON 中优先使用正斜杠;如果使用反斜杠,每个反斜杠都要写成 \\。

使用 Bootlin 并面向 Debian 11 时,可以额外传入运行库搜索路径和 libdl 入口:

{
  "linux-crt": "D:/aarch64--glibc--stable-2020.08-1/aarch64-buildroot-linux-gnu/sysroot/usr/lib",
  "linux-crtbegin": "D:/aarch64--glibc--stable-2020.08-1/lib/gcc/aarch64-buildroot-linux-gnu/9.3.0",
  "link-args": [
    "-LD:/aarch64--glibc--stable-2020.08-1/aarch64-buildroot-linux-gnu/sysroot/lib",
    "D:/aarch64--glibc--stable-2020.08-1/aarch64-buildroot-linux-gnu/sysroot/lib/libdl.so",
    "--sysroot=D:/aarch64--glibc--stable-2020.08-1/aarch64-buildroot-linux-gnu/sysroot"
  ]
}

构建命令与 Linux 相同:

c3c build hello-aarch64

Windows 直接解压 Linux 工具链时要特别小心。7-Zip 等工具可能把 .tar.xz 中的软链接还原成 0 字节普通文件,导致 libc.so.6、libdl.so.2 和目录链接 lib64 失效。

链接器随后可能报出:

ld.lld: error: unable to find library -lc
ld.lld: error: cannot open .../sysroot/usr/lib/crti.o

这通常不是压缩包损坏,而是软链接没有被保留。优先在 WSL 中用 tar 解压,是更稳妥的解决办法。如果必须保留纯 Windows 路线,就要核对关键文件大小,并按工具链中的真实文件名重建失效的硬链接或目录联接。

构建与验证

构建完成后,先检查文件类型:

file build/aarch64/00_hello_world

output 表示输出目录,可执行文件名由 name 决定。预期结果类似:

ELF 64-bit LSB pie executable, ARM aarch64, ...

再检查 ELF 需要的 glibc 符号版本:

readelf --version-info build/aarch64/00_hello_world |
  grep -o 'GLIBC_[0-9.]*' | sort -Vu

如果本机是 x86_64,不能直接执行 ARM64 程序,也不能把交叉产物交给 c3c run。可以在 Linux 或 WSL 中用 qemu-user 做最小验证:

sudo apt-get install qemu-user
qemu-aarch64 ./build/aarch64/00_hello_world

最后把产物放到目标开发板上再次运行。只有在目标板、目标 glibc 和目标 ABI 上完成验证,交叉编译才算真正闭环。

常见错误速查

现象大概率原因检查方向
Failed to find the C runtime at link time没找到目标 C runtime检查 linux-crt 是否包含 crt1.o 和 libc.so.6
unable to find library -lgcc没找到目标 GCC 运行时检查 linux-crtbegin 是否指向带 GCC 版本号的目录
cannot open /lib/ld-linux-aarch64.so.1链接脚本中的绝对路径没有映射增加正确的 --sysroot
cannot open /lib64/libm.so.6sysroot 或运行库搜索路径不完整检查 --sysroot、-L 和 lib64 目录
unable to find library -lcWindows 解压后软链接损坏用 WSL 解压,或重建共享库入口
GLIBC_X.Y not foundsysroot 的 glibc 比目标板新换用匹配目标系统的旧版 sysroot
c3c run 启动失败主机不能直接运行 ARM64 产物使用 qemu-user 或目标开发板

其中 GLIBC_X.Y not found 最容易被误判。它说明程序已经链接完成,但产物使用了目标板没有的 ABI 符号。继续调整 --sysroot 只能修复文件查找问题,无法修复已经写进 ELF 的版本要求。

裸机项目的例外

如果目标是 freestanding 或裸机程序,就不需要准备 Linux libc,通常也不应使用 linux-aarch64。此时可以关闭标准库和 libc,并使用类似下面的配置:

{
  "target": "elf-aarch64",
  "use-stdlib": false,
  "link-libc": false,
  "memory-env": "none",
  "no-entry": true
}

这种方式需要自行提供启动代码、内存布局和必要的运行时实现。它解决的是另一类问题,不属于普通 Linux 可执行文件的交叉编译。

小结

把 C3 交叉编译到 AArch64,关键不是“让 C3 支持 ARM64”,而是给链接器准备一套完整且版本匹配的目标环境。

Linux 和 WSL 可以直接使用发行版提供的交叉工具链,再把 linux-crt、linux-crtbegin 和 --sysroot 指向实际文件。Windows 原生路线需要额外管理解压后的工具链和软链接,因此更适合作为备用方案。

无论选择哪条路线,都应先读目标板的 glibc,再选 sysroot;构建后检查 ELF 架构和符号版本,最后在目标机上运行。这样,交叉编译的问题会从一连串链接器报错,缩小为可以逐项核对的路径与版本关系。

参考资料