gcc-3.3.6 老工具链隔离安装与自举编译避坑指南

简介:gcc-3.3.6.tar.gz 面向需要搭建交叉编译工具链的嵌入式开发者、系统软件学习者与旧平台维护人员,提供 GCC 3.3.6 完整源码归档,可用于在一种系统上构建面向另一种目标架构的编译器与配套工具。压缩包约30.03MB,共收录2000个文件,以C源码、Java源码、头文件及汇编相关文件为主,另含配置脚本、Makefile模板、文档、测试用例、库与头文件等,能够支撑源码配置、编译、测试到安装的完整流程。目前已有221人学习下载,适合作为理解GCC构建机制和交叉编译环境的实践材料。解压后可获得结构清晰的gcc-3.3.6目录,便于按配置脚本、编译前端、测试用例和平台适配文件分块查阅;配合INSTALL、README等说明,还能梳理依赖准备、目标架构定制和常见构建问题,对嵌入式移植与老工具链复现具有参考价值。

1. 别急着解包:gcc-3.3.6 装错一步,整台机器的编译链都跟着遭殃

有人拿到 gcc-3.3.6.tar.gz 的第一反应是解包、./configure、make、make install四连,然后在现代发行版上盯着满屏报错发呆。这是 3.3 分支的收尾版本,用来编译十几年前那批老代码、老驱动和特定固件,不适合当成日常开发工具。装它最典型的后果是:--prefix随手写成/usr,把系统的gcc、libstdc++状态搞乱,之后连装个普通软件都报错。适合谁?手里有只认 3.3 的老项目、需要把它固定在独立目录里随时调用的人。下面按装前检查、configure、编译安装、排错、验收的顺序走一遍。

2. 装前把地基打牢:gcc-3.3.6 的宿主编译器与依赖清单

gcc 这类基础工具的安装失败,八成出在宿主环境不匹配,报错只是结果。3.3.6 的源码写成的年代,C99 还没铺开,代码里大量老式函数定义和隐式声明,对编译器的默认标准极其敏感。所以动手解包之前,把宿主编译器、依赖包、目录存放位置这三件事先定下来,比后面改十次参数省事得多。

2.1 宿主 gcc 选 4.x:用最新编译器编 3.3.6 几乎必然失败

我一般不会拿发行版自带的最新 gcc 去编 3.3.6。原因很直接:新编译器默认走gnu17标准,对隐式函数声明、老式参数列表、register关键字这类写法的容忍度低得多,有些组合下 stage1 就直接停住。经验上比较稳的宿主是 gcc 4.x 系列,4.4、4.8 这类版本既认老语法,又足够新到能处理现代系统的头文件。如果你手上只有很新的工具链,别在主系统上反复试,起一个隔离的构建环境把老工具链装进去,编完拷出来就行。

先把环境信息记下来,出错时这些信息比任何猜测都管用。

# 记录宿主环境,出问题时第一时间对照 gcc --version | head -n1 # 宿主编译器版本,决定了这次能不能编过 ld --version | head -n1 # 链接器版本,影响生成的目标文件格式 make --version | head -n1 # 老版本 gcc 对 make 的并行行为比较敏感 uname -m # 目标架构,x86_64 还是 aarch64 getconf LONG_BIT # 确认是 64 位还是 32 位用户态 ls /usr/bin/gcc* 2>/dev/null # 系统里到底装了哪几个编译器

这几条命令看着简单,但能省掉大量来回试探。uname -m和getconf LONG_BIT要一起看,某些环境里内核是 64 位、用户态却是 32 位,这种组合下编出来的东西和预期完全不同。ls /usr/bin/gcc*是为了确认有没有可切换的老版本,有的话优先用gcc-4.8这样的全名调用,别去改系统默认的gcc软链。

提示:构建环境尽量和目标环境分开。老编译器一旦装进系统路径,后面排查任何编译问题都会多一层干扰。

2.2 依赖清单:3.3.6 不吃 GMP/MPFR/MPC,别再照抄 4.x 教程

网上大量“编译 gcc”的教程是围绕 4.3 之后的版本写的,那几版开始强制要求 GMP、MPFR,再往后又加了 MPC。3.3.6 没有这层依赖,照着那些教程去装这三个库纯属浪费时间,还容易在configure阶段被--with-gmp之类的参数误导。真正需要的东西其实很少:

依赖是否必需用途与说明
binutils(as、ld)必需汇编器和链接器,gcc 只负责生成汇编
glibc 开发头文件必需缺了会在configure阶段报无法创建可执行文件
make必需建议 GNU make,老版本对-j支持有差异
宿主 gcc必需用来编 stage1
flex / bison可选发布包的词法语法文件已生成,一般用不上
texinfo可选只影响文档构建,版本太新反而报错
GMP / MPFR / MPC不需要4.3 / 4.5 之后才成为硬依赖
zlib 开发包可选需要压缩调试信息段时才用得上

按这张表装依赖就够了,多余的一个都别加。

# Debian/Ubuntu 系:最小可用集合 sudo apt-get install -y build-essential binutils make flex bison # 需要生成 32 位目标时再补(multilib 支持) sudo apt-get install -y gcc-multilib libc6-dev-i386 # RHEL/CentOS 系对应写法 # sudo dnf groupinstall -y "Development Tools" # sudo dnf install -y glibc-devel

build-essential已经把 gcc、make、libc 头文件打包带上了,单独再写一遍 binutils 只是让依赖更明确。gcc-multilib和libc6-dev-i386不是必装项,只有当你后面要产出 32 位目标文件时才需要,装了它们configure检测到的能力会不一样,所以要提前决定,别编到一半才想起来补。

2.3 目录规划:源码、构建、安装三处必须分开

gcc 支持在源码目录之外构建,这个特性在 3.x 上就能用,而且强烈建议用。源码目录只读、构建目录随便删、安装目录单独隔离,出问题时删掉构建目录重来就行,不用重新解包。在源码目录里直接./configure也能编,但中间产物和源码混在一起,清理不干净,下次编译会读到上次的残留目标文件,报出一些完全对不上号的错误。

# 三处分离:源码、构建、安装各占一个目录 sudo mkdir -p /opt/src /opt/build /opt/gcc-3.3.6 sudo chown -R "$(id -u):$(id -g)" /opt/src /opt/build /opt/gcc-3.3.6 # 后面所有命令都在构建目录里执行 cd /opt/build

--prefix指向的/opt/gcc-3.3.6是最终安装位置,这个名字里带版本号很重要,将来要装第二份老编译器时不会互相覆盖。chown那一步是为了避免后面make install时到处sudo,权限混乱是这类老工具安装里最容易被忽略的麻烦来源,编译产物一半归 root 一半归普通用户,清理时特别难受。

3. 解包与 configure:gcc-3.3.6 那几个必须手动确认的参数

configure是整套安装里唯一值得慢下来的环节。参数写错不会立刻报错,而是编到一半才暴露,那时已经过去了半小时。所以先解包、先看--help,确认这份源码到底认哪些选项,再决定参数组合。不同小版本之间选项名称会有出入,照抄别人的命令行,最常见的结果就是一句unrecognized option。

3.1 解包与目录准备

# 解包到源码目录,注意 -C 指定目标位置 tar -xzf gcc-3.3.6.tar.gz -C /opt/src # 确认解出来的顶层目录名,别想当然 ls /opt/src # 创建独立的构建目录并进入 mkdir -p /opt/build/gcc-3.3.6-build cd /opt/build/gcc-3.3.6-build

解包后先ls看一眼顶层目录名,包名是gcc-3.3.6.tar.gz,解出来通常就是gcc-3.3.6,但有些二次打包的压缩包会多套一层目录,路径写错了后面configure直接找不到脚本。构建目录命名带上-build后缀,是为了在ps和日志里一眼分清哪个是源码、哪个是构建产物。

# 先看这份源码支持哪些选项,重点确认 prefix / languages / multilib /opt/src/gcc-3.3.6/configure --help | head -n 60

--help的输出不用全看,重点扫三段:--prefix相关、--enable-languages相关、--disable-开头的开关。如果某个你打算用的选项在输出里压根不存在,那就说明这份源码不认它,赶紧换方案,别硬写上去等报错。

3.2 configure 关键参数:一条一条说清楚为什么这么写

# 在 /opt/build/gcc-3.3.6-build 目录下执行 ../gcc-3.3.6/configure \ --prefix=/opt/gcc-3.3.6 \ --enable-languages=c,c++ \ --disable-multilib \ --disable-nls \ --enable-threads=posix \ --program-suffix=-3.3.6

--prefix必须指向独立目录,绝对不能是/usr或/usr/local。老版本 gcc 装进系统路径后,头文件搜索顺序会被改掉,系统里其他软件重新编译时可能拿到 3.3.6 的老头文件,报出一堆莫名其妙的类型错误。

--enable-languages=c,c++是省时间的关键。默认会尝试构建所有前端,包括 Fortran、Java、ObjC,每个都要单独走一遍 bootstrap,编译时间翻好几倍。只写c就没有g++,后面需要编 C++ 代码还得重来一遍,所以c,c++是老项目场景下最实用的组合,用不上的语言一律不写。

--disable-multilib表示不生成 32 位运行库。64 位系统上默认会尝试同时构建 32 位和 64 位两套库,宿主缺 32 位 crt 文件时会在编译中期报找不到crti.o。如果你的老代码本身就是 32 位目标,这个参数反而不能加,得先按 2.2 把 multilib 依赖补齐。

--disable-nls关掉多语言消息支持,少一层 gettext 依赖,报错信息变成纯英文。老工具链的本地化消息本来也没什么人看,关掉更干净。

--enable-threads=posix决定 libstdc++ 和运行时库用哪种线程模型,现代 Linux 上选 posix 是唯一合理的答案,选错会导致 C++ 程序里std::thread相关的符号解析异常。

--program-suffix=-3.3.6让安装出来的二进制叫gcc-3.3.6、g++-3.3.6。这一步是防冲突的核心,装了后缀之后,即使这个目录偶然进了 PATH,也不会顶掉系统默认的gcc,调用时必须写全名,等于强制你时刻清楚自己在用哪个版本。

3.3 32 位与 64 位、multilib 的取舍

这三个参数不在同一条路上,先想清楚再写:--build指构建机,--host指编译器将来运行的机器,--target指编译器生成代码的目标机器。本机自编自用,三个都不写,让configure自己探测就行。只有做交叉编译时才需要显式指定,三个值写混了会编出一个能装但跑不起来的产物。

场景multilib 参数额外依赖说明
只产出 64 位目标--disable-multilib无最省时间,推荐默认
需要 32 位和 64 位不加该参数gcc-multilib、libc6-dev-i386编译时间明显变长
纯 32 位老项目不加该参数同上建议先确认宿主支持

如果拿不准,就先用--disable-multilib编一遍跑通,确认这条链路没问题之后,再回头补 multilib 重编一次。一次只改一个变量,出错时才知道是哪个参数引起的。

4. 编译与安装:make bootstrap 的三个 stage 与并行度取舍

configure跑完只是把 Makefile 生成出来,真正耗时间的是编译。gcc 的自举构建比普通项目复杂,它会用自己的输出再编一遍自己,然后比对两次结果是否一致,这是编译器项目特有的自检手段。理解这三个 stage 各自在干什么,看到报错时就不会慌。

4.1 stage1、stage2、stage3 分别在做哪些事

stage1 用宿主编译器(比如 gcc 4.8)编译这份 3.3.6 源码,产出一个“还算能用的 3.3.6 编译器”。这个阶段的产物质量取决于宿主编译器,源码里的老写法也是在这一步被宿主编译器检验。

stage2 用 stage1 产出的编译器再编一遍同样的源码。这一次编译器是 3.3.6 自己,如果源码里有依赖宿主编译器特性的地方,就会在这一步暴露成编译错误。

stage3 用 stage2 的产物再编一遍,然后把 stage2 和 stage3 生成的目标文件逐个比较。两次结果一致,说明这个编译器已经能稳定复现自己的输出,自举成功。不一致时会看到Bootstrap comparison failure!,这是老版本 gcc 最典型的失败信息之一。

# 正式路径:完整自举,三阶段加比对 make bootstrap 2>&1 | tee bootstrap.log # 想省时间的替代方案:不做 stage2/stage3 比对,风险自担 # make all 2>&1 | tee all.log

make bootstrap是完整流程,出问题会明确卡在某个 stage 上,日志里的stage1、stage2字样能直接告诉你是源码问题还是自举不一致问题。make all省掉后面的比对,速度快不少,但少了自检这一层,在正式环境里我一般不用它,除非只是为了快速验证configure参数对不对。

4.2 并行度与内存预算:-j 不是越大越好

# 看核心数,决定 -j 的上限 nproc # 看可用内存(单位 GB),决定 -j 的实际取值 free -g # 并行编译并把完整输出留档 time make -j4 bootstrap 2>&1 | tee build.log

并行度按“内存 / 单进程峰值”来估,不是按核心数。老版本 gcc 编译单个文件时的峰值内存不算高,但-j开得太大仍会把内存吃满,触发 OOM 之后make只会报一句被信号终止,看不出真正原因。8G 内存的机器我给-j4比较稳,16G 以上可以上-j8,但第一次编某个老版本时,我习惯先用-j2跑通,确认没有奇怪的并发依赖问题,再放开并行度。

2>&1 | tee build.log这一段不要省。老版本 gcc 的构建日志动辄几万行,终端回滚早就刷没了,出错时真正有用的那几行往往在中间。tee同时写屏幕和文件,事后grep -n "Error" build.log比翻终端靠谱得多。

编译中断之后不要直接再make。老版本构建系统的依赖关系不如现在严密,中断时可能留下半成品目标文件,再跑一遍会报出和中断完全无关的错误。我的做法是直接把构建目录整个删掉重新configure,宁可多花十分钟,也别在污染过的中间产物上浪费时间。

4.3 make install 与安装后的路径处理

# 安装到 configure 时指定的 --prefix make install # 确认安装产物,重点看 bin 和 lib 两个目录 ls /opt/gcc-3.3.6/bin ls /opt/gcc-3.3.6/lib | head -n 20 # 直接调全名验证安装是否完整 /opt/gcc-3.3.6/bin/gcc-3.3.6 -v

装完之后,bin目录里应该能看到带-3.3.6后缀的gcc-3.3.6、g++-3.3.6、cpp-3.3.6,lib目录里能看到libstdc++.so.5相关的文件。如果bin里只有gcc没有后缀,说明--program-suffix没生效,回头检查是不是参数写错了位置。

路径处理上我倾向不加全局环境变量。要用的时候写全路径,或者写一个只作用于当前 shell 的小脚本:

# 只在需要时 source 一下,用完关掉终端就失效 export GCC336=/opt/gcc-3.3.6 export PATH="$GCC336/bin:$PATH"

这种方式的问题是需要频繁切换时会麻烦,优点是永远不会污染系统默认环境。反过来,如果你把/opt/gcc-3.3.6/bin写进/etc/profile.d/并且排在系统路径前面,那台机器上所有用户的默认编译器都会变成 3.3.6,这是很多“装完之后系统编译全挂”的起因。

5. 排查与避坑:gcc-3.3.6 从编译到安装最常遇到的五类问题

以下五条都是在实际环境里反复遇到的情况,按“现象 → 原因 → 解决”写,方便你对着日志找。日志里搜不到完全一致的文字也没关系,报错位置和阶段对得上,处理方式基本通用。

5.1 配置与编译阶段:三条高频报错

第一条:configure: error: C compiler cannot create executables

现象:configure刚开始几十秒就退出,屏幕上只有这一句,后面的信息全在config.log里。

原因:宿主编译器本身跑不通,常见的有三种:缺 libc 开发头文件、缺 32 位 crt 文件、构建目录所在分区挂了noexec。另外磁盘空间不足也会以这个形式报出来,因为编译器写不出可执行文件。

解决:先看日志最后 30 行,tail -n 30 config.log,里面会明确写是找不到crt1.o还是头文件缺失。头文件问题装glibc-devel/libc6-dev;crt1.o相关问题装 multilib 包;noexec就把构建目录挪到普通分区,df -h和mount | grep各看一次确认。

第二条:error: 'SSIZE_MAX' undeclared (first use in this function)

现象:编译进行到一半,某个源文件里报SSIZE_MAX未声明,前后还有一串相关的类型告警。

原因:新版本 glibc 的头文件把SSIZE_MAX藏在了特定的 feature 宏后面,老源码直接引用,现代默认宏组合下取不到。这个问题只在宿主头文件比较新的环境里出现,同样的源码在老系统上编不出这个错。

解决:在编译选项里补一个兜底定义,make CFLAGS="-g -O2 -DSSIZE_MAX=LONG_MAX" bootstrap。注意只在 64 位目标下用LONG_MAX是安全的,32 位目标应该用INT_MAX,写错会让边界判断出错。更彻底的做法是找到引用处补一行 fallback 定义,但改源码要留记录,否则下次换个人编又得重新查一遍。

第三条:fixincludes 阶段反复报错,或者文档构建报unknown command

现象:make跑到 fixincludes 阶段大量输出,内容是对系统头文件的解析错误;或者在doc目录里makeinfo报不认识某个命令。

原因:fixincludes 是专门修系统头文件兼容性的工具,它的解析能力停留在二十年前,遇到现代头文件里的扩展语法就处理不了。makeinfo则是 texinfo 版本太新,老.texi文件里的写法在新版本里被移除了。

解决:跳过 fixincludes 阶段,make STMP_FIXINC= bootstrap,代价是系统头文件的兼容修正要自己扛,编不过的地方手动加宏补丁。文档构建问题用make MAKEINFO=true bootstrap直接绕过,反正装编译器不是为了看手册。

注意:跳过 fixincludes 之后,如果后面编译老代码时报头文件相关的语法错误,先怀疑这一步,别去查老代码本身。

5.2 安装与链接阶段:两条更隐蔽的坑

第四条:gcc-3.3.6 -v正常,但编译出来的程序运行时报找不到libstdc++.so.5

现象:C++ 程序编译链接都通过,一运行就报error while loading shared libraries: libstdc++.so.5。

原因:3.3.6 带的是libstdc++.so.5,系统默认库路径里只有新版本的so.6,两者 soname 不同,不会互相替代,所以运行时确实找不到这个老库。

解决:只给需要它的程序加 rpath,g++-3.3.6 main.cpp -Wl,-rpath,/opt/gcc-3.3.6/lib,这样只有这个可执行文件会去那个目录找库。临时验证时可以用LD_LIBRARY_PATH=/opt/gcc-3.3.6/lib ./a.out,但别写进 shell 配置文件,全局生效之后其他程序的库搜索顺序也会被带偏。

第五条:-v显示版本对了,但链接时报unrecognized option或者生成的目标文件格式异常

现象:编译器版本看似正确,链接阶段却报出宿主 binutils 不认识的选项。

原因:--with-gnu-as、--with-gnu-ld没显式指定,gcc 运行时按 PATH 找as和ld,实际用到的可能是系统里版本很新的 binutils,和 3.3.6 预期的调用方式不一致。

解决:先查清楚它到底在用哪个,/opt/gcc-3.3.6/bin/gcc-3.3.6 -print-prog-name=as和-print-prog-name=ld,输出路径如果不是你预期的那个,就在configure时显式加上--with-as=/usr/bin/as --with-ld=/usr/bin/ld,或者干脆在调用时用-B指定目录。这一条隐蔽在于它不在编译阶段报错,只有真正做链接时才暴露。

6. 验收:三条命令确认 gcc-3.3.6 真的能干活

装完不等于能用。我见过太多“gcc -v输出正常但一编就废”的情况,所以验收要分开做:先确认版本和搜索路径,再确认能编出可执行文件,最后确认运行时链接没断。三步都过,这套环境才算真的落地。

# 第一步:版本与内置宏,确认是 3.3 分支而不是被别的 gcc 顶替 /opt/gcc-3.3.6/bin/gcc-3.3.6 -dumpversion echo | /opt/gcc-3.3.6/bin/gcc-3.3.6 -E -dM - | grep -E '__GNUC__|__GNUC_MINOR__' # 第二步:编一个最小 C 程序并运行,确认从源码到可执行文件链路通 cat > /tmp/hello.c <<'EOF' #include <stdio.h> int main(void) { printf("gcc336 ok\n"); return 0; } EOF /opt/gcc-3.3.6/bin/gcc-3.3.6 /tmp/hello.c -o /tmp/hello && /tmp/hello # 第三步:编一个 C++ 程序,确认 libstdc++ 版本和 rpath 都对 cat > /tmp/hello.cpp <<'EOF' #include <iostream> int main() { std::cout << "gcc336 c++ ok" << std::endl; return 0; } EOF /opt/gcc-3.3.6/bin/g++-3.3.6 /tmp/hello.cpp -Wl,-rpath,/opt/gcc-3.3.6/lib -o /tmp/hellocpp ldd /tmp/hellocpp | grep libstdc++

第一步里-dumpversion应该输出3.3.6,内置宏里__GNUC__是 3、__GNUC_MINOR__是 3。这两个值比-v的输出更可信,因为-v只反映二进制自己声明的版本,内置宏反映的是真正参与编译的头文件集合,两者对不上说明路径里有别的东西混进来了。第二步的 C 程序能跑,说明 crt 文件、链接器、库搜索都没问题。第三步的ldd输出里应该出现指向/opt/gcc-3.3.6/lib的libstdc++.so.5,如果指向系统里的so.6,说明 rpath 没生效,老版本的运行库其实没被用上,编出来的东西和预期不是一回事。

检查项预期结果不符合时先查
-dumpversion3.3.6--program-suffix是否生效
内置宏__GNUC__3是否有其他 gcc 头文件混入
C 程序运行正常输出crt 文件、链接器路径
ldd中 libstdc++指向安装目录的so.5rpath 参数位置
-print-prog-name=as预期路径--with-as是否指定

最后一次翻车就是省了这几步:-v看着挺好,直接拿去编老项目,结果链接出来的可执行文件在目标机上跑不起来,回头查了两个小时才发现用错了as。从那以后我每次装完老工具链,都强制走一遍这三条验收,不管多急,哪怕只编一个 hello world 也要跑完ldd看一眼。希望帮到你。

本文还有配套的精品资源,点击获取