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 相比,它主要有两个特点:
- 按 GCC 的 system header 规则处理搜索顺序
- 系统头文件内部的一些编译警告会被 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.o、crti.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 系统运行
真正需要记住的核心原则只有几条:
- 交叉编译器、目标 rootfs 和目标运行环境必须保持 ABI 兼容。
- 不要无意间混用 host x86_64 与 target AArch64 的头文件和库。
- 尽量不要混用不同 glibc sysroot 的系统头文件。
- Ubuntu/Debian 使用 Multiarch,非 Debian 工具链可能需要手工补
aarch64-linux-gnu 路径。 - 头文件找不到看
-I/-isystem**,CRT 找不到看 -B,库找不到看 -L,共享库间接依赖找不到看 -rpath-link。** - 先用最小程序验证 toolchain + sysroot,再处理大型工程本身的链接问题。
- 看到系统头文件内部出现离谱语法错误时,首先检查 include 路径污染,而不是修改系统头文件。
- 看到
hidden symbol ... referenced by DSO 时,应检查 ELF symbol visibility 和静态/动态链接边界,而不是继续调整 sysroot。
掌握这些关系之后,绝大多数 Linux AArch64 交叉编译问题都可以按照“头文件 → CRT → 库 → 间接依赖 → ELF 符号”这个顺序逐层定位。