اللغات: English | 简体中文 | 繁體中文 | 日本語 | 한국어 | Français | Deutsch | Español | Italiano | Русский | العربية
← واجهة 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→stack, arg2→r8, return→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 |
هذا ما يجعل تضمين الخطة في bitcode ونقلها عبر 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 |
الرسم التالي محفوظ بمصطلحاته الإنجليزية لأن خلط النص العربي بحروف الرسم يفسد محاذاته:
Source attribute Plugin IR pass
custom_attr(...) (neverc.ir.pass.post_opt)
│ │
└─────────────┬──────────────┘
▼
"neverc-callconv" = spec, CallingConv::NeverC_Custom
on the function and its direct call sites
│
▼
┌──────────────────────────────────────────┐
│ materializeCallingConventionPlans │
│ (after optimization, before codegen) │
│ │
│ spec → names to physregs │
│ plugin CC → PlanCallingConvention │
│ existing plan → validate schema/target │
└──────────────────────────────────────────┘
│
▼
"neverc-cc-plan-v1" = validated locations
spec removed; plan copied to direct call sites
│
▼
┌──────────────────────────────────────────┐
│ Backend CCAssignFn (one per target) │
│ CC_X86_NeverC / RetCC_X86_NeverC │
│ CC_AArch64_NeverC / RetCC_AArch64_NeverC│
│ │
│ reads the plan → assigns locations │
│ unmatched values → standard convention │
│ tail calls disabled │
└──────────────────────────────────────────┘
│
▼
Machine code with the custom register layout
المنفِّذ في الواجهة الخلفية تنفيذ يُكتب مرة واحدة؛ فكل قرارات السياسة تعيش داخل الإضافة. وإضافة اتفاقية جديدة لا تستلزم أبدًا إعادة بناء NeverC.
انظر PluginIR.h للجدول الأساسي المستخدَم أعلاه، وPluginTarget.h لـ NevercCallingConventionDescriptor، وSchema/PhaseSchema.json للمرحلة neverc.ir.pass.post_opt التي يرتبط بها التمرير.