简介:GCC 3.3.6完整源码包(gcc-3.3.6.tar.gz)面向需要构建嵌入式交叉编译工具链的开发者、系统程序员及Linux底层软件研究者。作为GNU编译器集合的经典版本,其源码涵盖C、C++、Objective-C、Fortran、Ada及Java等语言前端,并包含configure配置脚本、Makefile.in模板、文档、测试用例、库与头文件,以及cpp、gcc、g++、as、ld等核心工具,可用于特定硬件与操作系统组合下的编译器定制与工具链构建。压缩包共2000个文件,约30.03MB,文件类型以C源码为主体,辅以Java、头文件、汇编、Fortran及配置脚本等,覆盖编译、汇编、链接与目标架构适配等完整环节。已有221人学习下载,适合具备编译原理与系统编程基础、希望深入理解GCC内部机制或从事嵌入式交叉开发的读者。目录中包含大量针对不同处理器的目标架构配置模板与移植相关文档,可帮助系统研究GCC对多种硬件平台的后端支持,为在特定目标环境定制高效交叉编译器提供直接参考。
1. 还在折腾 gcc-3.3.6.tar.gz,不是怀旧,是在解决真实问题
某个老系统升级后跑不起来,日志里只有一行“undefined reference to `__gxx_personality_v0'”;另一边的嵌入式项目要复现十年前编译出的固件行为,现成的高版本编译器怎么编都不对。这时候你才会回头去找 gcc-3.3.6.tar.gz。3.3.6 是 GCC 3.3 系列的最后一个版本,它的价值不在于新特性,而在于“编译行为和二进制接口都停在了那个时代”:C++ ABI 还没换、产物体积小、对老代码的容忍度高。适合三类人:给老平台复现原始编译环境、锁住老 C++ 项目的二进制接口、搭交叉工具链时需要一个干净的编译器核心。这篇笔记不聊理论,直接从源码包讲到 configure、编译、ABI 和交叉场景,最后给你几条踩坑记录。
2. 从源码包到本机可用的编译器:configure 与编译的完整路径
2.1 编译前置检查:先确认你不是用新头文件编译老编译器
有人拿到 gcc-3.3.6.tar.gz 就直接./configure && make,然后被一堆error:xxxundeclared糊脸。原因多半是系统 glibc 头文件太新,老 GCC 的 fixincludes 阶段处理不了新版头文件的语法。动手之前先花两分钟做三件事:查当前 gcc 版本(引导编译器不能太老也不能太新)、查 binutils 版本、确认目标目录有写权限。
gcc --version ld --version ls -ld /opt 2>/dev/null || sudo mkdir -p /opt && sudo chown $(whoami) /opt这段检查的意义在于:3.3.6 不需要 GMP、MPFR、MPC 这些现代依赖,它认的是 binutils 和系统头文件。binutils 太老可能不认识新内核的某些指令,太新又可能在链接时引入老 GCC 没见过的段属性。实际经验是 binutils 2.14 到 2.24 之间都有人成功编过 3.3.6,别在这上面追求最新版。/opt检查是为了让后面make install有个独立的落点,避免污染系统默认路径。
2.2 configure 参数怎么给:让安装结果独立可用
老版本 configure 的选项比现代 GCC 少很多,但有几个必须亲手指定,否则编出来的东西要么装不进、要么会在系统路径里和现有 gcc 打架。下面是我常用的搭配:
tar xzf gcc-3.3.6.tar.gz cd gcc-3.3.6 mkdir -p build-3.3.6 && cd build-3.3.6 ../configure \ --prefix=/opt/gcc-3.3.6 \ --program-suffix=-3.3.6 \ --enable-languages=c,c++ \ --disable-nls \ --disable-checking这里--prefix指定最终安装目录,我坚持装到独立目录而不是/usr/local,因为 3.3.6 的头文件和库如果混进/usr/local,很可能被系统里的高版本工具链误用。--program-suffix=-3.3.6让安装产物变成gcc-3.3.6、g++-3.3.6,这样和系统 gcc 共存时不会互相覆盖。--enable-languages=c,c++只编最常用的两个前端,省略 gcj 和 g77,能省掉大量时间和依赖。--disable-nls去掉国际化支持,--disable-checking关闭内部检查,这两项对编译速度和后续运行性能都有明显改善。
2.3 编译阶段:普通 make 和 bootstrap 的区别
GCC 3.3.6 的常见做法是直接make,它只编译一次,速度尚可;如果你追求编译器自身的可靠性,可以执行make bootstrap,它会用刚编出来的 gcc 再编一遍自己。生产环境我一般做 bootstrap,验证环境或只是临时编个工具链时,普通 make 就够。
make -j4 # 快速路径,大约十几分钟到半小时 # 或者 make -j4 bootstrap # 自举验证,时间约 3 倍 make install-j4是并行度参数,老代码树对高并行度支持一般,超过 8 容易遇到奇怪的链接顺序问题。编译结束后在/opt/gcc-3.3.6/bin/gcc-3.3.6验证:
/opt/gcc-3.3.6/bin/gcc-3.3.6 --version /opt/gcc-3.3.6/bin/g++-3.3.6 --version验证时注意:3.3.6 的--version输出只有一行版本号,没有后续的配置信息输出,这是正常的,别当成异常。如果这里能正常打印,说明前面几步都通了。
3. C++ ABI 是新旧分水岭:链接不了、运行闪退都跟它有关
3.1 libstdc++.so.5 与 libstdc++.so.6:版本号背后的两套 ABI
3.3.6 的 C++ 运行库是libstdc++.so.5。从 GCC 3.4 开始,G++ 切换到新的 Itanium C++ ABI,运行库变成libstdc++.so.6,符号修饰规则和异常处理机制都变了。这意味着用 3.3.6 编出来的 C++ 模块,和系统里用高版本 g++ 编出来的模块,二进制层面不能安全混用。最典型的现象是:两个 .so 合在一起运行时,报undefined reference to__gxx_personality_v0'或std::string` 布局不匹配导致的内存踩踏。
如果项目里有 C++ 代码需要长期保持二进制兼容,锁死在 3.3.6 是合理的策略。对应关系可以记成下面这张表:
| 项目 | GCC 3.3.6 | GCC 3.4+ |
|---|---|---|
| libstdc++ 版本 | .so.5 | .so.6 |
| C++ ABI | 旧版 | Itanium C++ ABI |
| 异常处理模型 | 旧模型 | 新模型,使用__gxx_personality_v0 |
| 与高版本 g++ 混用 | 不建议 | 不建议跨大版本 |
项目里如果有编译好的第三方 C++ 库,注意看它依赖哪个 libstdc++。3.3.6 编出的程序依赖libstdc++.so.5,如果系统里找不到这个库,运行时会直接报cannot open shared object file。
3.2 用 nm 和 readelf 验证一个 .so 到底属于哪一代 ABI
拿到一个来路不明的 C++ 动态库,不要靠猜,用nm或readelf看一眼符号版本表就能确认。老手通常用下面这组命令:
readelf -V /opt/some/lib/libfoo.so | grep -A4 'version definition' nm -D --defined-only /opt/some/lib/libfoo.so | grep __gxx_personality ldd /opt/some/lib/libfoo.so | grep libstdc++readelf -V的输出里如果同时出现GLIBCXX_3.4和CXXABI_1.3,说明这个库是 GCC 3.4 之后编的,和老 3.3.6 环境混用大概率出事。nm若查不到__gxx_personality_v0,基本可以判断是旧 ABI 产物,和 3.3.6 同代。最后一行ldd看它链接的是libstdc++.so.5还是.so.6,这是最直接的判别标准。
另外一个容易翻车的点:LD_LIBRARY_PATH同时包含新旧两个 libstdc++ 目录时,加载器优先找到哪个就用哪个。3.3.6 编的程序被强制加载到.so.6,轻则大量敏感词无关的符号错误,重则启动即崩。排查这种事没有捷径,先LD_DEBUG=libs ./your_program 2>&1 | grep libstdc++看实际加载路径。
3.3 同一个项目里,为什么建议让所有 C++ 模块共用同一把 g++
实际项目里不存在“一个 .a 给 3.3.6 用、另一个 .so 给 4.x 用”还能长期安稳的道理。混合 ABI 的 C++ 程序在运行时表现像玄学:小对象能跑,大对象内存布局错位;短字符串正常,长字符串越界;单线程没事,多线程异常处理直接死循环。真遇到静态库和动态库不得不混用的情况,唯一的缓解办法是保持接口纯 C:所有跨模块边界都用 extern "C" 的函数指针和 POD 类型,把 C++ 封装在各自的模块内部。这个设计约束比任何补丁都可靠。
4. 把 gcc-3.3.6 编成交叉工具链:目标三元组与 sysroot 怎么配
4.1 先定目标三元组,再决定头文件和库从哪来
交叉编译老版本 GCC 的常见场景是为 mips、arm、ppc 的老内核做固件工具链。configure时的--target参数决定了整套工具链面向哪个平台,比如--target=arm-linux-gnu。这一步常见错法是只改--target不提供目标平台的头文件和库,编出来的 gcc 确实是交叉的,但一编译就因为没有目标头文件而失败。三维元组确定后,还需要目标平台的 sysroot,即目标系统的根文件系统目录,里面应当包含目标 glibc 的头文件、libc 和 libm 等基础库。
老版本 GCC 3.3.6 对 sysroot 的支持没有现代版本完整,常见做法是用--with-headers和--with-libs两点定位:
mkdir -p build-cross && cd build-cross ../configure \ --target=arm-linux-gnu \ --prefix=/opt/cross-3.3.6 \ --enable-languages=c,c++ \ --with-headers=/opt/target-arm/usr/include \ --with-libs=/opt/target-arm/usr/lib--with-headers指向目标平台的头文件目录,--with-libs指向目标平台的库目录。目录里的内容从目标设备的根文件系统复制出来即可。要特别提醒:这两处路径必须是完整的目录树,不能只放一个 libc.so,因为配置阶段会检查 crt 文件是否存在。常见做法是直接把目标板子根文件系统整个 rsync 到/opt/target-arm下,省得缺这缺那。
4.2 只编编译器核心,不编目标库:快速搭建交叉环境
交叉工具链最耗时的是目标端 libgcc 和 libstdc++ 的编译。如果只需要一个能编内核或裸机代码的 c 编译器,可以用all-gcc和install-gcc这两个目标,跳过库的构建。这个技巧在老源码树上同样可用:
make -j4 all-gcc make install-gccall-gcc只构建 gcc 前端的可执行文件(cc1、cc1plus 等),不会去碰目标平台的 libgcc、libstdc++。编完之后/opt/cross-3.3.6/bin/arm-linux-gnu-gcc就能用了。用它编出来的程序如果依赖 libc,链接时仍需手动指定目标平台库路径,所以这适合编内核、bootloader 这种不依赖 libc 的二进制。想编完整用户态程序,还是老老实实make编完整工具链,并在--prefix里指定独立目录,避免和目标板子的库混在一起。
4.3 交叉工具链的五个关键路径,少一个都会在诡异的地方崩
配置交叉编译时,比--target更容易错的是以下几个路径:binutils 的安装路径、目标头文件路径、目标库路径、crt 文件路径、libgcc 路径。前两个出错会在配置阶段直接报错,crt 路径错了则会在链接任意程序时报cannot find crt1.o,libgcc 路径错则是在链接结束阶段报一堆undefined reference to_aeabi*`` 之类的内部符号。后两个错误最坑,因为错误消息出在试用链接阶段,看起来像代码问题,实际是工具链路径问题。我习惯把五条路径写在一个文本文件里随工程走,每次配新环境直接对照检查。
5. 重编 GCC 3.3.6 的五个高频故障点:现象、原因、解决办法
5.1 configure 阶段疑难杂症:版本检查与目录检查先说是谁的问题
现象一:configure: error: C++ preprocessor "/lib/cpp" fails sanity check。原因很简单,系统默认 cpp 是某发行版的非 GCC 实现,或者本机装了一个不配套的独立 cpp。解决方法是显式告诉 configure 使用哪个编译器:CPP=/opt/gcc-3.3.6/bin/cpp-3.3.6这种写法,或者干脆在 PATH 上把老编译器目录排前面再跑 configure。
现象二:configure: error: cannot compute suffix of object files: cannot compile。原因是引导编译器用的默认头文件和 3.3.6 要检查的头文件不一致,常见于用新版 gcc 引导时在新代码上翻车。解决:用低一些但仍在可引导范围的版本(4.x 早期或 3.4 系)重新试一次,别指望 5.x 以上的 gcc 还能顺滑引导 3.3.6。
现象三:配置时提示找不到gnu ld或GNU as。原因大多是 PATH 里没有 binutils 的 bin 目录,或者系统默认 ld 不是 GNU 的。解决方法是把--with-as=/full/path/to/as和--with-ld=/full/path/to/ld显式传给 configure,别相信它对 PATH 的自动探测。
5.2 编译阶段的问题:模板实例化失败与头文件语法不兼容
现象四:编译 libstdc++ 时出现大量error: parse error before numeric constant,或者干脆在某个头文件内部崩溃。原因就是前面说的 glibc 头文件过新,老代码的 fixincludes 没有把新版头文件的扩展语法完全改写。解决有三个层次:第一个层次是换一张更老的内核头文件包,把--with-headers指向旧内核的 include 目录;第二个层次是手工维护一份改造过的头文件目录,把编译报错涉及的具体宏定义直接改掉;第三个层次是放弃本机编译,到旧系统容器里编,这是最省心的方案。
现象五:链接阶段报undefined reference tolibiconv_open'或_charset相关符号。原因不是你的代码问题,而是系统 iconv 库版本太新,符号被版本化。解决方法是在 configure 前LIBS=-liconv试一下,或者直接--disable-nls` 并关闭 iconv 相关检测。老树源码对现代系统字符集库的变化极其敏感,这类链接问题往往比编译问题更难定位,查 configure 日志是唯一正道。
6. 进阶技巧:针对本机的快速热身方案,以及最后一手救场
最后一招,价值在于帮你省掉两三次重编的试错时间。每次配置新环境前,先用“最小化热身”验证工具链环境是否兼容,再决定要不要执行make全量编译。先建一个最小编译目标:
mkdir /tmp/gcc-check && cd /tmp/gcc-check cat > t.c <<'EOF' int main() { return 0; } EOF /opt/gcc-3.3.6/bin/gcc-3.3.6 t.c -o t 2>&1 ldd t | grep libc这段检查的是三件事:目标编译器能否运行、能否链接出可执行文件、链接到的 libc 是不是预期版本。如果这块失败,后面全量编译也会对应翻车,不如在热身阶段就把头文件和库路径配好。另外,编译老 C++ 项目前,用下面这段检测模板与异常 ABI:
cat > abi.cc <<'EOF' #include <string> int main() { std::string s("x"); return s.size(); } EOF /opt/gcc-3.3.6/bin/g++-3.3.6 abi.cc -o abi LD_LIBRARY_PATH=/opt/gcc-3.3.6/lib ./abi能跑通这 8 行代码,说明 libstdc++ 的查找路径、异常处理表、std::string 布局都没问题。我自己的习惯是:凡是新环境,永远先跑这两个热身文件,再开始编真实工程。一次完整的 3.3.6 编译加调试起码三小时,热身只要两分钟,这笔时间花得值。希望帮到你。
本文还有配套的精品资源,点击获取