Langues: English | 简体中文 | 繁體中文 | 日本語 | 한국어 | Français | Deutsch | Español | Italiano | Русский | العربية
NeverC prend en charge des conventions d'appel personnalisées pilotées par les données : vous pouvez affecter n'importe quels registres physiques aux arguments et aux valeurs de retour de n'importe quelle fonction, entièrement depuis un plugin externe ou des attributs au niveau du source, sans modifier le compilateur ni la moindre définition TableGen.
Les conventions d'appel LLVM traditionnelles sont figées dans le backend via des fichiers .td / .inc. En ajouter ou en modifier une impose d'éditer les sources du compilateur et de relancer TableGen. NeverC remplace cela par un modèle piloté par les données à l'exécution, construit sur deux couches :
- Une spec — une courte chaîne rédigeable à la main, par exemple
gpr:rcx,rdx;ret:rax— est attachée à une fonction sous forme d'attribut chaîne"neverc-callconv", par un plugin ou par un attribut au niveau du source. - Avant la génération de code, l'hôte matérialise cette spec en un attribut
"neverc-cc-plan-v1": une table d'emplacements exacte, immuable et validée, liée à un schéma de cible précis. Le backend ne consomme que le plan.
La spec est ce que vous écrivez ; le plan est ce que le backend croit. Les conventions d'appel passent donc de « codées en dur dans le backend à la compilation » à « pilotées par une politique externe à l'exécution », sans renoncer à la vérification.
Une spec est une chaîne délimitée par des points-virgules. Chaque segment comporte une clé et une liste de noms de registres séparés par des virgules (insensible à la casse, tolérant aux espaces) :
gpr:rcx,rdx,r8,r9; xmm:xmm0,xmm1; ret:rax; ret_xmm:xmm0
| Segment | Alias | Signification |
|---|---|---|
args |
Mode positionnel : chaque jeton est un nom de registre ou stack/mem, affecté aux arguments par indice |
|
gpr |
arg_gpr |
Mode pool : registres d'arguments entiers/pointeurs, consommés dans l'ordre ; le débordement part sur la pile |
xmm |
arg_xmm |
Mode pool : registres d'arguments flottants/vectoriels |
fpr |
Alias neutre vis-à-vis de la cible pour xmm |
|
ret_gpr |
ret |
Registres de retour entiers/pointeurs |
ret_xmm |
Registres de retour flottants/vectoriels | |
ret_fpr |
Alias neutre vis-à-vis de la cible pour ret_xmm |
|
csr |
Ensemble personnalisé de registres callee-saved (par défaut : l'ensemble ABI standard) |
Tout segment peut être omis, et les segments inconnus sont ignorés. Ces clés sont définies une seule fois dans llvm/include/llvm/CodeGen/NeverCCallConv.h, si bien que producteurs et analyseur ne peuvent pas diverger.
Mode pool (gpr: / xmm:) : les arguments entiers prennent les registres du pool gpr dans l'ordre ; les arguments flottants et vectoriels puisent dans xmm. Quand un pool est épuisé, les arguments restants partent sur la pile.
Mode positionnel (args:) : l'argument i utilise le i-ème jeton. Chaque jeton est soit un nom de registre, soit stack / mem, ce qui force cet argument sur la pile :
args:rcx,stack,r8;ret:rax # arg0→rcx, arg1→pile, arg2→r8, retour→rax
Lorsque args est présent, il prime sur gpr / xmm. Un jeton désignant une mauvaise classe de registre pour le type de l'argument, un indice au-delà de la liste de jetons, ou un registre déjà alloué : tous retombent sur un emplacement de pile plutôt que de faire échouer la compilation.
Les noms de registres sont résolus via une table propre à chaque cible, seule source de vérité sur ce qu'une spec peut nommer.
| Architecture | Noms GPR | Noms SIMD | Choix de la largeur |
|---|---|---|---|
| x86-64 | rax, rbx, rcx, rdx, rsi, rdi, rbp, r8–r15 |
xmm0–xmm15 |
i32 → sous-registre 32 bits, i64/pointeur → 64 bits |
| AArch64 | x0–x28 |
v0–v31 |
i32→w, i64→x, f16→h, f32→s, f64→d, f128/vecteur→q |
Les GPR s'écrivent toujours dans leur forme 64 bits ; le backend les réduit au sous-registre correspondant au type de chaque valeur. Sur AArch64, les registres vectoriels s'écrivent v0–v31 et le backend choisit la forme H/S/D/Q selon le type.
- Registres réservés : le pointeur de pile est absent des deux tables (
rspsur x86-64,sp/x31sur AArch64), tout commex29/x30(FP/LR) sur AArch64. Une spec qui les nomme se contente de les ignorer et la valeur part à l'emplacement valide suivant. - Pointeur de trame :
rbpest sélectionnable sur x86-64 car c'est un registre callee-saved légitime, mais l'employer comme registre d'argument n'est sain que sous-fomit-frame-pointer. À utiliser à vos risques et périls. - Callee-saved : par défaut l'ensemble ABI standard.
csr:r12,r13déclare un ensemble personnalisé, et l'appelant construit un masque de registres préservés correspondant afin de savoir lesquels survivent à l'appel. Pris en charge sur x86-64 comme sur AArch64. - Conflits csr : si un registre figure à la fois dans
csret dans une liste d'arguments/retour, le plugin émet un avertissement — l'appelé le restaurerait et détruirait son rôle de transport de valeur. La compilation réussit malgré tout. - Fonctions variadiques : non prises en charge. Les deux backends émettent un diagnostic clair au lieu de mal transmettre silencieusement la partie variadique.
- Appels indirects : un appel par pointeur de fonction ne peut pas transporter une convention personnalisée. Le plugin avertit lorsque l'adresse d'une fonction à convention personnalisée est prise ; les appels indirects retombent sur la convention standard.
- Appels terminaux : désactivés dès que l'un des deux côtés de l'appel utilise la convention personnalisée, sur les deux backends.
- Valeurs non couvertes : tout argument ou retour que le plan ne couvre pas retombe sur la convention standard de la cible (SysV sur x86-64, AAPCS sur AArch64).
Le plugin de référence CustomCallConvPlugin.c est livré dans pluginsdk/examples/. Il enregistre une passe IR au niveau module sur la phase neverc.ir.pass.post_opt.
Compiler le plugin :
cd pluginsdk/examples && make CustomCallConvPlugin.dylib # ou .so / .dllMode attribut (par défaut) — seules les fonctions portant une annotation source custom_attr sont concernées :
neverc -fplugin=./CustomCallConvPlugin.dylib input.c -o output.oMode global — applique une seule spec à toutes les fonctions définies (exige un cc-all explicite) :
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.oFiltrer par préfixe de nom :
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.oDiversifier — alterner entre quatre dispositions intégrées pour que les fonctions n'en partagent pas une seule (anti-rétro-ingénierie) :
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.oLe plugin enregistre quatre options : cc-all et ccshuffle (des drapeaux, donc =1 ou =true est facultatif), plus ccspec et ccprefix (valeurs chaîne). Sans ccspec, le mode global utilise la valeur par défaut gpr:r10,r11,rsi,rdi;ret:rdx.
Annotez directement les fonctions en C avec l'attribut custom_attr, en syntaxe GNU ou Microsoft :
// Syntaxe GNU
__attribute__((custom_attr("neverc-callconv", "gpr:r10,r11,rsi;ret:rdx")))
int add3(int a, int b, int c) { return a + b + c; }
// Syntaxe Microsoft
__declspec(custom_attr("neverc-callconv", "gpr:r10;ret:rdx"))
int msfunc(int a) { return a; }custom_attr("key", "value") produit un attribut chaîne de fonction propre ("key"="value"), sans avertissement et sans passer par llvm.global.annotations. C'est un mécanisme générique : n'importe quelle paire clé/valeur fonctionne, pas seulement les conventions d'appel. Les passes IR et MIR le relisent avec F.getFnAttribute("key").
Attributs source et arguments de plugin fonctionnent ensemble. Une fonction portant un custom_attr passe par le chemin « mode attribut » du plugin ; cc-all couvre le reste. Chaque fonction est traitée au plus une fois.
Une spec nomme des registres ; elle ne dit pas où réside chaque octet de chaque valeur. Après le pipeline d'optimisation et avant la génération de code, l'hôte exécute materializeCallingConventionPlans, qui transforme chaque fonction CallingConv::NeverC_Custom en un plan exact et validé :
- Une fonction qui possède déjà un attribut
"neverc-cc-plan-v1"est validée, pas régénérée : son empreinte de schéma, son identifiant de cible et son identifiant de convention doivent correspondre à la cible courante. - Une fonction porteuse d'une spec
"neverc-callconv"voit ses noms de registres résolus contre la table de registres de la cible. Le plan obtenu remplace la spec, qui est ensuite retirée de l'IR. - Une fonction sans l'un ni l'autre, mais dont la cible enregistre une convention d'appel via l'ABI de plugin, est planifiée par le rappel
PlanCallingConventionde cette convention.
Chaque site d'appel direct hérite du plan de son appelé, ce qui maintient l'accord entre appelant et appelé d'une unité de traduction à l'autre. Le plan est une chaîne plate :
neverc-cc-plan-v1;schema=<empreinte>;target=<high>:<low>;cc=<high>:<low>;stack=<octets>;returns=<emplacements>;arguments=<emplacements>;callee-saved=<numéros de registres>
Chaque emplacement s'écrit <r|s>,<indice de valeur>,<décalage du fragment>,<taille>,<alignement>,<numéro de registre>,<décalage de pile>,<drapeaux>, et plusieurs emplacements sont séparés par |. Pour le chemin intégré, l'empreinte de schéma est llvm-<triplet cible> ; une cible enregistrée par un plugin fournit la sienne.
Comme les numéros de registres n'ont de sens qu'au regard du schéma qui les définit, une discordance est une erreur franche plutôt qu'une compilation silencieusement fausse :
| Situation | Diagnostic |
|---|---|
| La chaîne du plan ne s'analyse pas | malformed NeverC calling convention plan |
| L'empreinte de schéma diffère | NeverC calling convention plan belongs to a foreign target schema |
| L'identifiant de cible diffère | NeverC calling convention plan has a foreign target ID |
| L'identifiant de convention diffère | NeverC calling convention plan has a foreign convention ID |
C'est ce qui rend un plan sûr à intégrer dans du bitcode et à transporter à travers le LTO : un plan produit pour une autre cible ne peut pas être appliqué par accident.
Le plugin d'exemple n'utilise que la table IR core stable — il n'existe pas de point d'entrée dédié aux conventions d'appel. Appliquer une convention à une fonction tient en trois appels, plus la synchronisation des sites d'appel :
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 est le nom stable au niveau ABI de CallingConv::NeverC_Custom (valeur LLVM 1000). Le plugin parcourt ensuite les usages de la fonction avec GetValueUseCount / GetValueUse et, pour chaque usage qui est l'opérande appelé d'un call, invoke ou callbr, applique la même convention à l'instruction via SetInstructionProperty avec NEVERC_IR_PROPERTY_CALLING_CONVENTION. Tout autre usage signifie que l'adresse s'est échappée, d'où l'avertissement sur l'adresse prise.
Un plugin qui enregistre sa propre cible peut au contraire fournir un rappel PlanCallingConvention sur son NevercCallingConventionDescriptor et produire directement des plans, en sautant la couche spec. Voir Cible, MC, assembleur, objet.
La suite GoogleTest se trouve dans tests/neverc/CustomCallConvTests.cpp et compte 26 tests. Chacun compile le plugin d'exemple, traduit un petit programme en assembleur sous une spec donnée, puis vérifie le placement en registre ou sur la pile obtenu.
ninja -C build-neverc neverc-tests
build-neverc/bin/neverc-tests --gtest_filter='CustomCallConvTest.*'Couverture :
| Catégorie | Tests |
|---|---|
| x86-64 pool / positionnel / pile / débordement / i64 / sret / byval / repli | 9 |
AArch64 GPR / FPR / pile / csr / appel croisé entre specs différentes |
5 |
Frontend custom_attr (GNU / __declspec / bout en bout) |
3 |
| Matérialisation du plan et rejet de schéma | 3 |
Durcissement (csr, variadiques sur les deux cibles, indirect, rsp, conflit csr) |
6 |
Attribut source Passe IR du plugin
custom_attr(...) (neverc.ir.pass.post_opt)
│ │
└─────────────┬──────────────┘
▼
"neverc-callconv" = spec, CallingConv::NeverC_Custom
sur la fonction et ses sites d'appel directs
│
▼
┌──────────────────────────────────────────┐
│ materializeCallingConventionPlans │
│ (après optimisation, avant codegen) │
│ │
│ spec → noms vers registres phys. │
│ convention plugin → PlanCallingConv... │
│ plan existant → valider schéma / cible │
└──────────────────────────────────────────┘
│
▼
"neverc-cc-plan-v1" = emplacements validés
spec retirée ; plan copié sur les appels directs
│
▼
┌──────────────────────────────────────────┐
│ CCAssignFn du backend (une par cible) │
│ CC_X86_NeverC / RetCC_X86_NeverC │
│ CC_AArch64_NeverC / RetCC_AArch64_NeverC│
│ │
│ lit le plan → affecte les emplacements │
│ valeurs non couvertes → convention std │
│ appels terminaux désactivés │
└──────────────────────────────────────────┘
│
▼
Code machine avec la disposition personnalisée
L'exécuteur du backend est une implémentation faite une fois pour toutes — toutes les décisions de politique vivent dans le plugin. Ajouter une convention n'exige jamais de reconstruire NeverC.
Voir PluginIR.h pour la table de base utilisée ci-dessus, PluginTarget.h pour NevercCallingConventionDescriptor, et Schema/PhaseSchema.json pour la phase neverc.ir.pass.post_opt à laquelle la passe s'attache.