Ubuntu x86_64 交叉编译 AArch64

目录

Ubuntu x86_64 交叉编译 AArch64:Sysroot、Multiarch 与 GCC 搜索路径详解

在 x86_64 Ubuntu 主机上使用 GCC 交叉编译 AArch64 Linux 程序时,一个常见场景是:

  • 编译器使用 aarch64-none-linux-gnu-gcc
  • 目标设备运行 Ubuntu/Debian ARM64
  • 已经准备好目标设备的 ARM64 rootfs
  • 程序需要使用 rootfs 中的 glibc、GLib、OpenSSL 等库
  • 编译器自身也带有一套 libc/sysroot

这种环境最容易遇到的问题不是 CPU 架构本身,而是:

交叉编译器自带 sysroot 与 Ubuntu/Debian ARM64 rootfs 的目录布局不完全一致。

本文总结如何正确理解和配置 --sysroot-I-isystem-idirafter-B-L-rpath-link

1. 典型交叉编译环境

假设:

TOOLCHAIN=/opt/toolchains/aarch64-none-linux-gnu
ROOTFS=/opt/sysroots/ubuntu-arm64

CC=$TOOLCHAIN/bin/aarch64-none-linux-gnu-gcc

交叉编译器可以通过:

$CC -print-sysroot

查看默认 sysroot。

很多完整的 GNU Toolchain 自身已经包含:

libc/
├── usr/include/
├── usr/lib/
└── lib/

但如果最终程序运行在 Ubuntu ARM64 系统,并且需要使用该系统中的:

glibc
GLib
OpenSSL
以及其他 ARM64 动态库

就需要考虑使用目标系统 rootfs 作为真正的 sysroot。

2. 什么是 Sysroot?

--sysroot 是交叉编译中最重要的参数之一。

例如:

--sysroot=$ROOTFS

可以简单理解成:

告诉编译器和链接器:$ROOTFS 就是目标机器的 /

例如目标程序需要:

/lib/aarch64-linux-gnu/libc.so.6

那么交叉链接时实际对应:

$ROOTFS/lib/aarch64-linux-gnu/libc.so.6

类似地:

/usr/lib/aarch64-linux-gnu/libc.so

对应:

$ROOTFS/usr/lib/aarch64-linux-gnu/libc.so

这对于 glibc 尤其重要,因为 Ubuntu/Debian 中的 libc.so 通常还是一个 linker script,其中可能包含绝对路径。

如果没有正确设置 sysroot,链接器可能出现:

cannot find /lib/aarch64-linux-gnu/libc.so.6
cannot find /usr/lib/aarch64-linux-gnu/libc_nonshared.a
cannot find /lib/ld-linux-aarch64.so.1

这些文件并不一定真的缺失,而可能只是链接器没有在正确的 rootfs 中寻找。

3. Ubuntu/Debian 的 Multiarch 目录

Ubuntu 和 Debian 使用 Multiarch 目录组织不同架构的文件。

ARM64 的 GNU triplet 通常是:

aarch64-linux-gnu

因此常见目录为:

/usr/include/aarch64-linux-gnu/

/usr/lib/aarch64-linux-gnu/

/lib/aarch64-linux-gnu/

例如:

/usr/include/aarch64-linux-gnu/bits/
/usr/lib/aarch64-linux-gnu/libc.so
/lib/aarch64-linux-gnu/libc.so.6

但是某些独立 GNU Toolchain 使用:

aarch64-none-linux-gnu

于是出现一个关键问题:

交叉编译器:
aarch64-none-linux-gnu

Ubuntu rootfs:
aarch64-linux-gnu

虽然 ABI 可以兼容,但 GCC 不一定能够自动推导 Ubuntu/Debian 的 Multiarch 搜索路径。

因此需要手工补充相关目录。

4. -I:普通头文件搜索路径

最常见的是:

-I/path/to/include

它表示:

将这个目录加入普通头文件搜索路径。

例如:

-I../../Components
-I../include
-I../libs

适合项目自己的:

*.h

文件。

例如:

#include "project.h"

可以通过:

-I../include

让 GCC 找到。

对于自己的项目头文件,优先使用 -I

5. -isystem:系统头文件目录

-isystem 同样用于添加头文件目录,但告诉 GCC:

这个目录属于系统头文件。

例如:

-isystem $ROOTFS/usr/include

和普通 -I 相比,它主要有两个特点:

  1. 按 GCC 的 system header 规则处理搜索顺序
  2. 系统头文件内部的一些编译警告会被 GCC 抑制

因此目标系统的:

stdio.h
stdlib.h
pthread.h
bits/*.h

更适合通过 -isystem 引入。

6. 为什么 ARM64 Ubuntu 经常还需要这个目录?

仅仅:

-isystem $ROOTFS/usr/include

有时还不够。

例如:

#include <stdlib.h>

找到:

$ROOTFS/usr/include/stdlib.h

之后,stdlib.h 又可能包含:

#include <bits/libc-header-start.h>

但实际文件可能位于:

$ROOTFS/usr/include/aarch64-linux-gnu/bits/libc-header-start.h

如果交叉编译器没有自动识别 Ubuntu Multiarch 目录,就会出现:

fatal error: bits/libc-header-start.h:
No such file or directory

因此常见配置为:

-isystem $ROOTFS/usr/include/aarch64-linux-gnu
-isystem $ROOTFS/usr/include

这相当于手工补齐 Ubuntu ARM64 的 Multiarch include 路径。

7. -idirafter:最后才搜索的系统头文件

-idirafter 也是头文件搜索参数,例如:

-idirafter $ROOTFS/usr/include

它的特点是:

将这个目录作为低优先级的系统头文件目录,正常搜索路径都找不到之后,再到这里寻找。

可以粗略理解为:

-I
    ↓
正常 GCC/System include
    ↓
-idirafter

它非常适合这种场景:

主要使用交叉工具链自带的 libc/sysroot,但希望额外从另一个 rootfs 中获取 OpenSSL 等第三方头文件。

例如:

#include <stdlib.h>
#include <openssl/evp.h>

希望:

stdlib.h
    ↓
使用 GCC 自带 sysroot

openssl/evp.h
    ↓
GCC 自带 sysroot 没有
    ↓
最后去额外 rootfs 查找

就可以考虑:

-idirafter $ROOTFS/usr/include

但是,如果已经决定:

整个程序都针对这个 Ubuntu ARM64 rootfs 编译

那么应该统一使用:

--sysroot=$ROOTFS
-isystem $ROOTFS/usr/include/aarch64-linux-gnu
-isystem $ROOTFS/usr/include

而不是继续用 -idirafter 混合两套 libc 头文件环境。

8. -B:GCC 特殊文件搜索前缀

-B 经常被忽略,但在“非 Ubuntu 原生交叉工具链 + Ubuntu rootfs”的组合中非常重要。

例如:

-B$ROOTFS/usr/lib/aarch64-linux-gnu/

它可以影响 GCC 对编译器组件、启动文件以及相关资源的搜索。

实际交叉编译中最常见的问题是:

cannot find crt1.o
cannot find crti.o
cannot find crtn.o

Ubuntu ARM64 中这些文件通常位于:

$ROOTFS/usr/lib/aarch64-linux-gnu/crt1.o
$ROOTFS/usr/lib/aarch64-linux-gnu/crti.o
$ROOTFS/usr/lib/aarch64-linux-gnu/crtn.o

可以使用:

find $ROOTFS \
    -name crt1.o \
    -o -name crti.o \
    -o -name crtn.o

确认。

如果文件明明存在,但 GCC 仍然报告:

cannot find crt1.o

通常就是 GCC 没有自动搜索 Ubuntu Multiarch 目录。

此时可以补:

-B$ROOTFS/usr/lib/aarch64-linux-gnu/

9. -L:库文件搜索目录

-L 比较简单:

-L/path/to/lib

告诉链接器去哪里寻找:

*.so
*.a

例如 Ubuntu ARM64:

-L$ROOTFS/usr/lib/aarch64-linux-gnu
-L$ROOTFS/lib/aarch64-linux-gnu

之后:

-lglib-2.0

就可以找到对应:

libglib-2.0.so

类似:

-lssl
-lcrypto

用于查找:

libssl.so
libcrypto.so

10. -Wl,-rpath-link:寻找共享库的间接依赖

例如:

-Wl,-rpath-link,$ROOTFS/usr/lib/aarch64-linux-gnu

这里:

-Wl,

表示:

后面的参数不是给 GCC 自己,而是传递给 GNU ld。

而:

-rpath-link

主要用于​链接阶段寻找共享库的间接依赖​。

例如:

程序
 ↓
libA.so
 ↓
libB.so

程序直接链接:

libA.so

libA.so 自己还依赖:

libB.so

那么:

-Wl,-rpath-link,$ROOTFS/usr/lib/aarch64-linux-gnu

可以帮助链接器找到 libB.so

常见配置:

-Wl,-rpath-link,$ROOTFS/usr/lib/aarch64-linux-gnu
-Wl,-rpath-link,$ROOTFS/lib/aarch64-linux-gnu

注意它主要影响​链接阶段​,不要简单地把它和程序运行时的 RPATH/RUNPATH 混为一谈。

11. 一套典型 Ubuntu ARM64 Sysroot 配置

如果使用非 Debian/Ubuntu 原生布局的 AArch64 GNU Toolchain,同时希望以 Ubuntu ARM64 rootfs 作为目标环境,可以参考:

ROOTFS := /opt/sysroots/ubuntu-arm64

SYSROOT_FLAGS := \
	--sysroot=$(ROOTFS) \
	-isystem $(ROOTFS)/usr/include/aarch64-linux-gnu \
	-isystem $(ROOTFS)/usr/include \
	-B$(ROOTFS)/usr/lib/aarch64-linux-gnu/

ROOTFS_LIBS := \
	-L$(ROOTFS)/usr/lib/aarch64-linux-gnu \
	-L$(ROOTFS)/lib/aarch64-linux-gnu \
	-Wl,-rpath-link,$(ROOTFS)/usr/lib/aarch64-linux-gnu \
	-Wl,-rpath-link,$(ROOTFS)/lib/aarch64-linux-gnu

项目自己的头文件仍然使用:

CFLAGS += \
	-I./include \
	-I./libs

例如 GLib 这种有自己 include 布局的库:

CFLAGS += \
	-I$(ROOTFS)/usr/include/glib-2.0 \
	-I$(ROOTFS)/usr/lib/aarch64-linux-gnu/glib-2.0/include

12. 最小测试非常重要

在修改大型项目之前,最好先创建:

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    printf("Hello AArch64!\n");
    return 0;
}

然后:

$CC \
    --sysroot=$ROOTFS \
    -isystem $ROOTFS/usr/include/aarch64-linux-gnu \
    -isystem $ROOTFS/usr/include \
    -B$ROOTFS/usr/lib/aarch64-linux-gnu/ \
    -L$ROOTFS/usr/lib/aarch64-linux-gnu \
    -L$ROOTFS/lib/aarch64-linux-gnu \
    -Wl,-rpath-link,$ROOTFS/usr/lib/aarch64-linux-gnu \
    -Wl,-rpath-link,$ROOTFS/lib/aarch64-linux-gnu \
    test.c \
    -o test

然后:

file test

正确情况下应该看到类似:

ELF 64-bit LSB executable, ARM aarch64, dynamically linked

再检查动态加载器:

readelf -l test | grep interpreter

应该类似:

Requesting program interpreter: /lib/ld-linux-aarch64.so.1

这样可以先证明:

Toolchain + sysroot + glibc + startup files + linker 的基础组合是正确的。

之后再处理大型项目自身的第三方库问题。

13. 如何查看 GCC 实际搜索了哪些头文件目录?

这是排查交叉编译问题非常有用的方法:

echo | $CC \
    --sysroot=$ROOTFS \
    -E -v -

观察:

#include <...> search starts here:
...
End of search list.

可以快速发现:

  • 是否错误使用了宿主机 /usr/include
  • 是否遗漏 ARM64 Multiarch include
  • 是否混入其他 sysroot
  • GCC 到底使用了哪套 libc header

14. 如何查看 GCC 默认 Sysroot?

$CC -print-sysroot

如果工具链自身已经包含 sysroot,会输出对应路径。

这也能解释一个常见现象:

不指定 --sysroot 时能编译,一指定自己的 rootfs 反而出现大量错误。

因为前一种情况下 GCC 使用的是​工具链自己完整且匹配的 sysroot​;后一种情况下换成了 Ubuntu rootfs,但 GCC 不一定知道 Ubuntu Multiarch 目录应该怎么搜索。

15. 不要混用两套 glibc 头文件

交叉编译中一个很危险的配置是:

GCC 自带 glibc
        +
Ubuntu rootfs glibc headers
        +
Ubuntu rootfs libc.so

例如简单粗暴地:

-I$ROOTFS/usr/include

可能导致:

stdlib.h      来自 Ubuntu rootfs
features.h    来自另一套 libc
bits/*.h      又来自 GCC 自带 sysroot

最后报错位置可能出现在:

stdlib.h
stdio.h
pthread.h

甚至出现一些看起来完全不合理的语法错误。

这种情况下:

不要看到 ​ stdlib.h ​ 报错就认为 glibc 的 ​ stdlib.h​ 写错了。

真正的问题通常是 include 搜索路径污染或两套 libc header 被混合使用。

16. 静态库与共享库还有一个 Visibility 陷阱

大型 Runtime 或组件化工程中还可能遇到:

hidden symbol `SomeFunction' ... is referenced by DSO

这里:

DSO = Dynamic Shared Object

通常就是 .so

假设:

主程序
 ├── libComponent.a
 │      └── SomeFunction (GLOBAL HIDDEN)
 │
 └── libPlugin.so
        └── UND SomeFunction

共享库 libPlugin.so 希望从主程序解析:

SomeFunction

但是静态组件中的这个符号被设置为:

GLOBAL HIDDEN

那么即使主程序使用:

-Wl,-E

或者:

-Wl,--export-dynamic

也无法把 hidden symbol 作为普通动态符号导出给 DSO。

可以使用:

readelf -Ws libComponent.a | grep SomeFunction

检查定义。

使用:

readelf -Ws libPlugin.so | grep SomeFunction

检查共享库引用。

如果看到:

GLOBAL HIDDEN ... SomeFunction

和:

GLOBAL DEFAULT UND SomeFunction

基本就能确认问题。

如果共享模块本身由自己维护,一个可行方案是根据整体架构考虑将其静态集成:

plugin.c
   ↓
libPlugin.a
   ↓
最终 executable

让符号在最终 ELF 内部完成解析,而不是跨 DSO 边界解析 hidden symbol。

当然,对于插件化框架,更正规的方式通常是使用框架提供的 API、函数表或组件接口,而不是直接依赖内部 hidden symbol。

17. 常用排查命令

检查目标文件架构:

file program

检查动态加载器:

readelf -l program | grep interpreter

检查动态依赖:

readelf -d program | grep NEEDED

检查符号:

readelf -Ws library.so

检查动态符号:

nm -D library.so

检查静态库符号:

nm -A library.a

检查未定义符号:

readelf -Ws library.so |
awk '$7 == "UND" {print $8}' |
sort -u

寻找 CRT 文件:

find $ROOTFS \
    -name crt1.o \
    -o -name crti.o \
    -o -name crtn.o

查看 GCC include 搜索路径:

echo | $CC --sysroot=$ROOTFS -E -v -

查看 GCC 默认 sysroot:

$CC -print-sysroot

18. 参数速查表

参数 主要作用 典型用途
--sysroot=DIR 指定目标系统根目录 ARM64 rootfs
-I DIR 添加普通头文件路径 项目自己的头文件
-isystem DIR 添加系统头文件路径 glibc、系统 headers
-idirafter DIR 添加低优先级系统头文件路径 从其他 sysroot 兜底寻找
-B DIR GCC 搜索前缀 crt1.ocrti.o
-L DIR 添加链接库搜索目录 .so.a
-Wl,... 将参数传递给 linker 控制 GNU ld
-Wl,-rpath-link,DIR 查找共享库间接依赖 rootfs 中其他.so
-Wl,-E 导出 executable 的动态符号 DSO 需要访问主程序符号
-Wl,--whole-archive 强制加入静态库所有对象 组件式静态库

19. 最终理解

整个交叉编译过程可以抽象成:

x86_64 Ubuntu
       │
       │ 运行
       ▼
AArch64 Cross GCC
       │
       ├── --sysroot
       │       ↓
       │   ARM64 RootFS
       │
       ├── -I / -isystem
       │       ↓
       │   查找头文件
       │
       ├── -B
       │       ↓
       │   查找 CRT 等特殊文件
       │
       ├── -L
       │       ↓
       │   查找 .so / .a
       │
       └── -rpath-link
               ↓
           查找 .so 的间接依赖

最终生成:

ARM64 ELF
       │
       ▼
目标 ARM64 Linux 系统运行

真正需要记住的核心原则只有几条:

  1. 交叉编译器、目标 rootfs 和目标运行环境必须保持 ABI 兼容。
  2. 不要无意间混用 host x86_64 与 target AArch64 的头文件和库。
  3. 尽量不要混用不同 glibc sysroot 的系统头文件。
  4. Ubuntu/Debian 使用 Multiarch,非 Debian 工具链可能需要手工补 ​ aarch64-linux-gnu​ 路径。
  5. 头文件找不到看 ​-I/-isystem​**,CRT 找不到看 ​-B,库找不到看 ​-L,共享库间接依赖找不到看 ​-rpath-link。**
  6. 先用最小程序验证 toolchain + sysroot,再处理大型工程本身的链接问题。
  7. 看到系统头文件内部出现离谱语法错误时,首先检查 include 路径污染,而不是修改系统头文件。
  8. 看到 ​ hidden symbol ... referenced by DSO​ 时,应检查 ELF symbol visibility 和静态/动态链接边界,而不是继续调整 sysroot。

掌握这些关系之后,绝大多数 Linux AArch64 交叉编译问题都可以按照“​头文件 → CRT → 库 → 间接依赖 → ELF 符号​”这个顺序逐层定位。