部署机还是 CentOS 7.9 且不会升级系统,但新项目想用 C++23。 本文记录在 CentOS 7.9 上从源码构建 GCC 14.4.0 与 binutils 2.47,并让编出来的程序在原装 CentOS 7 上运行的过程。

为什么要自己编

  • devtoolset 最高只到 GCC 11,没有 <print>、<expected>。
  • 在 Rocky 9 等新系统上用 GCC 14 编,即使加了 -static-libstdc++ -static-libgcc,拿到 CentOS 7 上也跑不起来。 静态链接只解决了 C++ 运行时,glibc 用的还是编译机的版本。

在 Rocky 9.6(glibc 2.34)上用 gcc-toolset-14 编一个 C++23 程序:

// cxx23.cpp
#include <expected>
#include <print>
#include <string>

std::expected<int, std::string> Half(int x) {
  if (x % 2 != 0) return std::unexpected("odd");
  return x / 2;
}

int main() { std::println("{} {}", *Half(42), Half(3).error()); }
scl enable gcc-toolset-14 -- g++ -std=c++23 -O2 cxx23.cpp -o cxx23 -static-libstdc++ -static-libgcc
ldd ./cxx23
objdump -T cxx23 | grep -o -E 'GLIBC_[0-9.]+' | sort -uV | tail -1

ldd 里已经没有 libstdc++ 和 libgcc_s,但依赖的 glibc 最高到 2.34:

        linux-vdso.so.1 (0x00007fff79afe000)
        libm.so.6 => /lib64/libm.so.6 (0x00007fc55a59e000)
        libc.so.6 => /lib64/libc.so.6 (0x00007fc55a200000)
        /lib64/ld-linux-x86-64.so.2 (0x00007fc55a680000)
GLIBC_2.34

拷到 CentOS 7.9(glibc 2.17)上运行:

./cxx23: /lib64/libc.so.6: version `GLIBC_2.26' not found (required by ./cxx23)
./cxx23: /lib64/libc.so.6: version `GLIBC_2.25' not found (required by ./cxx23)
./cxx23: /lib64/libc.so.6: version `GLIBC_2.34' not found (required by ./cxx23)
./cxx23: /lib64/libc.so.6: version `GLIBC_2.32' not found (required by ./cxx23)

所以只能在 glibc 2.17 的环境里,也就是 CentOS 7 本身上编。

安装依赖

宿主编译器用 devtoolset-11,它在 SCL 源里。 CentOS 7 已经 EOL,SCL 源要改指向 vault 镜像:

sudo yum install centos-release-scl

sudo sed -E -e "s|^mirrorlist=|# mirrorlist=|g" \
  -e "s|^# ?baseurl=http://mirror.centos.org/centos/7|baseurl=https://mirrors.aliyun.com/centos-vault/7.9.2009|g" \
  -i.bak \
  /etc/yum.repos.d/CentOS-SCLo-scl*.repo

sudo yum clean all
sudo yum makecache fast

安装 devtoolset-11 和构建依赖:

sudo yum install devtoolset-11-gcc-c++ bzip2 m4
scl enable devtoolset-11 -- g++ --version

系统没有 texinfo 也没关系,正式发布的源码包里已经带了生成好的文档,构建时传 MAKEINFO=true 跳过即可。

下载源码

源码走国内镜像,签名与 keyring 从 GNU 官方下载,用来校验镜像上的包没被篡改。

mkdir -p "${HOME}/dev-tools/GCC/src" && cd "${HOME}/dev-tools/GCC/src"

curl -OL https://mirrors.aliyun.com/gnu/gcc/gcc-14.4.0/gcc-14.4.0.tar.xz
curl -OL https://mirrors.aliyun.com/gnu/binutils/binutils-2.47.tar.xz
curl -OL https://ftp.gnu.org/gnu/gcc/gcc-14.4.0/gcc-14.4.0.tar.xz.sig
curl -OL https://ftp.gnu.org/gnu/binutils/binutils-2.47.tar.xz.sig
curl -OL https://ftp.gnu.org/gnu/gnu-keyring.gpg

gpgv --keyring ./gnu-keyring.gpg gcc-14.4.0.tar.xz.sig gcc-14.4.0.tar.xz
gpgv --keyring ./gnu-keyring.gpg binutils-2.47.tar.xz.sig binutils-2.47.tar.xz

tar -Jxf gcc-14.4.0.tar.xz
tar -Jxf binutils-2.47.tar.xz

GMP、MPFR、MPC、ISL 用 GCC 自带的脚本下载,它会放进源码树一起编,并做 sha512 校验:

cd gcc-14.4.0
./contrib/download_prerequisites
cd ..

构建 binutils

CentOS 7 自带的 binutils 是 2.27,汇编不了 GCC 14 生成的部分代码,所以要一起编。 binutils 与 GCC 装到同一个前缀,GCC 会按自身位置的相对路径找到 as、ld,整个目录之后可以搬到任何地方。

source /opt/rh/devtoolset-11/enable
prefix="${HOME}/dev-tools/GCC/gcc-14.4.0"

mkdir build-binutils && cd build-binutils
../binutils-2.47/configure --prefix="${prefix}" \
  --disable-nls --disable-werror --enable-plugins \
  --disable-gdb --disable-gdbserver --disable-sim --disable-gprofng
make -j"$(nproc)" MAKEINFO=true
make install-strip MAKEINFO=true
cd ..

构建 GCC

先把刚装好的 binutils 放到 PATH 最前面,再编 GCC。 默认是三阶段 bootstrap,72 核的机器上大约 40 分钟。

export PATH="${prefix}/bin:${PATH}"

mkdir build-gcc && cd build-gcc
../gcc-14.4.0/configure --prefix="${prefix}" \
  --enable-languages=c,c++ --disable-multilib --disable-nls \
  --enable-checking=release --enable-threads=posix --enable-__cxa_atexit \
  --enable-clocale=gnu --enable-gnu-unique-object --enable-linker-build-id \
  --with-linker-hash-style=gnu --enable-plugin --enable-initfini-array \
  --enable-gnu-indirect-function --with-tune=generic
make -j"$(nproc)" MAKEINFO=true
make install-strip MAKEINFO=true
cd ..
  • --disable-multilib:不编 32 位的库,省得装 glibc-devel.i686。
  • install-strip:去掉调试符号,安装目录约 264 MB,打包后约 43 MB。

确认 as、ld 用的是新编的:

"${prefix}/bin/g++" --version
"${prefix}/bin/g++" -print-prog-name=as
"${prefix}/bin/g++" -print-prog-name=ld

打包与使用

cd "${HOME}/dev-tools/GCC"
tar -cJf gcc-14.4.0-centos-7.9-x64.tar.xz gcc-14.4.0

其他机器解压到任意目录即可使用。 因为机器上通常还有别的项目在用老版本 GCC,所以不写进 .bashrc,用到时再设置:

export PATH="${HOME}/dev-tools/GCC/gcc-14.4.0/bin:${PATH}"
export LD_LIBRARY_PATH="${HOME}/dev-tools/GCC/gcc-14.4.0/lib64:${LD_LIBRARY_PATH}"
export CC=gcc CXX=g++

g++ --version

LD_LIBRARY_PATH 是为了在本机直接运行编出来的程序,比如跑 ctest。

部署

编出来的程序依赖 GCC 14 的 libstdc++.so.6(GLIBCXX_3.4.32),而 CentOS 7 自带的只到 GLIBCXX_3.4.19。 这里选择把 libstdc++.so.6 与 libgcc_s.so.1 跟程序放在同一个目录,链接时加 RPATH $ORIGIN:

g++ -std=c++23 -O2 main.cpp -o main -Wl,-rpath,'$ORIGIN'
cp -L "${HOME}/dev-tools/GCC/gcc-14.4.0/lib64/libstdc++.so.6" .
cp -L "${HOME}/dev-tools/GCC/gcc-14.4.0/lib64/libgcc_s.so.1" .

CMake 里对应:

set(CMAKE_INSTALL_RPATH "$ORIGIN")

也可以用 -static-libstdc++ -static-libgcc 把运行时静态链进去,就不用带这两个 so 了。

RPATH 与 RUNPATH

动态段里记录搜索路径的有两种:

  • DT_RPATH:优先级高于 LD_LIBRARY_PATH。
  • DT_RUNPATH:优先级低于 LD_LIBRARY_PATH。

如果部署机的环境变量把 LD_LIBRARY_PATH 指向了老版本的 libstdc++,用 DT_RUNPATH 的程序就会加载到老版本,启动时报 GLIBCXX_3.4.32 not found。

上游 binutils 的 ld 默认生成 DT_RPATH,可以检查一下:

readelf -d main | grep -E 'RPATH|RUNPATH'
 0x000000000000000f (RPATH)              Library rpath: [$ORIGIN]

不要加 -Wl,--enable-new-dtags,加了就会变成 DT_RUNPATH。

验证

查看程序和随附的库依赖的 glibc 最高版本,不能超过 2.17:

for f in main libstdc++.so.6 libgcc_s.so.1; do
  echo "${f}: $(objdump -T "${f}" | grep -o -E 'GLIBC_[0-9.]+' | sort -uV | tail -1)"
done
main: GLIBC_2.14
libstdc++.so.6: GLIBC_2.17
libgcc_s.so.1: GLIBC_2.14

在干净环境下运行,并确认加载的是同目录的库:

env -i ./main
env -i ldd ./main | grep -E 'stdc|gcc_s'

把 LD_LIBRARY_PATH 指向老版本 libstdc++ 再运行一次,加载的仍然是同目录那份。

顺带一提,GCC 14 的 libstdc++ 里 std::chrono::system_clock::now() 走的是 vDSO。 用 strace -c -e trace=clock_gettime 统计,调用 2000 次 now() 没有产生 clock_gettime 系统调用; 而 CentOS 7 自带的 libstdc++ 每次调用都是一次真正的系统调用。

注意事项

  • GCC 11 起调试信息默认是 DWARF 5,CentOS 7 自带的 gdb 7.6 读不了,编译时加 -gdwarf-4 或者用 devtoolset-11 的 gdb。
  • GCC 14 的 C++23 不完整,比如直接格式化容器 std::println("{}", vec)(P2286)要 GCC 15。
  • 进程里最好只有一份 libstdc++,不要再链接老版本 GCC 编的 C++ 动态库,三方库也都用这套工具链从源码编。