Упрощённая модель Fil-C
- title
- Упрощённая модель Fil-C
- type
- summary
- summary
- Разбор того, как Fil-C обеспечивает безопасность памяти в C/C++: теневые AllocationRecord, параллельные невидимые байты для кучи и GC
- parent
- fil-c
- tags
- c, cpp, memory-safety, compilers, garbage-collection
- sources
- simplified-model-of-fil-c
- created
- 2026-04-18
- updated
- 2026-07-29
- lang
- ru
- translation_of
- simplified-model-of-fil-c
- source_updated
- 2026-07-29
- translated
- 2026-09-01
- translator
- lllm/antigravity/gemini-3.7-flash-medium
Разбор от Peter Cawley проекта Fil-C - реализации C/C++ с гарантией безопасности памяти. Настоящий Fil-C переписывает LLVM IR; упрощённая модель Cawley представлена как преобразование исходного кода C в C, о котором проще рассуждать и от которого рукой подать до релизной версии.
Основной механизм: теневые capability
Каждая локальная переменная указательного типа получает переменную-компаньона типа AllocationRecord*:
T1* p1; T1* p1; AllocationRecord* p1ar = NULL;
Где AllocationRecord хранит границы диапазона:
struct AllocationRecord {
char* visible_bytes;
char* invisible_bytes;
size_t length;
};
Обычные операции с указателями перемещают компаньона вместе с указателем: p1 = p2 превращается в p1 = p2, p1ar = p2ar. Адресная арифметика сохраняет того же компаньона (p1 = p2 + 10 сохраняет p1ar = p2ar). Приведение из целочисленного типа обнуляет компаньона.
Вызовы функций передают компаньона рядом с каждым аргументом-указателем, а конкретные функции стандартной библиотеки заменяются версиями из Fil-C: malloc становится filc_malloc, free - filc_free, и так далее.
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};
}
Три выделения памяти на каждый вызов malloc: сама запись, видимые пользователю байты и параллельный "невидимый" массив того же размера, что и пользовательский буфер.
Проверка границ при разыменовании
Каждое разыменование разворачивается в очевидные проверки:
assert(p1ar != NULL);
uint64_t i = (char*)p1 - p1ar->visible_bytes;
assert(i < p1ar->length);
assert((p1ar->length - i) >= sizeof(*p1));
x = *p1;
Теневая память для указателей в куче
Самая изящная деталь: когда указатели лежат в куче, компилятор не может просто завести локального компаньона. Вместо этого invisible_bytes выступает параллельным массивом с такой же индексацией, как у visible_bytes, но с типом элементов AllocationRecord*. Если указатель лежит по адресу visible_bytes + i, его capability лежит по адресу invisible_bytes + i. Для корректного доступа i должен быть выровнен по sizeof(AllocationRecord*).
Чтение или запись указателя через другой указатель выполняет два чтения/записи: одно для значения, второе для компаньона. Именно поэтому вызов memmove для восьми выровненных байтов ведёт себя иначе, чем восемь раздельных вызовов memmove по 1 байту: выровненная версия также переносит теневую память, а невыровненная - нет.
Сборщик мусора (GC)
filc_free освобождает visible_bytes и invisible_bytes, но не саму структуру AllocationRecord. Этим занимается сборщик мусора, который проходит по цепочкам AllocationRecord и освобождает недостижимые. В продакшене Fil-C использует FUGC - параллельный конкурентный инкрементальный сборщик; упрощённая модель может обойтись stop-the-world.
GC берёт на себя две дополнительные задачи:
- При освобождении недостижимого
AllocationRecordвызываетсяfilc_free. Соответственно, забытыйfreeбольше не приводит к утечке - память подчистит GC. Явный вызовfreeлишь ускоряет освобождение. - Если
AllocationRecordимеет нулевую длину (length == 0), указатели на него переписываются так, чтобы указывать на единую каноническую запись нулевой длины. Это даёт безопасную обработку use-after-free.
Когда сборщик мусора уже есть, появляется соблазн переложить на него больше работы. Fil-C так и поступает: если у локальной переменной берётся адрес и компилятор не может доказать, что он не утекает наружу, переменная перемещается в кучу. Парный вызов free не требуется - всё соберёт GC.
Сложности продакшен-реализации
Упрощённая модель опускает четыре сложных аспекта настоящего Fil-C:
- Потоки.
filc_freeне может сразу освобождать память - другой поток в этот момент может выполнять чтение. Атомарные операции с указателями требуют отдельной магии, поскольку базовое преобразование разбивает одно чтение на два (значение + компаньон), нарушая атомарность. - Указатели на функции. Дополнительное поле в
AllocationRecordпомечает исполняемый код. Вызовы проверяютp1 == p1ar->visible_bytesи этот флаг. Для защиты от атак через несоответствие типов (type confusion) ABI вызовов унифицирован: каждая функция принимает единственныйAllocationRecordс упакованной структурой аргументов. - Оптимизация памяти. Велик соблазн выделять
invisible_bytesлениво, объединять запись и видимые байты в одно выделение памяти и переиспользовать метаданные базового аллокатора. - Оптимизация производительности. Устранение лишних проверок границ (bounds-check elimination) и схожие приёмы для снижения накладных расходов.
Когда это использовать
Четыре сценария от Cawley:
- Большие существующие кодовые базы на C/C++, которые, вероятно, безопасны, но это не доказано. В них можно пойти на затраты по GC и производительности ради безопасности, например, как промежуточный шаг перед переписыванием на Rust/Go/Java.
- Санитайзер - аналог ASan с более сильными гарантиями для поиска ошибок.
- Безопасное вычисление на этапе компиляции в языках, где compile-time и runtime используют один и тот же язык (в качестве примера приводится Zig), даже если рантайм остаётся небезопасным.
- Наглядный практический пример концепции pointer-provenance. Компаньон
AllocationRecord*как раз и является provenance. Fil-C наглядно показывает, почему компиляторы не могут в общем случае заменятьif (p1 == p2) { f(p1); }наif (p1 == p2) { f(p2); }: одинаковые битовые представления могут нести разный provenance, и Fil-C передал бы вfразные capability.
См. также
- fil-c - сам проект
- memory-safety-absolutists - полемика вокруг Fil-C: делают ли его гарантии Rust избыточным, и какие двери закрывают несовместимость по ABI и наличие GC
- pointer-provenance - концепция модели памяти, которую Fil-C воплощает на практике
- abi-stability - защита указателей на функции в Fil-C требует унифицированного ABI, что открывает другой взгляд на проектирование ABI
- meta-tracing - ещё одна техника вида "перепишем программу ради нужного свойства", но ради скорости, а не безопасности
- raii / stroustrup-memory-leaks - вариант той же цели на уровне договоренностей: Страуструп говорит "пишите код без утечек", а Fil-C обеспечивает это механически, когда на дисциплину полагаться нельзя