Qemu.js со JIT поддршка: полнењето сè уште може да се врати назад

Пред неколку години Фабрис Белард напишано од jslinux е емулатор за компјутер напишан во JavaScript. После тоа имаше барем повеќе Виртуелен x86. Но, сите тие, колку што знам, беа толкувачи, додека Qemu, напишана многу порано од истиот Фабрис Белард, и, веројатно, секој модерен емулатор што самопочитува, користи JIT компилација на гостин код во код на системот домаќин. Ми се чинеше дека е време да се имплементира спротивната задача во однос на онаа што ја решаваат прелистувачите: JIT компилација на машински код во JavaScript, за што се чинеше најлогично да се пренесе Qemu. Се чини, зошто Qemu, постојат поедноставни и погодни емулатори - истиот VirtualBox, на пример - инсталиран и работи. Но, Qemu има неколку интересни карактеристики

  • отворен извор
  • способност за работа без двигател на јадрото
  • способност за работа во режим на преведувач
  • поддршка за голем број архитектури и домаќини и гости

Во однос на третата точка, сега можам да објаснам дека всушност, во режимот TCI, не се толкуваат самите инструкции на гостинската машина, туку бајтекодот добиен од нив, но тоа не ја менува суштината - со цел да се изгради и стартува Qemu на нова архитектура, ако имате среќа, доволен е компајлер C - пишувањето генератор на код може да се одложи.

И сега, по две години лежерно мешање со изворниот код на Qemu во слободното време, се појави работен прототип, во кој веќе можете да го стартувате, на пример, Колибри ОС.

Што е Емскриптен

Во денешно време се појавија многу компајлери, чиј краен резултат е JavaScript. Некои, како 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 - се обидов да го сторам ова сокет пар, но развојот, колку што разбрав, беше замрзнат.

Значи, се чини, еве ги изворите, тука е Емскриптен - земете го и составете. Но има и библиотеки од кои зависи Ќему и библиотеки од кои зависат тие библиотеки итн., а една од нив е libffi, од што зависи глибот. На интернет имаше гласини дека има една во големата колекција на пристаништа на библиотеки за Емскриптен, но некако беше тешко да се поверува: прво, немаше намера да биде нов компајлер, второ, беше премногу ниско ниво. библиотека само да се подигне и компајлира во JS. И не се работи само за монтажни влошки - веројатно, ако го извртите, за некои конвенции за повикување можете да ги генерирате потребните аргументи на оџакот и да ја повикате функцијата без нив. Но, Emscripten е незгодна работа: со цел генерираниот код да изгледа познат на оптимизатор на моторот на прелистувачот JS, се користат некои трикови. Конкретно, таканареченото превртување - генератор на код кој го користи примениот LLVM IR со некои апстрактни инструкции за транзиција се обидува повторно да создаде веродостојни ако, циклуси итн. Па, како се пренесуваат аргументите на функцијата? Секако, како аргументи за JS функциите, што е, ако е можно, не преку стекот.

На почетокот имаше идеја едноставно да напишам замена за libffi со JS и да извршам стандардни тестови, но на крајот се збунив како да ги направам моите датотеки за заглавија така што тие ќе работат со постоечкиот код - што можам да направам? како што велат, „Дали задачите се толку сложени „Дали сме толку глупави? Морав да го префрлам libffi на друга архитектура, така да се каже - за среќа, Emscripten ги има и двете макроа за вградено склопување (во Javascript, да - добро, без оглед на архитектурата, па асемблерот) и можност за извршување на кодот генериран во лет. Општо земено, откако извесно време се занимавав со фрагменти од libffi зависни од платформата, добив некој компајлибилен код и го извршив на првиот тест на кој наидов. На мое изненадување, тестот беше успешен. Запрепастен од мојата генијалност - без шега, тоа функционираше од првото лансирање - јас, сè уште не верувајќи им на своите очи, отидов повторно да го погледнам добиениот код, да проценам каде понатаму да копам. Еве по втор пат се налутив - единственото нешто што го направи мојата функција беше ffi_call - ова пријави успешен повик. Самиот повик немаше. Затоа, го испратив моето прво барање за повлекување, кое ја исправи грешката на тестот што е јасна за секој ученик на Олимпијадата - реалните бројки не треба да се споредуваат како a == b па дури и како a - b < EPS - исто така треба да го запомните модулот, инаку 0 ќе испадне многу еднаква на 1/3... Во принцип, дојдов до одредена порта на libffi, која ги поминува наједноставните тестови, и со која glib е составено - Решив дека ќе биде потребно, ќе го додадам подоцна. Гледајќи напред, ќе кажам дека, како што се испостави, компајлерот дури и не ја вклучи функцијата libffi во конечниот код.

Но, како што веќе реков, има некои ограничувања, а меѓу бесплатната употреба на различни недефинирани однесувања, беше скриена понепријатна карактеристика - JavaScript по дизајн не поддржува повеќенишки со заедничка меморија. Во принцип, ова обично може да се нарече дури и добра идеја, но не и за пренесување на код чија архитектура е врзана за нишките C. Општо земено, Firefox експериментира со поддршка на споделени работници, а Emscripten има имплементација на thread за нив, но не сакав да зависам од тоа. Морав полека да го искоренам мултинишкиот од кодот 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 backend кој генерира мајчин код, но дали TCI ќе работи на нив е друго прашање. Сепак, како што се испостави, документацијата на TCI јасно укажува на нешто слично. Како резултат на тоа, во кодот беа додадени повици на функции за неусогласено читање, кои беа пронајдени во друг дел од Qemu.

Уништување грамада

Како резултат на тоа, неусогласениот пристап до TCI беше коригиран, беше создадена главна јамка која за возврат го нарекува процесор, RCU и некои други мали нешта. И така го стартувам Qemu со опцијата -d exec,in_asm,out_asm, што значи дека треба да кажете кои блокови од кодот се извршуваат, а исто така во моментот на емитување да напишете кој бил гостин кодот, каков е кодот на домаќинот (во овој случај, бајтекод). Започнува, извршува неколку блокови за превод, ја пишува пораката за дебагирање што ја оставив дека RCU сега ќе започне и... паѓа abort() внатре во функција free(). Со чепкање во функцијата free() Успеавме да дознаеме дека во заглавието на блокот на грамада, што се наоѓа во осумте бајти пред доделената меморија, наместо големината на блокот или нешто слично, има ѓубре.

Уништување на купот - колку е слатко... Во таков случај, постои корисен лек - од (ако е можно) истите извори, склопете мајчин бинарен и стартувајте го под Valgrind. По некое време, бинарната беше подготвена. Го стартувам со истите опции - паѓа дури и при иницијализација, пред всушност да стигне до извршување. Непријатно е, се разбира - очигледно, изворите не беа сосема исти, што не е изненадувачки, бидејќи конфигурирањето извиди малку различни опции, но јас го имам Valgrind - прво ќе ја поправам оваа грешка, а потоа, ако имам среќа , ќе се појави оригиналниот. Го извршувам истото под Valgrind... Y-y-y, y-y-y, uh-uh, започна, помина низ иницијализацијата нормално и се пресели покрај оригиналната грешка без ниту едно предупредување за неточен пристап до меморијата, а да не зборуваме за падови. Животот, како што велат, не ме подготви за ова - програмата за паѓање престанува да се урива кога е лансирана под Walgrind. Што беше тоа е мистерија. Мојата хипотеза е дека еднаш во близина на тековната инструкција по пад при иницијализација, gdb покажа работа memset-а со валиден покажувач користејќи или mmx, или xmm регистри, тогаш можеби тоа беше некаква грешка во усогласувањето, иако сè уште е тешко да се поверува.

Во ред, се чини дека Валгринд не помага овде. И тука започна најодвратното - се чини дека сè дури и почнува, но паѓа од апсолутно непознати причини поради настан што можел да се случи пред милиони инструкции. Долго време не беше ни јасно како да се пристапи. На крајот, сепак морав да седнам и да дебагирам. Печатењето на она со што е препишано заглавјето покажа дека не личи на број, туку на некој вид бинарни податоци. И, ете, оваа бинарна низа беше пронајдена во датотеката на BIOS-от - односно, сега можеше со разумна сигурност да се каже дека е прелевање на баферот, па дури е јасно дека е напишана во овој бафер. Па, тогаш вакво нешто - во Emscripten, за среќа, нема рандомизација на адресниот простор, нема ни дупки во него, па може да напишете некаде на средината на кодот за да излезете податоци по покажувач од последното лансирање, погледнете ги податоците, погледнете го покажувачот и, ако не е променет, добијте храна за размислување. Навистина, потребни се неколку минути за да се поврзе по секоја промена, но што можете да направите? Како резултат на тоа, беше пронајдена специфична линија што го копираше BIOS-от од привремениот тампон во гостинската меморија - и, навистина, немаше доволно простор во баферот. Пронаоѓањето на изворот на таа чудна тампон адреса резултираше со функција qemu_anon_ram_alloc во датотека oslib-posix.c - логиката таму беше оваа: понекогаш може да биде корисно да се усогласи адресата на огромна страница со големина од 2 MB, за ова ќе побараме mmap прво уште малку, а потоа вишокот ќе го вратиме со помош munmap. И ако таквото усогласување не е потребно, тогаш ќе го посочиме резултатот наместо 2 MB getpagesize() - mmap сепак ќе даде подредена адреса... Така во Емскриптен 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> Како резултат на тоа, конечно имав шанса да ја пробам библиотеката на работа пипарсирање, и напишана е скрипта која ги генерира токму тие обвивки за точно функциите за кои се потребни.

И така, после тоа процесорот изгледаше како да работи. Се чини дека е затоа што екранот никогаш не бил иницијализиран, иако memtest86+ можеше да работи во мајчиното собрание. Овде е неопходно да се разјасни дека кодот за влез/излез на блокот Qemu е напишан во корутини. Emscripten има своја многу незгодна имплементација, но сепак требаше да биде поддржана во кодот Qemu, и можете да го дебагирате процесорот сега: Qemu поддржува опции -kernel, -initrd, -append, со кој можете да го подигнете Linux или, на пример, memtest86+, без воопшто да користите блок уреди. Но, тука е проблемот: во мајчиното собрание може да се види излезот на кернелот на Linux во конзолата со опцијата -nographic, и нема излез од прелистувачот до терминалот од каде што е стартуван emrun, не дојде. Тоа е, не е јасно: процесорот не работи или графичкиот излез не работи. И тогаш ми текна да почекам малку. Се испостави дека „процесорот не спие, туку едноставно трепка полека“, а по околу пет минути кернелот фрли куп пораки на конзолата и продолжи да виси. Стана јасно дека процесорот, генерално, работи, и треба да копаме во кодот за работа со SDL2. За жал, не знам како да ја користам оваа библиотека, па на некои места морав да дејствувам по случаен избор. Во одреден момент, линијата parallel0 блесна на екранот на сина позадина, што сугерираше некои размислувања. На крајот, се покажа дека проблемот е што Qemu отвора неколку виртуелни прозорци во еден физички прозорец, меѓу кои можете да се префрлате користејќи Ctrl-Alt-n: работи во мајчината верзија, но не и во Emscripten. Откако ќе се ослободите од непотребните прозорци користејќи опции -monitor none -parallel none -serial none и инструкции за насилно прецртување на целиот екран на секоја рамка, сè одеднаш функционираше.

Корутини

Значи, емулацијата во прелистувачот работи, но не можете да извршите ништо интересно со една флопи во неа, бидејќи нема блок I/O - треба да имплементирате поддршка за корутини. Qemu веќе има неколку корутински задни делови, но поради природата на JavaScript и генераторот на кодот Emscripten, не можете само да започнете со жонглирање на стекови. Се чини дека „сè нема, гипсот се отстранува“, но програмерите на Emscripten веќе се погрижија за сè. Ова е имплементирано доста смешно: ајде да го наречеме повикот на функција како овој сомнителен emscripten_sleep и неколку други кои го користат механизмот Asyncify, како и повици на покажувач и повици до која било функција каде што еден од претходните два случаи може да се појави подолу во оџакот. И сега, пред секој сомнителен повик, ќе избереме асинхрон контекст, а веднаш по повикот ќе провериме дали се случил асинхрон повик и ако има, ќе ги зачуваме сите локални променливи во овој асинхрон контекст, ќе покажеме која функција да ја префрлиме контролата кога треба да продолжиме со извршувањето и да излеземе од тековната функција. Ова е местото каде што има простор за проучување на ефектот расфрлање — за потребите на продолжување на извршувањето на кодот по враќањето од асинхрон повик, компајлерот генерира „никулци“ на функцијата почнувајќи по сомнителен повик — вака: ако има n сомнителни повици, тогаш функцијата ќе се прошири некаде n/2 пати — ова е сепак, ако не Имајте на ум дека по секој потенцијално асинхрон повик, треба да додадете зачувување на некои локални променливи во оригиналната функција. Последователно, дури морав да напишам едноставна скрипта во Python, која, врз основа на даден сет на особено прекумерно искористени функции кои наводно „не дозволуваат асинхронијата да помине низ себе“ (т.е. промоција на стек и сè што штотуку опишав не работа во нив), означува повици преку покажувачи во кои функциите треба да бидат игнорирани од компајлерот за да не се сметаат за асинхрони овие функции. И тогаш JS-датотеките под 60 MB се очигледно премногу - да речеме барем 30. Иако, еднаш поставував скрипта за склопување и случајно ги исфрлив опциите за поврзување, меѓу кои беше -O3. Го активирам генерираниот код, а Chromium ја јаде меморијата и паѓа. Потоа случајно погледнав што се обидуваше да преземе... Па, што да кажам, и јас ќе се замрзнав ако од мене беше побарано смислено да проучам и оптимизирам Javascript од 500+ MB.

За жал, проверките во кодот на библиотеката за поддршка на Asyncify не беа целосно пријателски longjmp-и кои се користат во кодот на виртуелниот процесор, но по мала закрпа што ги оневозможува овие проверки и насилно ги враќа контекстите како да е сè во ред, кодот функционира. И тогаш започна чудна работа: понекогаш се активираа проверки во кодот за синхронизација - истите оние што го уриваат кодот ако, според логиката на извршување, треба да се блокира - некој се обиде да зграпчи веќе заробен мутекс. За среќа, се покажа дека ова не е логичен проблем во серискиот код - едноставно ја користев стандардната функционалност на главната јамка обезбедена од Emscripten, но понекогаш асинхрониот повик целосно ќе го одвитка купот и во тој момент не успеваше setTimeout од главната јамка - на тој начин, кодот влезе во итерацијата на главната јамка без да ја напушти претходната итерација. Препиша на бесконечна јамка и emscripten_sleep, и проблемите со мутекси престанаа. Кодот стана уште пологичен - на крајот на краиштата, всушност, немам код што ја подготвува следната рамка за анимација - процесорот само пресметува нешто и екранот периодично се ажурира. Сепак, проблемите не застанаа тука: понекогаш извршувањето на Qemu едноставно завршуваше тивко без никакви исклучоци или грешки. Во тој момент се откажав од тоа, но, гледајќи напред, ќе кажам дека проблемот беше ова: корутинскиот код, всушност, не користи setTimeout (или барем не толку често колку што мислите): функција emscripten_yield едноставно го поставува знамето за асинхрон повик. Целата поента е во тоа emscripten_coroutine_next не е асинхрона функција: внатрешно го проверува знамето, го ресетира и ја пренесува контролата таму каде што е потребно. Односно, таму завршува промоцијата на стекот. Проблемот беше во тоа што поради користење после бесплатно, што се појави кога корутинскиот базен беше оневозможен поради фактот што не копирав важна линија код од постојниот корутински заден дел, функцијата qemu_in_coroutine се врати точно кога всушност требаше да се врати неточно. Ова доведе до повик emscripten_yield, над кој немаше никој на оџакот emscripten_coroutine_next, оџакот се расплетуваше до самиот врв, но не setTimeout, како што веќе реков, не беше изложен.

Генерирање на JavaScript код

И еве, всушност, ветеното „вртење на меленото месо назад“. Не навистина. Се разбира, ако работиме Qemu во прелистувачот и Node.js во него, тогаш, нормално, по генерирањето код во Qemu ќе добиеме целосно погрешен JavaScript. Но, сепак, некаква обратна трансформација.

Прво, малку за тоа како функционира Ќему. Ве молам, простете ми веднаш: јас не сум професионален развивач на Qemu и моите заклучоци може да бидат погрешни на некои места. Како што велат, „мислењето на ученикот не мора да се совпаѓа со мислењето на наставникот, аксиоматиката и здравиот разум на Пеано“. Qemu има одреден број на поддржани гостински архитектури и за секоја има директориум како target-i386. Кога градите, можете да наведете поддршка за неколку гостински архитектури, но резултатот ќе биде само неколку бинарни датотеки. Кодот за поддршка на гостинската архитектура, пак, генерира некои внатрешни операции на Qemu, кои TCG (Tiny Code Generator) веќе ги претвора во машински код за архитектурата на домаќинот. Како што е наведено во датотеката readme лоцирана во директориумот tcg, ова првично беше дел од обичен C компајлер, кој подоцна беше прилагоден за JIT. Затоа, на пример, целната архитектура во однос на овој документ повеќе не е архитектура на гости, туку архитектура домаќин. Во одреден момент, се појави друга компонента - Tiny Code Interpreter (TCI), кој треба да изврши код (речиси исти внатрешни операции) во отсуство на генератор на код за одредена архитектура на домаќинот. Всушност, како што е наведено во нејзината документација, овој толкувач не може секогаш да работи толку добро како генератор на JIT код, не само квантитативно во однос на брзината, туку и квалитативно. Иако не сум сигурен дека неговиот опис е целосно релевантен.

Отпрвин се обидов да направам полноправно TCG backend, но брзо се збунив во изворниот код и не сосема јасен опис на инструкциите за бајтекод, па решив да го завиткам преведувачот TCI. Ова даде неколку предности:

  • кога имплементирате генератор на код, не можете да го погледнете описот на инструкциите, туку кодот на преведувачот
  • можете да генерирате функции не за секој блок за превод што се среќава, туку, на пример, само по стотото извршување
  • ако генерираната шифра се промени (и тоа се чини дека е можно, судејќи според функциите со имиња што го содржат зборот patch), ќе треба да го поништам генерираниот 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. Во меѓувреме, има нишка во Гита во формат на блог: за секое „ниво“ што е барем некако поминато, додаден е детален коментар на руски јазик. Всушност, овој напис во голема мера е прераскажување на заклучокот git log.

Можете да го пробате сето тоа тука (пазете се од сообраќајот).

Што веќе функционира:

  • x86 виртуелен процесор работи
  • Постои работен прототип на генератор на JIT код од машински код до JavaScript
  • Постои шаблон за составување други 32-битни гостински архитектури: токму сега можете да му се восхитувате на Linux за замрзнување на архитектурата MIPS во прелистувачот во фазата на вчитување

Што друго можете да направите

  • Забрзајте ја емулацијата. Дури и во режимот JIT се чини дека работи побавно од Virtual x86 (но потенцијално постои цела Qemu со многу емулиран хардвер и архитектури)
  • Да се ​​направи нормален интерфејс - искрено, јас не сум добар веб-развивач, така што сега за сега ја преправив стандардната обвивка Emscripten најдобро што можам
  • Обидете се да стартувате посложени функции на Qemu - вмрежување, миграција на VM итн.
  • UPD: ќе треба да ги доставите вашите неколку настани и извештаи за грешки до Emscripten upstream, како што тоа го правеа претходните носачи на Qemu и други проекти. Им благодарам што можеа имплицитно да го искористат нивниот придонес за Емскриптен како дел од мојата задача.

Извор: www.habr.com

Додадете коментар