ARTICLE DETAIL

资讯详情

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

GCC 9.3.0源码编译实战:从解压到安装避坑指南

GCC 9.3.0源码编译实战:从解压到安装避坑指南 简介GCC 9.3.0 源码包面向需要指定编译器版本进行环境构建、源码阅读或二次开发的工程师以及在 RHEL/CentOS 7 等老旧系统中替换或扩展系统自带 GCC 的场景可直接离线获取免去在线检索与下载的不确定性。压缩包约 118.39MB共 2000 个文件以 1520 个 C 源文件和 344 个头文件为主体另有 37 个 C 文件、49 份 PDF 文档、28 个说明文本及 10 个 Shell 配置脚本。源码树完整覆盖预处理、编译、汇编、链接各阶段并包含 decNumber、regex、dlmalloc 等基础模块方便定位具体实现。9.3.0 属于 GCC 9 系列的稳定修订版修复了大量缺陷编译代码性能稳定适合作为旧版系统的替代编译器。当前已有 350 人学习/下载中高级开发者可借此进行交叉编译试验、定制优化选项或深入理解编译器内部机制。1. gcc-9.3.0.tar.gz 是给谁用的一个版本号背后的编译链故事刚拿到gcc-9.3.0.tar.gz这个文件你先别急着解压。文件名里已经把关键信息写完了这是 GCCGNU Compiler Collection9.3.0 版本的源码包主版本号 9 对应一次大的功能迭代次版本号 3 是特性增强修订号 0 表示这一条小版本线里的首个发布。.tar.gz是 tar 归档加 gzip 压缩Linux 和 macOS 下最通用的源码分发格式。这个包解决的是这样一个具体问题当你的系统仓库里只有 GCC 4.8 或者 8.3而项目编译要求至少 9.3 的时候源码编译是避开系统包管理器版本锁定最直接的路。它适合两类人一类是需要在 Ubuntu、CentOS、麒麟这类系统上自建编译工具链的工程师另一类是好奇 GCC 源码树里 libdecnumber、libiberty 这些运行时库到底怎么参与编译的开发者。这篇笔记按我实际拆包的顺序来写解压、读目录、配置编译、验证结果最后是四条翻车记录。2. 解压 tar.gz 与源码树布局先认清包里那十个文件再动手2.1 三步解压与归档完整性检查解压 tar.gz 之前先做两件事看 LIST 和查大小。不要直接tar -xzf一把梭万一压缩包传输出问题解到一半报错再排查会花费双倍时间。我习惯先预览包内容再解压。# 预览归档内容不真正解压 tar -tzf gcc-9.3.0.tar.gz | head -20 # 查看压缩包体积判断下载是否完整 du -h gcc-9.3.0.tar.gz # 正式解压到当前目录 tar -xzf gcc-9.3.0.tar.gz # 解压后进入源码目录 cd gcc-9.3.0这里-t是 list 模式只列出归档里的文件不落盘-z表示通过 gzip 解压-f指定归档文件名。head -20只显示前 20 行确认顶层目录结构是gcc-9.3.0/而不是一堆散文件堆在当前目录。解压完第一件事是检查顶层目录是否存在——如果直接在当前目录散开后面对configure的路径处理会非常别扭。还有一种常见做法是解压到/usr/local/src或者~/tools我倾向于放在/usr/local/src因为后面编译安装需要 root 权限源码目录放在系统级路径下不会出现普通用户目录权限不一致的问题。解压这一步不会覆盖系统里已有的 gcc它只是把源码摊开真正的写系统操作发生在后面的make install。2.2 认识包内的十个关键源文件解压之后你在顶层目录会看到gcc/、libgcc/、libstdc-v3/、libdecnumber/等子目录。项目正文里提到的bid_binarydecimal.c、decNumber.c、regex.c、dlmalloc.c、cp-demangle.c、bid128_fma.c、decBasic.c、bid128.c、bid128_compare.c、bid128_to_int32.c这些文件并不在顶层它们分散在 GCC 源码树的运行时库子目录里。先把这些文件对号入座你才能判断这个包是不是完整的也才能在后面编译遇到undefined reference时快速定位该去哪里找实现。源文件名在源码树中的位置作用说明decNumber.c/decBasic.clibdecnumber/十进制浮点数DFP核心运算实现对应 C 语言的_Decimal32/64/128类型bid128.c/bid128_compare.clibdecnumber/bid/Binary Integer Decimal 编码的 128 位十进制浮点运算与比较逻辑bid128_fma.clibdecnumber/bid/128 位十进制浮点的 fused multiply-add 融合乘加实现bid128_to_int32.clibdecnumber/bid/将 128 位十进制浮点转换为 32 位整数的库函数bid_binarydecimal.clibdecnumber/bid/二进制与十进制浮点互转的辅助表与转换例程cp-demangle.clibiberty/C 符号名 demangle 器cfilt工具和 GDB 都在用dlmalloc.clibiberty/或独立目录Doug Lea 的 malloc 实现GCC 在特定平台下用作运行时内存分配器替代regex.clibiberty/POSIX 风格正则表达式的运行时实现供 GCC 内部工具链使用看到这个清单你应该能明白GCC 不是一个单纯的编译器它是一个“编译器前端 中间表示 后端代码生成 运行时库”的复合体。libdecnumber里的那堆bid_*.c文件是为十进制浮点运算提供软件模拟的——当硬件不支持 DFP 指令时编译器生成的代码会调用这些库函数。cp-demangle.c则服务于符号反解你在 GDB 里看到Symbol别名全靠它工作。熟悉这些文件的职责后后面编译要是报某个符号找不到你就能判断是哪个子库没编进去而不是在整棵源码树里瞎 grep。2.3 离线环境下的搬运姿势很多实际部署场景里目标机器是内网节点不能直接访问外网下载源码。这时候你在跳板机上下好gcc-9.3.0.tar.gz需要把它推进内网常见做法是把 tar.gz 包传到目标机器后原地解压编译不要在本地先解压再传源码目录——散文件传起来非常慢而且容易丢文件。为保证搬运过程的完整性我会先对压缩包算一个校验值传输后再比对。# 在跳板机上计算 MD5 md5sum gcc-9.3.0.tar.gz # 在内网目标机上比对 echo 上一步得到的MD5值 gcc-9.3.0.tar.gz | md5sum -c - # 确认无误后解压 tar -xzf gcc-9.3.0.tar.gz -C /usr/local/srcmd5sum -c -从标准输入读取校验清单如果输出OK说明文件一致。-C /usr/local/src指定解压目标目录比先解压再mv省一步。这里要说明一个细节tar.gz 包在传输过程中经常出现截断但体积差异不大时du看不出来必须靠校验值兜底。我见过不少运维直接把包传过去就解压结果 configure 阶段报cannot find GMP其实是头文件缺失导致的连带问题——根源还在 tar 包没传完整。3. 从 configure 到 make installGCC 9.3.0 编译落地的标准流程3.1 configure 参数怎么给不要让系统编译器背锅GCC 源码编译的第一步是执行configure它负责探测目标系统的编译器、库和头文件并生成Makefile。这里最容易犯的错误是直接裸跑./configure然后用默认参数结果装到/usr/local/bin下面和系统自带的 gcc 混在一起后面排查版本问题非常痛苦。我一般这样配置# 在源码树外建一个独立的编译目录保持源码目录干净 mkdir -p /usr/local/src/gcc-build-9.3.0 cd /usr/local/src/gcc-build-9.3.0 # 调用源码目录下的 configure指定安装前缀和编译语言 ../gcc-9.3.0/configure \ --prefix/usr/local/gcc-9.3.0 \ --enable-languagesc,c \ --disable-multilib \ --disable-bootstrap这里的关键参数逐个解释。--prefix指定安装根目录后续所有二进制、头文件、库文件都会装到这个目录下卸载时直接删除这个目录即可不污染系统路径。--enable-languagesc,c控制编译哪些语言前端GCC 支持 Fortran、Ada、Go 等但多数业务场景只需要 C 和 C少编几个前端能显著缩短编译时间。--disable-multilib关闭 32/64 位多架构库生成除非你的项目明确需要 32 位版本编译否则这个选项能省掉大量无用的库构建。--disable-bootstrap这个选项值得展开讲默认 GCC 用“三层 bootstrap”方式编译先用系统编译器把 GCC 编一遍再用这个新编的 GCC 重新编译自己最后再用第二遍的结果再编译一遍确保编译器没有“吃自己的狗粮”时引入问题。这个过程耗时翻倍但对编译器质量有洁癖的人会保留。我的实际习惯是保留 bootstrap因为它在升级编译器后能发现很多隐藏的代码生成问题如果你只想快速得到一个能用的 9.3.0那就--disable-bootstrap。3.2 依赖三件套GMP、MPFR、MPC 缺一不可GCC 9.3.0 的 configure 阶段会检查三个数学库GMP多精度算术、MPFR多精度浮点、MPC复数多精度运算。这三个库是 GCC 实现中间表示和常量折叠的基础。在 Ubuntu 上直接装开发包是最省事的# Ubuntu/Debian 系 apt-get install -y libgmp-dev libmpfr-dev libmpc-dev # CentOS/RHEL 系 yum install -y gmp-devel mpfr-devel libmpc-devel如果系统仓库里没有这些包或者版本太旧常见做法是手动编译这三个库源码然后在 configure 时用--with-gmp/usr/local/gmp --with-mpfr/usr/local/mpfr --with-mpc/usr/local/mpc指定路径。需要注意的是configure 探测的是头文件和库文件的路径如果只指定了源码路径而不指定安装路径它依然找不到。装完依赖后先跑一遍configure看到checking for correct version of gmp... yes这类输出再继续避免后面 make 到一半才发现基础库缺失。这一步是 Ubuntu 安装 GCC 失败最常见的坑——报错信息里写着cannot find GMP但系统里其实已经装了 libgmp-dev原因是头文件路径在/usr/include/x86_64-linux-gnu/configure 默认没搜那个位置需要显式加CFLAGS-I/usr/include/x86_64-linux-gnu或者用上面那组--with参数。3.3 make 与 make install并行度、内存和安装后的路径处理configure 顺利通过后进入最耗时的编译阶段。GCC 9.3.0 全量编译在普通虚拟机上通常需要 40 到 90 分钟取决于 CPU 核数和内存。这里有个血泪参数经验make -j的并发数不是越大越好而是看内存。# 查询 CPU 核数 nproc # 以物理核数的一半启动编译保险 make -j$(nproc --ignore2) 21 | tee build.logtee build.log会把编译日志同时输出到终端和文件这个日志文件的价值在排查错误时非常大——GCC 编译报错信息动辄几百行窗口缓冲根本看不过来。我个人的做法是直接把日志落到文件编译挂掉后grep -i error build.log | tail -30定位首个错误。内存方面单核编译 GCC 大概需要 1.5GB 内存四核并行至少 6GB如果机器只有 4GB 内存make -j8很可能会触发 OOM killer把 cc1plus 进程无情杀掉日志尾巴上出现一行Killed。遇上翻车先用free -h看内存再决定降并发还是加 swap。make install 完成后别急着写ln -s先确认安装路径里的文件完整# 写入动态库搜索路径避免运行时找不到 libstdc.so.6 echo /usr/local/gcc-9.3.0/lib64 /etc/ld.so.conf.d/gcc-9.3.0.conf ldconfig # 查看安装结果 /usr/local/gcc-9.3.0/bin/gcc --version这里补充一个关于LD_LIBRARY_PATH与ldconfig的选择。我建议用ldconfig方式在/etc/ld.so.conf.d/下新增一个独立配置文件比设置环境变量更可控——环境变量只对当前 shell 生效换一个终端就丢了。如果你在编译其他项目时发现链接阶段报libstdc.so.6: version GLIBCXX_3.4.xx not found多半是动态库搜索路径没配好优先检查这个目录是否存在且被ldconfig收录。4. 碰撞一条真实编译链预处理、汇编、链接与 GIMPLE 中间表示4.1 四阶段命令行开关用 -E/-S/-c 把编译拆开GCC 把源码到可执行文件的转换拆成预处理、编译、汇编、链接四个阶段。理解这四个阶段不是为了背概念而是为了排查错误——你能通过命令行开关把编译停在任何一阶段观察中间产物。拿一个最简单的 C 文件演示/* hello.c */ #define VALUE 42 #include stdio.h int main(void) { printf(value%d\n, VALUE); return 0; }用不同的-开关分别停在各阶段# 只做预处理展开宏、包含头文件 gcc -E hello.c -o hello.i # 只做编译C 源码转汇编 gcc -S hello.i -o hello.s # 只做汇编汇编转机器码生成目标文件 gcc -c hello.s -o hello.o # 做链接目标文件转可执行文件 gcc hello.o -o hello-E的输出你直接看hello.i即可文件末尾就是 main 函数的展开版本VALUE已经被替换成42。-S生成的hello.s是汇编代码x86-64 指令在这里能看到movl $42, %esi。-c只生成目标文件不链接这在分文件编译大型项目时很常用。链接阶段才是undefined reference的重灾区——编译阶段只检查语法和类型符号是否真正存在是在链接时才确定的。比如你的代码用了printf但链接时没指定 libc就会报undefined reference to printf尽管编译阶段一切正常。4.2 GIMPLE 中间表示为什么这个包要带一堆运行时库在 GCC 内部前端和后端之间隔着一层叫 GIMPLE 的中间表示。C 前端把hello.c解析成语法树然后降级为 GIMPLE后端再把它转换为 RTL寄存器传输语言最终匹配到目标平台的指令。这个架构解释了为什么 GCC 9.3.0 源码包里会混着libdecnumber、libiberty这些不直接参与“编译”的库——GCC 编译 C 程序时语法分析部分会用到字符串处理、正则匹配等基础设施这些功能的实现就放在libiberty/regex.c里当你的程序用了_Decimal128类型时编译器生成对bid128_add等函数的调用而实现就在libdecnumber/bid/bid128_fma.c这些文件里。编译器在生成目标文件后链接器会从这些运行时库中回收符号。GIMPLE 本身你不太会直接碰但理解它的存在能帮你避开一个经典误区不要试图从 tar 包里单独抽取某个.c文件去编译你的程序。比如你直接把libdecnumber/bid/bid128.c拿出来gcc -c bid128.c大概率能编译出目标文件可一旦链接其他程序时引用它依赖关系没理顺就会出现一连串 undefined reference。因为这些文件是最底层的库实现它们之间互相调用必须放在完整库中一起构建。这也是为什么我说不要跳过第 2 章的文件清单——知道哪些文件属于哪个 subdir你就知道编译错误该去哪里找回缺失的符号。4.3 用包内源文件做一个最小符号实验为了验证上面这套机制可以在你刚才解压的源码树里做一个不污染原系统的小实验把libiberty/cp-demangle.c当作外部源文件写一个入口程序调用 demangle 函数体验一把“库代码参与链接”的过程。/* demo-demangle.c */ #include stdio.h #include stdlib.h /* 声明 libiberty 中的 demangle 入口 */ extern char *cplus_demangle(const char *mangled, int options); int main(void) { const char *sym _ZN3Foo3barEi; char *out cplus_demangle(sym, 0x8); /* DMGL_PARAMS */ if (out) { printf(demangled: %s\n, out); free(out); } else { printf(demangle failed\n); } return 0; }编译时指定源码树路径下的头文件和实现文件# 编译 libiberty 的 demangle 实现为库 gcc -c /usr/local/src/gcc-9.3.0/libiberty/cp-demangle.c -o cp-demangle.o # 编译主程序并链接该目标文件 gcc demo-demangle.c cp-demangle.o -o demo-demangle # 运行 ./demo-demangle注意cp-demangle.c内部还依赖libiberty的其他辅助函数直接编译单个文件可能报 undefined reference。这个实验的意义不在于立即成功而在于让你看到 GCC 源码树内部的耦合关系——这正是后面排查 undefined reference 时的直觉来源。如果链接报缺xmalloc等符号说明你得把libiberty整个目录编成库再链而不是只编一个文件。我一般会直接改用make -C libiberty生成libiberty.a再带着-L和-liberty链接这样符号依赖由静态库自动解决。5. 安装与再编译避坑指南四条翻车记录及排查办法5.1 装了新版gcc --version 还是旧版本现象make install执行完/usr/local/gcc-9.3.0/bin/gcc --version显示 9.3.0但回到任意目录执行gcc --version显示的依然是系统旧版本。原因which gcc打印出的路径是/usr/bin/gcc。系统的 PATH 环境变量中/usr/bin排在/usr/local/gcc-9.3.0/bin前面shell 优先找到了旧编译器。这跟 GCC 本身没关系是操作系统命令查找顺序的问题。解决两个方案二选一。如果你希望新 GCC 成为全局默认把安装目录放到 PATH 最前面并写进/etc/profile或~/.bashrc如果你希望保持系统 GCC 不动用显式路径调用即可不要改 PATH。注意锁定的目标上/usr/bin/gcc可能被系统包管理器所管理直接用ln -sf替换系统软链接后续apt/yum升级时可能被强制恢复这不是稳定做法。# 方案一用户级生效 echo export PATH/usr/local/gcc-9.3.0/bin:$PATH ~/.bashrc source ~/.bashrc # 确认 which gcc gcc --version排查时先执行type -a gcc看所有候选路径再执行readlink -f $(which gcc)看实际指向的真实二进制。很多时候你以为自己装了新版实际调用的是alternatives机制管理的软链接指向旧版本。把这两条命令练熟版本竞态问题一分钟就能定位。5.2 configure 报错找不到 GMP/MPFR/MPC现象Ubuntu 18.04 上执行configure输出接近结尾处报configure: error: cannot find GMP但系统的/usr/include/gmp.h明明存在。原因GCC 9.3.0 的 configure 脚本默认搜索/usr/local/include、/usr/include等标准路径而 Ubuntu 多架构系统把头文件放在/usr/include/x86_64-linux-gnu/脚本没搜到。这是 Ubuntu 安装 GCC 失败最常见的一幕。解决显式传递给 configure 头文件搜索路径。../gcc-9.3.0/configure \ --prefix/usr/local/gcc-9.3.0 \ --enable-languagesc,c \ CFLAGS-I/usr/include/x86_64-linux-gnu \ CXXFLAGS-I/usr/include/x86_64-linux-gnu另一种办法是所有依赖统一源码编译并指定--with-gmp三件套适合系统包管理器无法提供新版本依赖的情况。按我自己的经验能用系统包就先装系统包三件套各自源码编译会引入额外的版本匹配问题——MPFR 依赖 GMPMPC 依赖 MPFR版本不匹配时 configure 阶段就会爆出GMP version mismatch那才是真麻烦。5.3 make 过程中被 OOM Killer 杀死现象make -j8跑了二十多分钟日志最后一行是Killed没有任何具体报错。dmesg -T | tail -20能看到Out of memory相关记录。原因并行编译 GCC 时每个 cc1plus 进程平均占用 1~1.5GB 内存-j8意味着峰值内存需求超过 8GB小内存机器直接触发操作系统内存回收机制。解决先free -h确认可用内存再把并行数降下来。make -j2或make -j$(nproc / 2)均可。如果你的机器内存确实很小但 CPU 核数很多可以临时增加 swap 空间兜底但编译完成后建议回收 swap——GCC 编译产生的内存压力是阶段性的常驻大 swap 会影响系统整体性能。多轮编译的机子还可以考虑--disable-ltoLTO链接时优化在链接阶段会再次拉起编译进程内存峰值翻倍对低配机器不友好。5.4 源码树里单拎文件编译undefined reference 满天飞现象你按网上的教程把cp-demangle.c或bid128_fma.c直接gcc -c编成目标文件接着链接到一个测试程序时报出一串undefined reference to xmalloc、undefined reference to strhash之类的错误。原因前面第 4 章讲到的底层库耦合问题。GCC 源码树里的这些文件不是独立程序它们分布在不同 subdir彼此互相依赖。单独编译一个文件只是把该文件的函数打包进目标文件它调用但未实现的所有符号都会被链接器当作待解析符号报出来。解决按完整库构建。libdecnumber用make -C libdecnumberlibiberty用make -C libiberty然后链接时使用生成的静态库。如果你只是需要 demangle 功能通用做法是链接系统自带的libiberty如果发行版提供或直接调用cfilt命令不必和 GCC 源码树较劲。这个坑也再次印证了开头那句话这个 tar.gz 包是个复合工程不是散装 C 文件集合。5.5 Windows 上装完 GCCVS Code 报“gcc 不是内部或外部命令”现象Windows 环境下把下载的 tar.gz 解压通常通过 Git Bash 或 WSL 场景在 VS Code 终端执行gcc命令提示gcc 不是内部或外部命令。原因Windows 的命令提示符和 PowerShell 不认 tar.gz 里解压出来的 Linux 原生二进制。如果解压出来的是 ELF 格式文件那它只能在 WSL 里运行或者你安装了 MinGW-w64 但安装目录没有加进系统 PATH。解决确认场景。如果要用 VS Code 在 Windows 上编译 C/C正确做法是安装 MinGW-w64把bin目录追加到Path环境变量然后重启 VS Code。如果必须用 GCC 9.3.0 的源码包就在 WSL 里编译安装VS Code 通过 Remote-WSL 插件连接终端里的gcc自动指向 WSL 环境。这两种场景的交集很少但很多初学者会把“tar.gz 是 GCC”想成“解压即用”实际二进制必须由源码在目标系统上编译生成跨系统直接使用是不可能的。6. 验证安装没白费用 -v 和 --version 揪出版本竞态装完 GCC 9.3.0最后一步是验证“当前调用的到底是不是新装的”。这套验证方法也是我每次升级折腾完后固定走的一遍流程花两分钟能避免后续在编译大型项目时被诡异的旧头文件串入坑。第一步先用which -a gcc列出所有候选路径再手动执行新装目录下的可执行文件确认版本号which -a gcc readlink -f $(which gcc) /usr/local/gcc-9.3.0/bin/gcc --version第二步用-v抓编译器的内部搜索路径。这一步能揪出坑里最大的暗雷头文件搜索路径串线。比如你调用了新 GCC但#include cstdio时头文件取自系统旧 GCC 的/usr/include/c/8新旧头文件混用会冒出各种no matching function甚至编译崩溃。echo int main(){ return 0; } | /usr/local/gcc-9.3.0/bin/gcc -x c - -v 21 | grep -E ^ /|^LIBRARY_PATH|^COLLECT_GCC重点关注输出里的COLLECT_GCC/usr/local/gcc-9.3.0/bin/gcc和后面的 include 搜索路径列表。如果第一条路径是/usr/local/gcc-9.3.0/lib/gcc/.../include且/usr/local/gcc-9.3.0/include/c/9在列表头部说明新编译器在按自己的头文件目录工作如果第一条路径是/usr/include多半环境变量还没拎干净。第三步用一份真实代码验证新特性。GCC 9.3.0 相对老版本最有感知度的改进是完整支持 C17 特性。我一般用下面这个文件做冒烟测试// smoke.cpp #include variant #include optional #include string #include iostream int main() { std::variantint, std::string v smoke; std::optionalint o 42; std::cout std::getstd::string(v) *o \n; return 0; }/usr/local/gcc-9.3.0/bin/g -stdc17 smoke.cpp -o smoke ./smoke如果编译通过并输出smoke 42说明新 GCC 已经能用自己的标准库完成完整编译链路。注意这里用的是gGCC 9.3.0 的 C 前端对应的驱动命令是g不是gcc——gcc默认按.c后缀识别 C 语言直接gcc smoke.cpp也不会错但链接 C 标准库时行为略有差异养成用g编译 C 源码的习惯更稳妥。从那以后我每次拆这类源码包强制走一遍--version验证版本号、-v验证搜索路径、冒烟代码验证标准库三件事做完才敢说“装好了”。这套习惯帮我在各种发行版上躲过了无数个版本竞态的坑希望帮到你。本文还有配套的精品资源点击获取
返回列表