在 HarmonyOS 上从源码构建 GCC 16(gcc/g++ + libstdc++ + libsanitizer):完整记录
2026/8/31 18:25:20 网站建设 项目流程

在 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
  • 链接器:编译期用 GNUld(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/.../HomebrewBUILD="$CACHE/manual-builds/gcc-build-16"SYS=/storage/Users/currentUser/.harmonybrew/Cellar/ohos-sdk/26.0.0.18_1/native/sysrootSDK=/storage/Users/currentUser/.harmonybrew/Cellar/ohos-sdk/26.0.0.18_1/nativeBP=/storage/Users/currentUser/.harmonybrew/opt/binutilsSRC=/storage/Users/currentUser/ProjectSources/gcc# 1) 让 GCC 在多架构目录里找到 crt/libc(GCC 的多架构目录名是 aarch64-linux-musl)ln-sfnaarch64-linux-ohos"$SYS/usr/lib/aarch64-linux-musl"ln-sfnaarch64-linux-ohos"$SYS/usr/include/aarch64-linux-musl"# 注意是 -> ohos,不是 -> .# 2) 编译期工具目录:as/ar/nm 等用 binutils;ld 指向「GNU ld + 签名」wrappermkdir-p"$BUILD/tools"fortinas ar nm ranlib objdump objcopy strip readelf;doln-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 EOFchmod+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";exportTMPDIR="$BUILD/tmp"TMP="$BUILD/tmp"TEMP="$BUILD/tmp"cd"$BUILD"envPATH="$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 输出旧。fordingmp-6.3.0 mpfr-4.2.2 mpc-1.3.1 isl-0.24 gettext-0.22;dofind"$SRC/$d"-typef-exectouch-d'2026-08-01 00:00:00'{}+2>/dev/nullfind"$SRC/$d"-typef\(-nameaclocal.m4-o-nameconfigure-o-nameMakefile.in-o-nameconfig.h.in\)\-exectouch-d'2026-08-15 00:00:00'{}+2>/dev/nulldone# 5) 用「追加 -fPIC」的 clang wrapper 当宿主编译器(见 §5 的 PIC 坑)cat>"$BUILD/tools/clang"<<'EOF' #!/bin/sh exec /storage/Users/currentUser/Installed/clang-22/bin/clang "$@" -fPIC EOFcat>"$BUILD/tools/clang++"<<'EOF' #!/bin/sh exec /storage/Users/currentUser/Installed/clang-22/bin/clang++ "$@" -fPIC EOFchmod+x"$BUILD/tools/clang""$BUILD/tools/clang++"# 重新 configure(把 wrapper 当作 CC/CXX 烘焙进 Makefile)envPATH="$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__(...) EOFmake-j4all\"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) installmakeinstall\"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 的stdoutCOPY 重定位截断
-lgcc -lgcc_ehmusl 目标不装共享 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 的 stublibc.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-ofoo
  • --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 foundOHOS 头文件把ffs藏在 feature 宏后面CPPFLAGS=-D_GNU_SOURCE
aclocal-1.16: inaccessiblegettext tarball 时间戳乱,触发 autotools 重生成统一 touch 生成文件的时间戳(§2 步骤 4)
too many decimal points in numberOHOS 头用 clang 的__availability__(ohos, introduced=12.0.0)-D__availability__(...)=-include noavail.h
链接报undefined symbol: _Unwind_Resumemusl 不装共享 libgcc_s,默认 spec 缺 unwinderspecs 里*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.6specs 里加-rpath $prefix/lib64
fwrite(stdout)段错误(printf却正常)stdoutCOPY 重定位只有 4 字节默认-fPIC,走 GOT
clang 编 GCC 时 lld 报improper alignment ... LDST64_ABS_LO12_NCclang 非 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;最小示例:

importstd;automain()->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-j4all-target-libsanitizer"CFLAGS_FOR_TARGET=$TFLAGS""CXXFLAGS_FOR_TARGET=$TFLAGS"makeinstall-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 offset0x1000000000(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 上的进一步适配,思路与上文一致。)

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询