Fil-C 简化模型揭秘:内存安全实现、GC 机制及适用场景全解析

张开发
2026/4/19 1:39:40 15 分钟阅读

分享文章

Fil-C 简化模型揭秘:内存安全实现、GC 机制及适用场景全解析
Fil-C 简化模型全解析内存安全实现、GC 机制及适用场景揭秘本文于 2026 年 4 月 17 日发布于 [corsix.org](/)。近期关于 [Fil-C](https://fil-c.org/) 的讨论热度颇高。Fil-C 宣称是 C/C 的内存安全实现方案。你可通过 [详细细节](https://fil-c.org/invisicaps) 了解其实现方式但对初次接触 Fil-C 的人而言展示简化版本很有价值因为理解简化版后再理解生产级版本会容易许多。Fil-C 简化模型的代码转换真正的 Fil-C 有重写 LLVM IR 的编译器通道而简化模型是对 C/C 源代码自动重写将不安全代码转为安全代码。首次重写在每个函数内部每个指针类型的局部变量会附带 AllocationRecord* 类型的局部变量示例如下| 原始源代码 | Fil-C 转换后 || --- | --- || void f() {T1* p1;T2* p2;uint64_t x;... | void f() {T1* p1; AllocationRecord* p1ar NULL;T2* p2; AllocationRecord* p2ar NULL;uint64_t x;... |AllocationRecord 结构类似如下struct AllocationRecord {char* visible_bytes;char* invisible_bytes;size_t length;};对指针类型局部变量的简单操作会被重写以同时移动 AllocationRecord*| 原始源代码 | Fil-C 转换后 || --- | --- || p1 p2; | p1 p2, p1ar p2ar; || p1 p2 10; | p1 p2 10, p1ar p2ar; || p1 (T1*)x; | p1 (T1*)x, p1ar NULL; || x (uintptr_t)p1; | x (uintptr_t)p1; |当指针作为参数传递给函数或从函数返回时代码会被重写以同时包含 AllocationRecord* 和原始指针。对特定标准库函数的调用会额外重写为调用 Fil-C 版本的函数。综合起来有如下转换| 原始源代码 | Fil-C 转换后 || --- | --- || p1 malloc(x);...free(p1); | {p1, p1ar} filc_malloc(x);...filc_free(p1, p1ar); |简化版filc_malloc 的实现实际上会进行三次不同的内存分配而不仅仅是请求的那次void* filc_malloc(size_t length) {AllocationRecord* ar malloc(sizeof(AllocationRecord));ar-visible_bytes malloc(length);ar-invisible_bytes calloc(length, 1);ar-length length;return {ar-visible_bytes, ar};}指针解引用与边界检查当解引用指针变量时会使用附带的 AllocationRecord* 进行边界检查| 原始源代码 | Fil-C 转换后 || --- | --- || x *p1;...*p2 x; | assert(p1ar ! NULL);uint64_t i (char*)p1 - p1ar-visible_bytes;assert(i p1ar-length);assert((p1ar-length - i) sizeof(*p1));x *p1;...assert(p2ar ! NULL);uint64_t i (char*)p2 - p2ar-visible_bytes;assert(i p2ar-length);assert((p2ar-length - i) sizeof(*p2));*p2 x; |当存储或加载的值本身是指针时情况更复杂。指针类型的局部变量会由编译器插入附带的 AllocationRecord* 变量一旦指针存在于堆中invisible_bytes 就发挥作用如果 visible_bytes i 处有一个指针那么其附带的 AllocationRecord* 就在 invisible_bytes i 处。为确保对该数组的合理访问i 必须是 sizeof(AllocationRecord*) 的倍数。这部分额外逻辑如下| 原始源代码 | Fil-C 转换后 || --- | --- || p2 *p1;...*p1 p2; | assert(p1ar ! NULL);uint64_t i (char*)p1 - p1ar-visible_bytes;assert(i p1ar-length);assert((p1ar-length - i) sizeof(*p1));assert((i % sizeof(AllocationRecord*)) 0);p2 *p1;p2ar *(AllocationRecord**)(p1ar-invisible_bytes i);...assert(p1ar ! NULL);uint64_t i (char*)p1 - p1ar-visible_bytes;assert(i p1ar-length);assert((p1ar-length - i) sizeof(*p1));assert((i % sizeof(AllocationRecord*)) 0);*p1 p2;*(AllocationRecord**)(p1ar-invisible_bytes i) p2ar; |filc_free 与垃圾回收器filc_free 的实现类似如下void filc_free(void* p, AllocationRecord* par) {if (p ! NULL) {assert(par ! NULL);assert(p par-visible_bytes);free(par-visible_bytes);free(par-invisible_bytes);par-visible_bytes NULL;par-invisible_bytes NULL;par-length 0;}}filc_malloc 进行了三次分配但 filc_free 只释放了其中两次AllocationRecord 对象不会被 filc_free 释放。这个问题通过添加垃圾回收器GC解决。生产级的 Fil-C 有一个 [并行并发增量收集器](https://fil-c.org/fugc)简单模型用停止世界的收集器即可。收集器会遍历 AllocationRecord 对象释放不可达对象还会做两件事1. 释放不可达的 AllocationRecord 时对其调用 filc_free。2. 如果一个 AllocationRecord 的长度为 0任何指向该 AllocationRecord 的指针将被改为指向一个长度为 0 的规范 AllocationRecord。第一点意味着使用 Fil-C 时忘记调用 free 不会导致内存泄漏内存会被 GC 自动释放但调用 free 仍有用可让内存更早释放。第二点意味着对某个对象调用 free 后附带的 AllocationRecord 最终会不可达并被释放。利用 GC 与 Fil-C 版本的 memmove有了 GC 后可让获取局部变量的地址更安全。若编译器发现局部变量地址被获取且无法证明该地址不会超出局部变量生命周期Fil-C 转换会将该局部变量提升为通过 malloc 进行堆分配GC 会处理内存释放。Fil-C 版本的 memmove 采用合理启发式方法任意内存中的任何指针必须完全在任意内存内并且必须正确对齐。这导致移动八个对齐字节的 memmove 与分别移动八个单字节的 memmove 行为不同前者还会移动 invisible_bytes 的相应范围而后者则不会。生产级版本的额外复杂情况生产级版本中还有一些额外的复杂情况- **线程**并发会使 GC 更复杂filc_free 不能立即释放任何东西指针的原子操作也需要额外技巧。- **函数指针**AllocationRecord 中的额外元数据用于表示 visible_bytes 指针是指向可执行代码的指针函数调用 ABI 还需验证类型签名是否正确。- **内存使用优化**可让 filc_malloc 避免立即分配 invisible_bytes也可将 AllocationRecord 和 visible_bytes 合并到一次分配中。- **性能优化**Fil-C 的内存安全以性能为代价可尝试各种技巧挽回损失的性能。Fil-C 的适用场景在有了基本的理解之后我们思考什么时候会想使用 Fil-C1. 有大量看似能正常运行的 C/C 代码但尚未证明其内存安全愿意引入 GC 并承受较大的性能损失以换取内存安全也许作为临时措施直到用 Java、Go 或 Rust 重写代码。2. 可在 Fil-C 下运行代码来查找内存错误就像在 [ASan](https://clang.llvm.org/docs/AddressSanitizer.html) 下运行 C/C 代码一样。3. 若有一种编译时特性很强的语言且编译时语言与运行时语言相同例如 [Zig](https://ziglang.org/)可使用 Fil-C 进行安全的编译时评估即使运行时评估是不安全的。4. Fil-C 是一个具有指针来源的具体系统的有用示例对于喜欢思考 [指针来源](https://www.ralfj.de/blog/2020/12/14/provenance.html) 的人有一定价值。

更多文章