
1. 中标麒麟V7.6自带GCC-4.8.5为什么还是要手动编译一份新的在国产服务器操作系统的圈子里待久了总会被问到一个很现实的问题操作系统自带的编译器明明能跑通大部分老代码为什么还要费劲巴拉地自己编译一份GCC尤其是中标麒麟高级服务器操作系统V7.6这个版本它默认捆绑的编译器版本停留在GCC-4.8.5准确说是4.8.5系列这个版本对不少新项目来说已经明显不够用了。1.1 先看清V7.6这套系统的编译器底座中标麒麟高级服务器操作系统V7.6的内核和用户态工具链整体对齐的是RHEL7/CentOS7那一代技术体系。它的glibc版本通常在2.17上下系统自带的GCC就是4.8.5。这个组合在2015年前后是很标准的配置但放到今天问题就暴露出来了。最直接的痛点就是语言标准的支持落后——GCC-4.8.5对C14的支持只能算部分实现对C17几乎等于没有更别提C20了。具体表现是什么呢你的项目里只要用了std::make_unique、std::optional、结构化绑定、if constexpr这类写法编译阶段就会直接报错提示没有这个成员或者语法错误让人一头雾水。更麻烦的是很多第三方库比如较新版本的CMake、Boost的部分组件、RocksDB已经明确要求GCC 7以上才能编译这时候你再怎么改代码也无济于事唯一的出路就是把编译器本身换掉。1.2 到底该不该自己编译几个判断维度不是所有场景都值得自己动手编译GCC。我梳理了几个判断维度你可以对着自己的情况过一遍。如果你的目标只是跑一两个老项目或者系统里现成的软件包比如通过yum装的devtoolset就能满足那优先选择软件源里现成的工具链别折腾编译。但如果出现下面这几种情况就基本只能走源码编译这条路了目标机器无法访问外网的软件源devtoolset这类SCL仓库用不了需要特定小版本比如就是要8.5.0其他8.x版本不算保证构建产物可复现需要自己定制编译选项比如开启某个语言支持、调整默认的PIE/SSP策略项目要求编译器和运行时库自包含不想依赖系统源里那一堆版本换算。我实际遇到最多的是第二种和第四种。很多甲方环境是内网隔离的装个新编译器只能自己编译打包然后拷进去这时候搞清楚源码编译的完整流程就是硬需求了。1.3 升级目标与风险边界要先想清楚动手之前务必想清楚你要的是替换系统编译器还是并存一个新版本。这两个方向的后果完全不同。系统自带的GCC-4.8.5是整个操作系统的一部分很多系统级组件包括内核模块、部分系统库都是用它编译出来的。如果你冒失地把/usr/bin/gcc直接指向新的8.5.0万一新编译器默认开启了一些激进的优化或安全特性比如-fstack-protector-strong、默认PIE可能会让原来正常编译的系统组件出问题甚至影响到一些脚本的自动构建。所以我个人的原则很明确新编译器一律装到独立前缀比如/usr/local/gcc-8.5.0通过环境变量或alternatives机制去切换绝不覆盖系统原始编译器。这样既拿到了新能力又保留了随时回退的后路。这个原则会贯穿后面所有操作。2. 编译之前的环境侦察与依赖准备我见过太多人一上来就解压源码敲./configure结果卡在依赖报错上折腾半天。源码编译GCC的前期准备比编译过程本身更考验耐心。这一节我把环境盘点和依赖准备拆开讲清楚。2.1 先把系统信息和现有工具链摸清楚第一步是搞明白机器的底子。登录目标服务器后我会依次执行几条命令把关键信息记下来cat /etc/os-release uname -r ldd --version | head -n 1 gcc --version g --version make --version这几条命令分别告诉你系统发行版标识、内核版本、glibc版本、现有C/C编译器版本、make版本。为什么要在意这些因为GCC-8.5.0的编译对底座有硬性门槛。它要求glibc不低于2.17V7.6恰好满足要求有一个能编译C的引导编译器bootstrap compiler系统自带的g 4.8.5正好可以干这个活还要求make的版本不能太老。顺手再看一眼磁盘和内存df -h /usr/local free -h nproc编译一整套GCC含C/C/Fortran的源码树加上中间产物峰值占用能到10GB以上所以/usr/local所在的文件系统至少留出15GB的空余。内存方面如果单条编译单元在OOM边缘很容易被内核直接kill掉后面第五节的排查会专门讲这个。nproc是看核数决定了你后面能开多少并行任务。2.2 三个层次的依赖引导编译器、数学库、构建工具GCC的依赖可以分成三层来理解理清这个层次装依赖就不会漏。第一层是引导编译器。GCC要编译自己得先有个能用的C和C编译器。好消息是V7.6自带的gcc/g 4.8.5足够作为bootstrap不用额外装。但你要确认g在不在因为纯gcc包不含C前端缺少g会导致configure阶段报C compiler missing。第二层是数学库也就是GMP、MPFR、MPC这三兄弟。它们负责高精度浮点和大数运算是GCC内部运算的基石。GCC-8.5.0要求GMP≥4.3.2、MPFR≥2.4.2、MPC≥0.8.1。系统源里通常有gmp-devel、mpfr-devel、libmpc-devel装上就能用。如果你在内网环境拿不到这些rpm还有一个非常省事的办法——GCC源码目录里自带了contrib/download_prerequisites脚本它会自动去上游下载这三个库的源码并一起编译彻底摆脱对系统devel包的依赖。第三层是构建工具包括make、flex、bison、texinfo、wget或curl。flex和bison是词法/语法分析生成器编译GCC的某些前端会用到texinfo是为了生成文档如果你加了--disable-doc可以省掉。2.3 源配置与依赖安装实操在内网环境先确认yum源可用yum repolist如果源正常直接一条命令把该装的都装上yum install -y gcc gcc-c make flex bison texinfo \ gmp-devel mpfr-devel libmpc-devel \ wget tar xz这里有个容易被打脸的细节装完gmp-devel之后configure脚本有时候仍然找不到它原因是它找的是.so和头文件所在的路径如果系统把库放在非标准位置就找不到。解决办法是显式告诉configure路径后面第3节会讲。外网受限、download_prerequisites也跑不通的情况下我的备选方案是提前在一台能上网的机器上把GMP/MPFR/MPC的源码包按GCC要求的版本下载好连同GCC源码一起拷进内网然后在GCC源码根目录手动解压到对应位置configure会自动识别。这个离线打包的思路在内网交付场景里几乎是标配操作值得提前准备。提示依赖装好后别急着删rpm缓存后面如果configure报缺库重新定位问题会方便很多。3. GCC-8.5.0源码编译的完整流程拆解环境备齐接下来就是真正的编译。我把它拆成取源码、configure、make、install四步每一步都有关键取舍我会把取舍的理由讲透。3.1 源码获取与目录约定先从官方镜像下载源码包。注意GCC的源码只在根目录的gcc-x.y.z目录下不带子模块cd /usr/local/src wget https://ftp.gnu.org/gnu/gcc/gcc-8.5.0/gcc-8.5.0.tar.gz tar -xzf gcc-8.5.0.tar.gz cd gcc-8.5.0如果你需要GMP/MPFR/MPC的离线依赖就在这个目录里执行./contrib/download_prerequisites这个脚本会把三个依赖解压到源码树下并建立软链接configure时自动使用。我在内网环境经常手动完成这一步下载gmp-x.tar.bz2等文件解压成gmp-x目录放在源码根再创建同名软链接ln -s gmp-x gmp效果一样。一个重要的习惯构建目录和源码目录分开。GCC官方明确推荐out-of-tree build也就是不要在源码目录里直接configure而是新建一个build目录mkdir /usr/local/src/gcc-build cd /usr/local/src/gcc-build这样做的好处是源码树保持干净编译失败要重来的时候直接删掉整个build目录重新configure不用重新解压源码省时省心。3.2 configure阶段的参数取舍configure是整个编译的大脑参数选错了后面全白干。我常用的配置命令长这样/usr/local/src/gcc-8.5.0/configure \ --prefix/usr/local/gcc-8.5.0 \ --enable-languagesc,c,fortran \ --disable-multilib \ --enable-shared \ --enable-threadsposix \ --with-system-zlib \ --disable-bootstrap逐条说下为什么这么配。--prefix把安装路径钉死在独立目录避免污染系统这是我们保命的关键。--enable-languagesc,c,fortran只开我需要的语言前端每多一个语言编译时间就往上翻。如果你只写C/C把fortran删掉能省不少时间。--disable-multilib很关键。它在64位系统上只生成64位库跳过32位的编译。默认开启multilib会让编译时间翻倍还不止绝大多数现代服务器项目根本不需要32位关掉它。--enable-threadsposix让GCC支持POSIX线程影响C的std::thread等组件的可用性一般都要开。--with-system-zlib用系统自带的zlib而不是GCC自己带的那份能减小产物体积。--disable-bootstrap是用不用GCC编译自身三遍的开关。默认bootstrap会让整个编译过程重复三次用来验证编译器自举的正确性代价是时间乘以约3。如果只是内部使用、对极致正确性没那么苛求加上--disable-bootstrap能把编译时间压缩到三分之一。反过来如果是要交付给严格环境建议保留bootstrap以确认自举无误。如果configure报找不到GMP就补上三个显式路径--with-gmp/usr \ --with-mpfr/usr \ --with-mpc/usrconfigure成功结束的标志是最后打印出一段summary列出各语言前端的启用状态。看到summary基本就稳了一半。3.3 make与make install时间与资源管理接下来是漫长的编译。用并行加速make -j$(nproc)这里核数和内存要平衡。经验上每个并行编译单元的峰值内存大致在1.5到2GB之间。如果你机器只有8GB内存却开16个核并行很容易在编译某些大文件时触发OOM被kill。稳妥的做法是内存不够时并行度宁小勿大比如make -j4。宁可多等一会儿也不要让编译中途崩掉重来。整个编译过程视机器性能从半小时到两三个小时都很正常。我一般挂在nohup里跑配合日志输出nohup make -j8 build.log 21 tail -f build.log编译成功后执行安装make install安装完成后检查一下目录结构ls /usr/local/gcc-8.5.0/bin应该能看到gcc、g、gfortran等可执行文件。到这里新编译器算是造出来了但它还没上岗。4. 让新GCC安全生效多版本共存与切换机制装好不等于能用。系统默认还是会去调/usr/bin/gcc也就是老的4.8.5。你得决定用什么方式让新编译器生效这里提供两套方案从轻到重。4.1 环境变量方式最轻量的切换最不侵入的方式是改当前shell的PATHexport PATH/usr/local/gcc-8.5.0/bin:$PATH export LD_LIBRARY_PATH/usr/local/gcc-8.5.0/lib64:$LD_LIBRARY_PATH把新编译器的bin放前面shell就会优先找到它。第二条LD_LIBRARY_PATH非常关键因为GCC-8.5.0配套的libstdc.so是新的编译和运行程序时需要优先加载它否则可能用回系统的老版本。验证一下是否真的切过来了which gcc gcc --version如果gcc --version显示8.5.0就说明PATH生效了。这种方式的优点是完全可回退退出shell或者unset变量就恢复原样缺点是它是会话级的换个终端就没了。适合临时编译某个项目时使用。如果想让某个用户长期用新编译器把这两行加到~/.bashrc或~/.bash_profile里就行。但千万不要加到/etc/profile里那会影响系统所有用户和系统级构建风险太大。4.2 alternatives机制系统级统一切换如果希望整台机器或特定编译用户组统一使用新编译器同时又能方便地回退用alternatives是正道。中标麒麟/RHEL系用的是alternatives命令不是update-alternativesalternatives --install /usr/bin/gcc gcc /usr/local/gcc-8.5.0/bin/gcc 80 alternatives --install /usr/bin/g g /usr/local/gcc-8.5.0/bin/g 80 alternatives --install /usr/bin/cc cc /usr/local/gcc-8.5.0/bin/gcc 80 alternatives --config gcc--install那条里最后一个数字80是优先级优先级高的默认被选中。--config gcc会弹出交互菜单让你在4.8.5和8.5.0之间选当前生效的版本。想回退重新--config选回老版本即可。这里有个我个人踩过的坑只切换了gcc而忘了g和cc结果C代码用新编译器、C代码还在用老编译器链接时出现一堆ABI不兼容的诡异报错排查了很久才反应过来是符号链接没切全。切换时一定要把gcc、g、cc甚至cpp一起处理保持一套完整。4.3 验证编译能力是否真的到位光看gcc --version还不够得实际编一段用到新标准的代码确认它真的能干活。写一个测试文件#include iostream #include optional #include memory int main() { std::optionalint v 42; auto p std::make_uniqueint(7); if (v) std::cout ok\n; return 0; }用g -stdc17 test.cpp -o test ./test编译运行。如果报错找不到optional说明压根没切到新版本如果编译过了但运行时提示GLIBCXX_3.4.25 not found说明链接和运行时库路径有问题这正是下一节要重点讲的。5. 编译与使用过程中的典型报错及排查链路真刀真枪干的时候报错才是常态。我把这些年遇到的几类高频问题整理成排查链路希望你能少走弯路。5.1 configure阶段卡在数学库定位而不是乱装最典型的报错是configure: error: Building GCC requires GMP 4.3.2, MPFR 2.4.2, MPC 0.8.1新手的第一反应往往是再装一个devel包但这时正确的动作是先看GCC自己的配置日志。日志在build/config.log里搜GMP能找到它到底在哪找、为什么没找到。三种常见原因一是根本没装devel包二是装的位置不在默认搜索路径需要加--with-gmp/your/path三是系统里库存在但头文件缺失GCC要的是库头文件齐全缺头文件一样报错。搞清楚是哪一种再对症下药比盲目重装高效得多。用download_prerequisites的场景报错往往是网络问题导致依赖没下全。看contrib脚本的输出日志就能发现是哪个包下载失败手动补上即可。5.2 编译中途被kill十有八九是内存编译跑到一半突然中断日志末尾出现Killed或者signal 9这不是编译器的bug而是操作系统内存不足把编译进程杀了。用dmesg | tail能看到OOM killer的记录。解决办法有优先级首先降低并行度把-j16改成-j4其次如果可能临时加swap空间dd if/dev/zero of/swapfile bs1M count8192 chmod 600 /swapfile mkswap /swapfile swapon /swapfile还有一种被忽略的情况——磁盘满了。编译到某个阶段突然报no space left on device回头一看/usr/local所在的盘只剩几百MB。GCC的中间产物非常占地方预留15GB以上是底线。5.3 运行期报错libstdc版本冲突排查这个坑最隐蔽。程序在新编译器下编译成功拿到另一台机器跑或者在本机非编译环境下跑报/usr/lib64/libstdc.so.6: version GLIBCXX_3.4.25 not found原因是程序链接了新GCC自带的libstdc.so.6但运行时加载的是系统老版本。系统老库没有新版本符号于是报错。定位方法用strings /usr/local/gcc-8.5.0/lib64/libstdc.so.6 | grep GLIBCXX看看新库支持到哪个版本再和系统库对比。解决路径有两条一是运行时通过LD_LIBRARY_PATH指向新库目录二是把新libstdc注册进系统动态库搜索路径echo /usr/local/gcc-8.5.0/lib64 /etc/ld.so.conf.d/gcc-8.5.0.conf ldconfig第二种是系统级改动影响面大一定要评估后再做或者干脆在编译程序时用-static-libstdc把C运行时静态链进去彻底摆脱对目标机库版本的依赖。这个-static-libstdc技巧在网络交付场景里特别实用。报错类型典型信息根因首选方案configure依赖GMP/MPFR/MPC requires库或头文件缺失补依赖或显式指定路径编译被killKilled / signal 9内存不足降并行度或加swap磁盘写满no space left on device中间产物占满盘清理或换到大盘编译运行期符号GLIBCXX_3.4.25 not found运行时库版本旧静态链接或配置ld.so6. 压缩等待时间、控制体积与长期维护的实操经验编译一遍GCC动辄一两个小时谁都不想把时间浪费在重复劳动上。这一节聊聊怎么把效率和后续维护都安排明白。6.1 用ccache和并行度榨干机器性能ccache是个编译结果缓存工具它能在编译相同源文件时直接命中缓存把耗时砍下来一大截。对于需要反复编译、调试、重编的场景装一个ccache很值yum install -y ccache export CCccache gcc export CXXccache g之后再configureGCC的引导阶段就会走缓存。第一次编译不会变快但后续重编或者换参数再编时速度提升非常明显。并行度上我的经验公式是并行数 min(CPU核数, 内存GB数 / 2)。比如16核32GB的机器开到-j16没问题8核16GB的机器保守点-j8。别硬顶一次OOM重来浪费的时间远比多等一会儿多。另外把编译命令包在screen或tmux里跑可以避免SSH断线导致编译中断。这个细节看起来小但吃过亏的人都知道它有多重要。6.2 精简语言集合与文档控制安装体积默认编译会带上很多你可能永远用不到的东西。除了前面提到的--disable-multilib和精简--enable-languages还可以在install时跳过文档生成make install-strip它会在安装时对可执行文件执行strip去掉调试符号显著减小磁盘占用对于磁盘紧张的机器很实用。如果你确实需要生成PDF/HTML文档那再单独走make install但大多数服务器环境并不需要文档。安装完成后核对一下体积du -sh /usr/local/gcc-8.5.0一个只含C/C的8.5.0通常在一两个GB的量级算合理。6.3 版本回滚与日后升级的准备工作新编译器上线后别急着删掉旧的构建目录和旧版本。保留一份能用的老编译器是运维的保命符。万一新编译器暴露了某个兼容问题能立刻用alternatives切回4.8.5把业务先救活再慢慢排查。我习惯在/usr/local下保留多个版本目录比如gcc-8.5.0、gcc-9.3.0用软链接/usr/local/gcc-current指向当前生效版本。这样每次升级只是改一个软链接回滚也是改一个软链接干净利落。日后要上更高版本比如9.x、10.x流程完全一致只是把源码版本号和--prefix换一下而已前面这套方法论可以一直复用。注意每次新版本上线后务必把编译测试用例用到新标准的那段C代码在目标机器上真正编一遍、跑一遍确认可用再切生产别只凭gcc --version就下结论。我个人在多次内网交付里最大的体会是编译GCC本身的技术难点其实不算高真正决定成败的是前期环境盘点和后期回退预案这两头。把依赖摸清、把版本隔离做干净、把回退路径留好剩下的无非是花时间等编译跑完。反过来如果一上来就覆盖系统编译器、依赖又没备齐那后面大概率会在一个个报错里反复消耗最后连原来能跑的环境都搞坏了。编译这种底座级工具稳字当头永远是第一位的。