五指山市网站建设_网站建设公司_论坛网站_seo优化
2025/12/26 15:58:22 网站建设 项目流程

用TCC实现C语言编译器自举的全过程解析

最近在折腾一个嵌入式项目时,又翻出了那个老朋友——Tiny C Compiler(TCC)。这玩意儿你可能没怎么听过,但它真是个“小钢炮”:2万行C代码、启动飞快、不依赖复杂工具链,甚至能直接运行.c文件。更关键的是,它是理解编译器自举最干净的实验场。

说到“自举”,听起来有点玄乎,其实就一句话:用一种语言写它自己的编译器。比如Clang是用C++写的,Python解释器是用C写的,那TCC呢?它也是用标准C写的。问题来了:既然它是C写的,谁来编译它?总不能自己编译自己吧?

还真可以——只要我们先有个“种子”。


咱们今天就动手走一遍这个过程:从零开始,用一个预编译好的TCC去编译TCC源码,生成一个新的TCC,再让它反过来编译自己。整个流程不碰GCC、不依赖Visual Studio,纯粹靠TCC自己闭环完成。

别担心,这不像你想的那么抽象。我会带你一步步敲命令、看输出、解决报错,就像两个开发者坐在电脑前一起调试那样。


先说清楚我们要什么:

  • 一个能跑的tcc.exe—— 这是我们的“种子”
  • TCC的原始源码 —— 我们要拿它造出“第二代”

去官方地址下载最新稳定版:

http://download.savannah.gnu.org/releases/tinycc/

Windows用户找这个包:

tcc-0.9.27-win32-bin.zip

解压到比如C:\tcc_seed,你会看到几个关键部分:

tcc.exe ← 编译器本体 include\ ← stdio.h、stdlib.h 等头文件 lib\ ← libc库和链接支持 libtcc.dll ← 动态库,运行时要用

把这个路径加进系统PATH,然后打开命令行试试:

tcc -v

如果回显了版本号,说明“种子”已经种下了。


接下来搞源码。还是同一页面,下这个:

tcc-0.9.27.tar.bz2

解压出来放到C:\tcc_src。重点看看里面的几个文件:

文件名作用
tcc.c主程序入口,main函数在这
tccpp.c预处理器,处理 #include 和 #define
i386-gen.cx86指令生成模块
i386-link.c负责链接目标文件
tccelf.cELF/PE格式支持
libtcc.c提供API接口,可用于嵌入其他程序

这些就是TCC的核心拼图。现在我们要做的,就是用现有的tcc.exe把它们拼成一个新的可执行文件。


C:\tcc_src目录,执行这条命令:

tcc -o tcc_new.exe ^ tcc.c tccpp.c i386-gen.c i386-link.c ^ tccelf.c tccrun.c libtcc.c ^ -lws2_32 -lgdi32 -lcomdlg32

解释一下:

  • -o tcc_new.exe:输出新编译器
  • 列出所有核心源文件
  • Windows下需要链接一些GUI和网络库,哪怕你不写图形界面也得带上,否则链接失败

等几秒,没报错就说明成功了。你现在手里有了一个由TCC自己源码编译出来的TCC,叫tcc_new.exe

你可以对比下大小:原始tcc.exe可能400KB左右,新生成的也差不多。虽然没有优化,但功能完整。

当然,TCC源码里自带了个批处理脚本帮你自动化这事:

cd win32 make.bat

它会调用当前环境中的tcc来重新构建自己,默认生成tcc.exe并放在同目录。不过建议初学者先手动敲一遍上面那条长命令,更能体会“我在编译一个编译器”的感觉。


现在验证下新编译器能不能干活。

写个最简单的测试程序test.c

#include <stdio.h> int main() { printf("Hello from self-built TCC!\n"); return 0; }

用我们刚造出来的编译器来编译它:

tcc_new.exe test.c -o test.exe

运行:

test.exe

看到输出:

Hello from self-built TCC!

✅ 搞定!你的新编译器不仅能编译别人,还能正确处理标准库和链接流程。

但这还不算真正意义上的“自举”。真正的里程碑是:让这个新生的编译器去编译它自己


这才是重头戏。

想象一下:你现在有一个婴儿版TCC,它是被“父辈”生出来的。现在你要让它尝试“再生一个自己”。如果成功了,那就意味着它可以无限迭代下去——不需要外部帮助,完全自治。

但这里有个坑:tcc_new.exe不知道stdio.h在哪,也不知道libc怎么链接。它不像GCC那样内置了一堆搜索路径。

所以我们得给它配一套“生存环境”。

新建个目录:

C:\tcc_standalone

把下面这些东西复制进去:

copy tcc_new.exe C:\tcc_standalone\tcc.exe copy C:\tcc_seed\libtcc.dll C:\tcc_standalone\ xcopy /E C:\tcc_seed\include C:\tcc_standalone\include\ xcopy /E C:\tcc_seed\lib C:\tcc_standalone\lib\

这样你就得到了一个独立可发布的TCC发行版,其中的tcc.exe是你自己编译出来的。

切换过去:

cd C:\tcc_standalone

然后试着用它编译原始源码:

.\tcc.exe -o tcc_bootstrap.exe ^ C:\tcc_src\tcc.c ^ C:\tcc_src\tccpp.c ^ C:\tcc_src\i386-gen.c ^ C:\tcc_src\i386-link.c ^ C:\tcc_src\tccelf.c ^ C:\tcc_src\tccrun.c ^ C:\tcc_src\libtcc.c ^ -lws2_32 -lgdi32 -lcomdlg32

如果顺利生成tcc_bootstrap.exe,恭喜你——你完成了TCC的自举!

你可以继续用这个新生成的再去编译下一版,理论上可以一直递归下去。虽然每一版都长得一样,但从工程意义上讲,这个编译器已经实现了自我复制能力


这种“自己编译自己”的能力,不只是炫技。它背后有几个非常实际的意义:

首先,脱离重型工具链。很多嵌入式或恢复环境里没有GCC,也没有MSVC,但你只要带一个几百KB的TCC上去,就能现场编译C代码,甚至做JIT执行。

其次,验证语言完备性。C语言能写出一个完整的C编译器,说明它的表达力足够强,能覆盖系统编程的所有需求——内存管理、符号解析、代码生成、链接控制,全都Hold得住。

再者,TCC本身还有一些很酷的特性,比如:

tcc -run hello.c

可以直接运行C源文件,不用先编译成exe。这在写临时工具或者教学演示时特别方便。

还有,它支持静态链接后生成极小的二进制文件。我试过一个空main函数,编译出来才4KB,比很多Python打包出来的还小。

另外,通过libtcc接口,你还能把TCC当成库嵌入到别的程序里。比如你在Lua或Python中动态生成一段C代码,当场编译并调用,实现高性能扩展——类似JIT的效果,但更轻量。


当然,TCC也不是万能的。

它对C99的支持有限,比如变长数组(VLA)有些场景会出问题;优化级别只有-O0-O1,没法跟GCC-O2/-O3比性能;错误提示也比较简陋,不像Clang那样会给你画波浪线告诉你哪错了。

但正是这些“不完美”,让它更适合教学和实验。你不需要面对几十万行代码的LLVM,也不用啃GNU C复杂的宏体系。TCC的代码几乎是直白的,每个模块职责清晰,读起来像一本活教材。

顺便提一句,TCC的作者是 Fabrice Bellard,这位大佬你还真不能小瞧——FFmpeg、QEMU、BPG 图像格式、甚至圆周率计算的世界纪录都是他搞的。TCC只是他某个周末的副业作品……汗。


顺带一提,TCC还支持交叉编译。比如你想在x86机器上生成ARM平台的可执行文件,可以用:

tcc -target arm-linux-gnu-eabi -o demo_arm demo.c

前提是你有对应的头文件和库路径配置好。虽然生态不如GCC丰富,但在资源极度受限的设备上仍有潜力。


过程中你可能会遇到几个常见问题,列出来帮你避坑:

找不到 stdio.h?

报错:“include file ‘stdio.h’ not found”

原因很简单:TCC找不到include目录。

解决办法有两个:

一是把include文件夹放在当前目录,或者和tcc.exe放一起;

二是手动指定路径:

tcc -I C:\tcc_seed\include test.c

链接时报 undefined reference to WinMain?

这是Windows特有的坑。系统默认把你当GUI程序处理,但你写的是控制台程序。

加上这个参数就行:

tcc -mconsole test.c

明确告诉它:老子要的是命令行窗口。

提示 libtcc.dll 缺失?

运行时报“找不到 libtcc.dll”?

别慌,把libtcc.dll和你的可执行文件放同一个目录就行。Windows加载DLL优先查本地路径。


回头看这一整套流程,其实不只是技术操作,更像是一种哲学实践

我们用了三个阶段:

  1. 借力启动:靠一个外部的“种子”编译器起步
  2. 构建自身:用自己的源码造出第二个实例
  3. 闭环演化:让新生代具备再次构建自身的能力

这就像生物的繁殖机制——程序也能“生孩子”,而且孩子长大后还能再生下一代。

在操作系统开发、固件刷写、安全研究等领域,这种能力极其重要。比如你在一个裸机上跑 recovery 系统,没法联网装GCC,但如果预装了一个TCC,就可以现场编译修复工具,实现自救。

这也是为什么很多自制操作系统教程都会拿TCC当首选编译器:轻、快、可控、可移植。


最后总结一下:

  • TCC是一个极简主义的C编译器,适合学习与嵌入
  • 自举不是神话,而是可以通过几步命令真实达成的过程
  • 完成自举意味着该语言工具链达到了某种“成熟态”
  • 即使不用于生产,这个过程也能极大加深你对编译、链接、运行三阶段的理解

如果你对这类底层机制感兴趣,不妨试试下一步挑战:

  • 修改tcc.c,加个自定义关键字(比如let
  • 或尝试把TCC移植到另一个平台(如RISC-V模拟器)
  • 甚至试着让它支持一点点C++语法(别笑,有人真做过)

这些都不是遥不可及的事。而一切的起点,就是你现在手里的这个几百KB的小程序。

科哥技术微信:312088415
欢迎交流编译原理、自制语言、嵌入式开发相关话题

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询