开头这两天帮同事排查一个编译问题他新拉的项目代码直接报GCC version must be at least 11.0一查系统里装的还是 Ubuntu 20.04 自带的 GCC 9.4。其实这种尴尬在 20.04 上太常见了系统比较新但系统自带编译器比较老Python 3.12、TensorFlow、新版内核模块还有一堆 C20 特性的项目都在把最低编译器版本往上抬。你问能不能直接apt install gcc-1220.04 的官方源里根本没有最省事的办法就是源码编译。所以这篇文章就围绕一件事展开在 Ubuntu 20.04 上从源码编译安装最新版的 GCC这里以 12.2 版本为例方法完全适用于 12.3、13.x、14.x。我会把从依赖准备、configure 参数、编译到环境变量切换的全过程掰开揉碎来讲包括我实际踩过的坑和调试思路。如果你刚好需要给机器升级编译器或者想搞明白为什么gcc -v已经显示新版本但编译出来的程序还是报错这篇文章应该能帮到你。1. 环境准备编译前的三个关键认知1.1 为什么 20.04 官方源里没有新 GCC先说一个大家容易忽略的点Ubuntu 20.04 是 2020 年发布的 LTS 版本官方软件源里的 GCC 版本是 9.4这个版本在发布时是稳定的但软件源不会像滚动发行版那样持续跟进新版本。原因很简单LTS 的核心策略是稳定优先编译器这种底层工具一旦升级可能导致整个系统里大量二进制包需要重编这会破坏五年不升级、一直稳定用的承诺。所以如果你需要新版本的 GCC在 Ubuntu 上通常有三条路使用 Ubuntu Toolchain PPAppa:ubuntu-toolchain-r/test里面提供 gcc-12、gcc-13 等预编译包适合懒得编译的人使用 Conda 或者其他第三方发行渠道源码编译安装PPA 方案我平时也推荐装完即用但它的缺点是不可控——PPA 里的版本不会马上同步上游最新版而且如果你想自定义编译特性比如只编 C/C 前端、指定安装路径、静态链接某些库PPA 完全帮不上忙。源码编译虽然耗时但自由度最高对版本有精确控制这也是本文选择它的原因。1.2 编译 GCC 到底需要什么依赖编译 GCC 不是孤军奋战它依赖三个 GNU 基础数学库GMP大数运算、MPFR浮点精度、MPC复数计算。这三个库是 GCC 的底盘负责中间表示层面的数值计算和优化分析。如果系统里没有它们GCC 的 configure 阶段会直接报错。这里有个省事的技巧GCC 源码目录里自带了这三个库的下载脚本contrib/download_prerequisites运行时它会自动下载并解压到源码树中。但我在国内网络环境下试过几次下载速度不稳定而且脚本下载的是固定版本以后升级 GCC 还得重新搞。所以更推荐的做法是用系统自带的库包libgmp-dev、libmpfr-dev、libmpc-dev或者手动编译最新版。在开始之前先用下面的命令把基础工具链装好sudo apt update sudo apt install -y build-essential make flex bison libgmp-dev libmpfr-dev libmpc-devbuild-essential提供的是 gcc、g、make、dpkg-dev 这些基础工具虽然我们马上要换掉 gcc但在编译新 gcc 的时候还得靠老 gcc 来孵化新 gcc这就是经典的先用自己编译自己的自举过程英文叫 bootstrap。flex和bison是语法分析器生成器编译 GCC 的解析器必须用到别省。1.3 磁盘、内存和时间的心里预期源码编译不是一个点一下等 10 秒的过程。GCC 12.2 的源码包解压后大约 800MB编译生成的中间文件会额外占 3-5GB。所以建议至少留出 8GB 磁盘空间如果是 4GB 内存的小机器编译时开太多并行任务极易 OOM。时间方面我实测过的数据是8 核的机器并行编译make -j8大概需要 30-40 分钟4 核大概 1 小时起步如果是低配云主机2 核建议做好等 2 小时的准备。这个时间因人而异不用焦虑反正你不是唯一一个在等编译的人。2. 源码下载与校验一个被忽略的关键步骤2.1 从哪下载、怎么选下载源GCC 官方下载地址是https://gcc.gnu.org/所有发布版本都在那。不过国内直连官方站点的速度不太稳定我一般是用清华或者华为的镜像源下载速度快很多。比如要下载 12.2.0wget https://mirrors.tuna.tsinghua.edu.cn/gnu/gcc/gcc-12.2.0/gcc-12.2.0.tar.xz如果你要更新版本比如 13.2.0、14.1.0只需要把路径里的版本号改掉即可。注意文件名是gcc-12.2.0.tar.xz不是.tar.gz两者压缩算法不同tar.xz体积更小、解压时间稍长。另外提一嘴很多人不知道 GCC 的 release 分支在 GitHub 也有镜像如果你在 GitHub 上操作比较熟也可以从https://github.com/gcc-mirror/gcc拉取对应 tag 的代码。不过官方发布的 tarball 是经过完整测试的稳定性更好建议直接用 tarball 而不是 git clone。2.2 校验文件完整性的靠谱姿势下载完先别急着解压建议花十几秒做个校验。GCC 官方在下载目录里提供了.sig签名文件和.sha256哈希文件理论上可以这样验证echo $(cat gcc-12.2.0.tar.xz.sha256) gcc-12.2.0.tar.xz | sha256sum -c -如果输出gcc-12.2.0.tar.xz: OK就说明文件完整。实际操作中很多同学在这个环节偷懒结果解压编译到一半才报文件损坏或者压缩包意外结束再回头重新下载反而更浪费时间。2.3 解压与目录规划的反直觉建议这里分享一个我自己的习惯不要把编译过程放在源码目录里进行。GCC 官方推荐的做法是在源码树外面单独建一个 build 目录好处是源码目录保持干净编译产物不会污染源文件以后想重新 configure 或者切换配置直接清空 build 目录就行。tar -xf gcc-12.2.0.tar.xz mkdir -p build-gcc cd build-gcc这个外部构建的方式对于大项目来说简直是救命的。我第一次编译时直接在源码目录里执行 configure后来想改参数重新配置发现目录里到处都是 Makefile 和中间文件根本分不清哪些是源码哪些是产物。从那以后所有源码编译我都遵循这个习惯。3. configure 配置决定成败的参数选择3.1 我的推荐配置参数进入 build 目录后开始配置。我推荐这样配../gcc-12.2.0/configure \ --prefix/usr/local/gcc-12.2.0 \ --enable-languagesc,c \ --disable-multilib \ --enable-bootstrap下面逐一解释每个参数--prefix/usr/local/gcc-12.2.0指定安装路径。为什么不直接装到/usr/local因为后续可能需要多版本共存装到独立目录方便管理想删哪个版本就删哪个目录最干净。--enable-languagesc,c指定需要编译的语言前端。GCC 支持的远不止 C/C还包括 Fortran、Go、D 等。如果全不指定默认会编译所有支持的语言——这会导致编译时间翻几倍而一般开发只需要 C 和 C。除非你明确需要 Fortran否则就老老实实写上 c,c。--disable-multilib是很多人忽略但极其重要的参数。多库支持multilib允许在 64 位系统上同时生成 32 位和 64 位程序默认是启用的。但这需要额外的 32 位依赖库否则 configure 会报错就算不报错也会多编出一套 32 位运行时库徒增编译时间和空间。对绝大多数人来说64 位就够了直接禁用是对的。--enable-bootstrap启用三阶段自举编译。第一阶段用系统自带的 GCC 编出基础编译器第二阶段用这个新编译器重新编译 GCC 源码第三阶段再用第二轮的结果编一遍最后还会比较第二轮和第三轮产物是否一致以此验证编译器的正确性。这样编译出来的 GCC 更可靠但代价是时间几乎翻倍。如果只是自己开发用想加快速度也可以改成--disable-bootstrap我建议至少在第一次编译时打开它稳妥。3.2 依赖库路径的处理前面我们提到已经通过 apt 安装好了libgmp-dev、libmpfr-dev、libmpc-dev所以这里 configure 一般会自动找到它们。但不排除在一些精简系统上这三个库安装在非标准路径比如/usr/local/lib此时 configure 会找不到报错信息类似configure: error: Building GCC requires GMP 4.2, MPFR 2.4.0 and MPC 0.8.0解决方式有两个一是用--with-gmp/path --with-mpfr/path --with-mpc/path显式指定路径二是把这三个库的源码放到 GCC 源码目录下命名成gmp、mpfr、mpcGCC 的构建系统会自动编译它们。不过现在主流 Ubuntu 系统的默认路径都是/usr/include和/usr/lib/x86_64-linux-gnu只要 apt 装的基本一步到位。还有一个小坑某些系统预装的是libmpc-dev版本太老比如 1.0 甚至 0.8GCC 高版本会要求 MPC 1.0.1 以上。遇到这种情况最好还是用源码方式单独编译一个最新版 MPC 并指定路径比硬着头皮降级 GCC 版本更靠谱。3.3 环境变量与编译器选择configure 在执行时会默认使用当前 PATH 里的gcc来编译 GCC 自身的启动代码也就是 bootstrap 的第一阶段。如果你 PATH 里有多个 gcc比如 Conda 的 gcc、或者别的编译工具链建议在 configure 前先清一下环境变量export CC/usr/bin/gcc export CXX/usr/bin/g否则你可能会遇到configure: error: C compiler cannot create executables或者莫名其妙的版本不匹配错误。这一步平时不起眼但一旦出错排查起来会非常痛苦因为报错信息往往不是直接指向编译器路径的问题。4. 编译与安装最耗时也最容易出问题的阶段4.1 并行任务数与内存的关系configure 通过后执行make -j$(nproc)nproc返回 CPU 核心数-j指定并行任务数。但这里我建议不要无脑用满所有核心。GCC 本身就是个内存杀手我在这台 16G 内存的机器上用-j16编译内存峰值能冲到 12G 左右如果在 4G 内存的机器上开满 8 核几乎必死无疑表现是系统卡死或者一个 OOM killer 把 cc1plus 进程给杀了。最稳妥的方式是先用free -h看内存然后根据内存大小决定并行任务数。我的经验公式是并行数不要超过内存大小 / 1.5G。比如 8G 内存建议-j4或-j516G 内存再考虑 8 核以上。编译期间可以盯着终端正常情况下会滚动输出大量编译日志不用管它只要不报错就行。不过建议把日志重定向到文件里留个底make -j$(nproc) 21 | tee build.log这样就算编译中途断了翻日志也能快速定位问题不用瞪大眼睛从头到尾看终端。4.2 三大常见编译失败场景第一个常见失败场景是内存不足。日志里会出现大量virtual memory exhausted或者直接被系统 kill 掉前面说了处理方式是降-j参数。第二个是磁盘空间不足。源码包加中间文件加最终安装文件隐含几个 G 的空间如果/tmp或当前目录所在分区不够可能会在链接阶段报错。第三个是系统自带 GCC 版本过低。如果你的 Ubuntu 是 20.04 之前的老版本系统 GCC 可能在 7 以下此时编译 GCC 12 会因为缺少 C11 支持而失败。处理方式就是先升级系统 GCC或者用更老的 GCC 版本作为 bootstrap 编译器。另外还有一个比较隐蔽的问题如果启用了--enable-bootstrap第二阶段编译时会用新编出来的编译器去编整个 GCC 源码此时如果源码被改动过比如解压后手动打了补丁可能导致最终结果显示比较失败。所以编译期间最好别用编辑器去动源码目录里的文件。4.3 make install 与安装验证当 make 完成后没有任何 error执行安装sudo make install安装时间很快几分钟内完成。装完后验证/usr/local/gcc-12.2.0/bin/gcc --version如果输出版本号gcc (GCC) 12.2.0说明安装成功。这里额外提醒一下make install默认会把可执行文件装到--prefix/bin库文件装到--prefix/lib64头文件装到--prefix/include。不需要单独执行ldconfig因为这个路径不在系统默认的/usr/lib目录里不会干扰系统原有环境这种隔离式安装正是我们要的效果。5. 升级后还是旧版本多版本共存与环境变量实操5.1 为什么gcc -v还是旧版本不少人在安装新 GCC 后打开终端执行gcc -v发现系统还是显示老版本于是怀疑安装失败了。其实没失败只是 PATH 的问题。终端在执行gcc时会从$PATH环境变量里从头到尾挨个目录找找到第一个gcc就停了。通常/usr/bin在 PATH 里排在很前面而/usr/local/gcc-12.2.0/bin排在后面甚至不在 PATH 里所以 shell 默认找到了/usr/bin/gcc也就是 GCC 9.4。解决方式很简单把新版本的 bin 目录放到 PATH 最前面。在~/.bashrc末尾添加export PATH/usr/local/gcc-12.2.0/bin:$PATH export LD_LIBRARY_PATH/usr/local/gcc-12.2.0/lib64:$LD_LIBRARY_PATH然后source ~/.bashrc再执行gcc -v看到的版本号就会变成 12.2.0。要说明的是我见过一部分教程喜欢用update-alternatives来切换 gcc 版本。update-alternatives确实是一个管理多版本工具链的好工具适合那种不想改 PATH 的场景。但说实话对于编译器这种依赖一大堆随附库的工具链PATH 方式更直观切换也更快。不过用update-alternatives有一点好处它能把新版本的 gcc 也注册为系统的cc、gcc、g等默认链接某些构建系统比如一些自动配置的 CMake 工程在扫描编译器时会走/usr/bin/gcc这种固定路径这时候update-alternatives才有不可替代的作用。5.2 动态库找不到的坑第一个坑新版 GCC 编译出来的程序运行时可能会报error while loading shared libraries: libstdc.so.6: cannot open shared object file: No such file or directory。原因很简单新版 libstdc 库在/usr/local/gcc-12.2.0/lib64下不在系统默认搜索路径里。上面的LD_LIBRARY_PATH加上就好了。但如果你不想全局设置这个变量可以只对这个程序设置LD_LIBRARY_PATH/usr/local/gcc-12.2.0/lib64 ./your_program第二个坑如果你希望所有程序都能自动找到新库可以把新库目录写进/etc/ld.so.conf.d/gcc-12.2.0.conf内容就是路径然后执行sudo ldconfig。这种方式对系统全局更友好适合服务器管理员。但要注意改了之后系统里其他依赖老 libstdc 的程序可能会被新库覆盖引发二进制不兼容。所以一般个人开发环境我还是建议用LD_LIBRARY_PATH精确控制避免殃及池鱼。5.3 多版本共存的实际场景实际工作中机器上同时存在多个 GCC 是非常正常的。比如系统的 GCC 9 还在给某些老项目服务新项目用 GCC 12 编译两边井水不犯河水。我通常的管理方式是每个版本独立安装到自己的目录然后在~/.bashrc里用变量切换。比如alias gcc12/usr/local/gcc-12.2.0/bin/gcc alias g12/usr/local/gcc-12.2.0/bin/g这样平时默认还是系统 GCC需要新版本时显式调用 gcc12。或者直接在项目目录里使用 CMake 时指定cmake -DCMAKE_C_COMPILER/usr/local/gcc-12.2.0/bin/gcc \ -DCMAKE_CXX_COMPILER/usr/local/gcc-12.2.0/bin/g这种方式最适合旧系统新项目的迁移场景也是最可控的。6. 常见问题与排查技巧实录6.1 configure 阶段报错 GMP/MPFR/MPC如果你 configure 时遇到上面提到的 GMP/MPFR/MPC 缺失或版本太旧的问题建议优先升级系统的这三个库包sudo apt install --only-upgrade libgmp-dev libmpfr-dev libmpc-dev如果 apt 里没有更高版本Ubuntu 20.04 默认仓库里这三个库版本其实够用再考虑源码编译这三个库然后 configure 时加上--with-gmp/usr/local/gmp --with-mpfr/usr/local/mpfr --with-mpc/usr/local/mpc。源码编译它们的时间很短每个大约 1-2 分钟别恐惧。6.2 编译过程中 cc1plus 进程被杀这个问题在低内存机器上非常高发。现象是编译到中途终端冒出Killed然后整个编译进程结束了。用dmesg | tail -20看日志通常能看到oom-kill相关的记录。解决方案按优先级排降低并行度make -j2甚至make -j1关闭不必要的进程浏览器、IDE 全关掉把所有可用的内存留给编译临时增加 swap可以创建一个 4GB 的 swapfile 应急sudo fallocate -l 4G /swapfile sudo chmod 600 /swapfile sudo mkswap /swapfile sudo swapon /swapfile这是临时方案编译完可以关掉 swapsudo swapoff /swapfile然后删掉文件。6.3 编译完成后 gcc 版本显示正常但编译 C 代码报头文件缺失这种情况多半是头文件搜索路径没对上。新版 GCC 的头文件在/usr/local/gcc-12.2.0/include/c/12.2.0如果你直接调用新 gcc 编译一个 include 了iostream的程序它应该能自动找到。但如果用-nostdinc或者自定义了 CPATH 环境变量可能就乱了。排查思路echo | /usr/local/gcc-12.2.0/bin/g -E -v -这条命令会输出 g 的 include 搜索路径检查里面有没有新版本的头文件目录。如果没有检查C_INCLUDE_PATH和CPLUS_INCLUDE_PATH环境变量避免它们把搜索路径带偏。6.4 老项目编译突然报错换回旧版本解决不到万不得已别把系统默认 gcc 整个替换掉。Ubuntu 系统和很多软件包在编译安装时依赖系统自带的工具链如果你把/usr/bin/gcc的软链接改成了新版本可能导致系统包管理的某些脚本行为异常。所以前面我反复强调独立目录安装 PATH 切换就是这个原因。如果老项目必须用老 gcc但是 PATH 里已经是新版可以在项目编译时强制指定编译器路径而不是改全局环境这样最安全。结尾最后一次重编 GCC 12.2前后我用了大概一个下午中间踩的坑主要集中在三块configure 参数不熟悉导致反复重试、编译过程内存不足导致进程被杀、装好后 PATH 没配好导致升级了个寂寞。后来我把这三步的套路固定下来新机器装 GCC 基本一次过。我个人体会是源码编译本身不复杂但每一步都有讲究尤其是环境变量和依赖宁可多花几分钟提前规划也不要编到一半再回头拆墙。如果你也正好卡在这里按着上面的流程走一遍应该能省掉不少弯路。如果编译过程中遇到别的奇怪报错欢迎翻翻 build.log 再对照上面列出的场景排查大概率能找到一个方向。最后再啰嗦一句装好之后一定要跑一下gcc --version确认版本不然你永远不知道自己在用的是哪个编译器。