Skip to content

Latest commit

 

History

History
261 lines (190 loc) · 20.8 KB

File metadata and controls

261 lines (190 loc) · 20.8 KB

اللغات: 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).

الاستخدام

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

هذا ما يجعل تضمين الخطة في 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 التي يرتبط بها التمرير.