第 1 章:5 分钟跑起来
上一章讲了 goc 是什么。这一章把它跑起来。
目标:5 分钟内在这台机器上编译并运行第一个 C 程序,然后交叉编出一个 Linux 可执行文件。
环境要求
| 要求 | |
|---|---|
| 必需 | Go(构建工具链)、bash |
| 可选 | Python 带 unicorn 绑定(跑 Linux 腿验证用)、MinGW 的 objdump(看 DLL 依赖用) |
goc 本身只用 Go 写,没有 C 依赖。
第一步:编译工具链
一条命令产出四个可执行文件:
⚠️
build.sh走 Go 工具链,第一次会拉依赖。如果go不在 PATH 里,先确认go version能跑通。
第二步:第一个程序
直接编译并运行——注意不需要单独的编译+链接两步,goc 一次吐出可执行文件:
输出:
run 会把程序编译到临时目录再执行,并把程序的退出码透传出来——所以它可以直接用在脚本里做回归测试。
其它几种用法:
-c 是 gcc 的意思:只编译,不链接,产出真正可重定位的目标文件(PE 目标下是 COFF,Linux 目标下是 ELF64)。所以完整的两步构建和 gcc 一样:
也可以混着来——命令行里只要出现一个 .o,整个命令就走链接阶段,.c 会先被编译成临时的 .o 再一起链接:
-S 出来的汇编值得看一眼,它带个自明的头:
三个细节:
_start而不是mainCRTStartup——没有 CRT 启动代码。goc 自己造入口桩:取 argc/argv、对齐栈、call main、把返回值交给__goclib_exit。extern ExitProcess, kernel32——归属 DLL 直接写在原型的逗号后面,编译期收集,不需要单独的中心表。- 没有 msvcrt——写文件走
GetStdHandle+WriteFile。
第三步:看一眼产物体积
这是 goc 最直观的卖点:
6656 字节。对比 gcc 编同样的程序(同样 MSYS2 ucrt64,-O2,加不加 -static 结果一样):
小83%。而且 gcc 那 38989 里绝大部分是 CRT 启动代码和 printf 家族——不是你的代码。
第四步:交叉编译 Linux
改一个参数就行:
产出静态 ELF64。这个头跟 Windows 版完全不同——它没有导入表:
调用约定也从 Win64 切到了 SysV:参数从 rcx/rdx/r8/r9 变成 rdi/rsi/rdx/rcx/r8/r9,且没有 32 字节 shadow space。库那边同样按 __linux__ 分支重编:写走 write syscall,堆分配用 brk 做 bump allocator,退出转 exit_group。
💡 Windows 上没法 exec ELF,所以验证 Linux 产物需要 QEMU。goc 自带
tools/ucrun.py(Unicorn,即 QEMU 的 TCG 核心)跑真指令语义。CI 的 Ubuntu job 则直接在真实内核上 exec这些 ELF——那才是最硬的证明。
三个必踩的坑
坑 1:-o 的语义照抄 gcc
路径不存在时,-o 表示的是输出文件名,不是目录。
所以要输出到目录,必须先建:
这不是文档吹毛求疵——README 记载 run_tests_linux.sh 当年就因为少了 mkdir -p,第一个例子写出个名叫 bin/goc-out 的文件,后面所有例子一律 Not a directory。
坑 2:zip 必须整目录解压
Release 里的 zip 是开箱即用的工具链目录,但 goc 在运行时要从磁盘读 C 源码(goclib/)。只把 goc.exe 拷出来会立刻失败:
它不静默出错,但也编不了任何东西。查找顺序在 goc/libfs.go:
想让多份编译器共用一份库,设 GOCLIB_PATH 指向它即可。
坑 3:printf 的宽度被忽略
宽度一概忽略,精度只有 %f 和 %g 认。这是与标准 C 明确的差异,goclib/stdio.c 里 vfmt 的注释写明了是有意为之。
支持的格式是%d %i %u %o %x %X %s %c %f %g %p %%,长度修饰符 l h L z j t 会被解析(所有变参槽位都是 8 字节,解析掉即等价)。没有 %e %a %n。
如果不想踩这个坑,用内建的 print——它按静态类型分派,不走格式化:
跑测试
想验证整条链路,跑套件:
run_tests.sh 一次做八件事:构建 → 找带 unicorn 的 Python → Windows 腿 ×3(-O0/-O1/-Os)→ Linux 腿 ×3 → 委托 goa 套件 → 三个模块单测。
有个设计值得注意:-O1/-Os 腿拿的是和 -O0 同一份 golden。
也就是说优化 pass 只能"不许改变可观察输出",而不是"重新生成一份新的期望"。这个护栏比"优化后再更新 golden"严格得多——后者会让优化 bug 悄悄变成新基线。
⚠️ 找不到可
import unicorn的 Python 时,脚本会大声跳过 Linux 腿并打 WARNING,而不是假装通过。这是有意的。
两个环境变量:GOC_PYTHON=/path/to/python 指定带 unicorn 的解释器;GOC_SKIP_MSGBOX=1 跳过 GUI 用例(需要交互式桌面,CI runner 没有)。
多文件
多个 .c 各自是独立翻译单元,宏、typedef、struct 标签互不干扰:
static符号只在本文件可见,重名时自动改成每文件唯一的内部名- 两个文件都定义的同名外部符号 → 重复定义错误
⚠️ 但不能消费
.o。goc 没有独立的链接阶段,别指望先编成目标文件再链接。
下一步
现在你能编译、运行、交叉编译了。下一章写第一个 Windows GUI 程序——用 MessageBox 弹个框,顺便验证产物真的只依赖系统 DLL。
本章命令与数字核对于 2026-10-06,goc 提交
f200cd1。用goc --help看完整选项;-O* -Wall -W* -std -m* -g -static -pthread -f* -s -l -L里的多数选项接受但忽略(goc 是单遍编译器)。