QEMU.js: اکنون جدی و با WASM

روزی روزگاری برای تفریح ​​تصمیم گرفتم برگشت پذیری فرآیند را اثبات کند و یاد بگیرید که چگونه جاوا اسکریپت (به طور دقیق تر Asm.js) را از کد ماشین تولید کنید. QEMU برای آزمایش انتخاب شد و مدتی بعد مقاله ای در Habr نوشته شد. در نظرات به من توصیه شد که پروژه را در WebAssembly بازسازی کنم و حتی خودم را ترک کنم تقریبا تمام شده من به نوعی این پروژه را نمی خواستم ... کار پیش می رفت، اما بسیار کند، و اکنون، اخیراً در آن مقاله ظاهر شد تفسیر با موضوع "پس چگونه همه چیز به پایان رسید؟" در پاسخ به پاسخ دقیق من، شنیدم "این شبیه یک مقاله است." خوب اگر بتوانید یک مقاله وجود خواهد داشت. شاید کسی آن را مفید بیابد. از آن خواننده برخی از حقایق در مورد طراحی پشتیبان های تولید کد QEMU و همچنین نحوه نوشتن یک کامپایلر Just-in-Time برای یک برنامه وب را یاد می گیرد.

وظایف

از آنجایی که قبلاً یاد گرفته بودم که چگونه QEMU را به جاوا اسکریپت پورت کنم، این بار تصمیم گرفتم این کار را عاقلانه انجام دهم و اشتباهات قدیمی را تکرار نکنم.

خطای شماره یک: انشعاب از نقطه انتشار

اولین اشتباه من این بود که نسخه خود را از نسخه 2.4.1 بالادست فورک کردم. سپس به نظر من ایده خوبی به نظر می رسید: اگر انتشار نقطه وجود داشته باشد، احتمالاً از 2.4 ساده، و حتی بیشتر از شاخه، پایدارتر است. master. و از آنجایی که من قصد داشتم مقدار مناسبی از باگ های خود را اضافه کنم، اصلاً به هیچ کس دیگری نیاز نداشتم. احتمالاً اینطور شد. اما نکته اینجاست: QEMU ثابت نمی‌ماند، و حتی در مقطعی بهینه‌سازی کد تولید شده را تا 10 درصد اعلام کردند. فکر کردم: «آره، حالا می‌خواهم فریز کنم.» و شکست خوردم. در اینجا باید یک انحراف ایجاد کنیم: به دلیل ماهیت تک رشته ای QEMU.js و این واقعیت که QEMU اصلی به معنای عدم وجود چند رشته ای نیست (یعنی توانایی اجرای همزمان چندین مسیر کد نامرتبط، و نه فقط "استفاده از همه هسته ها") برای آن بسیار مهم است، کارکردهای اصلی رشته ها باید "آن را روشن کنم" تا بتوانم از بیرون فراخوانی کنم. این امر باعث ایجاد برخی مشکلات طبیعی در طول ادغام شد. با این حال، این واقعیت است که برخی از تغییرات از شاخه master، که من سعی کردم کد خود را با آن ادغام کنم، همچنین در انتشار نقطه (و بنابراین در شعبه من) انتخاب شده بودند نیز احتمالاً راحتی بیشتری را نداشتند.

به طور کلی، من تصمیم گرفتم که هنوز منطقی است که نمونه اولیه را بیرون بیاورم، آن را برای قطعات جدا کنم و نسخه جدیدی را از ابتدا بر اساس چیزی تازه تر بسازم و اکنون از master.

اشتباه شماره دو: روش TLP

در اصل، این یک اشتباه نیست، به طور کلی، فقط یک ویژگی ایجاد یک پروژه در شرایط سوء تفاهم کامل از هر دو "کجا و چگونه حرکت کنیم؟" و به طور کلی "آیا به آنجا خواهیم رسید؟" است. در این شرایط برنامه نویسی ناشیانه یک گزینه موجه بود، اما، طبیعتا، نمی خواستم بی جهت آن را تکرار کنم. این بار می‌خواستم عاقلانه این کار را انجام دهم: تعهدات اتمی، تغییر کد آگاهانه (و نه اینکه «کاراکترهای تصادفی را تا زمان کامپایل کردن (با اخطار) کنار هم قرار دهیم»، همانطور که لینوس توروالدز یک بار در مورد کسی گفته است، طبق ویکی‌نقل) و غیره.

اشتباه شماره سه: بدون اطلاع از فورد وارد آب شدن

من هنوز به طور کامل از شر آن خلاص نشده ام، اما اکنون تصمیم گرفته ام به هیچ وجه مسیر کمترین مقاومت را دنبال نکنم و "به عنوان یک بزرگسال" این کار را انجام دهم، یعنی پشتوانه TCG خود را از ابتدا بنویسم، تا نروم. بعداً باید بگوییم، "بله، این البته، به آرامی است، اما من نمی توانم همه چیز را کنترل کنم - TCI اینگونه نوشته شده است..." علاوه بر این، این در ابتدا یک راه حل واضح به نظر می رسید، زیرا من کد باینری تولید می کنم. همانطور که می گویند، "خنت جمع شدу، اما نه آن": کد البته باینری است، اما کنترل را نمی توان به سادگی به آن منتقل کرد - باید به صراحت برای کامپایل به مرورگر فشار داده شود، در نتیجه یک شی خاص از دنیای JS ایجاد می شود، که هنوز نیاز به جایی نجات یابد با این حال، در معماری‌های معمولی RISC، تا آنجا که من می‌دانم، یک وضعیت معمولی نیاز به تنظیم مجدد حافظه پنهان دستورالعمل برای کد بازسازی‌شده است - اگر این چیزی نیست که ما نیاز داریم، در هر صورت، نزدیک است. علاوه بر این، از آخرین تلاشم، متوجه شدم که به نظر نمی رسد کنترل به وسط بلوک ترجمه منتقل شود، بنابراین ما واقعاً نیازی به تفسیر بایت کد از هر افست نداریم و می توانیم به سادگی آن را از تابع در TB تولید کنیم. .

آمدند و لگد زدند

اگرچه بازنویسی کد را در ماه ژوئیه شروع کردم، اما یک ضربه جادویی بدون توجه رخ داد: معمولاً نامه‌هایی از GitHub به عنوان اعلان‌هایی در مورد پاسخ به مشکلات و درخواست‌های Pull می‌رسند، اما اینجا، ناگهان ذکر در تاپیک Binaryen به عنوان یک باطن qemu در زمینه، "او چنین کاری کرد، شاید او چیزی بگوید." ما در مورد استفاده از کتابخانه مرتبط Emscripten صحبت می کردیم باینرین برای ایجاد WASM JIT. خب من گفتم شما لایسنس آپاچی 2.0 اونجا داری و کلا QEMU تحت GPLv2 توزیع شده و خیلی با هم سازگار نیستن. ناگهان معلوم شد که مجوز می تواند باشد یه جوری درستش کن (نمی دانم: شاید آن را تغییر دهید، شاید مجوز دوگانه، شاید چیز دیگری ...). این البته باعث خوشحالی من شد، زیرا در آن زمان من قبلاً از نزدیک به آن نگاه کرده بودم فرمت باینری WebAssembly، و من به نوعی غمگین و نامفهوم بودم. همچنین کتابخانه‌ای وجود داشت که بلوک‌های اصلی را با گراف انتقال می‌بلعد، بایت کد را تولید می‌کرد و حتی در صورت لزوم آن را در خود مفسر اجرا می‌کرد.

بعد بیشتر شد یک نامه در لیست پستی QEMU، اما این بیشتر در مورد این سوال است که "به هر حال چه کسی به آن نیاز دارد؟" و هست ناگهان، معلوم شد که لازم است. اگر کم و بیش سریع کار کند، حداقل می توانید امکانات زیر را با هم خراش دهید:

  • راه اندازی چیزی آموزشی بدون هیچ گونه نصب
  • مجازی سازی در iOS، جایی که طبق شایعات، تنها برنامه ای که حق تولید کد در حال پرواز را دارد موتور JS است (این درست است؟)
  • نمایش سیستم عامل کوچک - تک فلاپی، داخلی، انواع سیستم عامل و غیره ...

ویژگی های زمان اجرا مرورگر

همانطور که قبلاً گفتم، QEMU به multithreading گره خورده است، اما مرورگر آن را ندارد. خب، یعنی نه... ابتدا اصلاً وجود نداشت، سپس WebWorkers ظاهر شد - تا آنجا که من متوجه شدم، این چند رشته ای بر اساس ارسال پیام است. بدون متغیرهای مشترک. به طور طبیعی، این مشکل در هنگام انتقال کدهای موجود بر اساس مدل حافظه مشترک ایجاد می کند. سپس تحت فشار افکار عمومی نیز با نام اجرا شد SharedArrayBuffers. کم کم معرفی شد، راه اندازی آن را در مرورگرهای مختلف جشن گرفتند، سپس سال نو را جشن گرفتند و سپس Meltdown... پس از آن به این نتیجه رسیدند که اندازه گیری زمان درشت یا درشت است، اما با کمک حافظه مشترک و یک نخی که شمارنده را افزایش می دهد، همه چیز یکسان است بسیار دقیق کار خواهد کرد. بنابراین ما multithreading را با حافظه مشترک غیرفعال کردیم. به نظر می رسد که آنها بعداً آن را دوباره روشن کردند، اما همانطور که از آزمایش اول مشخص شد، زندگی بدون آن وجود دارد، و اگر چنین است، ما سعی خواهیم کرد بدون تکیه بر چند رشته ای آن را انجام دهیم.

ویژگی دوم عدم امکان دستکاری در سطح پایین با پشته است: شما نمی توانید به سادگی متن فعلی را بگیرید، ذخیره کنید و به یک مورد جدید با یک پشته جدید تغییر دهید. پشته تماس توسط ماشین مجازی JS مدیریت می شود. به نظر می رسد، مشکل چیست، زیرا ما هنوز تصمیم گرفتیم جریان های قبلی را کاملاً دستی مدیریت کنیم؟ واقعیت این است که بلوک ورودی/خروجی در QEMU از طریق کوروتین‌ها پیاده‌سازی می‌شود، و اینجاست که دستکاری‌های سطح پایین پشته به کار می‌آیند. خوشبختانه، Emscipten قبلاً دارای مکانیزمی برای عملیات ناهمزمان است، حتی دو مورد: همگام سازی کنید и مترجم. اولین مورد از طریق نفخ قابل توجهی در کد جاوا اسکریپت تولید شده کار می کند و دیگر پشتیبانی نمی شود. دومی "روش صحیح" فعلی است و از طریق تولید بایت کد برای مفسر بومی کار می کند. البته به کندی کار می کند، اما کد را مخدوش نمی کند. درست است، پشتیبانی از کوروتین‌ها برای این مکانیسم باید به طور مستقل ارائه می‌شد (قبلاً کوروتین‌هایی برای Asyncify نوشته شده بود و تقریباً همان API برای Emterpreter پیاده‌سازی شده بود، فقط باید آنها را متصل کنید).

در حال حاضر، من هنوز موفق به تقسیم کد به یک کد کامپایل شده در WASM و تفسیر با استفاده از Emterpreter نشده ام، بنابراین دستگاه های بلاک هنوز کار نمی کنند (به قول آنها سری بعدی را ببینید ...). یعنی در نهایت باید چیزی شبیه به این چیز لایه‌ای خنده‌دار دریافت کنید:

  • بلوک I/O تفسیر شده خوب، آیا واقعا انتظار داشتید NVMe شبیه سازی شده با عملکرد بومی باشد؟ 🙂
  • کد QEMU اصلی کامپایل شده به صورت ایستا (مترجم، سایر دستگاه های شبیه سازی شده و غیره)
  • کد مهمان به صورت پویا در WASM کامپایل شده است

ویژگی های منابع QEMU

همانطور که احتمالاً قبلاً حدس زده اید، کدهای شبیه سازی معماری مهمان و کد تولید دستورالعمل های ماشین میزبان در QEMU از هم جدا شده اند. در واقع، حتی کمی پیچیده تر است:

  • معماری مهمان وجود دارد
  • وجود دارد شتاب دهنده ها، یعنی KVM برای مجازی سازی سخت افزار در لینوکس (برای سیستم های مهمان و میزبان سازگار با یکدیگر)، TCG برای تولید کد JIT در هر کجا. با شروع QEMU 2.9، پشتیبانی از استاندارد مجازی سازی سخت افزار HAXM در ویندوز ظاهر شد (جزئیات)
  • اگر از TCG استفاده می شود و نه مجازی سازی سخت افزار، پس از آن پشتیبانی از تولید کد جداگانه برای هر معماری میزبان و همچنین برای مفسر جهانی دارد.
  • ... و در مورد همه اینها - لوازم جانبی شبیه سازی شده، رابط کاربری، مهاجرت، ضبط مجدد و غیره.

در ضمن آیا می دانستید: QEMU می تواند نه تنها کل کامپیوتر، بلکه پردازنده را برای یک فرآیند کاربر جداگانه در هسته میزبان تقلید کند، که برای مثال توسط AFL fuzzer برای ابزار دقیق باینری استفاده می شود. شاید کسی بخواهد این حالت عملکرد QEMU را به JS پورت کند؟ 😉

مانند اکثر نرم افزارهای رایگان قدیمی، QEMU از طریق تماس ساخته می شود configure и make. فرض کنید تصمیم گرفتید چیزی اضافه کنید: یک باطن TCG، اجرای رشته، چیز دیگری. در ارتباط با Autoconf عجله نکنید که خوشحال/وحشتناک باشید (در صورت لزوم زیر خط بکشید) - در واقع، configure QEMU ظاهراً خود نوشته است و از چیزی تولید نشده است.

WebAssembly

پس این چیزی که WebAssembly (با نام مستعار WASM) نامیده می شود چیست؟ این جایگزینی برای Asm.js است و دیگر تظاهر به کد جاوا اسکریپت معتبر نیست. برعکس، صرفاً باینری و بهینه شده است و حتی نوشتن یک عدد صحیح در آن خیلی ساده نیست: برای فشرده بودن، در قالب ذخیره می شود. LEB128.

ممکن است درباره الگوریتم حلقه‌گذاری مجدد برای Asm.js شنیده باشید - این بازیابی دستورالعمل‌های کنترل جریان «سطح بالا» است (یعنی if-then-else، حلقه‌ها و غیره)، که موتورهای JS برای آن طراحی شده‌اند. سطح پایین LLVM IR، نزدیکتر به کد ماشین اجرا شده توسط پردازنده. طبیعتاً نمایش میانی QEMU به دومی نزدیک‌تر است. به نظر می رسد که اینجاست، بایت کد، پایان عذاب... و سپس بلوک ها، اگر-پس-دیگر و حلقه ها وجود دارد!..

و این دلیل دیگری برای مفید بودن Binaryen است: به طور طبیعی می تواند بلوک های سطح بالا را نزدیک به آنچه در WASM ذخیره می شود بپذیرد. اما همچنین می تواند کد را از نمودار بلوک های اصلی و انتقال بین آنها تولید کند. خب، قبلاً گفته‌ام که فرمت ذخیره‌سازی WebAssembly را در پشت API مناسب C/C++ پنهان می‌کند.

TCG (Tiny Code Generator)

GCT در اصل بود پس از آن، ظاهراً نتوانست در رقابت با GCC مقاومت کند، اما در نهایت جایگاه خود را در QEMU به عنوان مکانیزم تولید کد برای پلتفرم میزبان پیدا کرد. همچنین یک Backend TCG وجود دارد که مقداری بایت کد انتزاعی تولید می کند که بلافاصله توسط مفسر اجرا می شود، اما من تصمیم گرفتم این بار از استفاده از آن اجتناب کنم. با این حال، این واقعیت که در QEMU از قبل امکان فعال کردن انتقال به سل تولید شده از طریق تابع وجود دارد. tcg_qemu_tb_exec، برای من بسیار مفید بود.

برای افزودن یک باطن جدید TCG به QEMU، باید یک زیر شاخه ایجاد کنید tcg/<имя архитектуры> (در این مورد، tcg/binaryen) و شامل دو فایل است: tcg-target.h и tcg-target.inc.c и تجویز کند همه چیز در مورد configure. شما می توانید فایل های دیگری را در آنجا قرار دهید، اما، همانطور که از نام این دو می توانید حدس بزنید، هر دو در جایی گنجانده می شوند: یکی به عنوان یک فایل هدر معمولی (که در tcg/tcg.h، و آن یکی از قبل در فایل های دیگر در فهرست ها وجود دارد tcg, accel و نه تنها)، دیگری - فقط به عنوان یک قطعه کد در tcg/tcg.c، اما به توابع استاتیک خود دسترسی دارد.

تصمیم گرفتم که زمان زیادی را برای بررسی دقیق نحوه عملکرد آن صرف کنم، به سادگی «اسکلت‌های» این دو فایل را از پیاده‌سازی پشتیبان دیگری کپی کردم، و صادقانه این موضوع را در هدر مجوز نشان دادم.

پرونده tcg-target.h عمدتا شامل تنظیمات در فرم است #define-s:

  • چند رجیستر و چه عرضی در معماری هدف وجود دارد (به هر تعداد که بخواهیم داریم، هر تعداد که بخواهیم داریم - سوال بیشتر درباره این است که چه چیزی توسط مرورگر در معماری "کاملاً هدف" به کدهای کارآمدتر تولید می شود. ...)
  • تراز دستورات میزبان: در x86 و حتی در TCI، دستورالعمل ها به هیچ وجه تراز نیستند، اما من قصد دارم در بافر کد به هیچ وجه دستورالعمل ها، بلکه اشاره گر ساختارهای کتابخانه Binaryen را قرار دهم، بنابراین می گویم: 4 بایت ها
  • چه دستورالعمل های اختیاری باطن می تواند ایجاد کند - ما همه چیزهایی را که در Binaryen می یابیم اضافه می کنیم، به شتاب دهنده اجازه می دهیم بقیه را به موارد ساده تر تقسیم کند.
  • اندازه تقریبی کش TLB درخواست شده توسط باطن چقدر است. واقعیت این است که در QEMU همه چیز جدی است: اگرچه توابع کمکی وجود دارند که بارگذاری/ذخیره را با در نظر گرفتن MMU مهمان انجام می‌دهند (اکنون بدون آن کجا بودیم؟)، آنها حافظه پنهان ترجمه خود را به شکل یک ساختار ذخیره می‌کنند. که پردازش آن برای جاسازی مستقیم در بلوک های پخش راحت است. سوال این است که کدام افست در این ساختار با توالی کوچک و سریع دستورات به بهترین شکل پردازش می شود؟
  • در اینجا می توانید هدف یک یا دو رجیستر رزرو شده را تغییر دهید، تماس TB را از طریق یک تابع فعال کنید و به صورت اختیاری چند مورد کوچک را توصیف کنید. inline-عملکردهایی مانند flush_icache_range (اما این مورد ما نیست)

پرونده tcg-target.inc.cالبته معمولاً از نظر اندازه بسیار بزرگتر است و دارای چندین عملکرد اجباری است:

  • مقداردهی اولیه، از جمله محدودیت هایی که دستورالعمل ها می توانند بر روی کدام عملوندها عمل کنند. به طور آشکار توسط من از باطن دیگری کپی شده است
  • تابعی که یک دستورالعمل بایت کد داخلی را می گیرد
  • همچنین می توانید توابع کمکی را در اینجا قرار دهید و همچنین می توانید از توابع استاتیک استفاده کنید tcg/tcg.c

برای خودم، استراتژی زیر را انتخاب کردم: در اولین کلمات بلوک ترجمه بعدی، چهار نشانگر را یادداشت کردم: یک علامت شروع (مقدار مشخصی در مجاورت 0xFFFFFFFF، که وضعیت فعلی TB را تعیین می کند، زمینه، ماژول تولید شده و شماره جادویی برای اشکال زدایی. در ابتدا علامت در قرار داده شد 0xFFFFFFFF - nجایی که n - یک عدد مثبت کوچک، و هر بار که از طریق مفسر اجرا می شد، 1 افزایش می یافت. وقتی به آن رسید 0xFFFFFFFE، کامپایل انجام شد، ماژول در جدول تابع ذخیره شد، به یک "لانچر" کوچک وارد شد، که اجرا از آن انجام شد tcg_qemu_tb_execو ماژول از حافظه QEMU حذف شد.

به تعبیر کلاسیک، "عصا، چقدر در این صدا برای قلب پروگر در هم تنیده است...". با این حال، حافظه در جایی درز کرده بود. علاوه بر این، حافظه توسط QEMU مدیریت می شد! من یک کد داشتم که هنگام نوشتن دستورالعمل بعدی (خوب، یعنی یک اشاره گر)، یکی را که لینکش قبلاً در این مکان بود حذف کردم، اما این کمکی نکرد. در واقع، در ساده ترین حالت، QEMU حافظه را در هنگام راه اندازی اختصاص می دهد و کد تولید شده را در آنجا می نویسد. هنگامی که بافر تمام می شود، کد خارج می شود و کد بعدی در جای خود شروع به نوشتن می کند.

پس از مطالعه کد، متوجه شدم که ترفند شماره جادویی به من این امکان را می‌دهد که با آزاد کردن اشتباهی در یک بافر اولیه در اولین پاس، در تخریب پشته شکست نخورم. اما چه کسی بافر را بازنویسی می کند تا بعداً عملکرد من را دور بزند؟ همانطور که توسعه دهندگان Emscripten توصیه می کنند، وقتی با مشکلی مواجه شدم، کد حاصل را به برنامه اصلی منتقل کردم، Mozilla Record-Replay را روی آن تنظیم کردم... به طور کلی، در پایان متوجه یک چیز ساده شدم: برای هر بلوک، آ struct TranslationBlock با توضیحاتش حدس بزنید کجا... درست است، درست قبل از بلوک درست در بافر. با درک این موضوع، تصمیم گرفتم استفاده از عصا را ترک کنم (حداقل تعدادی)، و به سادگی عدد جادویی را بیرون انداختم و کلمات باقی مانده را به struct TranslationBlock، یک لیست پیوندی ایجاد می کند که می تواند به سرعت در هنگام بازنشانی حافظه پنهان ترجمه از آن عبور کرده و حافظه را آزاد کند.

برخی از عصاها باقی می مانند: به عنوان مثال، نشانگرهای علامت گذاری شده در بافر کد - برخی از آنها به سادگی هستند BinaryenExpressionRef، یعنی آنها به عباراتی نگاه می کنند که باید به صورت خطی در بلوک اصلی تولید شده قرار داده شوند، بخشی شرط انتقال بین BBها است، بخشی این است که کجا باید برود. خب، از قبل بلوک های آماده شده برای Relooper وجود دارد که باید طبق شرایط به هم متصل شوند. برای تشخیص آنها، از این فرض استفاده می شود که همه آنها حداقل چهار بایت در یک راستا قرار دارند، بنابراین می توانید با خیال راحت از حداقل دو بیت برای برچسب استفاده کنید، فقط باید به خاطر داشته باشید که در صورت لزوم آن را حذف کنید. به هر حال، چنین برچسب هایی قبلاً در QEMU برای نشان دادن دلیل خروج از حلقه TCG استفاده می شود.

با استفاده از Binaryen

ماژول ها در WebAssembly حاوی توابعی هستند که هر کدام شامل یک بدنه است که یک عبارت است. عبارات عبارتند از عملیات یکنواخت و باینری، بلوک های متشکل از لیست عبارات دیگر، جریان کنترل و غیره. همانطور که قبلاً گفتم، جریان کنترل در اینجا دقیقاً به صورت شاخه های سطح بالا، حلقه ها، فراخوانی تابع و غیره سازماندهی می شود. آرگومان های توابع بر روی پشته ارسال نمی شوند، اما به طور صریح، درست مانند JS. متغیرهای سراسری نیز وجود دارد، اما من از آنها استفاده نکرده ام، بنابراین در مورد آنها به شما نمی گویم.

توابع همچنین دارای متغیرهای محلی هستند که از صفر شماره گذاری شده اند، از نوع: int32 / int64 / float / double. در این حالت، اولین n متغیر محلی آرگومان هایی هستند که به تابع ارسال می شوند. لطفاً توجه داشته باشید که اگرچه همه چیز در اینجا از نظر جریان کنترل کاملاً سطح پایین نیست، اعداد صحیح هنوز دارای ویژگی "signed/unsigned" نیستند: نحوه رفتار عدد به کد عملیات بستگی دارد.

به طور کلی، Binaryen ارائه می دهد C-API ساده: شما یک ماژول ایجاد می کنید، در او ایجاد عبارات - یکپارچه، باینری، بلوک از عبارات دیگر، کنترل جریان و غیره. سپس یک تابع با یک عبارت به عنوان بدنه آن ایجاد می کنید. اگر شما هم مثل من یک نمودار انتقال سطح پایین دارید، کامپوننت relooper به شما کمک خواهد کرد. تا آنجا که من متوجه شدم، استفاده از کنترل سطح بالای جریان اجرا در یک بلوک، تا زمانی که از مرزهای بلوک فراتر نرود، امکان پذیر است - یعنی می توان مسیر داخلی سریع / آهسته ایجاد کرد. انشعاب مسیر در داخل کد پردازش کش داخلی TLB، اما نه برای تداخل با جریان کنترل "خارجی". هنگامی که یک relooper را آزاد می کنید، بلوک های آن آزاد می شوند؛ زمانی که یک ماژول را آزاد می کنید، عبارات، توابع و غیره اختصاص داده شده به آن ناپدید می شوند. عرصه.

با این حال، اگر می‌خواهید بدون ایجاد و حذف غیرضروری یک نمونه مفسر، کد را بلافاصله تفسیر کنید، ممکن است منطقی باشد که این منطق را در یک فایل C++ قرار دهید و از آنجا مستقیماً کل API C++ کتابخانه را مدیریت کنید، با دور زدن تنظیمات آماده. لفاف ساخته شده

بنابراین برای تولید کدی که نیاز دارید

// настроить глобальные параметры (можно поменять потом)
BinaryenSetAPITracing(0);

BinaryenSetOptimizeLevel(3);
BinaryenSetShrinkLevel(2);

// создать модуль
BinaryenModuleRef MODULE = BinaryenModuleCreate();

// описать типы функций (как создаваемых, так и вызываемых)
helper_type  BinaryenAddFunctionType(MODULE, "helper-func", BinaryenTypeInt32(), int32_helper_args, ARRAY_SIZE(int32_helper_args));
// (int23_helper_args приоб^Wсоздаются отдельно)

// сконструировать супер-мега выражение
// ... ну тут уж вы как-нибудь сами :)

// потом создать функцию
BinaryenAddFunction(MODULE, "tb_fun", tb_func_type, func_locals, FUNC_LOCALS_COUNT, expr);
BinaryenAddFunctionExport(MODULE, "tb_fun", "tb_fun");
...
BinaryenSetMemory(MODULE, (1 << 15) - 1, -1, NULL, NULL, NULL, NULL, NULL, 0, 0);
BinaryenAddMemoryImport(MODULE, NULL, "env", "memory", 0);
BinaryenAddTableImport(MODULE, NULL, "env", "tb_funcs");

// запросить валидацию и оптимизацию при желании
assert (BinaryenModuleValidate(MODULE));
BinaryenModuleOptimize(MODULE);

... اگر چیزی را فراموش کردم، ببخشید، این فقط برای نشان دادن مقیاس است و جزئیات در مستندات موجود است.

و اکنون crack-fex-pex شروع می شود، چیزی شبیه به این:

static char buf[1 << 20];
BinaryenModuleOptimize(MODULE);
BinaryenSetMemory(MODULE, 0, -1, NULL, NULL, NULL, NULL, NULL, 0, 0);
int sz = BinaryenModuleWrite(MODULE, buf, sizeof(buf));
BinaryenModuleDispose(MODULE);
EM_ASM({
  var module = new WebAssembly.Module(new Uint8Array(wasmMemory.buffer, $0, $1));
  var fptr = $2;
  var instance = new WebAssembly.Instance(module, {
      'env': {
          'memory': wasmMemory,
          // ...
      }
  );
  // и вот уже у вас есть instance!
}, buf, sz);

به منظور اتصال دنیای QEMU و JS و در عین حال دسترسی سریع به توابع کامپایل شده، آرایه ای ایجاد شد (جدولی از توابع برای وارد کردن به لانچر) و توابع تولید شده در آنجا قرار گرفتند. برای محاسبه سریع شاخص، ابتدا از شاخص بلوک ترجمه کلمه صفر به عنوان آن استفاده شد، اما سپس شاخص محاسبه شده با استفاده از این فرمول به سادگی در فیلد در struct TranslationBlock.

به هر حال، نسخه ی نمایشی (در حال حاضر با مجوز مبهم) فقط تو فایرفاکس خوب کار میکنه توسعه دهندگان کروم بودند به نوعی آماده نیست به این واقعیت که شخصی می خواهد بیش از هزار نمونه از ماژول های WebAssembly ایجاد کند، بنابراین آنها به سادگی یک گیگابایت فضای آدرس مجازی را برای هر ...

فعلاً همین است. شاید اگر کسی علاقه مند باشد مقاله دیگری وجود داشته باشد. یعنی حداقل باقی می ماند فقط دستگاه های بلوک کار کنند همچنین ممکن است منطقی باشد که کامپایل ماژول های WebAssembly را ناهمزمان کنیم، همانطور که در دنیای JS مرسوم است، زیرا هنوز یک مفسر وجود دارد که می تواند همه این کارها را تا زمانی که ماژول بومی آماده شود انجام دهد.

در نهایت یک معما: شما یک باینری را روی یک معماری 32 بیتی کامپایل کرده اید، اما کد، از طریق عملیات حافظه، از Binaryen، جایی در پشته، یا جایی دیگر در 2 گیگابایت بالای فضای آدرس 32 بیتی بالا می رود. مشکل این است که از نقطه نظر Binaryen این دسترسی به یک آدرس نتیجه بسیار بزرگ است. چگونه می توان این را دور زد؟

به روش ادمین

من در نهایت این را آزمایش نکردم، اما اولین فکرم این بود که "اگر لینوکس 32 بیتی را نصب کنم چه؟" سپس قسمت بالای فضای آدرس توسط هسته اشغال خواهد شد. تنها سوال این است که چقدر اشغال خواهد شد: 1 یا 2 گیگ.

به روش برنامه نویس (گزینه ای برای پزشکان)

بیایید یک حباب در بالای فضای آدرس منفجر کنیم. من خودم نمی فهمم چرا کار می کند - آنجا قبلا باید یک پشته وجود داشته باشد. اما «ما تمرین‌کنندگان هستیم: همه چیز برای ما کار می‌کند، اما هیچ‌کس نمی‌داند چرا...»

// 2gbubble.c
// Usage: LD_PRELOAD=2gbubble.so <program>

#include <sys/mman.h>
#include <assert.h>

void __attribute__((constructor)) constr(void)
{
  assert(MAP_FAILED != mmap(1u >> 31, (1u >> 31) - (1u >> 20), PROT_NONE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0));
}

... درست است که با Valgrind سازگار نیست، اما، خوشبختانه، خود Valgrind به طور موثر همه را از آنجا بیرون می راند :)

شاید کسی توضیح بهتری درباره نحوه کار این کد من بدهد...

منبع: www.habr.com

اضافه کردن نظر