BPF للصغار ، الجزء الأول: BPF الممتد

في البداية كانت هناك تقنية وكان اسمها BPF. نظرنا إليها سابق، العهد القديم، مقال في هذه السلسلة. في عام 2013، وبفضل جهود أليكسي ستاروفويتوف ودانيال بوركمان، تم تطويره وإدراجه في النص الأساسي Linux نسخة محسّنة مصممة خصيصًا لأجهزة 64 بت الحديثة. سُميت هذه التقنية الجديدة لفترة وجيزة BPF الداخلية، ثم أُعيد تسميتها إلى BPF الموسعة، والآن، بعد مرور عدة سنوات، يُطلق عليها الجميع ببساطة اسم BPF.

بشكل أساسي، يسمح BPF بتشغيل أي كود يقدمه المستخدم في مساحة النواة. Linux وقد أثبتت البنية الجديدة نجاحاً باهراً لدرجة أننا سنحتاج إلى عشرات المقالات الأخرى لشرح جميع تطبيقاتها. (الشيء الوحيد الذي أخفق المطورون في فعله، كما ترون في مخطط الكفاءة أدناه، هو تصميم شعار لائق).

توضح هذه المقالة بنية الجهاز الظاهري BPF، وواجهات kernel للعمل مع BPF، وأدوات التطوير، بالإضافة إلى نظرة عامة مختصرة ومختصرة جدًا على الإمكانات الحالية، أي. كل ما سنحتاجه في المستقبل لدراسة أعمق للتطبيقات العملية لـ BPF.
BPF للصغار ، الجزء الأول: BPF الممتد

ملخص المقال

مقدمة في بنية BPF. أولاً، سنلقي نظرة عامة على بنية BPF ونحدد المكونات الرئيسية.

السجلات ونظام الأوامر للجهاز الظاهري BPF. لدينا بالفعل فكرة عن البنية ككل، وسنصف هيكل الجهاز الظاهري BPF.

دورة حياة كائنات BPF ونظام الملفات bpffs. في هذا القسم، سنلقي نظرة فاحصة على دورة حياة كائنات BPF - البرامج والخرائط.

إدارة الكائنات باستخدام استدعاء نظام bpf. مع بعض الفهم للنظام الموجود بالفعل، سننظر أخيرًا في كيفية إنشاء الكائنات ومعالجتها من مساحة المستخدم باستخدام استدعاء نظام خاص − bpf(2).

Пишем программы BPF с помощью libbpf. بالطبع، يمكنك كتابة البرامج باستخدام استدعاء النظام. و لكنه صعب. للحصول على سيناريو أكثر واقعية، قام المبرمجون النوويون بتطوير مكتبة libbpf. سنقوم بإنشاء هيكل أساسي لتطبيق BPF والذي سنستخدمه في الأمثلة اللاحقة.

مساعدي النواة. سنتعلم هنا كيف يمكن لبرامج BPF الوصول إلى وظائف مساعد kernel - وهي أداة تعمل، إلى جانب الخرائط، على توسيع قدرات BPF الجديدة بشكل أساسي مقارنة بالأداة الكلاسيكية.

الوصول إلى الخرائط من برامج BPF. عند هذه النقطة، سنكون نعرف ما يكفي لنفهم بالضبط كيف يمكننا إنشاء برامج تستخدم الخرائط. ودعونا نلقي نظرة سريعة على أداة التحقق العظيمة والعظيمة.

ادوات التطوير. قسم المساعدة حول كيفية تجميع الأدوات المساعدة والنواة المطلوبة للتجارب.

الاستنتاج. وفي نهاية المقال من قرأ هذا الحد سيجد كلمات محفزة ووصف مختصر لما سيحدث في المقالات التالية. كما سنقوم بإدراج عدد من الروابط للدراسة الذاتية لمن ليس لديه الرغبة أو القدرة على انتظار الاستمرار.

مقدمة في الهندسة المعمارية BPF

قبل أن نبدأ في النظر في بنية BPF، سنشير مرة أخيرة (أوه) إلى BPF الكلاسيكي، والذي تم تطويره كاستجابة لظهور أجهزة RISC وحل مشكلة تصفية الحزم بكفاءة. تبين أن الهندسة المعمارية كانت ناجحة للغاية، حيث ولدت في التسعينات المحطمة في Berkeley UNIX، وتم نقلها إلى معظم أنظمة التشغيل الحالية، وقد نجت حتى العشرينات المجنونة وما زالت تجد تطبيقات جديدة.

تم تطوير BPF الجديد كاستجابة لانتشار أجهزة 64 بت والخدمات السحابية والحاجة المتزايدة إلى أدوات لإنشاء SDN (Sالبرمجيات-defined n(الشبكات). تم تطويره بواسطة مهندسي الشبكات الأساسية كبديل محسّن لـ BPF الكلاسيكي، ووجد BPF الجديد تطبيقًا في المهمة الصعبة المتمثلة في التتبع بعد ستة أشهر فقط. Linux الأنظمة، والآن، بعد ست سنوات من ظهورها، سنحتاج إلى مقال جديد كامل فقط لسرد الأنواع المختلفة من البرامج.

صور مضحكة

يعد BPF في جوهره جهازًا افتراضيًا لوضع الحماية يسمح لك بتشغيل تعليمات برمجية "تعسفية" في مساحة kernel دون المساس بالأمن. يتم إنشاء برامج BPF في مساحة المستخدم، ويتم تحميلها في النواة، ويتم توصيلها ببعض مصادر الأحداث. يمكن أن يكون الحدث، على سبيل المثال، تسليم حزمة إلى واجهة الشبكة، أو إطلاق بعض وظائف kernel، وما إلى ذلك. في حالة الحزمة، سيكون لبرنامج BPF حق الوصول إلى البيانات والبيانات الوصفية الخاصة بالحزمة (للقراءة وربما الكتابة، اعتمادًا على نوع البرنامج)؛ في حالة تشغيل وظيفة kernel، سيتم استخدام وسيطات الوظيفة، بما في ذلك المؤشرات إلى ذاكرة kernel، وما إلى ذلك.

دعونا نلقي نظرة فاحصة على هذه العملية. لتبدأ، دعونا نتحدث عن الفرق الأول من BPF الكلاسيكي، البرامج التي تمت كتابتها في المجمع. في الإصدار الجديد، تم توسيع البنية بحيث يمكن كتابة البرامج بلغات عالية المستوى، في المقام الأول، بالطبع، في C. لهذا، تم تطوير الواجهة الخلفية لـ llvm، والتي تسمح لك بإنشاء رمز بايت لبنية BPF.

BPF للصغار ، الجزء الأول: BPF الممتد

تم تصميم بنية BPF جزئيًا لتعمل بكفاءة على الأجهزة الحديثة. ولتنفيذ هذا الأمر عمليًا، تتم ترجمة الكود الثانوي BPF، بمجرد تحميله في النواة، إلى كود أصلي باستخدام مكون يسمى مترجم JIT (Jأوست In Tايمي). بعد ذلك، إذا كنت تتذكر، في BPF الكلاسيكي، تم تحميل البرنامج في kernel وإرفاقه بمصدر الحدث ذريًا - في سياق استدعاء نظام واحد. في البنية الجديدة، يحدث هذا على مرحلتين - أولاً، يتم تحميل الكود في النواة باستخدام استدعاء النظام bpf(2)وبعد ذلك، ومن خلال آليات أخرى تختلف حسب نوع البرنامج، يرتبط البرنامج بمصدر الحدث.

وهنا قد يكون لدى القارئ سؤال: هل كان ذلك ممكنا؟ كيف يتم ضمان سلامة تنفيذ هذا الكود؟ يتم ضمان سلامة التنفيذ لنا من خلال مرحلة تحميل برامج BPF والتي تسمى verifier (باللغة الإنجليزية تسمى هذه المرحلة verifier وسأستمر في استخدام الكلمة الإنجليزية):

BPF للصغار ، الجزء الأول: BPF الممتد

Verifier هو محلل ثابت يضمن عدم قيام البرنامج بتعطيل التشغيل العادي للنواة. هذا، بالمناسبة، لا يعني أن البرنامج لا يمكن أن يتداخل مع تشغيل النظام - يمكن لبرامج BPF، اعتمادًا على النوع، قراءة وإعادة كتابة أقسام من ذاكرة kernel، وإرجاع قيم الوظائف، والقص، والإلحاق، وإعادة الكتابة وحتى حزم الشبكة إلى الأمام. يضمن برنامج Verifier أن تشغيل برنامج BPF لن يؤدي إلى تعطل النواة وأن البرنامج الذي، وفقًا للقواعد، لديه حق الوصول للكتابة، على سبيل المثال، بيانات الحزمة الصادرة، لن يتمكن من الكتابة فوق ذاكرة kernel خارج الحزمة. سننظر إلى أداة التحقق بمزيد من التفصيل في القسم المقابل، بعد أن نتعرف على جميع المكونات الأخرى لـ BPF.

إذن ما الذي تعلمناه حتى الآن؟ يكتب المستخدم برنامجًا بلغة C، ويحمله في النواة باستخدام استدعاء النظام bpf(2)، حيث يتم فحصه بواسطة أداة التحقق وترجمته إلى رمز بايت أصلي. ثم يقوم نفس المستخدم أو مستخدم آخر بتوصيل البرنامج بمصدر الحدث ويبدأ في التنفيذ. يعد فصل التمهيد والاتصال ضروريًا لعدة أسباب. أولاً، يعد تشغيل أداة التحقق أمرًا مكلفًا نسبيًا، ومن خلال تنزيل نفس البرنامج عدة مرات فإننا نضيع وقت الكمبيوتر. ثانيا، تعتمد كيفية توصيل البرنامج بالضبط على نوعه، وقد لا تكون الواجهة "العالمية" التي تم تطويرها قبل عام مناسبة لأنواع جديدة من البرامج. (على الرغم من أن البنية أصبحت الآن أكثر نضجًا، إلا أن هناك فكرة لتوحيد هذه الواجهة على المستوى libbpf.)

وقد يلاحظ القارئ اليقظ أننا لم ننتهي من الصور بعد. في الواقع، كل ما سبق لا يفسر سبب تغيير BPF للصورة بشكل أساسي مقارنةً بـ BPF الكلاسيكي. هناك ابتكاران يعملان على توسيع نطاق التطبيق بشكل كبير وهما القدرة على استخدام الذاكرة المشتركة ووظائف مساعد kernel. في BPF، يتم تنفيذ الذاكرة المشتركة باستخدام ما يسمى بالخرائط - هياكل البيانات المشتركة مع واجهة برمجة التطبيقات المحددة. من المحتمل أنهم حصلوا على هذا الاسم لأن النوع الأول من الخريطة الذي ظهر كان جدول التجزئة. ثم ظهرت المصفوفات وجداول التجزئة المحلية (لكل وحدة معالجة مركزية) والمصفوفات المحلية وأشجار البحث والخرائط التي تحتوي على مؤشرات لبرامج BPF وغير ذلك الكثير. ما يثير اهتمامنا الآن هو أن برامج BPF لديها الآن القدرة على الاحتفاظ بالحالة بين المكالمات ومشاركتها مع البرامج الأخرى ومع مساحة المستخدم.

يتم الوصول إلى الخرائط من عمليات المستخدم باستخدام استدعاء النظام bpf(2)ومن برامج BPF التي تعمل في النواة باستخدام الوظائف المساعدة. علاوة على ذلك، فإن المساعدين موجودون ليس فقط للعمل مع الخرائط، ولكن أيضًا للوصول إلى قدرات النواة الأخرى. على سبيل المثال، يمكن لبرامج BPF استخدام الوظائف المساعدة لإعادة توجيه الحزم إلى واجهات أخرى، وإنشاء أحداث الأداء، والوصول إلى هياكل kernel، وما إلى ذلك.

BPF للصغار ، الجزء الأول: BPF الممتد

باختصار، يوفر BPF القدرة على تحميل رمز المستخدم التعسفي، أي رمز المستخدم الذي تم اختباره بواسطة المدقق، في مساحة kernel. يمكن لهذا الرمز حفظ الحالة بين المكالمات وتبادل البيانات مع مساحة المستخدم، كما أنه يتمتع بإمكانية الوصول إلى أنظمة kernel الفرعية التي يسمح بها هذا النوع من البرامج.

هذا مشابه بالفعل للإمكانيات التي توفرها وحدات kernel، مقارنة بها BPF لديها بعض المزايا (بالطبع، يمكنك فقط مقارنة التطبيقات المماثلة، على سبيل المثال، تتبع النظام - لا يمكنك كتابة برنامج تشغيل تعسفي باستخدام BPF). يمكنك ملاحظة حد دخول أقل (بعض الأدوات المساعدة التي تستخدم BPF لا تتطلب من المستخدم أن يكون لديه مهارات برمجة kernel، أو مهارات البرمجة بشكل عام)، وسلامة وقت التشغيل (ارفع يدك في التعليقات لأولئك الذين لم يخرقوا النظام عند الكتابة) أو وحدات الاختبار)، الذرية - هناك وقت توقف عند إعادة تحميل الوحدات، ويضمن نظام BPF الفرعي عدم تفويت أي أحداث (لكي نكون منصفين، هذا ليس صحيحًا بالنسبة لجميع أنواع برامج BPF).

إن وجود مثل هذه القدرات يجعل BPF أداة عالمية لتوسيع النواة، وهو ما يتم تأكيده عمليًا: تتم إضافة المزيد والمزيد من أنواع البرامج الجديدة إلى BPF، وتستخدم المزيد والمزيد من الشركات الكبيرة BPF على الخوادم القتالية 24 × 7، والمزيد والمزيد تبني الشركات الناشئة أعمالها على حلول تعتمد على BPF. يتم استخدام BPF في كل مكان: للحماية من هجمات DDoS، وإنشاء SDN (على سبيل المثال، تنفيذ شبكات لـ kubernetes)، كأداة رئيسية لتتبع النظام وجامع الإحصائيات، وفي أنظمة كشف التسلل وأنظمة وضع الحماية، وما إلى ذلك.

دعونا ننتهي من جزء النظرة العامة من المقالة هنا ونلقي نظرة على الجهاز الظاهري ونظام BPF البيئي بمزيد من التفاصيل.

الاستطراد: المرافق

لتتمكن من تشغيل الأمثلة في الأقسام التالية، قد تحتاج إلى عدد من الأدوات المساعدة، على الأقل llvm/clang مع دعم bpf و bpftool. في القسم ادوات التطوير يمكنك قراءة التعليمات الخاصة بتجميع الأدوات المساعدة، بالإضافة إلى النواة الخاصة بك. تم وضع هذا القسم أدناه حتى لا يخل بتناغم عرضنا.

BPF سجلات الجهاز الظاهري ونظام التعليمات

تم تطوير البنية ونظام الأوامر لـ BPF مع الأخذ في الاعتبار حقيقة أن البرامج ستتم كتابتها بلغة C، وبعد تحميلها في النواة، سيتم ترجمتها إلى الكود الأصلي. لذلك، تم اختيار عدد السجلات ومجموعة الأوامر مع الأخذ في الاعتبار التقاطع، بالمعنى الرياضي، بين قدرات الآلات الحديثة. بالإضافة إلى ذلك، تم فرض قيود مختلفة على البرامج، على سبيل المثال، حتى وقت قريب لم يكن من الممكن كتابة الحلقات والإجراءات الفرعية، وكان عدد التعليمات يقتصر على 4096 (الآن يمكن للبرامج المميزة تحميل ما يصل إلى مليون تعليمات).

يحتوي BPF على أحد عشر سجلاً 64 بت يمكن للمستخدم الوصول إليها r0-r10 وعداد البرنامج . يسجل r10 يحتوي على مؤشر إطار وهو للقراءة فقط. تتمتع البرامج بإمكانية الوصول إلى مكدس سعة 512 بايت في وقت التشغيل وكمية غير محدودة من الذاكرة المشتركة في شكل خرائط.

يُسمح لبرامج BPF بتشغيل مجموعة محددة من مساعدات kernel من نوع البرنامج، ومؤخرًا، تشغيل الوظائف العادية. يمكن لكل دالة مستدعى أن تأخذ ما يصل إلى خمس وسيطات، تم تمريرها في السجلات r1-r5، ويتم تمرير قيمة الإرجاع إلى r0. ويضمن أنه بعد العودة من الوظيفة، محتويات السجلات r6-r9 لن تتغير.

للحصول على ترجمة فعالة للبرنامج، قم بالتسجيل r0-r11 لجميع البنيات المدعومة يتم تعيينها بشكل فريد للسجلات الحقيقية، مع الأخذ في الاعتبار ميزات ABI للبنية الحالية. على سبيل المثال، ل x86_64 السجلات r1-r5، المستخدمة لتمرير معلمات الوظيفة، يتم عرضها rdi, rsi, rdx, rcx, r8، والتي تستخدم لتمرير المعلمات إلى الوظائف x86_64. على سبيل المثال، الكود الموجود على اليسار يترجم إلى الكود الموجود على اليمين مثل هذا:

1:  (b7) r1 = 1                    mov    $0x1,%rdi
2:  (b7) r2 = 2                    mov    $0x2,%rsi
3:  (b7) r3 = 3                    mov    $0x3,%rdx
4:  (b7) r4 = 4                    mov    $0x4,%rcx
5:  (b7) r5 = 5                    mov    $0x5,%r8
6:  (85) call pc+1                 callq  0x0000000000001ee8

Регистр r0 يستخدم أيضًا لإرجاع نتيجة تنفيذ البرنامج وفي السجل r1 يُمرر البرنامج مؤشرًا إلى السياق - اعتمادًا على نوع البرنامج، قد يكون هذا، على سبيل المثال، بنية struct xdp_md (لـ XDP) أو الهيكل struct __sk_buff (لبرامج الشبكة المختلفة) أو الهيكل struct pt_regs (لأنواع مختلفة من برامج التتبع)، الخ.

لذلك، كان لدينا مجموعة من السجلات، ومساعدي kernel، ومكدس، ومؤشر السياق، والذاكرة المشتركة في شكل خرائط. لا يعني ذلك أن كل هذا ضروري للغاية في الرحلة، ولكن...

دعنا نواصل الوصف ونتحدث عن نظام الأوامر للعمل مع هذه الكائنات. الجميع (الكل تقريبا) تعليمات BPF لها حجم ثابت 64 بت. إذا نظرت إلى تعليمات واحدة على جهاز Big Endian 64 بت، فسوف ترى ذلك

BPF للصغار ، الجزء الأول: BPF الممتد

ومن Code - هذا هو ترميز التعليمات، Dst/Src هي ترميزات جهاز الاستقبال والمصدر، على التوالي، Off - مسافة بادئة موقعة 16 بت، و Imm هو عدد صحيح موقّع 32 بت يُستخدم في بعض التعليمات (على غرار ثابت cBPF K). التشفير Code له أحد نوعين:

BPF للصغار ، الجزء الأول: BPF الممتد

تحدد فئات التعليمات 0، 1، 2، 3 أوامر العمل مع الذاكرة. هم تسمى, BPF_LD, BPF_LDX, BPF_ST, BPF_STX، على التوالى. الصفوف 4، 7 (BPF_ALU, BPF_ALU64) تشكل مجموعة من تعليمات ALU. الصفوف 5، 6 (BPF_JMP, BPF_JMP32) تحتوي على تعليمات القفز.

الخطة الإضافية لدراسة نظام تعليمات BPF هي كما يلي: بدلاً من إدراج جميع التعليمات ومعاييرها بدقة، سننظر إلى بعض الأمثلة في هذا القسم ومنهما سيتضح كيف تعمل التعليمات فعليًا وكيفية تنفيذها. تفكيك أي ملف ثنائي لـ BPF يدويًا. لتوحيد المواد لاحقًا في المقالة، سنلتقي أيضًا بالتعليمات الفردية في الأقسام المتعلقة بـ Verifier، ومترجم JIT، وترجمة BPF الكلاسيكية، وكذلك عند دراسة الخرائط، ووظائف الاتصال، وما إلى ذلك.

عندما نتحدث عن التعليمات الفردية، سنشير إلى الملفات الأساسية bpf.h и bpf_common.h، والتي تحدد الرموز الرقمية لتعليمات BPF. عند دراسة الهندسة المعمارية بنفسك و/أو تحليل الثنائيات، يمكنك العثور على دلالات في المصادر التالية، مرتبة حسب التعقيد: مواصفات eBPF غير الرسمية, الدليل المرجعي BPF وXDP، مجموعة التعليمات, التوثيق/الشبكات/filter.txt وبالطبع، في شفرات المصدر Linux — مدقق، مترجم JIT، مترجم BPF.

مثال: تفكيك BPF في رأسك

دعونا نلقي نظرة على مثال نقوم فيه بتجميع البرنامج readelf-example.c وإلقاء نظرة على الثنائي الناتج. سوف نكشف عن المحتوى الأصلي readelf-example.c أدناه، بعد أن نستعيد منطقه من الرموز الثنائية:

$ clang -target bpf -c readelf-example.c -o readelf-example.o -O2
$ llvm-readelf -x .text readelf-example.o
Hex dump of section '.text':
0x00000000 b7000000 01000000 15010100 00000000 ................
0x00000010 b7000000 02000000 95000000 00000000 ................

العمود الأول في الإخراج readelf هي مسافة بادئة وبالتالي يتكون برنامجنا من أربعة أوامر:

Code Dst Src Off  Imm
b7   0   0   0000 01000000
15   0   1   0100 00000000
b7   0   0   0000 02000000
95   0   0   0000 00000000

رموز الأوامر متساوية b7, 15, b7 и 95. تذكر أن البتات الثلاثة الأقل أهمية هي فئة التعليمات. في حالتنا، البت الرابع من جميع التعليمات يكون فارغًا، وبالتالي فإن فئات التعليمات هي 7، 5، 7، 5، على التوالي. BPF_ALU64، و5 هو BPF_JMP. بالنسبة لكلا الفئتين، تنسيق التعليمات هو نفسه (انظر أعلاه) ويمكننا إعادة كتابة برنامجنا مثل هذا (في نفس الوقت سنعيد كتابة الأعمدة المتبقية في شكل بشري):

Op S  Class   Dst Src Off  Imm
b  0  ALU64   0   0   0    1
1  0  JMP     0   1   1    0
b  0  ALU64   0   0   0    2
9  0  JMP     0   0   0    0

عملية b فئة ALU64 - هو BPF_MOV. يقوم بتعيين قيمة لسجل الوجهة. إذا تم تعيين قليلا s (المصدر) فتؤخذ القيمة من السجل المصدر وإذا لم يتم ضبطها كما في حالتنا فتؤخذ القيمة من الحقل Imm. لذلك في التعليمات الأولى والثالثة نقوم بتنفيذ العملية r0 = Imm. علاوة على ذلك، فإن عملية JMP من الفئة 1 هي BPF_JEQ (القفز إذا كان متساويا). في حالتنا، منذ بت S صفر، فهو يقارن قيمة السجل المصدر مع الحقل Imm. فإذا تقابلت القيم حدث التحول إلى PC + Offحيث PCكالعادة، يحتوي على عنوان التعليمة التالية. أخيرًا، عملية JMP Class 9 هي BPF_EXIT. تؤدي هذه التعليمات إلى إنهاء البرنامج والعودة إلى النواة r0. دعونا نضيف عمودًا جديدًا إلى جدولنا:

Op    S  Class   Dst Src Off  Imm    Disassm
MOV   0  ALU64   0   0   0    1      r0 = 1
JEQ   0  JMP     0   1   1    0      if (r1 == 0) goto pc+1
MOV   0  ALU64   0   0   0    2      r0 = 2
EXIT  0  JMP     0   0   0    0      exit

يمكننا إعادة كتابة هذا في شكل أكثر ملاءمة:

     r0 = 1
     if (r1 == 0) goto END
     r0 = 2
END:
     exit

إذا كنا نتذكر ما هو موجود في السجل r1 يتم تمرير البرنامج مؤشر إلى السياق من النواة، وفي السجل r0 يتم إرجاع القيمة إلى النواة، ثم يمكننا أن نرى أنه إذا كان المؤشر إلى السياق صفرًا، فإننا نعيد 1، وإلا - 2. دعونا نتحقق من أننا على حق من خلال النظر إلى المصدر:

$ cat readelf-example.c
int foo(void *ctx)
{
        return ctx ? 2 : 1;
}

نعم، إنه برنامج لا معنى له، لكنه يترجم إلى أربعة تعليمات بسيطة فقط.

مثال الاستثناء: تعليمات 16 بايت

لقد ذكرنا سابقًا أن بعض التعليمات تشغل أكثر من 64 بت. وهذا ينطبق، على سبيل المثال، على التعليمات lddw (الكود = 0x18 = BPF_LD | BPF_DW | BPF_IMM) - قم بتحميل كلمة مزدوجة من الحقول إلى السجل Imm. والحقيقة هي ذلك Imm يبلغ حجمه 32، والكلمة المزدوجة هي 64 بت، لذا فإن تحميل قيمة فورية 64 بت في السجل في تعليمات واحدة 64 بت لن ينجح. للقيام بذلك، يتم استخدام تعليماتين متجاورتين لتخزين الجزء الثاني من قيمة 64 بت في الحقل Imm. مثال:

$ cat x64.c
long foo(void *ctx)
{
        return 0x11223344aabbccdd;
}
$ clang -target bpf -c x64.c -o x64.o -O2
$ llvm-readelf -x .text x64.o
Hex dump of section '.text':
0x00000000 18000000 ddccbbaa 00000000 44332211 ............D3".
0x00000010 95000000 00000000                   ........

هناك تعليمتان فقط في البرنامج الثنائي:

Binary                                 Disassm
18000000 ddccbbaa 00000000 44332211    r0 = Imm[0]|Imm[1]
95000000 00000000                      exit

سنلتقي مرة أخرى بالتعليمات lddwعندما نتحدث عن عمليات النقل والعمل بالخرائط.

مثال: تفكيك BPF باستخدام الأدوات القياسية

لذلك، تعلمنا قراءة رموز BPF الثنائية ونحن على استعداد لتحليل أي تعليمات إذا لزم الأمر. ومع ذلك، تجدر الإشارة إلى أنه من الناحية العملية يكون تفكيك البرامج أكثر ملاءمة وأسرع باستخدام الأدوات القياسية، على سبيل المثال:

$ llvm-objdump -d x64.o

Disassembly of section .text:

0000000000000000 <foo>:
 0: 18 00 00 00 dd cc bb aa 00 00 00 00 44 33 22 11 r0 = 1234605617868164317 ll
 2: 95 00 00 00 00 00 00 00 exit

دورة حياة كائنات BPF ونظام الملفات bpffs

(لقد تعلمت لأول مرة بعض التفاصيل الموضحة في هذا القسم الفرعي من صيام أليكسي ستاروفويتوف مدونة بي بي إف.)

يتم إنشاء كائنات BPF - البرامج والخرائط - من مساحة المستخدم باستخدام الأوامر BPF_PROG_LOAD и BPF_MAP_CREATE مكالمة النظام bpf(2)سنتحدث عن كيفية حدوث ذلك بالضبط في القسم التالي. يؤدي هذا إلى إنشاء هياكل بيانات kernel لكل منها refcount (العدد المرجعي) يتم تعيينه على واحد، ويتم إرجاع واصف الملف الذي يشير إلى الكائن إلى المستخدم. بعد إغلاق المقبض refcount يتم تقليل الكائن بمقدار واحد، وعندما يصل إلى الصفر، يتم تدمير الكائن.

إذا كان البرنامج يستخدم الخرائط، إذن refcount ويتم زيادة هذه الخرائط بمقدار واحدة بعد تحميل البرنامج، أي. يمكن إغلاق واصفات الملفات الخاصة بهم من عملية المستخدم ولا يزال refcount لن تصبح صفراً:

BPF للصغار ، الجزء الأول: BPF الممتد

بعد تحميل البرنامج بنجاح، عادةً ما نقوم بإرفاقه بنوع ما من مولدات الأحداث. على سبيل المثال، يمكننا وضعه على واجهة الشبكة لمعالجة الحزم الواردة أو توصيلها ببعضها tracepoint في القلب. عند هذه النقطة، سيزيد أيضًا العداد المرجعي بمقدار واحد وسنتمكن من إغلاق واصف الملف في برنامج التحميل.

ماذا يحدث إذا قمنا الآن بإيقاف تشغيل أداة تحميل التشغيل؟ يعتمد ذلك على نوع منشئ الحدث (الخطاف). ستكون جميع خطافات الشبكة موجودة بعد اكتمال المُحمل، وهي ما يسمى بالخطافات العامة. على سبيل المثال، سيتم إصدار برامج التتبع بعد انتهاء العملية التي أنشأتها (وبالتالي تسمى محلية، من "محلية إلى العملية"). من الناحية الفنية، تحتوي الخطافات المحلية دائمًا على واصف ملف مطابق في مساحة المستخدم وبالتالي يتم إغلاقها عند إغلاق العملية، لكن الخطافات العامة لا تفعل ذلك. في الشكل التالي، باستخدام علامة الصليب الأحمر، أحاول إظهار كيف يؤثر إنهاء برنامج التحميل على عمر الكائنات في حالة الخطافات المحلية والعالمية.

BPF للصغار ، الجزء الأول: BPF الممتد

لماذا هناك فرق بين الخطافات المحلية والعالمية؟ يعد تشغيل بعض أنواع برامج الشبكة أمرًا منطقيًا بدون مساحة مستخدم، على سبيل المثال، تخيل حماية DDoS - يقوم أداة تحميل التشغيل بكتابة القواعد وتوصيل برنامج BPF بواجهة الشبكة، وبعد ذلك يمكن لمحمل التشغيل أن يقتل نفسه. من ناحية أخرى، تخيل برنامج تتبع تصحيح الأخطاء الذي كتبته على ركبتيك في عشر دقائق - عند الانتهاء، ترغب في عدم ترك أي مخلفات في النظام، وستضمن الخطافات المحلية ذلك.

ومن ناحية أخرى، تخيل أنك تريد الاتصال بنقطة تتبع في النواة وجمع الإحصائيات على مدار سنوات عديدة. في هذه الحالة، قد ترغب في إكمال جزء المستخدم والعودة إلى الإحصائيات من وقت لآخر. يوفر نظام الملفات bpf هذه الفرصة. إنه نظام ملفات زائفة في الذاكرة فقط يسمح بإنشاء ملفات تشير إلى كائنات BPF وبالتالي زيادة refcount أشياء. بعد ذلك، يمكن للمحمل الخروج، وستظل الكائنات التي أنشأها على قيد الحياة.

BPF للصغار ، الجزء الأول: BPF الممتد

يُطلق على إنشاء الملفات في bpffs التي تشير إلى كائنات BPF اسم "التثبيت" (كما في العبارة التالية: "يمكن للعملية تثبيت برنامج أو خريطة BPF"). يعد إنشاء كائنات ملفات لكائنات BPF أمرًا منطقيًا ليس فقط لإطالة عمر الكائنات المحلية، ولكن أيضًا لسهولة استخدام الكائنات العالمية - وبالرجوع إلى مثال برنامج حماية DDoS العالمي، نريد أن نكون قادرين على الحضور وإلقاء نظرة على الإحصائيات من وقت لآخر.

عادةً ما يتم تثبيت نظام الملفات BPF /sys/fs/bpf، ولكن يمكن أيضًا تثبيته محليًا، على سبيل المثال، مثل هذا:

$ mkdir bpf-mountpoint
$ sudo mount -t bpf none bpf-mountpoint

يتم إنشاء أسماء نظام الملفات باستخدام الأمر BPF_OBJ_PIN استدعاء نظام BPF. للتوضيح، لنأخذ برنامجًا ونجمعه ونرفعه ونثبته عليه bpffs. برنامجنا لا يفعل أي شيء مفيد، نحن نقدم الكود فقط حتى تتمكن من إعادة إنتاج المثال:

$ cat test.c
__attribute__((section("xdp"), used))
int test(void *ctx)
{
        return 0;
}

char _license[] __attribute__((section("license"), used)) = "GPL";

لنقم بتجميع هذا البرنامج وإنشاء نسخة محلية من نظام الملفات bpffs:

$ clang -target bpf -c test.c -o test.o
$ mkdir bpf-mountpoint
$ sudo mount -t bpf none bpf-mountpoint

الآن لنقم بتنزيل برنامجنا باستخدام الأداة المساعدة bpftool وإلقاء نظرة على مكالمات النظام المصاحبة bpf(2) (تمت إزالة بعض الأسطر غير ذات الصلة من إخراج التتبع):

$ sudo strace -e bpf bpftool prog load ./test.o bpf-mountpoint/test
bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_XDP, prog_name="test", ...}, 120) = 3
bpf(BPF_OBJ_PIN, {pathname="bpf-mountpoint/test", bpf_fd=3}, 120) = 0

لقد قمنا هنا بتحميل البرنامج باستخدام BPF_PROG_LOAD، تلقى واصف ملف من kernel 3 واستخدام الأمر BPF_OBJ_PIN تم تثبيت واصف الملف هذا كملف "bpf-mountpoint/test". بعد هذا برنامج البوت لودر bpftool انتهينا من العمل، ولكن بقي برنامجنا في النواة، على الرغم من أننا لم نربطه بأي واجهة شبكة:

$ sudo bpftool prog | tail -3
783: xdp  name test  tag 5c8ba0cf164cb46c  gpl
        loaded_at 2020-05-05T13:27:08+0000  uid 0
        xlated 24B  jited 41B  memlock 4096B

يمكننا حذف كائن الملف بشكل طبيعي unlink(2) وبعد ذلك سيتم حذف البرنامج المقابل:

$ sudo rm ./bpf-mountpoint/test
$ sudo bpftool prog show id 783
Error: get by id (783): No such file or directory

حذف الكائنات

عند الحديث عن حذف الكائنات، من الضروري توضيح أنه بعد أن قمنا بفصل البرنامج عن الخطاف (مولد الأحداث)، لن يؤدي أي حدث جديد إلى إطلاقه، ومع ذلك، سيتم إكمال جميع المثيلات الحالية للبرنامج بالترتيب الطبيعي .

تسمح لك بعض أنواع برامج BPF باستبدال البرنامج بسرعة، أي. توفير ذرية التسلسل replace = detach old program, attach new program. في هذه الحالة، ستنتهي جميع النسخ النشطة من الإصدار القديم من البرنامج من عملها، وسيتم إنشاء معالجات أحداث جديدة من البرنامج الجديد، وكلمة "atomicity" هنا تعني أنه لن يتم تفويت أي حدث.

ربط البرامج بمصادر الأحداث

في هذه المقالة، لن نصف بشكل منفصل ربط البرامج بمصادر الأحداث، لأنه من المنطقي دراسة ذلك في سياق نوع معين من البرامج. سم. مثال أدناه، حيث نعرض كيفية اتصال برامج مثل XDP.

معالجة الكائنات باستخدام استدعاء نظام bpf

برامج بي بي إف

يتم إنشاء جميع كائنات BPF وإدارتها من مساحة المستخدم باستخدام استدعاء النظام bpf، مع النموذج الأولي التالي:

#include <linux/bpf.h>

int bpf(int cmd, union bpf_attr *attr, unsigned int size);

هنا الفريق cmd هي إحدى قيم النوع enum bpf_cmd, attr - مؤشر لمعلمات برنامج معين و size — حجم الكائن وفقًا للمؤشر، أي. عادة هذا sizeof(*attr). في النواة 5.8 يتم استدعاء النظام bpf يدعم 34 أمرًا مختلفًا، و تحديد union bpf_attr تحتل 200 سطر. لكن لا ينبغي لنا أن نخاف من هذا، لأننا سوف نتعرف على الأوامر والمعلمات على مدار عدة مقالات.

لنبدأ مع الفريق BPF_PROG_LOAD، الذي ينشئ برامج BPF - يأخذ مجموعة من تعليمات BPF ويحملها في النواة. في لحظة التحميل، يتم تشغيل أداة التحقق، ثم مترجم JIT، وبعد التنفيذ الناجح، يتم إرجاع واصف ملف البرنامج إلى المستخدم. لقد رأينا ما سيحدث له بعد ذلك في القسم السابق حول دورة حياة كائنات BPF.

سنقوم الآن بكتابة برنامج مخصص يقوم بتحميل برنامج BPF بسيط، ولكن علينا أولاً أن نقرر نوع البرنامج الذي نريد تحميله - سيتعين علينا تحديده نوع وفي إطار هذا النوع اكتب برنامجاً يجتاز اختبار المدقق. ومع ذلك، من أجل عدم تعقيد العملية، إليك الحل الجاهز: سنأخذ برنامجا مثل BPF_PROG_TYPE_XDP، والتي سوف تعيد القيمة XDP_PASS (تخطي كافة الحزم). في مجمع BPF يبدو الأمر بسيطًا جدًا:

r0 = 2
exit

بعد أن قررنا ذلك أن سنقوم بالتحميل، ويمكننا أن نخبرك كيف سنفعل ذلك:

#define _GNU_SOURCE
#include <string.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <linux/bpf.h>

static inline __u64 ptr_to_u64(const void *ptr)
{
        return (__u64) (unsigned long) ptr;
}

int main(void)
{
    struct bpf_insn insns[] = {
        {
            .code = BPF_ALU64 | BPF_MOV | BPF_K,
            .dst_reg = BPF_REG_0,
            .imm = XDP_PASS
        },
        {
            .code = BPF_JMP | BPF_EXIT
        },
    };

    union bpf_attr attr = {
        .prog_type = BPF_PROG_TYPE_XDP,
        .insns     = ptr_to_u64(insns),
        .insn_cnt  = sizeof(insns)/sizeof(insns[0]),
        .license   = ptr_to_u64("GPL"),
    };

    strncpy(attr.prog_name, "woo", sizeof(attr.prog_name));
    syscall(__NR_bpf, BPF_PROG_LOAD, &attr, sizeof(attr));

    for ( ;; )
        pause();
}

تبدأ الأحداث المثيرة للاهتمام في البرنامج بتعريف المصفوفة insns - برنامج BPF الخاص بنا في كود الآلة. في هذه الحالة، يتم تعبئة كل تعليمات برنامج BPF في الهيكل bpf_insn. العنصر الأول insns يتوافق مع التعليمات r0 = 2، ثانية - exit.

تراجع. تحدد النواة وحدات ماكرو أكثر ملاءمة لكتابة رموز الآلة واستخدام ملف رأس النواة tools/include/linux/filter.h يمكننا أن نكتب

struct bpf_insn insns[] = {
    BPF_MOV64_IMM(BPF_REG_0, XDP_PASS),
    BPF_EXIT_INSN()
};

ولكن نظرًا لأن كتابة برامج BPF بالكود الأصلي ضرورية فقط لكتابة الاختبارات في النواة والمقالات حول BPF، فإن غياب وحدات الماكرو هذه لا يؤدي حقًا إلى تعقيد حياة المطور.

بعد تعريف برنامج BPF، ننتقل إلى تحميله في النواة. لدينا مجموعة الحد الأدنى من المعلمات attr يتضمن نوع البرنامج ومجموعة التعليمات وعددها والترخيص المطلوب والاسم "woo"الذي نستخدمه للعثور على برنامجنا على النظام بعد تنزيله. يتم تحميل البرنامج، كما وعدنا، في النظام باستخدام استدعاء النظام bpf.

في نهاية البرنامج، ينتهي بنا الأمر في حلقة لا نهائية تحاكي الحمولة. وبدون ذلك سيتم قتل البرنامج من قبل النواة عندما يتم إغلاق واصف الملف الذي أعاده استدعاء النظام إلينا bpfولن نراها في النظام.

حسنًا، نحن جاهزون للاختبار. دعونا تجميع وتشغيل البرنامج تحت straceللتأكد من أن كل شيء يعمل كما ينبغي:

$ clang -g -O2 simple-prog.c -o simple-prog

$ sudo strace ./simple-prog
execve("./simple-prog", ["./simple-prog"], 0x7ffc7b553480 /* 13 vars */) = 0
...
bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_XDP, insn_cnt=2, insns=0x7ffe03c4ed50, license="GPL", log_level=0, log_size=0, log_buf=NULL, kern_version=KERNEL_V
ERSION(0, 0, 0), prog_flags=0, prog_name="woo", prog_ifindex=0, expected_attach_type=BPF_CGROUP_INET_INGRESS}, 72) = 3
pause(

كل شيء على ما يرام، bpf(2) أعاد لنا المقبض 3 وذهبنا في حلقة لا نهاية لها pause(). دعونا نحاول العثور على برنامجنا في النظام. للقيام بذلك، سوف نذهب إلى محطة أخرى ونستخدم الأداة المساعدة bpftool:

# bpftool prog | grep -A3 woo
390: xdp  name woo  tag 3b185187f1855c4c  gpl
        loaded_at 2020-08-31T24:66:44+0000  uid 0
        xlated 16B  jited 40B  memlock 4096B
        pids simple-prog(10381)

نرى أن هناك برنامجًا محملاً على النظام woo معرفه العالمي هو 390 وهو قيد التقدم حاليًا simple-prog يوجد واصف ملف مفتوح يشير إلى البرنامج (وإذا كان simple-prog سوف تنتهي من المهمة، ثم woo سوف تختفي). كما هو متوقع البرنامج woo يأخذ 16 بايت - تعليماتين - من الرموز الثنائية في بنية BPF، ولكن في شكله الأصلي (x86_64) يبلغ بالفعل 40 بايت. دعونا نلقي نظرة على برنامجنا في شكله الأصلي:

# bpftool prog dump xlated id 390
   0: (b7) r0 = 2
   1: (95) exit

لا مفاجآت. الآن دعونا نلقي نظرة على الكود الذي تم إنشاؤه بواسطة مترجم JIT:

# bpftool prog dump jited id 390
bpf_prog_3b185187f1855c4c_woo:
   0:   nopl   0x0(%rax,%rax,1)
   5:   push   %rbp
   6:   mov    %rsp,%rbp
   9:   sub    $0x0,%rsp
  10:   push   %rbx
  11:   push   %r13
  13:   push   %r14
  15:   push   %r15
  17:   pushq  $0x0
  19:   mov    $0x2,%eax
  1e:   pop    %rbx
  1f:   pop    %r15
  21:   pop    %r14
  23:   pop    %r13
  25:   pop    %rbx
  26:   leaveq
  27:   retq

ليست فعالة جدا ل exit(2)، ولكن في الإنصاف، برنامجنا بسيط للغاية، وبالنسبة للبرامج غير التافهة، هناك حاجة بالطبع إلى المقدمة والخاتمة التي يضيفها مترجم JIT.

برنامج Maps

يمكن لبرامج BPF استخدام مناطق الذاكرة المنظمة التي يمكن الوصول إليها من خلال برامج BPF الأخرى والبرامج الموجودة في مساحة المستخدم. تسمى هذه الكائنات بالخرائط وسنوضح في هذا القسم كيفية التعامل معها باستخدام استدعاء النظام bpf.

لنفترض على الفور أن إمكانيات الخرائط لا تقتصر فقط على الوصول إلى الذاكرة المشتركة. توجد خرائط ذات أغراض خاصة تحتوي، على سبيل المثال، على مؤشرات لبرامج BPF أو مؤشرات لواجهات الشبكة، وخرائط للعمل مع أحداث الأداء، وما إلى ذلك. ولن نتحدث عنهم هنا حتى لا نربك القارئ. وبصرف النظر عن هذا، فإننا نتجاهل مشكلات المزامنة، لأن هذا ليس مهمًا بالنسبة لأمثلتنا. يمكن العثور على قائمة كاملة بأنواع الخرائط المتاحة في <linux/bpf.h>، وسنأخذ في هذا القسم كمثال النوع الأول تاريخيًا، وهو جدول التجزئة BPF_MAP_TYPE_HASH.

إذا قمت بإنشاء جدول تجزئة في C++، على سبيل المثال، ستقول unordered_map<int,long> wooوالتي تعني باللغة الروسية "أحتاج إلى طاولة woo حجم غير محدود، ومفاتيحه من النوع intوالقيم هي النوع long" من أجل إنشاء جدول تجزئة BPF، نحتاج إلى القيام بنفس الشيء تقريبًا، باستثناء أنه يتعين علينا تحديد الحد الأقصى لحجم الجدول، وبدلاً من تحديد أنواع المفاتيح والقيم، نحتاج إلى تحديد أحجامها بالبايت . لإنشاء خرائط استخدم الأمر BPF_MAP_CREATE مكالمة النظام bpf. دعونا نلقي نظرة على برنامج بسيط إلى حد ما يقوم بإنشاء خريطة. بعد البرنامج السابق الذي يقوم بتحميل برامج BPF، يجب أن يبدو هذا البرنامج بسيطًا بالنسبة لك:

$ cat simple-map.c
#define _GNU_SOURCE
#include <string.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <linux/bpf.h>

int main(void)
{
    union bpf_attr attr = {
        .map_type = BPF_MAP_TYPE_HASH,
        .key_size = sizeof(int),
        .value_size = sizeof(int),
        .max_entries = 4,
    };
    strncpy(attr.map_name, "woo", sizeof(attr.map_name));
    syscall(__NR_bpf, BPF_MAP_CREATE, &attr, sizeof(attr));

    for ( ;; )
        pause();
}

هنا نحدد مجموعة من المعلمات attrحيث نقول "أحتاج إلى جدول تجزئة يحتوي على مفاتيح وقيم الحجم sizeof(int)، حيث يمكنني وضع أربعة عناصر كحد أقصى." عند إنشاء خرائط BPF، يمكنك تحديد معلمات أخرى، على سبيل المثال، بنفس الطريقة كما في المثال مع البرنامج، حددنا اسم الكائن كما "woo".

لنقم بتجميع البرنامج وتشغيله:

$ clang -g -O2 simple-map.c -o simple-map
$ sudo strace ./simple-map
execve("./simple-map", ["./simple-map"], 0x7ffd40a27070 /* 14 vars */) = 0
...
bpf(BPF_MAP_CREATE, {map_type=BPF_MAP_TYPE_HASH, key_size=4, value_size=4, max_entries=4, map_name="woo", ...}, 72) = 3
pause(

وهنا نداء النظام bpf(2) أعاد لنا رقم الخريطة الواصف 3 وبعد ذلك، كما هو متوقع، ينتظر البرنامج المزيد من التعليمات في استدعاء النظام pause(2).

الآن دعنا نرسل برنامجنا إلى الخلفية أو نفتح محطة أخرى وننظر إلى كائننا باستخدام الأداة المساعدة bpftool (يمكننا تمييز خريطتنا عن الخرائط الأخرى باسمها):

$ sudo bpftool map
...
114: hash  name woo  flags 0x0
        key 4B  value 4B  max_entries 4  memlock 4096B
...

الرقم 114 هو المعرف العام لجسمنا. يمكن لأي برنامج على النظام استخدام هذا المعرف لفتح خريطة موجودة باستخدام الأمر BPF_MAP_GET_FD_BY_ID مكالمة النظام bpf.

الآن يمكننا اللعب بجدول التجزئة الخاص بنا. دعونا ننظر إلى محتوياته:

$ sudo bpftool map dump id 114
Found 0 elements

فارغ. دعونا نضع قيمة فيه hash[1] = 1:

$ sudo bpftool map update id 114 key 1 0 0 0 value 1 0 0 0

دعونا ننظر إلى الجدول مرة أخرى:

$ sudo bpftool map dump id 114
key: 01 00 00 00  value: 01 00 00 00
Found 1 element

مرحا! تمكنا من إضافة عنصر واحد. لاحظ أنه يتعين علينا العمل على مستوى البايت للقيام بذلك، منذ ذلك الحين bptftool لا يعرف نوع القيم الموجودة في جدول التجزئة. (يمكن نقل هذه المعرفة إليها باستخدام BTF، ولكن المزيد عن ذلك الآن.)

كيف يقوم bpftool بقراءة العناصر وإضافتها بالضبط؟ دعونا نلقي نظرة تحت غطاء محرك السيارة:

$ sudo strace -e bpf bpftool map dump id 114
bpf(BPF_MAP_GET_FD_BY_ID, {map_id=114, next_id=0, open_flags=0}, 120) = 3
bpf(BPF_MAP_GET_NEXT_KEY, {map_fd=3, key=NULL, next_key=0x55856ab65280}, 120) = 0
bpf(BPF_MAP_LOOKUP_ELEM, {map_fd=3, key=0x55856ab65280, value=0x55856ab652a0}, 120) = 0
key: 01 00 00 00  value: 01 00 00 00
bpf(BPF_MAP_GET_NEXT_KEY, {map_fd=3, key=0x55856ab65280, next_key=0x55856ab65280}, 120) = -1 ENOENT

أولاً قمنا بفتح الخريطة بمعرفها العالمي باستخدام الأمر BPF_MAP_GET_FD_BY_ID и bpf(2) أعاد إلينا الواصف 3. ثم استخدم الأمر BPF_MAP_GET_NEXT_KEY لقد وجدنا المفتاح الأول في الجدول بالتمرير NULL كمؤشر للمفتاح "السابق". إذا كان لدينا المفتاح يمكننا القيام به BPF_MAP_LOOKUP_ELEMالذي يُرجع قيمة إلى المؤشر value. الخطوة التالية هي أن نحاول العثور على العنصر التالي عن طريق تمرير مؤشر إلى المفتاح الحالي، لكن جدولنا يحتوي فقط على عنصر واحد وهو الأمر BPF_MAP_GET_NEXT_KEY عائدات ENOENT.

حسنًا، دعنا نغير القيمة حسب المفتاح 1، لنفترض أن منطق أعمالنا يتطلب التسجيل hash[1] = 2:

$ sudo strace -e bpf bpftool map update id 114 key 1 0 0 0 value 2 0 0 0
bpf(BPF_MAP_GET_FD_BY_ID, {map_id=114, next_id=0, open_flags=0}, 120) = 3
bpf(BPF_MAP_UPDATE_ELEM, {map_fd=3, key=0x55dcd72be260, value=0x55dcd72be280, flags=BPF_ANY}, 120) = 0

كما هو متوقع، الأمر بسيط جدًا: الأمر BPF_MAP_GET_FD_BY_ID يفتح خريطتنا عن طريق المعرف، والأمر BPF_MAP_UPDATE_ELEM بالكتابة فوق العنصر.

لذلك، بعد إنشاء جدول التجزئة من برنامج واحد، يمكننا قراءة وكتابة محتوياته من برنامج آخر. لاحظ أنه إذا تمكنا من القيام بذلك من سطر الأوامر، فإن أي برنامج آخر على النظام يمكنه القيام بذلك. بالإضافة إلى الأوامر الموضحة أعلاه، للعمل مع الخرائط من مساحة المستخدم، ما يلي:

  • BPF_MAP_LOOKUP_ELEM: العثور على القيمة عن طريق المفتاح
  • BPF_MAP_UPDATE_ELEM: تحديث/إنشاء القيمة
  • BPF_MAP_DELETE_ELEM: إزالة المفتاح
  • BPF_MAP_GET_NEXT_KEY: ابحث عن المفتاح التالي (أو الأول).
  • BPF_MAP_GET_NEXT_ID: يتيح لك تصفح جميع الخرائط الموجودة، وهذه هي الطريقة التي تعمل بها bpftool map
  • BPF_MAP_GET_FD_BY_ID: افتح خريطة موجودة بواسطة معرفها العالمي
  • BPF_MAP_LOOKUP_AND_DELETE_ELEM: تحديث قيمة الكائن ذريًا وإرجاع القيمة القديمة
  • BPF_MAP_FREEZE: جعل الخريطة غير قابلة للتغيير من مساحة المستخدم (لا يمكن التراجع عن هذه العملية)
  • BPF_MAP_LOOKUP_BATCH, BPF_MAP_LOOKUP_AND_DELETE_BATCH, BPF_MAP_UPDATE_BATCH, BPF_MAP_DELETE_BATCH: العمليات الجماعية. على سبيل المثال، BPF_MAP_LOOKUP_AND_DELETE_BATCH - هذه هي الطريقة الوحيدة الموثوقة لقراءة وإعادة تعيين جميع القيم من الخريطة

لا تعمل كل هذه الأوامر مع جميع أنواع الخرائط، ولكن بشكل عام، يبدو العمل مع أنواع الخرائط الأخرى من مساحة المستخدم مماثلًا تمامًا للعمل مع جداول التجزئة.

من أجل النظام، دعونا ننهي تجارب جدول التجزئة. هل تتذكر أننا أنشأنا جدولًا يمكن أن يحتوي على ما يصل إلى أربعة مفاتيح؟ دعونا نضيف بعض العناصر الأخرى:

$ sudo bpftool map update id 114 key 2 0 0 0 value 1 0 0 0
$ sudo bpftool map update id 114 key 3 0 0 0 value 1 0 0 0
$ sudo bpftool map update id 114 key 4 0 0 0 value 1 0 0 0

حتى الان جيدة جدا:

$ sudo bpftool map dump id 114
key: 01 00 00 00  value: 01 00 00 00
key: 02 00 00 00  value: 01 00 00 00
key: 04 00 00 00  value: 01 00 00 00
key: 03 00 00 00  value: 01 00 00 00
Found 4 elements

دعونا نحاول إضافة واحد آخر:

$ sudo bpftool map update id 114 key 5 0 0 0 value 1 0 0 0
Error: update failed: Argument list too long

وكما كان متوقعا، لم ننجح. دعونا نلقي نظرة على الخطأ بمزيد من التفاصيل:

$ sudo strace -e bpf bpftool map update id 114 key 5 0 0 0 value 1 0 0 0
bpf(BPF_MAP_GET_FD_BY_ID, {map_id=114, next_id=0, open_flags=0}, 120) = 3
bpf(BPF_OBJ_GET_INFO_BY_FD, {info={bpf_fd=3, info_len=80, info=0x7ffe6c626da0}}, 120) = 0
bpf(BPF_MAP_UPDATE_ELEM, {map_fd=3, key=0x56049ded5260, value=0x56049ded5280, flags=BPF_ANY}, 120) = -1 E2BIG (Argument list too long)
Error: update failed: Argument list too long
+++ exited with 255 +++

كل شيء على ما يرام: كما هو متوقع، الفريق BPF_MAP_UPDATE_ELEM يحاول إنشاء مفتاح خامس جديد، لكنه يتعطل E2BIG.

لذا، يمكننا إنشاء برامج BPF وتحميلها، بالإضافة إلى إنشاء الخرائط وإدارتها من مساحة المستخدم. من المنطقي الآن أن ننظر إلى كيفية استخدام الخرائط من برامج BPF نفسها. يمكننا أن نتحدث عن هذا بلغة البرامج التي يصعب قراءتها في رموز ماكرو الآلة، ولكن في الواقع حان الوقت لإظهار كيفية كتابة برامج BPF وصيانتها فعليًا - باستخدام libbpf.

(بالنسبة للقراء غير الراضين عن عدم وجود مثال منخفض المستوى: سنقوم بتحليل مفصل للبرامج التي تستخدم الخرائط والوظائف المساعدة التي تم إنشاؤها باستخدام libbpf وأخبرك بما يحدث على مستوى التدريس. للقراء الذين هم غير راضين كثيرا جداأضفنا مثال في المكان المناسب من المقال.)

كتابة برامج BPF باستخدام libbpf

يمكن أن تكون كتابة برامج BPF باستخدام رموز الآلة مثيرة للاهتمام فقط في المرة الأولى، ثم يبدأ الشبع. في هذه اللحظة عليك أن تحول انتباهك إلى llvm، والتي تحتوي على واجهة خلفية لإنشاء التعليمات البرمجية لبنية BPF، بالإضافة إلى مكتبة libbpf، والذي يسمح لك بكتابة جانب المستخدم لتطبيقات BPF وتحميل كود برامج BPF التي تم إنشاؤها باستخدام llvm/clang.

في الواقع، كما سنرى في هذه المقالة والمقالات اللاحقة، libbpf يقوم بالكثير من العمل بدونه (أو أدوات مماثلة - iproute2, libbcc, libbpf-go، وما إلى ذلك) من المستحيل أن تعيش. واحدة من السمات القاتلة للمشروع libbpf هو BPF CO-RE (ترجمة مرة واحدة، تشغيل في كل مكان) - مشروع يسمح لك بكتابة برامج BPF المحمولة من نواة إلى أخرى، مع إمكانية التشغيل على واجهات برمجة تطبيقات مختلفة (على سبيل المثال، عندما تتغير بنية النواة من الإصدار إلى الإصدار). لكي تتمكن من العمل مع CO-RE، يجب أن يتم تجميع النواة الخاصة بك بدعم BTF (وصفنا كيفية القيام بذلك في القسم ادوات التطوير. يمكنك التحقق مما إذا كانت النواة الخاصة بك مبنية باستخدام BTF أم لا بكل بساطة - من خلال وجود الملف التالي:

$ ls -lh /sys/kernel/btf/vmlinux
-r--r--r-- 1 root root 2.6M Jul 29 15:30 /sys/kernel/btf/vmlinux

يقوم هذا الملف بتخزين معلومات حول جميع أنواع البيانات المستخدمة في النواة ويتم استخدامه في جميع الأمثلة التي لدينا باستخدام libbpf. سنتحدث بالتفصيل عن CO-RE في المقالة التالية، ولكن في هذه المقالة - فقط قم ببناء نواة لنفسك CONFIG_DEBUG_INFO_BTF.

مكتبة libbpf يعيش الحق في الدليل tools/lib/bpf النواة وتطويرها يتم من خلال القائمة البريدية bpf@vger.kernel.org. ومع ذلك، يتم الاحتفاظ بمستودع منفصل لتلبية احتياجات التطبيقات الموجودة خارج النواة https://github.com/libbpf/libbpf حيث يتم عكس مكتبة kernel للوصول للقراءة بشكل أو بآخر كما هي.

في هذا القسم سننظر في كيفية إنشاء مشروع يستخدم libbpf، دعنا نكتب العديد من برامج الاختبار (التي لا معنى لها إلى حد ما) ونحلل بالتفصيل كيفية عمل كل شيء. سيسمح لنا هذا أن نشرح بسهولة أكبر في الأقسام التالية كيفية تفاعل برامج BPF مع الخرائط ومساعدي kernel وBTF وما إلى ذلك.

عادة ما تستخدم المشاريع libbpf أضف مستودع GitHub كوحدة فرعية لـ git، وسنفعل الشيء نفسه:

$ mkdir /tmp/libbpf-example
$ cd /tmp/libbpf-example/
$ git init-db
Initialized empty Git repository in /tmp/libbpf-example/.git/
$ git submodule add https://github.com/libbpf/libbpf.git
Cloning into '/tmp/libbpf-example/libbpf'...
remote: Enumerating objects: 200, done.
remote: Counting objects: 100% (200/200), done.
remote: Compressing objects: 100% (103/103), done.
remote: Total 3354 (delta 101), reused 118 (delta 79), pack-reused 3154
Receiving objects: 100% (3354/3354), 2.05 MiB | 10.22 MiB/s, done.
Resolving deltas: 100% (2176/2176), done.

الذهاب الى libbpf بسيط جدا:

$ cd libbpf/src
$ mkdir build
$ OBJDIR=build DESTDIR=root make -s install
$ find root
root
root/usr
root/usr/include
root/usr/include/bpf
root/usr/include/bpf/bpf_tracing.h
root/usr/include/bpf/xsk.h
root/usr/include/bpf/libbpf_common.h
root/usr/include/bpf/bpf_endian.h
root/usr/include/bpf/bpf_helpers.h
root/usr/include/bpf/btf.h
root/usr/include/bpf/bpf_helper_defs.h
root/usr/include/bpf/bpf.h
root/usr/include/bpf/libbpf_util.h
root/usr/include/bpf/libbpf.h
root/usr/include/bpf/bpf_core_read.h
root/usr/lib64
root/usr/lib64/libbpf.so.0.1.0
root/usr/lib64/libbpf.so.0
root/usr/lib64/libbpf.a
root/usr/lib64/libbpf.so
root/usr/lib64/pkgconfig
root/usr/lib64/pkgconfig/libbpf.pc

خطتنا التالية في هذا القسم هي كما يلي: سنكتب برنامج BPF مثل BPF_PROG_TYPE_XDP، كما في المثال السابق، ولكن في لغة C، نقوم بتجميعه باستخدام clang، واكتب برنامجًا مساعدًا لتحميله في النواة. في الأقسام التالية سوف نقوم بتوسيع إمكانيات كل من برنامج BPF والبرنامج المساعد.

مثال: إنشاء تطبيق كامل باستخدام libbpf

للبدء، نستخدم الملف /sys/kernel/btf/vmlinux، والذي تم ذكره أعلاه، وإنشاء ما يعادله في شكل ملف رأس:

$ bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h

سيخزن هذا الملف جميع هياكل البيانات المتوفرة في النواة، على سبيل المثال، هذه هي الطريقة التي يتم بها تعريف رأس IPv4 في النواة:

$ grep -A 12 'struct iphdr {' vmlinux.h
struct iphdr {
    __u8 ihl: 4;
    __u8 version: 4;
    __u8 tos;
    __be16 tot_len;
    __be16 id;
    __be16 frag_off;
    __u8 ttl;
    __u8 protocol;
    __sum16 check;
    __be32 saddr;
    __be32 daddr;
};

الآن سوف نكتب برنامج BPF الخاص بنا بلغة C:

$ cat xdp-simple.bpf.c
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>

SEC("xdp/simple")
int simple(void *ctx)
{
        return XDP_PASS;
}

char LICENSE[] SEC("license") = "GPL";

على الرغم من أن برنامجنا بسيط للغاية، إلا أننا لا نزال بحاجة إلى الاهتمام بالعديد من التفاصيل. أولاً، أول ملف رأس نقوم بإدراجه هو vmlinux.h، والذي أنشأناه للتو باستخدام bpftool btf dump - الآن لا نحتاج إلى تثبيت حزمة kernel-headers لمعرفة الشكل الذي تبدو عليه هياكل kernel. يأتي ملف الرأس التالي إلينا من المكتبة libbpf. الآن نحتاجه فقط لتحديد الماكرو SEC، الذي يرسل الحرف إلى القسم المناسب من ملف كائن ELF. برنامجنا موجود في القسم xdp/simple، حيث نحدد قبل الشرطة المائلة نوع البرنامج BPF - وهذا هو الاصطلاح المستخدم فيه libbpf، بناءً على اسم القسم، سيتم استبدال النوع الصحيح عند بدء التشغيل bpf(2). برنامج BPF نفسه C - بسيط جدًا ويتكون من سطر واحد return XDP_PASS. وأخيرا قسم منفصل "license" يحتوي على اسم الترخيص.

يمكننا تجميع برنامجنا باستخدام llvm/clang، الإصدار >= 10.0.0، أو الأفضل من ذلك، إصدار أكبر (راجع القسم ادوات التطوير):

$ clang --version
clang version 11.0.0 (https://github.com/llvm/llvm-project.git afc287e0abec710398465ee1f86237513f2b5091)
...

$ clang -O2 -g -c -target bpf -I libbpf/src/root/usr/include xdp-simple.bpf.c -o xdp-simple.bpf.o

من بين الميزات المثيرة للاهتمام: نشير إلى البنية المستهدفة -target bpf والطريق إلى الرؤوس libbpf، والذي قمنا بتثبيته مؤخرًا. أيضا، لا تنسى -O2، وبدون هذا الخيار قد تواجه مفاجآت في المستقبل. دعونا نلقي نظرة على الكود الخاص بنا، هل تمكنا من كتابة البرنامج الذي أردناه؟

$ llvm-objdump --section=xdp/simple --no-show-raw-insn -D xdp-simple.bpf.o

xdp-simple.bpf.o:       file format elf64-bpf

Disassembly of section xdp/simple:

0000000000000000 <simple>:
       0:       r0 = 2
       1:       exit

نعم، عملت! الآن، لدينا ملف ثنائي مع البرنامج، ونريد إنشاء تطبيق يقوم بتحميله في النواة. ولهذا الغرض المكتبة libbpf يقدم لنا خيارين - استخدام واجهة برمجة التطبيقات ذات المستوى الأدنى أو واجهة برمجة التطبيقات ذات المستوى الأعلى. سنذهب في الاتجاه الثاني، لأننا نريد أن نتعلم كيفية كتابة وتحميل وتوصيل برامج BPF بأقل جهد لدراستها اللاحقة.

أولاً، نحتاج إلى إنشاء "الهيكل العظمي" لبرنامجنا من ثنائيه باستخدام نفس الأداة المساعدة bpftool - السكين السويسرية لعالم BPF (والتي يمكن أن تؤخذ حرفيا، حيث أن دانييل بوركمان، أحد المبدعين والمشرفين على BPF، سويسري):

$ bpftool gen skeleton xdp-simple.bpf.o > xdp-simple.skel.h

في ملف xdp-simple.skel.h يحتوي على الكود الثنائي لبرنامجنا ووظائف الإدارة - تحميل كائننا وإرفاقه وحذفه. في حالتنا البسيطة، يبدو هذا مبالغة، ولكنه يعمل أيضًا في الحالة التي يحتوي فيها ملف الكائن على العديد من برامج وخرائط BPF ولتحميل ELF العملاق هذا نحتاج فقط إلى إنشاء الهيكل واستدعاء وظيفة واحدة أو اثنتين من التطبيق المخصص الذي نستخدمه يكتبون دعونا ننتقل الآن.

بالمعنى الدقيق للكلمة، برنامج التحميل لدينا هو تافه:

#include <err.h>
#include <unistd.h>
#include "xdp-simple.skel.h"

int main(int argc, char **argv)
{
    struct xdp_simple_bpf *obj;

    obj = xdp_simple_bpf__open_and_load();
    if (!obj)
        err(1, "failed to open and/or load BPF objectn");

    pause();

    xdp_simple_bpf__destroy(obj);
}

ومن struct xdp_simple_bpf المحددة في الملف xdp-simple.skel.h ويصف ملف الكائن الخاص بنا:

struct xdp_simple_bpf {
    struct bpf_object_skeleton *skeleton;
    struct bpf_object *obj;
    struct {
        struct bpf_program *simple;
    } progs;
    struct {
        struct bpf_link *simple;
    } links;
};

يمكننا أن نرى آثار واجهة برمجة التطبيقات (API) ذات المستوى المنخفض هنا: البنية struct bpf_program *simple и struct bpf_link *simple. يصف الهيكل الأول برنامجنا على وجه التحديد، وهو مكتوب في القسم xdp/simpleوالثاني يصف كيفية اتصال البرنامج بمصدر الحدث.

وظيفة xdp_simple_bpf__open_and_load، يفتح كائن ELF، ويوزعه، وينشئ جميع الهياكل والبنى التحتية (إلى جانب البرنامج، يحتوي ELF أيضًا على أقسام أخرى - البيانات، والبيانات للقراءة فقط، ومعلومات تصحيح الأخطاء، والترخيص، وما إلى ذلك)، ثم يقوم بتحميله إلى النواة باستخدام نظام يتصل bpf، والتي يمكننا التحقق منها عن طريق تجميع البرنامج وتشغيله:

$ clang -O2 -I ./libbpf/src/root/usr/include/ xdp-simple.c -o xdp-simple ./libbpf/src/root/usr/lib64/libbpf.a -lelf -lz

$ sudo strace -e bpf ./xdp-simple
...
bpf(BPF_BTF_LOAD, 0x7ffdb8fd9670, 120)  = 3
bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_XDP, insn_cnt=2, insns=0xdfd580, license="GPL", log_level=0, log_size=0, log_buf=NULL, kern_version=KERNEL_VERSION(5, 8, 0), prog_flags=0, prog_name="simple", prog_ifindex=0, expected_attach_type=0x25 /* BPF_??? */, ...}, 120) = 4

دعونا الآن نلقي نظرة على برنامجنا باستخدام bpftool. دعونا نجد هويتها:

# bpftool p | grep -A4 simple
463: xdp  name simple  tag 3b185187f1855c4c  gpl
        loaded_at 2020-08-01T01:59:49+0000  uid 0
        xlated 16B  jited 40B  memlock 4096B
        btf_id 185
        pids xdp-simple(16498)

وتفريغ (نستخدم نموذجًا مختصرًا للأمر bpftool prog dump xlated):

# bpftool p d x id 463
int simple(void *ctx):
; return XDP_PASS;
   0: (b7) r0 = 2
   1: (95) exit

شيء جديد! قام البرنامج بطباعة أجزاء من ملف مصدر C. وقد تم ذلك عن طريق المكتبة libbpf، الذي عثر على قسم التصحيح في الملف الثنائي، وقام بتجميعه في كائن BTF، وتحميله في النواة باستخدام BPF_BTF_LOAD، ثم حدد واصف الملف الناتج عند تحميل البرنامج باستخدام الأمر BPG_PROG_LOAD.

مساعدي النواة

يمكن لبرامج BPF تشغيل وظائف "خارجية" - مساعدات kernel. تسمح هذه الوظائف المساعدة لبرامج BPF بالوصول إلى هياكل kernel، وإدارة الخرائط، وكذلك التواصل مع "العالم الحقيقي" - إنشاء أحداث الأداء، والتحكم في الأجهزة (على سبيل المثال، إعادة توجيه الحزم)، وما إلى ذلك.

مثال: bpf_get_smp_processor_id

في إطار نموذج "التعلم بالقدوة"، دعونا نفكر في إحدى الوظائف المساعدة، bpf_get_smp_processor_id(), بعض في ملف kernel/bpf/helpers.c. تقوم بإرجاع رقم المعالج الذي يعمل عليه برنامج BPF الذي يطلق عليه. لكننا لسنا مهتمين بدلالاته بقدر اهتمامنا بحقيقة أن تنفيذه يأخذ سطرًا واحدًا:

BPF_CALL_0(bpf_get_smp_processor_id)
{
    return smp_processor_id();
}

تتشابه تعريفات وظائف المساعدة في BPF مع تعريفات استدعاءات النظام. Linuxهنا، على سبيل المثال، يتم تعريف دالة ليس لها وسائط. (يتم تعريف دالة تأخذ، على سبيل المثال، ثلاثة وسائط باستخدام ماكرو) BPF_CALL_3. الحد الأقصى لعدد الوسائط هو خمس.) ومع ذلك، هذا هو الجزء الأول فقط من التعريف. الجزء الثاني هو تحديد بنية النوع struct bpf_func_proto، والذي يحتوي على وصف للوظيفة المساعدة التي يفهمها المدقق:

const struct bpf_func_proto bpf_get_smp_processor_id_proto = {
    .func     = bpf_get_smp_processor_id,
    .gpl_only = false,
    .ret_type = RET_INTEGER,
};

تسجيل الوظائف المساعدة

لكي تتمكن برامج BPF من نوع معين من استخدام هذه الوظيفة، يجب عليها تسجيلها، على سبيل المثال، للنوع BPF_PROG_TYPE_XDP يتم تعريف وظيفة في النواة xdp_func_proto، والذي يحدد من معرف الوظيفة المساعدة ما إذا كان XDP يدعم هذه الوظيفة أم لا. وظيفتنا هي يدعم:

static const struct bpf_func_proto *
xdp_func_proto(enum bpf_func_id func_id, const struct bpf_prog *prog)
{
    switch (func_id) {
    ...
    case BPF_FUNC_get_smp_processor_id:
        return &bpf_get_smp_processor_id_proto;
    ...
    }
}

يتم "تعريف" أنواع برامج BPF الجديدة في الملف include/linux/bpf_types.h باستخدام الماكرو BPF_PROG_TYPE. تم تعريفه بين علامتي اقتباس لأنه تعريف منطقي، وفي مصطلحات لغة C يحدث تعريف مجموعة كاملة من الهياكل الخرسانية في أماكن أخرى. على وجه الخصوص، في الملف kernel/bpf/verifier.c جميع التعاريف من الملف bpf_types.h يتم استخدامها لإنشاء مجموعة من الهياكل bpf_verifier_ops[]:

static const struct bpf_verifier_ops *const bpf_verifier_ops[] = {
#define BPF_PROG_TYPE(_id, _name, prog_ctx_type, kern_ctx_type) 
    [_id] = & _name ## _verifier_ops,
#include <linux/bpf_types.h>
#undef BPF_PROG_TYPE
};

أي أنه لكل نوع من برامج BPF، يتم تحديد مؤشر إلى بنية بيانات من النوع struct bpf_verifier_ops، والتي تتم تهيئتها بالقيمة _name ## _verifier_ops، أي.، xdp_verifier_ops إلى xdp. بناء xdp_verifier_ops يحددها في ملف net/core/filter.c على النحو التالي:

const struct bpf_verifier_ops xdp_verifier_ops = {
    .get_func_proto     = xdp_func_proto,
    .is_valid_access    = xdp_is_valid_access,
    .convert_ctx_access = xdp_convert_ctx_access,
    .gen_prologue       = bpf_noop_prologue,
};

هنا نرى وظيفتنا المألوفة xdp_func_proto، والذي سيقوم بتشغيل أداة التحقق في كل مرة يواجه فيها تحديًا نوع ما وظائف داخل برنامج BPF، انظر verifier.c.

دعونا نلقي نظرة على كيفية استخدام برنامج BPF الافتراضي للوظيفة bpf_get_smp_processor_id. وللقيام بذلك، نعيد كتابة البرنامج من قسمنا السابق على النحو التالي:

#include "vmlinux.h"
#include <bpf/bpf_helpers.h>

SEC("xdp/simple")
int simple(void *ctx)
{
    if (bpf_get_smp_processor_id() != 0)
        return XDP_DROP;
    return XDP_PASS;
}

char LICENSE[] SEC("license") = "GPL";

رمز bpf_get_smp_processor_id يحددها в <bpf/bpf_helper_defs.h> مكتبة libbpf كيف

static u32 (*bpf_get_smp_processor_id)(void) = (void *) 8;

إنه، bpf_get_smp_processor_id هو مؤشر دالة قيمته 8، حيث 8 هي القيمة BPF_FUNC_get_smp_processor_id نوع enum bpf_fun_id، والذي تم تعريفه لنا في الملف vmlinux.h (ملف bpf_helper_defs.h في النواة بواسطة برنامج نصي، وبالتالي فإن الأرقام "السحرية" لا بأس بها). لا تأخذ هذه الدالة أي وسائط وتقوم بإرجاع قيمة من النوع __u32. وعندما نقوم بتشغيله في برنامجنا، clang يولد تعليمات BPF_CALL "النوع الصحيح" دعونا نجمع البرنامج وننظر إلى القسم xdp/simple:

$ clang -O2 -g -c -target bpf -I libbpf/src/root/usr/include xdp-simple.bpf.c -o xdp-simple.bpf.o
$ llvm-objdump -D --section=xdp/simple xdp-simple.bpf.o

xdp-simple.bpf.o:       file format elf64-bpf

Disassembly of section xdp/simple:

0000000000000000 <simple>:
       0:       85 00 00 00 08 00 00 00 call 8
       1:       bf 01 00 00 00 00 00 00 r1 = r0
       2:       67 01 00 00 20 00 00 00 r1 <<= 32
       3:       77 01 00 00 20 00 00 00 r1 >>= 32
       4:       b7 00 00 00 02 00 00 00 r0 = 2
       5:       15 01 01 00 00 00 00 00 if r1 == 0 goto +1 <LBB0_2>
       6:       b7 00 00 00 01 00 00 00 r0 = 1

0000000000000038 <LBB0_2>:
       7:       95 00 00 00 00 00 00 00 exit

في السطر الأول نرى التعليمات call، معامل IMM وهو ما يساوي 8، و SRC_REG - صفر. وفقًا لاتفاقية ABI التي يستخدمها برنامج التحقق، يعد هذا استدعاء للوظيفة المساعدة رقم ثمانية. بمجرد إطلاقه، المنطق بسيط. إرجاع القيمة من السجل r0 منسوخ الى r1 وفي الأسطر 2,3 يتم تحويله إلى كتابة u32 - يتم مسح الـ 32 بت العلوية. على الأسطر 4,5,6,7،2،XNUMX،XNUMX نعود XNUMX (XDP_PASS) أو 1 (XDP_DROP) اعتمادًا على ما إذا كانت الدالة المساعدة من السطر 0 قد أرجعت قيمة صفرية أو غير صفرية.

دعونا نختبر أنفسنا: قم بتحميل البرنامج وإلقاء نظرة على الإخراج bpftool prog dump xlated:

$ bpftool gen skeleton xdp-simple.bpf.o > xdp-simple.skel.h
$ clang -O2 -g -I ./libbpf/src/root/usr/include/ -o xdp-simple xdp-simple.c ./libbpf/src/root/usr/lib64/libbpf.a -lelf -lz
$ sudo ./xdp-simple &
[2] 10914

$ sudo bpftool p | grep simple
523: xdp  name simple  tag 44c38a10c657e1b0  gpl
        pids xdp-simple(10915)

$ sudo bpftool p d x id 523
int simple(void *ctx):
; if (bpf_get_smp_processor_id() != 0)
   0: (85) call bpf_get_smp_processor_id#114128
   1: (bf) r1 = r0
   2: (67) r1 <<= 32
   3: (77) r1 >>= 32
   4: (b7) r0 = 2
; }
   5: (15) if r1 == 0x0 goto pc+1
   6: (b7) r0 = 1
   7: (95) exit

حسنًا، عثر برنامج التحقق على مساعد kernel الصحيح.

مثال: تمرير الوسائط وأخيراً تشغيل البرنامج!

تحتوي جميع الوظائف المساعدة على مستوى التشغيل على نموذج أولي

u64 fn(u64 r1, u64 r2, u64 r3, u64 r4, u64 r5)

يتم تمرير معلمات الوظائف المساعدة في السجلات r1-r5، ويتم إرجاع القيمة في السجل r0. لا توجد دالات تتطلب أكثر من خمس وسيطات، ومن غير المتوقع إضافة دعم لها في المستقبل.

دعونا نلقي نظرة على مساعد kernel الجديد وكيفية تمرير BPF للمعلمات. دعونا نعيد الكتابة xdp-simple.bpf.c كالتالي (باقي الأسطر لم تتغير):

SEC("xdp/simple")
int simple(void *ctx)
{
    bpf_printk("running on CPU%un", bpf_get_smp_processor_id());
    return XDP_PASS;
}

يقوم برنامجنا بطباعة رقم وحدة المعالجة المركزية التي يعمل عليها. دعونا نجمعها وننظر إلى الكود:

$ llvm-objdump -D --section=xdp/simple --no-show-raw-insn xdp-simple.bpf.o

0000000000000000 <simple>:
       0:       r1 = 10
       1:       *(u16 *)(r10 - 8) = r1
       2:       r1 = 8441246879787806319 ll
       4:       *(u64 *)(r10 - 16) = r1
       5:       r1 = 2334956330918245746 ll
       7:       *(u64 *)(r10 - 24) = r1
       8:       call 8
       9:       r1 = r10
      10:       r1 += -24
      11:       r2 = 18
      12:       r3 = r0
      13:       call 6
      14:       r0 = 2
      15:       exit

في الأسطر 0-7 نكتب السلسلة running on CPU%un، ثم في السطر 8 نقوم بتشغيل الخط المألوف bpf_get_smp_processor_id. في الأسطر 9-12 نقوم بإعداد الحجج المساعدة bpf_printk - السجلات r1, r2, r3. لماذا هناك ثلاثة منهم وليس اثنان؟ لأن bpf_printkهذا هو مجمع الماكرو حول المساعد الحقيقي bpf_trace_printk، والذي يحتاج إلى تمرير حجم سلسلة التنسيق.

دعونا الآن نضيف سطرين إلى xdp-simple.cبحيث يتصل برنامجنا بالواجهة lo وبدأت حقا!

$ cat xdp-simple.c
#include <linux/if_link.h>
#include <err.h>
#include <unistd.h>
#include "xdp-simple.skel.h"

int main(int argc, char **argv)
{
    __u32 flags = XDP_FLAGS_SKB_MODE;
    struct xdp_simple_bpf *obj;

    obj = xdp_simple_bpf__open_and_load();
    if (!obj)
        err(1, "failed to open and/or load BPF objectn");

    bpf_set_link_xdp_fd(1, -1, flags);
    bpf_set_link_xdp_fd(1, bpf_program__fd(obj->progs.simple), flags);

cleanup:
    xdp_simple_bpf__destroy(obj);
}

هنا نستخدم الدالة bpf_set_link_xdp_fd، الذي يربط برامج BPF من نوع XDP بواجهات الشبكة. لقد قمنا بترميز رقم الواجهة loوهو دائمًا 1. نقوم بتشغيل الوظيفة مرتين لفصل البرنامج القديم أولاً إذا كان متصلاً. لاحظ أننا الآن لا نحتاج إلى التحدي pause أو حلقة لا نهائية: سيخرج برنامج التحميل الخاص بنا، ولكن لن يتم إيقاف برنامج BPF لأنه متصل بمصدر الحدث. بعد نجاح التنزيل والاتصال، سيتم تشغيل البرنامج لكل حزمة شبكة تصل lo.

لنقم بتنزيل البرنامج وإلقاء نظرة على الواجهة lo:

$ sudo ./xdp-simple
$ sudo bpftool p | grep simple
669: xdp  name simple  tag 4fca62e77ccb43d6  gpl
$ ip l show dev lo
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 xdpgeneric qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    prog/xdp id 669

البرنامج الذي قمنا بتنزيله يحمل المعرف 669 ونرى نفس المعرف على الواجهة lo. سوف نرسل بضع حزم ل 127.0.0.1 (الطلب + الرد):

$ ping -c1 localhost

والآن دعونا نلقي نظرة على محتويات ملف التصحيح الظاهري /sys/kernel/debug/tracing/trace_pipe، بحيث bpf_printk يكتب رسائله:

# cat /sys/kernel/debug/tracing/trace_pipe
ping-13937 [000] d.s1 442015.377014: bpf_trace_printk: running on CPU0
ping-13937 [000] d.s1 442015.377027: bpf_trace_printk: running on CPU0

تم رصد حزمتين على lo وتمت معالجته على CPU0 - نجح أول برنامج BPF متكامل لا معنى له!

من الجدير بالذكر أن bpf_printk ليس من قبيل الصدفة أنه يكتب في ملف التصحيح: هذا ليس المساعد الأكثر نجاحًا للاستخدام في الإنتاج، ولكن هدفنا كان إظهار شيء بسيط.

الوصول إلى الخرائط من برامج BPF

مثال: استخدام خريطة من برنامج BPF

تعلمنا في الأقسام السابقة كيفية إنشاء واستخدام الخرائط من مساحة المستخدم، والآن دعونا نلقي نظرة على جزء النواة. لنبدأ كالعادة بمثال. دعونا نعيد كتابة برنامجنا xdp-simple.bpf.c على النحو التالي:

#include "vmlinux.h"
#include <bpf/bpf_helpers.h>

struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __uint(max_entries, 8);
    __type(key, u32);
    __type(value, u64);
} woo SEC(".maps");

SEC("xdp/simple")
int simple(void *ctx)
{
    u32 key = bpf_get_smp_processor_id();
    u32 *val;

    val = bpf_map_lookup_elem(&woo, &key);
    if (!val)
        return XDP_ABORTED;

    *val += 1;

    return XDP_PASS;
}

char LICENSE[] SEC("license") = "GPL";

في بداية البرنامج أضفنا تعريف الخريطة woo: هذه مصفوفة مكونة من 8 عناصر تخزن قيمًا مثل u64 (في C سوف نحدد مثل هذه المصفوفة مثل u64 woo[8]). في برنامج "xdp/simple" نحصل على رقم المعالج الحالي في متغير key ومن ثم استخدام وظيفة المساعد bpf_map_lookup_element نحصل على مؤشر للإدخال المقابل في المصفوفة، والذي نزيده بمقدار واحد. ترجمت إلى اللغة الروسية: نقوم بحساب الإحصائيات التي قامت وحدة المعالجة المركزية بمعالجة الحزم الواردة عليها. دعونا نحاول تشغيل البرنامج:

$ clang -O2 -g -c -target bpf -I libbpf/src/root/usr/include xdp-simple.bpf.c -o xdp-simple.bpf.o
$ bpftool gen skeleton xdp-simple.bpf.o > xdp-simple.skel.h
$ clang -O2 -g -I ./libbpf/src/root/usr/include/ -o xdp-simple xdp-simple.c ./libbpf/src/root/usr/lib64/libbpf.a -lelf -lz
$ sudo ./xdp-simple

دعونا نتحقق من أنها متصلة lo وإرسال بعض الحزم:

$ ip l show dev lo
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 xdpgeneric qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    prog/xdp id 108

$ for s in `seq 234`; do sudo ping -f -c 100 127.0.0.1 >/dev/null 2>&1; done

الآن دعونا نلقي نظرة على محتويات المصفوفة:

$ sudo bpftool map dump name woo
[
    { "key": 0, "value": 0 },
    { "key": 1, "value": 400 },
    { "key": 2, "value": 0 },
    { "key": 3, "value": 0 },
    { "key": 4, "value": 0 },
    { "key": 5, "value": 0 },
    { "key": 6, "value": 0 },
    { "key": 7, "value": 46400 }
]

تمت معالجة جميع العمليات تقريبًا على وحدة المعالجة المركزية CPU7. هذا ليس مهما بالنسبة لنا، الشيء الرئيسي هو أن البرنامج يعمل ونفهم كيفية الوصول إلى الخرائط من برامج BPF - باستخدام хелперов bpf_mp_*.

مؤشر باطني

لذلك، يمكننا الوصول إلى الخريطة من برنامج BPF باستخدام مكالمات مثل

val = bpf_map_lookup_elem(&woo, &key);

حيث تبدو وظيفة المساعد

void *bpf_map_lookup_elem(struct bpf_map *map, const void *key)

لكننا نمرر المؤشر &woo إلى هيكل غير مسمى struct { ... }...

إذا نظرنا إلى مجمع البرنامج، نرى أن القيمة &woo لم يتم تعريفه فعليًا (السطر 4):

llvm-objdump -D --section xdp/simple xdp-simple.bpf.o

xdp-simple.bpf.o:       file format elf64-bpf

Disassembly of section xdp/simple:

0000000000000000 <simple>:
       0:       85 00 00 00 08 00 00 00 call 8
       1:       63 0a fc ff 00 00 00 00 *(u32 *)(r10 - 4) = r0
       2:       bf a2 00 00 00 00 00 00 r2 = r10
       3:       07 02 00 00 fc ff ff ff r2 += -4
       4:       18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll
       6:       85 00 00 00 01 00 00 00 call 1
...

ويرد في النقلات:

$ llvm-readelf -r xdp-simple.bpf.o | head -4

Relocation section '.relxdp/simple' at offset 0xe18 contains 1 entries:
    Offset             Info             Type               Symbol's Value  Symbol's Name
0000000000000020  0000002700000001 R_BPF_64_64            0000000000000000 woo

لكن إذا نظرنا إلى البرنامج المحمل بالفعل، نرى مؤشرًا للخريطة الصحيحة (السطر 4):

$ sudo bpftool prog dump x name simple
int simple(void *ctx):
   0: (85) call bpf_get_smp_processor_id#114128
   1: (63) *(u32 *)(r10 -4) = r0
   2: (bf) r2 = r10
   3: (07) r2 += -4
   4: (18) r1 = map[id:64]
...

وهكذا يمكننا أن نستنتج أنه في وقت إطلاق برنامج التحميل الخاص بنا، كان الرابط إلى &woo تم استبداله بشيء به مكتبة libbpf. أولا سوف ننظر إلى الإخراج strace:

$ sudo strace -e bpf ./xdp-simple
...
bpf(BPF_MAP_CREATE, {map_type=BPF_MAP_TYPE_ARRAY, key_size=4, value_size=8, max_entries=8, map_name="woo", ...}, 120) = 4
bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_XDP, prog_name="simple", ...}, 120) = 5

نحن نرى ذلك libbpf أنشأت خريطة woo ومن ثم تحميل برنامجنا simple. دعونا نلقي نظرة فاحصة على كيفية تحميل البرنامج:

  • يتصل xdp_simple_bpf__open_and_load من - الملف xdp-simple.skel.h
  • والذي يسبب xdp_simple_bpf__load من - الملف xdp-simple.skel.h
  • والذي يسبب bpf_object__load_skeleton من - الملف libbpf/src/libbpf.c
  • والذي يسبب bpf_object__load_xattr من libbpf/src/libbpf.c

سيتم استدعاء الوظيفة الأخيرة، من بين أمور أخرى bpf_object__create_maps، الذي يقوم بإنشاء الخرائط الموجودة أو فتحها، وتحويلها إلى واصفات ملفات. (هذا هو المكان الذي نرى فيه BPF_MAP_CREATE في الإخراج strace.) بعد ذلك يتم استدعاء الوظيفة bpf_object__relocate وهي التي تهمنا لأننا نتذكر ما رأيناه woo في جدول النقل باستكشافها، نجد أنفسنا في النهاية في الوظيفة bpf_program__relocateوالتي و يتعامل مع عمليات نقل الخريطة:

case RELO_LD64:
    insn[0].src_reg = BPF_PSEUDO_MAP_FD;
    insn[0].imm = obj->maps[relo->map_idx].fd;
    break;

لذلك نحن نأخذ تعليماتنا

18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll

واستبدال السجل المصدر فيه ب BPF_PSEUDO_MAP_FD، وأول IMM لواصف الملف لخريطتنا، وإذا كان يساوي، على سبيل المثال، 0xdeadbeef، ونتيجة لذلك سوف نتلقى التعليمات

18 11 00 00 ef eb ad de 00 00 00 00 00 00 00 00 r1 = 0 ll

هذه هي الطريقة التي يتم بها نقل معلومات الخريطة إلى برنامج BPF محدد تم تحميله. في هذه الحالة، يمكن إنشاء الخريطة باستخدام BPF_MAP_CREATE، ويتم فتحه بواسطة معرف باستخدام BPF_MAP_GET_FD_BY_ID.

المجموع عند الاستخدام libbpf الخوارزمية هي على النحو التالي:

  • أثناء التجميع، يتم إنشاء السجلات في جدول النقل للحصول على روابط للخرائط
  • libbpf يفتح كتاب كائنات ELF، ويبحث عن جميع الخرائط المستخدمة وينشئ واصفات الملفات لها
  • يتم تحميل واصفات الملفات في النواة كجزء من التعليمات LD64

وكما يمكنك أن تتخيل، هناك المزيد في المستقبل وسيتعين علينا أن ننظر إلى الجوهر. لحسن الحظ، لدينا دليل - لقد كتبنا المعنى BPF_PSEUDO_MAP_FD في سجل المصدر ويمكننا دفنه، والذي سيقودنا إلى قدس جميع القديسين - kernel/bpf/verifier.c، حيث تقوم دالة ذات اسم مميز باستبدال واصف الملف بعنوان بنية النوع struct bpf_map:

static int replace_map_fd_with_map_ptr(struct bpf_verifier_env *env) {
    ...

    f = fdget(insn[0].imm);
    map = __bpf_map_get(f);
    if (insn->src_reg == BPF_PSEUDO_MAP_FD) {
        addr = (unsigned long)map;
    }
    insn[0].imm = (u32)addr;
    insn[1].imm = addr >> 32;

(يمكن العثور على الكود الكامل رابط). حتى نتمكن من توسيع الخوارزمية لدينا:

  • أثناء تحميل البرنامج، يتحقق المدقق من الاستخدام الصحيح للخريطة ويكتب عنوان البنية المقابلة struct bpf_map

عند تنزيل ملف ELF الثنائي باستخدام libbpf هناك الكثير مما يحدث، لكننا سنناقش ذلك في مقالات أخرى.

تحميل البرامج والخرائط بدون libbpf

وكما وعدناكم، إليكم مثال للقراء الذين يريدون معرفة كيفية إنشاء وتحميل برنامج يستخدم الخرائط، دون مساعدة libbpf. يمكن أن يكون هذا مفيدًا عندما تعمل في بيئة لا يمكنك إنشاء تبعيات لها، أو حفظ كل جزء منها، أو كتابة برنامج مثل ply، الذي يقوم بإنشاء كود ثنائي BPF على الطاير.

لتسهيل اتباع المنطق، سنعيد كتابة مثالنا لهذه الأغراض xdp-simple. يمكن العثور على الكود الكامل والموسع قليلاً للبرنامج الذي تمت مناقشته في هذا المثال في هذا جوهر.

منطق طلبنا هو كما يلي:

  • إنشاء خريطة النوع BPF_MAP_TYPE_ARRAY باستخدام الأمر BPF_MAP_CREATE,
  • إنشاء برنامج يستخدم هذه الخريطة،
  • قم بتوصيل البرنامج بالواجهة lo,

الذي يترجم إلى الإنسان كما

int main(void)
{
    int map_fd, prog_fd;

    map_fd = map_create();
    if (map_fd < 0)
        err(1, "bpf: BPF_MAP_CREATE");

    prog_fd = prog_load(map_fd);
    if (prog_fd < 0)
        err(1, "bpf: BPF_PROG_LOAD");

    xdp_attach(1, prog_fd);
}

ومن map_create ينشئ خريطة بنفس الطريقة التي فعلناها في المثال الأول حول استدعاء النظام bpf - "النواة، من فضلك اصنع لي خريطة جديدة على شكل مجموعة من 8 عناصر مثل __u64 وأعد لي واصف الملف":

static int map_create()
{
    union bpf_attr attr;

    memset(&attr, 0, sizeof(attr));
    attr.map_type = BPF_MAP_TYPE_ARRAY,
    attr.key_size = sizeof(__u32),
    attr.value_size = sizeof(__u64),
    attr.max_entries = 8,
    strncpy(attr.map_name, "woo", sizeof(attr.map_name));
    return syscall(__NR_bpf, BPF_MAP_CREATE, &attr, sizeof(attr));
}

كما أن البرنامج سهل التحميل:

static int prog_load(int map_fd)
{
    union bpf_attr attr;
    struct bpf_insn insns[] = {
        ...
    };

    memset(&attr, 0, sizeof(attr));
    attr.prog_type = BPF_PROG_TYPE_XDP;
    attr.insns     = ptr_to_u64(insns);
    attr.insn_cnt  = sizeof(insns)/sizeof(insns[0]);
    attr.license   = ptr_to_u64("GPL");
    strncpy(attr.prog_name, "woo", sizeof(attr.prog_name));
    return syscall(__NR_bpf, BPF_PROG_LOAD, &attr, sizeof(attr));
}

الجزء الصعب prog_load هو تعريف برنامج BPF الخاص بنا على أنه مجموعة من الهياكل struct bpf_insn insns[]. لكن بما أننا نستخدم برنامجًا موجودًا في لغة C، فيمكننا الغش قليلاً:

$ llvm-objdump -D --section xdp/simple xdp-simple.bpf.o

0000000000000000 <simple>:
       0:       85 00 00 00 08 00 00 00 call 8
       1:       63 0a fc ff 00 00 00 00 *(u32 *)(r10 - 4) = r0
       2:       bf a2 00 00 00 00 00 00 r2 = r10
       3:       07 02 00 00 fc ff ff ff r2 += -4
       4:       18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll
       6:       85 00 00 00 01 00 00 00 call 1
       7:       b7 01 00 00 00 00 00 00 r1 = 0
       8:       15 00 04 00 00 00 00 00 if r0 == 0 goto +4 <LBB0_2>
       9:       61 01 00 00 00 00 00 00 r1 = *(u32 *)(r0 + 0)
      10:       07 01 00 00 01 00 00 00 r1 += 1
      11:       63 10 00 00 00 00 00 00 *(u32 *)(r0 + 0) = r1
      12:       b7 01 00 00 02 00 00 00 r1 = 2

0000000000000068 <LBB0_2>:
      13:       bf 10 00 00 00 00 00 00 r0 = r1
      14:       95 00 00 00 00 00 00 00 exit

في المجموع، نحن بحاجة لكتابة 14 تعليمات في شكل هياكل مثل struct bpf_insn (نصيحة: خذ التفريغ من الأعلى، وأعد قراءة قسم التعليمات، وافتحه linux/bpf.h и linux/bpf_common.h ومحاولة تحديد struct bpf_insn insns[] على المرء):

struct bpf_insn insns[] = {
    /* 85 00 00 00 08 00 00 00 call 8 */
    {
        .code = BPF_JMP | BPF_CALL,
        .imm = 8,
    },

    /* 63 0a fc ff 00 00 00 00 *(u32 *)(r10 - 4) = r0 */
    {
        .code = BPF_MEM | BPF_STX,
        .off = -4,
        .src_reg = BPF_REG_0,
        .dst_reg = BPF_REG_10,
    },

    /* bf a2 00 00 00 00 00 00 r2 = r10 */
    {
        .code = BPF_ALU64 | BPF_MOV | BPF_X,
        .src_reg = BPF_REG_10,
        .dst_reg = BPF_REG_2,
    },

    /* 07 02 00 00 fc ff ff ff r2 += -4 */
    {
        .code = BPF_ALU64 | BPF_ADD | BPF_K,
        .dst_reg = BPF_REG_2,
        .imm = -4,
    },

    /* 18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll */
    {
        .code = BPF_LD | BPF_DW | BPF_IMM,
        .src_reg = BPF_PSEUDO_MAP_FD,
        .dst_reg = BPF_REG_1,
        .imm = map_fd,
    },
    { }, /* placeholder */

    /* 85 00 00 00 01 00 00 00 call 1 */
    {
        .code = BPF_JMP | BPF_CALL,
        .imm = 1,
    },

    /* b7 01 00 00 00 00 00 00 r1 = 0 */
    {
        .code = BPF_ALU64 | BPF_MOV | BPF_K,
        .dst_reg = BPF_REG_1,
        .imm = 0,
    },

    /* 15 00 04 00 00 00 00 00 if r0 == 0 goto +4 <LBB0_2> */
    {
        .code = BPF_JMP | BPF_JEQ | BPF_K,
        .off = 4,
        .src_reg = BPF_REG_0,
        .imm = 0,
    },

    /* 61 01 00 00 00 00 00 00 r1 = *(u32 *)(r0 + 0) */
    {
        .code = BPF_MEM | BPF_LDX,
        .off = 0,
        .src_reg = BPF_REG_0,
        .dst_reg = BPF_REG_1,
    },

    /* 07 01 00 00 01 00 00 00 r1 += 1 */
    {
        .code = BPF_ALU64 | BPF_ADD | BPF_K,
        .dst_reg = BPF_REG_1,
        .imm = 1,
    },

    /* 63 10 00 00 00 00 00 00 *(u32 *)(r0 + 0) = r1 */
    {
        .code = BPF_MEM | BPF_STX,
        .src_reg = BPF_REG_1,
        .dst_reg = BPF_REG_0,
    },

    /* b7 01 00 00 02 00 00 00 r1 = 2 */
    {
        .code = BPF_ALU64 | BPF_MOV | BPF_K,
        .dst_reg = BPF_REG_1,
        .imm = 2,
    },

    /* <LBB0_2>: bf 10 00 00 00 00 00 00 r0 = r1 */
    {
        .code = BPF_ALU64 | BPF_MOV | BPF_X,
        .src_reg = BPF_REG_1,
        .dst_reg = BPF_REG_0,
    },

    /* 95 00 00 00 00 00 00 00 exit */
    {
        .code = BPF_JMP | BPF_EXIT
    },
};

تمرين لأولئك الذين لم يكتبوا هذا بأنفسهم - ابحث map_fd.

هناك جزء آخر لم يتم الكشف عنه في برنامجنا - xdp_attach. لسوء الحظ، لا يمكن توصيل برامج مثل XDP باستخدام استدعاء النظام bpfالأشخاص الذين قاموا بإنشاء BPF و XDP كانوا من مجتمع الإنترنت. Linuxوهذا يعني أنهم استخدموا ما كان مألوفًا لهم أكثر (ولكن ليس لـ طبيعي People) واجهة التفاعل مع النواة: مآخذ نت لينك، أنظر أيضا RFC3549. أبسط طريقة للتنفيذ xdp_attach يتم نسخ الكود من libbpf، وهي من الملف netlink.cوهذا ما فعلناه، مع اختصاره قليلاً:

مرحبًا بك في عالم مآخذ توصيل netlink

افتح نوع مقبس netlink NETLINK_ROUTE:

int netlink_open(__u32 *nl_pid)
{
    struct sockaddr_nl sa;
    socklen_t addrlen;
    int one = 1, ret;
    int sock;

    memset(&sa, 0, sizeof(sa));
    sa.nl_family = AF_NETLINK;

    sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
    if (sock < 0)
        err(1, "socket");

    if (setsockopt(sock, SOL_NETLINK, NETLINK_EXT_ACK, &one, sizeof(one)) < 0)
        warnx("netlink error reporting not supported");

    if (bind(sock, (struct sockaddr *)&sa, sizeof(sa)) < 0)
        err(1, "bind");

    addrlen = sizeof(sa);
    if (getsockname(sock, (struct sockaddr *)&sa, &addrlen) < 0)
        err(1, "getsockname");

    *nl_pid = sa.nl_pid;
    return sock;
}

نقرأ من هذا المقبس :

static int bpf_netlink_recv(int sock, __u32 nl_pid, int seq)
{
    bool multipart = true;
    struct nlmsgerr *errm;
    struct nlmsghdr *nh;
    char buf[4096];
    int len, ret;

    while (multipart) {
        multipart = false;
        len = recv(sock, buf, sizeof(buf), 0);
        if (len < 0)
            err(1, "recv");

        if (len == 0)
            break;

        for (nh = (struct nlmsghdr *)buf; NLMSG_OK(nh, len);
                nh = NLMSG_NEXT(nh, len)) {
            if (nh->nlmsg_pid != nl_pid)
                errx(1, "wrong pid");
            if (nh->nlmsg_seq != seq)
                errx(1, "INVSEQ");
            if (nh->nlmsg_flags & NLM_F_MULTI)
                multipart = true;
            switch (nh->nlmsg_type) {
                case NLMSG_ERROR:
                    errm = (struct nlmsgerr *)NLMSG_DATA(nh);
                    if (!errm->error)
                        continue;
                    ret = errm->error;
                    // libbpf_nla_dump_errormsg(nh); too many code to copy...
                    goto done;
                case NLMSG_DONE:
                    return 0;
                default:
                    break;
            }
        }
    }
    ret = 0;
done:
    return ret;
}

أخيرًا، هذه هي وظيفتنا التي تفتح مأخذ توصيل وترسل إليه رسالة خاصة تحتوي على واصف الملف:

static int xdp_attach(int ifindex, int prog_fd)
{
    int sock, seq = 0, ret;
    struct nlattr *nla, *nla_xdp;
    struct {
        struct nlmsghdr  nh;
        struct ifinfomsg ifinfo;
        char             attrbuf[64];
    } req;
    __u32 nl_pid = 0;

    sock = netlink_open(&nl_pid);
    if (sock < 0)
        return sock;

    memset(&req, 0, sizeof(req));
    req.nh.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg));
    req.nh.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
    req.nh.nlmsg_type = RTM_SETLINK;
    req.nh.nlmsg_pid = 0;
    req.nh.nlmsg_seq = ++seq;
    req.ifinfo.ifi_family = AF_UNSPEC;
    req.ifinfo.ifi_index = ifindex;

    /* started nested attribute for XDP */
    nla = (struct nlattr *)(((char *)&req)
            + NLMSG_ALIGN(req.nh.nlmsg_len));
    nla->nla_type = NLA_F_NESTED | IFLA_XDP;
    nla->nla_len = NLA_HDRLEN;

    /* add XDP fd */
    nla_xdp = (struct nlattr *)((char *)nla + nla->nla_len);
    nla_xdp->nla_type = IFLA_XDP_FD;
    nla_xdp->nla_len = NLA_HDRLEN + sizeof(int);
    memcpy((char *)nla_xdp + NLA_HDRLEN, &prog_fd, sizeof(prog_fd));
    nla->nla_len += nla_xdp->nla_len;

    /* if user passed in any flags, add those too */
    __u32 flags = XDP_FLAGS_SKB_MODE;
    nla_xdp = (struct nlattr *)((char *)nla + nla->nla_len);
    nla_xdp->nla_type = IFLA_XDP_FLAGS;
    nla_xdp->nla_len = NLA_HDRLEN + sizeof(flags);
    memcpy((char *)nla_xdp + NLA_HDRLEN, &flags, sizeof(flags));
    nla->nla_len += nla_xdp->nla_len;

    req.nh.nlmsg_len += NLA_ALIGN(nla->nla_len);

    if (send(sock, &req, req.nh.nlmsg_len, 0) < 0)
        err(1, "send");
    ret = bpf_netlink_recv(sock, nl_pid, seq);

cleanup:
    close(sock);
    return ret;
}

لذلك، كل شيء جاهز للاختبار:

$ cc nolibbpf.c -o nolibbpf
$ sudo strace -e bpf ./nolibbpf
bpf(BPF_MAP_CREATE, {map_type=BPF_MAP_TYPE_ARRAY, map_name="woo", ...}, 72) = 3
bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_XDP, insn_cnt=15, prog_name="woo", ...}, 72) = 4
+++ exited with 0 +++

دعونا نرى ما إذا كان برنامجنا متصلاً بـ lo:

$ ip l show dev lo
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 xdpgeneric qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    prog/xdp id 160

دعنا نرسل الأصوات ونلقي نظرة على الخريطة:

$ for s in `seq 234`; do sudo ping -f -c 100 127.0.0.1 >/dev/null 2>&1; done
$ sudo bpftool m dump name woo
key: 00 00 00 00  value: 90 01 00 00 00 00 00 00
key: 01 00 00 00  value: 00 00 00 00 00 00 00 00
key: 02 00 00 00  value: 00 00 00 00 00 00 00 00
key: 03 00 00 00  value: 00 00 00 00 00 00 00 00
key: 04 00 00 00  value: 00 00 00 00 00 00 00 00
key: 05 00 00 00  value: 00 00 00 00 00 00 00 00
key: 06 00 00 00  value: 40 b5 00 00 00 00 00 00
key: 07 00 00 00  value: 00 00 00 00 00 00 00 00
Found 8 elements

يا هلا، كل شيء يعمل. لاحظ، بالمناسبة، أن خريطتنا تظهر مرة أخرى في شكل بايت. هذا يرجع إلى حقيقة أنه على عكس libbpf لم نقم بتحميل معلومات النوع (BTF). لكننا سنتحدث أكثر عن هذا في المرة القادمة.

ادوات التطوير

في هذا القسم، سنلقي نظرة على الحد الأدنى من مجموعة أدوات مطور BPF.

بشكل عام، لا تحتاج إلى أي شيء خاص لتطوير برامج BPF - يعمل BPF على أي نواة توزيع مناسبة، ويتم إنشاء البرامج باستخدام clang، والتي يمكن توفيرها من الحزمة. ومع ذلك، نظرًا لأن BPF قيد التطوير، فإن النواة والأدوات تتغير باستمرار، إذا كنت لا ترغب في كتابة برامج BPF باستخدام الأساليب القديمة من عام 2019، فسيتعين عليك تجميعها

  • llvm/clang
  • pahole
  • جوهرها
  • bpftool

(للعلم: تم تشغيل هذا القسم وجميع الأمثلة الواردة في المقالة على Debian خفق

llvm/clang

يعتبر BPF صديقًا لـ LLVM، وعلى الرغم من أنه يمكن تجميع برامج BPF مؤخرًا باستخدام gcc، إلا أن كل التطوير الحالي يتم تنفيذه لـ LLVM. لذلك، أولا وقبل كل شيء، سوف نقوم ببناء الإصدار الحالي clang من بوابة:

$ sudo apt install ninja-build
$ git clone --depth 1 https://github.com/llvm/llvm-project.git
$ mkdir -p llvm-project/llvm/build/install
$ cd llvm-project/llvm/build
$ cmake .. -G "Ninja" -DLLVM_TARGETS_TO_BUILD="BPF;X86" 
                      -DLLVM_ENABLE_PROJECTS="clang" 
                      -DBUILD_SHARED_LIBS=OFF 
                      -DCMAKE_BUILD_TYPE=Release 
                      -DLLVM_BUILD_RUNTIME=OFF
$ time ninja
... много времени спустя
$

يمكننا الآن التحقق مما إذا كان كل شيء قد تم تجميعه بشكل صحيح:

$ ./bin/llc --version
LLVM (http://llvm.org/):
  LLVM version 11.0.0git
  Optimized build.
  Default target: x86_64-unknown-linux-gnu
  Host CPU: znver1

  Registered Targets:
    bpf    - BPF (host endian)
    bpfeb  - BPF (big endian)
    bpfel  - BPF (little endian)
    x86    - 32-bit X86: Pentium-Pro and above
    x86-64 - 64-bit X86: EM64T and AMD64

(تعليمات التجميع clang مأخوذة مني bpf_devel_QA.)

لن نقوم بتثبيت البرامج التي أنشأناها للتو، ولكن بدلاً من ذلك سنضيفها إليها فقط PATHعلى سبيل المثال:

export PATH="`pwd`/bin:$PATH"

(يمكن إضافة هذا إلى .bashrc أو إلى ملف منفصل. أنا شخصياً أضيف مثل هذه الأشياء إلى ~/bin/activate-llvm.sh وعند الضرورة أفعل ذلك . activate-llvm.sh.)

باهول وBTF

فائدة pahole يُستخدم عند بناء النواة لإنشاء معلومات تصحيح الأخطاء بتنسيق BTF. لن نخوض في هذه المقالة بالتفصيل حول تفاصيل تقنية BTF، بخلاف أنها مريحة ونريد استخدامها. لذا، إذا كنت ستقوم ببناء النواة الخاصة بك، قم بالبناء أولاً pahole (بدون pahole لن تتمكن من بناء النواة مع هذا الخيار CONFIG_DEBUG_INFO_BTF:

$ git clone https://git.kernel.org/pub/scm/devel/pahole/pahole.git
$ cd pahole/
$ sudo apt install cmake
$ mkdir build
$ cd build/
$ cmake -D__LIB=lib ..
$ make
$ sudo make install
$ which pahole
/usr/local/bin/pahole

حبات لتجربة BPF

عند استكشاف إمكانيات BPF، أريد تجميع النواة الخاصة بي. هذا، بشكل عام، ليس ضروريًا، نظرًا لأنك ستتمكن من تجميع وتحميل برامج BPF على نواة التوزيع، ومع ذلك، فإن امتلاك نواة خاصة بك يسمح لك باستخدام أحدث ميزات BPF، والتي ستظهر في توزيعتك خلال أشهر على أفضل تقدير. أو، كما هو الحال مع بعض أدوات التصحيح، لن يتم تجميعها على الإطلاق في المستقبل المنظور. كما أن جوهرها الخاص يجعل من المهم تجربة الكود.

من أجل بناء النواة، فإنك تحتاج أولاً إلى النواة نفسها، وثانيًا، إلى ملف تكوين النواة. لتجربة BPF يمكننا استخدام المعتاد الفانيليا النواة أو إحدى نوى المطورين. تاريخياً، تم تطوير BPF داخل مجتمع الشبكة. Linux وبالتالي، فإن جميع التغييرات تمر عاجلاً أم آجلاً عبر ديفيد ميلر، المسؤول عن صيانة الشبكة. Linuxبحسب طبيعتها - إصلاحات أو ميزات جديدة - تنتهي تغييرات الشبكة في أحد النواة الثانية: net أو net-next. يتم توزيع التغييرات الخاصة بـ BPF بنفس الطريقة بين bpf и bpf-next، والتي يتم بعد ذلك تجميعها في net وnet-next على التوالي. لمزيد من التفاصيل، انظر bpf_devel_QA и الأسئلة الشائعة حول netdev. لذا اختر النواة بناءً على ذوقك واحتياجات استقرار النظام الذي تختبره (*-next النوى هي الأكثر غير مستقرة من تلك المدرجة).

إن الحديث عن كيفية إدارة ملفات تكوين kernel هو خارج نطاق هذه المقالة - فمن المفترض أنك إما تعرف بالفعل كيفية القيام بذلك، أو مستعد للتعلم على المرء. ومع ذلك، يجب أن تكون الإرشادات التالية كافية إلى حد ما لتزويدك بنظام فعال يدعم BPF.

قم بتحميل أحد النوى المذكورة أعلاه:

$ git clone git://git.kernel.org/pub/scm/linux/kernel/git/bpf/bpf-next.git
$ cd bpf-next

قم ببناء الحد الأدنى من تكوين kernel العامل:

$ cp /boot/config-`uname -r` .config
$ make localmodconfig

تمكين خيارات BPF في الملف .config من اختيارك (على الأرجح CONFIG_BPF سيتم تمكينه بالفعل نظرًا لأن systemd يستخدمه). فيما يلي قائمة بالخيارات من النواة المستخدمة في هذه المقالة:

CONFIG_CGROUP_BPF=y
CONFIG_BPF=y
CONFIG_BPF_LSM=y
CONFIG_BPF_SYSCALL=y
CONFIG_ARCH_WANT_DEFAULT_BPF_JIT=y
CONFIG_BPF_JIT_ALWAYS_ON=y
CONFIG_BPF_JIT_DEFAULT_ON=y
CONFIG_IPV6_SEG6_BPF=y
# CONFIG_NETFILTER_XT_MATCH_BPF is not set
# CONFIG_BPFILTER is not set
CONFIG_NET_CLS_BPF=y
CONFIG_NET_ACT_BPF=y
CONFIG_BPF_JIT=y
CONFIG_BPF_STREAM_PARSER=y
CONFIG_LWTUNNEL_BPF=y
CONFIG_HAVE_EBPF_JIT=y
CONFIG_BPF_EVENTS=y
CONFIG_BPF_KPROBE_OVERRIDE=y
CONFIG_DEBUG_INFO_BTF=y

بعد ذلك، يمكننا تجميع الوحدات والنواة وتثبيتها بسهولة (بالمناسبة، يمكنك تجميع النواة باستخدام الوحدة المجمعة حديثًا clangبإضافة CC=clang):

$ make -s -j $(getconf _NPROCESSORS_ONLN)
$ sudo make modules_install
$ sudo make install

وأعد التشغيل بالنواة الجديدة (أستخدمها لهذا الغرض kexec من الحزمة kexec-tools):

v=5.8.0-rc6+ # если вы пересобираете текущее ядро, то можно делать v=`uname -r`
sudo kexec -l -t bzImage /boot/vmlinuz-$v --initrd=/boot/initrd.img-$v --reuse-cmdline &&
sudo kexec -e

com.bpftool

ستكون الأداة المساعدة الأكثر استخدامًا في المقالة هي الأداة المساعدة bpftool، يتم توفيرها كجزء من النواة Linuxتمت كتابة هذا البرنامج وصيانته من قبل مطوري BPF لمطوري BPF، ويمكن استخدامه لإدارة جميع أنواع كائنات BPF - تحميل البرامج، وإنشاء الخرائط وتعديلها، واستكشاف بيئة BPF، وغير ذلك الكثير. ويمكن العثور على التوثيق على شكل شفرة مصدرية لصفحات الدليل. في القلب أو تم تجميعها بالفعل على النت.

في وقت كتابة هذه السطور bpftool يأتي جاهزًا للاستخدام فقط مع أنظمة RHEL و Fedora و Ubuntu (انظر، على سبيل المثال، هذا الموضوع، الذي يحكي قصة التغليف غير المكتملة bpftool в Debianأما إذا كنت قد قمت بتجميع النواة بالفعل، فقم بالتجميع bpftool سهل مثل الفطيرة:

$ cd ${linux}/tools/bpf/bpftool
# ... пропишите пути к последнему clang, как рассказано выше
$ make -s

Auto-detecting system features:
...                        libbfd: [ on  ]
...        disassembler-four-args: [ on  ]
...                          zlib: [ on  ]
...                        libcap: [ on  ]
...               clang-bpf-co-re: [ on  ]

Auto-detecting system features:
...                        libelf: [ on  ]
...                          zlib: [ on  ]
...                           bpf: [ on  ]

$

(هنا ${linux} - هذا هو دليل النواة الخاص بك.) بعد تنفيذ هذه الأوامر bpftool سيتم جمعها في الدليل ${linux}/tools/bpf/bpftool ويمكن إضافته إلى المسار (أولاً وقبل كل شيء للمستخدم root) أو فقط انسخ إلى /usr/local/sbin.

جمع bpftool فمن الأفضل استخدام هذا الأخير clang، تم تجميعها كما هو موضح أعلاه، وتحقق مما إذا تم تجميعها بشكل صحيح - باستخدام الأمر، على سبيل المثال

$ sudo bpftool feature probe kernel
Scanning system configuration...
bpf() syscall for unprivileged users is enabled
JIT compiler is enabled
JIT compiler hardening is disabled
JIT compiler kallsyms exports are enabled for root
...

والتي سوف تظهر ميزات BPF التي تم تمكينها في النواة الخاصة بك.

بالمناسبة، يمكن تشغيل الأمر السابق كما

# bpftool f p k

يتم ذلك عن طريق القياس مع الأدوات المساعدة من الحزمة iproute2، حيث يمكننا، على سبيل المثال، أن نقول ip a s eth0 بدلا من ip addr show dev eth0.

اختتام

يتيح لك BPF وضع برغوث لقياس وظائف القلب وتغييرها بشكل فعال. تبين أن النظام كان ناجحًا للغاية، وفقًا لأفضل تقاليد UNIX: آلية بسيطة تسمح لك (بإعادة) برمجة النواة، مما سمح لعدد كبير من الأشخاص والمنظمات بالتجربة. وعلى الرغم من أن التجارب، وكذلك تطوير البنية التحتية لـ BPF نفسها، لم تنته بعد، إلا أن النظام يحتوي بالفعل على واجهة برمجة التطبيقات (ABI) مستقرة تسمح لك ببناء منطق أعمال موثوق به، والأهم من ذلك، فعال.

أود أن أشير إلى أنه في رأيي أصبحت التكنولوجيا شائعة جدًا لأنها تستطيع ذلك من ناحية لعب (يمكن فهم بنية الآلة بشكل أو بآخر في أمسية واحدة)، ومن ناحية أخرى، لحل المشكلات التي لم يكن من الممكن حلها (بشكل جميل) قبل ظهورها. هذان العنصران معًا يجبران الناس على التجربة والحلم، مما يؤدي إلى ظهور المزيد والمزيد من الحلول المبتكرة.

هذه المقالة، على الرغم من أنها ليست قصيرة بشكل خاص، إلا أنها مجرد مقدمة لعالم BPF ولا تصف الميزات "المتقدمة" والأجزاء المهمة من البنية. الخطة المستقبلية هي شيء من هذا القبيل: المقالة التالية ستكون نظرة عامة على أنواع برامج BPF (هناك 5.8 نوعًا من البرامج مدعومة في النواة 30)، ثم سننظر أخيرًا في كيفية كتابة تطبيقات BPF حقيقية باستخدام برامج تتبع النواة على سبيل المثال، فقد حان الوقت للحصول على دورة تدريبية أكثر تعمقًا حول بنية BPF، متبوعة بأمثلة لشبكات BPF وتطبيقات الأمان.

المقالات السابقة في هذه السلسلة

  1. BPF للصغار، الجزء صفر: BPF الكلاسيكي

روابط

  1. دليل مرجعي BPF و XDP - وثائق عن BPF من cilium، أو بشكل أكثر دقة من دانييل بوركمان، أحد المبدعين والمشرفين على BPF. هذه واحدة من الأوصاف الجادة الأولى، والتي تختلف عن الأوصاف الأخرى من حيث أن دانيال يعرف بالضبط ما يكتب عنه ولا توجد أخطاء فيه. على وجه الخصوص، يصف هذا المستند كيفية العمل مع برامج BPF لأنواع XDP وTC باستخدام الأداة المساعدة المعروفة ip من الحزمة iproute2.

  2. التوثيق/الشبكات/filter.txt - الملف الأصلي مع وثائق BPF الكلاسيكية ثم الموسعة. قراءة جيدة إذا كنت تريد التعمق في لغة التجميع والتفاصيل المعمارية الفنية.

  3. مدونة حول BPF من الفيسبوك. نادرًا ما يتم تحديثه، ولكن بشكل مناسب، كما يكتب أليكسي ستاروفويتوف (مؤلف كتاب eBPF) وأندري ناكريكو - (المشرف) هناك libbpf).

  4. أسرار برنامج bpftool. موضوع ممتع على تويتر من كوينتين مونيه مع أمثلة وأسرار استخدام bpftool.

  5. الغوص في BPF: قائمة مواد القراءة. قائمة ضخمة (ولا تزال محفوظة) من الروابط إلى وثائق BPF من Quentin Monnet.

المصدر: www.habr.com

شراء استضافة موثوقة للمواقع مع حماية DDoS وخوادم VPS VDS 🔥 اشترِ استضافة مواقع ويب موثوقة مع حماية من هجمات DDoS، وخوادم VPS وVDS | ProHoster