Qemu.js با پشتیبانی JIT: همچنان می‌توانید چرخ‌کردن را به عقب برگردانید

چند سال پیش فابریس بلارد نوشته شده توسط jslinux یک شبیه ساز رایانه شخصی است که با جاوا اسکریپت نوشته شده است. پس از آن حداقل بیشتر بود x86 مجازی. اما همه آنها، تا آنجا که من می دانم، مفسر بودند، در حالی که Qemu، که خیلی زودتر توسط همان Fabrice Bellard نوشته شده بود، و احتمالاً، هر شبیه ساز مدرنی که به خود احترام می گذارد، از کامپایل JIT کد مهمان در کد سیستم میزبان استفاده می کند. به نظرم رسید که زمان اجرای وظیفه مخالف در رابطه با کاری که مرورگرها حل می کنند فرا رسیده است: کامپایل JIT کد ماشین در جاوا اسکریپت، که برای آن پورت کردن Qemu منطقی ترین به نظر می رسید. به نظر می رسد، چرا Qemu، شبیه سازهای ساده تر و کاربر پسند - همان VirtualBox، به عنوان مثال - نصب شده و کار می کند. اما قمو چند ویژگی جالب دارد

  • متن باز
  • توانایی کار بدون درایور هسته
  • توانایی کار در حالت مترجم
  • پشتیبانی از تعداد زیادی معماری میزبان و مهمان

در مورد سومین نکته، اکنون می توانم توضیح دهم که در واقع، در حالت TCI، خود دستورالعمل های ماشین مهمان تفسیر نمی شوند، بلکه بایت کد به دست آمده از آنها تفسیر می شوند، اما این اصل را تغییر نمی دهد - برای ساخت و اجرا. Qemu در یک معماری جدید، اگر خوش شانس باشید، یک کامپایلر C کافی است - نوشتن یک تولید کننده کد را می توان به تعویق انداخت.

و اکنون، پس از دو سال سرهم کردن آرام با کد منبع Qemu در اوقات فراغت، یک نمونه اولیه کار ظاهر شد که در آن می توانید به عنوان مثال، سیستم عامل Kolibri را اجرا کنید.

Emscripten چیست

امروزه کامپایلرهای زیادی ظاهر شده اند که نتیجه نهایی آنها جاوا اسکریپت است. برخی مانند تایپ اسکریپت در ابتدا قرار بود بهترین راه برای نوشتن برای وب باشند. در عین حال، Emscripten راهی برای گرفتن کدهای C یا C++ موجود و کامپایل آن در فرمی قابل خواندن توسط مرورگر است. بر این صفحه ما پورت های بسیاری از برنامه های شناخته شده را جمع آوری کرده ایم: اینجابه عنوان مثال، می توانید به PyPy نگاه کنید - به هر حال، آنها ادعا می کنند که قبلا JIT دارند. در واقع، هر برنامه ای را نمی توان به سادگی کامپایل کرد و در یک مرورگر اجرا کرد - تعدادی وجود دارد امکانات، که باید با آن کنار بیایید، با این حال، همانطور که کتیبه در همان صفحه می گوید: «Emscripten را می توان برای کامپایل کردن تقریباً هر چیزی استفاده کرد. قابل حمل کد C/C++ به جاوا اسکریپت". یعنی تعدادی عملیات وجود دارد که رفتار آنها طبق استاندارد تعریف نشده است، اما معمولاً روی x86 کار می کنند - به عنوان مثال، دسترسی بدون تراز به متغیرها، که به طور کلی در برخی از معماری ها ممنوع است. به طور کلی. ، Qemu یک برنامه چند پلتفرمی است و می خواستم باور کنم، و قبلاً حاوی رفتارهای تعریف نشده زیادی نیست - آن را بگیرید و کامپایل کنید، سپس کمی با JIT سرهم کنید - و کار تمام است! اما اینطور نیست مورد...

اولین تلاش

به طور کلی، من اولین کسی نیستم که به فکر انتقال Qemu به جاوا اسکریپت است. سوالی در انجمن ReactOS پرسیده شد که آیا این امکان با استفاده از Emscripten وجود دارد یا خیر. حتی قبلاً شایعاتی وجود داشت مبنی بر اینکه Fabrice Bellard شخصاً این کار را انجام داده است ، اما ما در مورد jslinux صحبت می کردیم ، که تا آنجا که من می دانم فقط تلاشی برای دستیابی به عملکرد دستی کافی در JS است و از ابتدا نوشته شده است. بعداً ، Virtual x86 نوشته شد - منابع بدون ابهام برای آن ارسال شد و همانطور که گفته شد "واقع گرایی" بیشتر شبیه سازی امکان استفاده از SeaBIOS را به عنوان سیستم عامل فراهم کرد. علاوه بر این، حداقل یک تلاش برای پورت قمو با استفاده از Emscripten وجود داشت - من سعی کردم این کار را انجام دهم جفت سوکت، اما توسعه، تا آنجا که من متوجه شدم، متوقف شد.

بنابراین، به نظر می رسد، اینجا منابع است، اینجا Emscripten است - آن را بگیرید و کامپایل کنید. اما همچنین کتابخانه هایی وجود دارند که قمو به آنها وابسته است و کتابخانه هایی که آن کتابخانه ها به آنها وابسته هستند و غیره که یکی از آنها libffi، که glib به آن بستگی دارد. در اینترنت شایعاتی وجود داشت مبنی بر اینکه یکی از پورت‌های کتابخانه Emscripten وجود دارد، اما باور کردن آن به نوعی سخت بود: اولاً، قرار نبود یک کامپایلر جدید باشد، ثانیاً، بسیار سطح پایین بود. کتابخانه را انتخاب کنید و به JS کامپایل کنید. و این فقط موضوع درج های اسمبلی نیست - احتمالاً، اگر آن را بچرخانید، برای برخی از قراردادهای فراخوانی می توانید آرگومان های لازم را در پشته ایجاد کنید و تابع را بدون آنها فراخوانی کنید. اما Emscripten چیز پیچیده ای است: برای اینکه کد تولید شده برای بهینه ساز موتور JS مرورگر آشنا به نظر برسد، از برخی ترفندها استفاده می شود. به طور خاص، به اصطلاح Relooping - یک مولد کد با استفاده از LLVM IR دریافتی با برخی دستورالعمل‌های انتقال انتزاعی، سعی می‌کند اگرها، حلقه‌ها و غیره قابل قبولی را بازسازی کند. خوب، آرگومان ها چگونه به تابع منتقل می شوند؟ به طور طبیعی، به عنوان آرگومان های توابع JS، در صورت امکان، نه از طریق پشته.

در ابتدا این ایده وجود داشت که به سادگی یک جایگزین برای libffi با JS بنویسم و ​​تست های استاندارد را اجرا کنم، اما در پایان در مورد نحوه ساخت فایل های هدر خود گیج شدم تا با کد موجود کار کنند - چه کاری می توانم انجام دهم؟ همانطور که آنها می گویند، "آیا وظایف آنقدر پیچیده است "آیا ما خیلی احمق هستیم؟" من مجبور شدم libffi را به یک معماری دیگر منتقل کنم - خوشبختانه، Emscripten هم ماکرو برای مونتاژ درون خطی دارد (در جاوا اسکریپت، بله - خوب، معماری هر چه باشد، بنابراین اسمبلر) و توانایی اجرای کدهای تولید شده در پرواز را دارد. به طور کلی، پس از مدتی سرهم کردن با قطعات libffi وابسته به پلتفرم، تعدادی کد قابل کامپایل دریافت کردم و در اولین آزمایشی که با آن برخورد کردم، آن را اجرا کردم. در کمال تعجب، آزمایش موفقیت آمیز بود. حیرت زده از نبوغم - شوخی نیست، از همان اولین پرتاب کار کرد - من که هنوز چشمانم را باور نمی کنم، دوباره به کد حاصل نگاه کردم تا ارزیابی کنم که کجا باید حفاری کنم. در اینجا برای دومین بار دیوانه شدم - تنها کاری که عملکرد من انجام داد این بود ffi_call - این یک تماس موفق را گزارش کرد. خود تماسی وجود نداشت. بنابراین من اولین درخواست کشش خود را ارسال کردم، که یک خطا در آزمون را که برای هر دانش آموز المپیادی واضح است تصحیح کرد - اعداد واقعی نباید با یکدیگر مقایسه شوند. a == b و حتی چگونه a - b < EPS - شما همچنین باید ماژول را به خاطر بسپارید، در غیر این صورت 0 بسیار برابر با 1/3 می شود ... در کل من به یک پورت خاص از libffi رسیدم که ساده ترین تست ها را می گذراند و با آن glib است. گردآوری شده - تصمیم گرفتم که لازم باشد، بعداً آن را اضافه خواهم کرد. با نگاهی به آینده، می گویم که همانطور که مشخص شد، کامپایلر حتی تابع libffi را در کد نهایی لحاظ نکرده است.

اما، همانطور که قبلاً گفتم، محدودیت‌هایی وجود دارد، و در میان استفاده رایگان از رفتارهای تعریف‌نشده مختلف، ویژگی ناخوشایندتری پنهان شده است - جاوا اسکریپت با طراحی از چند رشته‌ای با حافظه مشترک پشتیبانی نمی‌کند. در اصل، این را معمولاً می توان حتی یک ایده خوب نامید، اما نه برای انتقال کدهایی که معماری آن به رشته های C گره خورده است. به طور کلی، فایرفاکس در حال آزمایش با پشتیبانی از کارگران اشتراکی است، و Emscripten یک پیاده‌سازی thread برای آنها دارد، اما من نمی‌خواستم به آن وابسته باشم. من مجبور شدم به آرامی multithreading را از کد 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 کامپایل می‌کند زیرا آنها یک Backend معمولی TCG دارند که کد بومی تولید می‌کند، اما اینکه آیا TCI روی آن‌ها کار خواهد کرد یا خیر، سوال دیگری است. با این حال، همانطور که معلوم شد، اسناد TCI به وضوح چیزی مشابه را نشان می دهد. در نتیجه، فراخوانی های تابع برای خواندن بدون تراز به کد اضافه شد که در قسمت دیگری از Qemu یافت شد.

تخریب توده

در نتیجه، دسترسی بدون تراز به TCI اصلاح شد، یک حلقه اصلی ایجاد شد که به نوبه خود پردازنده، RCU و برخی چیزهای کوچک دیگر را نامید. و بنابراین من Qemu را با گزینه راه اندازی می کنم -d exec,in_asm,out_asm، به این معنی که باید بگویید کدام بلوک های کد اجرا می شوند و همچنین در زمان پخش بنویسید که کد مهمان چیست و کد میزبان تبدیل شده است (در این مورد بایت کد). شروع می شود، چندین بلوک ترجمه را اجرا می کند، پیام اشکال زدایی را که گذاشتم می نویسد که RCU اکنون شروع می شود و ... خراب می شود. abort() داخل یک تابع free(). با سرهم بندی کردن عملکرد free() ما موفق شدیم بفهمیم که در هدر بلوک heap که در هشت بایت قبل از حافظه اختصاص داده شده قرار دارد، به جای اندازه بلوک یا چیزی مشابه، زباله وجود دارد.

تخریب هیپ - چقدر نازه... در چنین حالتی، یک درمان مفید وجود دارد - از (در صورت امکان) از همان منابع، یک باینری بومی جمع کنید و آن را تحت Valgrind اجرا کنید. بعد از مدتی باینری آماده شد. من آن را با همان گزینه‌ها راه‌اندازی می‌کنم - در حین تنظیم اولیه، قبل از اینکه واقعاً به اجرا برسد، از کار می‌افتد. البته ناخوشایند است - ظاهراً منابع دقیقاً یکسان نبودند ، که تعجب آور نیست ، زیرا پیکربندی گزینه های کمی متفاوت را بررسی می کند ، اما من Valgrind دارم - ابتدا این اشکال را برطرف خواهم کرد و سپس اگر خوش شانس باشم ، نسخه اصلی ظاهر می شود. من همان چیزی را تحت Valgrind راه‌اندازی می‌کنم... Y-y-y، y-y-y، اوه-اوه، شروع شد، به طور معمول مراحل اولیه را طی کرد و بدون هیچ هشداری در مورد دسترسی نادرست به حافظه، نه در مورد سقوط، از باگ اصلی عبور کرد. همانطور که می گویند زندگی من را برای این آماده نکرد - یک برنامه خراب هنگام راه اندازی تحت Walgrind متوقف می شود. آنچه بود یک راز است. فرضیه من این است که یک بار در مجاورت دستورالعمل فعلی پس از یک خرابی در هنگام اولیه سازی، gdb کار را نشان داد. memset-a با یک اشاره گر معتبر با استفاده از هر دو mmx، یا xmm ثبت می کند، پس شاید نوعی خطای هم ترازی بوده است، اگرچه هنوز باورش سخت است.

خوب، به نظر نمی رسد والگریند در اینجا کمکی کند. و در اینجا نفرت انگیزترین چیز شروع شد - به نظر می رسد همه چیز حتی شروع می شود ، اما به دلایل کاملاً ناشناخته به دلیل رویدادی که می توانست میلیون ها دستورالعمل پیش رخ داده باشد خراب می شود. برای مدت طولانی، حتی نحوه نزدیک شدن هم مشخص نبود. در نهایت، من هنوز باید بنشینم و اشکال زدایی کنم. چاپ چیزی که هدر با آن بازنویسی شده بود نشان داد که شبیه یک عدد نیست، بلکه نوعی داده باینری است. و ببینید، این رشته باینری در فایل BIOS پیدا شد - یعنی اکنون می شد با اطمینان معقول گفت که این یک سرریز بافر است و حتی واضح است که در این بافر نوشته شده است. خب، پس چیزی شبیه به این - در Emscripten، خوشبختانه، هیچ تصادفی سازی فضای آدرس وجود ندارد، هیچ حفره ای نیز در آن وجود ندارد، بنابراین می توانید جایی در وسط کد بنویسید تا داده ها را با اشاره گر از آخرین راه اندازی خارج کنید. به داده ها نگاه کنید، به اشاره گر نگاه کنید، و اگر تغییر نکرده است، منبعی برای فکر کردن به دست آورید. درست است، پس از هر تغییری چند دقیقه طول می کشد تا پیوند داده شود، اما چه کاری می توانید انجام دهید؟ در نتیجه، خط خاصی پیدا شد که بایوس را از بافر موقت به حافظه مهمان کپی می کرد - و در واقع، فضای کافی در بافر وجود نداشت. یافتن منبع آن آدرس بافر عجیب منجر به یک تابع شد 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) - توابع باید با تعداد آرگومان صحیح فراخوانی شوند. اگر این قانون نقض شود، بسته به تنظیمات اشکال زدایی، برنامه یا از کار می افتد (که خوب است) یا اصلاً عملکرد اشتباه را فراخوانی می کند (که برای اشکال زدایی ناراحت کننده خواهد بود). گزینه سومی نیز وجود دارد - فعال کردن تولید بسته‌بندی‌هایی که آرگومان‌ها را اضافه/حذف می‌کنند، اما در مجموع این لفاف‌ها فضای زیادی را اشغال می‌کنند، علیرغم این واقعیت که در واقع من فقط به کمی بیش از صد لفاف نیاز دارم. این به تنهایی بسیار ناراحت کننده است، اما مشخص شد که یک مشکل جدی تر وجود دارد: در کد تولید شده توابع wrapper، آرگومان ها تبدیل و تبدیل می شوند، اما گاهی اوقات تابع با آرگومان های تولید شده فراخوانی نمی شود - خوب، درست مانند در اجرای 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، و یک اسکریپت نوشته شد که دقیقاً همان wrapper ها را دقیقاً برای عملکردهایی که برای آنها لازم است تولید می کند.

و بنابراین، پس از آن به نظر می رسید که پردازنده کار می کند. به نظر می رسد به این دلیل است که صفحه هرگز مقداردهی اولیه نشده است، اگرچه memtest86+ می توانست در اسمبلی اصلی اجرا شود. در اینجا لازم به توضیح است که کد ورودی/خروجی بلوک Qemu به صورت کوروتین نوشته شده است. Emscripten پیاده سازی بسیار پیچیده خود را دارد، اما همچنان باید در کد Qemu پشتیبانی شود، و اکنون می توانید پردازنده را اشکال زدایی کنید: Qemu از گزینه ها پشتیبانی می کند. -kernel, -initrd, -append، که با آن می توانید لینوکس یا مثلا memtest86+ را بدون استفاده از دستگاه های بلوک بوت کنید. اما مشکل اینجاست: در اسمبلی بومی می توان خروجی هسته لینوکس را به کنسول با این گزینه مشاهده کرد -nographic، و خروجی از مرورگر به ترمینال از جایی که راه اندازی شده است وجود ندارد emrun، نیامد یعنی مشخص نیست: پردازنده کار نمی کند یا خروجی گرافیک کار نمی کند. و بعد به ذهنم رسید که کمی صبر کنم. معلوم شد که "پردازنده خواب نیست، بلکه به آرامی چشمک می زند" و بعد از حدود پنج دقیقه هسته یک دسته از پیام ها را روی کنسول پرتاب کرد و به آویزان کردن ادامه داد. مشخص شد که پردازنده به طور کلی کار می کند و ما باید کد کار با SDL2 را بررسی کنیم. متأسفانه، من نمی دانم چگونه از این کتابخانه استفاده کنم، بنابراین در برخی جاها مجبور شدم به طور تصادفی عمل کنم. در نقطه‌ای، خط parallel0 روی یک پس‌زمینه آبی روی صفحه چشمک زد که ایده‌هایی را مطرح کرد. در پایان، مشخص شد که مشکل این است که Qemu چندین پنجره مجازی را در یک پنجره فیزیکی باز می کند، که می توانید بین آنها با استفاده از Ctrl-Alt-n سوئیچ کنید: در ساخت بومی کار می کند، اما در Emscripten نه. پس از خلاص شدن از شر پنجره های غیر ضروری با استفاده از گزینه ها -monitor none -parallel none -serial none و دستورالعمل‌هایی برای ترسیم مجدد اجباری کل صفحه در هر فریم، همه چیز ناگهان کار کرد.

کوروتین ها

بنابراین، شبیه‌سازی در مرورگر کار می‌کند، اما نمی‌توانید هیچ فلاپی جالبی را در آن اجرا کنید، زیرا هیچ بلوکی ورودی/خروجی وجود ندارد - شما باید از برنامه‌های مشترک پشتیبانی کنید. Qemu در حال حاضر چندین پشتوانه معمولی دارد، اما به دلیل ماهیت جاوا اسکریپت و تولیدکننده کد Emscripten، نمی‌توانید به سادگی شروع به شعبده‌بازی پشته‌ها کنید. به نظر می رسد "همه چیز از بین رفته است، گچ در حال برداشتن است"، اما توسعه دهندگان Emscripten قبلاً از همه چیز مراقبت کرده اند. این بسیار خنده دار است: بیایید فراخوانی تابعی را مشکوک صدا کنیم emscripten_sleep و چندین مورد دیگر که از مکانیسم Asyncify استفاده می کنند، و همچنین اشاره گر و فراخوانی به هر تابعی که در آن یکی از دو مورد قبلی ممکن است در پایین پشته رخ دهد. و حالا قبل از هر تماس مشکوک یک متن غیر همگام را انتخاب می کنیم و بلافاصله بعد از تماس بررسی می کنیم که آیا تماس ناهمزمان رخ داده است یا خیر و در صورت وجود، همه متغیرهای محلی را در این زمینه غیر همگام ذخیره می کنیم، مشخص می کنیم کدام تابع برای انتقال کنترل به زمانی که نیاز به ادامه اجرا داریم و از تابع فعلی خارج می شویم. اینجاست که زمینه برای مطالعه اثر وجود دارد اسراف کردن - برای نیازهای ادامه اجرای کد پس از بازگشت از یک تماس ناهمزمان، کامپایلر "خرد" از تابع را ایجاد می کند که پس از یک تماس مشکوک شروع می شود - مانند این: اگر n تماس مشکوک وجود داشته باشد، تابع در جایی n/2 گسترش می یابد. بار - این هنوز است، اگر نه. به خاطر داشته باشید که پس از هر تماس بالقوه ناهمزمان، باید چند متغیر محلی را به تابع اصلی ذخیره کنید. پس از آن، حتی مجبور شدم یک اسکریپت ساده در پایتون بنویسم، که بر اساس مجموعه‌ای از توابع به‌خصوص بیش از حد استفاده شده که ظاهراً «اجازه نمی‌دهند ناهمزمانی از خود عبور کند» (یعنی ارتقاء پشته‌ای و هر چیزی که من توضیح دادم این کار را نمی‌کند. کار در آنها)، فراخوانی هایی را از طریق اشاره گرها نشان می دهد که در آن توابع باید توسط کامپایلر نادیده گرفته شوند تا این توابع ناهمزمان در نظر گرفته نشوند. و سپس فایل‌های JS زیر 60 مگابایت به وضوح بیش از حد هستند - حداقل 30 مگابایت را در نظر بگیریم. اگرچه، یک بار در حال تنظیم یک اسکریپت اسمبلی بودم و به طور تصادفی گزینه‌های پیوند دهنده را بیرون انداختم، از جمله -O3. من کد تولید شده را اجرا می کنم و Chromium حافظه را می خورد و خراب می شود. سپس به طور تصادفی به چیزی که او می‌خواست دانلود کند نگاه کردم... خوب، چه می‌توانم بگویم، اگر از من خواسته می‌شد که یک جاوا اسکریپت 500+ مگابایتی را با دقت مطالعه و بهینه‌سازی کنم، من هم یخ می‌زدم.

متأسفانه، بررسی‌های موجود در کد کتابخانه پشتیبانی Asyncify کاملاً دوستانه نبود longjmp-هایی که در کد پردازنده مجازی استفاده می‌شوند، اما پس از یک وصله کوچک که این بررسی‌ها را غیرفعال می‌کند و به زور زمینه‌ها را بازیابی می‌کند که انگار همه چیز خوب است، کد کار کرد. و سپس یک چیز عجیب شروع شد: گاهی اوقات بررسی هایی در کد همگام سازی انجام می شود - همان مواردی که اگر طبق منطق اجرا باید مسدود شود کد را خراب می کنند - شخصی سعی کرد یک mutex قبلاً ضبط شده را بگیرد. خوشبختانه، معلوم شد که این یک مشکل منطقی در کد سریال نیست - من به سادگی از عملکرد استاندارد حلقه اصلی ارائه شده توسط Emscripten استفاده می کردم، اما گاهی اوقات فراخوانی ناهمزمان به طور کامل پشته را باز می کرد و در آن لحظه شکست می خورد. setTimeout از حلقه اصلی - بنابراین، کد بدون خروج از تکرار قبلی وارد تکرار حلقه اصلی شد. بازنویسی در یک حلقه بی نهایت و emscripten_sleepو مشکلات mutexes متوقف شد. کد حتی منطقی تر شده است - در واقع، من کدی ندارم که فریم انیمیشن بعدی را آماده کند - پردازنده فقط چیزی را محاسبه می کند و صفحه به طور دوره ای به روز می شود. با این حال، مشکلات به همین جا ختم نشد: گاهی اوقات اجرای Qemu به سادگی بدون هیچ استثنا یا خطایی خاتمه می یافت. در آن لحظه من آن را رها کردم، اما، با نگاه کردن به آینده، می گویم که مشکل این بود: در واقع از کد کوروتین استفاده نمی شود. setTimeout (یا حداقل نه آنقدر که فکر می کنید): عملکرد emscripten_yield به سادگی پرچم تماس ناهمزمان را تنظیم می کند. تمام نکته این است emscripten_coroutine_next یک تابع ناهمزمان نیست: به صورت داخلی پرچم را بررسی می کند، آن را بازنشانی می کند و کنترل را به جایی که لازم است منتقل می کند. یعنی تبلیغ پشته به همین جا ختم می شود. مشکل این بود که به دلیل استفاده پس از آزاد شدن، که زمانی ظاهر شد که استخر کوروتین غیرفعال شد به دلیل این واقعیت که من یک خط کد مهم را از باطن کوروتین موجود کپی نکردم، تابع qemu_in_coroutine درست است در حالی که در واقع باید false برگردانده می شد. این منجر به تماس شد emscripten_yield، که بالای آن هیچ کس در پشته نبود emscripten_coroutine_next، پشته تا بالا باز شد، اما نه setTimeoutهمانطور که قبلاً گفتم، به نمایش گذاشته نشد.

تولید کد جاوا اسکریپت

و این در واقع وعده "برگرداندن گوشت چرخ کرده" است. نه واقعا. البته اگر Qemu را در مرورگر اجرا کنیم و Node.js را در آن اجرا کنیم، طبیعتاً پس از تولید کد در Qemu، جاوا اسکریپت کاملاً اشتباهی دریافت خواهیم کرد. اما هنوز، نوعی تغییر شکل معکوس.

ابتدا کمی در مورد نحوه کار کیمو. لطفاً فوراً مرا ببخشید: من یک توسعه دهنده حرفه ای Qemu نیستم و ممکن است در بعضی جاها نتیجه گیری من اشتباه باشد. همانطور که می گویند، "نظر دانش آموز نباید با نظر معلم، بدیهیات Peano و عقل سلیم مطابقت داشته باشد." Qemu دارای تعداد معینی معماری مهمان پشتیبانی شده است و برای هر کدام یک دایرکتوری مانند وجود دارد target-i386. هنگام ساخت، می توانید پشتیبانی از چندین معماری مهمان را مشخص کنید، اما نتیجه فقط چندین باینری خواهد بود. کدی که از معماری مهمان پشتیبانی می کند، به نوبه خود، برخی از عملیات داخلی Qemu را ایجاد می کند که TCG (Tiny Code Generator) قبلاً آنها را به کد ماشین برای معماری میزبان تبدیل می کند. همانطور که در فایل readme واقع در دایرکتوری tcg گفته شد، این در ابتدا بخشی از یک کامپایلر معمولی C بود که بعداً برای JIT اقتباس شد. بنابراین، به عنوان مثال، معماری هدف از نظر این سند دیگر یک معماری مهمان نیست، بلکه یک معماری میزبان است. در نقطه ای، مؤلفه دیگری ظاهر شد - Tiny Code Interpreter (TCI) که باید کد (تقریباً همان عملیات داخلی) را در غیاب تولیدکننده کد برای یک معماری میزبان خاص اجرا کند. در واقع، همانطور که مستندات آن بیان می کند، این مفسر ممکن است همیشه به خوبی یک تولید کننده کد JIT عمل نکند، نه تنها از نظر کمی، بلکه از نظر کیفی. اگرچه مطمئن نیستم که توضیحات او کاملا مرتبط باشد.

در ابتدا سعی کردم یک Backend کامل TCG ایجاد کنم، اما به سرعت در کد منبع و توضیحات کاملاً واضح دستورالعمل های بایت گیج شدم، بنابراین تصمیم گرفتم مفسر TCI را بپیچم. این چندین مزیت را به همراه داشت:

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

در مورد نکته سوم، مطمئن نیستم که پس از اجرای کد برای اولین بار، پچ کردن امکان پذیر باشد، اما دو نکته اول کافی است.

در ابتدا، کد به شکل یک سوئیچ بزرگ در آدرس دستورالعمل بایت کد اصلی تولید می شد، اما پس از یادآوری مقاله Emscripten، بهینه سازی JS تولید شده و حلقه مجدد، تصمیم گرفتم کدهای انسانی بیشتری تولید کنم، به خصوص که به صورت تجربی معلوم شد که تنها نقطه ورود به بلوک ترجمه، شروع آن است. زودتر از این کار، پس از مدتی یک کد مولد داشتیم که کدهایی را با if (البته بدون حلقه) تولید می کرد. اما بدشانسی، خراب شد و این پیام را داد که طول دستورالعمل ها نادرست است. علاوه بر این، آخرین دستورالعمل در این سطح بازگشتی بود brcond. بسیار خوب، من یک بررسی یکسان به تولید این دستورالعمل قبل و بعد از فراخوانی بازگشتی اضافه می کنم و... هیچ کدام از آنها اجرا نشد، اما پس از سوئیچ assert آنها همچنان با شکست مواجه شدند. در پایان، پس از مطالعه کد تولید شده، متوجه شدم که پس از سوئیچ، نشانگر دستورالعمل فعلی از پشته بارگذاری مجدد می شود و احتمالاً توسط کد جاوا اسکریپت تولید شده بازنویسی می شود. و اینطور معلوم شد. افزایش بافر از یک مگابایت به ده نتیجه ای نداشت و مشخص شد که مولد کد به صورت دایره ای کار می کند. باید بررسی می کردیم که از مرز سل فعلی فراتر نرفته ایم و اگر رفتیم آدرس سل بعدی را با علامت منفی صادر کنیم تا بتوانیم اجرا را ادامه دهیم. علاوه بر این، این مشکل را حل می کند "اگر این قطعه از بایت کد تغییر کرده باشد، کدام توابع تولید شده باید باطل شوند؟" - فقط تابعی که با این بلوک ترجمه مطابقت دارد باید باطل شود. به هر حال، اگرچه من همه چیز را در Chromium اشکال زدایی کردم (از آنجایی که من از فایرفاکس استفاده می کنم و استفاده از یک مرورگر جداگانه برای آزمایش ها برای من آسان تر است)، فایرفاکس به من کمک کرد ناسازگاری ها را با استاندارد 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 را دارد. در همین حال، یک موضوع در گیتا در قالب وبلاگ وجود دارد: برای هر "سطح" که حداقل به نحوی گذرانده شده است، یک تفسیر مفصل به زبان روسی اضافه شده است. در واقع، این مقاله تا حد زیادی بازگویی نتیجه گیری است git log.

می توانید همه آن را امتحان کنید اینجا (مراقب ترافیک باشید).

آنچه در حال حاضر کار می کند:

  • پردازنده مجازی x86 در حال اجرا است
  • یک نمونه اولیه از یک تولید کننده کد JIT از کد ماشین تا جاوا اسکریپت وجود دارد
  • الگویی برای مونتاژ سایر معماری‌های مهمان 32 بیتی وجود دارد: در حال حاضر می‌توانید لینوکس را به دلیل انجماد معماری MIPS در مرورگر در مرحله بارگیری تحسین کنید.

شما چه کار دیگه ای میتوانید انجام دهید

  • افزایش سرعت شبیه سازی حتی در حالت JIT به نظر می رسد کندتر از Virtual x86 اجرا شود (اما به طور بالقوه یک Qemu کامل با سخت افزارها و معماری های شبیه سازی شده زیادی وجود دارد)
  • برای ایجاد یک رابط معمولی - صادقانه بگویم، من توسعه دهنده وب خوبی نیستم، بنابراین در حال حاضر پوسته استاندارد Emscripten را به بهترین شکل ممکن بازسازی کرده ام.
  • سعی کنید توابع پیچیده تر Qemu را راه اندازی کنید - شبکه، مهاجرت VM و غیره.
  • UPD: شما باید پیشرفت‌ها و گزارش‌های اشکالات خود را به Emscripten upstream ارسال کنید، همانطور که باربرهای قبلی Qemu و پروژه‌های دیگر انجام دادند. از آنها تشکر می کنم که توانستند به طور ضمنی از مشارکت خود در Emscripten به عنوان بخشی از وظیفه من استفاده کنند.

منبع: www.habr.com

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