如果你在一个经常要跑科学计算的环境里工作,大概率迟早会遇到这么一件事:某个老程序的 Makefile 里明晃晃写着FC = gfortran,但你一make就报错,翻到项目文档要求的是老版本,而系统默认已经装到了一个新版本。版本不匹配会导致.mod文件不兼容、库链接错误,甚至整个编译流程直接卡死。我最近就为了一套遗留 Fortran 模块,硬生生把 gfortran 和 gcc 降级了一遍。期间踩过的坑比想象中多:源里没有旧包、源码编译缺前置依赖、装完 PATH 被搅乱、gfortran 和 gcc 版本对不上……所以才想把 gfortran 和 gcc 降级安装中的这些坑整理下来。
这篇文章不聊太虚的理论,只讲降级过程中你必须知道的版本关系、三条安装路线、源码编译细节,以及装好之后依然“跑不起来”时怎么排查。适合正在为老项目发愁的开发者,也适合第一次需要多版本编译器共存的新手。
1. 为什么非要降级?先搞懂 gcc 和 gfortran 的版本关系
1.1 别把 gfortran 和 gcc 看成一回事
很多人一开始会以为 gfortran 是独立软件,跟 gcc 没关系。实际上 gfortran 就是 gcc 编译器集合里的 Fortran 前端,它和后端、运行库是一套东西。你把 gfortran 降级了,但 gcc 主体还停留在新版本,短时间可能看不出问题,一旦项目里面混编 C 和 Fortran,或者构建系统同时探测gcc和gfortran的版本,就会开始互相拆台。
新旧版本之间最直接的冲突点是 ABI(应用二进制接口)。GCC 各版本默认的标准、函数签名、运行时库符号版本都在变化,尤其 Fortran 的.mod模块文件,并不是跨版本通用的。你用新版 gfortran 14 编译出的模块文件,让旧版 gfortran 9 去读,大概率会直接报module file version mismatch之类的错误。这就是为什么很多老项目会在 README 里明确写“需要 gfortran 9”,而不是随手拿系统默认编译器就能编。
1.2 怎么确定你真正需要哪个版本
降级前最忌讳的是凭感觉装一个“看起来差不多”的版本。我建议先做一次系统的文档调查,按优先级找三处信息:
- 项目根目录的 README、INSTALL、CHANGELOG,通常直接写明了编译器版本要求。
- 构建脚本:
configure.ac、CMakeLists.txt、Makefile里的检查逻辑,会暗示最低版本或推荐版本。 - 项目用到的第三方预编译库,发行说明里一般会标注“由 GCC 9 编译”,那就意味着你的工具链最好也是 GCC 9 系列。
如果你拿到的是一个源码包,可以直接跑一行搜索看看有没有隐含版本要求:
grep -R "gcc\|gfortran" README* configure.ac CMakeLists.txt 2>/dev/null | head -50注意,gcc 的版本号是主版本加次版本,比如 9.5.0。同一大版本内的小版本升级通常不破坏 ABI,但跨大版本就危险了。我的做法是:先确认目标大版本,再选该系列最后一个稳定的次版本,比如 GCC 9 系列用 9.5.0,GCC 8 系列用 8.5.0。这样既能满足项目要求,也能少碰一些老版本里已经修复的编译 bug。
2. 动手之前:确认现状和三条可行路线
2.1 先给系统做个体检
降级安装本质上是在一个已经被新工具链“占领”的系统里,塞进另一套旧工具链。所以动手之前一定要先搞清楚现状,别直接卸载。
我每次都会先跑这几条命令,把当前状态记录下来:
which -a gcc gfortran gcc --version gfortran --version ls -l /usr/bin/gcc* /usr/bin/gfortran* 2>/dev/null echo "PATH=$PATH" ldconfig -p | grep -E "libgfortran|libstdc\+\+" | head为什么要记录?因为后面无论用哪种方式安装,都可能要改 PATH、LD_LIBRARY_PATH或 alternatives 配置。如果中间改乱了,至少还能靠这份原始记录恢复。
这里有一个很重要的原则:不要卸载系统默认的新版 gcc。很多系统软件、内核模块、显卡驱动在安装时都依赖默认编译器,直接卸载轻则一堆软件包被连带移除,重则系统崩了。降级不是干掉新版换旧版,而是多装一套旧版并切换默认入口。
2.2 包管理器 / 源码编译 / 隔离环境怎么选
降级安装主要有三条路线,我整理成了一张对照表,方便你对号入座:
| 路线 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| 系统包管理器 | 安装快,依赖自动处理 | 旧版本不一定在源里,多版本切换麻烦 | 发行版源里刚好有目标版本 |
| 源码编译安装 | 想装哪个版本装哪个,前缀可自定义 | 编译时间长,需要 GMP/MPFR/MPC 等依赖 | 目标版本很老,或者需要自定义编译特性 |
| conda / 容器 | 完全隔离,不影响系统,安装快 | 需要额外环境工作,构建路径可能绕 | 同一台机器上有多个项目,版本诉求互相冲突 |
三条路线并不互斥。我经常是先用容器或者 conda 快速验证,确认项目确实能编过后,再决定要不要花时间源码编译一个长期使用的版本。
2.3 我的选型经验
如果这个旧编译器只是临时用来编一个项目,我绝对不碰系统包管理器和源码编译。最简单的是 conda 建一个隔离环境,或者容器启动一个带旧编译器的镜像,编译完拿产物就行。
如果这个旧编译器要长期作为团队默认工具链,那源码编译到/opt/gcc-9这种独立前缀就是正路。独立前缀的好处是:你可以随时切换回系统默认的 gcc/gfortran,两个版本共存,不会互相覆盖。相比之下,系统包管理器安装的版本通常会和系统路径绑定,后续想卸载或切换没那么干净。
3. 用包管理器“降级”时的坑
3.1 多版本共存,而不是卸载旧版本
在 apt 系发行版上,旧版本和新版本其实是可以共存的,软件包名称通常是gcc-9、gfortran-9、g++-9,注意不是gcc。安装命令本身很简单:
sudo apt update apt-cache policy gcc-9 gfortran-9 sudo apt install gcc-9 gfortran-9 g++-9安装完成后,你会发现/usr/bin/gcc-9和/usr/bin/gfortran-9都出现了,但/usr/bin/gcc还指向新版。这是正常的,也是很多人踩坑的地方:以为装完 gcc-9 就能用,结果一敲gcc --version还是新版。
另外,如果apt-cache policy显示没有候选版本,先别急着添加乱七八糟的第三方源。第三方源维护水平参差不齐,很可能引入依赖冲突,甚至把系统库替换成不兼容版本。更稳妥的做法是直接跳到 conda 或源码编译路线。
3.2 update-alternatives 只搞定了一半
要让gcc、g++、gfortran三个命令的默认入口都指向旧版本,需要手动配置update-alternatives。很多人只配了gcc,结果 gfortran 还是新版,后面活动目录,编译 Fortran 代码时用到的还是旧版吗?不是,是 gcc 和 gfortran 各管各的。
下面是一份可复制的配置流程:
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-9 90 \ --slave /usr/bin/g++ g++ /usr/bin/g++-9 sudo update-alternatives --install /usr/bin/gfortran gfortran /usr/bin/gfortran-9 90 sudo update-alternatives --config gcc sudo update-alternatives --config gfortran这里有一个容易忽略的细节:g++可以作为gcc的 slave 一起切换,但gfortran并不是gcc的 slave,必须单独设置。否则就会出现gcc --version已经是 9,gfortran --version还是新版的诡异状态。
如果你不想用update-alternatives,也可以在~/.bashrc里直接加:
export CC=/usr/bin/gcc-9 export CXX=/usr/bin/g++-9 export FC=/usr/bin/gfortran-9这种方式对 Makefile 和 CMake 通常有效,因为现代 build 系统会优先读取环境变量。但对那些在脚本里硬编码/usr/bin/gcc的项目无效。
3.3 依赖冲突:包管理器也救不了乱装的我
用包管理器装旧编译器时,最常见的报错是“需要先移除一大堆软件包”。有一次我想装某个旧版本,系统提示需要卸载libstdc++6的某个高版本库,我没细看就执行了移除确认,结果系统里跟编译器相关的包被连带删掉了一堆,连 Python 扩展编译都开始报错。
这里必须提醒:包管理器提示要“自动移除”的包,千万不要只看最后几行,一定要从头到尾看一遍。只要移除名单里出现 libc、binutils、python 解释器这类系统级软件,就说明这个旧包和当前系统不兼容,正确做法是取消安装,而不是强制移除。
想预演安装会动哪些包,可以先执行:
sudo apt-get install gcc-9 gfortran-9 --simulate--simulate只做解析和输出,不真正改动系统,是排查依赖冲突的安全工具。我后来养成了习惯,每次这种可能有依赖变动的安装,都先模拟一遍再动手。
3.4 依赖损坏之后的急救思路
如果你像我一样手快,已经造成依赖损坏,表现通常是所有apt命令都提示“依赖关系不正确,无法安装或移除软件包”。这时候先别慌,第一步是:
sudo apt-get -f install-f会尝试修复损坏的依赖关系,很多小问题到这里就解决了。如果修复失败,则要检查系统里是否有被强制卸载掉的核心包,比如某个版本的libc6。不同发行版恢复手段不一样,通用思路是用dpkg --force-depends先把核心包装回去,再让包管理器重新计算依赖。但这类操作风险很高,我建议先在虚拟机或容器里试一遍,不要直接在生产机器上硬来。
长期来看,系统包管理器更适合“恰好源里有目标版本”的情况,一旦需要降级到很老的版本,依赖问题会把你拖垮。这也是我越来越倾向 conda 和容器方案的原因。
4. 源码编译旧版 gfortran/gcc:绕不开的细节
4.1 下载源码和准备依赖
如果包管理器里没有,源码编译就是最可控的路线。源码编译 GCC 本身并不难,难在依赖准备和编译参数。老手也会在这里翻车,因为 GCC 编译依赖三个数学库:GMP、MPFR、MPC。它们分别负责大整数、浮点精确运算和复数运算,缺一个都会在configure阶段直接退出。
好在 GCC 源码目录自带一个脚本,可以帮你在本地准备这几个依赖:
tar -xzf gcc-9.5.0.tar.gz cd gcc-9.5.0 ./contrib/download_prerequisites如果你的网络环境受限,这三个库也可以手动下载,解压到 gcc 源码目录下,configure会自动识别。注意版本匹配:新 GCC 对这三个库有最低版本要求,但也不是越新越好,太新的库有时会和旧编译器源码里的测试逻辑冲突。一般直接用download_prerequisites脚本默认锁定的版本,最省事。
4.2 configure 参数真的有讲究
编译 GCC 不能直接进源码目录./configure,我通常会在源码目录外单独建一个 build 目录,避免污染源码树:
mkdir build && cd build ../configure \ --prefix=/opt/gcc-9 \ --enable-languages=c,c++,fortran \ --disable-multilib \ --disable-bootstrap \ --disable-nls每个参数背后都有原因:
--prefix=/opt/gcc-9:最重要。把旧编译器独立安装在单独目录,避免覆盖系统默认/usr/bin/gcc。--enable-languages=c,c++,fortran:只编译你需要的语言前端,能省至少三分之一编译时间。--disable-multilib:不生成 32 位兼容库,很多交叉编译环境根本用不到,开着反而容易因为缺 32 位库报错。--disable-bootstrap:跳过用新编译器编译自己多次的验证过程,速度会快很多,但安全性差一些。--disable-nls:禁用本地化语言支持,纯英文环境能减少一些依赖。
需要说明的是,--disable-bootstrap对初次尝试的人不一定是好事。bootstrap 会用刚刚编译出来的编译器重新编译一遍 GCC 自身,能提前暴露编译器和系统的兼容问题。如果你是在生产环境长期使用,我建议保留默认 bootstrap,多花点时间换个稳定;如果只是救急验证,关掉也行。
4.3 编译时长和资源控制
GCC 源码编译是一个很吃资源的活。拿我常用的 8 核 16G 内存机器来说,编译 GCC 9 系列,开启 bootstrap 的话,大概需要 40 到 90 分钟;关掉 bootstrap 能省不少时间。但这里的关键变量是make -j参数。
用make -j8时,内存占用会明显升高,如果系统同时还跑着别的服务,很容易触发 OOM。内存不太充裕时,我建议这样分配:
free -h nproc make -j4-j不是越大越好。一个保守的经验是,编译进程数不要超过 CPU 线程数的一半,否则内存会先扛不住。如果编译中途失败,不要急着删 build 目录,直接重新执行make通常可以从上次烂尾的地方接着编,省下大量时间。把编译日志重定向到文件里也很有用:
make -j4 2>&1 | tee /tmp/gcc-build.log这个日志是后面排查问题最重要的依据。
4.4 装完只有 PATH 对吗?还要一堆环境变量
make install顺利完成后,新编译器并不会自动可用。很多人只设置了PATH,结果编译 Fortran 时还是报找不到libgfortran.so。因为旧编译器安装到独立前缀后,它的动态库只在/opt/gcc-9/lib64里,系统的动态链接器默认不会去那里找。
我通常会在~/.bashrc里加这样一段:
export PREFIX=/opt/gcc-9 export PATH=$PREFIX/bin:$PATH export LD_LIBRARY_PATH=$PREFIX/lib64:$PREFIX/lib:$LD_LIBRARY_PATH export LIBRARY_PATH=$PREFIX/lib64:$PREFIX/lib:$LIBRARY_PATH export MANPATH=$PREFIX/share/man:$MANPATH export CC=$PREFIX/bin/gcc export CXX=$PREFIX/bin/g++ export FC=$PREFIX/bin/gfortranCC、CXX、FC这三个变量非常关键。很多构建系统在探测编译器时,优先读环境变量;如果你只改了 PATH,没有把FC指到/opt/gcc-9/bin/gfortran,项目里可能仍然用系统默认的老路径。还有一个小技巧:不要把这些变量全局写死,尤其是LD_LIBRARY_PATH。因为/opt/gcc-9/lib64里的库会影响到系统里其他软件的运行,建议在当前项目终端里临时导出,或者写进项目的env.sh脚本,而不是一登录就加载。
4.5 configure 常见失败怎么排查
源码编译最常见的失败在configure阶段,报错往往很抽象。我见过的几种典型情况:
- 缺少 GMP/MPFR/MPC:报错会直接提示你 building GCC requires GMP 4.2+、MPFR 2.4.0+、MPC 0.8.0+,但解决方法是先确认这三个库的 include 路径和 lib 路径是否被编译器找到。必要时用
--with-gmp、--with-mpfr、--with-mpc显式指定。 configure: error: cannot compute suffix of object files: cannot compile:这类错误去看config.log,里面通常藏着真正的编译错误,比如宿主系统缺少g++开发环境。GCC 源码编译需要宿主 C++ 编译器,就算你只需要 C 和 Fortran,也建议先确保g++可用。- 奇怪的“无法找到
crti.o”之类:多半是 64 位库路径没配对。检查--disable-multilib是否设置。
每次源码编译失败后,第一反应应该是打开config.log搜索error,而不是反复重跑make。我第一次编译 GCC 时,因为没看日志,把同一个命令跑了三遍,结果三遍都在同一个地方卡住,纯属浪费时间。
5. 版本切换完仍然“跑不起来”的高频问题
5.1 libgfortran.so 找不到,别急着改 LD_LIBRARY_PATH
旧编译器装好、PATH 也指过去之后,最常见的第一个崩溃信号是:
./test: error while loading shared libraries: libgfortran.so.5: cannot open shared object file原因很简单:程序运行时,动态链接器在系统的默认路径里找不到旧版libgfortran。解决方案有两种,我建议优先用第二种。
第一种是临时设置环境变量:
export LD_LIBRARY_PATH=/opt/gcc-9/lib64:$LD_LIBRARY_PATH适合手动验证,但如果你把设置写进.bashrc,全局影响会很大。有时候系统里某个 Python 包开始崩溃,就是被这种全局LD_LIBRARY_PATH坑的。
第二种是在编译时给程序写入 rpath:
gfortran -Wl,-rpath,/opt/gcc-9/lib64 test.f90 -o test这样编译出的可执行文件,运行时会主动去/opt/gcc-9/lib64找库,不需要用户手动设置环境变量。你可以用readelf -d test | grep -E "RPATH|RUNPATH"确认 rpath 是否写进去了。给一个项目设置 rpath,比全局改LD_LIBRARY_PATH干净得多。
5.2 gcc 和 gfortran 版本不一致的互相拆台
一个非常容易犯的错误是:which gcc指向系统新版,which gfortran指向旧版独立前缀。表面上两个命令都能跑,但混编项目一链接就出问题,一会儿符号找不到,一会儿libstdc++版本不匹配。
原因在于 gfortran 的驱动在链接时会按自己所属 GCC 版本去找配套库。如果你的 C 编译器来自新版,而 Fortran 编译器来自旧版,链接器的搜索路径就会变成“一部分新版、一部分旧版”,两个版本的库交叉引用,不出问题才怪。
我的习惯是:所有编译器相关的变量始终指向同一个前缀,最好直接用命令验证一致性:
echo "gcc path: $(command -v gcc)" echo "gfortran path: $(command -v gfortran)" gcc --version gfortran --version如果发现不一致,优先统一到旧版前缀。在 CMake 里尤其要小心,CMAKE_C_COMPILER和CMAKE_Fortran_COMPILER必须都是/opt/gcc-9/bin/下的可执行文件,而不是gcc全局命令。
5.3 老编译器遇到新 CPU 指令集
还有一个不太容易想到的坑是-march=native。在降级之前,项目可能在 Makefile 里写了-march=native,因为之前用的是新编译器,能识别现代 CPU 的特性。但是旧版编译器对新的 CPU 微架构支持不全,可能在编译时直接报:
error: bad value for '-march' switch这种情况下,最简单的就是把优化参数改成通用模型:
-march=x86-64 -mtune=generic性能损失通常可以接受,尤其对科学计算项目来说,能先跑通比什么都重要。如果是把旧编译器编译出的二进制搬到另一台 CPU 指令集不完全一样的机器上,同样可能触发“非法指令”错误。这倒不是降级的问题,而是-march=native带来的历史遗留问题。
5.4 常见症状排查速查表
我把自己折腾过程中遇到过的症状整理成了下面的速查表:
| 现象 | 排查命令 | 常见原因 |
|---|---|---|
gcc --version没变成旧版 | which -a gcc,ls -l /usr/bin/gcc | alternatives 没有配置,或 PATH 顺序不对 |
程序运行找不到libgfortran.so.* | ldd ./a.out,readelf -d ./a.out | 缺少 rpath,或LD_LIBRARY_PATH没设置 |
编译时gfortran命令不存在 | command -v gfortran | PATH 没有包含/opt/gcc-9/bin |
混编项目链接报libstdc++版本错误 | strings /path/libstdc++.so.6 | grep CXXABI | gcc/gfortran 版本不一致,或 rpath 指错目录 |
| Fortran 模块文件版本不匹配 | 查看编译日志里的.mod报错 | 多个编译器混用了同一个include目录 |
编译卡在configure: cannot compute | 打开build/config.log搜 error | 缺少依赖 / 宿主编译器不可用 / 路径错误 |
速查表没有覆盖所有情况,但能覆盖我遇到过的 80%。排查思路永远是先确认“用的是哪个编译器”,再确认“链接的是哪个库”,这两个问题查清楚了,基本就解决了一半。
6. 实在不想降级?还有两条更稳的路
6.1 用 conda 隔离出一套旧编译器
如果你只是某个项目需要旧版本,不想影响系统任何东西,conda 可能是最省心的选择。创建一个独立环境,指定编译器版本,无需 root,也无需和系统包管理器的依赖纠缠:
conda create -n legacy_env -c conda-forge \ gcc_linux-64=9.3.0 gfortran_linux-64=9.3.0 conda activate legacy_env gcc --version gfortran --version用 conda 时有个细节要注意:环境里的编译器命令通常带目标平台前缀,比如x86_64-conda-linux-gnu-gcc。虽然环境里也会有gcc的等价命令,但有些构建系统会优先找到 conda 环境外的系统编译器。最稳妥的办法是,在 conda 环境中显式导出编译器变量:
export CC=$CONDA_PREFIX/bin/x86_64-conda-linux-gnu-cc export CXX=$CONDA_PREFIX/bin/x86_64-conda-linux-gnu-c++ export FC=$CONDA_PREFIX/bin/x86_64-conda-linux-gnu-gfortranconda 环境的好处是:不用动系统,环境说扔就扔,说重建就重建。缺点是 conda 里的编译器包更新节奏未必完全贴合你最冷门的版本需求,但常规的 GCC 9、10、11 基本上都能找到。
6.2 用好容器,把工具链整个锁死
容器是另一个我很喜欢推荐的方案。它把“操作系统 + 编译器 + 依赖库”整个打包成一个环境,所有版本都在镜像里固定。对于那种需要长期协作、版本要求严格的项目,容器比 conda 更干净:
docker run --rm \ -v "$PWD":/work \ -w /work \ compiler-gcc9 \ bash -c "make clean && make"容器方案有几个实践细节:
- 构建产物默认会以 root 身份写入挂载目录,如果你的主机用户是普通用户,可能遇到文件权限问题。可以在容器启动参数里指定用户 ID,或者构建后手动修改属主。
- 容器内编译出的二进制,如果动态链接了容器里的
libgfortran,拿到宿主机运行可能仍然找不到库。最稳妥的做法是编译时加 rpath,或者把运行也放在同一个容器里。 - 不要忘记在 Dockerfile 里显式设置
ENV CC、ENV FC,否则容器内仍可能使用默认编译器。
容器对“一台机器上同时存在十个项目、十个不同工具链版本”的场景尤其适合。它和 conda 的区别是,conda 只隔离软件环境,容器连系统库一起隔离,隔离级别更高,当然也更重。
6.3 静态链接旧运行时,直接绕过动态库
最后一个技巧,不一定适合所有场景,但一旦能用就很省事:编译时把 Fortran 和 GCC 运行时静态链接进去,运行时不依赖系统里的旧动态库。
gfortran -static-libgfortran -static-libgcc mycode.f90 -o mycode这样编译出的可执行文件,不再需要目标机器上存在对应版本的libgfortran.so。缺点是二进制体积会变大,而且某些使用异常处理、动态加载的库未必能完美静态化。如果你只是把一个老计算程序交付到别的机器上跑,这一个参数很可能比折腾整套降级省力得多。
完全静态还可以用-static,但会引入 glibc 静态库相关问题,尤其是遇到 DNS 解析、用户切换这些功能时会有坑。我的建议是能不加就不加,先把-static-libgfortran -static-libgcc试起来,解决大部分动态库匹配问题。
最后说个我自己反复栽过的跟头:降级不像升级,不是装一个旧版本这么简单;它本质上是把一个旧工具链塞进一个已经被新工具链“占领”的系统。所以我现在的习惯是,第一优先用 conda 或容器,第二才是源码装到独立前缀,系统包管理器降级只当作补充。每次切换版本后,先跑一条验证命令:
echo 'print *, "hello"' | gfortran -x f95 - -o /tmp/hello && /tmp/hello能跑通,说明 gfortran 侧没问题;再跑一下gcc --version和gfortran --version,确认两个版本号是同系列。别嫌这些步骤琐碎,经验就是这么攒出来的。希望这篇能把 gfortran 和 gcc 降级安装里的这些坑,帮你提前填平。