首页 / 资讯中心 / 文章详情

gcc-3.3.6 源码构建与交叉编译工具链避坑

gcc-3.3.6 源码构建与交叉编译工具链避坑 ★ FEATURED ARTICLE
简介GCC 3.3.6 源码压缩包面向嵌入式开发、操作系统移植以及需要自建交叉编译工具链的开发者提供一份可配置、编译与安装的经典 GNU 编译器集合版本。包内约 2000 个文件、整体 30.03MB以 C 源码、Java 源码、头文件及汇编和 Ada 相关文件为主同时包含配置脚本、Makefile 模板、文档、测试用例与 cpp、gcc、g、as、ld 等工具源码覆盖 C/C、Fortran、Ada、Java 等多语言前端与后端。已有 221 人学习适合希望理解 GCC 构建流程、目标架构适配和交叉编译原理的读者。借助配置选项、构建模板与测试用例可掌握指定目标平台和安装路径、准备依赖、执行编译与功能验证等关键环节也能参考多架构适配片段和运行时库测试组织方式为移植编译器、排查构建错误及搭建嵌入式工具链提供可复用思路。1. 一块 2005 年的地基gcc-3.3.6 源码包在今天的真实用途去年接了个老设备维护的活儿固件里跑的二进制必须由那个年代的 gcc 生成换成新版编译器链出来的镜像启动到一半就卡死。排查了两天问题不在代码在 ABI。最后是从 gcc-3.3.6.tar.gz 这个源码包重新造了一套编译器才把问题按住。这份包的定位很清楚3.3 分支的收尾版本能提供那一代的后端代码生成、C 与 C 前端、以及老式的构建脚本。它解决的不是性能和新特性是可重复——同一份源码、同一组 configure 参数在哪台机器上重跑都能得到行为一致的编译器。适合维护老固件的人、复现早期实验环境的人、给老系统补编译能力又不想整机重装的人。不适合需要 C11 以上、新架构后端、多线程优化的场景硬上只会浪费时间。2. 认清 gcc-3.3.6 的边界能编什么、编不了什么动手之前得先把这份包的射程划清楚否则后面每一条报错都是无效排查。源码包本身只是原材料它的能力由三样东西决定前端支持的语言、后端支持的架构、以及宿主环境愿意给它多少配合。这三样在 3.3.6 里都是定死的越不过去。2.1 语言前端与目标架构3.3.6 手里到底有几张牌语言侧靠--enable-languages选择3.3.6 认的值是c、c、f77、java、objc。实际工程里我一般只开c,c理由很实在f77需要一个配套的 Fortran 77 运行时java要拖进完整的 GCJ 运行时和一大票 Java 类库编译时间翻几倍而且这些老运行时在现代 glibc 上几乎跑不起来objc相对轻但除非你手上真有 Objective-C 的老代码否则没必要给它留位置。开得越少bootstrap 越容易一次过。架构侧由--target指定3.3.6 的gcc/config/目录下有一个子目录就对应一个后端。那个年代覆盖得比较全的组合大概是这些目标三元组典型用途3.3.6 支持度i386-linux-gnu32 位 x86 应用成熟x86_64-linux-gnu64 位 x86可用但当年 64 位工具链很少人这么配arm-linux-gnuARM 应用处理器跑 Linux成熟arm-elf裸机 ARM配 newlib成熟交叉工具链最常见的组合mips-linux-gnuMIPS 路由器类设备成熟powerpc-linux-gnuPowerPC 嵌入式成熟avr-elf8 位单片机可用但资源受限场景要小心m68k-linux-gnu老式 68k 平台可用看不出来的一律别猜进源码包执行ls gcc/config/ | grep -i 你的架构前缀能列出目录就是有后端。目录存在但编译报错的概率也不低因为有些后端在 3.3 时代只是半成品avr和m68k都属于这类编译前先做一次裸机 hello world 的心理准备。2.2 为什么更推荐把构建环境退回旧容器这里有个血泪经验直接在现代发行版上编 3.3.6大概率卡在第一步。现代搭建环境有几个变化是致命的。宿主编译器默认语言标准从 gnu89 一路推到 gnu17老代码里大量 KR 风格函数定义、隐式 int、函数未声明就先使用在新标准下直接判成错误-fno-common改成默认老代码里靠 tentative definition 合并全局变量的写法会炸成重复符号现代 glibc 的头文件里塞了_Static_assert、__extension__和一堆新宏3.3.6 的fixincludes脚本根本不认识会把系统头文件修复成坏的。所以我的做法是分两条路按手上资源选有条件跑容器或虚拟机找一个 32 位的旧发行版镜像装进去在里面编。这条路成功率最高因为它的 glibc、binutils、make、texinfo 全是同一个年代的配合默契。只能在本机编给宿主编译器加降级开关同时把一堆生成工具换掉。这条路能走通但要准备打补丁。判断依据可以看一个很直接的信号gcc --version出来的版本号如果大于 4.7就别指望零改动通过先按 2.3 把依赖版本对齐了再说。2.3 依赖版本红线五个容易忽略的组件下面这张表是我踩过之后整理出来的横向对比了最低能用和实际推荐最后一行是当年反而更省事的错误认知组件最低要求推荐做法踩坑点宿主编译器gcc 2.95gcc 3.x 或 4.14.8 以上需要-stdgnu89加补丁make3.793.80 / 3.813.82 之后对老 Makefile 隐式规则更严格容易在bootstrap中途报没有规则可制作目标binutils2.132.15交叉场景下as/ld版本错配libgcc链接必挂texinfo4.x直接跳过5.0 之后对老.texi语法不容忍会报 unknown commandflex / bison2.5.4 / 1.35用发布包里已生成的 .c新版生成器重跑老语法文件会报一堆移进-归约冲突gmp / mpfr不需要不需要这两是 gcc 4.3 之后才外置的3.3.6 完全内置别去装表格最后一行值得多说一句。很多人一看要编 gcc 就条件反射去装gmp-devel、mpfr-devel对 3.3.6 来说这是纯噪音它的数学运算走自己内置的libgcc实现。真正需要提前确认的反而是build-essential之外的两样texinfo能不能关掉以及有没有 32 位编译支持。texinfo的绕法很干脆configure时或者make时传MAKEINFOtrue文档不生成编译照常走。32 位支持则要先试一次再说写个int main(){return 0;}用gcc -m32编一下能过就说明crt1.o齐全编 32 位目标没问题过不了就得先补 32 位运行库否则后面--disable-multilib也救不了目标库。3. 从 configure 到 bootstrap把 gcc-3.3.6 编成能跑的本机编译器前面把边界划完了这一章是完整的动手链路。整个过程分三步准备目录与参数、跑三阶段自举、安装并做四级验证。每一步都有明确的失败点按顺序走出了问题好定位。3.1 源码目录与构建目录分开configure 参数逐项过一遍老版本的 gcc 允许在源码目录里直接编但我不推荐。源码目录和构建目录分开出问题的时候可以只删构建目录重来源码包不用重新解压。这个习惯在反复试参数的时候能省掉大量时间。# 准备三个目录源码解压位置、构建工作区、最终安装前缀 mkdir -p /opt/src /opt/build/gcc-3.3.6 /opt/gcc-3.3.6 tar -xzf gcc-3.3.6.tar.gz -C /opt/src cd /opt/build/gcc-3.3.6 # 关键用老宿主编译器或者给现代 gcc 加降级开关 export CCgcc -stdgnu89 /opt/src/gcc-3.3.6/configure \ --prefix/opt/gcc-3.3.6 \ --enable-languagesc,c \ --disable-multilib \ --disable-nls \ --enable-shared \ --enable-__cxa_atexit \ --with-local-prefix/opt/gcc-3.3.6 \ --program-suffix-3.3.6逐项说清楚因为后面每个报错都跟这几行有关。--prefix一定不能写/usr写进去就是拿这套老编译器覆盖系统自带的装完系统里其他软件全编不过属于最难回滚的一种翻车。--program-suffix-3.3.6是配套的保险让产物叫gcc-3.3.6而不是gcc和系统编译器共存。--with-local-prefix默认值是/usr/localgcc 会把它塞进头文件搜索路径等于让老编译器去读新系统的/usr/local/include经常引发莫名其妙的类型冲突显式指到自己的前缀下最干净。--disable-multilib关掉 32/64 位双份库只在确实需要同时产出两种位宽时才打开。--enable-shared让libgcc_s.so生成出来老代码里有些 C 异常处理依赖它但如果你的目标是纯静态交付加--disable-shared能让安装目录小一圈也少一类运行时找不到库的故障。--enable-__cxa_atexit是 C 静态对象析构的正确注册方式宿主 glibc 得支持现代 glibc 都支持。configure跑完看最后几行正常会打印出thread model、gcc version、以及它探测到的 assembler / linker 版本。这里如果 assembler 版本显示newly created或者探测结果为空说明 binutils 没在 PATH 里先解决再往下走。3.2 make bootstrap 在干什么三阶段自举与内存控制make bootstrap不是一个普通的编译命令它是三轮自我编译加一次比对。理解这一点才能判断哪些报错是真的坏了哪些只是自举过程中的正常噪音。三阶段的分工是这样的stage1 用宿主编译器编出第一套 gcc这一套只用 C 编译器前端能力受限stage2 用 stage1 编出的 gcc 去编第二套这一套才是完整功能stage3 用 stage2 再编一遍最后比对 stage2 和 stage3 产出的目标文件是否一致。一致就说明这套编译器能稳定复现自己的输出编译器本身是可信的。这个比对机制就是 gcc 的后悔药没有它你无法判断一个诡异的代码生成问题到底是源码的锅还是编译器的锅。cd /opt/build/gcc-3.3.6 # 并行度别开太大老 Makefile 的依赖关系不完整 make bootstrap -j2 \ BOOT_CFLAGS-O2 -g0 \ MAKEINFOtrue-j2是保守值。3.3 时代的 Makefile 并行依赖声明不如新版严谨-j8在 stage2 之后的libstdc-v3里经常出现文件还没生成就被依赖的竞态表现是随机位置报找不到某个 .o。宁可慢一点。BOOT_CFLAGS控制自举时用的优化级别-g0去掉调试信息能明显降内存峰值cc1plus编大文件时单个进程吃几百 MB 很常见内存紧的机器上直接 OOM报出来是Internal compiler error或者被系统 kill很容易误判成源码 bug。如果想省磁盘可以把bootstrap换成bootstrap-lean它在 stage1 结束后就删掉中间目标文件。磁盘不是瓶颈的话还是用bootstrap保留中间产物便于出错时回溯。时间预期要有心理准备两核机器上完整走一遍bootstrap几个小时是常态。中途最想放弃的通常是libstdc-v3那一段它编得最慢、报错最多。3.3 安装、自检与 hello world 三级验证make bootstrap成功之后不要急着宣布完事先做一个不落盘的安装测试# 先装到临时目录确认文件布局没问题再正式安装 make install DESTDIR/tmp/gcc336-stage # 检查关键产物是不是都在 ls /tmp/gcc336-stage/opt/gcc-3.3.6/bin/ # 期望看到 gcc-3.3.6、c-3.3.6、cpp-3.3.6、gccbug-3.3.6 # 确认没问题再真装 make installDESTDIR这一步看着多余实际能提前暴露一类问题某些老版本的安装规则会因为前缀路径不存在而中途失败正式装到一半停下来系统里留半套文件非常难清理。用临时目录先跑一遍失败就删掉重来成本是几分钟。装完之后做三级验证缺一级都不算通过GCC/opt/gcc-3.3.6/bin/gcc-3.3.6 # 第一级编译器自身身份 $GCC -dumpmachine # 期望输出 x86_64-unknown-linux-gnu 或 i686-pc-linux-gnu $GCC -dumpversion # 期望输出 3.3.6 $GCC -v # 看 configure 时的那一行参数是否和你写的一致 # 第二级搜索路径没串味 $GCC -print-search-dirs # 重点看 libraries 一行里有没有出现宿主的 /usr/lib # 第三级真的能产出可执行文件 cat /tmp/hello.c EOF #include stdio.h int main(void) { printf(built by %s\n, __VERSION__); return 0; } EOF $GCC -O2 /tmp/hello.c -o /tmp/hello /tmp/hello-print-search-dirs那一行最容易被忽略但它能提前发现一类隐性污染如果 libraries 里混进了宿主机的/usr/lib你编出来的程序会静默地链接到新版本 glibc 的符号跑起来没问题拿到老目标机上直接报GLIBC_2.14 not found。这种问题在开发机上永远复现不了是最坑的一类。正规做法是在configure时就加--with-sysroot或者干脆用 chroot 把整个构建环境隔离掉。第三级验证里__VERSION__能打印出编译器的版本字符串等于顺手确认了它用的是自己而不是宿主编译器。输出里出现3.3.6才算闭环。4. 交叉编译落地用 3.3.6 造一套 arm 或 mips 目标编译器本机编译器只是练手3.3.6 真正的战场是交叉工具链。老设备的固件更新、老内核的重建、某些只在特定 GCC 版本上才能编过的第三方库都要靠它。交叉编译比本机构建多两个变数三元组怎么定以及目标 libc 什么时候进来。4.1 build、host、target 三个三元组怎么定这三个参数的名字容易混我用一句人话固定它们--build是你在哪台机器上敲命令--host是编出来的编译器将来在哪台机器上跑--target是它生成的代码在哪台机器上跑。绝大多数交叉编译场景是前两个相同、第三个不同也就是在同架构机器上生成另一架构的编译器/opt/src/gcc-3.3.6/configure \ --buildi686-pc-linux-gnu \ --hosti686-pc-linux-gnu \ --targetarm-elf \ --prefix/opt/arm-elf \ --with-gnu-as \ --with-gnu-ld \ --without-headers \ --with-newlib \ --enable-languagesc \ --disable-threads \ --disable-shared \ --disable-libssp \ --disable-nls \ --with-local-prefix/opt/arm-elf三元组拼法是架构-厂商-操作系统。厂商段在老版本里比较随意pc、unknown都能过操作系统段直接决定后端走哪条代码路径elf表示裸机、linux-gnu表示带 Linux 系统调用约定两者生成的启动代码完全不一样写错不会报错只会在目标板上跑飞。--with-gnu-as和--with-gnu-ld是告诉 gcc 用 GNU 系的汇编器和链接器交叉场景几乎必开因为目标板厂商提供的原生工具链通常买不到、也装不上。--disable-libssp在 3.3.6 上没有实际作用栈保护库是 3.4 才引入的写上纯粹是习惯动作去掉也干净。--with-local-prefix在交叉场景下更重要它决定 gcc 会不会去翻宿主的/usr/local/include。交叉编译器一旦读到宿主的头文件产出的二进制就会带宿主的类型定义目标板上必然出问题。4.2 两段式构建先--without-headers出 all-gcc再补 libc交叉工具链不能一把梭编完原因是循环依赖gcc 的完整编译需要目标 libc 的头文件而目标 libc 的编译又需要 gcc。破解办法是分两段第一段先出一个没有 libc 也能跑的最小编译器。--without-headers加--with-newlib的组合告诉构建系统先别指望目标 libc 的头文件libgcc需要的极少量声明我这边兜着。第一段只编all-gcc不碰目标库cd /opt/build/arm-elf-gcc make all-gcc -j2 MAKEINFOtrue make install-gccall-gcc这个目标只编编译器主体跳过libgcc、libstdc等目标库所以不会去要目标 libc 的头文件。install-gcc只装编译器本身。跑完之后/opt/arm-elf/bin/下应该出现arm-elf-gcc和arm-elf-cpp这时候它是不能用的——任何包含标准头的代码都编不过。接着编目标 libc。裸机场景常配 newlib这一步的重点是让 newlib 用自己的arm-elf-gcc来编export PATH/opt/arm-elf/bin:$PATH cd /opt/build/newlib-arm-elf /opt/src/newlib-1.14.0/configure \ --targetarm-elf \ --prefix/opt/arm-elf \ --disable-newlib-supplied-syscalls \ --enable-newlib-io-long-long make -j2 make install--disable-newlib-supplied-syscalls是关键开关。默认 newlib 会提供一套系统调用桩函数里面大多直接返回错误在真实硬件上跑起来表现是 printf 没有任何输出。关掉之后由你的板级支持代码提供_write、_sbrk这一组底层实现串口输出才通。libc 装好之后回到 gcc 的构建目录跑第二段这次把libgcc补齐cd /opt/build/arm-elf-gcc make all -j2 MAKEINFOtrue make install第二段之所以能跑通是因为目标 libc 的头文件已经在/opt/arm-elf/arm-elf/include里了libgcc编译时能拿到需要的声明。4.3 与 binutils、sysroot 的拼接顺序交叉工具链里 binutils 必须先于 gcc 装好因为 gcc 的configure会去探测目标的汇编器和链接器探测不到就退化成一个半残废的编译器。顺序是死的binutils → gcc 第一段 → libc → gcc 第二段。# 第一步binutils 先行它会提供 arm-elf-as / arm-elf-ld cd /opt/build/binutils-arm-elf /opt/src/binutils-2.15/configure \ --targetarm-elf \ --prefix/opt/arm-elf \ --disable-nls make -j2 make install # 确认工具链前缀齐了再回去编 gcc /opt/arm-elf/bin/arm-elf-as --version /opt/arm-elf/bin/arm-elf-ld --versionarm-elf-as --version打出来的版本号要记下来这是后面排查问题的锚点。同一套工具链里 binutils 和 gcc 的版本组合是有讲究的2.15 配 3.3.6 属于那个年代的常见搭配换成 2.17 可能因为 ELF 段属性处理变化导致libgcc链接告警换成 2.13 又可能不认新的重定位类型。sysroot 是另一件要提前决定的事。目标是跑 Linux 的设备时除了编译器还要有目标的 libc 头文件和库把这些文件放在一个目录下configure时用--with-sysroot/opt/arm-linux/sysroot指过去编译器就会只在这个目录里找目标库不会去翻宿主的。本机编译器那章提到的搜索路径污染问题在交叉场景下靠 sysroot 彻底解决。5. 避坑gcc-3.3.6 构建时最常见的六处翻车下面这些是我在实际构建里反复遇到的按报错出现的阶段分两组每条都按现象、原因、解决写。遇到不在列表里的报错先别急着搜对照 2.2 和 2.3 检查一遍环境版本八成能对上号。5.1 编译期报错老代码撞上现代宿主编译器现象一cc1: error: expected , ,, ;, asm or __attribute__ before xxx报错位置全在gcc/c-*或者libstdc-v3的源文件里报了几百条。原因宿主 gcc 版本太新。3.3.6 的源码里有 KR 风格的函数定义和大量隐式 int 声明4.8 之后这些直接判错而不是警告。解决优先换老宿主编译器换不了就在CC上挂-stdgnu89 -fgnu89-inline。C 部分还要额外挂-fpermissive让老式 C 写法降级成警告export CCgcc -stdgnu89 -fgnu89-inline export CXXg -fpermissive -stdgnu98注意CXX这个变量也要设否则 stage2 编libstdc时会回退到默认的g前面所有努力白费。现象二error: __builtin_va_list undeclared或者#include stdarg.h之后类型不认。原因宿主 glibc 的头文件把va_list定义成了编译器内建类型而 3.3.6 期望的是一个结构体。这是两代工具链的 ABI 差异无法通过加宏解决。解决换构建环境用带有同期 glibc 的旧容器。这是少数几个真正没法用参数绕过去的问题之一硬碰只会浪费一天。现象三makeinfo: Unknown command anchor或者itemx相关的语法错误发生在编译文档阶段。原因texinfo 5.0 之后收紧了语法检查3.3.6 的.texi文件里有大量当时合法、现在非法的写法。解决make时传MAKEINFOtrue直接跳过文档生成编译产物不受影响。另一种做法是touch一遍gcc/doc/*.info让 make 认为文档已是最新。5.2 链接与安装期报错crt 文件、路径污染与半装状态现象四cannot find crt1.o、cannot find -lc出现在本机构建的最后阶段或者第二级验证里。原因宿主缺 32 位运行库。3.3.6 默认按宿主默认位宽走如果三元组里写了i686但系统只有 64 位库就会找不到 32 位的启动文件。解决要么补齐 32 位开发包要么把三元组统一成x86_64-unknown-linux-gnu再配合--disable-multilib。两者选一不要既写 32 位三元组又关了 multilib那等于主动制造不匹配。现象五装完之后系统里其他软件突然编译失败报大量头文件类型冲突。原因--prefix指到了/usr或者没加--program-suffixgcc-3.3.6被装成了系统的gcc。这是最严重的翻车影响面超出这个项目本身。解决前缀一律指到私有目录加上--program-suffix。已经装坏了的先看make install打出来的文件清单逐条删。DESTDIR安装测试那个步骤就是为这个准备的走一遍能提前挡掉。现象六configure报cannot guess build type; you must specify one。原因源码包里的config.guess太老认不出当前的机器三元组通常出现在较新的 CPU 或者非主流发行版上。解决显式传--buildx86_64-unknown-linux-gnu不要指望它自己猜。三元组的具体值可以用宿主机的gcc -dumpmachine打出来照着抄就行。6. 用-print-search-dirs和 bootstrap 比对给自己留一层验证交付之前我习惯做一个三分钟的自检比漫无目的地跑测试用例有效得多。核心思路是让编译器自己把内部状态说出来然后人工核对这些状态是否和预期一致。所有参数都在gcc的-print-*系列里不需要额外工具。#!/bin/bash # 交付前自检四个输出缺一项就说明这套工具链不该发出去 GCC${1:-/opt/gcc-3.3.6/bin/gcc-3.3.6} echo 1. 目标三元组 $GCC -dumpmachine echo 2. 版本 $GCC -dumpversion echo 3. 搜索路径关注 libraries 里是否混入宿主 /usr/lib $GCC -print-search-dirs | sed -n /^libraries/p echo 4. 内建宏确认目标架构的宏定义正确 $GCC -dM -E - /dev/null | grep -E __i386__|__x86_64__|__arm__|__mips__第三个输出是重点。libraries那一行如果能找到宿主的/usr/lib说明--with-local-prefix或者 sysroot 没配对这时候产出的二进制在老目标机上有概率报GLIBC_2.14 not found但在你的开发机上一切正常属于最难查的一类问题。第四个输出用来确认架构宏比如目标是 ARM 却在里面看到了__x86_64__说明三元组写错或者后端选错这条路后面全是错的。第二个要看的验证点是make bootstrap最后那次比对有没有真的执行。日志里应该出现类似Comparing stages 2 and 3的段落并且没有任何comparison failed的行。这段日志如果被-j2的输出冲掉了可以单独跑make compare补上。比对通过意味着 stage2 和 stage3 产出完全一致编译器能复现自己的输出这是判断这套工具链能不能长期使用的硬指标。还有一个我常用的手法拿同一份源码分别用这套 3.3.6 和宿主编译器各编一次objdump -d出来比函数大小。新版编译器普遍能把老代码编得更小如果 3.3.6 编出来的函数反而更大通常说明-O2没生效回去查BOOT_CFLAGS有没有被环境变量覆盖掉。从那以后我每次交付工具链不管多急-print-search-dirs那一步都强制走一遍宁可多花三分钟。有一次省了这一步把带宿主路径的编译器发到现场三天后在一片板子上全线报错回滚的成本是几倍。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站