Кему.јс са ЈИТ подршком: још увек можете да окрећете млевено месо уназад

Пре неколико година Фабрис Белард написао јслинук је ПЦ емулатор написан у ЈаваСцрипт-у. После тога било је бар више Виртуал к86. Али сви су они, колико ја знам, били тумачи, док Кему, који је много раније написао исти Фабрис Белард, и, вероватно, било који модерни емулатор који поштује себе, користи ЈИТ компилацију гостујућег кода у код хост система. Чинило ми се да је дошло време да се имплементира супротан задатак у односу на онај који прегледачи решавају: ЈИТ компилација машинског кода у ЈаваСцрипт, за шта се чинило најлогичније портовати Кему. Чини се, зашто Кему, постоје једноставнији и једноставнији емулатори - исти ВиртуалБок, на пример - инсталиран и ради. Али Кему има неколико занимљивих карактеристика

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

Што се тиче треће тачке, сада могу да објасним да се заправо у ТЦИ режиму не тумаче саме инструкције машине за госте, већ бајткод добијен од њих, али то не мења суштину - да би се направила и покренула Кему на новој архитектури, ако имате среће, довољан је Ц компајлер - писање генератора кода може бити одложено.

А сада, након две године лаганог петљања са изворним кодом Кему у слободно време, појавио се радни прототип, у којем већ можете покренути, на пример, Колибри ОС.

Шта је Емсцриптен

У данашње време се појавило много компајлера чији је крајњи резултат ЈаваСцрипт. Неки, попут Типе Сцрипт, првобитно су били замишљени да буду најбољи начин за писање за веб. У исто време, Емсцриптен је начин да се постојећи Ц или Ц++ код преведе у форму која је читљива у претраживачу. на Ова страница Прикупили смо много портова познатих програма: овдеНа пример, можете погледати ПиПи - успут, они тврде да већ имају ЈИТ. У ствари, не може се сваки програм једноставно компајлирати и покренути у претраживачу - постоји број Карактеристике, са чиме се, међутим, морате помирити, пошто натпис на истој страници каже „Емсцриптен се може користити за састављање скоро свих преносив Ц/Ц++ код у ЈаваСцрипт". То јест, постоји низ операција које су недефинисано понашање према стандарду, али обично раде на к86 - на пример, неусклађени приступ променљивим, што је генерално забрањено на неким архитектурама. Генерално , Кему је програм на више платформи и , желео сам да верујем, и већ не садржи много недефинисаног понашања - узмите га и компајлирајте, а затим мало поправљајте ЈИТ - и готови сте! Али то није случај...

Први покушај

Уопштено говорећи, нисам прва особа која је дошла на идеју да пренесе Кему на ЈаваСцрипт. На РеацтОС форуму је постављено питање да ли је то могуће помоћу Емсцриптен-а. Још раније су се шушкале да је то лично урадио Фабрис Белард, али смо причали о јслинук-у, који је, колико ја знам, само покушај да се ручно постигне довољне перформансе у ЈС-у, а написан је од нуле. Касније је написан Виртуал к86 - за њега су објављени необускнути извори, а, како је наведено, већи „реализам“ емулације омогућио је коришћење СеаБИОС-а као фирмвера. Поред тога, постојао је бар један покушај портовања Кему-а користећи Емсцриптен - покушао сам да урадим ово соцкетпаир, али развој је, колико сам разумео, био замрзнут.

Дакле, чини се, ево извора, овде је Емсцриптен - узмите и саставите. Али постоје и библиотеке од којих зависи Кему, и библиотеке од којих зависе те библиотеке итд., а једна од њих је либффи, од којег глиб зависи. На интернету су се шушкале да постоји један у великој колекцији портова библиотека за Емсцриптен, али је некако било тешко поверовати: прво, није требало да буде нови компајлер, друго, био је сувише ниског нивоа. библиотеку коју треба само покупити и компајлирати у ЈС. И није ствар само у уметању склопа - вероватно, ако га окренете, за неке конвенције позивања можете генерисати потребне аргументе на стеку и позвати функцију без њих. Али Емсцриптен је зезнута ствар: да би генерисани код изгледао познато оптимизатору ЈС мотора претраживача, користе се неки трикови. Конкретно, такозвани релоопинг - генератор кода који користи примљени ЛЛВМ ИР са неким апстрактним упутствима за прелаз покушава да поново створи уверљиве иф-ове, петље итд. Па, како се аргументи прослеђују функцији? Наравно, као аргументи ЈС функцијама, односно, ако је могуће, не кроз стек.

На почетку је постојала идеја да једноставно напишем замену за либффи са ЈС-ом и покренем стандардне тестове, али на крају сам се збунио како да направим своје заглавље тако да раде са постојећим кодом - шта да радим, како кажу: „Да ли су задаци тако сложени „Јесмо ли тако глупи?“ Морао сам да пренесем либффи на другу архитектуру, да тако кажем – на срећу, Емсцриптен има и макрое за инлине асемблер (у Јавасцрипт-у, да – па, без обзира на архитектуру, дакле асемблер), и могућност покретања кода генерисаног у ходу. Генерално, након што сам неко време петљао са фрагментима либффи зависним од платформе, добио сам неки компајлибилни код и покренуо га на првом тесту на који сам наишао. На моје изненађење, тест је био успешан. Запањен својом генијалношћу – без шале, успело је од првог лансирања – ја сам, још увек не верујући својим очима, отишао да поново погледам резултујући код, да проценим где даље да копам. Овде сам полудео по други пут - једино што је моја функција урадила је ffi_call - ово је пријавило успешан позив. Није било самог позива. Зато сам послао свој први захтев за повлачење, који је исправио грешку у тесту која је јасна сваком ученику Олимпијаде – стварне бројеве не треба поредити као a == b па чак и како a - b < EPS - такође морате запамтити модул, иначе ће се 0 испоставити да је веома једнако 1/3... Генерално, смислио сам одређени порт либффи-а, који пролази најједноставније тестове и са којим је глиб саставио - одлучио сам да ће бити потребно, додаћу касније. Гледајући унапред, рећи ћу да, како се испоставило, компајлер није чак ни укључио либффи функцију у коначни код.

Али, као што сам већ рекао, постоје нека ограничења, а међу слободним коришћењем разних недефинисаних понашања, сакривена је још непријатнија карактеристика – ЈаваСцрипт по дизајну не подржава вишенитност са дељеном меморијом. У принципу, ово се обично може чак назвати и добром идејом, али не за пренос кода чија је архитектура везана за Ц нити. Уопштено говорећи, Фирефок експериментише са подршком дељених радника, а Емсцриптен има имплементацију птхреад за њих, али нисам желео да зависим од тога. Морао сам полако да искоријеним вишенитност из Кему кода – то јест, откријем гдје се покрећу нити, премјестим тијело петље која се покреће у овој нити у посебну функцију и позвати такве функције једну по једну из главне петље.

Други покушај

У неком тренутку је постало јасно да је проблем и даље присутан и да насумично гурање штака око кода неће довести до ничега. Закључак: потребно је некако систематизовати процес додавања штака. Стога је узета верзија 2.4.1, која је тада била свежа (не 2.5.0, јер ће, ко зна, у новој верзији бити грешака које још нису ухваћене, а ја имам довољно својих грешака ), а прва ствар је била да га безбедно препишете thread-posix.c. Па, то јест, као безбедно: ако је неко покушао да изврши операцију која је довела до блокирања, функција је одмах позвана abort() - наравно, ово није решило све проблеме одједном, али је барем било некако пријатније од тихог примања недоследних података.

Генерално, опције Емсцриптен-а су од велике помоћи у преносу кода на ЈС -s ASSERTIONS=1 -s SAFE_HEAP=1 - хватају неке типове недефинисаног понашања, као што су позиви на неусклађену адресу (што уопште није у складу са кодом за откуцане низове као што је HEAP32[addr >> 2] = 1) или позивање функције са погрешним бројем аргумената.

Иначе, грешке у поравнању су посебно питање. Као што сам већ рекао, Кему има „дегенерисану“ интерпретативну позадину за ТЦИ генерисање кода (маћушни тумач кода), а за изградњу и покретање Кему-а на новој архитектури, ако имате среће, довољан је компајлер Ц. Кључне речи "ако будеш имао среће". Нисам имао среће и испоставило се да ТЦИ користи неусклађени приступ када анализира свој бајт код. То јест, на свим врстама АРМ-а и других архитектура са нужно нивелираним приступом, Кему компајлира јер имају нормалну ТЦГ позадину која генерише изворни код, али да ли ће ТЦИ радити на њима је друго питање. Међутим, како се испоставило, ТЦИ документација јасно указује на нешто слично. Као резултат тога, коду су додати позиви функција за неусклађено читање, који су откривени у другом делу Кему-а.

Уништавање гомиле

Као резултат тога, неусклађени приступ ТЦИ је исправљен, створена је главна петља која је заузврат звала процесор, РЦУ и још неке ситнице. И тако покрећем Кему са опцијом -d exec,in_asm,out_asm, што значи да треба да кажете који блокови кода се извршавају, као и да у време емитовања напишете шта је био гостујући код, који је код хоста постао (у овом случају бајт код). Покреће се, извршава неколико блокова превођења, пише поруку за отклањање грешака коју сам оставио да ће се РЦУ сада покренути и... руши abort() унутар функције free(). Петљањем са функцијом free() Успели смо да сазнамо да је у заглављу хеап блока, које лежи у осам бајтова који претходе додељеној меморији, уместо величине блока или нечег сличног, било смећа.

Уништавање гомиле - како је слатко... У таквом случају постоји користан лек - из (ако је могуће) истих извора, саставите изворни бинарни фајл и покрените га под Валгринд-ом. После неког времена, бинарни фајл је био спреман. Покрећем га са истим опцијама - пада чак и током иницијализације, пре него што заиста дође до извршења. Непријатно је, наравно - очигледно, извори нису били потпуно исти, што није изненађујуће, јер је конфигурација изнашла мало другачије опције, али имам Валгринд - прво ћу поправити ову грешку, а онда, ако будем имао среће , појавиће се оригинални. Покрећем исту ствар под Валгринд-ом... И-и-и, и-и-и, ух-ух, почело је, нормално је прошло кроз иницијализацију и наставило даље од оригиналне грешке без иједног упозорења о погрешном приступу меморији, да не спомињемо падове. Живот ме, како кажу, није припремио за ово - програм за рушење престаје да се руши када се покрене под Валгриндом. Шта је то било је мистерија. Моја хипотеза је да је једном у близини тренутне инструкције након пада током иницијализације, гдб показао рад memset-а са важећим показивачем користећи било који mmx, или xmm регистре, онда је то можда била нека врста грешке у поравнању, мада је и даље тешко поверовати.

Ок, изгледа да Валгринд не помаже овде. И ту је почело оно најодвратније - све изгледа да је почело, али се руши из апсолутно непознатих разлога због догађаја који је могао да се деси пре милион инструкција. Дуго није било јасно ни како приступити. На крају сам ипак морао да седнем и отклоним грешке. Штампање онога чиме је заглавље преписано показало је да то не личи на број, већ на неку врсту бинарних података. И, ето, овај бинарни низ је пронађен у БИОС датотеци – то јест, сада се могло са разумном поузданошћу рећи да је у питању преливање бафера, а чак је јасно да је уписано у овај бафер. Па, онда нешто овако – у Емсцриптен-у, на срећу, нема насумичне адресног простора, нема ни рупа у њему, тако да можете да напишете негде на средини кода за излаз података по показивачу од последњег покретања, погледајте податке, погледајте показивач и, ако се није променио, потражите храну за размишљање. Истина, потребно је неколико минута за повезивање након било какве промене, али шта можете да урадите? Као резултат тога, пронађена је специфична линија која је копирала БИОС из привременог бафера у меморију за госте - и, заиста, није било довољно простора у баферу. Проналажење извора те чудне адресе бафера резултирало је функцијом qemu_anon_ram_alloc у фајлу oslib-posix.c - логика је била следећа: понекад може бити корисно поравнати адресу са огромном страницом величине 2 МБ, за то ћемо питати mmap прво још мало, а онда ћемо уз помоћ вратити вишак munmap. А ако такво поравнање није потребно, онда ћемо навести резултат уместо 2 МБ getpagesize() - mmap и даље ће издати усклађену адресу... Дакле, у Емсцриптену mmap само позива malloc, али се наравно не поравна на страници. Генерално, грешка која ме је фрустрирала неколико месеци исправљена је променом у два линије.

Карактеристике позива функција

А сада процесор нешто броји, Кему се не руши, али екран се не укључује, а процесор брзо иде у петље, судећи по излазу -d exec,in_asm,out_asm. Појавила се хипотеза: прекиди тајмера (или, уопште, сви прекиди) не стижу. И заиста, ако одврнете прекиде са матичног склопа, који је из неког разлога функционисао, добијате сличну слику. Али ово уопште није био одговор: поређење трагова издатих са горњом опцијом показало је да су се путање извршења врло рано разишле. Овде се мора рећи да је поређење онога што је снимљено помоћу лансера emrun отклањање грешака излаза са излазом матичног склопа није потпуно механички процес. Не знам тачно како се повезује програм који ради у претраживачу emrun, али неке линије на излазу се испостављају преуређене, тако да разлика у дифф-у још није разлог за претпоставку да су се путање разишле. Генерално, постало је јасно да према упутствима ljmpl постоји прелаз на различите адресе, а генерисани бајткод је суштински другачији: један садржи инструкцију за позивање помоћне функције, други не. Након гуглања инструкција и проучавања кода који преводи ова упутства, постало је јасно да је, прво, непосредно пре тога у регистру cr0 направљен је снимак – такође помоћу помоћника – који је пребацио процесор у заштићени режим, и друго, да јс верзија никада није прешла у заштићени режим. Али чињеница је да је још једна карактеристика Емсцриптена његова невољкост да толерише код као што је имплементација инструкција call у ТЦИ, који било који показивач функције резултира у типу long long f(int arg0, .. int arg9) - функције морају бити позване са тачним бројем аргумената. Ако се ово правило прекрши, у зависности од подешавања за отклањање грешака, програм ће се или срушити (што је добро) или ће уопште позвати погрешну функцију (што ће бити тужно за отклањање грешака). Постоји и трећа опција - омогући генерисање омотача који додају/уклањају аргументе, али укупно ови омоти заузимају доста простора, упркос чињеници да ми у ствари треба само нешто више од сто омотача. Ово је само по себи веома тужно, али се испоставило да постоји озбиљнији проблем: у генерисаном коду функција омотача, аргументи су конвертовани и конвертовани, али понекад функција са генерисаним аргументима није позвана - па, баш као у моја имплементација либффи. То јест, неки помагачи једноставно нису погубљени.

На срећу, Кему има машински читљиве листе помагача у облику датотеке заглавља попут

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

И тако, након тога је изгледало да процесор ради. Чини се да је то зато што екран никада није био иницијализован, иако је мемтест86+ могао да ради у матичном склопу. Овде је потребно разјаснити да је Кему блок И/О код написан у корутинама. Емсцриптен има сопствену веома зезнуту имплементацију, али је и даље требало да буде подржан у Кему коду, а сада можете да отклоните грешке у процесору: Кему подржава опције -kernel, -initrd, -append, са којим можете покренути Линук или, на пример, мемтест86+, а да уопште не користите блок уређаје. Али ево проблема: у матичном склопу се могао видети излаз Линук кернела на конзолу са опцијом -nographic, и нема излаза из претраживача на терминал одакле је покренут emrun, није дошао. То јест, није јасно: процесор не ради или графички излаз не ради. А онда ми је пало на памет да мало сачекам. Испоставило се да „процесор не спава, већ само полако трепће“, а након отприлике пет минута кернел је бацио гомилу порука на конзолу и наставио да виси. Постало је јасно да процесор, генерално, ради, и треба да се удубимо у код за рад са СДЛ2. Нажалост, не знам како да користим ову библиотеку, па сам на неким местима морао да се понашам насумично. У неком тренутку је на екрану на плавој позадини засветлела линија паралел0, што је наговестило неке мисли. На крају се испоставило да је проблем у томе што Кему отвара неколико виртуелних прозора у једном физичком прозору, између којих се можете пребацивати помоћу Цтрл-Алт-н: ради у матичној верзији, али не и у Емсцриптен-у. Након што се ослободите непотребних прозора помоћу опција -monitor none -parallel none -serial none и упутства да се насилно прецрта цео екран на сваком кадру, све је одједном функционисало.

Цороутинес

Дакле, емулација у претраживачу ради, али у њему не можете покренути ништа занимљиво са једном дискетом, јер нема блок И/О - морате имплементирати подршку за корутине. Кему већ има неколико позадинских програма корутине, али због природе ЈаваСцрипт-а и Емсцриптен генератора кода, не можете тек тако да почнете да жонглирате стековима. Чини се да је „све нестало, гипс се уклања“, али програмери Емсцриптена су се већ побринули за све. Ово је имплементирано прилично смешно: назовимо позив функције као што је овај сумњив emscripten_sleep и неколико других који користе механизам Асинцифи, као и позиве показивача и позиве било којој функцији где се један од претходна два случаја може појавити ниже у стеку. И сада, пре сваког сумњивог позива, изабраћемо асинхрони контекст, а одмах након позива проверићемо да ли је дошло до асинхроног позива и ако јесте сачуваћемо све локалне променљиве у овом асинхронизованом контексту, назначити коју функцију да пренесемо контролу када треба да наставимо са извршавањем и изађемо из тренутне функције. Овде постоји простор за проучавање ефекта расипање — за потребе наставка извршавања кода након повратка из асинхроног позива, компајлер генерише „заглавке“ функције почевши од сумњивог позива — овако: ако има н сумњивих позива, онда ће функција бити проширена негде н/2 пута — ово је још увек, ако не. Имајте на уму да после сваког потенцијално асинхроног позива морате да додате чување неких локалних променљивих оригиналној функцији. Након тога сам чак морао да напишем једноставну скрипту у Питхон-у, која, на основу датог скупа посебно претерано коришћених функција које наводно „не дозвољавају да асинхронија прође кроз себе“ (односно, промоција стека и све што сам управо описао не рад у њима), означава позиве преко показивача у којима преводилац треба да игнорише функције како се те функције не би сматрале асинхроним. А онда су ЈС датотеке испод 60 МБ очигледно превише - рецимо најмање 30. Мада, једном сам постављао скрипту за склапање и случајно избацио опције линкера, међу којима је било -O3. Покрећем генерисани код, а Цхромиум троши меморију и руши. Онда сам случајно погледао шта покушава да преузме... Па, шта да кажем, и ја бих се укочио да су ме замолили да пажљиво проучим и оптимизујем Јавасцрипт од 500+ МБ.

Нажалост, провере у коду библиотеке подршке Асинцифи нису биле сасвим пријатељске longjmp-с који се користе у коду виртуелног процесора, али након мале закрпе која онемогућава ове провере и насилно враћа контексте као да је све у реду, код је радио. А онда је почела чудна ствар: понекад су се покретале провере у коду за синхронизацију – исте оне које руше код ако би, према логици извршења, требало да буде блокиран – неко је покушао да зграби већ ухваћен мутекс. На срећу, испоставило се да ово није логичан проблем у серијализованом коду – једноставно сам користио стандардну функционалност главне петље коју је обезбедио Емсцриптен, али понекад би асинхрони позив потпуно размотао стек и у том тренутку би пропао setTimeout из главне петље – тако је код ушао у итерацију главне петље без напуштања претходне итерације. Преписано на бесконачној петљи и emscripten_sleep, и проблеми са мутексима су престали. Код је чак постао логичнији - у ствари, ја немам неки код који припрема следећи оквир анимације - процесор само нешто израчуна и екран се периодично ажурира. Међутим, проблеми се ту нису зауставили: понекад би се извршавање Кему-а једноставно прекинуло тихо без икаквих изузетака или грешака. У том тренутку сам одустао од тога, али, гледајући унапред, рећи ћу да је проблем био следећи: код корутине, у ствари, не користи setTimeout (или барем не онолико често колико мислите): функција emscripten_yield једноставно поставља ознаку асинхроног позива. Цела поента је у томе emscripten_coroutine_next није асинхрона функција: интерно проверава заставицу, ресетује је и преноси контролу тамо где је потребна. То јест, промоција стека се ту завршава. Проблем је био у томе што је због коришћења после-фрее, који се појавио када је скуп корутина био онемогућен због чињенице да нисам копирао важну линију кода са постојећег позадинског дела корутине, функција qemu_in_coroutine вратио труе када је у ствари требало да врати фалсе. Ово је довело до позива emscripten_yield, изнад које није било никога на стогу emscripten_coroutine_next, стек се развио до самог врха, али не setTimeout, као што сам већ рекао, није био изложен.

Генерисање ЈаваСцрипт кода

А ево, у ствари, обећаног „враћања млевеног меса назад“. Не баш. Наравно, ако покренемо Кему у претраживачу, а Ноде.јс у њему, онда ћемо, наравно, након генерисања кода у Кему-у добити потпуно погрешан ЈаваСцрипт. Али ипак, нека врста обрнуте трансформације.

Прво, мало о томе како Кему функционише. Опростите ми одмах: ја нисам професионални Кему програмер и моји закључци могу бити погрешни на неким местима. Како кажу, „мишљење ученика не мора да се поклапа са мишљењем наставника, Пеановом аксиоматиком и здравим разумом“. Кему има одређени број подржаних гостујућих архитектура и за сваку постоји директоријум као target-i386. Када градите, можете навести подршку за неколико гостујућих архитектура, али резултат ће бити само неколико бинарних датотека. Код за подршку гостујуће архитектуре, заузврат, генерише неке интерне Кему операције, које ТЦГ (Тини Цоде Генератор) већ претвара у машински код за архитектуру домаћина. Као што је наведено у реадме датотеци која се налази у тцг директоријуму, ово је првобитно био део обичног Ц компајлера, који је касније прилагођен за ЈИТ. Стога, на пример, циљна архитектура у смислу овог документа више није гостујућа, већ хост архитектура. У неком тренутку се појавила још једна компонента - Тини Цоде Интерпретер (ТЦИ), која би требало да изврши код (скоро исте унутрашње операције) у одсуству генератора кода за одређену архитектуру домаћина. У ствари, како се наводи у његовој документацији, овај интерпретер можда неће увек радити тако добро као генератор ЈИТ кода, не само квантитативно у смислу брзине, већ и квалитативно. Мада нисам сигуран да је његов опис потпуно релевантан.

У почетку сам покушао да направим пуноправни ТЦГ бацкенд, али сам се брзо збунио у изворном коду и не сасвим јасном опису инструкција бајткода, па сам одлучио да умотам ТЦИ интерпретер. Ово је дало неколико предности:

  • када имплементирате генератор кода, не можете погледати опис инструкција, већ код интерпретатора
  • можете генерисати функције не за сваки наиђени блок превода, већ, на пример, тек након стотог извршења
  • ако се генерисани код промени (а то изгледа могуће, судећи по функцијама са називима који садрже реч закрпа), мораћу да поништим генерисани ЈС код, али ћу бар имати од чега да га поново генеришем

Што се тиче треће тачке, нисам сигуран да је закрпање могуће након што се код први пут изврши, али прве две тачке су довољне.

Првобитно, код је генерисан у облику великог прекидача на адреси оригиналне инструкције бајт кода, али онда, сећајући се чланка о Емсцриптену, оптимизацији генерисаног ЈС-а и поновном понављању, одлучио сам да генеришем више људског кода, поготово што је емпиријски показало се да је једина улазна тачка у блок превода његов почетак. Тек што је речено него урађено, након неког времена имали смо генератор кода који је генерисао код са ифс (иако без петљи). Али лоша срећа, срушио се, дајући поруку да су упутства била неке нетачне дужине. Штавише, последња инструкција на овом нивоу рекурзије је била brcond. У реду, додаћу идентичну проверу генерисању ове инструкције пре и после рекурзивног позива и... ниједна од њих није извршена, али после прекидача ассерт ипак нису успеле. На крају, након проучавања генерисаног кода, схватио сам да се након пребацивања показивач на тренутну инструкцију поново учитава из стека и вероватно је преписан генерисаним ЈаваСцрипт кодом. И тако се испоставило. Повећање бафера са једног мегабајта на десет није довело ни до чега, а постало је јасно да генератор кода ради у круговима. Морали смо да проверимо да нисмо изашли ван граница тренутне ТБ, а ако јесмо, онда дамо адресу следећег ТБ са знаком минус да бисмо могли да наставимо са извршењем. Поред тога, ово решава проблем „које генерисане функције треба да буду поништене ако се овај део бајткода променио?“ — само функција која одговара овом блоку превода треба да буде поништена. Иначе, иако сам све отклонио у Цхромиум-у (пошто користим Фирефок и лакше ми је да користим посебан претраживач за експерименте), Фирефок ми је помогао да исправим некомпатибилности са стандардом асм.јс, након чега је код почео да ради брже у Цхромиум.

Пример генерисаног кода

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"]

Закључак

Дакле, посао још увек није завршен, али сам уморан од потајног довођења ове дугорочне конструкције до савршенства. Зато сам одлучио да објавим оно што за сада имам. Код је на местима мало застрашујући, јер је ово експеримент и није унапред јасно шта треба да се уради. Вероватно, онда је вредно издати нормалне атомске урезивања поврх неке модерније верзије Кему-а. У међувремену, у Гити постоји нит у формату блога: за сваки „ниво“ који је бар некако пређен, додат је детаљан коментар на руском. Заправо, овај чланак је у великој мери понављање закључка git log.

Можете пробати све овде (чувај се саобраћаја).

Шта већ ради:

  • к86 виртуелни процесор ради
  • Постоји радни прототип генератора ЈИТ кода од машинског кода до ЈаваСцрипт-а
  • Постоји шаблон за склапање других 32-битних гостујућих архитектура: управо сада можете да се дивите Линуку због замрзавања МИПС архитектуре у претраживачу у фази учитавања

Шта још можете да урадите

  • Убрзајте емулацију. Чак и у ЈИТ режиму изгледа да ради спорије од Виртуал к86 (али потенцијално постоји цео Кему са пуно емулираног хардвера и архитектура)
  • Да направим нормалан интерфејс - искрено, нисам добар веб програмер, па сам за сада преправио стандардну шкољку Емсцриптен најбоље што могу
  • Покушајте да покренете сложеније Кему функције - умрежавање, миграција ВМ-а, итд.
  • УПД: мораћете да пошаљете неколико својих развоја и извештаја о грешкама Емсцриптен-у узводно, као што су радили претходни портери Кему-а и других пројеката. Хвала им што су могли имплицитно да искористе свој допринос Емсцриптену као део мог задатка.

Извор: ввв.хабр.цом

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