ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Linux纯命令行内网源码编译安装GCC:依赖、bootstrap与排错

Linux纯命令行内网源码编译安装GCC:依赖、bootstrap与排错 在一台连不了外网的内网服务器上你要把某个新项目跑起来结果 configure 脚本一句话把你拦住了gcc 版本太低。这时候 apt install gcc 是没用的源里的版本依然是几年前那个装上去版本号纹丝不动。这类场景我在过去几年遇到过不止十次最后都指向同一条路Linux 纯命令行环境下从源码编译安装 gcc。它不像 apt 那样一条命令搞定中间会碰到依赖缺失、内存被打爆、装完版本号还是旧的这类问题但只要路子走对整个过程其实是可控的。这篇就把我从准备依赖到验证成功的完整流程拆开讲涉及源码包获取、依赖库处理、configure 参数取舍、make 阶段的崩溃排查、以及新旧版本共存的细节。适合已经会基本 Linux 命令、需要在一台没有外网或版本受限的机器上升级编译器的同学也适合想搞清楚 gcc 自举过程到底在干什么的人。1. 什么情况下必须走源码编译这条路大部分人的第一反应是sudo apt install gcc -y或者yum install gcc这没错日常开发够用。但有几类场景包管理器是真的帮不上忙硬扛着找 deb 包反而更浪费时间。先把这几种情况说清楚你判断一下自己是不是其中之一如果不是其实没必要折腾源码编译。1.1 系统源里的版本被锁死但项目要求 C17 甚至 C20最典型的是 Ubuntu 18.04 或者 CentOS 7 这类老系统。它们的默认源里 gcc 停在 7.x 或者 4.8你要用的库比如某些新版本的 C 库在头文件里直接写了#if __cplusplus 201703L就报错。这种情况下加 PPA 源是一种办法但很多生产环境根本不允许加第三方源或者加了源之后因为依赖链冲突升级失败。我见过最惨的一次是升级 gcc 的时候把系统的 libstdc 动了结果一堆系统工具启动不了。所以真正稳妥的做法是源码编译到一个独立目录/usr/local/gcc-13.2.0跟系统自带的完全隔离谁也不影响谁。1.2 内网隔离环境连不上任何软件源这是最硬的需求。有些服务器部署在纯内网只有一个跳板机能出网服务器本身apt update直接超时。这种情况下你需要在一台能上网的机器上下好gcc-13.2.0.tar.gz连同依赖库的源码包一起拷进内网。整个编译过程不依赖任何网络请求纯命令行操作这也是标题里“纯命令行”的意义所在——没有图形界面没有包管理器兜底全靠你自己一步步敲。1.3 需要特定语言支持而发行版包被裁剪过发行版的 gcc 包往往会拆成gcc、g、gfortran好几个默认只装 C 和 C。如果你要编 Fortran 或者 Ada 代码还得单独找包版本还可能对不上。源码编译的好处是--enable-languages这一项完全由你决定想要什么语言就开什么语言编出来的东西是一个完整的工具链前后端版本严格一致。做科学计算的同学对这点应该深有体会gfortran 和 gcc 版本错配引起的链接错误非常难查。注意如果你只是想在本地写点小代码练手发行版自带的 gcc 完全够用不要为了“用上新版本”去源码编译。源码编译 gcc 的时间成本和踩坑成本都不低一次完整编译在普通机器上通常要半小时到两小时机器差一点或者并行度开太高导致卡死时间会更长。2. 编译之前先把依赖链理顺GMP、MPFR、MPC 与 ISLgcc 源码不能直接 configure 就编它自身依赖几个高精度数学库。这一步是新手翻车最多的地方很多人一上来就../configure然后报一堆 “cannot find GMP” 或者 “header version mismatch”一查就是小半天。先把这几个库的关系和两种处理路线讲明白后面会省很多事。2.1 为什么编译器需要高精度数学库GMP、MPFR、MPC 这三个库分别处理大整数运算、多精度浮点运算和多精度复数运算。听起来跟“编译 C 代码”没关系其实关系很大编译器在编译期要做常量折叠比如constexpr计算、浮点字面量转换、模板元编程里的数值运算这些都要在编译阶段用远高于目标平台精度的算术完成保证结果跟目标平台运行时一致。ISL 则是给 GRAPHITE 循环优化框架用的属于可选依赖开了--with-isl才需要。三个库的关系是这样MPFR 依赖 GMPMPC 又依赖 GMP 和 MPFR所以顺序不能乱。如果你打算自己编这三个库编译顺序必须是 GMP → MPFR → MPC每个都要先./configure --prefix...再make make install。这也是为什么手动装依赖特别费时间光这几个库就要编三四轮。2.2 两种处理路线系统包和源码内嵌目录第一条路线是用系统包Ubuntu 下就是apt install libgmp-dev libmpfr-dev libmpc-dev libisl-dev。这条路快但有两个坑一是开发包版本可能太旧gcc 13 对 MPFR 的最低版本有要求版本不够会在 bootstrap 阶段报奇怪的错误二是这些包的头文件在/usr/include库在/usr/lib/x86_64-linux-gnu如果系统里还装过手工编的高版本链接时到底用哪个很难说清楚。我一般只在确定版本够新的情况下才走这条路。第二条路线是把依赖源码放进 gcc 源码根目录让 gcc 自己的构建系统去编译它们。这是我最推荐的方式原因是完全隔离、版本可控、不污染系统目录。具体做法在下一节讲。两条路的对比可以看下面这张表处理方式优点缺点适用场景系统开发包安装快一条命令版本不可控可能污染链接路径版本足够新的临时环境源码内嵌目录隔离干净版本可控首次编译多花十几分钟生产环境、内网环境手工单独编译完全自主可复用给其他项目步骤多顺序易错需要给多个软件共用同一套依赖2.3 download_prerequisites 脚本的真实行为gcc 源码目录里有个contrib/download_prerequisites脚本很多人不知道它具体干了什么。它会从 GNU 官方镜像下载 GMP、MPFR、MPC、ISL 四个包的指定版本 tar 包然后解压到源码根目录并创建软链接名字就叫gmp、mpfr、mpc、isl。gcc 的 configure 检测到这些目录后会在构建过程中先编译它们然后静态链接进去或者放到目标目录整个过程对外是透明的。内网环境的做法就是在能上网的机器上先跑一遍这个脚本把下载下来的四个.tar.bz2文件拷到内网机器的 gcc 源码根目录再跑一次脚本。脚本发现有现成的压缩包就不会再去下载直接解压这样可以离线完成。这一步特别关键很多人不知道可以这么做白白在内网里挣扎。# 在能上网的机器上提前把包下好 cd gcc-13.2.0 ./contrib/download_prerequisites # 这时候目录下会有 gmp-6.x.x.tar.bz2 之类的文件 # 把这些 tar.bz2 拷到内网机器的 gcc 源码根目录再执行同样的脚本提示脚本下载的版本是跟 gcc 版本绑定好的比如 gcc 13.2.0 对应 MPFR 4.1.0、MPC 1.2.1、ISL 0.24 左右。别自己换版本换了之后如果没通过 gcc 内部的版本检查会直接报错。另外拷贝的时候一定要把.tar.bz2原文件带过去只拷解压后的目录脚本是不认的。3. configure 参数逐项拆解从 prefix 到 bootstrap依赖准备好就到了配置阶段。这一步的参数直接决定后面编译能不能过、编出来的东西放在哪、要不要花两倍时间。网上有很多复制粘贴的命令但没人告诉你为什么这么写。我把自己常用的那套参数一项项拆开讲你可以按需增删。3.1 prefix 与多版本共存的目录约定--prefix指定安装位置默认是/usr/local但我强烈建议带上版本号比如--prefix/usr/local/gcc-13.2.0。理由很简单将来你要再装一个 gcc 14两个版本放在不同目录切换只需要改 PATH不会有任何文件互相覆盖。如果都装到/usr/local第二次编译会覆盖第一次的一部分文件出问题很难回滚。带版本号的另一个好处是make uninstall或者直接rm -rf那个目录就能干净卸载比包管理器还省心。配置前先建一个独立的构建目录不要在源码目录里直接 configure这叫 out-of-tree build是 GNU 工具链的标准做法。源码目录保持干净出问题重新建个 build 目录就行不用重新解压源码。tar -xf gcc-13.2.0.tar.gz cd gcc-13.2.0 ./contrib/download_prerequisites mkdir build cd build ../configure --prefix/usr/local/gcc-13.2.0 \ --enable-languagesc,c,fortran \ --disable-multilib \ --with-system-zlib3.2 enable-languages 与 disable-multilib 的实际影响--enable-languages决定编哪些前端。默认只编 C也就是 gcc 本体。要做 C 开发就必须加 c要 Fortran 加 fortran。注意不加 c 编出来的包里是没有 g 的很多新手装完发现 g 命令找不到就是因为漏了这一项。如果你想省时间就只写自己需要的语言多一个前端就多一份编译时间。--disable-multilib是关掉 32 位支持。现代服务器基本都是 64 位开 multilib 会额外编译一套 32 位的运行库编译时间翻倍不说还需要系统装了 32 位的 glibc 开发包否则配置阶段就报错。除非你确实要交叉编译或者跑 32 位程序否则一律加上这个参数。--with-system-zlib是复用系统自带的 zlib省一次 zlib 的编译。zlib 版本兼容性很好用系统的没问题。对应的还有--with-system-readline但那个容易因为 readline 版本差异出问题我不太建议开。3.3 bootstrap 机制到底在做什么这是最容易让人困惑的一项。gcc 默认开启 bootstrap也就是“三阶段自举”。过程是这样先用宿主 gcc 编出一个 stage1 编译器用 stage1 编出 stage2 编译器再用 stage2 编出 stage3然后比较 stage2 和 stage3 的产物如果完全一致说明编译器是稳定的把它作为最终结果。为什么这么设计因为用旧编译器编新编译器有可能旧编译器的 bug 会传导到新编译器里导致新编译器在编译某些代码时产生错误结果而这种错误平时根本看不出来。三阶段比较就是一道保险如果两次独立编译出来的二进制一致说明编译过程没有被某个随机 bug 影响。开了 bootstrap编译时间大约是关掉的两到三倍。常见做法是生产环境保留 bootstrap本地测试加--disable-bootstrap省时间。我的建议是第一次装千万别关因为你不知道宿主编译器有没有毛病等确认这套源码能编过之后后续做一些实验性构建再考虑关闭。4. make 阶段崩掉的几种典型情况与排查链路configure 顺利跑完不代表没事真正的难关在make。这一步动辄一两个小时中途崩掉会让人很崩溃。我按遇到频率从高到低把几个典型情况和排查思路写下来你遇到报错的时候可以对着查。4.1 cc1plus 被 OOM Killer 干掉并行度怎么算最常见的崩溃是编译到一半突然报internal compiler error: Killed (program cc1plus)或者 shell 里没有任何提示make 直接退出日志里也看不出错。这种情况九成是内存不够被系统的 OOM Killer 杀了。bootstrap 阶段每个编译进程尤其是编译 C 的 cc1plus峰值内存大概在 1.5 到 2GB如果你用make -j$(nproc)在 16 核机器上开 16 个并行任务就算有 32GB 内存也可能被瞬间吃光。排查方法很简单编译的时候另开一个终端用free -h盯着内存或者在编译失败后跑dmesg | tail -50如果看到Out of memory: Killed process xxxx (cc1plus)就实锤了。解决方法是降低并行度经验值是-j的值乘以 2GB 不超过可用内存。16GB 内存就make -j68GB 就make -j3宁慢勿崩崩一次重头再来更亏。# 查看内存和交换分区 free -h # 编译失败后查看内核杀进程记录 dmesg | grep -i killed process # 降低并行度重新编译 make -j6另外交换分区swap要留着别关。虽然 swap 会拖慢速度但它在内存峰值的时候能救命避免进程被直接杀掉。如果你是在容器里编译还要注意容器的内存限制free看到的是宿主机内存但实际能用的可能远小于这个数。4.2 汇编器版本过旧与强制指定 as第二种常见报错是汇编阶段失败提示Error: unknown pseudo-op或者no such instruction。这是因为 gcc 编出来的汇编代码用了新指令但系统里的 GNU as汇编器版本太老不认识。gcc 和 binutils 是配套的新版本 gcc 往往需要一定版本以上的 binutils。处理方式有两种。一种是升级系统 binutils但升级系统工具风险较大。另一种是在 configure 里用--with-as/path/to/as和--with-ld/path/to/ld指定一套新的 binutils。做法是自己源码编译一套 binutils 装到独立目录然后 configure 的时候指过去。这个思路和 gcc 本身隔离部署是一脉相承的都是为了不动系统文件。判断是不是汇编器问题可以在报错的时候看日志上一条命令通常是一行as或者gcc -c xxx.s把那条命令的汇编文件留下来单独执行一下看清楚是哪个指令不认识。如果是-Wa,--noexecstack这类选项问题那就是另一回事了。4.3 bootstrap 比较失败宿主编译器缺陷与构建目录污染最让人抓狂的是 bootstrap 阶段报comparison failed前面所有编译都成功了就在最后一步 stage2 和 stage3 对比的时候不一致。这个错误的成因有好几种得逐个排除。第一种是宿主编译器本身有 bug导致 stage1 编出来的东西不稳定。这种情况比较少但确实存在尤其是一些老系统上打过补丁的 gcc。第二种是构建目录不干净比如之前编译失败过build目录里残留了中间产物直接重新 make 就会出问题。我的习惯是只要失败过就rm -rf build mkdir build重来宁可多花配置的几分钟。第三种是随机性因素比如某些环境里 ASLR 或者时间戳被编进了二进制导致对比失败。这种情况可以加--disable-bootstrap绕过但要清楚这是在放弃一道安全检查。还有一种隐藏原因是并行编译引起的竞争把-j降到 1 再试一次如果单线程能过那就是构建系统的并行 bug可以改用较小的并行度。提示make失败之后日志往往已经刷过去看不到了。建议编译的时候把输出重定向到文件make -j6 21 | tee build.log出错之后直接grep -n Error build.log定位比在终端里翻历史高效得多。5. 装完之后 gcc --version 还是旧的PATH 与动态库的真实顺序make install跑完很多人兴冲冲地敲gcc --version发现还是原来的版本然后开始怀疑人生。这个现象非常普遍本质上不是安装失败而是你的 shell 根本没用上新装的 gcc。搞清楚 PATH 和动态库的查找顺序这个问题就迎刃而解。5.1 which gcc 和 type -a gcc 的区别which gcc只会告诉你 PATH 里第一个命中的 gcc 在哪。而type -a gcc会把所有命中的 gcc 都列出来还会告诉你哪些是别名、哪些是函数。排查版本问题的时候一定要用type -a gcc因为它能让你看到系统里到底有几个 gcc以及各自的路径。经常出现的情况是/usr/bin/gcc在前/usr/local/gcc-13.2.0/bin在后shell 当然用前者。解决办法是把新版本的 bin 目录放到 PATH 最前面注意是最前面不是随便加。写进/etc/profile.d/下一个单独的脚本文件对所有用户生效同时不影响系统默认配置。# 写成独立配置文件方便管理和回滚 cat /etc/profile.d/gcc13.sh EOF export PATH/usr/local/gcc-13.2.0/bin:$PATH EOF chmod x /etc/profile.d/gcc13.sh # 重新登录或者手动 source 生效 source /etc/profile.d/gcc13.sh改完之后重新开一个 shell再执行type -a gcc确认/usr/local/gcc-13.2.0/bin/gcc排在第一位才算成功。5.2 libstdc.so.6 版本冲突的处理PATH 改对了gcc --version也显示新版本了但编译 C 程序的时候又报错version GLIBCXX_3.4.30 not found。这是动态链接器找的是系统里的旧 libstdc而你的程序需要新版本里才有的符号。系统自带的/usr/lib/x86_64-linux-gnu/libstdc.so.6版本是固定的不能随便替换替换了系统里依赖它的一堆程序可能起不来。正确做法是让动态链接器同时知道新版本的库目录。把新 gcc 的库路径写进/etc/ld.so.conf.d/然后执行ldconfig刷新缓存。注意路径是lib64还是lib取决于你的构建配置进目录看一眼再写别想当然。# 确认库文件确实存在 ls /usr/local/gcc-13.2.0/lib64/libstdc.so.6* # 写入配置 echo /usr/local/gcc-13.2.0/lib64 /etc/ld.so.conf.d/gcc13.conf ldconfig # 验证链接器能不能找到 ldconfig -p | grep libstdcldconfig -p会列出链接器缓存里所有库确认新的 libstdc 在里面并且路径正确。还有一种情况是程序运行时用了RPATH或者环境变量LD_LIBRARY_PATH覆盖了默认查找顺序那样的话ldconfig也救不了得检查编译命令里有没有带-Wl,-rpath。5.3 用 update-alternatives 做可回退的版本切换如果你的机器需要同时保留系统 gcc 和新 gcc又不想每次改 PATH可以用update-alternatives管理。它会给每个版本分配优先级你在一个命令里切换出问题还能一键切回系统版本。这个机制在需要频繁切换编译器的场景下很实用。# 注册新版本优先级设高一点 update-alternatives --install /usr/bin/gcc gcc /usr/local/gcc-13.2.0/bin/gcc 100 update-alternatives --install /usr/bin/g g /usr/local/gcc-13.2.0/bin/g 100 # 交互式选择 update-alternatives --config gcc不过这里要提醒一句update-alternatives操作的是/usr/bin/gcc这个公共入口本质上还是在动系统层面的软链接。如果机器上跑着关键业务切换之前最好先确认没有程序依赖旧版本的编译行为。相比之下我更喜欢 PATH 方案改动小、影响面清晰、出问题改一个文件就能恢复。6. 验证安装结果与后续版本升级的一点体会到这一步命令都敲完了但事情没结束。真正验证安装成功不能只看版本号还得实际编译一个用到新特性的程序把编译和运行链路都走通一遍。另外未来升级到更新版本时如果操作不当会把已有环境搞乱这里也一并说清楚。6.1 用一段真实代码验证编译和运行版本号对上了只说明 gcc 本体是新的不代表头文件和运行库都对。我的验证方式是写一段用新标准的代码包含一个 C17 或 C20 的特性编译执行看结果。比如下面这段用了 C17 的if constexpr和结构化绑定用旧编译器直接编不过能编过就说明前端和标准库都是新的。// test_cpp17.cpp #include iostream #include tuple #include string template typename T std::string describe(const T v) { if constexpr (std::is_integral_vT) { return integer: std::to_string(v); } else { return other type; } } int main() { auto [a, b] std::make_tuple(1, std::string(hello)); std::cout describe(a) std::endl; std::cout b std::endl; return 0; }编译的时候显式指定标准然后运行再顺手看一下编译器的搜索路径是否正确g -stdc17 -o test_cpp17 test_cpp17.cpp ./test_cpp17 g --version g -print-search-dirs g -print-file-namelibstdc.so最后一条命令会打印出编译器实际使用的 libstdc 路径如果指向的是/usr/local/gcc-13.2.0/lib64/下的库就说明整套链路都打通了。这一步比单纯看版本号可靠得多我强烈建议每个人都做一遍。6.2 升级到新版本时的推荐做法假设半年后你想从 13.2.0 升到 14.x我的建议是不要动原来的目录新版本装到/usr/local/gcc-14.x.x然后把/etc/profile.d/gcc13.sh里的路径改成新目录或者新建一个更高优先级的配置文件。原来的 13 版本保留着万一新版本编出来的程序有兼容性问题改回 PATH 就能立刻恢复。磁盘空间要提前规划。一套完整的 gcc 源码加编译产物加安装目录占用大概 15 到 20GB具体看开了多少语言和目标架构。内网机器空间紧张的时候可以在make install完成后删掉源码和 build 目录只留安装目录这样能压到 3GB 左右。还有一点是编译时间源码编译 gcc 这件事第一次做的时候特别容易低估它。我现在一般会把它安排在下班前启动或者放到虚拟机里让它自己跑而不是坐在那儿盯着。中途去干别的事情回来看看build.log有没有报错比守着一行行刷屏舒服得多。编译过程中如果发现机器风扇狂转、响应变慢多半是并行度开太高了及时降下来别硬扛到系统卡死那样反而会丢失中间的编译进度。对整个流程回看一遍其实最花时间的不是敲命令而是排查那些看起来莫名其妙的报错。依赖版本、内存、PATH、动态库这四个点是绝大多数失败案例的根源把这几处的原理弄清楚剩下的就是等待和验证。装一次之后你就会发现源码编译 gcc 没有想象中那么可怕它只是要求你每一步都清楚自己在做什么。
返回列表