Sprachen: English | 简体中文 | 繁體中文 | 日本語 | 한국어 | Français | Deutsch | Español | Italiano | Русский | العربية
NeverC unterstützt datengesteuerte benutzerdefinierte Aufrufkonventionen — Sie können den Argumenten und Rückgabewerten jeder Funktion beliebige physische Register zuweisen, vollständig aus einem externen Plugin oder über Attribute auf Quellcode-Ebene, ohne den Compiler oder irgendeine TableGen-Definition zu ändern.
Herkömmliche LLVM-Aufrufkonventionen sind über .td / .inc-Dateien fest ins Backend eingebacken. Eine hinzuzufügen oder zu ändern erfordert, die Compiler-Quellen zu bearbeiten und TableGen erneut auszuführen. NeverC ersetzt das durch ein zur Laufzeit datengesteuertes Modell aus zwei Schichten:
- Eine Spec — eine kurze, von Hand schreibbare Zeichenkette wie
gpr:rcx,rdx;ret:rax— wird einer Funktion als String-Attribut"neverc-callconv"angehängt, entweder von einem Plugin oder von einem Attribut auf Quellcode-Ebene. - Vor der Codegenerierung materialisiert der Host diese Spec zu einem Attribut
"neverc-cc-plan-v1": einer unveränderlichen, validierten Tabelle exakter Orte, gebunden an ein bestimmtes Zielschema. Das Backend verarbeitet ausschließlich den Plan.
Die Spec ist das, was Sie schreiben; der Plan ist das, worauf das Backend vertraut. Aufrufkonventionen wandern damit von „zur Übersetzungszeit im Backend hartcodiert“ zu „zur Laufzeit durch externe Richtlinien gesteuert“ — ohne die Überprüfung aufzugeben.
Eine Spec ist eine durch Semikolon getrennte Zeichenkette. Jedes Segment besteht aus einem Schlüssel und einer kommagetrennten Liste von Registernamen (Groß-/Kleinschreibung egal, Leerzeichen werden toleriert):
gpr:rcx,rdx,r8,r9; xmm:xmm0,xmm1; ret:rax; ret_xmm:xmm0
| Segment | Aliase | Bedeutung |
|---|---|---|
args |
Positionsmodus: Jedes Token ist ein Registername oder stack/mem und wird den Argumenten nach Index zugewiesen |
|
gpr |
arg_gpr |
Pool-Modus: Argumentregister für Ganzzahlen/Zeiger, der Reihe nach belegt; Überlauf geht auf den Stack |
xmm |
arg_xmm |
Pool-Modus: Argumentregister für Gleitkomma-/Vektorwerte |
fpr |
Zielneutraler Alias für xmm |
|
ret_gpr |
ret |
Rückgaberegister für Ganzzahlen/Zeiger |
ret_xmm |
Rückgaberegister für Gleitkomma-/Vektorwerte | |
ret_fpr |
Zielneutraler Alias für ret_xmm |
|
csr |
Benutzerdefinierter Satz callee-saved Register (Vorgabe: der Standard-ABI-Satz) |
Jedes Segment darf entfallen, unbekannte Segmente werden ignoriert. Die Schlüssel sind genau einmal in llvm/include/llvm/CodeGen/NeverCCallConv.h definiert, sodass Erzeuger und Parser nicht auseinanderdriften können.
Pool-Modus (gpr: / xmm:): Ganzzahlargumente nehmen der Reihe nach Register aus dem gpr-Pool; Gleitkomma- und Vektorargumente bedienen sich aus xmm. Ist ein Pool erschöpft, gehen die restlichen Argumente auf den Stack.
Positionsmodus (args:): Argument i verwendet das i-te Token. Jedes Token ist entweder ein Registername oder stack / mem, was dieses Argument auf den Stack zwingt:
args:rcx,stack,r8;ret:rax # arg0→rcx, arg1→Stack, arg2→r8, Rückgabe→rax
Ist args vorhanden, hat es Vorrang vor gpr / xmm. Ein Token, das die falsche Registerklasse für den Typ des Arguments nennt, ein Index jenseits der Tokenliste und ein bereits belegtes Register führen allesamt auf einen Stack-Platz, statt die Übersetzung scheitern zu lassen.
Registernamen werden über eine zielspezifische Tabelle aufgelöst — die einzige Quelle der Wahrheit dafür, was eine Spec benennen darf.
| Architektur | GPR-Namen | SIMD-Namen | Breitenwahl |
|---|---|---|---|
| x86-64 | rax, rbx, rcx, rdx, rsi, rdi, rbp, r8–r15 |
xmm0–xmm15 |
i32 → 32-Bit-Unterregister, i64/Zeiger → 64 Bit |
| AArch64 | x0–x28 |
v0–v31 |
i32→w, i64→x, f16→h, f32→s, f64→d, f128/Vektor→q |
GPRs werden stets in ihrer 64-Bit-Schreibweise notiert; das Backend verengt sie auf das Unterregister, das zum Typ des jeweiligen Werts passt. Vektornamen schreibt man auf AArch64 als v0–v31, und das Backend wählt anhand des Typs die Form H/S/D/Q.
- Reservierte Register: Der Stackpointer fehlt in beiden Tabellen (
rspauf x86-64,sp/x31auf AArch64), ebensox29/x30(FP/LR) auf AArch64. Eine Spec, die eines davon nennt, überspringt es schlicht, und der Wert landet am nächsten gültigen Ort. - Frame-Pointer:
rbpist auf x86-64 wählbar, denn es ist ein legitimes callee-saved Register; als Argumentregister ist es aber nur unter-fomit-frame-pointerunbedenklich. Verwendung auf eigene Gefahr. - Callee-saved: Vorgabe ist der Standard-ABI-Satz.
csr:r12,r13deklariert einen eigenen Satz, und der Aufrufer baut eine passende Preserved-Register-Maske, damit er weiß, welche Register den Aufruf überleben. Auf x86-64 wie auf AArch64 unterstützt. - csr-Konflikte: Steht ein Register sowohl in
csrals auch in einer Argument-/Rückgabeliste, warnt das Plugin — der Aufgerufene würde es wiederherstellen und damit seine Rolle als Wertträger zerstören. Die Übersetzung gelingt trotzdem. - Variadische Funktionen: nicht unterstützt. Beide Backends geben eine klare Diagnose aus, statt den variadischen Teil stillschweigend falsch zu übergeben.
- Indirekte Aufrufe: Ein Aufruf über Funktionszeiger kann keine benutzerdefinierte Konvention tragen. Das Plugin warnt, wenn die Adresse einer Funktion mit benutzerdefinierter Konvention genommen wird; indirekte Aufrufe fallen auf die Standardkonvention zurück.
- Tail Calls: deaktiviert, sobald eine der beiden Seiten eines Aufrufs die benutzerdefinierte Konvention verwendet — auf beiden Backends.
- Nicht erfasste Werte: Jedes Argument und jeder Rückgabewert, den der Plan nicht abdeckt, fällt auf die Standardkonvention des Ziels zurück (SysV auf x86-64, AAPCS auf AArch64).
Das Referenz-Plugin CustomCallConvPlugin.c liegt unter pluginsdk/examples/. Es registriert einen modulweiten IR-Pass in der Phase neverc.ir.pass.post_opt.
Plugin bauen:
cd pluginsdk/examples && make CustomCallConvPlugin.dylib # oder .so / .dllAttributmodus (Vorgabe) — betroffen sind nur Funktionen mit einer custom_attr-Annotation im Quellcode:
neverc -fplugin=./CustomCallConvPlugin.dylib input.c -o output.oGlobaler Modus — wendet eine einzige Spec auf jede definierte Funktion an (erfordert ein ausdrückliches 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.oNach Namenspräfix filtern:
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.oDiversifizieren — vier eingebaute Layouts im Wechsel, damit die Funktionen sich nicht ein einziges teilen (Anti-Reverse-Engineering):
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.oDas Plugin registriert vier Optionen: cc-all und ccshuffle (Flags, =1 oder =true ist also optional) sowie ccspec und ccprefix (Zeichenkettenwerte). Ohne ccspec verwendet der globale Modus die Vorgabe gpr:r10,r11,rsi,rdi;ret:rdx.
Annotieren Sie Funktionen direkt in C mit dem Attribut custom_attr, in GNU- oder Microsoft-Syntax:
// GNU-Syntax
__attribute__((custom_attr("neverc-callconv", "gpr:r10,r11,rsi;ret:rdx")))
int add3(int a, int b, int c) { return a + b + c; }
// Microsoft-Syntax
__declspec(custom_attr("neverc-callconv", "gpr:r10;ret:rdx"))
int msfunc(int a) { return a; }custom_attr("key", "value") erzeugt ein sauberes String-Attribut an der Funktion ("key"="value") — ohne Warnungen und ohne llvm.global.annotations. Es ist ein Allzweckmechanismus: Jedes Schlüssel/Wert-Paar funktioniert, nicht nur Aufrufkonventionen. IR- und MIR-Passes lesen es mit F.getFnAttribute("key") wieder aus.
Quellcode-Attribute und Plugin-Argumente arbeiten zusammen. Eine Funktion mit custom_attr läuft über den Attributmodus-Pfad des Plugins; cc-all deckt den Rest ab. Jede Funktion wird höchstens einmal verarbeitet.
Eine Spec benennt Register; sie sagt nicht, wo jedes Byte jedes Werts liegt. Nach der Optimierungspipeline und vor der Codegenerierung führt der Host materializeCallingConventionPlans aus, was jede Funktion mit CallingConv::NeverC_Custom in einen exakten, validierten Plan überführt:
- Eine Funktion, die bereits ein Attribut
"neverc-cc-plan-v1"besitzt, wird validiert, nicht neu erzeugt — ihr Schema-Digest, ihre Target-ID und ihre Konventions-ID müssen zum aktuellen Ziel passen. - Bei einer Funktion mit einer
"neverc-callconv"-Spec werden die Registernamen gegen die Registertabelle des Ziels aufgelöst. Der entstehende Plan ersetzt die Spec, die danach aus der IR entfernt wird. - Eine Funktion ohne beides, deren Ziel aber über die Plugin-ABI eine Aufrufkonvention registriert, wird vom
PlanCallingConvention-Callback dieser Konvention geplant.
Jede direkte Aufrufstelle erbt den Plan ihres Aufgerufenen — das hält Aufrufer und Aufgerufenen über Übersetzungseinheiten hinweg beim selben Layout. Der Plan ist eine flache Zeichenkette:
neverc-cc-plan-v1;schema=<Digest>;target=<high>:<low>;cc=<high>:<low>;stack=<Bytes>;returns=<Orte>;arguments=<Orte>;callee-saved=<Registernummern>
Jeder Ort lautet <r|s>,<Wertindex>,<Fragment-Offset>,<Größe>,<Ausrichtung>,<Registernummer>,<Stack-Offset>,<Flags>, mehrere Orte werden durch | getrennt. Auf dem eingebauten Weg ist der Schema-Digest llvm-<Ziel-Triple>; ein per Plugin registriertes Ziel liefert seinen eigenen.
Da Registernummern nur gegenüber dem Schema Sinn ergeben, das sie definiert, ist eine Abweichung ein harter Fehler statt einer stillen Fehlübersetzung:
| Situation | Diagnose |
|---|---|
| Die Plan-Zeichenkette lässt sich nicht parsen | malformed NeverC calling convention plan |
| Der Schema-Digest weicht ab | NeverC calling convention plan belongs to a foreign target schema |
| Die Target-ID weicht ab | NeverC calling convention plan has a foreign target ID |
| Die Konventions-ID weicht ab | NeverC calling convention plan has a foreign convention ID |
Genau das macht es sicher, einen Plan in Bitcode einzubetten und durch LTO zu tragen: Ein für ein anderes Ziel erzeugter Plan kann nicht versehentlich angewendet werden.
Das Beispiel-Plugin nutzt allein die stabile IR-Core-Tabelle — einen eigenen Einstiegspunkt für Aufrufkonventionen gibt es nicht. Eine Konvention auf eine Funktion anzuwenden, sind drei Aufrufe plus die Synchronisierung der Aufrufstellen:
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 ist der ABI-stabile Name für CallingConv::NeverC_Custom (LLVM-Wert 1000). Anschließend läuft das Plugin mit GetValueUseCount / GetValueUse die Verwendungen der Funktion ab und setzt für jede Verwendung, die der Callee-Operand eines call, invoke oder callbr ist, dieselbe Konvention per SetInstructionProperty mit NEVERC_IR_PROPERTY_CALLING_CONVENTION auf der Instruktion. Jede andere Verwendung bedeutet, dass die Adresse entkommen ist — daher die Warnung zur genommenen Adresse.
Ein Plugin, das ein eigenes Ziel registriert, kann stattdessen einen PlanCallingConvention-Callback an seinem NevercCallingConventionDescriptor bereitstellen und Pläne direkt erzeugen, wodurch die Spec-Schicht entfällt. Siehe Target, MC, Assembly, Objekte.
Die GoogleTest-Suite liegt in tests/neverc/CustomCallConvTests.cpp und umfasst 26 Tests. Jeder baut das Beispiel-Plugin, übersetzt ein kleines Programm unter einer gegebenen Spec nach Assembly und prüft die resultierende Register- oder Stack-Platzierung.
ninja -C build-neverc neverc-tests
build-neverc/bin/neverc-tests --gtest_filter='CustomCallConvTest.*'Abdeckung:
| Kategorie | Tests |
|---|---|
| x86-64 Pool / Position / Stack / Überlauf / i64 / sret / byval / Rückfall | 9 |
AArch64 GPR / FPR / Stack / csr / Aufruf über unterschiedliche Specs hinweg |
5 |
Frontend custom_attr (GNU / __declspec / Ende zu Ende) |
3 |
| Plan-Materialisierung und Schema-Zurückweisung | 3 |
Härtung (csr, Varargs auf beiden Zielen, indirekt, rsp, csr-Konflikt) |
6 |
Quellcode-Attribut Plugin-IR-Pass
custom_attr(...) (neverc.ir.pass.post_opt)
│ │
└─────────────┬──────────────┘
▼
"neverc-callconv" = Spec, CallingConv::NeverC_Custom
an der Funktion und ihren direkten Aufrufstellen
│
▼
┌──────────────────────────────────────────┐
│ materializeCallingConventionPlans │
│ (nach Optimierung, vor Codegen) │
│ │
│ Spec → Namen zu Physregs │
│ Plugin-CC → PlanCallingConvention │
│ Plan vorhanden → Schema/Ziel prüfen │
└──────────────────────────────────────────┘
│
▼
"neverc-cc-plan-v1" = validierte Orte
Spec entfernt; Plan auf direkte Aufrufstellen kopiert
│
▼
┌──────────────────────────────────────────┐
│ Backend-CCAssignFn (eine pro Ziel) │
│ CC_X86_NeverC / RetCC_X86_NeverC │
│ CC_AArch64_NeverC / RetCC_AArch64_NeverC│
│ │
│ liest den Plan → weist Orte zu │
│ nicht erfasste Werte → Standardkonv. │
│ Tail Calls deaktiviert │
└──────────────────────────────────────────┘
│
▼
Maschinencode mit dem eigenen Register-Layout
Der Ausführer im Backend ist eine einmalige Implementierung — sämtliche Richtlinienentscheidungen leben im Plugin. Eine neue Konvention hinzuzufügen erfordert nie einen Neubau von NeverC.
Die oben verwendete Kerntabelle steht in PluginIR.h, NevercCallingConventionDescriptor in PluginTarget.h, die Phase neverc.ir.pass.post_opt, an die sich der Pass hängt, in Schema/PhaseSchema.json.