Qemu.js с поддръжка на JIT: може да се направи обратното на каймата

Преди няколко години Фабрис Беллар написа jslinux — емулятор на компютър, написан на JavaScript. След това имаше поне още Virtual x86. Но, доколкото знам, всички те бяха интерпретатори, докато написаният много по-рано от същия Фабрис Беллар Qemu, а и вероятно всеки уважаващ себе си съвременен емулятор, използва JIT-компилация на гостовия код в кода на хостовата система. Помислих, че е време да реализираме обратната задача, свързана с тази, която решават браузърите: JIT-компилация на машинен код в JavaScript, за което най-логично ми се видя да портируем Qemu. Изглежда, че защо точно Qemu, има и по-прости и удобни за потребителя емулятори — като VirtualBox, например — инсталирай и работи. Но Qemu има няколко интересни особености

  • открити изходни кодове
  • възможност да работи без ядрен драйвър
  • възможност да работи в режим на интерпретатор
  • поддръжка на много на брой хостови и гостови архитектури

Относно третата точка, сега вече мога да поясня, че всъщност в режим TCI се интерпретират не самите гостови машинни инструкции, а полученият от тях байткод, но това не променя същността — за да се компилира и стартира Qemu на нова архитектура, ако имаш късмет, е достатъчен C компилатор — написването на кодогенератор може да се отложи.

И ето, след две години неспешно ровене във времето от свободното ми време в източниците на Qemu, се появи работещ прототип, в който вече може да се стартира, например, Kolibri OS.

Какво е Emscripten

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

И така, изглежда, че ето изходните кодове, ето Emscripten — просто компилирай. Но има и библиотеки, от които Qemu зависи, и библиотеки, от които зависият тези библиотеки и т.н., а една от тях — libffi, от която зависи glib. В интернет имаше слухове, че в голямата колекция от библиотеки под Emscripten има и такава, но по-скоро се вярваше с труд: от една страна, новият компилатор не я компилираше, от друга страна, това е твърде нискоуровнева библиотека, за да може просто така да се компилира в JS. И не става въпрос само за асемблерни вмъквания — вероятно, ако се изкриви, за някои calling conventions може и без тях да се оформят нужните аргументи на стека и да се извика функция. Само че Emscripten е хитра работа: за да изглежда генерираният код познат на оптимизатора на JS двигателя в браузъра, се използват някои трикове. По-специално, така нареченото relooping — кодогенераторът на базата на полученото LLVM IR с определени абстрактни инструкции за преходи се опитва да възстанови правдоподобни if-ове, цикли и т.н. А аргументите на функциите как се предават? Разбира се, като аргументи на JS функции, тоест по възможност не през стека.

В началото идеята беше просто да напиша замяна на libffi с JS и да пусна стандартните тестове, но в крайна сметка се заплетох в това как да направя своите заглавни файлове, за да работят с съществуващия код — какво да се прави, както се казва, "Не е ли задачата толкова сложна, не сме ли ние толкова тъпи". Прихвърлих се да портвам libffi на още една архитектура, ако може да се изрази така — за щастие, в Emscripten има както макроси за inline assembly (на JavaScript, ага — каква архитектура, такъв и асемблер), така и възможност да се стартира генерираният на ходу код. Обобщавайки, след като прекарах известно време с платформено зависимите фрагменти на libffi, получих нещо, което компилира, и го пуснах на първия удобен тест. За моя изненада тестът премина успешно. Удивен от гениалността си — шега ли е, сработи от първия опит — все още несигурен, се заех отново да погледна полученото кодово, да оценя накъде да се копае по-нататък. Тук втори път ми замръзна всичко — единственото, което правеше функцията ми, ffi_call — беше да докладва за успешен извикване. Самото извикване не е съществувало. Така изпратих първия си pull request, който поправяше очевидна грешка в теста, разбираема за всеки олимпиадист — веществените числа не трябва да се сравняват като a == b и дори като a - b < EPS — трябва да не забравя и модула, иначе 0 ще се окаже много равно на 1/3… Ами, получи се нещо като порт на libffi, който преминава най-простите тестове и с който се компилира glib — реших, ако се наложи, после ще допиша. Изпреварвайки събитията, ще кажа, че, както се оказа, компилаторът дори не включи финалния код на функцията libffi.

Но, както вече казах, има някои ограничения и сред свободното използване на разнообразно неопределено поведение на повърхността се появи неприятен аспект — JavaScript по проект не поддържа многопоточност с обща памет. В принципе, това може да се нарече дори добра идея, но не и за портироване на код, чиято архитектура е свързана с потоци от C. Всъщност в 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 — те хващат някои видове неопределено поведение като опити за достъп до не подравнен адрес (което изобщо не съвпада с кода за 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... У-у-у, е-ее, то стартира, нормално преминава инициализацията и преминава напред, минавайки покрай изходния бъг без единствено предупреждение за неправилен достъп до паметта, да не говорим за падания. На такова нещо животът не ме беше подготвил - програмата, която пада, спира да пада, когато се стартира под Valgrind. Какво беше това - загадка. Моята хипотеза е, че след като в околността на текущата инструкция след падането при инициализация gdb е показал работа. memset-а с валиден указател с използването то ли mmx, то ли xmm регистри, то възможно е да е била някаква грешка в подравняването, макар и да се вярва слабо.

О-кей, Valgrind тук не помага. А тук започна най-дразнещото — всичко сякаш стартира, но пада по абсолютно неизвестни причини за събитие, което е могло да се случи милиони инструкции назад. Дълго време дори не беше ясно как да се подходи. В крайна сметка, все пак, трябваше да седна и да отстраня проблема. Печатът на това, с което беше презаписан заглавието, показа, че това изглежда не е число, а по-скоро някакви бинарни данни. И, о чудо, тази бинарна строка беше намерена в файла с BIOS — тоест с достатъчна увереност можехме да кажем, че това е било препълване на буфера и дори да разберем какво се записва в този буфер. А след това, както се казва — в 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 се извършва преход по различни адреси, а и байткодът се генерира принципиално различен: в едната версия има инструкция за извикване на C функция-хелпер, а в другата — няма. След гуглене на инструкциите и изучаване на кода, който тези инструкции транслира, стана ясно, че, от една страна, непосредствено преди нея в регистъра 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 ядрото на конзолата с опцията -nographic, а от браузъра нямаше никакъв изход в терминала, откъдето беше стартиран emrun, не постъпваше. Тоест не е ясно: процесорът не работи или изходът на графиката. А после ми хрумна да почакам малко. Оказа се, че "процесорът не спи, а просто бавно мига", и след около пет минути ядрото изхвърли на конзолата поредица съобщения и продължи да си стои. Стана ясно, че процесорът в общи линии работи и трябва да се копае в кода за работа с SDL2. За съжаление не умея да ползвам тази библиотека, затова на места трябваше да действам наслуки. В един момент на екрана мигна строчка parallel0 на синьо, което наведе на известни мисли. В крайна сметка се оказа, че работата беше в това, че Qemu отваря няколко виртуални прозорци в един физически прозорец, между които може да се превключва с Ctrl-Alt-n: в нативната версия работи, в Emscripten — не. След избавянето от излишните прозорци с помощта на опциите -monitor none -parallel none -serial none и указанието принудително да се прерисува целия екран на всеки кадър, всичко внезапно заработи.

Корутини

И така, еймуляцията в браузера работи, но не може да се стартира нищо интересно от еднодисковия, защото няма блочно входно-изходно устройство — необходимо е да се реализира поддръжка на корутини. В Qemu вече има няколко backend-а за корутини, но заради особеностите на JavaScript и кодогенератора Emscripten не може просто да се вземе и да се започне да се жонглира със стековете. Изглежда, че "всичко е загубено, гипсът се сваля", но разработчиците на Emscripten вече са се погрижили за всичко. Реализирано е доста забавно: а какво ще кажете да наречем подозрителен повик на функция като emscripten_sleep , и няколко други, които използват механизма Asyncify, а също така повиквания по указател и повиквания на всяка функция, при които по-долу в стека може да се случи един от предходните два случая. А сега преди всяко подозрително повикване ще отделим async контекст и веднага след повикването — ще проверим дали е състояло асинхронно повикване, и ако е, тогава ще запазим всички локални променливи в този async контекст, ще посочим на коя функция да предадем контрола, когато е необходимо да продължим изпълнението, и ще излезем от текущата функция. Там вече има поле за изследване на ефекта разтваряне — за нуждите на продължаването на изпълнението на кода след връщането от асинхронно повикване, компилаторът генерира "рязове" на функцията, започващи след подозрителното повикване — ето така: ако има n подозрителни повиквания, то функцията ще бъде разтворена някъде на n/2 пъти — това е, ако не броим, че в оригиналната функция трябва след всяко потенциално асинхронно повикване да се добави запазването на част от локалните променливи. Впоследствие дори се наложи да пиша несложен скрипт на Питон, който по зададено множество особено разтворени функции, които предполагам, че "не пропускат асинхронността през себе си" (тоест в тях не се задейства навиването на стека и всичко това, което току-що описах), указва повикванията по указатели в кои функции трябва да се игнорират от компилатора, за да не се разглеждат тези функции като асинхронни. Иначе JS файловете под 60 Мб — това вече е очевидно прекалено — нека поне бъдат 30. Въпреки това, веднъж настройвах скрипта за компилиране и случайно изхвърлих опциите на линковчика, сред които беше и -O3. Стартирам генерирания код, и Chromium изяжда памет и спира. След това случайно погледнах какво се опитва да зареди... Какво да кажа, и аз ще засядам, ако ми кажат да изучавам и оптимизирам JavaScript с 500+ МБ.

За съжаление, проверките в кода на библиотеката, поддържаща Asyncify, не съвпадаха наистина с longjmp-ите, използвани в кода на виртуалния процесор, но след малък патч, който деактивира тези проверки и принудително възстановява контекстите, сякаш всичко е наред, кодът заработи. И тук започнаха странностите: понякога се активираха проверките в кода за синхронизация — точно тези, които аварийно завършват кода, ако логиката на изпълнението предполага, че той трябва да блокира — някой се опитваше да захваща вече захванат мютекс. За щастие, това не беше логически проблем в сериализирания код — просто използвах стандартната функционалност на основния цикъл, предоставена от Emscripten, но понякога асинхронният извикване напълно разгръщаше стека, а в този момент сработваше setTimeout от основния цикъл — така кодът навлизаше в итерация на основния цикъл, без да излиза от предишната итерация. Преписах го на безкраен цикъл и 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. Следователно, например, целевата архитектура в термини на този документ — това вече не е гост, а хост архитектура. В определен момент се е появил още един компонент — Tiny Code Interpreter (TCI), който трябва да изпълнява кода (почти същите вътрешни операции) в отсъствието на кодогенератор под конкретна хост архитектура. Всъщност, както се казва в неговата документация, този интерпретатор не винаги може да работи толкова добре, колкото JIT-кодогенераторът, не само количествено по отношение на скоростта, но и качествено. Въпреки че не съм сигурен, че описанието му е напълно актуално.

Първоначално се опитвах да направя пълноценен TCG бекенд, но бързо се обърках в изходния код и не съвсем разбираемото описание на инструкциите на байткода, затова реших да обгърна интерпретатора TCI. Това предостави веднага няколко предимства:

  • при реализирането на кодогенератора можеше да се гледа не в описанието на инструкциите, а в кода на интерпретатора
  • Може да се генерират функции не за всеки срещнат блок на транслация, а например, само след стотното изпълнение.
  • В случай че се промени генерираният код (което очевидно е възможно, съдейки по функциите с имена, съдържащи думата patch), ще трябва да инвалидизирам генерирания JS код, но поне ще имам от какво да го перегенерирам.

По третия пункт не съм сигурен, че патчингът е възможен след първото изпълнение на кода, но и първите два пункта са достатъчни.

Първоначално кодът се генерираше под формата на голям switch по адреса на входната инструкция на байткода, но след като си спомних статията за Emscripten, оптимизацията на генерирания JS и relooping, реших да генерирам по-човешки код, особено след като емпирично се установи, че единствената точка на вход в блока на транслация е началото му. Както е казано, така и направено, след известно време получих кодогенератор, генериращ код с if-ове (макар и без цикли). Но ето че възникна проблем, той падаше, показвайки съобщение, че инструкцията се оказа с неправилна дължина. При това последната инструкция на това ниво на рекурсия беше. brcond. Добре, ще добавя идентична проверка в генерирането на тази инструкция преди рекурсивния извикване и след него и… нито една от тях не се изпълни, но след switch-a на assert-а все пак се сринаха. В крайна сметка, изучавайки генерирания код, осъзнах, че след switch-a указателят на текущата инструкция се презарежда от стека и вероятно се изтрива от генерирания 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 под формата на блог: към всеки преминат "нивото" е добавен разширен коментар на български. Всъщност, тази статия до известна степен представлява преразказ на извода. git log.

Може да опитате всичко това тук. (внимавайте, трафик).

Какво вече работи:

  • Работи виртуален процесор x86
  • Има работещ прототип на JIT-генератор на код от машинен код в JavaScript
  • Има основа за компилиране на други 32-битови гостуващи архитектури: можете да се насладите на зареждащия се Linux за архитектура MIPS, който се затлачва в браузъра.

Какво още може да се направи

  • Да се ускори емуляцията. Дори в режим JIT, изглежда че работи по-бавно от Virtual x86 (но потенциално има цял Qemu с множество емулирани хардуер и архитектури)
  • Да се направи нормален интерфейс — не съм особено добър уеб разработчик, затова за момента преработих стандартната обвивка на Emscripten, както успях.
  • Да опитате да стартирате по-сложни функции на Qemu — мрежа, миграция на VM и т.н.
  • UPD: Трябва да предам в апстрийма на Emscripten моите малобройни разработки и бъг-репорти, както направиха предишните портировачи на Qemu и други проекти. Благодаря им за възможността да ползвам тяхния принос в Emscripten в рамките на моята задача.

Източник: habr.com

Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри 🔥 Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри | ProHoster