在 HarmonyOS 上从源码构建 GCC 16(gcc/g++ + libstdc++ + libsanitizer):完整记录
注:本文涉及的所有工作以及本文的编写全部由 AI 完成。这么繁琐的事情当然是要交给 AI 了。
本文记录在 MateBook Pro S(HarmonyOS 7,aarch64,musl)上,用 Harmonybrew 的
binutils+ 已自建的clang-22作为宿主编译器,从releases/gcc-16分支把GCC 16 的
c/c++编译器以及libgcc/libstdc++全部构建出来,安装到
~/Installed/gcc-16,最终做到g++ foo.cpp -o foo零参数编译、跑通 C++23 与
import std;的全过程。与 LLVM 那份记录一样,绝大多数坑来自「HarmonyOS 看起来像 Linux、又不完全是」:
uname -s返回HarmonyOS、/tmp只读、~/挂在 hmdfs(分布式文件系统,行为怪异)、ELF 必须签名、OHOS 头文件是给 clang 写的。
0. 结论先说
- 安装位置:
/storage/Users/currentUser/Installed/gcc-16 - 目标 triple:
aarch64-linux-musl(GCC 的 musl 动态链接器正是/lib/ld-musl-aarch64.so.1,与鸿蒙运行时一致) - 宿主编译器:
~/Installed/clang-22/bin/clang{,++}(你们之前编的 LLVM 22) - 汇编器:brew 的
binutils里的 GNUas - 链接器:编译期用 GNU
ld(binutils)+ 自动签名 wrapper;安装后的 gcc 默认链接器仍是 SDK 的ld.lld(自动--code-sign) - 最终用法:
g++ hello.cpp -o hello------ 零参数(sysroot/-I/-L/-rpath 全部 baked)g++ -std=c++23 foo.cpp -o foo------ C++23 特性(std::println、std::expected、std::ranges等)g++ -std=c++23 -fmodules --compile-std-module foo.cpp -o foo------import std;
1. 关键前提
- 机器是 aarch64,4 核,内存 23G(可用约 7G + 50G swap)。
- 运行时:
/lib/ld-musl-aarch64.so.1就是 musl 的加载器,它自己同时扮演libc.so
(所以系统里看不到单独的libc.so,NEEDED libc.so会被加载器自解析)。 - 头文件/链接库都在 ohos-sdk 的 sysroot 里:
~/.harmonybrew/Cellar/ohos-sdk/26.0.0.18_1/native/sysroot,
libc 头在usr/include,arch 相关库在usr/lib/aarch64-linux-ohos。 - 坑 1:
~/挂在 hmdfs 上,/tmp只读。 构建目录必须放到非 hmdfs 的挂载点上。
$(brew --cache)落在/data/storage/el2/base/haps/entry/files/Homebrew(真正的 hmfs),
行为正常。本文所有构建都在
$(brew --cache)/manual-builds/gcc-build-16下进行。
2. 构建命令
bash
CACHE=$(brew --cache) # /data/storage/el2/.../Homebrew
BUILD="$CACHE/manual-builds/gcc-build-16"
SYS=/storage/Users/currentUser/.harmonybrew/Cellar/ohos-sdk/26.0.0.18_1/native/sysroot
SDK=/storage/Users/currentUser/.harmonybrew/Cellar/ohos-sdk/26.0.0.18_1/native
BP=/storage/Users/currentUser/.harmonybrew/opt/binutils
SRC=/storage/Users/currentUser/ProjectSources/gcc
# 1) 让 GCC 在多架构目录里找到 crt/libc(GCC 的多架构目录名是 aarch64-linux-musl)
ln -sfn aarch64-linux-ohos "$SYS/usr/lib/aarch64-linux-musl"
ln -sfn aarch64-linux-ohos "$SYS/usr/include/aarch64-linux-musl" # 注意是 -> ohos,不是 -> .
# 2) 编译期工具目录:as/ar/nm 等用 binutils;ld 指向「GNU ld + 签名」wrapper
mkdir -p "$BUILD/tools"
for t in as ar nm ranlib objdump objcopy strip readelf; do
ln -sfn "$BP/bin/$t" "$BUILD/tools/$t"; done
# 见 §4:ld 用 GNU ld(bfd),因为 lld 会拒绝 clang 生成的一种 misaligned 重定位
# 此处先放一个 wrapper:GNU ld 链接后自动 binary-sign-tool 自签名
cat > "$BUILD/tools/ld-real-wrapper" <<'EOF'
#!/bin/sh
GNULD=/storage/Users/currentUser/.harmonybrew/opt/binutils/bin/ld
SIGN=/storage/Users/currentUser/.harmonybrew/bin/binary-sign-tool
out=""; reloc=0; prev_o=0
for a in "$@"; do
[ $prev_o -eq 1 ] && { out="$a"; prev_o=0; }
case "$a" in -r) reloc=1;; -o) prev_o=1;; --output=*) out="${a#--output=}";; -o*) out="${a#-o}";; esac
done
"$GNULD" "$@"; rc=$?
[ $rc -eq 0 ] && [ $reloc -eq 0 ] && [ -n "$out" ] && [ -f "$out" ] && \
"$SIGN" sign -selfSign 1 -inFile "$out" -outFile "$out" >/dev/null 2>&1
exit $rc
EOF
chmod +x "$BUILD/tools/ld-real-wrapper"
ln -sfn "$BUILD/tools/ld-real-wrapper" "$BUILD/tools/ld"
ln -sfn "$BUILD/tools/ld-real-wrapper" "$BUILD/tools/ld.lld"
# 3) configure(out-of-tree;build 目录必须在 hmfs 上!)
mkdir -p "$BUILD/tmp"; export TMPDIR="$BUILD/tmp" TMP="$BUILD/tmp" TEMP="$BUILD/tmp"
cd "$BUILD"
env PATH="$BUILD/tools:$PATH" CC="$BP/../opt/binutils/bin/as" \
CC=/storage/Users/currentUser/Installed/clang-22/bin/clang \
CXX=/storage/Users/currentUser/Installed/clang-22/bin/clang++ \
AR="$BP/bin/ar" RANLIB="$BP/bin/ranlib" NM="$BP/bin/nm" OBJDUMP="$BP/bin/objdump" \
CPPFLAGS="-D_GNU_SOURCE" \
"$SRC/configure" \
--prefix=/storage/Users/currentUser/Installed/gcc-16 \
--with-sysroot="$SYS" \
--build=aarch64-linux-musl --host=aarch64-linux-musl --target=aarch64-linux-musl \
--enable-languages=c,c++ \
--disable-multilib --enable-multiarch --disable-bootstrap --enable-checking=release \
--disable-nls \
--disable-libsanitizer --disable-libquadmath --disable-libssp \
--disable-libgomp --disable-libitm --disable-libvtv \
--with-as="$BP/bin/as" --with-ld="$SDK/llvm/bin/ld.lld"
# 4) 时间戳修正:gmp/mpfr/mpc/isl/gettext 都是从 tarball 解压的,时间戳乱,
# make 会去跑 aclocal-1.16(没装)重新生成 autotools 文件。
# 把「生成文件」统一改成比「源文件」新、比 build 输出旧。
for d in gmp-6.3.0 mpfr-4.2.2 mpc-1.3.1 isl-0.24 gettext-0.22; do
find "$SRC/$d" -type f -exec touch -d '2026-08-01 00:00:00' {} + 2>/dev/null
find "$SRC/$d" -type f \( -name aclocal.m4 -o -name configure -o -name Makefile.in -o -name config.h.in \) \
-exec touch -d '2026-08-15 00:00:00' {} + 2>/dev/null
done
# 5) 用「追加 -fPIC」的 clang wrapper 当宿主编译器(见 §5 的 PIC 坑)
cat > "$BUILD/tools/clang" <<'EOF'
#!/bin/sh
exec /storage/Users/currentUser/Installed/clang-22/bin/clang "$@" -fPIC
EOF
cat > "$BUILD/tools/clang++" <<'EOF'
#!/bin/sh
exec /storage/Users/currentUser/Installed/clang-22/bin/clang++ "$@" -fPIC
EOF
chmod +x "$BUILD/tools/clang" "$BUILD/tools/clang++"
# 重新 configure(把 wrapper 当作 CC/CXX 烘焙进 Makefile)
env PATH="$BUILD/tools:$PATH" \
CC="$BUILD/tools/clang" CXX="$BUILD/tools/clang++" \
AR="$BP/bin/ar" RANLIB="$BP/bin/ranlib" NM="$BP/bin/nm" OBJDUMP="$BP/bin/objdump" \
CPPFLAGS="-D_GNU_SOURCE" \
"$SRC/configure" [......参数同上......]
# 6) build(target 库需要一个头文件把 clang 的 __availability__ 属性消掉)
cat > "$BUILD/noavail.h" <<'EOF'
/* Neutralize Clang-only __availability__ attribute for GCC. */
#define __availability__(...)
EOF
make -j4 all \
"CFLAGS_FOR_TARGET=-g -O2 -include $BUILD/noavail.h -DHAVE_DECL_BASENAME=1" \
"CXXFLAGS_FOR_TARGET=-g -O2 -include $BUILD/noavail.h -DHAVE_DECL_BASENAME=1"
# 7) install
make install \
"CFLAGS_FOR_TARGET=-g -O2 -include $BUILD/noavail.h -DHAVE_DECL_BASENAME=1" \
"CXXFLAGS_FOR_TARGET=-g -O2 -include $BUILD/noavail.h -DHAVE_DECL_BASENAME=1"
说明:上面为「讲清原理」写得很啰嗦,实际一条条跑即可。构建时间:4 核大约 2~3 小时
(不含各种踩坑重试)。
3. 安装后的「零参数」收尾:specs 文件
GCC 会自动加载 $prefix/lib/gcc/<target>/<version>/specs。我们在
~/Installed/gcc-16/lib/gcc/aarch64-linux-musl/16.2.1/specs 里放:
text
*cc1_options:
+ -D__availability__(...)= -fPIC
*libgcc:
-lgcc -lgcc_eh
*link:
+ -rpath /storage/Users/currentUser/Installed/gcc-16/lib64 -rpath /storage/Users/currentUser/Installed/gcc-16/lib
四条各自的作用:
| 条目 | 作用 |
|---|---|
-D__availability__(...)= |
把 OHOS 头文件里的 clang 专用 __attribute__((__availability__(ohos, introduced=12.0.0))) 消成空,否则 GCC 会报 too many decimal points in number |
-fPIC |
让 GCC 生成 PIC 代码,全局变量走 GOT,避免 §6 的 stdout COPY 重定位截断 |
-lgcc -lgcc_eh |
musl 目标不装共享 libgcc_s,默认 -lgcc -lgcc_s_asneeded 会缺 _Unwind_Resume;改成静态 libgcc_eh.a |
-rpath ... |
让运行时找得到 lib64/libstdc++.so.6 等(等价于你们 clang 里的 -Wl,-rpath) |
注意:specs 里每个
*xxx:条目之间要留一个空行 ,否则 GCC 会把后面的*yyy:当参数去执行。
4. 坑:clang 生成的 R_AARCH64_LDST64_ABS_LO12_NC 与 lld 不兼容
clang 在 aarch64 上(非 PIC 模式,访问局部字符串常量)会生成
R_AARCH64_LDST64_ABS_LO12_NC 重定位,且目标地址可能只有 4 字节对齐:
- lld :直接报
improper alignment for relocation ... is not aligned to 8 bytes。 - GNU ld(bfd) :不检查这个对齐,但非 PIC 访问动态符号
stdout时会报
relocation truncated to fit(外部符号不能用绝对寻址)。
结论:宿主编译 GCC 自身时,必须让 clang 生成 PIC 代码 (外部符号走 GOT)。
GCC 的 Makefile 会追加 -fno-PIE,它会覆盖 -fPIC,所以不能只把 -fPIC 塞进
CFLAGS(会排在 -fno-PIE 之前)。正解是 §2 里的 clang wrapper :在参数列表
最后 追加 -fPIC,让它始终是最后一个 PIC 相关选项。
5. 坑:target 库需要 -D_GNU_SOURCE / __availability__ / basename
- OHOS 的
<strings.h>把ffs藏在_XOPEN_SOURCE || _GNU_SOURCE || _BSD_SOURCE
后面,且 OHOS 头文件不像原生 musl 那样自动 includefeatures.h。所以宿主 ISL 的
configure 会报No ffs implementation found。解法:configure 时加CPPFLAGS="-D_GNU_SOURCE"。 - OHOS 头文件大量使用 clang 专用
__attribute__((__availability__(ohos, introduced=12.0.0)))
(288 个头文件、6670 处)。GCC 解析不了12.0.0。解法:target 库编译时
-include noavail.h把__availability__(...)定义成空。 -D_GNU_SOURCE会让系统的basename声明暴露出来,与 libiberty.h 里的
basename冲突(conflicting types for 'basename')。解法:target 编译时加
-DHAVE_DECL_BASENAME=1,让 libiberty.h 跳过自己的声明。
6. 坑:stdout 的 COPY 重定位只有 4 字节 → 运行时段错误
现象:printf(隐式 stdout)正常,但 fwrite(..., stdout) / fprintf(stdout, ...)
段错误。原因是 OHOS sysroot 的 stub libc.so 里 stdout 符号大小是 4 字节 ,
GCC 生成 R_AARCH64_COPY 时只拷 4 字节,而真实 FILE * 是 8 字节指针,被截断。
- clang 为什么没事:clang 默认 PIE,
stdout走 GOT(R_AARCH64_GLOB_DAT),不 COPY。 - 解法:让 gcc 默认也生成 PIC(见 §3 的
-fPIC),全局变量走 GOT,不再 COPY。
7. import std; 用法与 hmdfs 的坑
编译 import std;:
bash
g++ -std=c++23 -fmodules --compile-std-module foo.cpp -o foo
--compile-std-module会让驱动把bits/stdc++.h、bits/std.cc、bits/std.compat.cc
三个模块单元先编译成gcm.cache/std.gcm等(首次较慢,之后按 gcm 缓存复用)。- 编译产物运行需要 libstdc++,已由 §3 的
-rpath解决。
hmdfs 坑 :gcm.cache/std.gcm 是 GCC 用 mmap 写的大文件(约 30MB)。在 ~/
(hmdfs 分布式文件系统)上这个写会失败(得到 0 字节的 std.gcm~,报
imports must be built before being imported)。在 $(brew --cache) 这类 hmfs
挂载点上则正常。所以:要 import std; 时,在非 hmdfs 目录里编译 (或把
gcm.cache 软链到 hmfs 上的目录)。
8. 踩坑速查
| 现象 | 原因 | 解法 |
|---|---|---|
config.status: can't create ./confXXXXXX/subs1.awk: Permission denied |
~/ 挂 hmdfs,umask 077 建的目录带 setgid 位,沙箱拒绝写入 |
构建目录挪到 $(brew --cache)(hmfs) |
configure: error: No ffs implementation found |
OHOS 头文件把 ffs 藏在 feature 宏后面 |
CPPFLAGS=-D_GNU_SOURCE |
aclocal-1.16: inaccessible |
gettext tarball 时间戳乱,触发 autotools 重生成 | 统一 touch 生成文件的时间戳(§2 步骤 4) |
too many decimal points in number |
OHOS 头用 clang 的 __availability__(ohos, introduced=12.0.0) |
-D__availability__(...)= 或 -include noavail.h |
链接报 undefined symbol: _Unwind_Resume |
musl 不装共享 libgcc_s,默认 spec 缺 unwinder | specs 里 *libgcc: -lgcc -lgcc_eh |
conflicting types for 'basename' |
-D_GNU_SOURCE 暴露系统 basename 与 libiberty 冲突 |
-DHAVE_DECL_BASENAME=1 |
运行报 Error loading shared library libstdc++.so.6 |
运行时找不到 lib64/libstdc++.so.6 |
specs 里加 -rpath $prefix/lib64 |
fwrite(stdout) 段错误(printf 却正常) |
stdout COPY 重定位只有 4 字节 |
默认 -fPIC,走 GOT |
clang 编 GCC 时 lld 报 improper alignment ... LDST64_ABS_LO12_NC |
clang 非 PIC 代码 + lld 严格检查 | 用「追加 -fPIC」的 clang wrapper;链接用 GNU ld |
import std; 报 imports must be built before being imported,std.gcm~ 0 字节 |
hmdfs 上 mmap 写大 gcm 失败 | 在 hmfs 目录编译,或软链 gcm.cache 到 hmfs |
9. 验证结果
text
$ ~/Installed/gcc-16/bin/gcc hello.c -o h && ./h # hello-gcc-16-C
$ ~/Installed/gcc-16/bin/g++ hello.cpp -o h && ./h # 零参数,vector 正常
$ ~/Installed/gcc-16/bin/g++ -std=c++23 p.cpp -o p && ./p # std::println / std::expected / std::ranges
$ ~/Installed/gcc-16/bin/g++ -std=c++23 -fmodules --compile-std-module m.cpp -o m && ./m
# import std; + std::println + ranges::fold_left
import std; 最小示例:
cpp
import std;
auto main() -> int {
std::println("import std works: {}", 42);
std::vector<int> v{1,2,3,4};
std::println("fold sum = {}", std::ranges::fold_left(v, 0, std::plus{}));
}
10. 构建 sanitizers(libsanitizer)
前面 §2 的 configure 里我显式关了 --disable-libsanitizer,所以要补编 sanitizer
运行时,只需重新 configure 去掉这个开关 ,再单独编 libsanitizer 这个 target
库(不用重编整个 GCC;但见下面的「注意」)。
bash
# 1) 重新 configure(去掉 --disable-libsanitizer,其余参数与 §2 完全相同)
# CC/CXX 仍是 tools/clang{,++}(追加 -fPIC 的 wrapper)
"$SRC/configure" \
--prefix=... --with-sysroot="$SYS" \
--build=aarch64-linux-musl --host=aarch64-linux-musl --target=aarch64-linux-musl \
--enable-languages=c,c++ --disable-multilib --enable-multiarch \
--disable-bootstrap --enable-checking=release --disable-nls \
--disable-libquadmath --disable-libssp --disable-libgomp --disable-libitm --disable-libvtv \
--with-as="$BP/bin/as" --with-ld="$SDK/llvm/bin/ld.lld" # 注意:没有 --disable-libsanitizer
# 2) 单独编 + 装 libsanitizer(带 OHOS 头文件冲突的 guard,见下)
TFLAGS="-g -O2 -include $BUILD/noavail.h -DHAVE_DECL_BASENAME=1 -D_LINUX_SYSINFO_H -D_UAPI_LINUX_SOCKET_H"
make -j4 all-target-libsanitizer "CFLAGS_FOR_TARGET=$TFLAGS" "CXXFLAGS_FOR_TARGET=$TFLAGS"
make install-target-libsanitizer "CFLAGS_FOR_TARGET=$TFLAGS" "CXXFLAGS_FOR_TARGET=$TFLAGS"
注意 :重新 configure 会触发一次级联重编(host 库 + gcc 自身 + 各 target 库都重来一遍,
因为 top 的 config.status 变了)。而且中途会撞 autoconf 的 cache 一致性检查
(
CFLAGS has changed since the previous run),把$BUILD/aarch64-linux-musl/*/config.cache清掉即可。代价约 1.5 小时,属于重新 configure 的固有成本。
10.1 坑:struct sysinfo / struct sockaddr_storage 重定义
libsanitizer 是 LLVM compiler-rt 的移植,sanitizer_platform_limits_posix.cpp 会同时
include <sys/sysinfo.h> / <sys/socket.h> 和 <linux/sysctl.h>(间接拉进
<linux/sysinfo.h> / <linux/socket.h>)。OHOS 头文件(bionic 风格)里这两套同名结构体
会冲突:
text
error: redefinition of 'struct sysinfo'
error: redefinition of 'struct sockaddr_storage'
解法:给 target 编译加两个 include guard,跳过冲突的 linux 头:
text
-D_LINUX_SYSINFO_H -D_UAPI_LINUX_SOCKET_H
(这正是你在 LLVM compiler-rt 里撞到、最后靠关掉 sanitizer 绕开的那类问题。)
10.2 实测结果
| Sanitizer | 命令 | 结果 |
|---|---|---|
| UBSan | -fsanitize=undefined |
✅ 可用(实测检出 signed integer overflow + 堆栈) |
| TSan | -fsanitize=thread |
✅ 可用(实测检出 data race) |
| ASan | -fsanitize=address |
⚠️ 编译/链接成功,运行时挂(见 10.3) |
| LSan | 随 ASan | ⚠️ 同上 |
| HWASan | -fsanitize=hwaddress |
⚠️ 同上 |
装好的运行时在 $prefix/lib64/:libasan.so.8、libubsan.so.1、liblsan.so.0、
libtsan.so.2、libhwasan.so.0。
10.3 ASan/LSan/HWASan 运行时失败:39-bit VA
现象:
text
==pid==Error: heap size 40000002000 exceeds max user virtual address 7fffffffff
==pid==ERROR: AddressSanitizer failed to allocate ... at address 0x040000000000 (error code: 22)
还有一条 ASan 特有的、更早报的:
text
ASan runtime does not come first in initial library list; you should either
link runtime to your application or manually preload it with LD_PRELOAD.
两条的根因:
-
does not come first:musl 的dl_iterate_phdr顺序里 libc 排在 libasan 之前,而 ASan 这个检查是按 glibc 的 DSO 顺序写的。可先用
ASAN_OPTIONS=verify_asan_link_order=0跳过这个检查。 -
39-bit VA :这台机器的内核是
CONFIG_ARM64_VA_BITS=39(用户 VA 上限 512GB =0x7fffffffff)。但 GCC 的 libsanitizer 对「非 Android 的 aarch64」用的是旧的 48-bit 假设:libsanitizer/asan/asan_allocator.h:kAllocatorSize = 0x40000000000(4TB),
而 Android 分支用的是0x2000000000(128G,注释明说 "Android needs to support
39, 42 and 48 bit VMA");libsanitizer/asan/asan_mapping.h:aarch64 用固定 shadow offset0x1000000000(1<<36);- 编译器侧
gcc/config/aarch64/aarch64.cc的aarch64_asan_shadow_offset返回1<<36。
39-bit VA 不是鸿蒙独有------它是 ARM64 Linux 的标准内核配置项(Android 手机大量在用)。
LLVM/compiler-rt 早就用「128G 分配器 + 动态 shadow」适配了 39/42/48 位;GCC 的
libsanitizer 这个快照在 aarch64 非 Android 路径还没跟上。
修法方向(未做):把
asan_allocator.h里非 Android aarch64 的kAllocatorSize改成
0x2000000000(128G,配VeryCompactSizeClassMap),必要时再切动态 shadow;且要保证编译器侧 shadow offset 与运行时一致,需重编 libasan + 编译器。这是一个可提给
GCC(component=libsanitizer)的合理 bug:aarch64 Linux 在 39-bit VA 下 ASan 运行时失败,
而 Android 分支已处理。
(未完待续:ASan 的 39-bit VA 布局补丁、std.compat、import std; 在 hmdfs 上的进一步适配,思路与上文一致。)