در ابتدا یک فناوری وجود داشت و به آن BPF می گفتند. نگاهش کردیم قبلی، مقاله عهد عتیق این مجموعه. در سال 2013، با تلاش الکسی استاروویتوف و دانیل بورکمن، یک نسخه بهبود یافته از آن، بهینه سازی شده برای ماشین های مدرن 64 بیتی، توسعه یافت و در هسته لینوکس گنجانده شد. این فناوری جدید به طور خلاصه Internal BPF نامیده شد، سپس به Extended BPF تغییر نام داد و اکنون پس از چندین سال، همه به سادگی آن را BPF می نامند.
به طور کلی، BPF به شما امکان می دهد کدهای دلخواه ارائه شده توسط کاربر را در فضای هسته لینوکس اجرا کنید، و معماری جدید آنقدر موفق بود که برای توصیف همه برنامه های آن به ده ها مقاله دیگر نیاز خواهیم داشت. (همانطور که در کد عملکرد زیر می بینید، تنها کاری که توسعه دهندگان به خوبی انجام ندادند، ایجاد یک لوگوی مناسب بود.)
این مقاله ساختار ماشین مجازی BPF، رابطهای هسته برای کار با BPF، ابزارهای توسعه، و همچنین مروری کوتاه و بسیار کوتاه از قابلیتهای موجود را شرح میدهد. همه چیزهایی که در آینده برای مطالعه عمیق تر کاربردهای عملی BPF به آن نیاز خواهیم داشت.
خلاصه مقاله
مقدمه ای بر معماری BPF ابتدا نگاهی پرنده به معماری BPF خواهیم داشت و اجزای اصلی را ترسیم می کنیم.
مدیریت اشیاء با استفاده از فراخوانی سیستم bpf. با درک کمی از سیستم در حال حاضر، ما در نهایت به نحوه ایجاد و دستکاری اشیاء از فضای کاربر با استفاده از یک فراخوانی خاص سیستم خواهیم پرداخت. bpf(2).
Пишем программы BPF с помощью libbpf. البته می توانید با استفاده از تماس سیستمی برنامه بنویسید. اما سخت است. برای یک سناریوی واقعی تر، برنامه نویسان هسته ای یک کتابخانه ایجاد کردند libbpf. ما یک اسکلت اصلی برنامه BPF ایجاد خواهیم کرد که در مثال های بعدی از آن استفاده خواهیم کرد.
کمک کننده های هسته. در اینجا می آموزیم که چگونه برنامه های BPF می توانند به توابع کمک کننده هسته دسترسی پیدا کنند - ابزاری که همراه با نقشه ها، اساساً قابلیت های BPF جدید را در مقایسه با کلاسیک گسترش می دهد.
دسترسی به نقشه ها از برنامه های BPF. در این مرحله، ما به اندازه کافی می دانیم که دقیقاً چگونه می توانیم برنامه هایی ایجاد کنیم که از نقشه ها استفاده می کنند. و حتی بیایید نگاهی گذرا به تأیید کننده بزرگ و قدرتمند بیندازیم.
ابزار توسعه بخش راهنما در مورد نحوه جمع آوری ابزارها و هسته مورد نیاز برای آزمایش ها.
نتیجه گیری. در پایان مقاله، کسانی که تا اینجا مطالعه کرده اند، کلمات انگیزشی و شرح مختصری از آنچه اتفاق خواهد افتاد را در مقالات بعدی خواهند یافت. همچنین تعدادی لینک برای خودآموزی برای کسانی که تمایل یا توانایی انتظار برای ادامه را ندارند، فهرست می کنیم.
مقدمه ای بر معماری BPF
قبل از اینکه معماری BPF را در نظر بگیریم، برای آخرین بار (oh) به آن اشاره خواهیم کرد BPF کلاسیککه به عنوان پاسخی به ظهور ماشین های RISC توسعه یافت و مشکل فیلترینگ بسته های کارآمد را حل کرد. این معماری به قدری موفق بود که در دهه نود در برکلی یونیکس متولد شد، به اکثر سیستم عاملهای موجود منتقل شد، تا دهه بیستم جان سالم به در برد و هنوز هم در حال یافتن برنامههای کاربردی جدید است.
BPF جدید به عنوان پاسخی به فراگیر بودن ماشینهای 64 بیتی، خدمات ابری و افزایش نیاز به ابزارهایی برای ایجاد SDN توسعه داده شد.Sنرم افزاری-dکارآمد nورق کاری). BPF جدید که توسط مهندسان شبکه هسته به عنوان جایگزینی بهبودیافته برای BPF کلاسیک توسعه یافته است، به معنای واقعی کلمه شش ماه بعد کاربردهایی را در کار دشوار ردیابی سیستم های لینوکس پیدا کرد و اکنون، شش سال پس از ظهور آن، ما به یک مقاله کامل بعدی نیاز داریم تا انواع مختلف برنامه ها را لیست کنید.
عکس های خنده دار
در هسته خود، BPF یک ماشین مجازی سندباکس است که به شما امکان میدهد تا کدهای «خودسرانه» را در فضای هسته بدون به خطر انداختن امنیت اجرا کنید. برنامه های BPF در فضای کاربر ایجاد می شوند، در هسته بارگذاری می شوند و به منبع رویداد متصل می شوند. یک رویداد می تواند، برای مثال، تحویل یک بسته به یک رابط شبکه، راه اندازی برخی از عملکردهای هسته و غیره باشد. در مورد بسته، برنامه BPF به داده ها و ابرداده های بسته دسترسی خواهد داشت (برای خواندن و احتمالاً نوشتن، بسته به نوع برنامه)؛ در مورد اجرای یک تابع هسته، آرگومان های تابع، از جمله اشاره گر به حافظه هسته و غیره.
بیایید نگاهی دقیق تر به این روند بیندازیم. برای شروع، اجازه دهید در مورد اولین تفاوت با BPF کلاسیک صحبت کنیم، برنامه هایی که برای آنها در اسمبلر نوشته شده است. در نسخه جدید، معماری گسترش یافت تا برنامهها را بتوان به زبانهای سطح بالا، البته در درجه اول، به زبان C نوشت. برای این کار، یک Backend برای llvm ایجاد شد که به شما امکان میدهد بایت کد برای معماری BPF تولید کنید.
معماری BPF تا حدی برای اجرای کارآمد بر روی ماشینهای مدرن طراحی شده است. برای انجام این کار در عمل، بایت کد BPF، پس از بارگیری در هسته، با استفاده از مؤلفه ای به نام کامپایلر JIT به کد اصلی ترجمه می شود.Just In Tزمان). در مرحله بعد، اگر به خاطر داشته باشید، در BPF کلاسیک، برنامه در هسته بارگذاری شده و به صورت اتمی به منبع رویداد متصل می شود - در چارچوب یک فراخوانی سیستمی واحد. در معماری جدید، این در دو مرحله اتفاق می افتد - ابتدا کد با استفاده از یک فراخوانی سیستم در هسته بارگذاری می شود. bpf(2)و سپس، بعداً، از طریق مکانیسم های دیگری که بسته به نوع برنامه متفاوت است، برنامه به منبع رویداد متصل می شود.
در اینجا خواننده ممکن است یک سوال داشته باشد: آیا این امکان وجود داشت؟ ایمنی اجرای چنین کدی چگونه تضمین می شود؟ ایمنی اجرا با مرحله بارگیری برنامه های BPF به نام verfier برای ما تضمین می شود (در زبان انگلیسی به این مرحله verfier می گویند و من همچنان از کلمه انگلیسی استفاده خواهم کرد):
Verifier یک تحلیلگر استاتیک است که تضمین می کند که برنامه در عملکرد طبیعی هسته اختلال ایجاد نمی کند. به هر حال، این بدان معنا نیست که برنامه نمی تواند در عملکرد سیستم تداخل داشته باشد - برنامه های BPF، بسته به نوع، می توانند بخش هایی از حافظه هسته را بخوانند و بازنویسی کنند، مقادیر توابع را برگردانند، برش، اضافه کنند، بازنویسی کنند. و حتی بسته های شبکه را فوروارد کنید. Verifier تضمین می کند که اجرای یک برنامه BPF هسته را خراب نمی کند و برنامه ای که طبق قوانین دسترسی نوشتن دارد، به عنوان مثال، داده های یک بسته خروجی، نمی تواند حافظه هسته را در خارج از بسته بازنویسی کند. بعد از اینکه با سایر اجزای BPF آشنا شدیم، در قسمت مربوطه به بررسی کمی جزئیات بیشتر خواهیم پرداخت.
پس تا الان چه آموخته ایم؟ کاربر برنامه ای را به زبان C می نویسد و با استفاده از فراخوانی سیستم، آن را در هسته بارگذاری می کند bpf(2)، جایی که توسط یک تأیید کننده بررسی می شود و به بایت کد بومی ترجمه می شود. سپس همان کاربر یا کاربر دیگری برنامه را به منبع رویداد متصل می کند و شروع به اجرا می کند. جداسازی دانلود و اتصال به چند دلیل ضروری است. اولاً، اجرای یک تأیید کننده نسبتاً گران است و با چندین بار دانلود یک برنامه، وقت رایانه را تلف می کنیم. ثانیا، نحوه اتصال یک برنامه دقیقاً به نوع آن بستگی دارد و یک رابط "جهانی" که یک سال پیش توسعه یافته است ممکن است برای انواع جدید برنامه ها مناسب نباشد. (اگرچه اکنون که معماری در حال بالغ شدن است، ایده ای برای یکسان سازی این رابط در سطح وجود دارد libbpf.)
خواننده ی توجه ممکن است متوجه شود که ما هنوز با تصاویر به پایان نرسیده ایم. در واقع، همه موارد فوق توضیح نمی دهد که چرا BPF اساساً تصویر را در مقایسه با BPF کلاسیک تغییر می دهد. دو نوآوری که به طور قابل توجهی دامنه کاربرد را گسترش می دهند، توانایی استفاده از حافظه مشترک و توابع کمک کننده هسته است. در BPF، حافظه مشترک با استفاده از به اصطلاح نقشه ها - ساختارهای داده مشترک با یک API خاص پیاده سازی می شود. آنها احتمالاً این نام را به این دلیل گرفتند که اولین نوع نقشه ای که ظاهر شد جدول هش بود. سپس آرایهها، جداول هش محلی (به ازای هر CPU) و آرایههای محلی، درختهای جستجو، نقشههای حاوی نشانگرهای برنامههای BPF و موارد دیگر ظاهر شدند. چیزی که اکنون برای ما جالب است این است که برنامههای BPF اکنون این قابلیت را دارند که حالت را بین تماسها حفظ کنند و آن را با سایر برنامهها و با فضای کاربر به اشتراک بگذارند.
نقشه ها از فرآیندهای کاربر با استفاده از تماس سیستمی قابل دسترسی است bpf(2)و از برنامه های BPF که در هسته با استفاده از توابع کمکی اجرا می شوند. علاوه بر این، کمککنندگان نه تنها برای کار با نقشهها، بلکه برای دسترسی به سایر قابلیتهای هسته نیز وجود دارند. برای مثال، برنامههای BPF میتوانند از توابع کمکی برای ارسال بستهها به واسطهای دیگر، تولید رویدادهای perf، دسترسی به ساختارهای هسته و غیره استفاده کنند.
به طور خلاصه، BPF توانایی بارگیری کد کاربر دلخواه، یعنی تست شده توسط تایید کننده را در فضای هسته فراهم می کند. این کد می تواند حالت بین تماس ها را ذخیره کند و داده ها را با فضای کاربر تبادل کند و همچنین به زیرسیستم های هسته مجاز توسط این نوع برنامه دسترسی دارد.
این قبلاً مشابه قابلیت های ارائه شده توسط ماژول های هسته است که در مقایسه با آن BPF مزایایی دارد (البته فقط می توانید برنامه های مشابه را مقایسه کنید ، به عنوان مثال ردیابی سیستم - نمی توانید یک درایور دلخواه با BPF بنویسید). میتوانید آستانه ورودی پایینتری (برخی از ابزارهایی که از BPF استفاده میکنند نیازی به مهارت برنامهنویسی هسته یا به طور کلی مهارت برنامهنویسی ندارند)، ایمنی زمان اجرا (برای کسانی که هنگام نوشتن سیستم را خراب نکردهاند، دست خود را در نظرات بالا ببرید. یا ماژولهای آزمایشی)، اتمی بودن - هنگام بارگذاری مجدد ماژولها، خرابی وجود دارد، و زیرسیستم BPF تضمین میکند که هیچ رویدادی از دست نمیرود (منصفانه، این برای همه انواع برنامههای BPF صادق نیست).
وجود چنین قابلیت هایی BPF را به ابزاری جهانی برای گسترش هسته تبدیل می کند، که در عمل تأیید می شود: انواع برنامه های جدید بیشتری به BPF اضافه می شود، شرکت های بزرگ بیشتری از BPF در سرورهای رزمی 24×7 استفاده می کنند، بیشتر و بیشتر. استارتآپها کسبوکار خود را بر اساس راهحلهایی میسازند که مبتنی بر BPF هستند. BPF در همه جا استفاده می شود: در محافظت در برابر حملات DDoS، ایجاد SDN (به عنوان مثال، پیاده سازی شبکه برای kubernetes)، به عنوان ابزار اصلی ردیابی سیستم و جمع آوری کننده آمار، در سیستم های تشخیص نفوذ و سیستم های sandbox و غیره.
بیایید قسمت نمای کلی مقاله را در اینجا تمام کنیم و ماشین مجازی و اکوسیستم BPF را با جزئیات بیشتری بررسی کنیم.
انحراف: آب و برق
برای اینکه بتوانید مثالهای بخشهای زیر را اجرا کنید، ممکن است به تعدادی ابزار کمکی نیاز داشته باشید، حداقل llvm/clang با پشتیبانی bpf و bpftool. در بخش ابزار توسعه می توانید دستورالعمل های مونتاژ ابزارها و همچنین هسته خود را بخوانید. این بخش در زیر قرار داده شده است تا هماهنگی ارائه ما به هم نخورد.
سیستم ثبت و دستورالعمل ماشین مجازی BPF
معماری و سیستم فرمان BPF با در نظر گرفتن این واقعیت توسعه داده شد که برنامه ها به زبان C نوشته می شوند و پس از بارگذاری در هسته، به کد بومی ترجمه می شوند. بنابراین، تعداد رجیسترها و مجموعه دستورات با توجه به تقاطع، به معنای ریاضی، قابلیتهای ماشینهای مدرن انتخاب شدند. علاوه بر این، محدودیت های مختلفی برای برنامه ها اعمال شد، به عنوان مثال، تا همین اواخر امکان نوشتن حلقه ها و زیر روال ها وجود نداشت و تعداد دستورالعمل ها به 4096 محدود می شد (اکنون برنامه های ممتاز می توانند تا یک میلیون دستورالعمل بارگیری کنند).
BPF یازده رجیستر 64 بیتی در دسترس کاربر دارد r0-r10 و یک شمارنده برنامه ثبت نام r10 حاوی یک نشانگر فریم است و فقط خواندنی است. برنامه ها به یک پشته 512 بایتی در زمان اجرا و مقدار نامحدودی از حافظه مشترک در قالب نقشه دسترسی دارند.
برنامه های BPF مجاز به اجرای مجموعه خاصی از کمک کننده های هسته نوع برنامه و اخیراً توابع منظم هستند. هر تابع فراخوانی شده می تواند تا پنج آرگومان را در رجیسترها ارسال کند r1-r5، و مقدار بازگشتی به آن ارسال می شود r0. تضمین می شود که پس از بازگشت از تابع، محتویات رجیسترها r6-r9 تغییر نخواهد کرد.
برای ترجمه کارآمد برنامه، ثبت نام کنید r0-r11 برای همه معماریهای پشتیبانیشده، با در نظر گرفتن ویژگیهای ABI معماری فعلی، بهطور منحصربهفرد به رجیسترهای واقعی نگاشت میشوند. به عنوان مثال، برای x86_64 ثبت می کند r1-r5، برای انتقال پارامترهای تابع استفاده می شود، روی نمایش داده می شود rdi, rsi, rdx, rcx, r8، که برای ارسال پارامترها به توابع موجود استفاده می شود x86_64. به عنوان مثال، کد سمت چپ به کد سمت راست به شکل زیر ترجمه می شود:
ثبت نام r0 همچنین برای برگرداندن نتیجه اجرای برنامه و در ثبات استفاده می شود r1 برنامه یک اشاره گر به متن ارسال می شود - بسته به نوع برنامه، این می تواند به عنوان مثال، یک ساختار باشد struct xdp_md (برای XDP) یا ساختار struct __sk_buff (برای برنامه های مختلف شبکه) یا ساختار struct pt_regs (برای انواع مختلف برنامه های ردیابی) و غیره.
بنابراین، ما مجموعهای از ثباتها، کمککنندههای هسته، یک پشته، یک اشارهگر زمینه و حافظه مشترک در قالب نقشهها داشتیم. نه اینکه همه اینها در سفر کاملا ضروری باشد، اما...
بیایید توضیحات را ادامه دهیم و در مورد سیستم فرمان برای کار با این اشیا صحبت کنیم. همه (تقریبا همه) دستورالعمل های BPF دارای اندازه 64 بیتی ثابت هستند. اگر به یک دستورالعمل در دستگاه Big Endian 64 بیتی نگاه کنید، خواهید دید
اینجا Code - این رمزگذاری دستورالعمل است، Dst/Src به ترتیب کدهای گیرنده و منبع هستند، Off - تورفتگی امضا شده 16 بیتی و Imm یک عدد صحیح با علامت 32 بیتی است که در برخی دستورالعمل ها استفاده می شود (شبیه به ثابت cBPF K). رمزگذاری Code یکی از دو نوع دارد:
کلاس های دستورالعمل 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 کلاسیک و همچنین هنگام مطالعه نقشه ها، فراخوانی توابع و غیره ملاقات خواهیم کرد.
بیایید به مثالی نگاه کنیم که در آن یک برنامه را کامپایل می کنیم readelf-example.c و به باینری حاصل نگاه کنید. ما محتوای اصلی را فاش خواهیم کرد readelf-example.c در زیر، پس از بازیابی منطق آن از کدهای باینری:
کدهای دستوری برابر هستند b7, 15, b7 и 95. به یاد بیاورید که سه بیت کم اهمیت ترین کلاس دستورالعمل هستند. در مورد ما، بیت چهارم تمام دستورالعمل ها خالی است، بنابراین کلاس های دستورالعمل به ترتیب 7، 5، 7، 5 هستند. کلاس 7 است. 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 کلاس 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 را برمی گردانیم. بیایید با نگاه کردن به منبع بررسی کنیم که حق با ماست:
بله، این یک برنامه بی معنی است، اما فقط به چهار دستورالعمل ساده ترجمه می شود.
مثال استثنایی: دستورالعمل 16 بایتی
قبلاً اشاره کردیم که برخی دستورالعمل ها بیش از 64 بیت را اشغال می کنند. این به عنوان مثال در مورد دستورالعمل ها صدق می کند lddw (کد = 0x18 = BPF_LD | BPF_DW | BPF_IMM) - یک کلمه دوگانه از فیلدها را در رجیستر بارگذاری کنید Imm. واقعیت این است که Imm دارای اندازه 32 و یک کلمه دوگانه 64 بیت است، بنابراین بارگذاری یک مقدار فوری 64 بیتی در یک ثبات در یک دستورالعمل 64 بیتی کار نخواهد کرد. برای این کار از دو دستورالعمل مجاور برای ذخیره قسمت دوم مقدار 64 بیتی در فیلد استفاده می شود Imm... مثال:
ما دوباره با دستورالعمل ملاقات خواهیم کرد lddw، وقتی در مورد جابجایی و کار با نقشه صحبت می کنیم.
مثال: جداسازی BPF با استفاده از ابزارهای استاندارد
بنابراین، ما یاد گرفته ایم که کدهای باینری BPF را بخوانیم و در صورت لزوم آماده تجزیه هر دستورالعملی هستیم. با این حال، شایان ذکر است که در عمل جدا کردن برنامه ها با استفاده از ابزارهای استاندارد راحت تر و سریع تر است، به عنوان مثال:
(من برای اولین بار برخی از جزئیات شرح داده شده در این بخش فرعی را از آن یاد گرفتم پست الکسی استاروویتوف در وبلاگ BPF.)
اشیاء BPF - برنامه ها و نقشه ها - از فضای کاربر با استفاده از دستورات ایجاد می شوند BPF_PROG_LOAD и BPF_MAP_CREATE تماس سیستمی bpf(2)، در بخش بعدی دقیقاً در مورد چگونگی این اتفاق صحبت خواهیم کرد. این ساختارهای داده هسته و برای هر یک از آنها ایجاد می کند refcount (شمارش مرجع) روی یک تنظیم می شود و یک توصیفگر فایل که به شی اشاره می کند به کاربر بازگردانده می شود. بعد از بسته شدن دسته refcount جسم به اندازه یک کاهش می یابد و وقتی به صفر رسید، جسم از بین می رود.
اگر برنامه از نقشه ها استفاده می کند، پس refcount این نقشه ها پس از بارگذاری برنامه یک بار افزایش می یابد، یعنی. توصیفگرهای فایل آنها را می توان از فرآیند کاربر بسته و ثابت کرد refcount صفر نمی شود:
پس از بارگیری موفقیت آمیز یک برنامه، معمولاً آن را به نوعی مولد رویداد متصل می کنیم. برای مثال، میتوانیم آن را روی یک رابط شبکه قرار دهیم تا بستههای دریافتی را پردازش کنیم یا به برخی از آنها متصل کنیم tracepoint در هسته در این مرحله شمارنده مرجع نیز یک افزایش می یابد و می توانیم توصیفگر فایل را در برنامه لودر ببندیم.
اگر اکنون بوت لودر را خاموش کنیم چه اتفاقی می افتد؟ بستگی به نوع مولد رویداد (قلاب) دارد. همه هوک های شبکه پس از تکمیل لودر وجود خواهند داشت، این ها به اصطلاح قلاب های جهانی هستند. و به عنوان مثال، برنامههای ردیابی پس از پایان فرآیندی که آنها را ایجاد کرده است منتشر میشوند (و بنابراین محلی، از «محلی به فرآیند» نامیده میشوند). از نظر فنی، قلابهای محلی همیشه یک توصیفگر فایل مربوطه در فضای کاربر دارند و بنابراین زمانی که فرآیند بسته میشود، بسته میشوند، اما هوکهای سراسری اینطور نیستند. در شکل زیر با استفاده از صلیب های قرمز سعی می کنم نشان دهم که پایان برنامه لودر چه تاثیری بر طول عمر اجسام در مورد هوک های محلی و جهانی دارد.
چرا بین هوک های محلی و جهانی تمایز وجود دارد؟ اجرای برخی از انواع برنامه های شبکه بدون فضای کاربری منطقی است، به عنوان مثال، محافظت از DDoS را تصور کنید - بوت لودر قوانین را می نویسد و برنامه BPF را به رابط شبکه متصل می کند، پس از آن بوت لودر می تواند برود و خود را بکشد. از سوی دیگر، یک برنامه ردیابی اشکال زدایی را تصور کنید که در ده دقیقه روی زانوهای خود نوشتید - وقتی تمام شد، دوست دارید هیچ زباله ای در سیستم باقی نماند و قلاب های محلی این کار را تضمین می کنند.
از طرف دیگر، تصور کنید که میخواهید به یک نقطه ردیابی در هسته متصل شوید و در طول سالها آمار جمعآوری کنید. در این صورت، شما می خواهید بخش کاربری را تکمیل کنید و هر از چند گاهی به آمار برگردید. سیستم فایل bpf این فرصت را فراهم می کند. این یک سیستم فایل کاذب فقط در حافظه است که اجازه ایجاد فایل هایی را می دهد که به اشیاء BPF ارجاع می دهند و در نتیجه افزایش می یابد. refcount اشیاء. پس از این، لودر می تواند خارج شود و اشیایی که ایجاد کرده زنده می مانند.
ایجاد فایلهایی در bpffs که به اشیاء BPF ارجاع میدهند «پین کردن» نامیده میشود (مانند عبارت زیر: «فرآیند میتواند یک برنامه یا نقشه BPF را پین کند»). ایجاد اشیاء فایل برای اشیاء BPF نه تنها برای افزایش عمر اشیاء محلی، بلکه برای استفاده از اشیاء جهانی نیز منطقی است - با بازگشت به مثال برنامه حفاظت از DDoS جهانی، میخواهیم بتوانیم بیایم و آمار را بررسی کنیم. گاهی اوقات.
سیستم فایل BPF معمولاً در آن نصب می شود /sys/fs/bpf، اما می توان آن را به صورت محلی نیز نصب کرد، به عنوان مثال، به صورت زیر:
$ mkdir bpf-mountpoint
$ sudo mount -t bpf none bpf-mountpoint
نام فایل سیستم با استفاده از دستور ایجاد می شود BPF_OBJ_PIN تماس سیستم BPF. برای نشان دادن، بیایید یک برنامه را برداریم، آن را کامپایل کنیم، آپلود کنیم و به آن پین کنیم bpffs. برنامه ما هیچ کار مفیدی انجام نمی دهد، ما فقط کد را ارائه می دهیم تا بتوانید مثال را تکرار کنید:
حالا بیایید برنامه خود را با استفاده از ابزار دانلود کنیم bpftool و به تماس های سیستم همراه نگاه کنید bpf(2) (برخی خطوط نامربوط از خروجی strace حذف شدند):
در اینجا ما برنامه را با استفاده از بارگذاری کرده ایم BPF_PROG_LOAD، یک توصیفگر فایل از هسته دریافت کرد 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. در این حالت، تمام نمونههای فعال نسخه قدیمی برنامه کار خود را به پایان میرسانند و کنترلکنندههای رویداد جدید از برنامه جدید ایجاد میشوند و "اتمیسیته" در اینجا به این معنی است که هیچ رویدادی از دست نخواهد رفت.
پیوست کردن برنامه ها به منابع رویداد
در این مقاله، ما به طور جداگانه اتصال برنامه ها به منابع رویداد را شرح نمی دهیم، زیرا مطالعه این موضوع در زمینه یک نوع برنامه خاص منطقی است. سانتی متر. مثال در زیر، نحوه اتصال برنامه هایی مانند XDP را نشان می دهیم.
دستکاری اشیا با استفاده از فراخوانی سیستم bpf
برنامه های 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
بعد از اینکه تصمیم گرفتیم که ما آپلود خواهیم کرد، می توانیم به شما بگوییم که چگونه این کار را انجام خواهیم داد:
رویدادهای جالب در یک برنامه با تعریف آرایه شروع می شود insns - برنامه BPF ما در کد ماشین. در این مورد، هر دستورالعمل برنامه BPF در ساختار بسته بندی می شود bpf_insn. عنصر اول insns با دستورالعمل ها مطابقت دارد r0 = 2، دومین - exit.
عقب نشینی هسته ماکروهای راحت تری را برای نوشتن کدهای ماشین و استفاده از فایل هدر هسته تعریف می کند tools/include/linux/filter.h می توانستیم بنویسیم
اما از آنجایی که نوشتن برنامه های BPF در کد بومی فقط برای نوشتن تست ها در هسته و مقالاتی در مورد BPF ضروری است، عدم وجود این ماکروها واقعاً زندگی توسعه دهنده را پیچیده نمی کند.
پس از تعریف برنامه BPF، به بارگذاری آن در هسته می رویم. مجموعه مینیمالیستی ما از پارامترها attr شامل نوع برنامه، مجموعه و تعداد دستورالعمل ها، مجوز مورد نیاز و نام است "woo"که از آن برای یافتن برنامه خود در سیستم پس از دانلود استفاده می کنیم. برنامه همانطور که قول داده بود با استفاده از یک تماس سیستمی در سیستم بارگذاری می شود bpf.
در پایان برنامه در یک حلقه بی نهایت قرار می گیریم که محموله را شبیه سازی می کند. بدون آن، وقتی توصیفگر فایلی که فراخوانی سیستم به ما بازگردانده بسته شود، برنامه توسط هسته کشته می شود. bpf، و ما آن را در سیستم نخواهیم دید.
خب ما آماده آزمایش هستیم بیایید برنامه را در زیر اسمبل کرده و اجرا کنیم straceبرای بررسی اینکه همه چیز همانطور که باید کار می کند:
همه چیز خوب است، bpf(2) دستگیره 3 را به ما برگرداند و ما وارد یک حلقه بی نهایت با pause(). بیایید سعی کنیم برنامه خود را در سیستم پیدا کنیم. برای انجام این کار به ترمینال دیگری می رویم و از ابزار کمکی استفاده می کنیم bpftool:
می بینیم که یک برنامه بارگذاری شده روی سیستم وجود دارد woo که شناسه جهانی آن 390 است و در حال حاضر در حال انجام است simple-prog یک توصیفگر فایل باز وجود دارد که به برنامه اشاره می کند (و اگر simple-prog پس کار را تمام خواهد کرد woo ناپدید خواهد شد). همانطور که انتظار می رفت، برنامه woo 16 بایت - دو دستورالعمل - از کدهای باینری در معماری BPF می گیرد، اما در شکل اصلی آن (x86_64) در حال حاضر 40 بایت است. بیایید به برنامه خود در شکل اصلی آن نگاه کنیم:
بدون شگفتی حالا بیایید به کد تولید شده توسط کامپایلر 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 نیاز است.
نقشه ها
برنامه های 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 را بارگیری می کند، این برنامه باید برای شما ساده به نظر برسد:
در اینجا مجموعه ای از پارامترها را تعریف می کنیم attr، که در آن می گوییم «به یک جدول هش با کلیدها و مقادیر اندازه نیاز دارم sizeof(int)، که من می توانم حداکثر چهار عنصر را در آن قرار دهم." هنگام ایجاد نقشه های BPF، می توانید پارامترهای دیگری را تعیین کنید، به عنوان مثال، به همان ترتیبی که در مثال با برنامه، نام شی را به صورت "woo".
در اینجا تماس سیستم است 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 دقیقا چگونه عناصر را می خواند و اضافه می کند؟ بیایید نگاهی به زیر کاپوت بیندازیم:
ابتدا نقشه را با شناسه جهانی آن با استفاده از دستور باز کردیم 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:
همانطور که انتظار می رود، بسیار ساده است: دستور 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 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، که دارای یک Backend برای تولید کد برای معماری BPF و همچنین یک کتابخانه است libbpf، که به شما امکان می دهد سمت کاربر برنامه های BPF را بنویسید و کد برنامه های BPF تولید شده با استفاده از آن را بارگیری کنید. llvm/clang.
در واقع، همانطور که در این مقاله و مقالات بعدی خواهیم دید، libbpf بدون آن (یا ابزارهای مشابه) کارهای بسیار زیادی انجام می دهد iproute2, libbcc, libbpf-goو غیره) زندگی کردن غیرممکن است. یکی از ویژگی های قاتل پروژه libbpf BPF CO-RE (Compile Once, Run Everywhere) است - پروژه ای که به شما امکان می دهد برنامه های BPF را بنویسید که از یک هسته به هسته دیگر قابل حمل هستند، با قابلیت اجرا بر روی API های مختلف (به عنوان مثال، زمانی که ساختار هسته از نسخه تغییر می کند. به نسخه). برای اینکه بتوانید با CO-RE کار کنید، هسته شما باید با پشتیبانی BTF کامپایل شود (ما در بخش نحوه انجام این کار را توضیح می دهیم. ابزار توسعه. با وجود فایل زیر می توانید بررسی کنید که آیا هسته شما با BTF ساخته شده است یا نه:
این فایل اطلاعات مربوط به انواع داده های مورد استفاده در هسته را ذخیره می کند و در تمام مثال های ما با استفاده از آن استفاده می شود libbpf. در مقاله بعدی به تفصیل در مورد CO-RE صحبت خواهیم کرد، اما در این مقاله - فقط برای خود یک هسته بسازید با CONFIG_DEBUG_INFO_BTF.
کتابخانه libbpf درست در دایرکتوری زندگی می کند tools/lib/bpf هسته و توسعه آن از طریق لیست پستی انجام می شود [email protected]. با این حال، یک مخزن جداگانه برای نیازهای برنامه هایی که خارج از هسته زندگی می کنند نگهداری می شود https://github.com/libbpf/libbpf که در آن کتابخانه هسته برای دسترسی به خواندن کم و بیش همانطور که هست آینه شده است.
در این بخش به نحوه ایجاد پروژه ای با استفاده از آن می پردازیم libbpf، بیایید چندین برنامه آزمایشی (کم و بیش بی معنی) بنویسیم و نحوه عملکرد آن را با جزئیات تجزیه و تحلیل کنیم. این به ما امکان می دهد تا در بخش های بعدی به راحتی توضیح دهیم که چگونه برنامه های BPF با نقشه ها، کمک کننده های هسته، BTF و غیره تعامل دارند.
به طور معمول پروژه ها با استفاده از libbpf یک مخزن GitHub را به عنوان یک زیر ماژول git اضافه کنید، همین کار را انجام خواهیم داد:
طرح بعدی ما در این بخش به شرح زیر است: یک برنامه 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 در هسته اینگونه تعریف می شود:
اگرچه برنامه ما بسیار ساده بود، اما هنوز باید به جزئیات زیادی توجه کنیم. اول، اولین فایل هدر که اضافه می کنیم این است vmlinux.h، که ما فقط با استفاده از آن تولید کردیم bpftool btf dump - اکنون نیازی به نصب بسته kernel-headers نداریم تا بفهمیم ساختارهای کرنل چگونه هستند. فایل هدر زیر از کتابخانه برای ما می آید libbpf. اکنون فقط برای تعریف ماکرو به آن نیاز داریم SEC، که کاراکتر را به بخش مناسب فایل شی ELF ارسال می کند. برنامه ما در بخش موجود است xdp/simple، جایی که قبل از اسلش نوع برنامه BPF را تعریف می کنیم - این قراردادی است که در آن استفاده می شود libbpf، بر اساس نام بخش، در هنگام راه اندازی، نوع صحیح را جایگزین می کند bpf(2). خود برنامه BPF است C - بسیار ساده و از یک خط تشکیل شده است return XDP_PASS. در نهایت یک بخش جداگانه "license" حاوی نام مجوز است.
ما میتوانیم برنامه خود را با استفاده از llvm/clang، نسخه >= 10.0.0 یا بهتر از آن بیشتر، کامپایل کنیم (به بخش مراجعه کنید ابزار توسعه):
از جمله ویژگی های جالب: ما معماری هدف را نشان می دهیم -target bpf و مسیر رسیدن به سرفصل ها libbpf، که اخیرا نصب کردیم. همچنین، فراموش نکنید -O2، بدون این گزینه ممکن است در آینده با شگفتی هایی روبرو شوید. بیایید به کد خود نگاه کنیم، آیا موفق شدیم برنامه مورد نظر خود را بنویسیم؟
بله، کار کرد! اکنون، ما یک فایل باینری با برنامه داریم و می خواهیم برنامه ای ایجاد کنیم که آن را در هسته بارگذاری کند. برای این منظور کتابخانه libbpf دو گزینه به ما ارائه می دهد - از یک API سطح پایین تر یا یک API سطح بالاتر استفاده کنید. ما راه دوم را خواهیم رفت، زیرا می خواهیم یاد بگیریم که چگونه برنامه های 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 و فایل شی ما را شرح می دهد:
ما می توانیم ردپای یک API سطح پایین را در اینجا ببینیم: ساختار struct bpf_program *simple и struct bpf_link *simple. ساختار اول به طور خاص برنامه ما را که در بخش نوشته شده است، توصیف می کند xdp/simpleو دومی نحوه اتصال برنامه به منبع رویداد را توضیح می دهد.
تابع xdp_simple_bpf__open_and_load، یک شی ELF را باز می کند، آن را تجزیه می کند، تمام ساختارها و زیرساخت ها را ایجاد می کند (علاوه بر برنامه، ELF شامل بخش های دیگری نیز می شود - داده، داده فقط خواندنی، اطلاعات اشکال زدایی، مجوز و غیره)، و سپس آن را با استفاده از یک سیستم در هسته بارگذاری می کند. زنگ زدن bpf، که می توانیم با کامپایل و اجرای برنامه بررسی کنیم:
بیایید اکنون به برنامه خود با استفاده از آن نگاه کنیم 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)
و dump (از فرم کوتاه شده دستور استفاده می کنیم 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 می توانند توابع "خارجی" - کمک کننده های هسته را اجرا کنند. این توابع کمکی به برنامه های BPF اجازه می دهد تا به ساختارهای هسته دسترسی داشته باشند، نقشه ها را مدیریت کنند، و همچنین با "دنیای واقعی" ارتباط برقرار کنند - ایجاد رویدادهای perf، کنترل سخت افزار (به عنوان مثال، تغییر مسیر بسته ها) و غیره.
مثال: bpf_get_smp_processor_id
در چارچوب پارادایم "یادگیری با مثال"، اجازه دهید یکی از توابع کمکی را در نظر بگیریم. bpf_get_smp_processor_id(), معین در پرونده kernel/bpf/helpers.c. شماره پردازنده ای را که برنامه BPF که آن را فراخوانی کرده در حال اجرا است برمی گرداند. اما ما آنقدر به معنایی آن علاقه نداریم که اجرای آن یک خط است:
تعاریف تابع کمکی BPF مشابه تعاریف فراخوانی سیستم لینوکس است. در اینجا برای مثال تابعی تعریف شده است که هیچ آرگومان ندارد. (یک تابع که مثلاً سه آرگومان می گیرد با استفاده از ماکرو تعریف می شود BPF_CALL_3. حداکثر تعداد آرگومان ها پنج است.) با این حال، این تنها بخش اول تعریف است. بخش دوم تعریف ساختار نوع است struct bpf_func_proto، که حاوی توضیحاتی از تابع کمکی است که تأیید کننده آن را درک می کند:
برای اینکه برنامه های BPF از یک نوع خاص از این تابع استفاده کنند، باید آن را ثبت کنند، مثلاً برای نوع BPF_PROG_TYPE_XDP یک تابع در هسته تعریف شده است xdp_func_proto، که از شناسه تابع کمکی مشخص می کند که آیا XDP از این تابع پشتیبانی می کند یا خیر. عملکرد ما این است پشتیبانی می کند:
انواع جدید برنامه BPF در فایل "تعریف" شده اند include/linux/bpf_types.h با استفاده از یک ماکرو BPF_PROG_TYPE. در نقل قول تعریف می شود زیرا یک تعریف منطقی است و در زبان C تعریف مجموعه کاملی از سازه های بتنی در جاهای دیگر رخ می دهد. به طور خاص، در پرونده kernel/bpf/verifier.c تمام تعاریف از فایل bpf_types.h برای ایجاد آرایه ای از ساختارها استفاده می شود bpf_verifier_ops[]:
یعنی برای هر نوع برنامه BPF یک اشاره گر به یک ساختار داده از آن نوع تعریف می شود struct bpf_verifier_ops، که با مقدار مقدار دهی اولیه می شود _name ## _verifier_ops، یعنی xdp_verifier_ops برای xdp... ساختار xdp_verifier_opsتعیین شده توسط در پرونده net/core/filter.c به شرح زیر است:
در اینجا ما عملکرد آشنای خود را می بینیم xdp_func_proto، که تأیید کننده را هر بار که با چالشی روبرو می شود اجرا می کند نوعی توابع داخل یک برنامه BPF را ببینید verifier.c.
بیایید ببینیم که چگونه یک برنامه BPF فرضی از این تابع استفاده می کند bpf_get_smp_processor_id. برای انجام این کار، برنامه را از قسمت قبلی خود به صورت زیر بازنویسی می کنیم:
به این معنا که، 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:
در خط اول دستورالعمل ها را می بینیم 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
بسیار خوب، تأیید کننده کمک کننده هسته درست را پیدا کرد.
مثال: پاس دادن آرگومان ها و در نهایت اجرای برنامه!
همه توابع کمکی در سطح اجرا یک نمونه اولیه دارند
u64 fn(u64 r1, u64 r2, u64 r3, u64 r4, u64 r5)
پارامترهای توابع کمکی در ثبات ها ارسال می شوند r1-r5، و مقدار در رجیستر برگردانده می شود r0. هیچ تابعی وجود ندارد که بیش از پنج آرگومان داشته باشد و انتظار نمی رود در آینده پشتیبانی از آنها اضافه شود.
بیایید نگاهی به کمک کننده جدید هسته و نحوه ارسال پارامترهای 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;
}
برنامه ما شماره CPU را که روی آن اجرا می شود چاپ می کند. بیایید آن را کامپایل کنیم و به کد نگاه کنیم:
در خطوط 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 و واقعا شروع شد!
در اینجا ما از تابع استفاده می کنیم 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 به شرح زیر است:
در ابتدای برنامه یک تعریف نقشه اضافه کردیم woo: این یک آرایه 8 عنصری است که مقادیری مانند آن را ذخیره می کند u64 (در C چنین آرایه ای را تعریف می کنیم u64 woo[8]). در یک برنامه "xdp/simple" ما شماره پردازنده فعلی را در یک متغیر دریافت می کنیم key و سپس با استفاده از تابع helper bpf_map_lookup_element یک اشاره گر به ورودی مربوطه در آرایه می گیریم که یک عدد آن را افزایش می دهیم. به روسی ترجمه شده است: ما آماری را محاسبه می کنیم که CPU بسته های دریافتی را پردازش کرده است. بیایید سعی کنیم برنامه را اجرا کنیم:
بیایید بررسی کنیم که او به آن متصل است 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
تقریباً تمام فرآیندها در CPU7 پردازش شدند. این برای ما مهم نیست، نکته اصلی این است که برنامه کار می کند و ما نحوه دسترسی به نقشه ها از برنامه های BPF را درک می کنیم - با استفاده از хелперов bpf_mp_*.
شاخص عرفانی
بنابراین، میتوانیم با استفاده از تماسهایی مانند، از برنامه BPF به نقشه دسترسی پیدا کنیم
$ 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):
بنابراین، می توانیم نتیجه بگیریم که در زمان راه اندازی برنامه لودر ما، پیوند به &woo با چیزی با کتابخانه جایگزین شد libbpf. ابتدا به خروجی نگاه می کنیم strace:
ما آن را می بینیم 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;
و رجیستر منبع در آن را جایگزین کنید 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:
(کد کامل را می توان یافت по ссылке). بنابراین می توانیم الگوریتم خود را گسترش دهیم:
در حین بارگذاری برنامه، تایید کننده استفاده صحیح از نقشه را بررسی می کند و آدرس ساختار مربوطه را می نویسد. 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 و توصیفگر فایل را به من برگردانید":
بخش مشکل prog_load تعریف برنامه BPF ما به عنوان مجموعه ای از ساختارها است struct bpf_insn insns[]. اما از آنجایی که ما از برنامه ای استفاده می کنیم که در C داریم، می توانیم کمی تقلب کنیم:
در کل باید 14 دستورالعمل را در قالب ساختارهایی مانند struct bpf_insn (توصیه: زباله را از بالا بردارید، بخش دستورالعمل ها را دوباره بخوانید، باز کنید linux/bpf.h и linux/bpf_common.h و سعی کنید تعیین کنید struct bpf_insn insns[] بدون کمک دیگری):
تمرینی برای کسانی که خودشان این را ننوشته اند - پیدا کنید map_fd.
یک بخش نامعلوم دیگر در برنامه ما باقی مانده است - xdp_attach. متأسفانه برنامه هایی مانند XDP را نمی توان با استفاده از تماس سیستمی متصل کرد bpf. افرادی که BPF و XDP را ایجاد کردند از جامعه آنلاین لینوکس بودند، به این معنی که از آشناترین آنها استفاده کردند (اما نه معمولی افراد) رابط برای تعامل با هسته: سوکت های نت لینک، همچنین ببینید RFC3549. ساده ترین راه برای پیاده سازی xdp_attach در حال کپی کردن کد از libbpf، یعنی از فایل netlink.c، کاری که ما انجام دادیم و کمی آن را کوتاه کردیم:
به دنیای سوکت های نت لینک خوش آمدید
یک نوع سوکت شبکه را باز کنید 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;
}
در نهایت، در اینجا تابع ما است که یک سوکت را باز می کند و یک پیام ویژه حاوی یک توصیفگر فایل به آن ارسال می کند:
بیایید ببینیم آیا برنامه ما به آن متصل شده است یا خیر 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
هورا، همه چیز کار می کند. ضمناً توجه داشته باشید که نقشه ما دوباره به صورت بایت نمایش داده می شود. این به دلیل این واقعیت است که بر خلاف libbpf ما اطلاعات نوع (BTF) را بارگیری نکردیم. اما دفعه بعد بیشتر در این مورد صحبت خواهیم کرد.
ابزار توسعه
در این بخش، حداقل جعبه ابزار توسعه BPF را بررسی خواهیم کرد.
به طور کلی، برای توسعه برنامه های BPF به چیز خاصی نیاز ندارید - BPF روی هر هسته توزیع مناسبی اجرا می شود و برنامه ها با استفاده از clang، که از پکیج قابل تهیه است. با این حال، با توجه به این واقعیت که BPF در حال توسعه است، هسته و ابزارها دائما در حال تغییر هستند، اگر نمی خواهید برنامه های BPF را با استفاده از روش های قدیمی از سال 2019 بنویسید، باید کامپایل کنید.
llvm/clang
pahole
هسته آن
bpftool
(برای مرجع، این بخش و تمام نمونه های مقاله بر روی Debian 10 اجرا شد.)
llvm/clang
BPF با LLVM سازگار است و اگرچه اخیراً برنامه های BPF را می توان با استفاده از gcc کامپایل کرد، همه توسعه های فعلی برای LLVM انجام می شود. بنابراین، ابتدا نسخه فعلی را می سازیم clang از git:
$ 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
... много времени спустя
$
اکنون می توانیم بررسی کنیم که آیا همه چیز به درستی جمع شده است یا خیر:
(دستورالعمل مونتاژ clang گرفته شده توسط من از bpf_devel_QA.)
ما برنامه هایی را که به تازگی ساخته ایم نصب نمی کنیم، بلکه فقط آنها را به آنها اضافه می کنیم PATH، برای مثال:
export PATH="`pwd`/bin:$PATH"
(این را می توان به آن اضافه کرد .bashrc یا به یک فایل جداگانه من شخصاً مواردی از این دست را به آن اضافه می کنم ~/bin/activate-llvm.sh و در صورت لزوم این کار را انجام می دهم . activate-llvm.sh.)
Pahole و 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 در جامعه شبکه لینوکس انجام می شود و بنابراین همه تغییرات دیر یا زود از طریق دیوید میلر، نگهدارنده شبکه لینوکس انجام می شود. بسته به ماهیت آنها - ویرایش ها یا ویژگی های جدید - تغییرات شبکه به یکی از دو هسته تقسیم می شود - net یا net-next. تغییرات برای BPF به همین ترتیب بین آنها توزیع می شود bpf и bpf-next، که سپس به ترتیب در net و net-next جمع می شوند. برای جزئیات بیشتر، نگاه کنید bpf_devel_QA и netdev-سؤالات متداول. بنابراین یک هسته را بر اساس سلیقه خود و نیازهای پایداری سیستمی که روی آن آزمایش می کنید انتخاب کنید (*-next هسته ها ناپایدارترین در بین موارد ذکر شده هستند).
صحبت در مورد نحوه مدیریت فایل های پیکربندی هسته فراتر از محدوده این مقاله است - فرض بر این است که شما یا از قبل می دانید چگونه این کار را انجام دهید، یا آماده برای یادگیری بدون کمک دیگری. با این حال، دستورالعملهای زیر باید کمابیش برای ارائه یک سیستم فعال با BPF کافی باشد.
یکی از کرنل های بالا را دانلود کنید:
$ git clone git://git.kernel.org/pub/scm/linux/kernel/git/bpf/bpf-next.git
$ cd bpf-next
یک پیکربندی هسته کاری حداقلی بسازید:
$ 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
bpftool
پرکاربردترین ابزار مورد استفاده در مقاله، ابزار خواهد بود bpftool، به عنوان بخشی از هسته لینوکس ارائه می شود. این توسط توسعه دهندگان BPF برای توسعه دهندگان BPF نوشته و نگهداری می شود و می توان از آن برای مدیریت انواع اشیاء BPF - بارگذاری برنامه ها، ایجاد و ویرایش نقشه ها، کاوش در زندگی اکوسیستم BPF و غیره استفاده کرد. اسناد در قالب کدهای منبع برای صفحات man را می توان یافت در هسته یا قبلاً تدوین شده است در شبکه.
در زمان نگارش این مطلب bpftool فقط برای RHEL، فدورا و اوبونتو آماده است (به عنوان مثال، این موضوع، که داستان ناتمام بسته بندی را روایت می کند bpftool در دبیان). اما اگر قبلاً هسته خود را ساخته اید، آن را بسازید 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 به شما این امکان را می دهد که یک کک را به طور موثر اندازه گیری کنید و عملکرد هسته را تغییر دهید. این سیستم در بهترین سنت های یونیکس بسیار موفق بود: یک مکانیسم ساده که به شما امکان می دهد هسته را (دوباره) برنامه ریزی کنید به تعداد زیادی از افراد و سازمان ها اجازه آزمایش می دهد. و اگرچه آزمایشها و همچنین توسعه زیرساخت BPF هنوز به پایان نرسیده است، سیستم در حال حاضر دارای یک ABI پایدار است که به شما امکان میدهد منطق تجاری قابل اعتماد و مهمتر از همه مؤثر ایجاد کنید.
من می خواهم توجه داشته باشم که به نظر من این فناوری بسیار محبوب شده است زیرا از یک طرف می تواند بازی (معماری یک ماشین را می توان در یک عصر کم و بیش درک کرد) و از طرف دیگر برای حل مشکلاتی که قبل از ظهور آن (به زیبایی) قابل حل نبودند. این دو مؤلفه در کنار هم افراد را به آزمایش و رویاپردازی وادار می کند که منجر به ظهور راه حل های نوآورانه تری می شود.
این مقاله، اگرچه مختصر نیست، اما تنها مقدمهای بر دنیای BPF است و ویژگیهای «پیشرفته» و بخشهای مهم معماری را توصیف نمیکند. طرح پیش رو چیزی شبیه به این است: مقاله بعدی مروری بر انواع برنامه های BPF خواهد بود (5.8 نوع برنامه در هسته 30 پشتیبانی می شود)، سپس در نهایت نحوه نوشتن برنامه های کاربردی BPF واقعی با استفاده از برنامه های ردیابی هسته را بررسی خواهیم کرد. به عنوان مثال، وقت آن است که یک دوره آموزشی عمیق تر در مورد معماری BPF، و به دنبال آن نمونه هایی از شبکه BPF و برنامه های امنیتی ارائه دهیم.
راهنمای مرجع BPF و XDP - مستندات مربوط به BPF از cilium، یا دقیق تر از دانیل بورکمن، یکی از سازندگان و نگهبانان BPF. این یکی از اولین توصیفات جدی است که با بقیه تفاوت دارد زیرا دانیل دقیقاً می داند در مورد چه چیزی می نویسد و هیچ اشتباهی در آنجا وجود ندارد. به طور خاص، این سند نحوه کار با برنامه های BPF از انواع XDP و TC را با استفاده از ابزار معروف شرح می دهد. ip از بسته iproute2.
Documentation/Networking/filter.txt - فایل اصلی با مستندات کلاسیک و سپس توسعه یافته BPF. اگر می خواهید به زبان اسمبلی و جزئیات معماری فنی بپردازید، خواندن خوبی است.
وبلاگ در مورد BPF از فیس بوک. همانطور که الکسی استاروویتوف (نویسنده eBPF) و آندری ناکریکو - (نگهدار) در آنجا می نویسند، به ندرت، اما به درستی به روز می شود. libbpf).
اسرار bpftool. یک تاپیک سرگرم کننده در توییتر از کوئنتین مونه با مثال ها و اسرار استفاده از bpftool.