QEMU.js: الآن جاد ومع WASM

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

المهام

نظرًا لأنني تعلمت بالفعل كيفية نقل QEMU إلى JavaScript "بطريقة ما"، فقد تقرر هذه المرة القيام بذلك بحكمة وعدم تكرار الأخطاء القديمة.

الخطأ رقم واحد: فرع من تحرير النقطة

كان خطأي الأول هو تفرع نسختي من الإصدار الأولي 2.4.1. ثم بدت لي فكرة جيدة: إذا كان إصدار النقطة موجودًا، فمن المحتمل أن يكون أكثر استقرارًا من الإصدار 2.4 البسيط، والأكثر من ذلك الفرع master. وبما أنني خططت لإضافة قدر لا بأس به من الأخطاء الخاصة بي، لم أكن بحاجة إلى أخطاء أي شخص آخر على الإطلاق. ربما هذا هو ما حدث. ولكن هذا هو الأمر: إن وحدة QEMU لا تقف ساكنة، بل إنها أعلنت في وقت ما عن تحسين الكود الذي تم إنشاؤه بنسبة 10%. فكرت وانهارت: "نعم، الآن سوف أتجمد". نحن هنا بحاجة إلى إجراء استطراد: نظرًا لطبيعة QEMU.js ذات الترابط الواحد وحقيقة أن QEMU الأصلي لا يعني عدم وجود تعدد الخيوط (أي القدرة على تشغيل عدة مسارات تعليمات برمجية غير مرتبطة في وقت واحد، و ليس فقط "استخدام جميع النوى") أمر بالغ الأهمية بالنسبة لها، فالوظائف الرئيسية للسلاسل التي كان عليّ "إيقافها" حتى أتمكن من الاتصال بها من الخارج. وقد خلق هذا بعض المشاكل الطبيعية أثناء عملية الدمج. ومع ذلك، فإن حقيقة أن بعض التغييرات من الفرع master، الذي حاولت دمج الكود الخاص بي به، تم أيضًا اختياره بشكل الكرز في إصدار النقطة (وبالتالي في فرعي) ربما أيضًا لم يكن ليضيف راحة.

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

الخطأ الثاني: منهجية TLP

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

الخطأ الثالث: النزول إلى الماء دون معرفة المخاض

ما زلت لم أتخلص تمامًا من هذا، لكنني قررت الآن عدم اتباع المسار الأقل مقاومة على الإطلاق، والقيام بذلك "كشخص بالغ"، أي كتابة الواجهة الخلفية لـ TCG الخاصة بي من الصفر، حتى لا يجب أن أقول لاحقًا، "نعم، هذا بالطبع، ببطء، لكن لا يمكنني التحكم في كل شيء - هكذا تتم كتابة TCI..." علاوة على ذلك، بدا هذا في البداية كحل واضح منذ ذلك الحين أقوم بإنشاء كود ثنائي. وكما يقولون: "تجمع غنتу"، ولكن ليس هذا": الكود بالطبع ثنائي، ولكن لا يمكن نقل التحكم إليه ببساطة - يجب دفعه بشكل صريح إلى المتصفح لتجميعه، مما يؤدي إلى كائن معين من عالم JS، والذي لا يزال بحاجة إلى يتم حفظها في مكان ما. ومع ذلك، في بنيات RISC العادية، بقدر ما أفهم، فإن الموقف النموذجي هو الحاجة إلى إعادة تعيين ذاكرة التخزين المؤقت للتعليمات بشكل صريح للتعليمات البرمجية المُعاد إنشاؤها - إذا لم يكن هذا ما نحتاجه، فهو قريب على أي حال. بالإضافة إلى ذلك، من محاولتي الأخيرة، تعلمت أن التحكم لا يبدو أنه تم نقله إلى منتصف كتلة الترجمة، لذلك لا نحتاج حقًا إلى ترجمة الكود الثانوي من أي إزاحة، ويمكننا ببساطة توليده من الوظيفة الموجودة على TB .

جاؤوا وركلوا

على الرغم من أنني بدأت في إعادة كتابة التعليمات البرمجية مرة أخرى في شهر يوليو، فقد تسللت ركلة سحرية دون أن يلاحظها أحد: عادةً ما تصل الرسائل من GitHub كإشعارات حول الردود على المشكلات وطلبات السحب، ولكن هنا، فجأة ذكر في الموضوع Binaryen كخلفية qemu في السياق: «لقد فعل شيئًا كهذا، ربما سيقول شيئًا». كنا نتحدث عن استخدام مكتبة Emscripten ذات الصلة بينارين لإنشاء WASM JIT. حسنًا، قلت إن لديك ترخيص Apache 2.0 هناك، ويتم توزيع QEMU ككل ضمن GPLv2، وهي غير متوافقة تمامًا. فجأة اتضح أن الترخيص يمكن أن يكون إصلاحه بطريقة أو بأخرى (لا أعرف: ربما أغيره، ربما الترخيص المزدوج، ربما شيئًا آخر...). وهذا بالطبع جعلني سعيدًا، لأنه بحلول ذلك الوقت كنت قد نظرت عن كثب بالفعل تنسيق ثنائي WebAssembly، وكنت حزينًا وغير مفهوم إلى حدٍ ما. كانت هناك أيضًا مكتبة يمكنها استيعاب الكتل الأساسية مع الرسم البياني الانتقالي، وإنتاج الكود الثانوي، وحتى تشغيله في المترجم نفسه، إذا لزم الأمر.

ثم كان هناك المزيد خطاب في القائمة البريدية لـ QEMU، ولكن هذا يتعلق أكثر بالسؤال، "من يحتاج إليها على أي حال؟" و هو فجأة، اتضح أنه ضروري. كحد أدنى، يمكنك تجميع إمكانيات الاستخدام التالية، إذا كانت تعمل بسرعة أكبر أو أقل:

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

ميزات وقت تشغيل المتصفح

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

الميزة الثانية هي استحالة التلاعب على مستوى منخفض بالمكدس: لا يمكنك ببساطة أخذ السياق الحالي وحفظه والانتقال إلى سياق جديد باستخدام مكدس جديد. تتم إدارة مكدس الاستدعاءات بواسطة الجهاز الظاهري JS. يبدو أن المشكلة هي أننا ما زلنا نقرر إدارة التدفقات السابقة يدويًا بالكامل؟ الحقيقة هي أن كتلة الإدخال/الإخراج في QEMU يتم تنفيذها من خلال coroutines، وهذا هو المكان الذي ستكون فيه عمليات معالجة المكدس ذات المستوى المنخفض مفيدة. لحسن الحظ، يحتوي Emscipten بالفعل على آلية للعمليات غير المتزامنة، حتى اثنتين: غير متزامن и مترجم. يعمل الأول من خلال تضخم كبير في كود JavaScript الذي تم إنشاؤه ولم يعد مدعومًا. والثاني هو "الطريقة الصحيحة" الحالية ويعمل من خلال إنشاء الكود الثانوي للمترجم الأصلي. إنه يعمل، بالطبع، ببطء، لكنه لا يؤدي إلى تضخيم الكود. صحيح، كان لا بد من المساهمة في دعم coroutines لهذه الآلية بشكل مستقل (كانت هناك بالفعل coroutines مكتوبة لـ Asyncify وكان هناك تنفيذ لنفس واجهة برمجة التطبيقات تقريبًا لـ Emterpreter، ما عليك سوى توصيلها).

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

  • كتلة I/O المفسرة حسنًا، هل كنت تتوقع حقًا محاكاة NVMe بأداء أصلي؟ 🙂
  • كود QEMU الرئيسي المترجم بشكل ثابت (المترجم، الأجهزة الأخرى التي تمت محاكاتها، وما إلى ذلك)
  • تم تجميع كود الضيف ديناميكيًا في WASM

ميزات مصادر QEMU

كما خمنت على الأرجح، يتم فصل التعليمات البرمجية الخاصة بمحاكاة بنيات الضيف والتعليمات البرمجية الخاصة بإنشاء تعليمات الجهاز المضيف في QEMU. في الواقع، الأمر أصعب قليلاً:

  • هناك بنيات الضيف
  • غير المسرعات، وهي KVM للمحاكاة الافتراضية للأجهزة على نظام Linux (لأنظمة الضيف والمضيف المتوافقة مع بعضها البعض)، وTCG لإنشاء كود JIT في أي مكان. بدءًا من QEMU 2.9، ظهر دعم لمعيار المحاكاة الافتراضية لأجهزة HAXM على نظام التشغيل Windows (التفاصيل)
  • إذا تم استخدام TCG بدلاً من المحاكاة الافتراضية للأجهزة، فسيكون لديها دعم منفصل لإنشاء التعليمات البرمجية لكل بنية مضيفة، بالإضافة إلى المترجم العالمي
  • ... وحول كل هذا - الأجهزة الطرفية التي تمت محاكاتها، وواجهة المستخدم، والترحيل، وإعادة تشغيل السجل، وما إلى ذلك.

بالمناسبة هل تعلم: لا تستطيع QEMU محاكاة الكمبيوتر بأكمله فحسب، بل يمكنها أيضًا محاكاة المعالج لعملية مستخدم منفصلة في النواة المضيفة، والتي يتم استخدامها، على سبيل المثال، بواسطة Fuzzer AFL للأجهزة الثنائية. ربما يرغب شخص ما في نقل وضع تشغيل QEMU هذا إلى JS؟ 😉

مثل معظم البرامج المجانية القديمة، تم إنشاء QEMU من خلال المكالمة configure и make. لنفترض أنك قررت إضافة شيء ما: واجهة TCG الخلفية، أو تنفيذ سلسلة الرسائل، أو شيء آخر. لا تتسرع في الشعور بالسعادة/الرعب (ضع خطًا حسب الاقتضاء) من احتمالية التواصل مع Autoconf - في الواقع، configure يبدو أن QEMU مكتوبة ذاتيًا ولا يتم إنشاؤها من أي شيء.

WebAssembly

إذن ما هو هذا الشيء المسمى WebAssembly (المعروف أيضًا باسم WASM)؟ يعد هذا بديلاً لـ Asm.js، ولم يعد يتظاهر بكونه رمز JavaScript صالحًا. على العكس من ذلك، فهو ثنائي بحت ومُحسّن، وحتى مجرد كتابة عدد صحيح فيه ليس بالأمر السهل جدًا: بالنسبة للضغط، يتم تخزينه بالتنسيق LEB128.

ربما تكون قد سمعت عن خوارزمية إعادة التكرار الخاصة بـ Asm.js - وهي عبارة عن استعادة تعليمات التحكم في تدفق التنفيذ "عالية المستوى" (أي، الحلقات، وما إلى ذلك)، والتي تم تصميم محركات JS من أجلها، من LLVM IR منخفض المستوى، أقرب إلى كود الجهاز الذي ينفذه المعالج. وبطبيعة الحال، فإن التمثيل المتوسط ​​لـ QEMU أقرب إلى الثاني. يبدو أن هذا هو، bytecode، نهاية العذاب... وبعد ذلك هناك كتل، إذا-ثم-وحلقات!..

وهذا سبب آخر يجعل Binaryen مفيدًا: يمكنه بشكل طبيعي قبول الكتل عالية المستوى القريبة مما سيتم تخزينه في WASM. ولكن يمكنها أيضًا إنتاج تعليمات برمجية من رسم بياني للكتل الأساسية والانتقالات بينها. حسنًا، لقد قلت بالفعل أنه يخفي تنسيق تخزين WebAssembly خلف واجهة برمجة تطبيقات C/C++ الملائمة.

TCG (مولد الكود الصغير)

TCG كان في الأصل الواجهة الخلفية لمترجم C. ثم، على ما يبدو، لم تتمكن من الصمود في وجه المنافسة مع دول مجلس التعاون الخليجي، ولكن في النهاية وجدت مكانها في QEMU كآلية لتوليد التعليمات البرمجية للمنصة المضيفة. هناك أيضًا واجهة خلفية 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-س:

  • كم عدد السجلات وما هو العرض الموجود في البنية المستهدفة (لدينا العدد الذي نريده، بقدر ما نريد - السؤال يدور أكثر حول ما سيتم إنشاؤه في تعليمات برمجية أكثر كفاءة بواسطة المتصفح على البنية "الهدف الكامل" ...)
  • محاذاة تعليمات المضيف: في نظام التشغيل x86، وحتى في TCI، لا تتم محاذاة التعليمات على الإطلاق، لكنني سأضع في المخزن المؤقت للكود ليس تعليمات على الإطلاق، ولكن مؤشرات إلى هياكل مكتبة Binaryen، لذلك سأقول: 4 بايت
  • ما هي التعليمات الاختيارية التي يمكن أن تولدها الواجهة الخلفية - نقوم بتضمين كل ما نجده في Binaryen، ونسمح للمسرع بتقسيم الباقي إلى تعليمات أبسط بنفسه
  • ما هو الحجم التقريبي لذاكرة التخزين المؤقت TLB التي تطلبها الواجهة الخلفية. الحقيقة هي أن كل شيء في QEMU جدي: على الرغم من وجود وظائف مساعدة تقوم بالتحميل/التخزين مع مراعاة MMU الضيف (أين سنكون بدونها الآن؟)، إلا أنهم يحفظون ذاكرة التخزين المؤقت للترجمة الخاصة بهم في شكل بنية، والتي تكون معالجتها ملائمة للتضمين مباشرة في كتل البث. والسؤال هو، ما هو الإزاحة في هذه البنية التي تتم معالجتها بكفاءة أكبر من خلال تسلسل صغير وسريع من الأوامر؟
  • هنا يمكنك تعديل الغرض من سجل واحد أو اثنين من السجلات المحجوزة، وتمكين استدعاء TB من خلال وظيفة ووصف اثنين من السجلات الصغيرة بشكل اختياري inline-وظائف مثل flush_icache_range (ولكن هذا ليس حالنا)

ملف tcg-target.inc.cبالطبع، عادة ما يكون حجمه أكبر بكثير ويحتوي على العديد من الوظائف الإلزامية:

  • التهيئة، بما في ذلك القيود المفروضة على التعليمات التي يمكن أن تعمل على أي معاملات. تم نسخها بشكل صارخ من قبلي من خلفية أخرى
  • وظيفة تأخذ تعليمات بايت كود داخلية واحدة
  • يمكنك أيضًا وضع وظائف مساعدة هنا، ويمكنك أيضًا استخدام وظائف ثابتة من tcg/tcg.c

اخترت لنفسي الاستراتيجية التالية: في الكلمات الأولى من قالب الترجمة التالي، كتبت أربع مؤشرات: علامة البداية (قيمة معينة في المنطقة المجاورة) 0xFFFFFFFF، والتي تحدد الحالة الحالية للسل)، والسياق، والوحدة النمطية التي تم إنشاؤها، والرقم السحري لتصحيح الأخطاء. في البداية تم وضع العلامة 0xFFFFFFFF - nحيث n - رقم موجب صغير، وفي كل مرة تم تنفيذه عن طريق المترجم زاد بمقدار 1. عندما وصل 0xFFFFFFFEتم التجميع، وتم حفظ الوحدة في جدول الوظائف، وتم استيرادها إلى "مشغل" صغير، حيث انتقل التنفيذ من tcg_qemu_tb_exec، وتمت إزالة الوحدة من ذاكرة QEMU.

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

بعد دراسة الكود، أدركت أن الحيلة ذات الرقم السحري سمحت لي بعدم الفشل في تدمير الكومة من خلال تحرير شيء خاطئ في مخزن مؤقت غير مهيأ في التمريرة الأولى. ولكن من الذي يعيد كتابة المخزن المؤقت لتجاوز وظيفتي لاحقًا؟ كما ينصح مطورو Emscripten، عندما واجهت مشكلة، قمت بنقل الكود الناتج مرة أخرى إلى التطبيق الأصلي، وقمت بتعيين Mozilla Record-Replay عليه... بشكل عام، في النهاية أدركت شيئًا بسيطًا: لكل كتلة، أ struct TranslationBlock مع وصفه. خمن أين... هذا صحيح، قبل الكتلة مباشرة في المخزن المؤقت. وإدراكًا لذلك، قررت التوقف عن استخدام العكازات (على الأقل بعضها)، وقمت ببساطة بإلقاء الرقم السحري، ونقل الكلمات المتبقية إلى struct TranslationBlock، وإنشاء قائمة مرتبطة منفردة يمكن اجتيازها بسرعة عند إعادة تعيين ذاكرة التخزين المؤقت للترجمة، وتحرير الذاكرة.

تبقى بعض العكازات: على سبيل المثال، مؤشرات محددة في المخزن المؤقت للتعليمات البرمجية - بعضها ببساطة BinaryenExpressionRefأي أنهم ينظرون إلى التعبيرات التي يجب وضعها خطيًا في الكتلة الأساسية التي تم إنشاؤها، الجزء هو شرط الانتقال بين BBs، والجزء هو المكان الذي يجب أن تذهب إليه. حسنًا، هناك كتل معدة بالفعل لـ Relooper ويجب توصيلها وفقًا للشروط. لتمييزها، يتم استخدام الافتراض بأن جميعها تتم محاذاتها بأربع بايتات على الأقل، بحيث يمكنك استخدام البتتين الأقل أهمية للتسمية بأمان، ما عليك سوى أن تتذكر إزالتها إذا لزم الأمر. بالمناسبة، يتم استخدام هذه التسميات بالفعل في QEMU للإشارة إلى سبب الخروج من حلقة TCG.

باستخدام ثنائيارين

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

تحتوي الوظائف أيضًا على متغيرات محلية مرقمة من الصفر من النوع: int32 / int64 / float / double. في هذه الحالة، المتغيرات المحلية n الأولى هي الوسائط التي تم تمريرها إلى الدالة. يرجى ملاحظة أنه على الرغم من أن كل شيء هنا ليس بمستوى منخفض تمامًا من حيث تدفق التحكم، إلا أن الأعداد الصحيحة لا تزال لا تحمل السمة "موقعة/غير موقعة": تعتمد كيفية تصرف الرقم على رمز العملية.

بشكل عام، يوفر Binaryen واجهة برمجة تطبيقات C بسيطة: تقوم بإنشاء وحدة نمطية، فيه إنشاء تعبيرات - أحادية، ثنائية، كتل من التعبيرات الأخرى، تدفق التحكم، إلخ. ثم تقوم بإنشاء دالة بتعبير كجسمها. إذا كان لديك، مثلي، رسم بياني انتقالي منخفض المستوى، فإن مكون إعادة التشغيل سيساعدك. بقدر ما أفهم، من الممكن استخدام تحكم عالي المستوى في تدفق التنفيذ في الكتلة، طالما أنه لا يتجاوز حدود الكتلة - أي أنه من الممكن جعل المسار الداخلي سريعًا/بطيئًا يتفرع المسار داخل رمز معالجة ذاكرة التخزين المؤقت TLB المدمج، ولكن لا يتداخل مع تدفق التحكم "الخارجي". عندما تقوم بتحرير أداة إعادة التشغيل، يتم تحرير الكتل الخاصة بها، وعندما تقوم بتحرير وحدة نمطية، تختفي التعبيرات والوظائف وما إلى ذلك المخصصة لها الساحة.

ومع ذلك، إذا كنت تريد تفسير التعليمات البرمجية بسرعة دون إنشاء وحذف مثيل مترجم فوري غير ضروري، فقد يكون من المنطقي وضع هذا المنطق في ملف C++، ومن هناك إدارة واجهة برمجة تطبيقات 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);

... إذا نسيت أي شيء، آسف، هذا فقط لتمثيل المقياس، والتفاصيل موجودة في الوثائق.

والآن يبدأ الكراك-فكس-بكس، شيء من هذا القبيل:

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.

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

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

وأخيرا لغز: لقد قمت بتجميع ملف ثنائي على بنية 32 بت، لكن الكود، من خلال عمليات الذاكرة، يتسلق من Binaryen، إلى مكان ما على المكدس، أو إلى مكان آخر في الجزء العلوي البالغ 2 جيجابايت من مساحة العنوان 32 بت. المشكلة هي أنه من وجهة نظر Binaryen، فإن هذا يؤدي إلى الوصول إلى عنوان ناتج كبير جدًا. حول كيفية الحصول على هذا؟

على طريقة المشرف

لم أقم باختبار هذا في نهاية المطاف، لكن فكرتي الأولى كانت "ماذا لو قمت بتثبيت نظام Linux 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

إضافة تعليق