简介:GCC 3.3.6 源码压缩包,面向从事嵌入式开发、操作系统移植与交叉编译工具链构建的工程师及高年级学习者,用于在宿主机上定制生成面向目标架构的编译器套件。包内共约2000个文件,压缩后约30.03MB,以C语言源码(约7800个.c)为主体,辅以C++、Java、Fortran、Ada等语言前端的实现与头文件,并包含configure配置脚本、Makefile.in模板、.md机器描述文件、.texi技术文档及成体系的测试用例,完整覆盖预处理器、编译器、汇编器与链接器的构建素材。解压后得到gcc-3.3.6目录,其中按语言前端、目标平台配置、文档与测试分块组织,便于按目标架构裁剪语言组件与运行时库,理解GCC内部结构与交叉编译配置流程。已有221人学习下载,适合需要复现经典工具链、查阅一手源码的读者参考。
1. 为什么还要摸 gcc-3.3.6:新编译器编不过老代码这件事
接手一份 2004 年前后的遗留代码,源码里全是 K&R 风格函数定义、隐式 int、register变量、<iostream.h>这种老式 C++ 头文件,用现在的 GCC 13/14 一编,几十条 error 糊脸,CFLAGS 里加-fpermissive也压不住。这时候 gcc-3.3.6.tar.gz 就是一台时间机器。它是 GCC 3.3 系列的最后一个维护版本,默认 C 标准是 gnu89,C++ 默认 C++98,libstdc++ 的 soname 还停在libstdc++.so.5,和现代系统完全是两套脾气。它解决的问题很窄:让那些写于二十年前、只认老编译器的 C/C++ 代码重新编得过。适合复现遗留项目、补老工具链、做编译器行为对比的人;日常新项目别碰它,它是考古工具,不是生产力主力。
2. 拆包与构建环境:configure 之前先把地基垫平
2.1 包里有什么,真正要装的依赖是哪几个
解包之后先别急着configure,花两分钟看一眼目录结构。gcc-3.3.6 顶层有configure、gcc/(C 前端)、cp/(C++ 前端)、libstdc++-v3/、boehm-gc/、libjava/、zlib/这些目录。release tar 包的一个好处是,bison/flex 生成的 parser 文件已经打在包里了,只要不改.y文件,就不需要额外装 bison 和 flex。这一点和从版本库直接 checkout 源码完全不同,也是很多人误以为要装一堆构建工具的原因。
真正需要的依赖只有宿主 C 编译器、make、binutils(as/ld)和 glibc 开发头文件。GMP 和 MPFR 不是必需项——这两个库变成 GCC 的硬依赖是 4.3 之后的事,3.3.6 时代的源码还没这么讲究。texinfo 属于“有更好、没有也能绕”的类别,后面避坑章会专门讲怎么跳过它。
# 解包并确认顶层结构 tar -xzf gcc-3.3.6.tar.gz cd gcc-3.3.6 ls -1 | head -30 # 确认 release 包自带生成的 parser,不需要 bison/flex ls -l gcc/c-parse.c gcc/c-parse.y 2>/dev/null上面第二条命令是在验证“包是不是完整的 release 包”。如果c-parse.c存在,说明 parser 已生成,可以跳过 bison;如果只有.y没有.c,那说明拿到的是开发树,得先把 bison/flex 补齐。这个判断能省掉不少无头苍蝇式的依赖安装。
| 依赖项 | 是否必需 | 说明 |
|---|---|---|
| 宿主 gcc/g++ | 必需 | 用来编 stage1,版本越老越省事 |
| make | 必需 | GNU make,3.80 以上即可 |
| binutils(as/ld) | 必需 | 链接阶段要用,太新也可能出问题 |
| glibc-devel | 必需 | 提供系统头文件和 crt 对象 |
| texinfo | 可选 | 只影响 info 文档,可MAKEINFO=true跳过 |
| bison/flex | 通常不需要 | release 包已生成 parser |
| GMP/MPFR | 不需要 | 4.3 之后才成为硬依赖 |
2.2 宿主 GCC 别选太新的,这是成功率的第一变量
3.3.6 的源码是 2004 年的 C 代码,用 gnu89 写成,隐式函数声明、隐式 int、旧式参数列表到处都是。现代 GCC 从 14 开始把implicit-function-declaration从 warning 提升为默认 error,CFLAGS 里塞-std=gnu89也救不回来,因为新 frontend 的严格性不只看标准开关。我自己的经验分布是:宿主用 GCC 4.8 到 5.4,成功率最高;GCC 7 到 9 要加不少 workaround;GCC 12 以上基本要打补丁才走得通。
如果手头机器只有新 GCC,优先考虑在容器里跑一个老发行版镜像,而不是在宿主机上硬扛。这不是偷懒,是把“编译器版本”这个变量先固定住,后面排查才有基准。选定宿主之后,用CC和CXX两个环境变量把它传给顶层 configure,不设的话默认就用系统gcc。
# 看当前宿主编译器 gcc --version | head -1 # 如果系统装了多版本,挑一个老一点的 ls /usr/bin/gcc-* 2>/dev/null export CC=/usr/bin/gcc-4.8 export CXX=/usr/bin/g++-4.8CC/CXX会被顶层 configure 继承,并一路传到 stage1 的编译命令里。这两个变量设错,后面所有阶段都会跟着错,所以动手前先用$CC --version确认一遍,别嫌麻烦。
提示:不要在宿主编译器版本上抱侥幸心理。3.3.6 编不过的第一大原因就是宿主太新,先把这个变量控制住,再谈其他参数。
2.3 configure 参数逐项拆:砍掉不需要的前端
GCC 官方推荐 out-of-tree build,也就是在源码目录外单独建一个 build 目录。这样做的好处很实际:编译失败时整个 build 目录一删就干净了,源码树不会被中间文件污染,也不会出现“改过 configure 但旧目标文件还在”的玄学问题。
参数上我一般这么配:--prefix指到独立目录,绝不覆盖系统/usr;--enable-languages=c,c++只编 C 和 C++,把 java、fortran、objc 全部砍掉。3.3.6 的 libjava 依赖老版本 boehm-gc 和一堆已经消失的头文件,在现代系统上几乎必挂,留着它只是给自己增加失败面。--disable-multilib用来避免 64 位主机上找不到 32 位 crt 对象;--disable-nls省掉 gettext 依赖;--disable-checking关闭编译器内部的额外一致性检查,release 模式默认就偏松,显式写上是为了让行为确定。
mkdir -p /opt/src/gcc-build-3.3.6 cd /opt/src/gcc-build-3.3.6 ../gcc-3.3.6/configure \ --prefix=/opt/gcc-3.3.6 \ --enable-languages=c,c++ \ --disable-multilib \ --disable-nls \ --disable-checking \ 2>&1 | tee configure.logtee configure.log不是装饰,configure 的输出里有大量探测结果,后面编译失败时经常要回头查“某个头文件当时到底检测到没有”。把日志留下来,等于给自己留了一颗后悔药。
| 参数 | 作用 | 不写会怎样 |
|---|---|---|
--prefix | 安装到独立目录 | 默认装进/usr/local,覆盖系统工具链 |
--enable-languages | 限定前端 | 默认全开,java/fortran 大概率编译失败 |
--disable-multilib | 不生成 32 位库 | 64 位主机上链接报cannot find crti.o |
--disable-nls | 关闭国际化 | 多一层 gettext 依赖和潜在报错 |
--disable-checking | 关闭内部检查 | 编译更慢,行为随版本浮动 |
3. bootstrap 三阶段编译:make 在跑什么、卡在哪一步
3.1 stage1、stage2、stage3 的分工
GCC 的默认构建方式是自举(bootstrap),分三个阶段。stage1 用宿主编译器编译 GCC 源码,产出一个叫stage1/xgcc的编译器;stage2 用这个xgcc再编译一遍同一份源码,产出stage2/xgcc;stage3 再用 stage2 的产物编第三遍,最后比较 stage2 和 stage3 生成的目标文件是否一致。一致,说明新编译器能正确编译自己,自举通过;不一致,就是Bootstrap comparison failure。
这个过程很慢,但它验证的是编译器本身的正确性。调试阶段可以用--disable-bootstrap先跑通单阶段,拿到一个能用的二进制;正式交付或要求可复现时,还是建议保留完整自举。时间账大概是:单核三阶段 30 到 60 分钟,视机器而定;make -j4能压到 5 到 15 分钟。
cd /opt/src/gcc-build-3.3.6 # 记录起始时间,方便判断是慢还是卡死 date make -j4 2>&1 | tee build.log-j4是稳妥值,不是随便定的。老代码在并行编译时内存峰值比想象中高,-j32很容易把 cc1 进程送进 OOM killer。后面避坑章会专门讲这个现象。
3.2 日志里该盯哪几行
编译期间另开一个终端tail -f build.log,盯几个关键标记就够了:Configuring in ...表示进入某个子目录;stage1/xgcc、stage2/xgcc是阶段切换的信号;Comparing stages 2 and 3表示进入最后比对;Bootstrap comparison failure!则是自举失败的明确信号。大部分时间日志刷得很快,真正需要停下来看的是出现make[3]: ***的那几行。
中断之后不要从头翻日志,用 grep 定位最后一个错误块,再看它前面八十行,基本就能判断是头文件缺失、语法不兼容还是链接失败。
# 定位最后一个 make 错误 grep -n "^make.*\*\*\*" build.log | tail -5 # 看错误前后的上下文 tail -80 build.loggrep只负责找位置,tail -80负责给上下文。两个命令配合,比在几千行日志里肉眼扫描高效得多。如果错误出现在libstdc++-v3目录,大概率是 glibc 头文件不兼容;如果出现在gcc/目录且报隐式声明,那就是宿主编译器太新。
3.3 中断、续跑与彻底重来
make本身支持续跑,只要 configure 参数和宿主环境没变,重新敲make会从断点继续。但有两种情况必须删掉 build 目录重来:一是改过 configure 参数,二是换过CC/CXX。原因很简单,stage1 和 stage2 用的编译器不一致时,自举比对必然失败,而且报错信息往往指向一个和根因无关的文件,特别迷惑。
判断要不要重来,看 build 目录里有没有完整的stage1产物。如果只是某个子目录挂了、stage1 还在,可以进子目录单独make试试;但 3.3.6 的子目录依赖链比较绕,我一般直接删了重来,省下的排查时间比重新编译的时间更值钱。
# 彻底重来:删 build 目录,保留源码和 configure.log 备查 cd /opt/src rm -rf gcc-build-3.3.6 mkdir gcc-build-3.3.6 && cd gcc-build-3.3.6 ../gcc-3.3.6/configure \ --prefix=/opt/gcc-3.3.6 \ --enable-languages=c,c++ \ --disable-multilib \ --disable-nls \ --disable-checking \ 2>&1 | tee configure.log重来之前把旧的configure.log改名存一下,万一新一次配置行为和上次不一样,还能对比。这个习惯在排查“为什么同一份源码两次编译结果不同”时特别有用。
4. 安装、路径隔离与多版本共存:别让 .so.5 污染系统
4.1 make install 之后的目录长什么样
make install跑完,先别急着写代码,花几分钟确认目录布局。3.3.6 的库目录是lib/gcc-lib/<target>/3.3.6/,不是新版本那种lib/gcc/。这个差异在写 rpath、做打包、配 CMake toolchain 文件时都会咬人。C++ 头文件在include/c++/3.3.6/下,和现代 GCC 的include/c++/13/是同一套思路,只是路径前缀不同。
make install # 确认二进制和库的落点 ls /opt/gcc-3.3.6/bin find /opt/gcc-3.3.6/lib -maxdepth 3 -name "libstdc++*" ls /opt/gcc-3.3.6/include/c++/find那一条是为了确认libstdc++.so.5到底在哪一层。不同 target triplet 下路径会略有差异,提前记下来,后面配运行时路径就不用猜。
| 路径 | 内容 |
|---|---|
/opt/gcc-3.3.6/bin/gcc | C 编译器驱动 |
/opt/gcc-3.3.6/bin/g++ | C++ 编译器驱动 |
/opt/gcc-3.3.6/lib/gcc-lib/<target>/3.3.6/ | cc1、cc1plus、libgcc |
/opt/gcc-3.3.6/lib/libstdc++.so.5 | C++ 运行库 |
/opt/gcc-3.3.6/include/c++/3.3.6/ | C++ 标准库头文件 |
4.2 运行时库:libstdc++.so.5 和系统 .so.6 的隔离
GCC 3.3 的 libstdc++ soname 是libstdc++.so.5,现代系统是.so.6。两者 soname 不同,不会互相覆盖,这是好事;但用 3.3.6 编出来的 C++ 程序运行时必须能找到.so.5,否则启动就报error while loading shared libraries。常见做法有两种:在调用脚本里显式设LD_LIBRARY_PATH,或者链接时加-Wl,-rpath,/opt/gcc-3.3.6/lib把路径烧进二进制。
# 用 3.3.6 编一个 C++ 程序并检查依赖 /opt/gcc-3.3.6/bin/g++ -o demo demo.cpp ldd demo | grep libstdc++ # 如果运行时找不到 .so.5,临时补路径 LD_LIBRARY_PATH=/opt/gcc-3.3.6/lib ./demoldd那一步是在确认二进制到底链了哪个 soname。如果显示libstdc++.so.6,说明g++实际调到的还是系统编译器,得回头检查 PATH 顺序。
注意:不要把
/opt/gcc-3.3.6/lib加进全局LD_LIBRARY_PATH或/etc/ld.so.conf。老库和新库的符号版本混在一起,系统里其他 C++ 程序可能出现难以定位的加载问题。按需在脚本里设,是最稳的做法。
4.3 三种共存方式怎么选
和系统 GCC 共存,常见做法有三类。第一类是独立 prefix 加 PATH 切换,最简单,用时source一个脚本,退出 shell 就恢复;缺点是容易忘,换个终端窗口就找不到编译器。第二类是 configure 时加--program-suffix=-3.3.6,生成gcc-3.3.6、g++-3.3.6这样带后缀的名字,可以和系统 gcc 长期并存;这个参数在 3.3.6 上能用,但装完要逐个确认bin/下的名字,别假设所有子目录行为一致。第三类是容器或虚拟机隔离,最干净,适合团队复现,代价是重。
我本地习惯用第一类,配一个env-3.3.6.sh,需要时 source 一下;团队交付用第三类,把整个工具链封进镜像,谁复现都是同一套环境。
# env-3.3.6.sh export GCC336_HOME=/opt/gcc-3.3.6 export PATH=$GCC336_HOME/bin:$PATH export LD_LIBRARY_PATH=$GCC336_HOME/lib:$LD_LIBRARY_PATH export CC=$GCC336_HOME/bin/gcc export CXX=$GCC336_HOME/bin/g++这个脚本把 PATH、运行时库路径和 CC/CXX 一次性固定住。source env-3.3.6.sh之后,gcc --version应该显示 3.3.6;新开一个 shell 就自动回到系统默认,不会把环境搞混。
| 方式 | 隔离程度 | 适合场景 | 主要代价 |
|---|---|---|---|
| 独立 prefix + PATH | 中 | 个人本地复现 | 换终端需重新 source |
--program-suffix | 中 | 长期共存、脚本调用 | 装完要核对二进制名 |
| 容器/虚拟机 | 高 | 团队交付、CI 复现 | 镜像体积和启动开销 |
5. 五类高频翻车现场与排查:从隐式声明到 OOM
5.1 隐式函数声明直接报 error
现象:编译刚开始不久就出现error: implicit declaration of function 'gets',或者error: unknown type name 'uint8',一屏接一屏。原因:现代 GCC 默认标准变严,GCC 14 起隐式声明默认就是 error,-fpermissive在 C 模式下不是万能药,很多诊断压不下去。解决:首选换老宿主(4.8/5.4);次选在 CFLAGS 里放宽诊断,但只能压住一部分,压不住的还得打补丁或改源码。
# 兜底方案:放宽诊断,不保证全部编过 CFLAGS="-std=gnu89 -fpermissive -Wno-error=implicit-function-declaration" \ CXXFLAGS="-std=gnu++98 -fpermissive" \ ../gcc-3.3.6/configure \ --prefix=/opt/gcc-3.3.6 \ --enable-languages=c,c++ \ --disable-multilib这段配置只是把“硬 error”降级成“warning”,让编译能继续往下走。它救不了类型定义缺失这类结构性问题,所以看到仍然报错时,不要在这条路上死磕,换宿主才是正解。
5.2 makeinfo 报错或干脆缺失
现象:编译到文档目录时报makeinfo: command not found,或者装了新版 texinfo 之后报unknown command,指向.texi文件里的某一行。原因:要么没装 texinfo,要么装的是 7.x,语法和 2004 年的.texi不兼容。解决:直接MAKEINFO=true跳过文档生成。info 文档不是运行必需品,编译器和库能装出来就行。
# 跳过 info 文档生成,只编二进制和库 make MAKEINFO=true -j4 make MAKEINFO=true installMAKEINFO=true相当于把 makeinfo 替换成/bin/true,所有文档目标都“成功”但不产出文件。这个技巧对老源码包普遍适用,不止 3.3.6。
5.3 libstdc++-v3 和新版 glibc 头文件打架
现象:C 前端编完了,卡在libstdc++-v3,报'isnan' was not declared in this scope、'strtold' is not a member of 'std',或者struct sigaltstack重定义。原因:2004 年的 libstdc++ 源码假设老 glibc 用宏提供isnan/isinf,现代 glibc 改成了函数;sigaltstack的结构体定义也变了。解决:如果项目只用 C,直接--enable-languages=c砍掉 C++ 前端,绕开整块;如果必须用 C++,就在容器里换老发行版,或者找社区补丁。
5.4 64 位主机上的 crti.o 与 multilib
现象:链接阶段报cannot find crti.o或cannot find -lc。原因:默认 target 是 x86_64,链接时需要 32 位运行库,而系统没装 multilib 开发包。解决:加--disable-multilib不生成 32 位库;确实需要 32 位产物的,装 multilib 开发包,或者用--target=i686-linux-gnu走交叉编译路线。
5.5 并行编译把内存打爆
现象:make -j32跑一阵子进程被 kill,dmesg里能看到 oom-killer 记录,或者直接报cc1: out of memory。原因:stage2/3 的 cc1 在优化大文件时内存峰值很高,老代码没做内存友好处理,并行度一高就撞墙。解决:降到-j4或-j$(nproc/2),加 swap,单核慢慢跑。用编译时间换稳定,这笔账在复现场景里通常划算。
这五类问题基本覆盖了 3.3.6 在现代系统上的主要失败面。排查顺序建议按“宿主版本 → 日志定位 → 前端裁剪 → 并行度”走,从外到内,别一上来就改源码。
6. 验证与进阶:拿一段 K&R 老代码把 3.3.6 钉死
装完不验证,等于没装。最直接的验证方式不是看gcc -v,而是拿一段典型的老代码真编一遍。下面这段是 2004 年前后常见的 K&R 风格:参数类型写在参数表后面,现代 C 标准早就把它归到历史垃圾堆里,但 3.3.6 默认的 gnu89 完全接受。
/* legacy.c —— 典型的 K&R 风格 C 代码 */ #include <stdio.h> /* 参数类型在参数表之后声明 */ int add(a, b) int a; int b; { return a + b; } int main() { printf("%d\n", add(1, 2)); return 0; }用两套编译器分别编一次,对比行为差异:
# 现代 GCC 在严格标准下会告警甚至报错 gcc -std=c17 -Werror legacy.c -o legacy_new 2>&1 | head -5 # 用 3.3.6 直接编 /opt/gcc-3.3.6/bin/gcc -o legacy_336 legacy.c ./legacy_336 # 确认版本 /opt/gcc-3.3.6/bin/gcc -v 2>&1 | grep "gcc version"legacy_336能正常输出3,说明编译器驱动、cc1、汇编器和链接器整条链路都通了。如果这一步失败,问题一定在安装或运行时库路径上,而不是源码。
进阶用法是把编译环境固化成 Makefile 片段或 wrapper 脚本,团队里谁复现都用同一套变量,避免“我这儿能编你那儿不能编”。更细一点,可以做一张默认行为对照表,写清两套编译器的差异,贴进项目 README,后来人一看就知道哪些语法不能碰。
| 行为项 | GCC 3.3.6 | 现代 GCC |
|---|---|---|
| 默认 C 标准 | gnu89 | gnu17 / gnu23 |
| 默认 C++ 标准 | C++98 | C++17 及以上 |
本文还有配套的精品资源,点击获取