Qemu.js з падтрымкай JIT: фарш усё ж можна пракруціць назад

Некалькі гадоў таму Фабрыс Белар напісаў jslinux - эмулятар ПК, напісаны на JavaScript. Пасля гэтага быў яшчэ як мінімум Virtual x86. Але ўсе яны, наколькі мне вядома, з'яўляліся інтэрпрэтатарамі, у той час як напісаны значна раней тым жа Фабрысам Беларам Qemu, ды і, мусіць, любы які паважае сябе сучасны эмулятар, выкарыстае JIT-кампіляцыю гасцявога кода ў код хаставой сістэмы. Мне здалося, што самы час рэалізаваць зваротную задачу ў адносінах да той, якую вырашаюць браўзэры: JIT-кампіляцыю машыннага кода ў JavaScript, для чаго больш лагічна бачылася партаваць Qemu. Здавалася б, чаму менавіта Qemu, ёсць жа прасцейшыя і user-friendly эмулятары - той жа VirtualBox, напрыклад - паставіў і працуе. Але ў Qemu ёсць некалькі цікавых асаблівасцяў

  • адкрытыя зыходнікі
  • магчымасць працаваць без драйвера ядра
  • магчымасць працаваць у рэжыме інтэрпрэтатара
  • падтрымка вялікай колькасці як хостовых, так і гасцявых архітэктур

На рахунак трэцяга пункта зараз-то я ўжо магу растлумачыць, што насамрэч у рэжыме TCI інтэрпрэтуюцца не самі гасцявыя машынныя інструкцыі, а атрыманы з іх байткод, але істы гэта не змяняе каб сабраць і запусціць Qemu на новай архітэктуры, калі павязе, дастаткова кампілятара C - напісанне кодогенератора можна адкласці.

І вось, пасля двух гадоў павольнага калупання ў вольны час зыходнікаў Qemu з'явіўся які працуе прататып, у якім ужо можна запусціць, напрыклад, Kolibri OS.

Што такое Emscripten

У наш час з'явілася шмат кампілятараў, канчатковым вынікам працы якіх з'яўляецца JavaScript. Некаторыя, такія як Type Script, першапачаткова задумваліся як лепшы спосаб пісаць для вэба. У той жа час, Emscripten - гэта спосаб узяць існуючы код на C або C ++, і скампіляваць яго ў выгляд, зразумелы браўзэру. На гэтай старонцы сабрана нямала партоў вядомых праграм: тут, напрыклад, можна паглядзець на PyPy - дарэчы, як сцвярджаецца, у іх ужо ёсць JIT. Насамрэч, не любую праграму можна проста скампіляваць і запусціць у браўзэры — ёсць шэраг асаблівасцяў, з якімі даводзіцца мірыцца, зрэшты, як абвяшчае надпіс на гэтай жа старонцы "Emscripten can be used to compile almost any партатыўны C/C++ code to JavaScript". Гэта значыць існуе шэраг аперацый, якія з'яўляюцца нявызначанымі паводзінамі па стандарце, але звычайна працуюць на x86 - да прыкладу, нявыраўнены доступ да зменных, які на некаторых архітэктурах наогул забаронены. Увогуле, Qemu - праграма кросплатформенная і , хацелася верыць, і так не ўтрымлівае вялікай колькасці неапярэджаных паводзін - бяры і кампілюй, потым крыху павазіцца з JIT - і говото!Але не тут-то было…

Першая спроба

Наогул кажучы, я не першы, каму прыйшла ў галаву ідэя партаваць Qemu на JavaScript. На форуме ReactOS задавалася пытанне, ці магчыма гэта з дапамогай Emscripten. Яшчэ раней хадзілі чуткі, што гэта зрабіў асабіста Фабрыс Белар, але гаворка ішла пра jslinux, які, наколькі мне вядома, з'яўляецца якраз спробай уручную дабіцца на JS дастатковай прадукцыйнасці, і напісаны з нуля. Пазней быў напісаны Virtual x86 да яго былі выкладзеныя необфусцированные зыходнікі, і, як сцвярджалася, вялікая рэалістычнасць эмуляцыі дазволіла выкарыстоўваць SeaBIOS у якасці firmware. Акрамя таго, была як мінімум адна спроба партаваць Qemu з дапамогай Emscripten - гэта спрабаваў зрабіць socketpair, Але распрацоўка, наколькі я зразумеў, была замарожана.

Такім чынам, здавалася б, вось зыходнікі, вось Emscripten бяры і кампілюй. Але ёсць яшчэ і бібліятэкі, ад якіх Qemu залежыць, і бібліятэкі ад якіх залежаць тыя бібліятэкі і г.д., прычым адна з іх - libffi, ад якой залежыць glib. У інтэрнэце знаходзіліся чуткі, што ў вялікай калекцыі партоў бібліятэк пад Emscripten ёсць і яна, але верылася неяк з цяжкасцю: па-першае, новым кампілятарам яна не збіралася, па-другое, гэта занадта нізкаўзроўневая бібліятэка, каб проста так узяць, і скампілявацца ў JS. І справа нават не толькі ў асэмблерных устаўках напэўна, калі перакруціцца, то для некаторых calling conventions можна і без іх сфармаваць патрэбныя аргументы на стэку і выклікаць функцыю. Вось толькі Emscripten - штуковіна хітрая: для таго, каб згенераваны код выглядаў звыкла для аптымізатара JS-рухавічка браўзэра, выкарыстоўваюцца некаторыя трукі. У прыватнасці, так званы relooping – кодагенератар па атрыманым LLVM IR з нейкімі абстрактнымі інструкцыямі пераходаў спрабуе ўзнавіць праўдападобныя if-ы, цыклы і г.д. Ну а аргументы ў функцыі перадаюцца як? Натуральна, як аргументы JS-функцый, гэта значыць па магчымасці не праз стэк.

Напачатку была думка проста напісаць замену libffi на JS і прагнаць штатныя тэсты, але ў выніку я заблытаўся ў тым, як зрабіць свае загалоўкавыя файлы, каб яны працавалі з існым кодам — што ўжо зрабіць, як гаворыцца, "Ці задачы такія складаныя , ці то мы такія тупыя". Прыйшлося партаваць libffi на яшчэ адну архітэктуру, калі можна так выказацца – на шчасце, у Emscripten ёсць як макрасы для inline assembly (на джаваскрыпце, ага – ну, якая архітэктура, такі і асэмблер), так і магчымасць запусціць згенераваны на ходу код. Увогуле, павазіўшыся некаторы час з платформенна-залежнымі фрагментамі libffi, я атрымаў нейкі які кампілюецца код, і прагнаў яго на першым які трапіўся цесцю. На маё здзіўленне, тэст прайшоў паспяхова. Офігеў ад сваёй геніяльнасці - ці жарт, зарабіла з першага запуску - я, усё яшчэ не верачы сваім вачам, палез яшчэ раз паглядзець на атрыманы код, ацаніць, куды капаць далей. Тут я афігеў другасна - адзінае, што рабіла мая функцыя ffi_call - Гэта рапартавала аб паспяховым выкліку. Самога выкліку не было. Так я адправіў свой першы pull request, які выпраўляў зразумелую любому алімпіядніку памылку ў цесцю - рэчавыя лікі не варта параўноўваць як a == b і нават як a - b < EPS - трэба яшчэ модуль не забыцца, а то 0 апынецца вельмі нават роўны 1/3… Увогуле, у мяне атрымаўся нейкі порт libffi, які праходзіць самыя простыя тэсты, і з якім кампілюецца glib - вырашыў, трэба будзе, потым дапішу. Забягаючы наперад скажу, што, як аказалася, у фінальны код функцыі libffi кампілятар нават не ўключыў.

Але, як я ўжо казаў, ёсць некаторыя абмежаванні, і сярод вольнага выкарыстання разнастайных нявызначаных паводзін зачасалася асаблівасць непрыемней - JavaScript by design не падтрымлівае шматструменнасць з агульнай памяццю. У прынцыпе, гэта звычайна можна нават назваць нядрэннай ідэяй, але не для партавання кода, чыя архітэктура завязана на сішныя патокі. Наогул кажучы, у Firefox ідуць эксперыменты па падтрымцы shared workers, і рэалізацыя pthread для іх у Emscripten прысутнічае, але залежаць ад гэтага не жадалася. Прыйшлося паволі выкарчоўваць шматструменнасць з кода Qemu - гэта значыць вышукваць, дзе запускаюцца струмені, выносіць цела цыклу, які выконваецца ў гэтым струмені ў асобную функцыю, і па чарзе выклікаць такія функцыі з асноўнага цыклу.

другая спроба

У нейкі момант стала зразумела, што воз і цяпер там, і што бессістэмнае распіхванне мыліц па кодзе да дабра не давядзе. Выснова: трэба неяк сістэматызаваць працэс дадання мыліц. Таму была ўзята свежая на той момант версія 2.4.1 (не 2.5.0, таму што, ці мала, тамака апынуцца яшчэ не адлоўленыя багі новай версіі, а мне і сваіх багаў хопіць), і перш за ўсё быў бяспечным чынам перапісаны thread-posix.c. Ну гэта значыць, як бяспечным: калі хтосьці спрабаваў выканаць аперацыю, якая прыводзіць да блакіроўкі, тут жа выклікалася функцыя abort() - Вядома, гэта не вырашала адразу ўсіх праблем, але як мінімум, гэта неяк прыемней, чым ціха атрымліваць некансістэнтнасць дадзеных.

Наогул, у партаванні кода на JS вельмі дапамагаюць опцыі Emscripten -s ASSERTIONS=1 -s SAFE_HEAP=1 - яны адлоўліваюць некаторыя віды undefined behavior накшталт зваротаў па не выраўнаваны адрас (што зусім не адпавядае кодам для typed arrays накшталт HEAP32[addr >> 2] = 1) або выклік функцыі з няправільным колькасцю аргументаў.

Дарэчы, памылкі выраўноўвання - асобная тэма. Як я ўжо казаў, у Qemu ёсць "выраджаны" інтэрпрэтаваны бэкэнд кодагенерацыі TCI (tiny code interpreter), і каб сабраць і запусціць Qemu на новай архітэктуры, калі павязе, дастаткова кампілятара C. Ключавыя словы "калі павязе". Мне вось не павезла, і аказалася, што TCI пры разборы свайго байт-кода выкарыстоўвае не выраўнаваны доступ. Гэта значыць на ўсякіх там ARM і іншых архітэктурах з абавязкова выраўнаваным доступам Qemu кампілюецца таму, што для іх ёсць нармальны TCG-бэкенд, які генеруе натыўны код, а ці запрацуе на іх TCI – гэта яшчэ пытанне. Зрэшты, як аказалася, у дакументацыі на TCI нешта падобнае відавочна паказвалася. У выніку ў код былі дададзеныя выклікі функцый для не выраўнаванага чытання, якія выявіліся ў іншай частцы Qemu.

Разбурэнне кучы

У выніку, не выраўнаваны доступ у TCI быў выпраўлены, зроблены галоўны цыкл, які па чарзе выклікаў працэсар, RCU і нешта па дробязі. І вось я запускаю Qemu з опцыяй -d exec,in_asm,out_asm, Якая азначае, што трэба казаць, якія блокі кода выконваюцца, а таксама ў момант трансляцыі пісаць, які гасцявы код быў, які хаставы код стаў (у дадзеным выпадку, байткод). Яно запускаецца, выконвае некалькі блокаў трансляцыі, піша пакінутае мной адладкавае паведамленне, што зараз запусціцца RCU і… падае па abort() ўнутры функцыі free(). Шляхам калупання функцыі free() атрымалася высветліць, што ў загалоўку блока кучы, які ляжыць у васьмі байтах, якія папярэднічалі вылучанай памяці, замест памеру блока ці чагосьці падобнага апынулася смецце.

Разбурэнне кучы як міла… У падобным выпадку ёсць карысны сродак з (па магчымасці) тых жа зыходнікаў сабраць натыўны бінарнік і прагнаць пад Valgrind. Праз некаторы час бінарнік быў гатовы. Запускаю з тымі ж опцыямі - падае яшчэ на ініцыялізацыі, не дайшоўшы да, уласна, выканання. Непрыемна, вядома - відаць, зыходнікі былі не зусім тыя ж, што не дзіўна, бо configure выведаў некалькі іншыя опцыі, але ў мяне ж ёсць Valgrind - спачатку гэтую багу папраўлю, а потым, калі павязе, і зыходная выявіцца. Запускаю ўсё тое ж самае пад Valgrind… Ы-ы-ы, у-у-у, э-э-э, яно запусцілася, нармальна прайшло ініцыялізацыю і пайшло далей міма зыходнага бага без адзінага папярэджання аб няправільным доступе да памяці, не кажучы ўжо аб падзеннях. Да такога жыццё мяне, як той казаў, не рыхтавала — праграма, якая падае, перастае падаць пры запуску пад валгрындам. Што гэта было - загадка. Мая гіпотэза, што раз у наваколлях бягучай інструкцыі пасля падзення пры ініцыялізацыі gdb паказваў працу memset-а з валідным паказальнікам з выкарыстаннем ці то mmx, ці то xmm рэгістраў, то, магчыма, гэта была нейкая памылка выраўноўвання, хоць усё роўна верыцца слаба.

О-кей, Valgrind тут, падобна, не памагаты. І вось тут пачалося самае брыдкае - усё, накшталт, нават запускаецца, але падае па абсалютна невядомых прычынах з-за падзеі, якая магла адбыцца мільёны інструкцый таму. Доўгі час нават падступіцца было незразумела як. У выніку прыйшлося ўсёткі сесці і адладжваць. Друк таго, чым быў перапісаны загаловак, паказаў, што гэта падобна не да ліку, а, хутчэй, да нейкіх бінарных дадзеных. І, аб цуд, гэты бінарны радок знайшоўся ў файле з биосом — гэта значыць зараз можна было з дастатковай упэўненасцю сказаць, што гэта было перапаўненне буфера, і нават зразумела, што ў гэты буфер запісвалася. Ну а далей неяк так - у Emscripten, на шчасце, рандомизации адраснай прасторы няма, дзюр у ім таксама няма, таму можна напісаць дзе небудзь у сярэдзіне кода выснова дадзеных па паказальніку з мінулага запуску, паглядзець на дадзеныя, паглядзець на паказальнік, і , Калі той не змяніўся, атрымаць інфармацыю да разважання. Праўда, на лінкоўку пасля любой змены марнуецца пару хвілін, але што зробіш. У выніку быў знойдзены пэўны радок, якая капіюе BIOS з часовага буфера ў гасцёўню памяць – і, сапраўды, у буферы не аказалася дастатковага месца. Пошук крыніцы таго дзіўнага адраса буфера прывёў у функцыю qemu_anon_ram_alloc у файле oslib-posix.c - логіка там была такая: часам можа быць карысна выраўнаваць адрас па huge page памерам 2 Мб, для гэтага папросім у mmap спачатку крыху больш, а потым лішняе вернем з дапамогай munmap. А калі такое выраўноўванне не патрабуецца, то пакажам замест 2 Мб вынік getpagesize() - mmap жа ўсё роўна выдасць выраўнаваны адрас… Дык вось у Emscripten mmap проста выклікае malloc, а той, натуральна, па старонцы не выраўноўвае. Увогуле, бага, якая хвалявала мяне пару месяцаў, выправілася зменай у два радках.

Асаблівасці выкліку функцый

І вось ужо працэсар нешта лічыць, Qemu не падае, але экран не ўключаецца, і працэсар хутка зацыкляецца, судзячы па выснове -d exec,in_asm,out_asm. З'явілася гіпотэза: не прыходзяць перапыненні таймера (ну ці ўвогуле ўсе перапыненні). І сапраўды, калі ад натыўнай зборкі, якая чамусьці працавала, адкруціць перапыненні, атрымліваецца падобная карціна. Але разгадка аказалася зусім не ў гэтым: параўнанне трасіровак, выдадзеных з названай вышэй опцыяй, паказала, што траекторыі выканання разыходзяцца вельмі рана. Тут трэба сказаць, што параўнанне запісанага з дапамогай запускатара emrun адладкавай высновы з высновай натыўнай зборкі - не зусім механічны працэс. Не ведаю дакладна, як запушчаная ў браўзэры праграма злучаецца з emrun, Але некаторыя радкі ў выснове аказваюцца перастаўленыя месцамі, таму адрозненне ў дыфе - гэта яшчэ не нагода лічыць, што траекторыі разышліся. Увогуле, стала зразумела, што па інструкцыі ljmpl адбываецца пераход па розных адрасах, ды і байткод генеруецца прынцыпова розны: у адным ёсць інструкцыя выкліку сішной функцыі-хелпера, у іншым - не. Пасля гуглення інструкцыі і вывучэнні кода, які гэтыя інструкцыі транслюе, стала зразумела, што, па-першае, непасрэдна да яе ў рэгістр cr0 рабілася запіс - таксама з дапамогай хелпера -, якая перакладае працэсар у абаронены рэжым, а па-другое, што js-версія ў абаронены рэжым так і не перайшла. А справа ў тым, што яшчэ адной асаблівасцю Emscripten з'яўляецца нежаданне цярпець код накшталт рэалізацыі інструкцыі. call у TCI, якая любы паказальнік на функцыю прыводзіць да тыпу long long f(int arg0, .. int arg9) - функцыі павінны выклікацца з правільнай колькасцю аргументаў. Пры парушэнні гэтага правіла ў залежнасці ад адладкавых налад праграма або ўпадзе (што добра), або выкліча зусім не тую функцыю (што сумна будзе адладжваць). Ёсць яшчэ і трэці варыянт - уключыць генерацыю абгортак, якія дадаюць / выкідваюць аргументы, але сумарна гэтыя абгорткі займаюць вельмі нямала месца, пры тым, што фактычна мне трэба ўсяго толькі крыху больш за сотню абгортак. Адно толькі гэта вельмі сумна, але аказалася больш сур'ёзная праблема: у згенераваным кодзе функцый-акрутак аргументы канвертаваліся-канвертаваліся, толькі вось функцыя са згенераванымі аргументамі часам не выклікалася – ну прама як у маёй рэалізацыі libffi. Гэта значыць некаторыя хелперы проста не выконваліся.

На шчасце, у Qemu ёсць машыначытальныя спісы хелпераў у выглядзе загалоўкавых файлаў накшталт

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

Выкарыстоўваюцца яны даволі пацешна: спачатку самай мудрагелістай выявай перавызначаюцца макрасы. DEF_HELPER_n, а потым инклудится helper.h. Аж да таго, што макрас раскрываецца ў ініцыялізатар структуры і коску, а потым вызначаецца масіў, а замест элементаў - #include <helper.h> У выніку, нарэшце падвярнулася нагода паспрабаваць у працы бібліятэку. pyparsing, і быў напісаны скрыпт генеравальны абгорткі роўна тыя і роўна для тых функцый, для якіх трэба.

І вось, пасля гэтага працэсар быццам бы зарабіў. Быццам бы, таму што экран так і не ініцыялізаваўся, хаця ў натыўнай зборцы ўдалося запусціць memtest86+. Тут трэба ўдакладніць, што код блокавага ўводу-высновы Qemu напісаны на каруцінах. У Emscripten ёсць свая вельмі выкручастая рэалізацыя, але яе яшчэ трэба было падтрымаць у кодзе Qemu, а адладжваць працэсар можна ўжо цяпер: Qemu падтрымлівае опцыі -kernel, -initrd, -append, з дапамогай якіх можна загрузіць Linux ці, напрыклад, memtest86+, наогул не выкарыстоўваючы блокавыя прылады. Але вось няўдача: у натыўнай зборцы можна было назіраць выснову Linux kernel на кансоль з опцыяй -nographic, а з браўзэра ніякай высновы ў тэрмінал, адкуль быў запушчаны emrun, не прыходзіла. Гэта значыць незразумела: працэсар не працуе ці вывад графікі. А потым мне прыйшло ў галаву крыху пачакаць. Аказалася, што "працэсар не спіць, а проста павольна міргае", і хвілін праз пяць ядро ​​выкінула на кансоль пачак паведамленняў і пайшло віснуць далей. Стала зразумела, што працэсар, у цэлым, працуе, і капаць трэба ў кодзе працы з SDL2. Карыстацца гэтай бібліятэкай я, нажаль, не ўмею, таму месцамі прыйшлося дзейнічаць наўздагад. У нейкі момант на экране прамільгнула радок parallel0 на сінім фоне, што наводзіла на нейкія думкі. У выніку аказалася, што справа была ў тым, што Qemu адкрывае некалькі віртуальных вокнаў у адным фізічным акне, паміж якімі можна перамыкацца па Ctrl-Alt-n: у натыўнай зборцы яно працуе, у Emscripten – не. Пасля збавення ад лішніх вокнаў з дапамогай опцый -monitor none -parallel none -serial none і ўказанні прымусова перамалёўваць увесь экран на кожным кадры ўсё раптам зарабіла.

Каруціны

Такім чынам, эмуляцыя ў браўзэры працуе, але нічога цікавага аднадыскетнага ў ёй не запусціць, таму што няма блокавага ўводу-высновы - трэба рэалізоўваць падтрымку каруцін. У Qemu ужо ёсць некалькі coroutine backend-ов, але ў сілу асаблівасцяў JavaScript і кодагенератара Emscripten нельга проста ўзяць і пачаць жангляваць стэкамі. Здавалася б, "усё знікла, гіпс здымаюць", але распрацоўшчыкі Emscripten ужо пра ўсё паклапаціліся. Рэалізавана гэта даволі пацешна: а давайце назавем падазроным выклік функцыі накшталт emscripten_sleep і некалькіх іншых, якія выкарыстоўваюць механізм Asyncify, а таксама выклікі па паказальніку і выклікі любой функцыі, дзе ніжэй па стэку можа адбыцца адзін з папярэдніх двух выпадкаў. А зараз перад кожным падазроным выклікам вылучым async context, а адразу пасля выкліку - праверым, ці не адбыўся асінхронны выклік, і калі адбыўся, то захаваем усе лакальныя зменныя ў гэты async context, пакажам, на якую функцыю перадаваць кіраванне, калі трэба будзе працягнуць выкананне , і выйдзем з бягучай функцыі. Вось ужо дзе прастор для вывучэння эфекту. растарошчвання - для патрэб працягу выканання кода пасля вяртання з асінхроннага выкліку кампілятар генеруе "абрубкі" функцыі, якія пачынаюцца пасля падазронага выкліку - вось так: калі ёсць n падазроных выклікаў, то функцыя будзе скасаваная дзесьці ў n/2 разоў - гэта яшчэ, калі не улічваць, што ў зыходную функцыю трэба пасля кожнага патэнцыйна асінхроннага выкліку дадаць захаванне часткі лакальных зменных. Пасля нават прыйшлося пісаць няхітры скрыпт на Пітоне, які па зададзеным мностве асабліва спустошаных функцый, якія, як мяркуецца, "не прапускаюць асінхроннасць скрозь сябе" (гэта значыць у іх не спрацоўвае раскрутка стэка і ўсё тое, што я толькі што апісаў), паказвае, выклікі праз паказальнікі ў якіх функцыях трэба ігнараваць кампілятару, каб дадзеныя функцыі не разглядаліся як асінхронныя. А то JS-файлы пад 60 Мб – гэта ўжо відавочна перабор – хай ужо хаця б 30. Хоць, як-то раз я наладжваў зборачны скрыпт, і выпадкова выкінуў опцыі лінкоўшчыка, сярод якіх была і -O3. Запускаю згенераваны код, і Chromium выжырае памяць і падае. Я потым выпадкова паглядзеў на тое, што ён спрабаваў загрузіць… Ну, што я магу сказаць, я б таксама завіс, калі б мяне папрасілі ўдумліва вывучыць і аптымізаваць джаваскрыпт на 500+ Мб.

На жаль, праверкі ў кодзе бібліятэкі падтрымкі Asyncify не зусім сябравалі з longjmp-амі, якія выкарыстоўваюцца ў кодзе віртуальнага працэсара, але пасля невялікага патча, які адключае гэтыя праверкі і прымусова аднаўляе кантэксты так, як быццам усё добра, код зарабіў. І тут пачалося дзіўнае: часам спрацоўвалі праверкі ў кодзе сінхранізацыі - тыя самыя, якія аварыйна завяршаюць код, калі па логіцы выканання ён павінен заблакавацца - хтосьці спрабаваў захапіць ужо захоплены м'ютэкс. На шчасце, гэта апынулася не лагічная праблема ў серыялізаваным кодзе - проста я выкарыстаў штатную функцыянальнасць main loop, якая прадстаўляецца Emscripten, але часам асінхронны выклік цалкам разгортваў стэк, а ў гэты момант спрацоўваў setTimeout ад main loop - такім чынам, код заходзіў у ітэрацыю галоўнага цыклу, не выйшаўшы з папярэдняй ітэрацыі. Перапісаў на бясконцым цыкле і emscripten_sleep, і праблемы з м'ютэксамі спыніліся. Код нават больш лагічным стаў - бо, па сутнасці, у мяне няма нейкага кода, які падрыхтоўвае чарговы кадр анімацыі - проста працэсар нешта лічыць і экран перыядычна абнаўляецца. Зрэшты, праблемы на гэтым не спыніліся: часам выкананне Qemu проста ціха завяршалася без якіх бы там ні было выключэнняў і памылак. У той момант я на гэта забіў, але, забягаючы наперад скажу, што праблема была вось у чым: код каруцін, насамрэч, наогул не выкарыстоўвае setTimeout (ну ці, прынамсі, не так часта, як можна падумаць): функцыя emscripten_yield проста выстаўляе сцяжок асінхроннага выкліку. Уся соль у тым, што emscripten_coroutine_next не з'яўляецца асінхроннай функцыяй: усярэдзіне сябе яна правярае сцяжок, скідае яго і перадае кіраванне куды трэба. Гэта значыць на ёй раскрутка стэка сканчаецца. Праблема была ў тым, што з-за use-after-free, які выяўляўся пры адключаным пуле каруцін з-за таго, што я не дакапіраваў важны радок кода з існуючага coroutine backend, функцыя qemu_in_coroutine вяртала true, калі насамрэч павінна была вярнуць false. Гэта прыводзіла да выкліку emscripten_yield, вышэй якога па стэку не было emscripten_coroutine_next, Стэк разгортваўся да самага верху, але ніякіх setTimeout, як я ўжо казаў, не выстаўлялася.

Кодагенерацыя JavaScript

А вось, уласна, і абяцанае "проворачивание фаршу назад". Насамрэч няма. Вядома, калі запусціць у браўзэры Qemu, а ў ім – Node.js, то, натуральна, пасля кодогенерации у Qemu мы атрымаем зусім не той JavaScript. Але ўсё-ткі, якое-ніякае, а зваротнае пераўтварэнне.

Для пачатку крыху пра тое, як працуе Qemu. Адразу прашу мне дараваць: я не з'яўляюся прафесійным распрацоўшчыкам Qemu і мае высновы могуць быць месцамі памылковыя. Як гаворыцца, "меркаванне студэнта не абавязана супадаць з меркаваннем выкладчыка, аксіяматыкай Пеана і здаровым сэнсам". У Qemu ёсць нейкая колькасць падтрымліваемых гасцявых архітэктур і для кожнай ёсць каталог накшталт target-i386. Пры зборцы можна ўказаць падтрымку некалькіх гасцявых архітэктур, але ў выніку атрымаецца проста некалькі бінарнікаў. Код для падтрымкі гасцёўні архітэктуры, у сваю чаргу, генеруе нейкія ўнутраныя аперацыі Qemu, якія TCG (Tiny Code Generator) ужо ператварае ў машынны код хаставой архітэктуры. Як сцвярджаецца ў readme-файле, які ляжыць у каталогу tcg, першапачаткова гэта была частка звычайнага кампілятара C, якую потым прыстасавалі пад JIT. Таму, напрыклад, target architecture у тэрмінах гэтага дакумента – гэта ўжо не гасцёўня, а хаставая архітэктура. У нейкі момант з'явіўся яшчэ адзін кампанент – Tiny Code Interpreter (TCI), які павінен выконваць код (практычна тыя ж унутраныя аперацыі) у адсутнасці кодагенератара пад пэўную хаставую архітэктуру. Насамрэч, як гаворыцца ў яго дакументацыі, гэты інтэрпрэтатар можа не заўсёды працаваць гэтак жа добра, як JIT-кодагенератар не толькі колькасна ў плане хуткасці, але і якасна. Хаця не ўпэўнены, што яго апісанне цалкам актуальнае.

Спачатку я спрабаваў зрабіць паўнавартасны TCG backend, але хутка заблытаўся ў зыходніках і не зусім зразумелым апісанні інструкцый байткода, таму вырашыў абгарнуць інтэрпрэтатар TCI. Гэта дало адразу некалькі пераваг:

  • пры рэалізацыі кодагенератара можна было глядзець не ў апісанне інструкцый, а ў код інтэрпрэтатара
  • можна генераваць функцыі не для кожнага сустрэтага блока трансляцыі, а напрыклад, толькі пасля сотага выканання
  • у выпадку змены згенераванага кода (а такое, відаць, магчыма, мяркуючы па функцый з назвамі, якія змяшчаюць слова patch) мне трэба будзе інвалідаваць згенераваны JS-код, але ў мяне хаця б будзе, з чаго яго перагенераваць

На рахунак трэцяга пункта не ўпэўнены, што патчынг магчымы пасля таго, як код будзе ў першы раз выкананы, але і першых двух пунктаў дастаткова.

Першапачаткова код генераваўся ў выглядзе вялікага switch па адрасе зыходнай інструкцыі байткода, але потым, успомніўшы артыкул пра Emscripten, аптымізацыю генераванага JS і relooping, вырашыў генераваць больш чалавечы код, тым больш, што па-эмпірычнаму атрымлівалася, што адзіная кропка ўваходу ў блок трансляцыі - гэта яго пачатак. Сказана – зроблена, праз некаторы час атрымаўся кодагенератар, які генеруе код з if-амі (хоць і без цыклаў). Але вось няўдача, ён падаў, выдаючы паведамленне аб тым, што інструкцыя аказалася нейкай няправільнай даўжыні. Пры гэтым апошняя інструкцыя на гэтым узроўні рэкурсіі была brcond. Добра, дадам ідэнтычную праверку ў генерацыю гэтай інстукцыі да рэкурсіўнага выкліку і пасля і… не адна з іх не выканалася, але пасля свіча па assert-у ўсё ж зваліліся. У рэшце рэшт, вывучыўшы згенераваны код, я зразумеў, што пасля switch-а паказальнік на бягучую інструкцыю перазагружаецца са стэка і, верагодна, пераціраецца генераваным JavaScript-кодам. Так яно і аказалася. Павелічэнне буфера з аднаго мегабайта да дзесяці ні да чаго не прывяло, і стала зразумела, што кодагенератар бегае па крузе. Прыйшлося правяраць, што мы не выйшлі за межы бягучага TB, і калі выйшлі, то выдаваць адрас наступнага TB са знакам мінус, каб можна было працягнуць выкананне. Да таго ж гэта вырашае праблему "якія згенераваныя функцыі інвалідаваць, калі памяняўся вось гэты кавалачак байткода?" - інвалідаваць трэба толькі тую функцыю, якая адпавядае гэтаму блоку трансляцыі. Дарэчы, хоць адладжваў я ўсё ў 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-бітных гасцявых архітэктур: вы прама зараз можаце палюбавацца на віслы ў браўзэры на этапе загрузкі Лінукс для архітэктуры MIPS

Што можна яшчэ зрабіць

  • Паскорыць эмуляцыю. Нават у рэжыме JIT яно працуе, падобна, павольней, чым Virtual x86 (затое патэнцыйна ёсць цэлы Qemu з вялікай колькасцю эмуляванага жалеза і архітэктур)
  • Зрабіць нармальны інтэрфейс - вэб-распрацоўшчык з мяне, прама скажам, так сабе, таму пакуль перарабіў стандартную абалонку Emscripten, як здолеў
  • Паспрабаваць запусціць больш складаныя функцыі Qemu – сетка, міграцыю VM і г.д.
  • UPD: трэба будзе аддаць у апстрым Emscripten свае нешматлікія напрацоўкі і баг-рэпарты, як гэта рабілі папярэднія партыроўшчыкі Qemu і іншых праектаў. Дзякуй ім за тое, што была магчымасць няяўна карыстацца іх укладам у Emscripten у рамках сваёй задачы.

Крыніца: habr.com

Дадаць каментар