Языки: English | 简体中文 | 繁體中文 | 日本語 | 한국어 | Français | Deutsch | Español | Italiano | Русский | العربية
NeverC поддерживает пользовательские соглашения о вызовах, управляемые данными: вы можете назначить произвольные физические регистры аргументам и возвращаемым значениям любой функции целиком из внешнего плагина или через атрибуты на уровне исходного кода — не меняя компилятор и ни одного определения TableGen.
Традиционные соглашения о вызовах в LLVM зашиты в бэкенд через файлы .td / .inc. Чтобы добавить или изменить одно из них, нужно править исходники компилятора и заново запускать TableGen. NeverC заменяет это моделью, управляемой данными во время выполнения, из двух слоёв:
- Спецификация — короткая строка, которую можно написать вручную, например
gpr:rcx,rdx;ret:rax, — прикрепляется к функции как строковый атрибут"neverc-callconv", либо плагином, либо атрибутом на уровне исходного кода. - Перед генерацией кода хост материализует эту спецификацию в атрибут
"neverc-cc-plan-v1": неизменяемую проверенную таблицу точных мест, привязанную к конкретной схеме цели. Бэкенд читает только план.
Спецификацию пишете вы; плану доверяет бэкенд. Соглашения о вызовах тем самым переезжают из «жёстко зашитых в бэкенд на этапе компиляции» в «управляемые данными внешней политики во время выполнения» — без отказа от проверок.
Спецификация — строка, разделённая точками с запятой. Каждый сегмент состоит из ключа и списка имён регистров через запятую (регистр букв не важен, пробелы допускаются):
gpr:rcx,rdx,r8,r9; xmm:xmm0,xmm1; ret:rax; ret_xmm:xmm0
| Сегмент | Псевдонимы | Значение |
|---|---|---|
args |
Позиционный режим: каждый токен — имя регистра либо stack/mem, назначается аргументам по индексу |
|
gpr |
arg_gpr |
Режим пула: регистры для целочисленных/указательных аргументов, расходуются по порядку; излишек уходит в стек |
xmm |
arg_xmm |
Режим пула: регистры для вещественных/векторных аргументов |
fpr |
Нейтральный к цели псевдоним для xmm |
|
ret_gpr |
ret |
Регистры возврата для целых чисел/указателей |
ret_xmm |
Регистры возврата для вещественных/векторных значений | |
ret_fpr |
Нейтральный к цели псевдоним для ret_xmm |
|
csr |
Пользовательский набор регистров callee-saved (по умолчанию — стандартный набор ABI) |
Любой сегмент можно опустить, неизвестные сегменты игнорируются. Ключи определены ровно один раз в llvm/include/llvm/CodeGen/NeverCCallConv.h, поэтому создатели спецификаций и парсер не могут разойтись.
Режим пула (gpr: / xmm:): целочисленные аргументы по порядку берут регистры из пула gpr; вещественные и векторные — из xmm. Когда пул исчерпан, оставшиеся аргументы уходят в стек.
Позиционный режим (args:): аргумент i использует i-й токен. Каждый токен — это либо имя регистра, либо stack / mem, что принудительно отправляет аргумент в стек:
args:rcx,stack,r8;ret:rax # arg0→rcx, arg1→стек, arg2→r8, возврат→rax
Если args присутствует, он имеет приоритет над gpr / xmm. Токен, называющий неподходящий для типа аргумента класс регистров, индекс за пределами списка токенов и уже занятый регистр — всё это приводит к слоту в стеке, а не к падению сборки.
Имена регистров разрешаются по таблице, своей для каждой цели: она и есть единственный источник истины о том, что спецификация вправе называть.
| Архитектура | Имена GPR | Имена SIMD | Выбор ширины |
|---|---|---|---|
| x86-64 | rax, rbx, rcx, rdx, rsi, rdi, rbp, r8–r15 |
xmm0–xmm15 |
i32 → 32-битный подрегистр, i64/указатель → 64 бита |
| AArch64 | x0–x28 |
v0–v31 |
i32→w, i64→x, f16→h, f32→s, f64→d, f128/вектор→q |
GPR всегда записываются в 64-битной форме; бэкенд сужает их до подрегистра, соответствующего типу каждого значения. Векторные имена на AArch64 пишутся как v0–v31, а форму H/S/D/Q бэкенд выбирает по типу.
- Зарезервированные регистры: указателя стека нет ни в одной из таблиц (
rspна x86-64,sp/x31на AArch64), как иx29/x30(FP/LR) на AArch64. Спецификация, называющая такой регистр, просто пропускает его, и значение уходит в следующее допустимое место. - Указатель кадра:
rbpна x86-64 выбрать можно — это полноценный callee-saved регистр, но использовать его как регистр аргумента безопасно только при-fomit-frame-pointer. На ваш страх и риск. - Callee-saved: по умолчанию стандартный набор ABI.
csr:r12,r13объявляет свой набор, и вызывающая сторона строит соответствующую маску сохраняемых регистров, чтобы знать, какие переживут вызов. Поддерживается и на x86-64, и на AArch64. - Конфликты csr: если регистр попал одновременно в
csrи в список аргументов или возврата, плагин выдаёт предупреждение — вызываемая функция восстановила бы его и разрушила его роль переносчика значения. Компиляция при этом всё равно завершается успешно. - Вариативные функции: не поддерживаются. Компилятор выдаёт внятную диагностику на обоих бэкендах, а не передаёт вариативную часть молча и неверно.
- Косвенные вызовы: вызов по указателю на функцию не может нести пользовательское соглашение. Плагин предупреждает, когда берётся адрес функции с пользовательским соглашением; косвенные вызовы откатываются к стандартному соглашению.
- Хвостовые вызовы: отключаются, как только пользовательское соглашение используется хотя бы одной стороной вызова, — на обоих бэкендах.
- Значения вне плана: любой аргумент или возвращаемое значение, которое план не покрывает, откатывается к стандартному соглашению цели (SysV на x86-64, AAPCS на AArch64).
Эталонный плагин CustomCallConvPlugin.c поставляется в pluginsdk/examples/. Он регистрирует IR-проход уровня модуля в фазе neverc.ir.pass.post_opt.
Сборка плагина:
cd pluginsdk/examples && make CustomCallConvPlugin.dylib # или .so / .dllРежим атрибутов (по умолчанию) — затрагиваются только функции с аннотацией custom_attr в исходнике:
neverc -fplugin=./CustomCallConvPlugin.dylib input.c -o output.oГлобальный режим — применяет одну спецификацию ко всем определённым функциям (требует явного cc-all):
neverc -fplugin=./CustomCallConvPlugin.dylib \
-fplugin-arg=org.neverc.example.custom-callconv:cc-all \
-fplugin-arg=org.neverc.example.custom-callconv:ccspec="gpr:r10,r11,rsi;ret:rdx" \
input.c -o output.oФильтр по префиксу имени:
neverc -fplugin=./CustomCallConvPlugin.dylib \
-fplugin-arg=org.neverc.example.custom-callconv:cc-all \
-fplugin-arg=org.neverc.example.custom-callconv:ccprefix=secret_ \
-fplugin-arg=org.neverc.example.custom-callconv:ccspec="gpr:r9,r8;ret:rax" \
input.c -o output.oРазнообразие — циклически перебирать четыре встроенные раскладки, чтобы у функций не было одной общей (защита от обратной разработки):
neverc -fplugin=./CustomCallConvPlugin.dylib \
-fplugin-arg=org.neverc.example.custom-callconv:cc-all \
-fplugin-arg=org.neverc.example.custom-callconv:ccshuffle \
input.c -o output.oПлагин регистрирует четыре опции: cc-all и ccshuffle (флаги, поэтому =1 или =true необязательны), а также ccspec и ccprefix (строковые значения). Без ccspec глобальный режим использует значение по умолчанию gpr:r10,r11,rsi,rdi;ret:rdx.
Размечайте функции прямо в C атрибутом custom_attr — в синтаксисе GNU или Microsoft:
// Синтаксис GNU
__attribute__((custom_attr("neverc-callconv", "gpr:r10,r11,rsi;ret:rdx")))
int add3(int a, int b, int c) { return a + b + c; }
// Синтаксис Microsoft
__declspec(custom_attr("neverc-callconv", "gpr:r10;ret:rdx"))
int msfunc(int a) { return a; }custom_attr("key", "value") создаёт чистый строковый атрибут функции ("key"="value") — без предупреждений и без llvm.global.annotations. Это механизм общего назначения: подойдёт любая пара «ключ/значение», не только соглашения о вызовах. IR- и MIR-проходы читают его обратно через F.getFnAttribute("key").
Атрибуты в исходнике и аргументы плагина работают вместе. Функцию с custom_attr обрабатывает ветка режима атрибутов плагина; cc-all покрывает остальные. Каждая функция обрабатывается не более одного раза.
Спецификация называет регистры, но не говорит, где лежит каждый байт каждого значения. После конвейера оптимизаций и перед генерацией кода хост запускает materializeCallingConventionPlans, превращая каждую функцию с CallingConv::NeverC_Custom в точный проверенный план:
- Функция, у которой атрибут
"neverc-cc-plan-v1"уже есть, проверяется, а не создаётся заново: её отпечаток схемы, идентификатор цели и идентификатор соглашения обязаны совпасть с текущей целью. - У функции со спецификацией
"neverc-callconv"имена регистров разрешаются по таблице регистров цели. Полученный план заменяет спецификацию, после чего её убирают из IR. - Функция, у которой нет ни того ни другого, но чья цель регистрирует соглашение о вызовах через ABI плагинов, планируется обратным вызовом
PlanCallingConventionэтого соглашения.
Каждая прямая точка вызова наследует план своей вызываемой функции — именно это удерживает вызывающего и вызываемого в согласии о раскладке между единицами трансляции. План представляет собой плоскую строку:
neverc-cc-plan-v1;schema=<отпечаток>;target=<high>:<low>;cc=<high>:<low>;stack=<байты>;returns=<места>;arguments=<места>;callee-saved=<номера регистров>
Каждое место записывается как <r|s>,<индекс значения>,<смещение фрагмента>,<размер>,<выравнивание>,<номер регистра>,<смещение в стеке>,<флаги>, а несколько мест разделяются символом |. На встроенном пути отпечаток схемы равен llvm-<триплет цели>; цель, зарегистрированная плагином, поставляет свой собственный.
Поскольку номера регистров осмысленны только относительно определившей их схемы, расхождение — это жёсткая ошибка, а не молчаливая порча кода:
| Ситуация | Диагностика |
|---|---|
| Строка плана не разбирается | malformed NeverC calling convention plan |
| Отпечаток схемы не совпадает | NeverC calling convention plan belongs to a foreign target schema |
| Не совпадает идентификатор цели | NeverC calling convention plan has a foreign target ID |
| Не совпадает идентификатор соглашения | NeverC calling convention plan has a foreign convention ID |
Именно поэтому план безопасно встраивать в биткод и проносить через LTO: план, созданный для другой цели, не будет применён случайно.
Пример плагина использует только стабильную таблицу IR core — отдельной точки входа для соглашений о вызовах нет. Применение соглашения к функции — это три вызова плюс синхронизация точек вызова:
NevercIRAttributeHandle Attribute = {0};
Core->CreateStringAttribute(Core->Context, Task, SV("neverc-callconv"), Spec,
&Attribute);
Core->AddFunctionAttribute(Core->Context, Task, Function,
NEVERC_IR_ATTRIBUTE_LOCATION_FUNCTION, 0, Attribute);
Core->SetFunctionCallingConvention(Core->Context, Task, Function,
NEVERC_IR_CALLING_CONVENTION_NEVER_C_CUSTOM);NEVERC_IR_CALLING_CONVENTION_NEVER_C_CUSTOM — стабильное на уровне ABI имя для CallingConv::NeverC_Custom (значение 1000 в LLVM). Затем плагин обходит использования функции через GetValueUseCount / GetValueUse и для каждого использования, которое является операндом-вызываемым в call, invoke или callbr, ставит то же соглашение на инструкцию через SetInstructionProperty с NEVERC_IR_PROPERTY_CALLING_CONVENTION. Любое другое использование означает, что адрес утёк, — отсюда и предупреждение о взятии адреса.
Плагин, регистрирующий собственную цель, может вместо этого предоставить обратный вызов PlanCallingConvention в своём NevercCallingConventionDescriptor и выпускать планы напрямую, минуя слой спецификаций. См. Целевая платформа, MC, ассемблер, объектные файлы.
Набор GoogleTest лежит в tests/neverc/CustomCallConvTests.cpp и содержит 26 тестов. Каждый собирает пример плагина, компилирует небольшую программу в ассемблер с заданной спецификацией и проверяет получившееся размещение в регистре или в стеке.
ninja -C build-neverc neverc-tests
build-neverc/bin/neverc-tests --gtest_filter='CustomCallConvTest.*'Покрытие:
| Категория | Тесты |
|---|---|
| x86-64: пул / позиции / стек / вытеснение / i64 / sret / byval / откат | 9 |
AArch64: GPR / FPR / стек / csr / вызов между разными спецификациями |
5 |
Фронтенд custom_attr (GNU / __declspec / сквозной) |
3 |
| Материализация плана и отклонение чужой схемы | 3 |
Ужесточение (csr, вариативные на обеих целях, косвенный вызов, rsp, конфликт csr) |
6 |
Атрибут в исходнике IR-проход плагина
custom_attr(...) (neverc.ir.pass.post_opt)
│ │
└─────────────┬──────────────┘
▼
"neverc-callconv" = спецификация, NeverC_Custom
на функции и её прямых точках вызова
│
▼
┌──────────────────────────────────────────┐
│ materializeCallingConventionPlans │
│ (после оптимизации, до кодогенерации) │
│ │
│ спецификация → имена в физрегистры │
│ CC плагина → PlanCallingConvention │
│ готовый план → проверка схемы/цели │
└──────────────────────────────────────────┘
│
▼
"neverc-cc-plan-v1" = проверенные места
спецификация удалена; план скопирован в вызовы
│
▼
┌──────────────────────────────────────────┐
│ CCAssignFn бэкенда (по одной на цель) │
│ CC_X86_NeverC / RetCC_X86_NeverC │
│ CC_AArch64_NeverC / RetCC_AArch64_NeverC│
│ │
│ читает план → назначает места │
│ вне плана → стандартное соглашение │
│ хвостовые вызовы отключены │
└──────────────────────────────────────────┘
│
▼
Машинный код с заданной раскладкой регистров
Исполнитель в бэкенде — реализация, написанная один раз: все решения политики живут в плагине. Добавление нового соглашения никогда не требует пересборки NeverC.
Использованную выше основную таблицу см. в PluginIR.h, NevercCallingConventionDescriptor — в PluginTarget.h, а фазу neverc.ir.pass.post_opt, к которой подключается проход, — в Schema/PhaseSchema.json.