Qemu.js s podporo JIT: mletje je vendarle mogoče ustaviti

Pred nekaj leti je Fabrice Bellard napisal jslinux — emulátor PC, napisan v JavaScriptu. Po tem je bilo še vsaj Virtual x86. A vsi so, kolikor vem, bili interpreterji, medtem ko Qemu, ki ga je napisal že prej prav tako Fabrice Bellard, zagotovo uporablja JIT-kompilacijo gostiteljskega kode. Zdel se mi je pravi čas, da realiziram obratno nalogo glede na to, kar rešujejo brskalniki: JIT-kompilacijo strojnega kode v JavaScript, kar se mi je logično zdelo, da je portiranje Qemu. Zdi se, zakaj prav Qemu, obstajajo preprostejši in uporabniku prijaznejši emulatorji — na primer, VirtualBox — namestiš in deluje. A Qemu ima nekaj zanimivih značilnosti

  • odprti izvorni kodi
  • možnost delovanja brez jedrskega gonilnika
  • možnost delovanja v načinu interpreterja
  • podpora za široko paleto gostiteljskih in gostovalnih arhitektur

Kar se tiče tretje točke, zdaj pa že lahko pojasnim, da se v načinu TCI dejansko ne interpretirajo sami gostiteljski strojni ukazi, ampak bajtkod, pridobljen iz njih, ampak to ne spremeni bistva — za sestavljanje in zagon Qemu na novi arhitekturi, če imaš srečo, zadostuje compiler C — pisanje generatorja kode lahko odložiš.

In tako, po dveh letih počasnega pečenja izvorne kode Qemu, se je pojavil delujoč prototip, v katerem že lahko zaženeš, na primer, Kolibri OS.

Kaj je Emscripten

V današnjem času je nastalo veliko compilerjev, katerih končni rezultat dela je JavaScript. Nekateri, kot je TypeScript, so bili sprva zasnovani kot najboljši način pisanja za splet. Hkrati pa je Emscripten način, da vzameš obstoječo kodo v C ali C++ in jo skompiliraš v obliko, razumljivo brskalniku. Na tej strani je zbrano kar nekaj portov znanih programov: ovde, na primer, lahko si ogledaš PyPy — mimogrede, trdi se, da že imajo JIT. Dejansko pa ni mogoče preprosto skompilirati katere koli programe in jih zagnati v brskalniku — obstaja vrsta posebnosti, s katerimi se je treba sprijazniti, kot pravi napis na tej strani "Emscripten se lahko uporablja za kompilacijo skoraj vsakega prenosnega C/C++ код в JavaScript. То есть существует ряд операций, которые являются неопределённым поведением по стандарту, но обычно работают на x86 — к примеру, невыровненный доступ к переменным, который на некоторых архитектурах вообще запрещён. В общем, Qemu — программа кроссплатформенная и, хотелось верить, и так не содержит большого количества неопределённого поведения — бери и компилируй, потом немного повозиться с JIT — и готово! Но не тут-то было…

Prvi poskus

Вообще говоря, я не первый, кому пришла в голову идея портировать Qemu на JavaScript. На форуме ReactOS задавался вопрос, возможно ли это с помощью Emscripten. Ещё раньше ходили слухи, что это сделал лично Фабрис Беллар, но речь шла о jslinux, который, насколько мне известно, является как раз попыткой вручную добиться на JS достаточной производительности и написан с нуля. Позднее был написан Virtual x86 — к нему были выложены необфусцированные исходники, и, как утверждалось, большая "реалистичность" эмуляции позволила использовать SeaBIOS в качестве firmware. Кроме того, была как минимум одна попытка портировать Qemu с помощью Emscripten — это пытался сделать socketpair, но разработка, насколько я понял, была заморожена.

Итак, казалось бы, вот исходники, вот Emscripten — бери и компилируй. Но есть ещё и библиотеки, от которых Qemu зависит, и библиотеки, от которых зависят те библиотеки и т.д., причём одна из них — libffi, ki je odvisna od gliba. Na internetu so krožile govorice, da v veliki zbirki vrat knjižnic pod Emscripten obstaja tudi ona, vendar je bilo to težko verjeti: najprej, novi prevajalnik je ni sestavljal, in drugič, to je preveč nizkonivojska knjižnica, da bi jo kar tako preprosto prevesti v JS. In stvar ni le v vstavkih v asemblerju — verjetno, če se malce igramo, za nekatere konvencije klicanja lahko brez njih oblikujemo potrebne argumente na stogu in pokličemo funkcijo. Samo Emscripten je zapletena stvar: da bi generiran koda izgledala običajno za optimizator JS-motorja brskalnika, se uporabljajo nekateri triki. Zlasti tako imenovano relooping — generator kode poskuša na podlagi prejetega LLVM IR z nekaterimi abstraktnimi navodili skokov ponovno ustvariti verjetne if-izjave, zanke itd. No, in kako se argumenti funkcij prenašajo? Seveda, kot argumenti JS-funkcij, torej čim bolj nepreklicno ne preko stoga.

Na začetku je bila misel, da bi preprosto napisali zamenjavo libffi za JS in zagnali standardne teste, vendar sem se nazadnje zapletel v to, kako pripraviti svoje glave, da bi delovale z obstoječo kodo — kar najti, kot se reče, "Ali so naloge tako zapletene, ali smo mi tako neumen." Moral sem portirati libffi na še eno arhitekturo, če lahko tako rečem — na srečo, v Emscripten so tako makroji za inline assembly (na javasript, da — no, kakšna arhitektura, takšen tudi asembler), kot tudi možnost za zagon kode, generirane na licu mesta. Skratka, po nekaj časa preživljenem z fragmenti libffi, ki so odvisni od platforme, sem dobil neko kodo, ki se lahko prevaja, in jo zagnal na prvem testu, ki mi je prišel pod roke. Na moje presenečenje, test je uspešno opravil. Presenečen nad svojo genialnostjo — šalo na stran, delovalo je ob prvem zagonu — sem, še vedno ne verjamejo svojim očem, ponovno pogledal, da bi ocenil, kam naprej kopati. Tukaj sem bil ponovno presenečen — edina stvar, ki jo je moja funkcija ffi_call — je bila obvestitev o uspešnem klicu. Samega klica ni bilo. Tako sem poslal svoj prvi pull request, ki je popravljal očitno napako v testu, ki je bila jasna vsakemu olimpijskemu tekmovalcu — realnih števil ne bi smeli primerjati kot a == b in celo kot a - b < EPS — ne smem zaboraviti ni modul, inace bi 0 mogao ispasti ravno 1/3… Uglavnom, napravio sam neku libffi prikaz, koji prolazi najosnovnije testove i s kojim se kompilira glib — odlučio sam, ako bude potrebno, dodat ću kasnije. unaprijed ću reći da, kako se ispostavilo, u konačni kod funkcije libffi kompajler čak nije uključio.

No, kao što sam već rekao, postoje neka ograničenja, a među slobodnim korištenjem raznovrsnog neodređenog ponašanja ušuljala se jedna nelagoda — JavaScript po dizajnu ne podržava višedretveno programiranje s zajedničkom memorijom. U principu, to se obično može čak nazvati dobrom idejom, ali ne i za prenos koda čija je arhitektura vezana uz C niti. U načelu, u Firefoxu se izvode eksperimenti za podršku zajedničkim radnicima, a implementacija pthread za njih u Emscripten-u postoji, ali nije mi se željelo oslanjati na to. Morao sam polagano iskorištavati višedretvenost iz Qemu koda — to jest, locirati gdje se pokreću niti, iznijeti telo petlje koja se izvodi u toj niti u zasebnu funkciju, i naizmjenično pozivati takve funkcije iz osnovne petlje.

Drugi poskus

U nekom trenutku, postalo je jasno da se to samo poteže, i da bezsistematsko raspršivanje hiperskobima po kodu neće doneti ništa dobro. Zaključak: treba nekako sistematizirati proces dodavanja hiperskoba. Stoga je uzeta sveža u to vreme verzija 2.4.1 (ne 2.5.0, jer, ko zna, možda se tu još nalaze neotkriveni bagovi nove verzije, a svojih bagova mi i te kako dovoljno), a prvo što je sigurno prepisano thread-posix.c. Pa, da kažem, kako sigurno: ako je neko pokušao izvršiti operaciju koja dovodi do blokade, odmah se poziva funkcija abort() — naravno, ovo nije odmah rešilo sve probleme, ali barem je nekako prijatnije nego tiho dobijati nekonzistentnost podataka.

Uopšte, prilikom portiranja koda na JS veoma pomažu opcije Emscripten -s ASSERTIONS=1 -s SAFE_HEAP=1 — one hvataju neke vrste neodređenog ponašanja poput pristupanja neizravnoj adresi (što se uopšte ne slaže s kodom za typed arrays poput HEAP32[addr >> 2] = 1) ili pozivanja funkcije s pogrešnim brojem argumenata.

Nadal, napake poravnanja so posebna tema. Kot sem že rekel, v Qemu obstaja "degenerirana" interpretacijska koda za generacijo TCI (tiny code interpreter), in da sestaviš ter zaženeš Qemu na novi arhitekturi, je dovolj, če imaš C prevajalnik. Ključne besede "če bo sreča". Meni pa sreča ni bila naklonjena, saj se je izkazalo, da TCI pri analizi svojega bajtnega kode uporablja neurejen dostop. To pomeni, da se na različnih arhitekturah, kot so ARM in druge, ki zahtevajo urejen dostop, Qemu kompilira, ker imajo normalen TCG-bakend, ki generira naravno kodo, vprašanje pa je, ali bo TCI deloval na njih. Vendar pa se je izkazalo, da je v dokumentaciji za TCI to eksplicitno navedeno. Na koncu so v kodo dodali klice funkcij za neurejeno branje, ki so se pojavili na drugem delu Qemu.

Uničenje kupčka

Nazadnje je bil neurejen dostop v TCI popravljeni, glavni cikel je bil ustanovljen, ki je zaporedno klical procesor, RCU in še kaj malega. In tako zažene Qemu z možnostjo -d exec,in_asm,out_asm, kar pomeni, da je potrebno povedati, kateri bloki kode se izvajajo, prav tako ob prevajanju zapisati, kateri je bil gostujoči kod in kateri je postal gostiteljski kod (v tem primeru, bajtkod). Zažene se, izvede nekaj blokov prevajanja, napiše ostalo debug sporočilo, da se bo zdaj zagnal RCU in… pade po abort() znotraj funkcije free(). Po raziskovanju funkcije free() je bilo ugotovljeno, da je v glavi bloka kupčka, ki leži v osmih bajtih, ki predhodijo dodeljeni pomnilniku, namesto velikosti bloka ali česa podobnega ostal odpadek.

Unich bøsen — hvordan dog… I sådanne tilfælde er der et nyttigt værktøj — at samle en native binær fra (hvis muligt) de samme kilder og køre det under Valgrind. Efter noget tid var binæren klar. Jeg starter med de samme indstillinger — den fejler stadig ved initialisering, uden at nå frem til den egentlige udførelse. Det er selvfølgelig ubehageligt — åbenbart var kilderne ikke helt de samme, hvilket ikke er overraskende, da configure havde fundet et par andre indstillinger, men jeg har jo Valgrind — først vil jeg rette denne fejl, og så, hvis jeg er heldig, vil den oprindelige fejl også vise sig. Jeg kører det hele under Valgrind… Øh-øh-øh, åh-åh-åh, det startede, gik normalt gennem initialiseringen og fortsatte videre forbi den oprindelige fejl uden et eneste advarsel om forkert hukommelsesadgang, for ikke at tale om nedbrud. Til dette var livet, som man siger, ikke forberedt — et program, der falder, stopper med at falde, når det køre under Valgrind. Hvad var det — et mysterium. Min hypotese er, at da der i nærheden af den aktuelle instruktion efter krakket under initialiseringen, viste gdb arbejde memset-en med en gyldig peger ved hjælp af enten mmx, eller xmm registre, så var det måske en eller anden form for alignment-fejl, selvom det stadig virker usandsynligt.

О-кей, 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. Pojavila se hipoteza: ne dolaze prekidi tajmera (ili čak svi prekidi). I zaista, ako se od nativne verzije, koja je iz nekog razloga radila, isključe prekidi, dobijemo sličnu sliku. Međutim, rešenje nije bilo u tome: upoređivanje tragova izvršavanja koje sam dobio sa prethodno navedenom opcijom pokazalo je da se putanje izvršavanja razdvajaju vrlo rano. Treba reći da poređenje snimljenog izlaza pomoću pokretača emrun i izlaza nativne verzije — nije baš mehanički proces. Ne znam tačno kako se program pokrenut u pretraživaču povezuje sa emrun, ali se neke linije u izlazu ispostavljaju da su presložene, tako da razlika u diff-u — nije još jedan razlog da se smatra da su se putanje razdvojile. U svakom slučaju, postalo je jasno da prema uputstvu ljmpl dolazi do prelaska na različite adrese, a i bajtkod se generiše suštinski različito: u jednom postoji instrukcija poziva C funkcije-helpera, u drugom — ne. Nakon pretrage instrukcija i proučavanja koda koji te instrukcije prevodi, jasno je postalo da, prvo, neposredno do nje u registar cr0 bila je zapisana — takođe pomoću helpera — koja prevodi procesor u zaštićeni režim, a drugo, js-verzija nikada nije prešla u zaštićeni režim. A radi se u stvari o tome da još jedna karakteristika Emscripten-a ne trpi kod poput implementacije instrukcije call u TCI, koja svake pokazivače na funkcije dovodi do tipa long long f(int arg0, .. int arg9) — funkcije moraju biti pozvane sa pravilnim brojem argumenata. Kada se ovo pravilo prekrši, u zavisnosti od podešavanja za debagovanje, program ili padne (što je dobro), ili poziva potpuno drugu funkciju (što će biti tužno za debagovanje). Postoji i treća opcija — uključiti generisanje omotača koji dodaju/uklanjaju argumente, ali ukupno ti omotači zauzimaju veoma značajno mesto, s obzirom na to da mi zapravo treba nešto više od stotinu omotača. Samo ovo je veoma tužno, ali se ispostavila ozbiljnija problematika: u generisanom kodu funkcija-omotača argumenti su se konvertovali, konvertovali, samo što funkcija sa generisanim argumentima ponekad nije bila pozvana — baš kao u mojoj implementaciji libffi. Dakle, neki helperi jednostavno nisu izvršeni.

Na srečo Qemu vsebuje strojno berljive sezname pomočnikov v obliki glave, kot je

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

Uporabljajo se na precej zabaven način: najprej se makrosi predefinirajo na najbolj bizarne načine DEF_HELPER_n, nato se vključijo helper.h. Do te mere, da se makros razširi v inicializator strukture in vejico, nato pa se določi polje, namesto elementov pa so #include <helper.h> Na koncu se je končno ponujala priložnost, da preizkusim knjižnico pyparsing, in napisan je bil skript, ki generira ovitke prav za tiste in tiste funkcije, za katere je to potrebno.

In tako je po tem procesor nekako začel delovati. Nekako, ker se zaslon ni nikoli inicializiral, čeprav je v naravni različici uspelo zagnati memtest86+. Tukaj je treba pojasniti, da je koda blokovnega vnosa/izvoza Qemu napisana z uporabo korutin. V Emscripten obstaja precej zapletena izvedba, vendar jo je bilo še treba podpreti v kodi Qemu, medtem ko se lahko procesor zdaj že odpravlja: Qemu podpira možnosti -kernel, -initrd, -append, s katerimi lahko naložite Linux ali, na primer, memtest86+, brez uporabe blokovnih naprav. A naleteli smo na težavo: v naravni različici je bilo mogoče opazovati izhod Linux jedra na konzoli z opcijo -nographic, iz brskalnika pa ni bilo nobenega izhoda v terminal, od koder je bil zagnan emrun, ni prišlo. Torej ni jasno: procesor ne deluje ali je težava v grafiki. Potem sem se spomnil, da bi moral malo počakati. Izkazalo se je, da "procesor ne spi, temveč samo počasi utripne", in čez približno pet minut se je jedro pojavilo na konzoli s kupom sporočil in še naprej zamrlo. Postalo je jasno, da procesor na splošno deluje, in je treba iskati v kodi dela z SDL2. Te knjižnice žal ne znam uporabljati, zato sem moral včasih ravnati na slepo. V nekem trenutku je na zaslonu utripnila vrstica parallel0 na modrem ozadju, kar je vzbudilo nekaj misli. Na koncu se je izkazalo, da je težava v tem, da Qemu odpira več virtualnih oken v enem fizičnem oknu, med katerimi lahko preklapljate s Ctrl-Alt-n: v naravni različici deluje, v Emscripten pa ne. Po tem, ko sem se rešil od odvečnih oken s pomočjo možnosti -monitor none -parallel none -serial none in navedbe, da je treba prisilno prepleskati celotno zaslon na vsakem okvirju, je vse naenkrat delovalo.

Korutine

Torej, emulacija v brskalniku deluje, vendar ni mogoče zagnati nič zanimivega za eno disketno napravo, ker ni blokovnega vnosa/izvoda — potrebno je realizirati podporo za korutine. V Qemu je že nekaj coroutine backend-ov, vendar zaradi posebnosti JavaScripta in generatorja kode Emscripten ni mogoče preprosto vzeti in začeti žonglirati s skladovnimi okviri. Zdelalo se je, da "je vse izgubljeno, gips se odstranjuje", toda razvijalci Emscripten so že poskrbeli za vse. Realizirano je precej zabavno: pojdimo in imenujmo domnevni klic funkcije, kot je emscripten_sleep , in nekaj drugih, ki uporabljajo mehanizem Asyncify, ter klice po kazalcu in klice katere koli funkcije, kjer lahko spodaj v skladovnem okviru nastane eden od prejšnjih dveh primerov. In zdaj pred vsakim domnevnim klicem dodelimo async context, takoj po klicu pa preverimo, ali je prišlo do asinhronega klica, in če se je, shranimo vse lokalne spremenljivke v to async context, navedemo, na katero funkcijo prenesti nadzor, ko bo treba nadaljevati izvedbo, in zapustimo trenutno funkcijo. Tukaj je prosti prostor za raziskovanje učinka destabilizacije — za potrebe nadaljevanja izvajanja kode po vrnitvi iz asinhronega klica prevajalnik generira "kos vrstice" funkcije, ki se začnejo po domnevnem klicu — tako: če je n domnevnih klicev, bo funkcija destabilizirana nekje od n/2-krat — to je še, če ne upoštevamo, da je treba v izvorno funkcijo po vsakem potencialno asinhronem klicu dodati shranjevanje dela lokalnih spremenljivk. Kasneje sem celo moral napisati preprost skript v Pythona, ki na podlagi določenega nabora posebej destabiliziranih funkcij, ki domnevno "ne prepuščajo asinhronosti skozi sebe" (to pomeni, da se v njih ne prižge razvijanje sklada in vse to, kar sem ravnokar opisal), kaže, katerih klicev po kazalcih je treba prezreti prevajalniku, da se te funkcije ne obravnavajo kot asinhrone. Ker so JS-datoteke pod 60 MB — to je že očitno preveč — naj bodo vsaj 30. Čeprav sem enkrat nastavljal skript za gradnjo in slučajno izključil možnosti povezovalnika, med katerimi je bila tudi -O3. Zagonim generirano kodo, in Chromium požre spomin in pade. Naključno sem nato pogledal, kar je poskušal naložiti... No, kaj naj rečem, tudi jaz bi obstal, če bi me prosili, naj premišljeno preučim in optimiziram JavaScript na 500+ MB.

Na žalost preverjanja v kodi knjižnice za podporo Asyncify niso povsem sodelovala z longjmp-ji, ki se uporabljajo v kodi virtualnega procesorja, a po manjšem popravku, ki onemogoča ta preverjanja in prisilno obnavlja kontekste, kot da je vse v redu, je koda začela delovati. In takrat se je začelo čudno: včasih so se aktivirala preverjanja v kodi za sinhronizacijo — tista, ki nenadoma končajo kodo, če bi se po logiki izvajanja morala zakleniti — nekdo je poskušal prevzeti že prevzeti mutex. Na srečo to ni bila logična težava v serializirani kodi — preprosto sem uporabil osnovno funkcionalnost glavnega zanke, ki jo ponuja Emscripten, a včasih je asinhroni klic povsem raztegoval sklad, in v tem trenutku se je aktiviral setTimeout iz glavne zanke — tako je koda šla v iteracijo glavne zanke, ne da bi zapustila prejšnjo iteracijo. Prepisal sem jo v neskončno zanko in emscripten_sleep, in težave z mutexi so prenehale. Koda je postala celo smiselnejša — kajti, v bistvu, nimam neke kode, ki pripravlja naslednji okvir animacije — preprosto procesor nekaj izračuna in zaslon se občasno posodobi. Vendar težave s tem niso prenehale: včasih se je izvajanje Qemu preprosto tiho končalo brez kakršnih koli izjem in napak. Takrat sem na to pozabil, a da ne skrajšam, bom rekel, da je bila težava v tem: koda korutin v resnici sploh ne uporablja setTimeout (no ali, vsaj, ne tako pogosto, kot bi si lahko mislili): funkcija emscripten_yield preprosto nastavi zastavico asinhronega klica. Vsa slanina je v tem, da emscripten_coroutine_next ni asinhrona funkcija: v sebi preveri zastavico, jo ponastavi in prenese nadzor, kamor je treba. To pomeni, da se na njej razvršti sklad. Težava je bila v tem, da je zaradi use-after-free, ki se je pojavil, ko je bil bazen korutin onemogočen, saj nisem kopiral pomembne vrstice kode iz obstoječega coroutine backenda, funkcija qemu_in_coroutine vrnila true, ko bi v resnici morala vrniti false. To je vodilo do klica emscripten_yield, nad katerim v skladu z nalogo ni bilo emscripten_coroutine_next, sklad se je raztegnil do vrha, vendar ni bilo nobenih setTimeout, kot sem že omenil, ni bilo definirano.

Generiranje kode JavaScript

Tukaj je, obljubljeno "vrtenje mletega mesa nazaj". V resnici ne. Seveda, če zaženemo Qemu v brskalniku, v njem pa Node.js, bomo po generiranju kode v Qemu dobili čisto drugačen JavaScript. A kljub temu, vsaj v nekem smislu, obratna transformacija.

Najprej malo o tem, kako deluje Qemu. Takoj se opravičujem: nisem profesionalni razvijalec Qemu in moje ugotovitve so lahko kdaj napačne. Kot pravijo, "mnenje študenta ne mora nujno sovpadati z mnenjem učitelja, Peanovo aksiomatiko in zdravo pametjo". Qemu ima več podprtih gostiteljskih arhitektur, za vsako od njih pa je imenik, kot je target-i386. Med gradnjo lahko navedete podporo za več gostiteljskih arhitektur, a končni rezultat so preprosto različni binarni izdelki. Koda za podporo gostiteljski arhitekturi generira nekatere notranje operacije Qemu, ki jih TCG (Tiny Code Generator) nato pretvori v strojno kodo gostiteljske arhitekture. Kot trdi datoteka readme, ki leži v imeniku tcg, je bila to prvotno del običajnega C prevajalnika, ki so ga kasneje prilagodili za JIT. Zato je, na primer, ciljna arhitektura v smislu tega dokumenta že ne gostiteljska, temveč gostiteljska arhitektura. V nekem trenutku se je pojavil še en komponent — Tiny Code Interpreter (TCI), ki naj bi izvajal kodo (pravzaprav iste notranje operacije) v odsotnosti generatorja kode za določeno gostiteljsko arhitekturo. V resnici, kot je zapisano v njegovi dokumentaciji, ta interpreter morda ne deluje vedno tako dobro, kot JIT generator kode, in to ne le kvantitativno z vidika hitrosti, temveč tudi kvalitativno. Čeprav nisem prepričan, da je njegova opis popolnoma aktualen.

Sprva sem poskušal narediti poln TCG backend, a sem se hitro zmedel v izvorni kodi in nekoliko nerazumljivem opisu navodil bajtkode, zato sem se odločil oviti interpreter TCI. To je prineslo takojšnje prednosti:

  • pri implementaciji generatorja kode nisem gledal v opis navodil, temveč v kodo interpreterja.
  • V funkcijah lahko generiramo le po določenem številu izvedb, na primer šele po stoti izvedbi.
  • Če se generirana koda spremeni (kar je očitno mogoče, glede na funkcije, katerih imena vsebujejo besedo patch), bom moral invalidirati generirano JS-kodo, toda vsaj imel bom iz česa jo ponovno generirati.

Glede tretje točke nisem prepričan, da je patching možen po prvi izvedbi kode, vendar sta že prvima dvema točkama dovolj.

Sprva je bila koda generirana v obliki velikega switcha na naslovu izvorne bajtne kode, vendar sem se nato, po spominu na članek o Emscriptenu, optimizaciji generirane JS in reloopingu, odločil generirati bolj človeško berljivo kodo, še posebej, ker se je empirično izkazalo, da je edina točka dostopa v blok prevajanja začetek. Tako je bilo storjeno, čez nekaj časa je nastal generator kode, ki je generiral kodo z if-stavki (čeprav brez zank). Ampak tu je težava, padel je in izdal sporočilo, da je bila navodila nepravilne dolžine. Pri tem je bila zadnja navodila na tej ravni rekurzije. brcond. V redu, dodal bom identično preverjanje pri generaciji te instrukcije pred rekurzivnim klicem in po njem in… nobena od njiju ni bila izvršena, vendar je po switch-u na assert vseeno padla. Končno, po pregledu generirane kode, sem ugotovil, da se po switchu kazalec na trenutne instrukcije ponovno naloži iz stacka in ga verjetno prekrije generirana JavaScript-koda. Tako je bilo. Povečanje pomnilnika s enega megabajta na deset ni nič spremenilo in postalo je jasno, da generator kode kroži v krogu. Moral sem preveriti, ali smo zapustili meje trenutnega TB in, če smo, vrniti naslov naslednjega TB s predznakom minus, da bi lahko nadaljevali z izvajanjem. Poleg tega to rešuje vprašanje "katere generirane funkcije invalidirati, če se je ta del bajtne kode spremenil?" — invalidirati je treba le tisto funkcijo, ki ustreza temu bloku prevajanja. Mimogrede, čeprav sem vse to odpravljen v Chromu (ker uporabljam Firefox in mi je lažje uporabiti ločen brskalnik za eksperimente), mi je Firefox pomagal odpraviti neskladnosti s standardom asm.js, po čemer je koda postala hitrejša v Chromu.

Primer generiranega kode

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

Zaključek

Torej, delo še ni končano, a me je skrivaj postalo dolgčas, da bi to dolgo gradnjo pripeljal do popolnosti. Zato sem se odločil, da do zdaj objavim tisto, kar imam. Koda je v nekaterih delih strašljiva, saj je to eksperiment in vnaprej ni jasno, kaj je treba storiti. Morda bo kasneje smiselno urediti normalne atomske commit-e nad kakšno bolj sodobno različico Qemu. Dokler pa je tu veja v gitu v obliki bloga: vsakemu vsaj nekoliko končanemu "levelu" je dodan podroben komentar v slovenščini. Ta članek je pravzaprav v veliki meri povzetek izhoda. git log.

Poskusite to vse ovde (pozor, promet).

Kaj že zdaj deluje:

  • Deluje virtualni procesor x86
  • Obstoja delujoči prototip JIT-kodegeneratorja iz strojne kode v JavaScript
  • Obstoja osnutek za sestavljanje drugih 32-bitnih gostujočih arhitektur: prav zdaj se lahko čudite, kako se ob nalaganju zagozdi Linux za arhitekturo MIPS

Kaj še lahko storimo

  • Pospešiti emulacijo. Tudi v JIT načinu deluje, zdi se, počasneje kot Virtual x86 (vsaj potencialno je tu cel Qemu z večjim številom emuliranih komponent in arhitektur)
  • Urediti normalen vmesnik — spletni razvijalec iz mene, milo rečeno, ni prav dober, zato sem trenutno predelal standardno lupino Emscripten, kolikor sem lahko.
  • Poskusiti zagnati bolj zapletene funkcije Qemu — omrežje, migracijo VM itd.
  • UPD: Treba bo predati v upstream Emscripten svoje redke dosežke in poročila o napakah, kot so to storili prejšnji prenašalci Qemu in drugih projektov. Hvala njim, da sem imel priložnost tiho koristiti njihov prispevek k Emscriptenu v okviru svoje naloge.

Izvor: habr.com

Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki 🔥 Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki | ProHoster