Skip to content

Latest commit

 

History

History
255 lines (187 loc) · 23.6 KB

File metadata and controls

255 lines (187 loc) · 23.6 KB

Языки: English | 简体中文 | 繁體中文 | 日本語 | 한국어 | Français | Deutsch | Español | Italiano | Русский | العربية

← ABI плагинов NeverC

Пользовательские соглашения о вызовах

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).

Использование

1. Через плагин (рекомендуется)

Эталонный плагин 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.

2. Атрибуты на уровне исходного кода

Размечайте функции прямо в 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").

3. Совместно

Атрибуты в исходнике и аргументы плагина работают вместе. Функцию с 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: план, созданный для другой цели, не будет применён случайно.

API плагина

Пример плагина использует только стабильную таблицу 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.