ARTICLE DETAIL

资讯详情

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

gcc-10.1.0源码编译实战:从configure到替换系统默认工具链

gcc-10.1.0源码编译实战:从configure到替换系统默认工具链 简介这是GNU编译器套件GCC 10.1.0的完整源码压缩包由GNU项目维护发布面向需要深入理解编译器内部机制、进行工具链定制或参与GCC项目贡献的开发者与研究人员也适合系统学习编译原理的高年级本科生与研究生。压缩包内共含2000个文件以C语言源文件和头文件为主体包括1517个C语言源文件、347个头文件以及37个C源文件覆盖了编译器前端、中间表示、优化器和后端代码生成等核心模块这套结构对理解一次完整编译流程极为直观同时附带49个PDF文档、28个TXT说明文本和10个Shell脚本可用于查阅设计说明、辅助构建配置或作为学习导引。资源包整体大小125.1MB目前已有270人关注学习。通过这份源码读者可以完整研究GCC 10.1.0的工程组织方式跟随各语言前端到中间表示再到后端机器码的完整生成脉络深入理解各类编译优化策略与指令选择逻辑也可以在现有代码基础上修改或新增特性为深入编译技术研究或参与开源编译器开发提供扎实基础。1. 为什么 gcc-10.1.0.tar.gz 值得自己编译一遍下载到gcc-10.1.0.tar.gz这个文件的人多半不是闲逛而是已经被系统包管理器坑过Ubuntu 上用 apt 升级 gcc 失败、CentOS 7.9 默认还停在很旧版本、某个老项目又必须用 GCC 10 的编译特性……正因为这些场景源码包才是唯一稳定的路径。tar.gz表示这是 GCC 官方源码压缩包10.1.0是 GCC 10 系列的起点版本相比 GCC 9.x 对 C 标准支持更完整默认行为也更严格。它解决的问题是不依赖发行版仓库自己决定编译器版本、安装目录和链接行为。适合两类人需要固定工具链的团队以及被旧系统限制的开发者。下面按我实际编译的顺序来讲从依赖检查、configure 参数到 make 日志和安装后的替换最后把踩过的坑一次性摆出来。2. 动手前的依赖准备把 gmp/mpfr/mpc 和 GNU make 先安排明白源码编译的第一步往往不是直接./configure而是先检查宿主环境。GCC 的源码包虽然叫“源码”但它编译过程中还依赖几个外部库GMP 负责大整数运算MPFR 负责任意精度浮点MPC 负责复数运算。前端解析和优化阶段会用到它们缺少或版本太老时configure 会直接报错退出。常见的报错是configure: error: Building GCC requires GMP 4.2, MPFR 2.4.0 and MPC 0.8.0这行字一出现说明你还没进入真正的编译阶段依赖没配齐。很多人习惯用包管理器思维看待源码包以为解压后make就能自动把依赖装好这是误判。GCC 不像 LLVM 那样自带一个很大的组件仓库也不像 MSVC 那样装一个 IDE 插件就能跑它走的是 Unix 传统路线configure自己去系统里找库找不到就失败失败信息还不一定友好。所以我把依赖检查放在第一步省得后面反复重跑。2.1 从 rpm/deb 思维切到源码思维GCC 的依赖不会自己装好我在新机器上编译 GCC 前会先跑一组最基础的命令确认三件事存在能编译的 gcc/g、存在 GNU make、存在 ld。GCC 10 要求宿主编译器至少能编译 C11 代码系统自带的 gcc 4.8 以上一般都可以但越新越省心。make 必须是 GNU Make某些 BSD make 会在复杂 Makefile 里翻车。binutils 里的 ld/as 决定后续链接行为GCC 编译时会调用它们所以不能缺。# 1. 现有编译器版本宿主编译器只负责引导第一遍编译 gcc --version | head -n 1 # 2. make 必须是 GNU Make而不是 BSD make make --version | head -n 1 # 3. binutils 里的 ld/as 决定后续链接行为 ld --version | head -n 1这三条命令的输出没有明显报错再继续跑依赖安装。Debian/Ubuntu 系可以直接用包管理器补齐三个库CentOS 7.9 则需要对应开启 EPEL 或使用 release 仓库里自带的包。不同发行版的包名不完全一样分别执行下面两段里适合你的那一段。# Debian / Ubuntu sudo apt-get install libgmp-dev libmpfr-dev libmpc-dev # RHEL / CentOS sudo yum install gmp-devel mpfr-devel libmpc-devel这里有一个选择如果系统仓库里的三个库版本比较旧或者你根本不想污染系统目录我一般改用 GCC 源码包自带的download_prerequisites脚本把 GMP、MPFR、MPC 的源码直接解进 GCC 源码树。这样 GCC 在 configure 时会优先使用源码树里的版本和系统库完全隔离。两种方案都能走通区别只是你要不要留下全局共享库。2.2 用现有编译器做“引导”先理解 bootstrap 的意义GCC 源码编译默认会执行三遍构建这个过程叫 bootstrap。第一遍用系统旧 gcc 编译出临时编译器第二遍用临时编译器重新编译 GCC第三遍再用第二遍的产物编译一次最后比较二、三遍的产物是否一致。这样能验证新编译器是否有能力编译自己对编译器这种本身也是程序的东西来说是很有价值的自检。如果你的目标是赶紧拿到一个能用的 GCC 10.1.0不追求极致的自举验证configure 时加上--disable-bootstrap可以省掉一多半时间。但这里有个代价如果宿主编译器和你下载的 GCC 版本差异很大而你又发现新编译器编译某些项目时产生奇怪的行为第一怀疑对象就应该是关闭了 bootstrap。我个人的默认习惯是保留 bootstrap除非是 CI 里需要快速出一个可用编译器才把--disable-bootstrap打开。宿主机上的 gcc 还承担另一个职责决定最终编译器使用的动态库 ABI。如果系统自带的 gcc 是老版本编译出的 GCC 10.1.0 在后续链接 C 程序时可能因为系统 libstdc 版本太老而报符号找不到。这个问题不是 GCC 本体的问题而是运行环境的问题后面第 5 章会专门讲。2.3 下载校验和目录规划sha256sum、磁盘空间和 prefix下载 gcc-10.1.0.tar.gz 最忌讳的是文件下载一半或损坏后直接用。你可能会遇到下载 gcc 网速过慢的时段于是换镜像、换下载工具折腾完却忘了校验文件完整性。我拿到源码包的第一件事就是sha256sum比对官方给出的哈希值。如果和官网不一致宁可重新下载也不要用一个来历不明的源文件去编译。# 校验源码包完整性哈希值一定要和官方页面一致 sha256sum gcc-10.1.0.tar.gz # 确认 /opt 有足够空间GCC 构建的中间文件非常占磁盘 df -h /optGCC 源码包解压后加上 build 目录里的*.o文件和中间二进制整体占用几个 GB 是常态。把构建目录放在/tmp是翻车高频操作因为/tmp经常是 tmpfs内存稍小就直接写满。我一般把源码解压到$HOME/gcc-srcbuild 目录放在$HOME/gcc-build最终安装目录固定到/opt/gcc-10.1.0。这样只有最后make install时需要 root编译过程完全在普通用户下完成避免污染系统默认目录。3. 用 gcc-10.1.0.tar.gz 做一次标准构建configure/make/install 全流程环境检查完才进入正式构建。我倾向于把流程分成四步解压、准备依赖、配置、编译安装。每一步都留日志文件关键输出落到磁盘这样失败时能回查不是对着终端黑匣子猜。3.1 解压与三件套用 download_prerequisites 代替手工编译先把 tarball 解压到专门目录。GCC 源码包解压后顶层目录名是gcc-10.1.0这一点在自动化脚本里很有用写死这个目录名可以省一步环境变量判断。SRC$HOME/gcc-src mkdir -p $SRC tar -xf gcc-10.1.0.tar.gz -C $SRC ls $SRC/gcc-10.1.0/configure如果之前没有用 apt/yum 安装三个依赖库推荐直接跑源码树自带的下载脚本。它会把匹配 GCC 10.1.0 的 GMP、MPFR、MPC 源码包下载并解压到源码树内后续 configure 会优先使用它们。cd $SRC/gcc-10.1.0 ./contrib/download_prerequisites这个脚本的好处是版本匹配问题交给 GCC 官方处理不会出现系统库太老导致 configure 失败。坏处是它需要联网下载网速慢时可能超时。如果脚本中途断了重新跑一次即可它会把已下载的文件覆盖掉不影响后续步骤。我一般跑完会看一眼目录下是不是多了gmp、mpfr、mpc三个子目录再继续下一步。3.2 configure 的最小参数--prefix、--enable-languages 与 multilib现在进入正式配置。GCC 支持在源码目录里直接建 build 目录也可以完全脱离源码目录做 out-of-tree build。我坚持 in-tree 之外另开目录理由很简单出问题时rm -rf build就能彻底重来不用在源码目录里找哪个文件是编译产物。BUILD$HOME/gcc-build/gcc-10.1.0 mkdir -p $BUILD cd $BUILD $SRC/gcc-10.1.0/configure \ --prefix/opt/gcc-10.1.0 \ --enable-languagesc,c \ --disable-multilib \ --disable-bootstrap \ --enable-default-pie这几个参数里最重要的是--prefix。它决定最终安装位置我放在/opt/gcc-10.1.0想卸载时直接删这个目录不会和系统自带的/usr/bin/gcc扯上关系。如果你用默认的/usr/localGCC 的gcc、g会进/usr/local/bin虽然也能用但和发行版自带的命令混在同一个 PATH 里后面排查“升级后为啥还是旧版本”会多绕一层。--enable-languagesc,c只编译 C 和 C 前端。如果你还要 Fortran就改成c,c,fortranAda、Go 这些前端依赖更多系统库不是刚需就不加。--disable-multilib关掉 32 位编译支持。64 位系统上如果不需要-m32关了能避开缺少 32 位库的麻烦。--enable-default-pie让编译出来的可执行文件默认是位置无关的安全性更好但如果你在特别老的系统上部署这个参数可能导致旧汇编代码链接失败需要根据场景取舍。3.3 make -j 与构建日志输出到文件失败时不再黑匣子配置完成且没有任何 error 后开始编译。这里最容易犯的错是把-j$(nproc)直接拉满。nproc返回的是逻辑核数不是可用内存能撑住的编译并发度。GCC 编译过程中会有多个cc1plus进程同时跑每个进程很容易吃掉几百 MB 到 1GB 内存。我曾经在一台 8 核只有 8GB 内存的机器上开-j8编译到中段直接被内核 OOM Killer 杀掉。后来学乖了内存不大时按nproc/2 1取并发数稳得多。cd $BUILD # 构建日志输出到文件stdout/stderr 都收进去 make -j$(($(nproc) / 2 1)) $BUILD/build.log 21 # 编译结束看退出码并用 tail 抓最后一段输出错误通常都在末尾 echo $? tail -n 30 $BUILD/build.log日志输出的关键是把21写在重定向符号后面这样 stderr 也会进入同一个文件。如果你还想在终端同时看到实时输出可以用make | tee build.log但管道模式下如果 make 被中断日志里的内容可能少最后一截。我个人更推荐直接重定向到文件然后另开一个终端用tail -f build.log跟踪进度。编译完成后安装。因为安装目录是/opt/gcc-10.1.0普通用户没有写权限这里用 sudo。安装日志也单独存一份避免和编译日志混在一起排查问题时能区分是哪一步出的问题。sudo make install $BUILD/install.log 21 tail -n 20 $BUILD/install.log这一步后/opt/gcc-10.1.0/bin/gcc应该已经存在。如果你的构建目录里之前编译过一次但没 clean然后再一次 configure可能会出现 Makefile 残留导致诡异行为。遇到这种情况直接删除整个 build 目录重新建比研究为什么残留更省时间。4. 安装后的生效问题让新 gcc 成为默认又不动系统旧 gcc安装完成不等于能用。很多人在这里经历“gcc 升级后为啥还是旧版本”的困惑明明make install成功了打开终端gcc --version还是老版本。这不是安装出问题而是你没有让新路径生效。默认情况下系统找命令的顺序由 PATH 决定/usr/bin排在/opt/gcc-10.1.0/bin前面自然先命中老 gcc。4.1 先验证 /opt 下的 gccbin/include/lib 三个维度验证新编译器时我不用gcc --version一个指标就下结论。版本号对了不代表编译和链接也对了。更稳的验证是看三样东西可执行文件、头文件搜索路径、库搜索路径。三者都在/opt/gcc-10.1.0下才能确认安装完整。# 1. 可执行文件带绝对路径运行避免被 PATH 干扰 /opt/gcc-10.1.0/bin/gcc --version # 2. 编译一个最小 C 程序测试头文件路径 cat /tmp/hello.cpp EOF #include iostream int main() { std::cout __VERSION__ std::endl; return 0; } EOF # 3. 用绝对路径 g 编译并运行验证 include 和 lib 都正确 /opt/gcc-10.1.0/bin/g -stdc17 /tmp/hello.cpp -o /tmp/hello /tmp/hello这里我特意用g而不是gcc因为 C 程序链接时需要自动带libstdcg驱动会处理这些细节。如果用gcc直接编译.cpp后续链接很可能不明不白地失败。__VERSION__会被预处理器展开成10.1.0程序能输出这个版本号说明头文件和标准库都工作正常。4.2 升级后还是旧版本PATH 优先级与 update-alternatives验证完绝对路径没问题再来解决“默认命令”的问题。最简单的方式是改 PATH把新目录放到最前面。这个方法对当前终端立即生效也容易切回系统旧编译器适合只给自己用。export PATH/opt/gcc-10.1.0/bin:$PATH gcc --version在 Ubuntu 上,「gcc 不是内部或外部命令」这类报错也多半和 PATH 有关。很多人装了新 gcc但终端会话是在安装前打开的PATH 没有重新加载于是 VSCode 里的终端还指向旧的/usr/bin。关了重开终端或者重新加载~/.bashrc问题就没了。但 PATH 只对 shell 生效系统里的/usr/bin/gcc仍然指向旧版本。如果系统全局的构建脚本还会调用/usr/bin/gcc就需要用update-alternatives做系统级切换。这样既能保留旧 gcc又能让系统默认命令指向新版本将来想切回来也容易。sudo update-alternatives --install /usr/bin/gcc gcc /opt/gcc-10.1.0/bin/gcc 100 sudo update-alternatives --install /usr/bin/g g /opt/gcc-10.1.0/bin/g 100 sudo update-alternatives --config gcc sudo update-alternatives --config g注意 CentOS 上命令名可能是alternatives参数体系一样。我一般不推荐直接ln -sf覆盖/usr/bin/gcc因为系统自带的软件包升级时可能会把这个软链接冲掉或者别的工具依赖旧的 gcc 行为你换掉的瞬间就把系统默认行为也换了后期很难追责。4.3 链接期和运行期libstdc.so 的查找路径GCC 的安装目录里有bin、include、lib、lib64四块其中lib64下的libstdc.so是 C 程序的运行时依赖。你用/opt/gcc-10.1.0/bin/g编译链接时链接器会优先在当前安装目录找库所以编译期通常没问题。但程序运行起来时动态加载器不一定知道新库在哪。如果运行时报error while loading shared libraries: libstdc.so.6就需要让系统知道这个库路径。# 把库路径写进 ld.so.conf.d然后刷新缓存 sudo tee /etc/ld.so.conf.d/gcc-10.1.0.conf /dev/null EOF /opt/gcc-10.1.0/lib64 /opt/gcc-10.1.0/lib EOF sudo ldconfig ldconfig -p | grep libstdc如果你的程序要分发给别的机器而对方系统里的 libstdc 比你本机旧编译时就要考虑把库打包带过去。最简单做法是链接时写死 RPATH/opt/gcc-10.1.0/bin/g /tmp/hello.cpp -o /tmp/hello \ -Wl,-rpath,/opt/gcc-10.1.0/lib64这样运行时动态加载器会先去/opt/gcc-10.1.0/lib64找库找不到再回系统路径。编译期链接成功只是第一步运行期能加载到正确版本才算真正结束。5. gcc-10.1.0 源码编译常见坑与排查5 条现场记录源码编译 GCC 的坑很多人以为是编译器的玄学真相大多是可以复现的环境问题。下面五条是我认为最有代表性的按现象、原因、解决三段式记录。5.1 configure 报cannot find crt1.o也没有明显头绪现象configure 执行到一半报configure: error: C compiler cannot create executables后面跟着一行cannot find crt1.o。第一次见时我以为系统 libc 坏了实际并不是。原因这个报错通常由两类原因触发。一是环境变量LIBRARY_PATH或LD_LIBRARY_PATH里残留了旧编译器路径导致 configure 测试编译时链接到了不存在的目录二是在 64 位系统上默认 enable multilib而机器没有安装 32 位运行时库configure 尝试生成 32 位测试程序时缺少crt1.o。解决先清掉可能污染链接的变量再关掉不需要的 multilib。unset LIBRARY_PATH LD_LIBRARY_PATH cd $BUILD $SRC/gcc-10.1.0/configure \ --prefix/opt/gcc-10.1.0 \ --enable-languagesc,c \ --disable-multilib \ --disable-bootstrap另外检查一下系统里是否存在/usr/lib/x86_64-linux-gnu/crt1.o。如果连这个文件都没有说明 libc6-dev 没装全先装基础开发包再回来继续。configure 失败后别只盯着最后一行往上游翻几行通常能看到更准确的提示。5.2 make 中段报 internal compiler error 或被 Killed现象make -j8跑到一半日志里出现internal compiler error: Killed或者直接显示程序变成Killed再往下翻还有一堆编译错误。很多人误以为是 GCC 10.1.0 的 bug发帖问“新编译器是不是不稳定”。原因GCC 编译 GCC 时cc1plus会同时起来多个进程每个进程的峰值内存可能超过 1GB。-j参数开得越大并发进程越多内存耗尽后内核 OOM Killer 就会杀掉进程日志里留下的只有被杀进程的异常退出。解决降并发数而不是怀疑源代码坏了。可以用free -h看一下物理内存小内存机器把-j压到 2 或者 4 重新执行 make。GNU make 支持从上次失败的位置继续不用从头开始所以直接重新跑同一命令未编译完的目标会接着编译。free -h # 用 dmesg 确认是否 OOM dmesg | grep -i out of memory | tail -n 5 # 降并发继续日志另存一份便于对比 make -j2 $BUILD/build-retry.log 21担心内存不够也可以在配置时关闭 bootstrap--disable-bootstrap能从根上减少一整套编译过程。对我来说宁可多等一小时也不愿意看到Killed后还要重启。5.3 换到新 gcc 后编译 C 仍报cannot find -lstdc现象已经把/opt/gcc-10.1.0/bin加进 PATHgcc --version也显示 10.1.0编译 C 程序正常但编译 C 程序时链接报cannot find -lstdc。原因大概率你用的是gcc命令而不是g或者系统里还残留着针对旧 gcc 的LIBRARY_PATH环境变量。GCC 的gcc驱动默认按 C 语言方式处理链接不会自动带上libstdcg才会把 C 标准库路径和-lstdc一起补上。另外旧的LIBRARY_PATH会让链接器优先去老版本目录找库找不到就报错。解决C 程序一律用/opt/gcc-10.1.0/bin/g并且编译时确认环境变量干净。/opt/gcc-10.1.0/bin/g -stdc17 /tmp/hello.cpp -o /tmp/hello # 非要手动加库也可以但这不是好习惯 /opt/gcc-10.1.0/bin/gcc /tmp/hello.cpp -L/opt/gcc-10.1.0/lib64 -lstdc -o /tmp/hello头文件路径同理。如果你看到的是fatal error: iostream: No such file or directory说明 g 仍在搜索系统老版本的头文件。这时用g -v看它到底在找哪个 include 目录基本一眼就能定位是不是 PATH 顺序问题。5.4 运行期报GLIBCXX_3.4.xx not found现象编译、链接都通过了把生成的可执行文件拷到另一台机器运行程序直接报version GLIBCXX_3.4.xx not found甚至还指到/usr/lib64/libstdc.so.6。原因编译机器上的 libstdc 是 GCC 10.1.0 提供的版本符号比较新目标机器系统自带的 libstdc 是旧版缺少新符号。动态链接器按照默认路径找到了旧库自然报符号找不到。这不叫编译失败而是部署环境不匹配。解决要么把新库路径告诉目标机器要么在编译时把 libstdc 静态链进去。静态链接是便携性最强的方式/opt/gcc-10.1.0/bin/g /tmp/hello.cpp -o /tmp/hello \ -static-libstdc -static-libgcc动态库方式就用-Wl,-rpath把新库路径写进可执行文件再配合目标机器的ldconfig。这个坑最容易在 CentOS 7.9 这类老系统上踩因为你升级的是编译器不是系统基础库旧发行版不会自动跟随。5.5 GCC 10 默认-fno-common导致链接报重复定义现象项目原本在 GCC 9.3.0 下编译正常换到 gcc-10.1.0 后链接器突然报多个目标文件里出现multiple definition of xxx。很多人第一反应是新编译器把代码编译坏了项目里也没有同名变量。原因GCC 10 把未初始化的全局变量默认处理从-fcommon改成了-fno-common。老代码里如果头文件内定义了全局变量多个 cpp 文件 include 同一个头在 GCC 9 及更早版本下会被合并成同一个符号链接器不报错GCC 10 开始每个编译单元都生成独立定义链接期就变成冲突。解决先找出头文件里的全局变量定义把它改成extern声明再在一个源文件里做实际定义。这是长期方案。临时想验证是不是这个原因可以在编译命令里加-fcommon/opt/gcc-10.1.0/bin/g -stdc17 -fcommon main.cpp util.cpp -o app如果加了-fcommon就能链接通过那基本确认是旧代码对公共符号的隐性依赖。我建议顺手把代码改掉因为后面升级到 GCC 11、GCC 12 时-fcommon只会越来越边缘化。6. 把这套 gcc-10.1.0 变成团队工具链的进阶做法到了这一步你已经能用 gcc-10.1.0 编译普通程序。如果这只是个人开发机够用了但要是团队多台机器都要用同一套编译器我建议额外做两件事保存完整的构建记录以及做一个专用 wrapper 脚本。我习惯把 configure 那行命令原样写进一个文本文件和 build.log 放在同一目录。原因是半年后再看这台机器你根本记不住当时加了哪些参数尤其--disable-multilib、--disable-bootstrap、--enable-languages这种决定行为的参数错一个都可能影响后续所有人。保存命令不等于保存环境但至少能让你在新机器上按同一路径复现。GCC 10.1.0 不是长期支持版后续如果需要在同一台机器上再装 GCC 12我的做法是单独建/opt/gcc-12.1.0不覆盖现有安装。这样不同项目可以用不同的/opt/gcc-xx/bin/g互不干扰。系统级默认版本再靠 update-alternatives 统一维护团队里所有人拿到的版本一致。对编译性能有极端要求的人可以再跑一遍make profiledbootstrap。它会在编译过程中先做一次 profile再用 profile 数据引导后续编译最终编译器整体性能通常比普通 bootstrap 好一点。代价是构建时间明显变长。普通业务项目不值得多花这个时间但如果你是给别的编译器做宿主环境的值得一试。最后是我一直在用的一个习惯不直接改/usr/bin/gcc而是把新目录通过 wrapper 暴露给项目。cat ~/bin/gcc10-c EOF #!/bin/bash export PATH/opt/gcc-10.1.0/bin:$PATH export LD_LIBRARY_PATH/opt/gcc-10.1.0/lib64:${LD_LIBRARY_PATH:-} exec /opt/gcc-10.1.0/bin/g $ EOF chmod x ~/bin/gcc10-c这样项目里想用新工具链时显式调用gcc10-c不想用时继续用系统 g风险范围可控。编译工具链这东西最怕的就是黑匣子不知道版本、不知道参数、不知道库路径。把这些都固化成文件遇到问题翻日志比重新猜一遍快得多。希望帮到你。本文还有配套的精品资源点击获取
返回列表