先说一下我为什么会去折腾这个东西。之前在一台CentOS 7服务器上编译一个C++项目,用的是C++17标准,结果configure阶段直接报错,提示gcc版本过旧、不支持某些特性。一查版本,系统自带的gcc还是4.8.5。这台机器跑着线上服务,不能轻易重装系统,也不能乱动底层库,所以只能在CentOS 7这个老环境里装一个高版本GCC。这篇文章就是基于这个背景整理出来的,目标是让读者在CentOS 7上顺利装好gcc/g++ 9.3,同时把那些容易踩的坑——比如"明明升级了,一敲gcc -v还是旧版本"这类问题——一并讲清楚。
文章适合两类人看:一类是被迫在老系统上做新开发的运维或后端工程师,另一类是刚接触Linux、想搞明白软件编译安装原理的入门读者。前者可以直接跳到操作步骤抄作业,后者我建议从头看一遍,因为里面涉及的依赖、路径、软链接这些概念,在很多软件安装场景里都能复用。
1. 版本选型:为什么偏偏是GCC 9.3
CentOS 7官方仓库自带的GCC版本是4.8.5,这是个非常古老的分支。它的C++11支持还算完整,但C++14基本是残缺的,C++17更是一点边都不沾。如果你只是编译一些老项目,GCC 4.8.5凑合能用;但如果要编译TensorFlow、新版Redis、Python 3.8以上的源码,或者任何用到现代C++特性的项目,直接就卡死在编译器这一步。
我选GCC 9.3而不是更新的版本,主要基于三点考虑。
第一,9.x系列是GCC的一个长期稳定分支,9.3属于该分支的修复版本,相比9.1、9.2修了不少编译器的内部错误(ICE,Internal Compiler Error)。这类内部错误在编译大型C++项目时经常随机触发,修复版能显著减少这种闹心问题。
第二,CentOS 7的系统库环境相对老旧。GCC 10以上版本虽然也能编译安装,但它在生成目标代码时默认假设的某些运行环境行为,和CentOS 7的binutils、glibc配合得并没有那么好。9.3的生成代码风格和CentOS 7自带的2.27版本glibc兼容性更稳,编译出来的程序放到线上不容易因为动态库版本问题翻车。
第三,GCC 9.3完整支持了C++17标准,这对于绝大多数现代C++项目来说是一道分水岭。C++17之前的编译器在模板元编程、结构化绑定、if constexpr这些特性上要么不支持,要么支持得很别扭,9.3全面落地了这些能力,日常开发完全够用。
有人会问,为什么不直接用SCL仓库里的devtoolset-9?这个后面会专门对比,先卖个关子。简单说,SCL方式安装简单,但它把GCC装在一个隔离的环境里,和系统原生的编译链整合得不够彻底,很多第三方项目在编译过程中找不到它的库路径,处理起来反而麻烦。
2. 安装前置条件:磁盘、依赖库与工具链的一个都不能少
在动手装GCC之前,先确认三样东西:磁盘空间、基础编译工具、必要的依赖库。这三样缺了哪个,编译过程中都会以非常费解的方式报错,而新手往往被这些报错带偏,以为是GCC源码本身的问题。
2.1 磁盘空间与内存评估
GCC 9.3的源码压缩包大约110MB,解压后约800MB,编译过程产生的临时文件更多。加上gmp、mpfr、mpc这几个依赖库的编译产物,以及最终安装到系统的文件,建议预留至少5GB可用磁盘空间。内存方面,GCC编译时有几个阶段极其吃内存,尤其是C++标准库的头文件解析阶段。如果是1GB内存的小机器,建议先加swap,否则随时可能遇到"internal compiler error: Killed"这类被OOM Killer杀掉进程的报错。
可以用这个命令快速检查磁盘:
df -h /usr/local确保剩余空间不低于5GB。再看内存和swap:
free -h如果swap是0而内存又小于2GB,建议用fallocate快速加一个swap文件,这个操作在生产环境里也能临时救急。
2.2 基础编译工具
有个容易被忽略的点:编译GCC本身也需要一个能用的C编译器。CentOS 7系统自带的gcc 4.8.5虽然老,但它本身是完好的,可以用来编译GCC 9.3的C部分。此外还需要make、bison、flex这些构建工具。一次装齐:
yum install -y gcc gcc-c++ make bison flex如果你是全新最小化安装的CentOS 7,这一步会把最基本的编译环境补全。注意,这里的gcc是为了后面"编译GCC的引导编译器",装完之后它会继续扮演这个角色,等新版本编译好以后,再由新版本接管系统默认编译器的位置。
2.3 依赖库:GMP、MPFR、MPC
这是整个安装过程中最容易出问题的部分。GCC在编译过程中依赖三个数学库:GMP(大数运算)、MPFR(浮点运算)、MPC(复数运算)。CentOS 7的默认yum源里其实有这三个库的旧版本,但版本太老,GCC 9.3要求的版本高于系统自带的,所以在configure阶段就会检查不通过。
解决办法有两个。第一个是从EPEL仓库安装,EPEL里通常有稍新一些的版本,但也不保证满足GCC 9.x的要求。第二个是让GCC源码包自动下载并构建这些依赖。
更靠谱的是直接在源码目录里执行:
cd gcc-9.3.0 ./contrib/download_prerequisites这个脚本会从GCC官网的镜像站点自动下载gmp-6.1.0、mpfr-3.1.4、mpc-1.0.3,并解压到GCC源码目录下。GCC的configure脚本会自动识别这些目录,在编译GCC的同时把它们一并编译链接为静态库。整个过程不需要额外干预,出问题的概率最低。
如果服务器在内网环境、无法直接访问外网,你需要在能联网的机器上先下载这些依赖包并传到服务器,放到gcc-9.3.0目录下,然后手动解压,保证解压后的目录名和脚本中预期的一致。这个"离线模式"的操作后续会单独说。
3. 通过SCL仓库快速安装GCC 9的路径与边界
说完源码编译,先插一段SCL的安装方式,因为它是很多教程的首选,但用起来有坑。
SCL(Software Collections)是Red Hat官方推出的软件版本覆盖方案。CentOS 7下可以通过centos-release-scl软件包启用这个仓库,然后直接安装devtoolset-9。命令如下:
yum install -y centos-release-scl yum install -y devtoolset-9-gcc devtoolset-9-gcc-c++安装完成后,你不能直接用gcc命令,因为SCL把新版本安装到了一个独立的目录:/opt/rh/devtoolset-9/root/usr/bin/gcc。要使用它,需要先启用环境变量:
scl enable devtoolset-9 bash这条命令会打开一个新的bash,在这个bash里gcc -v就会显示9.3.1。注意这里有个细节:SCL的环境变量只在当前bash会话里生效,退出这个bash再进来,gcc又变回4.8.5。有人通过往/etc/profile.d里写source命令来永久生效,但实测下来,这样会让部分依赖系统默认编译器的第三方脚本行为异常,因为它们拿到的gcc是9.x,而系统某些库却是按4.8.5的规范生成的,偶尔会碰到ABI校验错误。
另外,SCL版本的gcc在编译C++代码时,默认的搜索路径包含了/opt/rh/devtoolset-9/root/usr/lib/gcc/...,如果你在编译某些使用了autoconf的项目时,configure脚本会去检查gcc是否能编译可执行程序,这时候大概率能通过。但遇到CMake项目时,可能出现"CMAKE_CXX_COMPILER版本与预期不符"这类问题,需要显式指定编译器路径,比较繁琐。
所以我的结论是:SCL方式适合只想临时编个程序、不想编译整个GCC源码的场景;如果你需要长期维护一个现代C++项目,或者需要自定义GCC的编译选项,老老实实源码编译才是正路。
这也是本文标题选择9.3源码编译的原因:一劳永逸,把控制权握在自己手里。
4. 源码编译GCC 9.3的完整拆卸式流程
下面进入正题,完整走一遍源码编译安装GCC 9.3的过程。我尽量把每个步骤背后的意图讲清楚,而不是简单复制粘贴一堆命令。
4.1 下载与解压GCC源码
首先从GCC官网的镜像站下载9.3.0的源码包。考虑到国内网络环境,建议挑一个离你近的镜像。清华源和阿里源都长期同步GCC的发布包,速度稳定。
cd /usr/local/src wget https://mirrors.tuna.tsinghua.edu.cn/gnu/gcc/gcc-9.3.0/gcc-9.3.0.tar.gz tar -zxvf gcc-9.3.0.tar.gz cd gcc-9.3.0解压之后,先别急着configure。跑一下依赖脚本:
./contrib/download_prerequisites这个脚本执行完,会在目录里看到gmp-6.1.0、mpfr-3.1.4、mpc-1.0.3这几个文件夹。到了这一步,如果不放心,可以检查一下这几个文件夹是否存在,确认没问题再继续。
4.2 创建独立的编译目录
这是一个非常重要的经验:千万不要在GCC源码目录里直接运行configure和make。GCC官方明确建议在源码目录之外单独建一个build目录,这样源码树保持干净,编译产物集中在一个地方,方便之后彻底清理。
mkdir /usr/local/src/gcc-9.3.0-build cd /usr/local/src/gcc-9.3.0-build这个"源码目录和编译目录分离"的做法也适用于很多其他软件,比如binutils、glibc。编译过程中产生的临时文件、中间文件密密麻麻,如果和源码混在一起,出了问题很难排查。
4.3 configure参数解析
在build目录里执行configure,参数要仔细斟酌。我用的命令如下:
../gcc-9.3.0/configure \ --prefix=/usr/local/gcc-9.3.0 \ --enable-bootstrap \ --enable-languages=c,c++ \ --disable-multilib \ --enable-checking=release逐个说下这些参数的意义:
- --prefix:指定安装路径。这里我装了/usr/local/gcc-9.3.0,单独建目录的好处是以后想卸载,直接rm -rf这个目录就行,干净利落。不建议直接覆盖到/usr,因为那样会和系统的旧版gcc搅在一起,以后想恢复原状就麻烦了。
- --enable-bootstrap:开启引导编译。GCC会先用系统现有的编译器编译一遍,然后用第一次编译出的GCC再编译一遍自己,最后用第二次编译出的GCC编译第三遍,三层编译后生成的编译器才是最稳定的。代价是编译时间大幅增加,但为了可靠性,这一步值得等。
- --enable-languages=c,c++:只需要C和C++编译器。如果你还需要Fortran、Go、Ada等语言支持,可能要在这里加上对应项,但大部分人用不到,加上了反而拖慢编译速度。
- --disable-multilib:禁用多架构库。CentOS 7默认x86_64架构,一般情况下不需要同时生成32位和64位库。这个参数能显著减少编译时间和链接负担。如果你的机器确实需要编译32位程序,那就不能加这个参数。
- --enable-checking=release:在release模式下只做最小限度的编译期检查,这个参数的目的是降低编译器自身的运行开销,对于生产环境是标准配置。
如果configure阶段报了某个依赖库找不到,通常就是前面download_prerequisites没执行成功,或者下载后没有解压到正确位置。
4.4 make编译与make install
配置完成后,开始编译:
make -j$(nproc)-j$(nproc)的意思是让make并发任务数和CPU核心数一致。如果你的机器是2核4线程,nproc可能返回4,这时候并发编译能快很多。但要注意,GCC编译进程特别吃内存,如果你在2GB内存的机器上直接开4个并发任务,很容易把内存打满。这类机器建议保守一点:
make -j2编译时长按机器性能差异很大。4核8线程的机器编GCC 9.3大概需要40分钟到1小时;如果是2核的入门云主机,可能要1.5到2小时。这个过程会输出大量编译日志,中间如果看到某个文件报错,不要急着重新整体编译,先定位具体错误原因。
一个比较常见的错误是:
cannot compute suffix of object files: cannot compile这类错误通常是configure阶段检测编译器失败,多半是依赖库缺失或者环境变量有问题。回头检查步骤2里的基础工具是否装全。
编译完成后执行:
make install这条命令会把所有编译好的二进制和头文件拷贝到/usr/local/gcc-9.3.0目录下。执行完后验证一下关键文件是否到位:
/usr/local/gcc-9.3.0/bin/gcc --version如果能看到gcc (GCC) 9.3.0这样的输出,说明安装本身已经成功。
5. 环境变量与软链接:彻底解决"gcc还是旧版本"的经典困惑
很多人在这一步卡住,明明装了新版GCC,执行gcc -v却还是4.8.5。原因很简单:系统命令查找时,PATH变量里/usr/bin排在/usr/local/gcc-9.3.0/bin前面,而系统默认的gcc在/usr/bin/gcc,根本没跑到你新装的位置。
5.1 调整PATH环境变量
我建议为GCC 9.3创建单独的环境变量配置文件,方便管理和回滚:
cat > /etc/profile.d/gcc-9.3.0.sh <<'EOF' export PATH=/usr/local/gcc-9.3.0/bin:$PATH export LD_LIBRARY_PATH=/usr/local/gcc-9.3.0/lib64:/usr/local/gcc-9.3.0/lib:$LD_LIBRARY_PATH export CC=/usr/local/gcc-9.3.0/bin/gcc export CXX=/usr/local/gcc-9.3.0/bin/g++ EOF source /etc/profile.d/gcc-9.3.0.sh这里有三条变量值得解释:
- PATH:让shell找到新版gcc、g++、gcov等可执行文件。
- LD_LIBRARY_PATH:让运行时动态链接器能找到新版GCC的C++标准库(libstdc++.so.6)。这一步特别关键,因为新版g++编译出的C++程序默认依赖新版libstdc++,如果这个库路径不在LD_LIBRARY_PATH里,编译时能过,运行时直接报"version `GLIBCXX_3.4.28' not found"错误。
- CC和CXX:很多configure脚本和Makefile会读取这两个变量来决定用哪个编译器,显式指向新版可以避免它们又跑去用系统旧版。
设置完环境变量后,重新登录shell或者直接执行source,然后验证:
which gcc gcc -v正常情况下,which gcc会指向/usr/local/gcc-9.3.0/bin/gcc,gcc -v输出里也会显示9.3.0。
5.2 软链接潜在的坑与建议
有教程建议直接把/usr/bin/gcc软链接到新版本:
ln -sf /usr/local/gcc-9.3.0/bin/gcc /usr/bin/gcc这个方法见效快,但风险很大。CentOS 7系统内部的很多组件的编译运行都依赖4.8.5,尤其是一些内核模块、系统库的构建脚本,它们会默认调用/usr/bin/gcc,如果你把软链接改成9.3,可能因为ABI差异导致编译失败,甚至影响系统包管理器运行。
我的建议是:不要动/usr/bin下面的软链接,让新版GCC通过PATH环境变量优先生效。这样系统工具需要旧版时,仍然可以显式调用/usr/bin/gcc;你的项目默认使用新版。两者井水不犯河水。
5.3 动态库缓存的调整
另一个必须注意的是动态库缓存。LD_LIBRARY_PATH方式是临时的,如果你想让系统范围的程序都能找到新版libstdc++.so,可以把库路径写入ld.so.conf:
echo "/usr/local/gcc-9.3.0/lib64" > /etc/ld.so.conf.d/gcc-9.3.0.conf ldconfig这样执行二进制时,动态链接器会在默认路径之外额外搜索这个目录。但如果你又设置了LD_LIBRARY_PATH,两者同时存在时,LD_LIBRARY_PATH优先级更高。以我的实践经验,两者都配好最稳妥。
6. 编译实测:一个C++17程序的完整验证
环境配置好以后,写个测试程序验证一下g++是否能正常编译、运行,并且确认用的确实是不折不扣的C++17特性。
这里用一个简单的例子,展示结构化绑定和if constexpr两个C++17特性:
#include <iostream> #include <tuple> #include <type_traits> template <typename T> auto get_value() { if constexpr (std::is_integral_v<T>) { return 1; } else { return 0.5; } } int main() { auto [a, b] = std::make_tuple(10, 0.5); std::cout << "a = " << a << ", b = " << b << std::endl; std::cout << "int value = " << get_value<int>() << std::endl; std::cout << "double value = " << get_value<double>() << std::endl; return 0; }编译:
g++ -std=c++17 test.cpp -o test ./test如果输出正常,说明g++ 9.3编译C++17程序没有问题。接着还要检查一个容易被忽略的东西——libstdc++.so.6的版本:
strings /usr/local/gcc-9.3.0/lib64/libstdc++.so.6 | grep GLIBCXX输出列表里应该包含GLIBCXX_3.4.28或更新的标识,这是GCC 9.x新增的符号版本。那些报错"GLIBCXX_3.4.28 not found"的盆友,基本都是因为运行程序时没有把新版libstdc++的路径暴露给动态链接器,要么加LD_LIBRARY_PATH,要么走ldconfig,二者必有其一。
再验证一下编译出的二进制依赖的运行时库:
ldd test如果libstdc++.so.6一栏指向的还是系统旧版本,刚设置的环境变量未必被当前shell进程正确读取了。检查一下是否重新登录过、或者source过配置文件。
7. 编译过程与使用过程中的典型报错及排查链路
源码编译GCC的报错千奇百怪,但我总结下来,高频问题其实就那么几类。这里把完整的排查思路写出来,比直接给答案更有价值。
7.1 configure阶段报"cannot find crt1.o"
这种报错的大意是链接器找不到C运行时启动文件。常见原因是configure时检测64位库路径失败。如果configure命令里加了--disable-multilib还是报这个错,检查一下系统是否缺少glibc-devel:
yum install -y glibc-devel glibc-static装完后重新configure。因为crt1.o这个文件通常由glibc-devel包提供,路径在/usr/lib64/crt1.o,缺了它,任何C程序的编译链接都会失败。
7.2 编译中途"internal compiler error"
ICE有两种情况。一种是GCC自身的Bug,换一个更新或更旧的版本试试;另一种是内存不足导致进程被系统杀掉。区分方法很简单:看编译输出末尾有没有"Killed"字样,如果有,基本可以断定是内存问题。处置办法是减少make并行数,或者临时加swap。
还有一种容易忽略的情况是磁盘写满。GCC编译时会产生海量中间文件,如果/tmp或build目录所在分区的磁盘满了一定报错。用df -h检查一下,把编译目录换到空间充足的分区即可。
7.3 make install后头文件找不到
装好之后写代码,发现怎么也include不到新版本头文件。这是因为/usr/include被系统头文件目录占着,新版GCC的头文件默认安装在/usr/local/gcc-9.3.0/include/c++/9.3.0下面。一般你直接用g++编程序时会自动搜索这个路径,但如果某些IDE或第三方构建工具自己指定了头文件搜索路径,就会覆盖GCC的默认行为,导致找不到。
解决办法是在构建系统里显式加上:
-I/usr/local/gcc-9.3.0/include/c++/9.3.0同时也要加:
-L/usr/local/gcc-9.3.0/lib64这样链接器才知道去哪里找libstdc++.so。
7.4 运行时"version GLIBCXX_X.X.X not found"
这个问题在前文提到过。它的本质是:编译时用的libstdc++(新版)和运行时加载的libstdc++(旧版)不是同一个。运行中程序的动态库解析顺序默认是系统的/lib64和/usr/lib64,而新版libstdc++在/usr/local/gcc-9.3.0/lib64下面,没被找到。
修复链路按优先级排列:
echo "/usr/local/gcc-9.3.0/lib64" > /etc/ld.so.conf.d/gcc-9.3.0.conf ldconfig然后用ldd重新查看程序依赖,确认libstdc++.so.6是否指向新版。如果指向的还是旧版,说明ldconfig没有生效,检查路径是否写对。
8. 离线环境下的安装补充方案
前面提到过内网服务器无法直接访问外网的情况,这里单独补一块。离线安装的核心是:在能联网的机器上把所有需要的文件下载好,然后一起传到目标机器。
需要提前准备的包括:GCC源码包、三个依赖库的源码包。download_prerequisites脚本会从网上下载,离线环境不能直接执行,需要手动准备。依赖库的下载地址如下:
- gmp: https://gmplib.org/download/gmp/gmp-6.1.0.tar.bz2
- mpfr: https://www.mpfr.org/mpfr-3.1.4/mpfr-3.1.4.tar.bz2
- mpc: https://ftp.gnu.org/gnu/mpc/mpc-1.0.3.tar.gz
把GCC源码包和这三个依赖包都放到同一台离线机器的/usr/local/src目录下,解压GCC源码:
tar -zxvf gcc-9.3.0.tar.gz cd gcc-9.3.0 tar -jxvf gmp-6.1.0.tar.bz2 tar -jxvf mpfr-3.1.4.tar.bz2 tar -zxvf mpc-1.0.3.tar.gz mv gmp-6.1.0 gmp mv mpfr-3.1.4 mpfr mv mpc-1.0.3 mpc注意,这里必须把解压后的目录重命名成gmp、mpfr、mpc这种不带版本号的名字,因为GCC源码的configure脚本恰好以这些短目录名作为匹配条件。如果你不重命名,可以创建一个符号链接,效果一样:
ln -s gmp-6.1.0 gmp ln -s mpfr-3.1.4 mpfr ln -s mpc-1.0.3 mpc后面的configure和make就和在线环境完全一致了。
另外,离线机器上如果连基础工具链都没装,那就更麻烦了,需要从Red Hat的ISO安装镜像里制作本地yum源。这个内容可以单独写一篇,这里只提一句:把CentOS 7的DVD ISO挂载到/mnt目录,然后配置一个以file:///mnt/为baseurl的yum源文件,就能通过yum install把gcc、make等工具装齐。
9. 切换到新版本GCC后的进一步配置与注意事项
安装和验证到这里基本算结束了,但实际开发中还有些细枝末节,不注意会继续踩坑。
9.1 让CMake项目使用新版GCC
CMake默认会去/usr/bin里找编译器。如果你想让它使用新版,需要在配置时显式指定:
cmake -DCMAKE_C_COMPILER=/usr/local/gcc-9.3.0/bin/gcc \ -DCMAKE_CXX_COMPILER=/usr/local/gcc-9.3.0/bin/g++ \ ..还有一种做法是在CMakeLists.txt里通过set指定编译器路径,但这样做会让CMakeLists丧失可移植性,同队其他人要是没装这个路径的编译器,整个项目就挂了。所以我更推荐在配置阶段传参。
9.2 新版GCC编译出的程序部署到其他机器
如果你的程序需要拷贝到其他CentOS 7机器运行,你会发现程序依赖了新版libstdc++.so.6。目标机器上如果没有这个库,就会报"error while loading shared libraries: libstdc++.so.6: cannot open shared object file"。
做法是把新版libstdc++.so.6也一起拷贝到目标机器的/usr/local/gcc-9.3.0/lib64目录下,并在目标机器上配置ld.so.conf.d,或者在程序启动脚本里设置LD_LIBRARY_PATH。更稳妥的方案是编译时使用静态链接:
g++ -static-libstdc++ -static-libgcc test.cpp -o test这样libstdc++和libgcc的内容会直接编译进可执行文件,运行时完全不依赖目标机器上的动态链接库。代价是可执行文件体积变大,但对于部署到多台服务器的情况,这个代价完全值得。
9.3 与Python等解释型语言扩展模块的兼容
不少Python的C扩展模块在编译时会调用系统gcc。如果你把PATH环境变量全局改成了新版GCC,扩展模块也会跟着用新版编译。这在大多数时候没有问题,但偶尔会碰到个别扩展模块的setup.py写得不够健壮,对新版GCC的编译警告处理得不好,导致编译失败。
我的习惯是,在编译Python扩展模块时,临时把PATH里的新版路径去掉,用回旧版gcc,编完了再恢复。这听起来原始,但确实能避免很多莫名的兼容问题。
10. 日常使用中的一点小习惯
最后分享几个我在实际使用中养成的习惯,不一定适合所有人,但确实帮我省了不少事。
第一,我习惯在项目根目录下放一个env.sh,里面写着编译这个项目要用的环境变量:
export PATH=/usr/local/gcc-9.3.0/bin:$PATH export LD_LIBRARY_PATH=/usr/local/gcc-9.3.0/lib64:$LD_LIBRARY_PATH这样换一台新机器拉完代码后,先source env.sh,再编译,不需要把环境变量全局乱改。
第二,我把环境变量配置写进/etc/profile.d/之后,做了备份。因为以后系统升级或者装了别的软件,可能会覆盖这个文件。备份一下,出问题能快速恢复。
第三,GCC版本升级之后,旧的编译产物最好用make clean清理一遍再重新编译。我见过不少"明明升级了编译器,重新编译还是报旧错误"的情况,多半是Makefile增量编译导致,没有真正触发重新编译。
这次在CentOS 7上装GCC 9.3的完整过程就是这样。从版本选型、环境准备、源码编译、环境配置到典型坑位的排查,一路走下来,核心就一句话:版本隔离、路径分明、环境变量管好。做到这三点,不管以后是升级到10.x还是11.x,或者换一台机器重新部署,都能少走很多弯路。