简介:这是 GNU 编译器集合 9.1.0 的完整源码压缩包,面向 Linux/Unix 平台上的 C、C++ 开发人员,也适用于嵌入式工程师、系统软件研究者以及编译原理课程的师生。源码包包含 C 与 C++ 语言前端、各支持语言的运行时库和标准库,同时提供基于 autoconf 和 automake 的构建脚本,可针对不同操作系统自动配置生成 Makefile,便于跨平台编译。压缩包为 tar.gz 格式,体积约 118.36MB;上游未给出文件总数及具体类型明细,因此这里不罗列目录,通常此类源码包内部以 .c、.h、.cc、.def 等源代码文件为主,配合 configure 脚本与 Makefile.in 模板,支撑完成探测、配置、编译与安装全过程。当前已有 269 人浏览学习。借助这份源代码,读者可以完整追踪 GCC 从预处理、词法语法分析、优化到代码生成和汇编的流水线,理解编译器各模块如何协同工作;还能通过修改源码、自定义优化开关,或针对特定处理器进行交叉编译,锻炼系统级调试与工程实践能力。该版本属于较新的主线版本,修复了旧版若干缺陷,并增添对新语言特性的支持,对深入掌握编译器技术有较高的学习价值。
1. 为什么非要自己编译 GCC 9.1.0:一个 tar.gz 的典型出场方式
系统默认编译器太老,是所有想用新特性的人迟早要面对的问题。Ubuntu 18.04 的 GCC 7 在 C++17 支持上还有明显残缺,CentOS 7 和部分银河麒麟系统干脆停留在 GCC 4.8.5,连if constexpr都不认识。包管理器里没有你要的版本,于是 gcc-9.1.0.tar.gz 这个源码包成了唯一选择——它是 GCC 9 主线的第一个发行版,解压、configure、make、install 四步就能得到一套完全独立的新编译器。这篇内容写给两种人:被旧编译器卡住的 Linux 使用者,和需要在不破坏现有编译环境的前提下引入新工具链的运维。全文不聊虚的,只讲我自己会照着做的完整流程、参数取舍和踩过的坑,从拿到 tar.gz 到能稳定交付一个新 GCC 环境。
2. 从 tar.gz 到可用编译器:GCC 9.1.0 的完整编译流程
2.1 解压与三件套:GMP、MPFR、MPC 提前就位
先做最基础的一步:解压。gcc-9.1.0.tar.gz是标准的 tar + gzip 打包,一条命令解出来就是gcc-9.1.0源码目录。我一般会把源码包放在/opt/src这类专门目录里,方便和后面的构建目录分开管理,避免源码目录和编译产物混在一起。
mkdir -p /opt/src cd /opt/src tar -xf gcc-9.1.0.tar.gz cd gcc-9.1.0这里说的是 Linux 上从源码搭建 GCC 环境的标准路径;我见过有人想在 macOS 下用 clang 或者 gcc 直接编出 Windows 的 .exe,那是交叉编译工具链的事,得另配 mingw-w64 之类的东西,和现在从 tar.gz 编原生 GCC 的流程完全是两码事,别混在一起想。
解压之后,有一个省事的关键动作:GCC 源码里带了一个contrib/download_prerequisites脚本,它的作用是下载 GMP、MPFR、MPC 三个数学库的固定版本,并在 configure 时自动指向它们。这三个库是 GCC 编译自身的前置依赖:GMP 提供大整数和高精度浮点运算,MPFR 在 GMP 之上做高精度浮点,MPC 又依赖 MPFR 处理复数。大多数 Linux 发行版都装了其中某些库,但版本常常不满足 GCC 9.1.0 的要求,configure 会直接拒绝继续。
./contrib/download_prerequisites这个脚本跑完会创建gmp、mpfr、mpc三个符号链接,指向下载好的目录,源码目录本身不被污染。如果机器能联网,这一步是最省事的做法。如果离线环境,常见做法是手工准备这三个库的源码包,解压后放进gcc-9.1.0目录里并做同名符号链接,链接名必须是gmp、mpfr、mpc,GCC 在 configure 阶段会按这三个名字去识别。
这里有个值得注意的细节:download_prerequisites下载的版本经过 GCC 官方验证,相互之间匹配,不会出现 GMP 太新、MPFR 太旧之类的组合错误。你自己手工装的时候,建议按 GCC 9.1.0 的 configure 报错提示来调整版本,它会在报错信息里直接告诉你最低版本号和当前系统版本号。三件套准备不全会让 configure 卡住,具体报错我放在避坑章细说。
2.2 用 out-of-tree 构建:configure 参数与安装路径的关系
GCC 官方文档明确不建议在源码目录内直接 configure 构建(in-tree build),那样容易把源码目录弄乱,清理也麻烦。实践里我一直用 out-of-tree:在外面建一个专门的 build 目录,所有编译中间文件都放这里,以后想重来直接删目录就行,后悔药做得很干净。
cd /opt/src mkdir -p build-gcc-9.1.0 cd build-gcc-9.1.0 ../gcc-9.1.0/configure \ --prefix=/opt/gcc-9.1.0 \ --enable-languages=c,c++ \ --disable-multilib参数说明逐个讲。--prefix决定最终安装位置,我习惯用独立目录而不是直接塞进/usr/local,这样以后卸载就是把整个目录删掉,完全不碰系统其它文件。--enable-languages=c,c++只构建 C 和 C++ 前端,GCC 默认还会带上 fortran、ada、go 等一堆前端,编译时间和磁盘占用都会明显增加,对绝大多数场景来说只留c,c++足够。--disable-multilib是关掉 32/64 位双库支持;默认开启时 GCC 会尝试在 x86_64 机器上也编 32 位版本的库,一旦系统缺少 32 位 glibc 的 dev 包,make 到一半才报错,很烦人。吃不准自己的需求就直接关掉。
还有一个我常用的参数:--program-suffix=-9.1,让安装出来的可执行文件名变成gcc-9.1而不是gcc。这样它可以和系统自带 gcc 完全并行,不用抢 PATH 里同一个名字,但代价是日常使用时得改脚本里的调用名。走 update-alternatives 的人通常不加这个参数,两种流派我先都摆出来,第 4 章会给一套整合方案。
configure 结束时强烈建议把输出里checking for...的检查项扫一遍,重点是最后有没有带error字样的结论。如果 configure 报错退出,直接回到第 2.1 步重新检查三件套,别硬着头皮往后走,后面 make 一样会挂。
2.3 make 与 make install:并行数的选取和环境变量持久化
configure 成功后进入真正的编译阶段。GCC 默认走 bootstrap 三阶段构建,也就是用刚编出来的第一版编译器再编两遍自身,确保最终产物足够健壮,但这同时也是时间开销的大头。想快点出结果可以用--disable-bootstrap关掉,代价是产物可靠性略降。
make -j$(nproc)这里有一个非常关键的 -j 参数坑:-j8不是越快越好。GCC 的 C++ 前端单进程能吃掉 1~2GB 内存,8 并发会直接顶穿 8GB 机器的物理内存在 swap 里挣扎,情况严重时 OOM killer 会挑最肥的cc1plus进程下手,屏幕上出现Killed。内存不富裕的机器建议用make -j2或make -j4先探底,反正编译一次十几分钟跑不掉,比中途挂掉重来强。这一步顺利完成的标准是最后出现类似The directory that contains a successful GCC build is:的提示,然后才轮到安装。
sudo make installmake install会把编译器复制到--prefix指定的目录下,也就是/opt/gcc-9.1.0。这一步需要 root 权限。装完先别急着用,检查两个东西:ls /opt/gcc-9.1.0/bin应能看到gcc、g++、cpp等可执行文件;find /opt/gcc-9.1.0 -name "libstdc++.so*"能看到新库文件。然后做环境变量持久化——如果只开了一个终端,export一次就够了,但重启终端又是旧 gcc,所以该写进 profile 得写。
cat > /etc/profile.d/gcc-9.sh <<'EOF' export PATH=/opt/gcc-9.1.0/bin:$PATH export LD_LIBRARY_PATH=/opt/gcc-9.1.0/lib64:$LD_LIBRARY_PATH EOF source /etc/profile.d/gcc-9.sh写/etc/profile.d是 Linux 上最通用的做法:登录 shell 会自动加载,用户无需改自己的 bashrc。注意lib64还是lib,取决于发行版和 gcc 的 multilib 配置,x86_64 上默认是lib64;不确定就按上面find的结果改路径。关于LD_LIBRARY_PATH全局导出的风险和更稳的替代方案,我放在第 4 章展开,这里先让新 GCC 能跑起来。
注意:
make install成功后不要随手删掉 build 目录。你后面可能要重新编译某个子模块,或者发现某个 configure 参数选错了需要重来,留着 build 目录能省下重新 configure 的时间。
3. 踩坑记录:GCC 9.1.0 编译安装中我遇到的 5 个问题与排查
3.1 gcc --version 还是旧版本:PATH 与 hash 缓存
现象:make install全部成功,终端里gcc --version还是旧版本,尤其刚敲完source或者开新终端的时候最常出现。很多人在这一步直接开始怀疑安装出了问题。
原因:两件事叠在一起。一是 PATH 里/usr/bin排在/opt/gcc-9.1.0/bin前面,shell 先找到了旧 gcc;二是 shell 对命令路径有 hash 缓存,which gcc的结果一旦被缓存,就算 PATH 改了也还是旧的。
解决:
export PATH=/opt/gcc-9.1.0/bin:$PATH hash -r which gcc gcc --versionhash -r清掉 shell 缓存,which gcc看实际路径,gcc --version看版本。如果which显示/opt/gcc-9.1.0/bin/gcc但version还是旧号,那基本是动态库被污染,跳到 3.4 解决。
3.2 configure 卡在三件套版本检测:依赖没就位
现象:configure 报错,具体是checking for correct version of gmp.h... no,或者直接提示Building GCC requires GMP 4.3.2+ MPFR 2.4.6+ MPC 0.8.1+。这是我见过最多的 Ubuntu 安装 gcc 失败现场。
原因:三件套没在 GCC 认为正确的路径。常见三种:一是系统根本没装,二是装了但版本低于要求,三是头文件和库文件装了但没在默认搜索路径里。第三种多发生在多版本系统上,比如/usr/local/include里有新 gmp.h,但 configure 的默认搜索顺序没把/usr/local/include放前面。
解决:最稳的是补跑download_prerequisites,它能保证版本组合。离线或不想下载时,手工把三个符号链接放进 gcc 源码目录:
ln -s /opt/gmp-6.1.2 gcc-9.1.0/gmp ln -s /opt/mpfr-4.0.2 gcc-9.1.0/mpfr ln -s /opt/mpc-1.1.0 gcc-9.1.0/mpc然后重新 configure。注意符号链接路径要写对,如果当前在/opt/src下执行,链接名是相对于gcc-9.1.0目录的。版本号别乱凑,configure 报什么最低版本就给什么,给新版一般也可以,但别给太激进的新版,库接口偶尔会 break,不值得赌。
3.3 make 中途触发 Killed:内存与并行数没匹配
现象:make 执行到一半进程消失,终端留下一行Killed,或者显示cc1plus: internal compiler error: Killed (program cc1plus)。这种情况在内存 8GB 的机器上尤其高发。
原因:物理内存被耗尽,内核 OOM killer 把编译进程杀了。GCC 编译大型 C++ 文件是内存刺客,单进程吃 1~2GB 很常见,并行数设得太高直接爆。
解决:先看内存再定并行数。
free -h make -j2如果还崩,换-j1加临时 swap 也能熬过去:
fallocate -l 8G /swapfile chmod 600 /swapfile mkswap /swapfile swapon /swapfile加 swap 是临时的,重启失效,但足够让一次完整编译熬过去。这个坑的血泪经验是:先free -h看可用内存再决定-j,而不是无脑nproc。
3.4 新编译的程序报 GLIBCXX_3.4.20 not found:动态库被旧的抢走了
现象:新 GCC 编译成功,程序一运行就报/lib/x86_64-linux-gnu/libstdc++.so.6: version 'GLIBCXX_3.4.20' not found。这是 gcc 升级后最容易让人以为"编译器没装对"的报错。
原因:编译时调用的 g++ 是新的,但链接时动态库搜索顺序把程序指向了系统旧版libstdc++.so.6。动态链接器默认先找LD_LIBRARY_PATH,再找 ELF 里的 RUNPATH/RPATH,最后才回系统默认目录;LD_LIBRARY_PATH没包含新库目录,自然用了旧的。
解决:最直接的是让程序跑起来前先加上库路径:
export LD_LIBRARY_PATH=/opt/gcc-9.1.0/lib64:$LD_LIBRARY_PATH ./your_program更省心的做法是把库路径写进程序 ELF,编译时直接加-Wl,-rpath:
g++ app.cpp -o app -Wl,-rpath,/opt/gcc-9.1.0/lib64这样程序跑起来自动优先用新库,机器上没设LD_LIBRARY_PATH也一样。两种方式我建议优先 rpath,它对环境没有全局副作用。具体参数边界放第 4 章。
3.5 VSCode 一键编译仍走旧 gcc:#include 错误与 compilerPath
现象:VSCode 里配好 task 一键编译运行,结果报#include errors detected,或者 F5 调试时还是调用了旧 gcc;在c_cpp_properties.json里设置了 compilerPath 指向新路径,插件还是识别不了标准库头文件。
原因:VSCode 的 C/C++ 插件里 compilerPath 和 includePath 是两个独立配置,只改 compilerPath 不改 includePath 时,插件还是会按它默认的系统头文件路径去找 C++ 标准库。而新 GCC 的头文件在/opt/gcc-9.1.0/include/c++/9.1.0下,插件自然找不到。
解决:同时改两个文件。c_cpp_properties.json里 compilerPath 指向新 gcc,includePath 手工加入 C++ 标准库头文件目录:
{ "configurations": [ { "name": "Linux", "compilerPath": "/opt/gcc-9.1.0/bin/gcc", "includePath": [ "${workspaceFolder}/**", "/opt/gcc-9.1.0/include/c++/9.1.0", "/opt/gcc-9.1.0/include/c++/9.1.0/*", "/opt/gcc-9.1.0/include" ] } ] }这里 includePath 里的9.1.0/*是为了把x86_64-pc-linux-gnu这类平台子目录一并包含。不同 configure 的 target 目录名不一样,不确定就ls /opt/gcc-9.1.0/include/c++/9.1.0/看一下实际目录名再填。tasks.json 里的 command 也要写成绝对路径/opt/gcc-9.1.0/bin/gcc,避免任务还走 PATH 里的旧命令。
4. 多版本共存与路径管理:让新 GCC 接管又不弄乱旧环境
4.1 update-alternatives:把 GCC 9.1.0 绑定成可切换的默认版本
当系统里同时存在两个 gcc,最怕的是依赖旧 gcc 的第三方东西被新 gcc 偷偷替换掉。update-alternatives 提供了"显式切换"机制:默认命令还是/usr/bin/gcc,但它只是个符号链接,指向/etc/alternatives/gcc,再由 alternatives 指向你选定的真实版本。
sudo update-alternatives --install /usr/bin/gcc gcc /opt/gcc-9.1.0/bin/gcc 100 sudo update-alternatives --install /usr/bin/g++ g++ /opt/gcc-9.1.0/bin/g++ 100 sudo update-alternatives --config gcc--install后面三个要素:链接位置、链接组名、优先级数值。数值越大优先级越高,update-alternatives 会自动选优先级高的;手动--config可以临时切到另一个。这个机制在 Debian/Ubuntu 和基于 Debian 的银河麒麟上都适用,CentOS/RHEL 对应命令叫alternatives,语法接近但不同大版本对链接组的写法有差异,具体以alternatives --help为准。要注意的是,这个接管动作会把/usr/bin/gcc从真实文件变成符号链接,dpkg -V校验会认为包文件被修改;在意这个就去make install时加--program-suffix,用独立文件名避开系统路径。
用了 update-alternatives 后,日常 PATH 不需要加/opt/gcc-9.1.0/bin,因为/usr/bin/gcc始终是统一入口。这和你自己 export PATH 的差别在于:任何命令行工具只要锁定/usr/bin/gcc就能拿到当前默认版本,不会因为环境变量没设而悄悄变回旧版。我经常来回切版本,这套机制管理成本低很多。
还有一个不太起眼但很实用的习惯:编译第三方源码包时,优先用CC=/opt/gcc-9.1.0/bin/gcc CXX=/opt/gcc-9.1.0/bin/g++ ./configure来指定编译器,而不是改全局默认。这样这个第三方包用的是新编译器,系统里其它东西还是旧编译器,互不打扰。
4.2 -I、-L 与 LIBRARY_PATH:控制头文件和库的搜索顺序
即使 PATH 和 update-alternatives 都指向了新 gcc,还有一个隐患:头文件。GCC 能自动找到自己安装目录下的标准头文件,因为它会以编译器二进制所在路径为锚点推导 include 根;但你的项目如果用了第三方库,那些旧头文件大多还在/usr/include,新 GCC 默认搜索顺序也会带上系统 include 目录,于是"编译器新、依赖旧"的局面就出现了。
控制头文件搜索顺序最直接的是-I:
/opt/gcc-9.1.0/bin/gcc -I/opt/gcc-9.1.0/include -I/opt/boost/include app.cpp -o app-I目录会在系统 include 之前被搜索,但它只影响预处理阶段。链接时库的搜索顺序用-L指定:
/opt/gcc-9.1.0/bin/gcc app.o -L/opt/gcc-9.1.0/lib64 -lstdc++-L只把目录加到链接器搜索列表,实际找哪个库还受库名和版本影响,最终结果要用ldd看。
环境变量层面还有LIBRARY_PATH:它给 GCC 前端提供"系统额外库搜索路径",等价于给 gcc 的链接过程补充-L。批量处理时导出它最快:
export LIBRARY_PATH=/opt/gcc-9.1.0/lib64:$LIBRARY_PATH但它和-L一样只管链接期,不管运行期。运行期的搜索归LD_LIBRARY_PATH和 rpath 管,所以别指望LIBRARY_PATH解决 3.4 那种运行报错。
一个常见的误用:把LD_LIBRARY_PATH全局导出,结果所有老的二进制程序全部切到了新 libstdc++,有些二进制固化依赖旧版本符号,直接跑崩。我的习惯是LD_LIBRARY_PATH只在项目启动脚本里 export,或干脆用下一节的 rpath,避免全局污染整个系统的环境搭建。
4.3 rpath 与 LD_LIBRARY_PATH 的搭配:部署时动态库怎么带
第 3.4 步踩过的那类问题,根源就在运行期搜索路径没固化。部署阶段最头疼的场景是:把程序从开发机拷贝到一台没装 GCC 9.1.0 的服务器上,动态库带不走,启动就想半天。LD_LIBRARY_PATH是你手动告诉系统"去哪里找库",但它依赖外部环境变量,启动脚本一没配就回到 3.4 的报错;更麻烦的是LD_LIBRARY_PATH的优先级高于 RUNPATH,可能覆盖程序内预设的路径,反而造成混乱。
写进 ELF 的 rpath 是更稳定的答案。编译时加上这么一段:
g++ -std=c++17 app.cpp -o app \ -Wl,-rpath,/opt/gcc-9.1.0/lib64 \ -Wl,--enable-new-dtags-Wl,-rpath把/opt/gcc-9.1.0/lib64写进程序的动态段;--enable-new-dtags让它生成 RUNPATH 而不是老式 RPATH。区别很关键:RUNPATH 的优先级低于LD_LIBRARY_PATH,也就是说即使运行环境里有人设了LD_LIBRARY_PATH指向别处,程序仍然优先遵循自己的 RUNPATH,但同时还是能被环境变量覆盖,给后续调换库版本留了余地。
验证是否生效:
readelf -d app | grep -E 'RPATH|RUNPATH' ldd app | grep stdc++readelf显示 RUNPATH 存在,ldd显示libstdc++.so.6指向/opt/gcc-9.1.0/lib64,说明运行期依赖已经固化。部署时只要把这个目录和程序一起拷贝,服务器上有没有新 GCC 都能跑。
如果连/opt/gcc-9.1.0/lib64都不想带,也可以把libstdc++.so.6静态编进程序:-static-libstdc++ -static-libgcc。代价是产物体积变大、ABI 灵活性降低;GCC 9.1.0 的 C++ 库大量用了弱符号和动态版本控制,静态链接在某些场景会引发行为差异。一般二三十人的内部工具我才会这么干,对外交付的产品还是 rpath 稳。
5. 验证安装:用三个最小程序确认 GCC 9.1.0 真正生效
安装完成、路径也理顺之后,不要急着一头扎进项目里,先跑三个最小验证。第一个是确认命令本身:
which gcc gcc --versionwhich显示/opt/gcc-9.1.0/bin/gcc才说明 PATH 生效;如果显示/usr/bin/gcc,哪怕 version 是新版本,也说明你依赖的其实是 alternatives 或 PATH 之外的另一条解析链,回看第 3.1 步。
第二个验证编译一个打印版本宏的 C 程序。__VERSION__是编译器内置宏,直接反映真正干活那个编译器的版本号,比命令行显示更可靠。
cat > ver.c <<'EOF' #include <stdio.h> int main(void) { printf("GCC %s\n", __VERSION__); return 0; } EOF gcc ver.c -o ver && ./ver输出 GCC 9.1.0 即通过。如果输出旧版本,问题必然出在调用链上,先gcc --version再readlink -f $(which gcc)一层层查。
第三个验证 C++17 特性加动态库归属。用if constexpr写一个最小模板,配-std=c++17编译:
#include <iostream> #include <type_traits> template <typename T> constexpr const char* kind() { if constexpr (std::is_integral_v<T>) return "int"; else if constexpr (std::is_floating_point_v<T>) return "float"; else return "other"; } int main() { std::cout << kind<int>() << "\n"; std::cout << kind<double>() << "\n"; }g++ ver_cpp17.cpp -o ver_cpp17 -std=c++17 ./ver_cpp17 ldd ver_cpp17 | grep stdc++ldd输出里 grep 到/opt/gcc-9.1.0/lib64/libstdc++.so.6才算完整通过;如果它指向/usr/lib下的旧库,说明第 3.4 步的 rpath 或LD_LIBRARY_PATH还没生效。if constexpr能编过本身就证明 C++17 支持完整,这在 GCC 4.8 上是连语法都过不去的。
我自己现在每台机器装完 GCC 都会把上面四段命令合成一个check_gcc.sh放进工程目录,换机器、换环境先跑一遍,三分钟定位是命令层、编译层还是运行层的问题,省到就是赚到。希望这篇能帮你在 GCC 9.1.0 上少走一段弯路。
本文还有配套的精品资源,点击获取