Qemu.js مع دعم JIT: لا يزال من الممكن إرجاع الحشو

قبل بضع سنوات، فابريس بيلارد كتبها jslinux هو محاكي كمبيوتر مكتوب بلغة JavaScript. بعد ذلك كان هناك على الأقل أكثر من ذلك الظاهري إلى x86. لكن كلهم، على حد علمي، كانوا مترجمين فوريين، في حين أن Qemu، الذي كتبه في وقت سابق بكثير نفس فابريس بيلارد، وربما أي محاكي حديث يحترم نفسه، يستخدم تجميع JIT لكود الضيف في كود النظام المضيف. بدا لي أن الوقت قد حان لتنفيذ المهمة المعاكسة للمهمة التي تحلها المتصفحات: تجميع JIT لرمز الجهاز في JavaScript، والذي بدا من المنطقي أكثر أن يتم تنفيذه بواسطة Qemu. يبدو أن Qemu، هناك محاكيات أبسط وسهلة الاستخدام - نفس VirtualBox، على سبيل المثال - مثبتة وتعمل. لكن لدى Qemu العديد من الميزات المثيرة للاهتمام

  • مفتوح المصدر
  • القدرة على العمل بدون سائق النواة
  • القدرة على العمل في وضع مترجم
  • دعم لعدد كبير من بنيات المضيف والضيف

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

والآن، بعد عامين من العبث على مهل بكود مصدر Qemu في وقت فراغي، ظهر نموذج أولي للعمل، حيث يمكنك بالفعل تشغيل نظام التشغيل Kolibri، على سبيل المثال.

ما هو إمسكريتن

في الوقت الحاضر، ظهر العديد من المترجمين، والنتيجة النهائية هي جافا سكريبت. بعضها، مثل Type Script، كان المقصود منها في الأصل أن تكون أفضل طريقة للكتابة على الويب. وفي الوقت نفسه، تعد Emscripten طريقة لأخذ كود C أو C++ الموجود وتجميعه في نموذج يمكن قراءته في المتصفح. على هذه الصفحة لقد جمعنا العديد من منافذ البرامج المعروفة: هناعلى سبيل المثال، يمكنك إلقاء نظرة على PyPy - بالمناسبة، يزعمون أن لديهم JIT بالفعل. في الواقع، لا يمكن تجميع كل برنامج وتشغيله في المتصفح ببساطة - فهناك عدد الميزات، والتي يجب عليك تحملها، حيث أن النقش الموجود على نفس الصفحة يقول "يمكن استخدام Emscripten لتجميع أي شيء تقريبًا المحمول كود C/C++ إلى JavaScript". أي أن هناك عددًا من العمليات التي تعتبر سلوكًا غير محدد وفقًا للمعيار، ولكنها تعمل عادةً على x86 - على سبيل المثال، الوصول غير المحاذاة إلى المتغيرات، وهو أمر محظور بشكل عام في بعض البنيات. بشكل عام ، Qemu هو برنامج مشترك بين الأنظمة الأساسية، وأردت أن أصدق أنه لا يحتوي بالفعل على الكثير من السلوكيات غير المحددة - خذه وقم بتجميعه، ثم قم بالتعديل قليلاً باستخدام JIT - وبذلك تكون قد انتهيت! ولكن هذا ليس هو الحل قضية...

أول محاولة

بشكل عام، أنا لست أول شخص يأتي بفكرة نقل Qemu إلى JavaScript. تم طرح سؤال في منتدى ReactOS إذا كان ذلك ممكنًا باستخدام Emscripten. حتى في وقت سابق، كانت هناك شائعات بأن فابريس بيلارد فعل ذلك شخصيًا، لكننا كنا نتحدث عن jslinux، والذي، على حد علمي، هو مجرد محاولة لتحقيق أداء كافٍ يدويًا في JS، وقد تمت كتابته من الصفر. في وقت لاحق، تمت كتابة Virtual x86 - تم نشر مصادر غير مشوشة له، وكما ذكرنا، فإن "الواقعية" الأكبر للمحاكاة جعلت من الممكن استخدام SeaBIOS كبرامج ثابتة. بالإضافة إلى ذلك، كانت هناك محاولة واحدة على الأقل لنقل Qemu باستخدام Emscripten - لقد حاولت القيام بذلك المقبسلكن التنمية، بقدر ما أفهم، تم تجميدها.

لذلك، يبدو أن هذه هي المصادر، وهنا Emscripten - خذها وقم بتجميعها. ولكن هناك أيضًا مكتبات تعتمد عليها Qemu، ومكتبات تعتمد عليها تلك المكتبات، وما إلى ذلك، وإحدى هذه المكتبات هي لبفي، والذي يعتمد عليه العفوي. كانت هناك شائعات على الإنترنت تفيد بوجود واحد في المجموعة الكبيرة من منافذ المكتبات الخاصة بـ Emscripten، ولكن كان من الصعب تصديق ذلك إلى حد ما: أولاً، لم يكن المقصود منه أن يكون مترجمًا جديدًا، وثانيًا، كان مستوى منخفض جدًا المكتبة لالتقاطها وتجميعها إلى JS. ولا يتعلق الأمر فقط بإدراج التجميع - ربما، إذا قمت بتحريفها، بالنسبة لبعض اصطلاحات الاتصال، يمكنك إنشاء الوسائط اللازمة على المكدس واستدعاء الوظيفة بدونها. لكن Emscripten أمر صعب: من أجل جعل التعليمات البرمجية التي تم إنشاؤها تبدو مألوفة لمُحسِّن محرك JS للمتصفح، يتم استخدام بعض الحيل. على وجه الخصوص، ما يسمى بإعادة التكرار - يحاول مولد التعليمات البرمجية باستخدام LLVM IR المستلم مع بعض تعليمات الانتقال المجردة إعادة إنشاء ifs المعقولة والحلقات وما إلى ذلك. حسنًا، كيف يتم تمرير الوسائط إلى الوظيفة؟ بطبيعة الحال، كوسائط لوظائف JS، إن أمكن، ليس من خلال المكدس.

في البداية، كانت هناك فكرة تتمثل في كتابة بديل لـ libffi باستخدام JS وإجراء اختبارات قياسية، ولكن في النهاية كنت في حيرة من أمري بشأن كيفية إنشاء ملفات الرأس الخاصة بي بحيث تعمل مع الكود الموجود - فماذا يمكنني أن أفعل؟ كما يقولون: "هل المهام معقدة إلى هذا الحد، "هل نحن أغبياء إلى هذا الحد؟" اضطررت إلى نقل libffi إلى بنية أخرى، إذا جاز التعبير - لحسن الحظ، يحتوي Emscripten على وحدات ماكرو للتجميع المضمن (في Javascript، نعم - حسنًا، مهما كانت البنية، أي المجمع)، والقدرة على تشغيل التعليمات البرمجية التي تم إنشاؤها بسرعة. بشكل عام، بعد تعديل أجزاء libffi المعتمدة على النظام الأساسي لبعض الوقت، حصلت على بعض التعليمات البرمجية القابلة للتجميع وقمت بتشغيلها في أول اختبار صادفته. لدهشتي، كان الاختبار ناجحًا. أذهلتني عبقريتي - ليست مزحة، لقد نجحت منذ الإطلاق الأول - وما زلت غير مصدق لعيني، وذهبت لإلقاء نظرة على الكود الناتج مرة أخرى، لتقييم مكان الحفر بعد ذلك. هنا شعرت بالجنون للمرة الثانية - الشيء الوحيد الذي فعلته وظيفتي هو ffi_call - تم الإبلاغ عن مكالمة ناجحة. لم يكن هناك مكالمة في حد ذاتها. لذلك أرسلت طلب السحب الأول الخاص بي، والذي صحح خطأً في الاختبار يكون واضحًا لأي طالب في الأولمبياد - لا ينبغي مقارنة الأرقام الحقيقية كما هي. a == b وحتى كيف a - b < EPS - تحتاج أيضًا إلى تذكر الوحدة، وإلا فإن 0 سيكون مساويًا جدًا لـ 1/3... بشكل عام، توصلت إلى منفذ معين من libffi، والذي يجتاز أبسط الاختبارات، والذي يتم من خلاله استخدام glib جمعت - قررت أنه سيكون من الضروري، سأضيفه لاحقا. بالنظر إلى المستقبل، سأقول أنه، كما اتضح فيما بعد، لم يقم المترجم حتى بتضمين وظيفة libffi في الكود النهائي.

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

المحاولة الثانية

وفي مرحلة ما، أصبح من الواضح أن المشكلة لا تزال قائمة، وأن تحريك العكازات بشكل عشوائي حول الكود لن يؤدي إلى أي خير. الخلاصة: نحن بحاجة إلى تنظيم عملية إضافة العكازات بطريقة أو بأخرى. لذلك، تم أخذ الإصدار 2.4.1، الذي كان جديدًا في ذلك الوقت، (وليس 2.5.0، لأنه، من يدري، ستكون هناك أخطاء في الإصدار الجديد لم يتم اكتشافها بعد، ولدي ما يكفي من الأخطاء الخاصة بي )، وكان أول شيء هو إعادة كتابته بأمان thread-posix.c. حسنًا، هذا آمن: إذا حاول شخص ما إجراء عملية تؤدي إلى الحظر، فسيتم استدعاء الوظيفة على الفور abort() - بالطبع، لم يحل كل المشاكل في وقت واحد، ولكن على الأقل كان أكثر متعة إلى حد ما من تلقي البيانات غير المتسقة بهدوء.

بشكل عام، خيارات Emscripten مفيدة جدًا في نقل التعليمات البرمجية إلى JS -s ASSERTIONS=1 -s SAFE_HEAP=1 - يلتقطون بعض أنواع السلوك غير المحدد، مثل الاستدعاءات إلى عنوان غير محاذى (وهو ما لا يتوافق على الإطلاق مع التعليمات البرمجية للمصفوفات المكتوبة مثل HEAP32[addr >> 2] = 1) أو استدعاء دالة بعدد خاطئ من الوسائط.

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

تدمير الكومة

ونتيجة لذلك، تم تصحيح الوصول غير المحاذي إلى TCI، وتم إنشاء حلقة رئيسية تسمى بدورها المعالج وRCU وبعض الأشياء الصغيرة الأخرى. ولذا أقوم بتشغيل Qemu مع هذا الخيار -d exec,in_asm,out_asm، مما يعني أنك بحاجة إلى تحديد كتل التعليمات البرمجية التي يتم تنفيذها، وأيضًا في وقت البث لكتابة رمز الضيف، وما أصبح رمز المضيف (في هذه الحالة، الرمز الثانوي). يبدأ، وينفذ العديد من كتل الترجمة، ويكتب رسالة تصحيح الأخطاء التي تركتها والتي تفيد بأن RCU سيبدأ الآن و... يتعطل abort() داخل وظيفة free(). من خلال التلاعب بالوظيفة free() تمكنا من معرفة أنه في رأس كتلة الكومة، التي تقع في البايتات الثمانية التي تسبق الذاكرة المخصصة، بدلاً من حجم الكتلة أو شيء مشابه، كانت هناك قمامة.

تدمير الكومة - كم هو لطيف... في مثل هذه الحالة، هناك علاج مفيد - من (إن أمكن) نفس المصادر، قم بتجميع ثنائي أصلي وتشغيله تحت Valgrind. وبعد مرور بعض الوقت، أصبح الثنائي جاهزًا. أقوم بتشغيله بنفس الخيارات - فهو يتعطل حتى أثناء التهيئة، قبل الوصول فعليًا إلى التنفيذ. إنه أمر غير سار، بالطبع - على ما يبدو، لم تكن المصادر متماثلة تمامًا، وهذا ليس مفاجئًا، لأن التكوين اكتشف خيارات مختلفة قليلاً، ولكن لدي Valgrind - أولاً سأصلح هذا الخطأ، وبعد ذلك، إذا كنت محظوظًا ، سيظهر الأصل. أنا أقوم بتشغيل نفس الشيء ضمن Valgrind... بدأ الأمر، وخضع للتهيئة بشكل طبيعي وانتقل إلى ما بعد الخطأ الأصلي دون تحذير واحد بشأن الوصول غير الصحيح إلى الذاكرة، ناهيك عن حالات السقوط. الحياة، كما يقولون، لم تعدني لهذا - يتوقف البرنامج المعطل عن الانهيار عند إطلاقه تحت Walgrind. ما كان عليه هو لغزا. فرضيتي هي أنه بمجرد التواجد بالقرب من التعليمات الحالية بعد حدوث عطل أثناء التهيئة، أظهر gdb العمل memset-a بمؤشر صالح باستخدام أي منهما mmx، سواء xmm السجلات، فربما كان ذلك نوعًا من خطأ المحاذاة، على الرغم من أنه لا يزال من الصعب تصديق ذلك.

حسنًا، يبدو أن Valgrind لا يساعد هنا. وهنا بدأ الأمر الأكثر إثارة للاشمئزاز - يبدو أن كل شيء قد بدأ، لكنه يتعطل لأسباب غير معروفة تمامًا بسبب حدث كان من الممكن أن يحدث منذ ملايين التعليمات. لفترة طويلة، لم يكن من الواضح حتى كيفية الاقتراب. في النهاية، لا يزال يتعين علي الجلوس وتصحيح الأخطاء. أظهرت طباعة ما تمت إعادة كتابة الرأس أنه لا يبدو كرقم، بل كنوع من البيانات الثنائية. وها هو، تم العثور على هذه السلسلة الثنائية في ملف BIOS - أي أنه من الممكن الآن أن نقول بثقة معقولة أنه كان تجاوز سعة المخزن المؤقت، ومن الواضح أنه تمت كتابته في هذا المخزن المؤقت. حسنًا، شيء من هذا القبيل - في Emscripten، لحسن الحظ، لا يوجد عشوائية لمساحة العنوان، ولا توجد ثغرات فيها أيضًا، حتى تتمكن من الكتابة في مكان ما في منتصف الكود لإخراج البيانات بواسطة المؤشر من الإطلاق الأخير، انظر إلى البيانات، وانظر إلى المؤشر، وإذا لم يتغير، فاحصل على طعام للتفكير. صحيح أن الارتباط يستغرق بضع دقائق بعد أي تغيير، ولكن ماذا يمكنك أن تفعل؟ ونتيجة لذلك، تم العثور على سطر محدد يقوم بنسخ BIOS من المخزن المؤقت المؤقت إلى ذاكرة الضيف - وبالفعل، لم تكن هناك مساحة كافية في المخزن المؤقت. أدى العثور على مصدر عنوان المخزن المؤقت الغريب هذا إلى ظهور دالة qemu_anon_ram_alloc في ملف oslib-posix.c - كان المنطق كالتالي: في بعض الأحيان قد يكون من المفيد محاذاة العنوان مع صفحة ضخمة بحجم 2 ميغابايت، ولهذا سنطلب mmap أولاً أكثر قليلاً، ثم سنعيد الفائض بمساعدة munmap. وإذا لم تكن هذه المحاذاة مطلوبة، فسنشير إلى النتيجة بدلا من 2 ميغابايت getpagesize() - mmap سيظل يعطي عنوانًا محاذيًا... لذلك في Emscripten mmap مكالمات فقط malloc، ولكن بالطبع لا تتم محاذاته على الصفحة. بشكل عام، تم تصحيح الخلل الذي أحبطني لمدة شهرين من خلال تغيير في двух خطوط.

ميزات وظائف الاتصال

والآن يقوم المعالج بحساب شيء ما، لا يتعطل Qemu، لكن الشاشة لا تعمل، ويدخل المعالج بسرعة في الحلقات، إذا حكمنا من خلال الإخراج -d exec,in_asm,out_asm. ظهرت فرضية: المقاطعات المؤقتة (أو بشكل عام جميع المقاطعات) لا تصل. وبالفعل، إذا قمت بفك المقاطعات من التجميع الأصلي، والتي عملت لسبب ما، فستحصل على صورة مماثلة. ولكن لم يكن هذا هو الحل على الإطلاق: أظهرت مقارنة الآثار الصادرة مع الخيار أعلاه أن مسارات التنفيذ تباعدت مبكرًا جدًا. هنا لا بد من القول أن مقارنة ما تم تسجيله باستخدام المشغل emrun لا يعد تصحيح الأخطاء باستخدام مخرجات التجميع الأصلي عملية ميكانيكية تمامًا. لا أعرف بالضبط كيفية اتصال البرنامج الذي يعمل في المتصفح emrun، ولكن تبين أن بعض الخطوط في الإخراج قد تم إعادة ترتيبها، لذا فإن الاختلاف في الاختلاف ليس سببًا بعد لافتراض أن المسارات قد تباعدت. بشكل عام، أصبح من الواضح أنه وفقا للتعليمات ljmpl هناك انتقال إلى عناوين مختلفة، ويكون الرمز الثانوي الذي تم إنشاؤه مختلفًا بشكل أساسي: يحتوي أحدهما على تعليمات لاستدعاء وظيفة مساعدة، والآخر لا يحتوي على ذلك. وبعد البحث في جوجل عن التعليمات ودراسة الكود الذي يترجم هذه التعليمات اتضح أولا أن قبلها مباشرة في السجل cr0 تم إجراء تسجيل - أيضًا باستخدام مساعد - والذي حول المعالج إلى الوضع المحمي، وثانيًا، أن إصدار js لم يتحول أبدًا إلى الوضع المحمي. لكن الحقيقة هي أن ميزة أخرى في Emscripten هي عدم رغبتها في تحمل التعليمات البرمجية مثل تنفيذ التعليمات call في TCI، والذي ينتج عنه أي مؤشر دالة في النوع long long f(int arg0, .. int arg9) - يجب استدعاء الوظائف بالعدد الصحيح من الوسائط. إذا تم انتهاك هذه القاعدة، اعتمادًا على إعدادات تصحيح الأخطاء، فسيتعطل البرنامج (وهو أمر جيد) أو يستدعي وظيفة خاطئة على الإطلاق (وهو ما سيكون من المحزن تصحيحه). هناك أيضًا خيار ثالث - تمكين إنشاء الأغلفة التي تضيف / تزيل الوسائط، ولكن في المجمل، تشغل هذه الأغلفة مساحة كبيرة، على الرغم من حقيقة أنني في الواقع أحتاج فقط إلى ما يزيد قليلاً عن مائة غلاف. هذا وحده أمر محزن للغاية، ولكن تبين أن هناك مشكلة أكثر خطورة: في التعليمات البرمجية التي تم إنشاؤها لوظائف المجمع، تم تحويل الوسائط وتحويلها، ولكن في بعض الأحيان لم يتم استدعاء الوظيفة ذات الوسائط التي تم إنشاؤها - حسنًا، تمامًا كما في تنفيذ libffi الخاص بي. أي أن بعض المساعدين لم يتم إعدامهم ببساطة.

لحسن الحظ، يحتوي Qemu على قوائم مساعدين يمكن قراءتها بواسطة الآلة في شكل ملف رأس مثل

DEF_HELPER_0(lock, void)
DEF_HELPER_0(unlock, void)
DEF_HELPER_3(write_eflags, void, env, tl, i32)

يتم استخدامها بشكل مضحك للغاية: أولاً، يتم إعادة تعريف وحدات الماكرو بأكثر الطرق غرابة DEF_HELPER_n، ثم يتم تشغيله helper.h. إلى الحد الذي يتم فيه توسيع الماكرو إلى مُهيئ بنية وفاصلة، ثم يتم تعريف مصفوفة، وبدلاً من العناصر - #include <helper.h> ونتيجة لذلك، أتيحت لي الفرصة أخيرًا لتجربة المكتبة في العمل pyparsing، وتمت كتابة برنامج نصي يقوم بإنشاء تلك الأغلفة بالضبط للوظائف المطلوبة لها.

وهكذا، بعد ذلك بدا أن المعالج يعمل. يبدو أن السبب في ذلك هو أنه لم تتم تهيئة الشاشة مطلقًا، على الرغم من أن memtest86+ كان قادرًا على التشغيل في التجميع الأصلي. من الضروري هنا توضيح أن كود الإدخال / الإخراج الخاص بـ Qemu block مكتوب في coroutines. لدى Emscripten تطبيق صعب للغاية، لكنه لا يزال بحاجة إلى الدعم في كود Qemu، ويمكنك تصحيح أخطاء المعالج الآن: يدعم Qemu الخيارات -kernel, -initrd, -append، والتي يمكنك من خلالها تشغيل Linux أو، على سبيل المثال، memtest86+، دون استخدام الأجهزة المحظورة على الإطلاق. ولكن هنا تكمن المشكلة: في التجميع الأصلي، يمكن للمرء رؤية إخراج Linux kernel إلى وحدة التحكم مع هذا الخيار -nographicولا يوجد إخراج من المتصفح إلى الجهاز الذي تم تشغيله منه emrun، لم يأت. أي أنه ليس من الواضح: المعالج لا يعمل أو أن إخراج الرسومات لا يعمل. وبعد ذلك خطر لي أن أنتظر قليلاً. اتضح أن "المعالج لا ينام، ولكن ببساطة يومض ببطء"، وبعد حوالي خمس دقائق، ألقت النواة مجموعة من الرسائل على وحدة التحكم واستمرت في التعليق. أصبح من الواضح أن المعالج يعمل بشكل عام، ونحن بحاجة إلى البحث في التعليمات البرمجية للعمل مع SDL2. لسوء الحظ، لا أعرف كيفية استخدام هذه المكتبة، لذلك اضطررت إلى التصرف بشكل عشوائي في بعض الأماكن. في مرحلة ما، يومض الخط الموازي 0 على الشاشة على خلفية زرقاء، مما يشير إلى بعض الأفكار. في النهاية، اتضح أن المشكلة تكمن في أن Qemu يفتح عدة نوافذ افتراضية في نافذة فعلية واحدة، يمكنك التبديل بينها باستخدام Ctrl-Alt-n: فهو يعمل في الإصدار الأصلي، ولكن ليس في Emscripten. بعد التخلص من النوافذ غير الضرورية باستخدام الخيارات -monitor none -parallel none -serial none وتعليمات لإعادة رسم الشاشة بأكملها بالقوة على كل إطار، كل شيء يعمل فجأة.

كوروتين

لذلك، تعمل المحاكاة في المتصفح، لكن لا يمكنك تشغيل أي شيء مثير للاهتمام باستخدام قرص مرن واحد، لأنه لا يوجد كتلة إدخال/إخراج - تحتاج إلى تنفيذ دعم لـ coroutines. لدى Qemu بالفعل العديد من الواجهات الخلفية لـ coroutine، ولكن نظرًا لطبيعة JavaScript ومولد أكواد Emscripten، لا يمكنك البدء في التعامل مع الأكوام ببساطة. يبدو أن "كل شيء قد اختفى، تتم إزالة الجص"، لكن مطوري Emscripten قد اهتموا بالفعل بكل شيء. تم تنفيذ هذا بشكل مضحك للغاية: دعنا نسمي استدعاء دالة مثل هذا أمرًا مريبًا emscripten_sleep والعديد من الحالات الأخرى التي تستخدم آلية Asyncify، بالإضافة إلى استدعاءات المؤشر واستدعاءات أي وظيفة حيث قد تحدث إحدى الحالتين السابقتين في أسفل المكدس. والآن، قبل كل مكالمة مشبوهة، سنختار سياقًا غير متزامن، وبعد المكالمة مباشرة، سنتحقق مما إذا كانت هناك مكالمة غير متزامنة قد حدثت، وإذا حدث ذلك، فسنحفظ جميع المتغيرات المحلية في هذا السياق غير المتزامن، ونشير إلى الوظيفة لنقل التحكم إلى الوقت الذي نحتاج فيه إلى مواصلة التنفيذ والخروج من الوظيفة الحالية. هذا هو المكان الذي يوجد فيه مجال لدراسة التأثير تبديد - لتلبية احتياجات استمرار تنفيذ التعليمات البرمجية بعد العودة من مكالمة غير متزامنة، يقوم المترجم بإنشاء "بذرة" للوظيفة بدءًا من مكالمة مشبوهة - مثل هذا: إذا كان هناك n من المكالمات المشبوهة، فسيتم توسيع الوظيفة في مكان ما n/2 مرات — لا يزال هذا الوضع قائمًا، إذا لم يكن الأمر كذلك، ضع في اعتبارك أنه بعد كل مكالمة غير متزامنة محتملة، تحتاج إلى إضافة حفظ بعض المتغيرات المحلية إلى الوظيفة الأصلية. بعد ذلك، اضطررت إلى كتابة نص بسيط بلغة بايثون، والذي يعتمد على مجموعة معينة من الوظائف المفرطة الاستخدام بشكل خاص والتي من المفترض أنها "لا تسمح بعدم التزامن بالمرور عبر نفسها" (أي ترويج المكدس وكل ما وصفته للتو لا العمل فيها)، يشير إلى الاستدعاءات من خلال المؤشرات التي يجب أن يتجاهل فيها المترجم الوظائف بحيث لا تعتبر هذه الوظائف غير متزامنة. ومن الواضح أن ملفات JS التي يقل حجمها عن 60 ميجابايت هي أكثر من اللازم - دعنا نقول 30 على الأقل. على الرغم من أنني كنت أقوم بإعداد برنامج نصي للتجميع، وألقيت عن طريق الخطأ خيارات الرابط، من بينها -O3. أقوم بتشغيل الكود الذي تم إنشاؤه، فيستهلك Chromium الذاكرة ويتعطل. ثم نظرت بالصدفة إلى ما كان يحاول تنزيله... حسنًا، ماذا يمكنني أن أقول، كنت سأتجمد أيضًا لو طُلب مني دراسة وتحسين ملف Javascript بحجم 500+ ميجابايت بعناية.

لسوء الحظ، لم تكن عمليات التحقق من كود مكتبة دعم Asyncify مناسبة تمامًا longjmp-s المستخدمة في كود المعالج الظاهري، ولكن بعد تصحيح صغير يعطل عمليات التحقق هذه ويستعيد السياقات بقوة كما لو كان كل شيء على ما يرام، نجح الكود. ثم بدأ شيء غريب: في بعض الأحيان يتم تشغيل عمليات التحقق من رمز المزامنة - نفس تلك التي تعطل الكود إذا كان يجب حظره وفقًا لمنطق التنفيذ - حاول شخص ما الاستيلاء على كائن المزامنة الذي تم التقاطه بالفعل. لحسن الحظ، تبين أن هذه ليست مشكلة منطقية في الكود المتسلسل - كنت ببساطة أستخدم وظيفة الحلقة الرئيسية القياسية التي توفرها Emscripten، ولكن في بعض الأحيان قد يؤدي الاستدعاء غير المتزامن إلى إلغاء تغليف المكدس بالكامل، وفي تلك اللحظة قد يفشل setTimeout من الحلقة الرئيسية - وبالتالي، دخل الكود في تكرار الحلقة الرئيسية دون مغادرة التكرار السابق. إعادة كتابة على حلقة لا نهاية لها و emscripten_sleep، وتوقفت مشاكل كائنات المزامنة. لقد أصبح الكود أكثر منطقية - في الواقع، ليس لدي بعض التعليمات البرمجية التي تقوم بإعداد إطار الرسوم المتحركة التالي - يقوم المعالج فقط بحساب شيء ما ويتم تحديث الشاشة بشكل دوري. ومع ذلك، فإن المشاكل لم تتوقف عند هذا الحد: في بعض الأحيان، يتم إنهاء تنفيذ Qemu بصمت دون أي استثناءات أو أخطاء. في تلك اللحظة تخليت عن ذلك، ولكن، بالنظر إلى المستقبل، سأقول أن المشكلة كانت كما يلي: كود الكوروتين، في الواقع، لا يستخدم setTimeout (أو على الأقل ليس بالقدر الذي قد تعتقده): الوظيفة emscripten_yield ببساطة قم بتعيين علامة الاتصال غير المتزامنة. بيت القصيد هو أن emscripten_coroutine_next ليست وظيفة غير متزامنة: فهي تقوم داخليًا بفحص العلم وإعادة ضبطه ونقل التحكم إلى المكان المطلوب. وهذا يعني أن الترويج للمكدس ينتهي عند هذا الحد. كانت المشكلة أنه بسبب الاستخدام بعد الاستخدام المجاني، والذي ظهر عندما تم تعطيل تجمع coroutine بسبب حقيقة أنني لم أقم بنسخ سطر مهم من التعليمات البرمجية من الواجهة الخلفية لـ coroutine الموجودة، فإن الوظيفة qemu_in_coroutine عاد صحيحا في حين أنه في الواقع كان ينبغي أن يعود كاذبا. أدى هذا إلى مكالمة emscripten_yield، وفوقها لم يكن هناك أحد على المكدس emscripten_coroutine_next، تكشفت المكدس إلى الأعلى، ولكن لا setTimeout، كما قلت بالفعل، لم يتم عرضها.

توليد كود جافا سكريبت

وهنا، في الواقع، الموعود بـ “إعادة اللحم المفروم”. ليس حقيقيًا. بالطبع، إذا قمنا بتشغيل Qemu في المتصفح، وNode.js فيه، فمن الطبيعي، بعد إنشاء التعليمات البرمجية في Qemu، سنحصل على JavaScript خاطئ تمامًا. ولكن لا يزال هناك نوع من التحول العكسي.

أولاً، القليل عن كيفية عمل Qemu. من فضلك سامحني على الفور: أنا لست مطور Qemu محترفًا وقد تكون استنتاجاتي خاطئة في بعض الأماكن. وكما يقولون، "لا يجب أن يتطابق رأي الطالب مع رأي المعلم وبديهيات بيانو والفطرة السليمة". يحتوي Qemu على عدد معين من بنيات الضيف المدعومة ويوجد لكل منها دليل مثل target-i386. عند البناء، يمكنك تحديد الدعم للعديد من بنيات الضيف، ولكن النتيجة ستكون مجرد عدة ثنائيات. يقوم الكود الذي يدعم بنية الضيف بدوره بإنشاء بعض عمليات Qemu الداخلية، والتي يحولها TCG (Tiny Code Generator) بالفعل إلى كود الآلة للبنية المضيفة. كما هو مذكور في الملف التمهيدي الموجود في دليل tcg، كان هذا في الأصل جزءًا من مترجم C العادي، والذي تم تكييفه لاحقًا لـ JIT. ولذلك، على سبيل المثال، لم تعد البنية المستهدفة فيما يتعلق بهذه الوثيقة هي بنية ضيف، بل هي بنية مضيفة. في مرحلة ما، ظهر مكون آخر - Tiny Code Interpreter (TCI)، والذي يجب أن ينفذ التعليمات البرمجية (نفس العمليات الداخلية تقريبًا) في حالة عدم وجود منشئ التعليمات البرمجية لبنية مضيفة معينة. في الواقع، كما تنص وثائقه، قد لا يؤدي هذا المترجم دائمًا أداءً جيدًا كمولد كود JIT، ليس فقط من حيث السرعة من حيث الكمية، ولكن أيضًا من حيث الجودة. على الرغم من أنني لست متأكدًا من أن وصفه مناسب تمامًا.

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

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

فيما يتعلق بالنقطة الثالثة، لست متأكدًا من إمكانية التصحيح بعد تنفيذ الكود لأول مرة، لكن النقطتين الأوليين تكفيان.

في البداية، تم إنشاء الكود في شكل مفتاح كبير على عنوان تعليمات الكود الثانوي الأصلي، ولكن بعد ذلك، تذكرت المقالة حول Emscripten، وتحسين JS الذي تم إنشاؤه وإعادة التكرار، قررت إنشاء المزيد من التعليمات البرمجية البشرية، خاصة أنه من الناحية التجريبية تبين أن نقطة الدخول الوحيدة إلى كتلة الترجمة هي البداية. لم يكد الأمر على ذلك، فبعد فترة أصبح لدينا منشئ الأكواد الذي أنشأ الأكواد باستخدام ifs (وإن كان ذلك بدون حلقات). ولكن لسوء الحظ، تعطل البرنامج، مما أدى إلى ظهور رسالة مفادها أن طول التعليمات غير صحيح. علاوة على ذلك، فإن التعليمات الأخيرة على هذا المستوى العودي كانت brcond. حسنًا، سأضيف فحصًا مطابقًا لإنشاء هذه التعليمات قبل وبعد الاستدعاء المتكرر و... لم يتم تنفيذ أي منها، ولكن بعد مفتاح التأكيد، ما زالوا فاشلين. في النهاية، بعد دراسة الكود الذي تم إنشاؤه، أدركت أنه بعد التبديل، تتم إعادة تحميل المؤشر إلى التعليمات الحالية من المكدس ومن المحتمل أن تتم الكتابة فوقه بواسطة كود JavaScript الذي تم إنشاؤه. وهكذا اتضح. إن زيادة المخزن المؤقت من واحد ميغا بايت إلى عشرة لم تؤد إلى أي شيء، وأصبح من الواضح أن مولد التعليمات البرمجية كان يعمل في دوائر. كان علينا التأكد من أننا لم نتجاوز حدود السل الحالية، وإذا فعلنا ذلك، فقم بإصدار عنوان السل التالي بعلامة الطرح حتى نتمكن من مواصلة التنفيذ. بالإضافة إلى ذلك، يؤدي هذا إلى حل مشكلة "ما هي الوظائف التي تم إنشاؤها والتي يجب إبطالها إذا تم تغيير هذا الجزء من الرمز الثانوي؟" — فقط الوظيفة التي تتوافق مع كتلة الترجمة هذه هي التي يجب إبطالها. بالمناسبة، على الرغم من أنني قمت بتصحيح كل شيء في Chromium (نظرًا لأنني أستخدم Firefox ومن الأسهل بالنسبة لي استخدام متصفح منفصل للتجارب)، فقد ساعدني Firefox في تصحيح حالات عدم التوافق مع معيار asm.js، وبعد ذلك بدأ الكود في العمل بشكل أسرع الكروم.

مثال على التعليمات البرمجية التي تم إنشاؤها

Compiling 0x15b46d0:
CompiledTB[0x015b46d0] = function(stdlib, ffi, heap) {
"use asm";
var HEAP8 = new stdlib.Int8Array(heap);
var HEAP16 = new stdlib.Int16Array(heap);
var HEAP32 = new stdlib.Int32Array(heap);
var HEAPU8 = new stdlib.Uint8Array(heap);
var HEAPU16 = new stdlib.Uint16Array(heap);
var HEAPU32 = new stdlib.Uint32Array(heap);

var dynCall_iiiiiiiiiii = ffi.dynCall_iiiiiiiiiii;
var getTempRet0 = ffi.getTempRet0;
var badAlignment = ffi.badAlignment;
var _i64Add = ffi._i64Add;
var _i64Subtract = ffi._i64Subtract;
var Math_imul = ffi.Math_imul;
var _mul_unsigned_long_long = ffi._mul_unsigned_long_long;
var execute_if_compiled = ffi.execute_if_compiled;
var getThrew = ffi.getThrew;
var abort = ffi.abort;
var qemu_ld_ub = ffi.qemu_ld_ub;
var qemu_ld_leuw = ffi.qemu_ld_leuw;
var qemu_ld_leul = ffi.qemu_ld_leul;
var qemu_ld_beuw = ffi.qemu_ld_beuw;
var qemu_ld_beul = ffi.qemu_ld_beul;
var qemu_ld_beq = ffi.qemu_ld_beq;
var qemu_ld_leq = ffi.qemu_ld_leq;
var qemu_st_b = ffi.qemu_st_b;
var qemu_st_lew = ffi.qemu_st_lew;
var qemu_st_lel = ffi.qemu_st_lel;
var qemu_st_bew = ffi.qemu_st_bew;
var qemu_st_bel = ffi.qemu_st_bel;
var qemu_st_leq = ffi.qemu_st_leq;
var qemu_st_beq = ffi.qemu_st_beq;

function tb_fun(tb_ptr, env, sp_value, depth) {
  tb_ptr = tb_ptr|0;
  env = env|0;
  sp_value = sp_value|0;
  depth = depth|0;
  var u0 = 0, u1 = 0, u2 = 0, u3 = 0, result = 0;
  var r0 = 0, r1 = 0, r2 = 0, r3 = 0, r4 = 0, r5 = 0, r6 = 0, r7 = 0, r8 = 0, r9 = 0;
  var r10 = 0, r11 = 0, r12 = 0, r13 = 0, r14 = 0, r15 = 0, r16 = 0, r17 = 0, r18 = 0, r19 = 0;
  var r20 = 0, r21 = 0, r22 = 0, r23 = 0, r24 = 0, r25 = 0, r26 = 0, r27 = 0, r28 = 0, r29 = 0;
  var r30 = 0, r31 = 0, r41 = 0, r42 = 0, r43 = 0, r44 = 0;
    r14 = env|0;
    r15 = sp_value|0;
  START: do {
    r0 = HEAPU32[((r14 + (-4))|0) >> 2] | 0;
    r42 = 0;
    result = ((r0|0) != (r42|0))|0;
    HEAPU32[1445307] = r0;
    HEAPU32[1445321] = r14;
    if(result|0) {
    HEAPU32[1445322] = r15;
    return 0x0345bf93|0;
    }
    r0 = HEAPU32[((r14 + (16))|0) >> 2] | 0;
    r42 = 8;
    r0 = ((r0|0) - (r42|0))|0;
    HEAPU32[(r14 + (16)) >> 2] = r0;
    r1 = 8;
    HEAPU32[(r14 + (44)) >> 2] = r1;
    r1 = r0|0;
    HEAPU32[(r14 + (40)) >> 2] = r1;
    r42 = 4;
    r0 = ((r0|0) + (r42|0))|0;
    r2 = HEAPU32[((r14 + (24))|0) >> 2] | 0;
    HEAPU32[1445307] = r0;
    HEAPU32[1445308] = r1;
    HEAPU32[1445309] = r2;
    HEAPU32[1445321] = r14;
    HEAPU32[1445322] = r15;
    qemu_st_lel(env|0, r0|0, r2|0, 34, 22759218);
if(getThrew() | 0) abort();
    r0 = 3241038392;
    HEAPU32[1445307] = r0;
    r0 = qemu_ld_leul(env|0, r0|0, 34, 22759233)|0;
if(getThrew() | 0) abort();
    HEAPU32[(r14 + (24)) >> 2] = r0;
    r1 = HEAPU32[((r14 + (12))|0) >> 2] | 0;
    r2 = HEAPU32[((r14 + (40))|0) >> 2] | 0;
    HEAPU32[1445307] = r0;
    HEAPU32[1445308] = r1;
    HEAPU32[1445309] = r2;
    qemu_st_lel(env|0, r2|0, r1|0, 34, 22759265);
if(getThrew() | 0) abort();
    r0 = HEAPU32[((r14 + (24))|0) >> 2] | 0;
    HEAPU32[(r14 + (40)) >> 2] = r0;
    r1 = 24;
    HEAPU32[(r14 + (52)) >> 2] = r1;
    r42 = 0;
    result = ((r0|0) == (r42|0))|0;
    if(result|0) {
    HEAPU32[1445307] = r0;
    HEAPU32[1445308] = r1;
    }
    HEAPU32[1445307] = r0;
    HEAPU32[1445308] = r1;
    return execute_if_compiled(22759392|0, env|0, sp_value|0, depth|0) | 0;
    return execute_if_compiled(23164080|0, env|0, sp_value|0, depth|0) | 0;
    break;
  } while(1); abort(); return 0|0;
}
return {tb_fun: tb_fun};
}(window, CompilerFFI, Module.buffer)["tb_fun"]

اختتام

لذا، فإن العمل لم يكتمل بعد، لكنني تعبت من جلب هذا البناء طويل الأمد سرًا إلى الكمال. ولذلك قررت أن أنشر ما لدي الآن. الكود مخيف بعض الشيء في بعض الأماكن، لأن هذه تجربة، وليس من الواضح مسبقًا ما يجب القيام به. ربما، فمن المفيد إصدار التزامات ذرية عادية بالإضافة إلى بعض الإصدارات الأحدث من Qemu. في غضون ذلك، يوجد موضوع في Gita بتنسيق مدونة: لكل "مستوى" تم اجتيازه بطريقة ما على الأقل، تمت إضافة تعليق مفصل باللغة الروسية. في الواقع، هذه المقالة هي إلى حد كبير إعادة سرد للاستنتاج git log.

يمكنك تجربة كل شيء هنا (احذر من حركة المرور).

ما الذي يعمل بالفعل:

  • تشغيل المعالج الظاهري x86
  • يوجد نموذج أولي عملي لمولد كود JIT من كود الجهاز إلى JavaScript
  • يوجد قالب لتجميع بنيات ضيف 32 بت أخرى: يمكنك الآن الاستمتاع بنظام التشغيل Linux لتجميد بنية MIPS في المتصفح في مرحلة التحميل

ماذا يمكنك أن تفعل أيضا

  • تسريع مضاهاة. حتى في وضع JIT يبدو أنه يعمل بشكل أبطأ من Virtual x86 (ولكن من المحتمل أن يكون هناك Qemu كامل به الكثير من الأجهزة والبنيات التي تمت محاكاتها)
  • لإنشاء واجهة عادية - بصراحة، أنا لست مطور ويب جيدًا، لذا فقد قمت الآن بإعادة تصميم غلاف Emscripten القياسي بأفضل ما أستطيع
  • حاول تشغيل وظائف Qemu الأكثر تعقيدًا - الشبكات، وترحيل الأجهزة الافتراضية، وما إلى ذلك.
  • UPD: ستحتاج إلى إرسال بعض التطورات وتقارير الأخطاء إلى Emscripten في المراحل الأولية، كما فعل الحمالون السابقون في Qemu والمشاريع الأخرى. شكرًا لهم على قدرتهم على استخدام مساهمتهم ضمنيًا في Emscripten كجزء من مهمتي.

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

إضافة تعليق