在 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 里的 GNU as
  • 链接器:编译期用 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::printlnstd::expectedstd::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.soNEEDED 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. 构建命令

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 里放:

*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 那样自动 include features.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.sostdout 符号大小是 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;

g++ -std=c++23 -fmodules --compile-std-module foo.cpp -o foo
  • --compile-std-module 会让驱动把 bits/stdc++.hbits/std.ccbits/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 importedstd.gcm~ 0 字节 hmdfs 上 mmap 写大 gcm 失败 在 hmfs 目录编译,或软链 gcm.cache 到 hmfs

9. 验证结果

$ ~/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; 最小示例:

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;但见下面的「注意」)。

# 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 风格)里这两套同名结构体
会冲突:

error: redefinition of 'struct sysinfo'
error: redefinition of 'struct sockaddr_storage'

解法:给 target 编译加两个 include guard,跳过冲突的 linux 头:

-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.8libubsan.so.1liblsan.so.0
libtsan.so.2libhwasan.so.0

10.3 ASan/LSan/HWASan 运行时失败:39-bit VA

现象:

==pid==Error: heap size 40000002000 exceeds max user virtual address 7fffffffff
==pid==ERROR: AddressSanitizer failed to allocate ... at address 0x040000000000 (error code: 22)

还有一条 ASan 特有的、更早报的:

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.

两条的根因:

  1. does not come first:musl 的 dl_iterate_phdr 顺序里 libc 排在 libasan 之前,
    而 ASan 这个检查是按 glibc 的 DSO 顺序写的。可先用
    ASAN_OPTIONS=verify_asan_link_order=0 跳过这个检查。

  2. 39-bit VA:这台机器的内核是 CONFIG_ARM64_VA_BITS=39(用户 VA 上限 512GB =
    0x7fffffffff)。但 GCC 的 libsanitizer 对「非 Android 的 aarch64」用的是旧的 48-bit 假设:

    • libsanitizer/asan/asan_allocator.hkAllocatorSize = 0x40000000000(4TB),
      而 Android 分支用的是 0x2000000000(128G,注释明说 “Android needs to support
      39, 42 and 48 bit VMA”);
    • libsanitizer/asan/asan_mapping.h:aarch64 用固定 shadow offset 0x1000000000(1<<36);
    • 编译器侧 gcc/config/aarch64/aarch64.ccaarch64_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.compatimport std; 在 hmdfs 上的进一步适配,思路与上文一致。)

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐