简介:gcc-3.3.6.tar.gz 是 GNU 编译器集合 3.3.6 版的完整源码归档,主要面向嵌入式开发、操作系统移植与编译原理学习者,用于在主机上构建面向异构目标平台的交叉编译工具链,也可作为定制编译器、研究 GCC 内部实现的基础素材。压缩包共 2000 个文件,类型以 C 源码、Java 源码、头文件为主,并包含汇编、C++、Fortran 及配置模板等文件,整体约 30.03MB,覆盖语言前端、后端、运行库与构建系统等多个层次。已有 221 人学习或下载,说明其在交叉编译与旧版工具链复原场景中仍具参考价值。解压后可获得较完整的 GCC 源码树,内含配置脚本、Makefile 模板、编译安装文档、测试用例以及库和头文件,并附带预处理器、C/C++ 编译器、汇编器与链接器等工具的实现代码。读者可据此完成工具链配置、编译、测试与安装,理解目标架构适配和构建选项定制,适合需要复原旧环境或深入编译器工程的中高级开发者。
1. GCC 3.3.6 源码包:为什么老工具链还要从 tar.gz 自己编
手上有一套老 C 工程,Makefile 里写死了旧版 ABI 和编译选项,换到现代编译器就报符号冲突;或者实验要求固定编译器版本,复现某次构建结果。这时gcc-3.3.6.tar.gz就不是“古董收藏”,而是 GCC 3.3.6 源码包,能从源码编出一套可独立安装的老工具链。它解决的是“现代系统默认编译器编不过旧代码、旧脚本又必须原样跑”的问题,适合维护老项目的开发者、做编译工具链实验的人,以及需要固定编译器做可复现构建的从业者。
要不要用,先看边界:它不是装完就能替代系统编译器的万能包,构建过程依赖宿主环境,现代系统上会踩到旧头文件、旧文档工具和新 C 库之间的兼容坑。它的价值在于把编译器版本锁死,避免“今天能编、明天升级就翻车”的玄学问题。下面按解包、配置、编译安装、验证、排错、固化的顺序拆开,命令都能照着改路径执行。
2. 解包与构建环境:从源码树结构看清自举链路
2.1 源码树里哪些目录决定构建顺序
GCC 的构建不是“编译一个可执行文件”那么简单,它要先用宿主编译器编出一个最小可用的 stage1 编译器,再用 stage1 编 stage2,最后用 stage2 再编一遍做比对,这就是自举。源码树顶层的configure是总入口,gcc/目录放驱动、前端、后端和运行时支撑,libstdc++-v3/是 C++ 标准库,libiberty/是通用辅助函数,include/放公共头文件。先看清一级目录,能判断后面编 C 还是 C++、文档构建会不会拖后腿。
我一般不解包后直接进源码目录 configure,而是先列一下压缩包顶层结构,确认解包后目录名和预期一致。下面两条命令只读不写,适合在下载目录里先跑一遍:
# 查看压缩包前 40 行条目,确认顶层目录名 tar -tzf gcc-3.3.6.tar.gz | sed -n '1,40p' # 按斜杠切分,去重看一级目录,判断源码树组成 tar -tzf gcc-3.3.6.tar.gz | awk -F/ 'NF>1{print $2}' | sort -u-t是列出压缩包内容,-z表示 gzip 解压,-f指定文件名。第二行用awk以/为分隔符,NF>1排除纯文件名,$2取一级目录,再排序去重。这样能在解包前发现压缩包内是否多套了一层目录,避免后面cd错位置。源码树结构看清后,再准备宿主工具,顺序就不会乱。
2.2 宿主工具与依赖清单怎么核
GCC 3.3.6 的构建依赖不算多,但每一样缺了都会在 make 阶段才暴露,排查成本高。常见做法是先把命令可用性核一遍,再决定是否继续。宿主 C 编译器负责 stage1,make负责驱动,as和ld负责汇编与链接,flex、bison、gperf参与生成解析器和哈希表,makeinfo只影响文档。老版本对工具版本比较敏感,新版makeinfo的参数不一定兼容旧 Makefile,这一点后面避坑章会展开。
| 组件 | 用途 | 检查命令 | 注意 |
|---|---|---|---|
| C 编译器 | 自举 stage1 | cc --version | 确认不是空壳链接 |
| GNU make | 驱动构建 | make --version | 优先 GNU make |
| binutils | 汇编与链接 | as --version、ld --version | 目标架构要一致 |
| C 库头文件 | 系统调用与库声明 | ls /usr/include/stdio.h | 老 GCC 对头文件敏感 |
| flex | 词法分析器 | flex --version | 老版本更稳 |
| bison | 语法分析器 | bison --version | 同上 |
| gperf | 完美哈希生成 | gperf --version | 缺失时看构建目标 |
| texinfo | 生成文档 | makeinfo --version | 可用MAKEINFO=true跳过 |
| awk/sed | 构建脚本 | awk --version、sed --version | GNU 版本更稳 |
| patch | 打兼容补丁 | patch --version | 可选但建议有 |
下面这段循环只做检查,不安装任何东西。把输出贴到构建记录里,后面出问题能快速判断是环境缺件还是源码本身报错:
# 逐个检查构建常用命令,缺失时打印 MISSING for t in cc make as ld flex bison gperf makeinfo awk sed patch; do printf '%-10s ' "$t" command -v "$t" || echo "MISSING" donecommand -v返回命令路径,找不到时返回非零,正好接|| echo。printf '%-10s '只是对齐输出,方便肉眼扫。缺makeinfo不一定要立刻装,因为 GCC 3.3.6 的文档构建经常是现代环境里最先失败的一环,可以先用MAKEINFO=true跳过文档,把编译器本体编出来再说。缺flex、bison则不建议跳过,解析器生成是构建链的一部分。
2.3 解包前的目录规划与隔离策略
源码、构建、安装三个目录必须分开。GCC 支持 out-of-tree 构建,在源码目录里直接 configure 会把生成文件混进源码树,后面想重来一遍都难清理。我一般固定三层:/opt/src放源码包和解包目录,/opt/build放构建目录,/opt/toolchains放安装 prefix。路径只是示例,读者可以换成自己有写权限的目录,但不要把 prefix 设成/usr,也不要把bin直接覆盖系统路径。
# 建立源码、构建、安装三类目录 mkdir -p /opt/src /opt/build /opt/toolchains # 解包到源码目录,-C 指定目标目录 tar -xzf gcc-3.3.6.tar.gz -C /opt/src # 确认解包后的顶层目录存在,不进入源码树构建 ls -ld /opt/src/gcc-3.3.6-C让 tar 先切换目录再解包,避免在当前目录炸出一堆文件。ls -ld只确认目录本身,不展开内容。构建目录要单独建,例如/opt/build/gcc-3.3.6,后面 configure 时用绝对路径调用源码树里的configure。安装 prefix 用独立目录,例如/opt/toolchains/gcc-3.3.6,这样卸载时删目录即可,不会牵动系统默认编译器。隔离做得好,后面多版本共存和排错会省很多事。
3. configure 参数与安装布局:把 prefix 和语言前端定死
3.1 关键配置项怎么选:prefix、languages、program-suffix
configure 阶段决定的东西比想象中多:装到哪里、编哪些语言前端、可执行文件叫什么、要不要 multilib、线程模型怎么选。对 GCC 3.3.6 这种老源码包,参数越少越稳,第一次构建不要把所有语言都打开。常见做法是先只编 C,确认工具链能跑通,再回头加 C++;如果一上来就c,c++,失败点会混在libstdc++-v3里,日志很难一眼定位。
| 参数 | 作用 | 建议 |
|---|---|---|
--prefix | 安装根目录 | 用独立目录,不用/usr |
--enable-languages | 选择前端 | 首次用c,成功后再加c++ |
--program-suffix | 给可执行文件加后缀 | 用-3.3.6避免和系统 gcc 冲突 |
--disable-multilib | 只构建一种位宽 | 先减少变量,需要 32 位再单独处理 |
--enable-threads=posix | 线程模型 | 老项目常用 posix |
--disable-checking | 关闭内部断言 | 可加快构建,排错时可去掉 |
--with-gnu-as | 明确使用 GNU as | 宿主是 GNU binutils 时加上 |
--with-gnu-ld | 明确使用 GNU ld | 同上 |
--build/--host/--target | 交叉编译三元组 | 本机自用不要乱填 |
--program-suffix=-3.3.6很关键。它让安装出来的驱动叫gcc-3.3.6、g++-3.3.6,而不是直接叫gcc、g++。这样即使把安装目录加进 PATH,也不会把系统默认编译器顶掉。--disable-multilib也不是必选项,但在现代 64 位宿主上先关掉多库构建,能少掉一批库路径问题。参数确定后,配置命令要保存到日志里,后面复现时不用凭记忆拼。
3.2 配置命令逐段拆解与失败日志定位
下面这条命令在构建目录里执行,调用源码树的 configure。首次构建只开 C 前端,安装到独立 prefix,可执行文件加后缀,关 multilib,明确使用 GNU 汇编器和链接器。命令末尾用tee同时输出到终端和configure.log,后面排查有据可查:
# 进入独立构建目录,不在源码树内 configure cd /opt/build/gcc-3.3.6 # 调用源码树的 configure,参数按行拆开便于修改 /opt/src/gcc-3.3.6/configure \ --prefix=/opt/toolchains/gcc-3.3.6 \ --enable-languages=c \ --program-suffix=-3.3.6 \ --disable-multilib \ --enable-threads=posix \ --disable-checking \ --with-gnu-as \ --with-gnu-ld \ 2>&1 | tee configure.log2>&1把标准错误并入标准输出,tee既显示又落盘。configure 失败时不要只看终端最后一行,先看config.log尾部,再看关键错误。下面两条命令一个看上下文,一个过滤常见失败关键词:
# 看 config.log 最后 80 行,确认失败调用栈 tail -n 80 config.log # 过滤找不到命令、错误关键字,只看最后 30 条 grep -n "cannot find\|not found\|error" config.log | tail -n 30config.log记录的是 configure 阶段的探测结果,包括编译器能不能编出测试程序、头文件是否存在、链接器是否可用。很多失败不是 GCC 源码的问题,而是宿主 C 编译器编一个小测试程序就失败了。遇到cannot find先确认命令路径,遇到error先看它前面调用的完整命令行。configure 通过后,构建目录里会生成Makefile和config.status,这两个文件要保留,后面复现和排错都用得上。
3.3 make 自举、并行度与安装
GCC 的 make 阶段建议用bootstrap目标,它会走三阶段自举,比单纯make更能验证编译器自身是否稳定。并行度不要一上来拉满,老源码包和新版 make 的组合在并行时偶发依赖顺序问题,先-j1或-j2跑通,再考虑加。下面命令把构建日志和安装日志分开保存,出问题时能判断是编译失败还是安装阶段失败:
# 两路并行先跑,保守一点;不稳就改成 -j1 make -j2 bootstrap 2>&1 | tee make.log # 自举成功后安装到 prefix make install 2>&1 | tee install.logbootstrap会多次调用刚编出来的编译器,耗时比普通构建长,但能暴露 stage2 和 stage3 不一致的问题。-j2是折中值,机器核多也可以先保持二路,等确认稳定再提高。如果文档构建报makeinfo相关错误,可以用make MAKEINFO=true -j2 bootstrap跳过文档,先拿到编译器本体;但跳过文档不等于跳过检查,后面仍要跑最小程序验证。安装日志里重点看bin/、lib/gcc-lib/和include/是否落到 prefix 下,路径不对说明 prefix 或 program-suffix 没生效。
3.4 安装后目录布局与可执行文件命名
安装完成后先看目录布局,不要急着写代码。bin/下应该是带后缀的驱动,lib/gcc-lib/<target>/3.3.6/下是cc1、libgcc.a等内部文件,include/下是配套头文件。不同宿主的目标三元组不一样,所以下面用<target>占位,实际路径以-dumpmachine输出为准。用find限定深度,避免在整棵安装树里翻太久:
# 查安装后的关键可执行文件和内部库 find /opt/toolchains/gcc-3.3.6 -maxdepth 4 -type f \ \( -name 'gcc*' -o -name 'cc1*' -o -name 'libgcc*' \) | sort # 查目标三元组,后面写包装脚本会用到 /opt/toolchains/gcc-3.3.6/bin/gcc-3.3.6 -dumpmachine-maxdepth 4限制搜索深度,-type f只看文件,括号里的-name用-o连接,外面加转义括号避免被 shell 解释。-dumpmachine输出的是这套工具链认的目标三元组,不是宿主uname的简单结果。目录布局确认后,再进入验证阶段;如果bin/gcc-3.3.6不存在,先回头检查--program-suffix是否拼错,或者make install是否真的执行到了安装步骤。
4. 验证与多版本共存:用最小程序确认 ABI 没跑偏
4.1 最小 C 程序与目标文件检查
编译器编出来不等于能用,先写一个最小 C 程序,确认预处理、编译、汇编、链接四步都通。下面程序只依赖标准输入输出,不引入额外库,适合做第一轮验证。编译时加-v看驱动实际调用了哪些内部程序,输出里能看到cc1、as、collect2的路径,路径落在新 prefix 下才说明没有误用系统工具链:
/* /tmp/hello.c:最小验证程序 */ #include <stdio.h> int main(void) { printf("gcc-3.3.6 ok\n"); return 0; }# 用绝对路径调用新编译器,避免 PATH 干扰 /opt/toolchains/gcc-3.3.6/bin/gcc-3.3.6 -v -o /tmp/hello /tmp/hello.c # 运行并检查文件类型、ELF 头和动态链接信息 /tmp/hello file /tmp/hello readelf -h /tmp/hello | sed -n '1,20p' ldd /tmp/hello-v会把版本、目标三元组、搜索目录和实际调用命令打出来,重点看gcc version 3.3.6和cc1路径。file看 ELF 位宽和架构,readelf -h看Class、Machine、Type,ldd看动态链接器是不是预期的那一个。如果程序能跑但ldd指向了系统新库,说明运行时库路径没隔离好,后面 C++ 程序更容易出问题。C 程序通过后,再验证 C++ 前端;如果只编了 C,这一步先跳过。
C++ 验证程序如下,编译时用g++-3.3.6,运行前把老库目录加进LD_LIBRARY_PATH。老libstdc++的 ABI 和新系统库不兼容,直接跑可能报符号找不到:
// /tmp/hello.cpp:验证 C++ 前端和运行库 #include <iostream> int main() { std::cout << "g++-3.3.6 ok" << std::endl; return 0; }# 编译 C++ 程序,输出详细调用链 /opt/toolchains/gcc-3.3.6/bin/g++-3.3.6 -v -o /tmp/hello_cpp /tmp/hello.cpp # 查运行时依赖,确认 libstdc++ 指向老工具链目录 ldd /tmp/hello_cpp | grep libstdc++如果g++-3.3.6不存在,说明 configure 时只开了 C,这是预期结果,不要误判为安装失败。C++ 程序链接成功但运行时报GLIBCXX相关符号缺失,通常是运行时加载了系统新libstdc++,用LD_LIBRARY_PATH把老库目录提前即可。验证阶段的目标不是“编过一个 hello”,而是确认编译器、汇编器、链接器和运行库都落在同一套 prefix 下。
4.2 PATH 切换与 Makefile 显式指定 CC
多版本共存最稳的方式不是改系统默认gcc,而是在项目里显式指定编译器绝对路径。临时切换可以把新工具链的bin加到 PATH 前面,但因为可执行文件带了-3.3.6后缀,命令名仍然是gcc-3.3.6,不会抢占gcc。下面命令确认当前 shell 能找到新驱动,同时不改变系统默认命令:
# 把新工具链 bin 放到 PATH 前面,仅影响当前 shell export PATH=/opt/toolchains/gcc-3.3.6/bin:$PATH # 确认命令解析到新目录,系统 gcc 仍是原样 command -v gcc-3.3.6 command -v gcc gcc-3.3.6 --versioncommand -v看命令实际路径,--version看版本号。因为--program-suffix存在,gcc-3.3.6和系统gcc是两个名字,冲突概率低。真正容易出问题的是项目 Makefile 里写死了CC = gcc,这时 PATH 切换不会自动改掉它,必须在命令行覆盖或用绝对路径。下面 Makefile 把CC写成绝对路径,适合把老项目固定到这套工具链:
# Makefile:显式指定老编译器,不依赖 PATH CC := /opt/toolchains/gcc-3.3.6/bin/gcc-3.3.6 CFLAGS := -O2 -Wall hello: hello.c $(CC) $(CFLAGS) -o $@ $<CC :=是立即展开赋值,避免被环境变量意外覆盖。$@表示目标名,$<表示第一个依赖,这是 Makefile 的自动变量。这样写的好处是构建记录里能直接看到编译器路径,换机器时只需要改一行。不要把CC设成gcc-3.3.6后指望所有子 make 都继承,有些老项目会在子目录里重置CC,必要时用make CC=/opt/.../gcc-3.3.6在命令行强制覆盖。
4.3 用包装脚本固定工具链
项目里既有 C 又有 C++,还要固定运行时库路径时,包装脚本比手写一长串环境变量省事。下面脚本先取目标三元组,再把老库目录拼进LD_LIBRARY_PATH,最后exec真正的编译器。exec会用新进程替换当前 shell 进程,参数原样传递,不会多套一层 shell:
#!/bin/sh # gcc-3.3.6-wrapper:固定工具链路径和运行库搜索路径 set -eu TOOLCHAIN=/opt/toolchains/gcc-3.3.6 TARGET=$("$TOOLCHAIN/bin/gcc-3.3.6" -dumpmachine) export PATH="$TOOLCHAIN/bin:$PATH" export LD_LIBRARY_PATH="$TOOLCHAIN/lib:$TOOLCHAIN/lib/gcc-lib/$TARGET/3.3.6${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" exec "$TOOLCHAIN/bin/gcc-3.3.6" "$@"set -eu让变量未定义或命令失败时直接退出,避免脚本带着半截环境继续跑。-dumpmachine的输出用于拼lib/gcc-lib下的版本目录,不同宿主不一样,不能写死。${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}是条件拼接,原变量为空时不额外加冒号。保存后chmod +x,在 Makefile 里把CC指向这个脚本即可。包装脚本适合放进项目tools/目录一起归档,换机器时只改TOOLCHAIN一行。
5. 避坑与排查:老 GCC 在现代宿主上的五个翻车点
5.1 现代 C 库头文件与老 GCC 解析冲突
现象:make 跑到libgcc或运行库阶段报error: expected '=', ',', ';', 'asm' or '__attribute__' before ...,或者提示某个SSIZE_MAX、__BEGIN_DECLS相关宏未定义。原因不一定是源码写错,而是宿主 C 库头文件比 GCC 3.3.6 新太多,老前端不认识新头文件里的写法。GCC 3.3.6 自带的运行库头文件也老,和新系统头文件混在一起时,预处理阶段就会炸。
解决思路分三层:第一层,尽量在隔离的旧系统构建根里编,让头文件和库版本匹配;第二层,首次只开 C 前端,减少libstdc++-v3带来的变量;第三层,实在要在新宿主上编,就给关键头文件打兼容补丁,但补丁要跟着源码包一起归档,否则下次复现又得重来。排查时先单线程跑,把第一个错误截出来:
# 单线程构建,保留完整日志 make -j1 2>&1 | tee make.log # 只看前 20 条 error,定位第一个失败点 grep -n "error:" make.log | head -n 20第一个error:比后面一堆连锁错误更有价值。如果错误出现在系统头文件包含链里,不要急着改 GCC 源码,先确认是不是宿主头文件版本过新。常见做法是准备一个只含旧头文件和旧库的 sysroot,configure 时把搜索路径指过去;这比在源码里到处打补丁干净。
5.2 makeinfo 缺失或版本过新
现象:构建到文档目录时报makeinfo: command not found,或者makeinfo: unrecognized option,终端里一串Makefile:xxx: recipe for target ... failed。原因在于 GCC 3.3.6 的文档构建脚本按老版 texinfo 的参数写,新版makeinfo不一定兼容;有些精简宿主干脆没装 texinfo。文档失败不代表编译器本体失败,但 make 默认会停在那里。
解决时可以先用MAKEINFO=true跳过文档目标,把编译器编出来,再决定要不要补文档。下面命令把文档工具替换成true,其他构建流程不变:
# 跳过文档生成,先构建编译器本体 make MAKEINFO=true -j2 bootstrap 2>&1 | tee make-noinfo.logMAKEINFO=true是把 make 变量覆盖成true命令,调用时直接返回成功,不生成文档。这样做的前提是安装时也不依赖文档目标,make install一般不受影响。若必须保留文档,就装一套与老源码匹配的 texinfo,或者单独用新版工具生成后手动放进share/。我一般把“跳过文档”写进构建脚本注释里,避免后面有人以为文档缺失是安装坏了。
5.3 C++ 前端自举失败
现象:configure 时开了c,c++,make 到libstdc++-v3报_GLIBCXX_USE_C99相关错误,或者链接阶段提示std::__throw_length_error未定义。原因是老版 C++ 标准库和新宿主 C 库、新编译器阶段之间存在兼容缝隙,尤其在现代系统上直接自举时更容易出现。这个坑不是改一个编译选项就能过去,得把构建顺序拆开。
更稳的做法是第一次只编 C,确认gcc-3.3.6可用后,再单独加 C++ 前端重配一次;如果仍然失败,就把 C++ 运行库的构建和 C 编译器安装分开处理。运行阶段还要注意LD_LIBRARY_PATH,别让新系统的libstdc++抢先加载。下面命令用于确认 C++ 程序实际链接到哪套库:
# 编译 C++ 程序后查运行库依赖 /opt/toolchains/gcc-3.3.6/bin/g++-3.3.6 -v -o /tmp/hello_cpp /tmp/hello.cpp ldd /tmp/hello_cpp | grep libstdc++如果ldd输出指向系统库目录,而不是新 prefix 下的lib,运行时就可能出现符号版本不匹配。把新工具链的库目录放进 `LD
本文还有配套的精品资源,点击获取