تصور کنید یک بازی کامل یا یک مفسر زبان برنامهنویسی، نه در فضای کاربر، بلکه مستقیماً در قلب تپنده سیستمعامل و درون هسته لینوکس اجرا شود. این اتفاق که تا پیش از این غیرممکن به نظر میرسید، اکنون با پروژهای به نام BPF Capsule به واقعیت تبدیل شده است.
تأییدکننده (Verifier) هسته eBPF بهگونهای طراحی شده است که هر برنامهای را که از بازگشت (Recursion) استفاده کند، فاقد محدودیتهای اثباتپذیر برای حلقهها باشد یا از پشتهی بسیار کوچک ۵۱۲ بایتی فراتر رود، رد کند. این محدودیتها باعث میشد اجرای بازی DOOM درون هسته لینوکس غیرممکن به نظر برسد. در سیستمی که تنها پنج ثبات آرگومان (Argument Registers) وجود دارد، عمق فراخوانیها محدود است و هسته باید منشأ و مجوزهای هر اشارهگر را ردیابی کند، اجرای استاندارد برنامههای پیچیده بهشدت محدود است. با این حال، طبق گزارشهای منتشر شده در ۹ سپتامبر ۲۰۲۶، پروژه BPF Capsule ثابت کرد که منطقهای پیچیده نرمافزاری — از موتورهای بازی گرفته تا مفسرهای زبان — میتوانند بهطور کامل در JIT (Just-In-Time compiler) هسته اجرا شوند.
در دموهای این پروژه، هر تیک بازی (Game Tick)، شامل یک فریم کامل، در یک فراخوانی واحد BPF به پایان میرسد. در این ساختار، فضای کاربر (Userspace) فایل WAD و ورودیهای کیبورد را تأمین میکند و در مقابل، اشارهگری به فریمبافر (Framebuffer) نهایی دریافت میکند. دلیل پیکسلپیکسل به نظر رسیدن بازی در نمایشها این است که فریمها با استفاده از کاراکترهای ترمینال روی پروتکل SSH رندر میشوند.
برای درک این دستاورد، باید ابتدا با eBPF آشنا شویم؛ سیستمی که سالهاست استاندارد طلایی برای توسعه افزونههای امن و سریع هسته است. eBPF به توسعهدهندگان اجازه میدهد بایتکدهایی را برای شبکه یا ردیابی (Tracing) بدون بارگذاری ماژولهای ریسکی هسته اجرا کنند. در این فرآیند، یک برنامه به بایتکد برای یک ماشین ثباتی کوچک کامپایل شده، از طریق سیستمکال bpf(2) بارگذاری میشود و به یک قلاب (Hook)، مانند یک بسته ورودی یا یک Tracepoint در سیستمکالها، متصل میگردد. اما همان بررسیهای امنیتی که از سیستم محافظت میکنند — یعنی اجرای نمادین (Symbolic Execution) و ردیابی سختگیرانه حافظه — مانند یک دیوار برای هر برنامهای بزرگتر از چند صد دستورالعمل عمل میکنند. اگر Verifier نتواند اثبات کند که یک اشارهگر امن است یا یک حلقه حتماً خاتمه مییابد، برنامه پیش از اجرا متوقف میشود.
BPF Capsule یک کامپایلر و محیط زمان اجرا برای برنامههای بزرگ C در محیط BPF معمولی است که هیچ نیازی به پچ کردن هسته لینوکس یا داشتن یک ماشین مجازی مجزا در فضای کاربر ندارد. این ابزار از اهدافی به قدیمی لینوکس ۵.۱۵ پشتیبانی میکند و روی هر دو معماری x86-64 و arm64 اجرا شده است. Capsule محدودیتهای Verifier را با تغییر شکل برنامه حل میکند. بهجای تلاش برای متقاعد کردن هسته به امن بودن یک برنامه عظیم C، آن را به قطعات کوچک و قابل مدیریتی به نام «مناطق» (Regions) تقسیم میکند. هر منطقه تکهای از کد با محدوده مشخص است که Verifier بهراحتی آن را تأیید میکند. سپس یک توزیعکننده (Dispatcher) کوچک بین این مناطق جابهجا میشود و عملاً یک ماشین مجازی ایجاد میکند که هسته آن را به عنوان مجموعهای از فراخوانیهای تابع ساده و امن میپذیرد.
فراتر از یک بازی: کاربردهای عملی
اگرچه اجرای DOOM یک تست استرس جذاب و با پروفایل بالا است، اما هدف اصلی پروژه، فعالسازی منطقهای پیچیده اپلیکیشن در هسته است؛ کارهایی مانند تجزیه (Parsing) پیشرفته بستههای شبکه یا نگهداری آمارهای دقیق. در حالت عادی، وقتی برنامهای برای محدودیتهای eBPF بیش از حد پیچیده است، توسعهدهندگان مجبورند آن را بهصورت دستی بازنویسی کنند تا Verifier را راضی کنند. Capsule یک مسیر خودکار را بررسی میکند که کدهای C، C++ یا Rust (نسخه no_std) را گرفته و آنها را به شکلی تبدیل میکند که هسته استاندارد لینوکس بپذیرد.
امروزه، همین طرح از طیف گستردهای از نرمافزارهای پیچیده پشتیبانی میکند:
- مفسرها: Lua، QuickJS و CPython 3.14.
- پایگاهدادهها و کتابخانهها: SQLite و zlib.
- محیطهای اجرا: wasm3 و Rust (نسخه
no_std). - هوش مصنوعی: llama2.c.
بهطور خاص، از Lua و پایتون برای بازرسی بستههای زنده مستقیماً از طریق XDP (Express Data Path) استفاده شده است. این پروژه از یک پورت دستی که از طریق یک مفسر کند اجرا میشد، تکامل یافت تا به سیستم فعلی مناطق (Regions) و فایبرها (Fibers) برسد.
جزئیات فنی: شکستن دیوارهای Verifier
نویسنده BPF Capsule برای عبور از محدودیتهای معماری eBPF، چندین مکانیزم کلیدی را پیاده کرده است:
پولشویی اشارهگرها (Pointer Laundering): Verifier معمولاً ردیابی اشارهگرهای ذخیره شده در حافظه را از دست میدهد و با آنها مانند اسکالارهای معمولی رفتار میکند. Capsule از یک سیستم «دفترداری دوطرفه» استفاده میکند. این سیستم یک کپی «پولشویی شده» از پایه بخش (Section Base) را به عنوان یک اسکالر و یک کپی اصلی را به عنوان
PTR_TO_MAP_VALUEنگه میدارد. با تفریق پایه اسکالر از آدرس ناشناخته، سیستم یک آفست محاسبه میکند، بررسی میکند که در محدوده باشد و سپس آن را به اشارهگر اصلی اضافه میکند تا دوباره یک اشارهگر معتبر هسته به دست آورد. در نمونههای اولیه، این کار شامل یک سلول volatile به نامglobalConvو یک جدول نوع BTF بود که به هسته دروغ میگفت؛ یعنی ادعا میکرد تابعی هیچ آرگومانی نمیگیرد وu64برمیگرداند تا Verifier را فریب دهد.نرمالسازی حلقهها: برای جلوگیری از اتمام بودجه یک میلیون دستورالعمل Verifier، Capsule تمام متغیرهای حلقه را از طریق یک شماره تکرار واحد به نام
nبیان میکند. اگر یک حلقه متغیرهایi،jوpرا پیش ببرد، حلقه تبدیلشده آنها را به صورتi = i0 + n * i_stepو به همین ترتیب برای بقیه بازسازی میکند. این کار به Verifier اجازه میدهد بهجای یک گره پیچیده از PHI nodes در LLVM IR، تنها یک تکرارکننده محدود را ببیند. برای کمک به ادغام وضعیتها و جلوگیری از پیمایش تکتک تکرارها، شمارندهها در حافظه volatile ذخیره و دوباره بارگذاری میشوند تا بهجای یک مقدار ثابت، یک محدوده (Range) به Verifier ارائه شود.پشتههای نرمافزاری: چون پشته واقعی BPF به ۵۱۲ بایت محدود است، Capsule پشته نرمافزاری خود را در حافظه هسته پیاده کرده است. این پشته به سمت آدرسهای پایینتر رشد میکند، بهطوری که یک اشارهگر فریم (
fp) مرز را مشخص میکند و یک اشارهگر پشته (sp) مرز تخصیص یافته را نشان میدهد. این ساختار اجازه میدهد زنجیرههای فراخوانی عمیق و بازگشتها (Recursion) اتفاق بیفتند، زیرا هسته تنها مجموعهای از انتقالهای تخت بین مناطق را میبیند. یک فریم شامل آرگومانهای متغیر (Variadic)، آرگومانهای ثابت، یک ناحیه نتیجه اختیاری، منطقه بازگشت وfpفراخواننده است.فایبرها (Fibers): این سیستم «فایبرها» را معرفی میکند که ترکیبی از پشته نرمافزاری، وضعیت ذخیره شده و یک شناسه منطقه (Region ID) است. هر فایبر دارای فضای ذخیرهسازی وضعیت و پشته مجزا است، در حالی که متغیرهای سراسری (Globals) و Heap مشترک هستند. این قابلیت، اجرای همزمان و ذخیرهسازی
_Thread_localرا درون هسته ممکن میسازد. رکورد کنترل یک فایبر، منطقه بعدی را درresume_region_idبه صورت یک شناسه عددی ذخیره میکند.توزیع مناطق: یک منطقه در نقاطی مانند فراخوانیهای پیچیده، بازگشت (Return)، تسلیم (Yield) یا لبههای بازگشتی نامناسب در حلقه به پایان میرسد. توزیعکننده، منطقه بعدی را با استفاده از یک شناسه بستهبندی شده (۱۶ بیت برای منطقه و ۸ بیت برای تابع فیزیکی) فراخوانی میکند. برای دور زدن محدودیت ۲۵۶ تابع، Capsule گروههایی از مناطق را در توابع فیزیکی BPF با اندازه تقریباً یکسان بستهبندی میکند. برای برنامههای عظیم مانند CPython، ابزار
bpf-capsule-ld --freplaceتوابع فیزیکی را با استفاده از BPF trampolines به افزونههای BPF منتقل میکند (که در x86-64 و لینوکس ۶.۰+ در arm64 پشتیبانی میشود).
عملکرد و بنچمارکها
این رویکرد هزینهای در عملکرد دارد؛ زیرا برنامه باید مدام به توزیعکننده بازگردد تا بین مناطق جابهجا شود، بنابراین کندتر از کد Native در فضای کاربر است. هزینه بسته به تعداد عبور از مرز مناطق و الگوهای دسترسی به حافظه متفاوت است.
نتایج روی Intel i7-12700K (لینوکس ۷.۱.۳، ۴.۹ گیگاهرتز، پروفایل ۶.۹):
- DOOM (یک فریم): ۰.۳۶۷ میلیثانیه (۳.۵ برابر کندتر از حالت Native که ۰.۱۰۵ میلیثانیه است).
- SQLite: ۲۸۹.۴ میلیثانیه (۶.۶ برابر کندتر از حالت Native که ۴۳.۸ میلیثانیه است).
- Lua: ۵۲۴.۰ میلیثانیه (۷.۵ برابر کندتر از حالت Native که ۷۰.۱ میلیثانیه است).
- QuickJS: ۱۰۶۸.۰ میلیثانیه (۹.۲ برابر کندتر از حالت Native که ۱۱۶.۶ میلیثانیه است).
- CPython: ۱۸۸۹.۲ میلیثانیه (۱۵.۶ برابر کندتر از حالت Native که ۱۲۰.۹ میلیثانیه است).
- llama2.c (Q8): ۲۷۴.۸ میلیثانیه (۱۸.۸ برابر کندتر از حالت Native که ۱۴.۶ میلیثانیه است).
- llama2.c (FP32): ۴۷۱.۲ میلیثانیه (۶۲.۹ برابر کندتر از حالت Native که ۷.۵ میلیثانیه است).
نتایج روی ARM64 Cortex-A720 (لینوکس ۷.۰.۱۲، ۲.۶ گیگاهرتز، پروفایل ۶.۱۰):
- DOOM (یک فریم): ۰.۹۱۳ میلیثانیه (۴.۰ برابر کندتر از حالت Native که ۰.۲۲۷ میلیثانیه است).
- SQLite: ۶۳۹.۲ میلیثانیه (۷.۹ برابر کندتر از حالت Native که ۸۱.۴ میلیثانیه است).
- Lua: ۱۰۹۰.۴ میلیثانیه (۸.۵ برابر کندتر از حالت Native که ۱۲۷.۶ میلیثانیه است).
- QuickJS: ۲۳۱۳.۱ میلیثانیه (۱۰.۳ برابر کندتر از حالت Native که ۲۲۴.۰ میلیثانیه است).
- CPython: ۴۴۸۷.۹ میلیثانیه (۱۸.۵ برابر کندتر از حالت Native که ۲۴۲.۳ میلیثانیه است).
- llama2.c (Q8): ۵۳۹.۳ میلیثانیه (۲۳.۲ برابر کندتر از حالت Native که ۲۳.۲ میلیثانیه است).
- llama2.c (FP32): ۷۷۵.۲ میلیثانیه (۵۹.۶ برابر کندتر از حالت Native که ۱۳.۰ میلیثانیه است).
جریمه سنگین برای llama2.c در حالت FP32 به دلیل نبود واحد پردازش اعشاری (FPU) در هسته است که باعث میشود هر عملیات به محاسبات اعشاری نرمافزاری از طریق compiler-rt تبدیل شود. در مقابل، مدل Q8 بخش زیادی از این محاسبات را با عملیات اعداد صحیح جایگزین میکند و جریمه را به حدود ۱۹ تا ۲۳ برابر کاهش میدهد.
کاربرد در دنیای واقعی: XDP و Lua
فراتر از جنبه نمایشی اجرای DOOM، این پروژه کاربرد عملی در شبکههای پرسرعت را نشان میدهد. با متصل کردن Lua 5.5.1 و CPython به قلابهای XDP، سیستم میتواند بستههای زنده را با استفاده از زبانهای اسکریپتنویسی سطح بالا بازرسی کند.
برای حفظ امنیت، Capsule مناطق را به «مناطق اسکالر» و «مناطق کانتکست» تقسیم میکند. مناطق کانتکست، struct xdp_md * واقعی را به عنوان اولین آرگومان دریافت میکنند و آن را در طول زنجیره فراخوانی در ثباتها و پشته واقعی BPF نگه میدارند. این کار مانع از ذخیره اشارهگر در یک فریم نرمافزاری میشود (که باعث رد شدن برنامه توسط Verifier میشد). کامپایلر این آرگومان پنهان را از طریق capsule_borrowed_ctx() بین مرز مناطق منتقل میکند.
در یک تست دانلود گیگابیتی روی ماشین ARM64، یک ناظر بسته مبتنی بر Lua توانست توان عملیاتی ۹۶۴.۶ مگابیت بر ثانیه را هنگام تجزیه هدرها (با هزینه ۵.۸ میکروثانیه برای هر بسته) حفظ کند. اما وقتی هر بسته به یک خط متن برای فضای کاربر تبدیل شد، توان عملیاتی به ۲۹۵.۵ مگابیت بر ثانیه (۳۵.۴ میکروثانیه برای هر بسته) کاهش یافت. CPython کندتر بود و به ۷۶.۳ مگابیت بر ثانیه (۱۳۱.۱ میکروثانیه برای هر بسته) رسید.
مدل حافظه
در هستههای مدرن (لینوکس ۶.۹ به بالا)، Capsule از bpf_arena برای ایجاد یک پنجره مجازی ۴ گیگابایتی تراز شده (Aligned) استفاده میکند که شامل متغیرهای سراسری، Heap و پشتههای فایبر است. یک اشارهگر Capsule در واقع یک آدرس window_base + displacement معمولی است. پیش از دسترسی به حافظه، کامپایلر از دستور addr_space_cast استفاده میکند تا نتیجه را برای Verifier به عنوان PTR_TO_ARENA علامتگذاری کند.
برای هستههای قدیمیتر، سیستم این پنجره را از قطعات ۴ مگابایتی Map سرهم میکند. کامپایلر متغیرهای سراسری را بهطور متوالی قرار داده و آنها را به Mapهای داده سراسری مجزا (تا ۳۲ عدد) تقسیم میکند. تمام فضای باقیمانده، از جمله پشتههای نرمافزاری، در یک Map از نوع ARRAY ذخیره میشود.
برای مدیریت امنیت حافظه در این قطعات:
- تراز (Alignment): کامپایلر به تراز LLVM اعتماد میکند. مقادیر
uint64_tکه بهطور طبیعی تراز شدهاند، تضمین میشود که در یک قطعه ۴ مگابایتی قرار گیرند. - تکه تکه شدن (Fragmentation): هر بار خواندن یا نوشتن با تراز کمتر از عرض داده، به قطعات تراز شده تقسیم میشود. برای مثال، یک خواندن ۸ بایتی با تراز بایت، به هشت خواندن تکبایتی تبدیل میشود تا از عبور از مرز Map جلوگیری شود.
- تخصیص (Allocation): Capsule از الگوریتم TLSF (Two-Level Segregated Fit) برای توابع
malloc()وfree()استفاده میکند. توابع تخصیصدهنده باCAPSULE_NOSUSPENDعلامتگذاری شدهاند، به این معنی که کامپایلر اثبات میکند آنها بدون بازگشت به توزیعکننده به پایان میرسند و تضمین میکند که قفلها (Locks) در مرزهای تعلیق نگه داشته نمیشوند. اگر قفل مشغول باشد،malloc()خارج از آن عملیات دوباره تلاش میکند.
تحلیل: تغییر پارادایم هسته
این پیشرفت فرض بنیادی مبنی بر اینکه eBPF فقط برای برنامههای «کوچک» است را تغییر میدهد. با انتقال پیچیدگی از مرحله تحلیل Verifier به مرحله تبدیل کامپایلر، BPF Capsule عملاً هسته لینوکس را به یک محیط اجرای عمومی برای هر کد no_std Rust یا C/C++ تبدیل میکند.
این امر از طریق یک زنجیره ابزار (Toolchain) سفارشی محقق شده است: bpf-capsule-cc بیتکد تولید میکند و bpf-capsule-ld برنامه را با محیط زمان اجرا لینک کرده و پاسهای Capsule را پیش از تحویل نتیجه به بکاند BPF در LLVM اجرا میکند. این سیستم از Picolibc به عنوان کتابخانه C استفاده میکند و یک لایه پلتفرم برای errno محلی فایبر و Heap از نوع TLSF فراهم میکند.
برای توسعهدهندگان، این به معنای توانایی استقرار ماشینهای وضعیت پیچیده، تجزیهکنندههای پیشرفته یا حتی مدلهای سبک هوش مصنوعی (مانند llama2.c) مستقیماً در مسیر دادهها (Data Path) است. هزینه این کار، از دست دادن سرعت خام در مقایسه با کد Native است، اما دستاورد آن افزایش شدید قدرت بیان کد بدون به خطر انداختن پایداری هسته است.
گام بعدی شما
- اگر از Nix استفاده میکنید، میتوانید این پیادهسازی را با دستور
sudo nix run github:ayles/bpf-capsule#doomتست کنید. - نویسنده استدلال میکند که تکیه فعلی بر اسمبلی volatile و دروغهای BTF، نشانهای از شکاف بین LLVM و Verifier است. هدف نهایی، ایجاد یک قرارداد رسمی بین کامپایلر و Verifier است تا این ترفندهای دستی را با زیرساختهای مشترک جایگزین کند.
- بررسی کنید که آیا منطقهای پیچیده شبکه شما میتواند از فضای کاربر به هسته منتقل شود تا تأخیرهای جابهجایی داده کاهش یابد.
اما داستان سختافزاری این تحول حتی شگفتانگیزتر است — به تحلیل ما دربارهی تراشههای Blackwell و بهینهسازیهای سطح پایین مراجعه کنید.




گفتگو