Qemu.js s podrškom za JIT: miješanje se ipak može okrenuti nazad.

Prije nekoliko godina, Fabrice Bellard napisao je jslinux — emulator za PC, napisan u JavaScriptu. Nakon toga, barem Virtual x86. No, koliko znam, svi su oni bili interpretatori, dok je Qemu, napisan mnogo ranije od strane Fabricea Bellarda, a vjerojatno i svaki moderni emulator, koristio JIT-kompilaciju gostujućeg koda u kod domaćina. Činilo mi se da je pravo vrijeme za realizaciju obrnutog zadatka onim što rade preglednici: JIT-kompilaciju strojno generiranog koda u JavaScript, za što se kao logičan izbor činila portacija Qemu. Čini se, zašto baš Qemu, postoje jednostavniji i korisnički prilagođeniji emulatori — primjerice, VirtualBox — jednostavno se instalira i radi. Ali Qemu ima nekoliko zanimljivih posebnosti

  • otvoreni izvorni kod
  • mogućnost rada bez jezgrenog drajvera
  • mogućnost rada u interpretatorskom režimu
  • podrška za širok spektar domaćih i gostujućih arhitektura

Što se tiče treće točke, sada već mogu objasniti da se u režimu TCI ne interpretiraju samo gostujuće strojne instrukcije, nego i bytecode koji se iz njih dobije, ali to ne mijenja suštinu — kako bi se Qemu prikupio i pokrenuo na novoj arhitekturi, u najboljem slučaju, potreban je samo C kompajler — pisanje kodnog generatora može se odgoditi.

I tako, nakon dvije godine polagane analize izvornih kodova Qemu, pojavio se funkcionalni prototip, u kojem se već može pokrenuti, na primjer, Kolibri OS.

Što je Emscripten

U naše vrijeme pojavilo se mnogo kompajlera, čiji je krajnji rezultat rad JavaScript. Neki, poput Type Script, prvotno su projektirani kao bolji način pisanja za web. U isto vrijeme, Emscripten je način da se postojeći kod na C ili C++ prevede u oblik razumljiv preglednicima. Na ovoj stranici okupljeno je dosta portova poznatih programa: ovdje, na primjer, možete pogledati PyPy — usput, kako se tvrdi, već imaju JIT. Zapravo, ne svaki program može se jednostavno prevesti i pokrenuti u pregledniku — postoji niz karakteristika, s kojima se moraš suočiti, no, kao što piše na ovoj istoj stranici, "Emscripten se može koristiti za prevođenje gotovo bilo kojeg portable C/C++ kod za JavaScript. To znači da postoji niz operacija koje su prema standardu neodređeno ponašanje, ali obično rade na x86 — na primjer, neporavnat pristup varijablama, koji je na nekim arhitekturama potpuno zabranjen. Uglavnom, Qemu je program multiplatformski i, nadamo se, ne sadrži veliku količinu neodređenog ponašanja — uzmi i kompajliraj, a zatim malo se poigraj s JIT-om — i gotovo! Ali, nije to tako lako...

Prvi pokušaj

Uopšteno govoreći, nisam prvi koji je imao ideju portirati Qemu na JavaScript. Na forumu ReactOS postavljalo se pitanje je li to moguće uz pomoć Emscriptena. Još ranije su kružile glasine da je to uradio лично Fabrice Bellard, ali se radilo o jslinuxu, koji, koliko ja znam, predstavlja pokušaj da se ručno postigne dovoljna performansa na JS-u i napisan je od nule. Kasnije je napisan Virtual x86 — za njega su objavljeni neobfuskovani izvori, i, kako se tvrdilo, veća "realističnost" emulacije omogućila je korišćenje SeaBIOS-a kao firmware-a. Pored toga, postojala je barem jedna pokušaj portiranja Qemu uz pomoć Emscriptena — to su pokušali uraditi socketpair, ali razvoj, koliko sam shvatio, je obustavljen.

Dakle, činilo bi se da su tu izvori, tu je Emscripten — uzmi i kompajliraj. No, postoje i biblioteke od kojih Qemu zavisi, i biblioteke od kojih zavise te biblioteke itd., a jedna od njih je libffi, od koje ovisi glib. Na internetu su kružile glasine da u velikoj kolekciji portova biblioteka pod Emscriptenom postoji, ali nekako se nije vjerovalo: prvo, novim kompajlerom se nije mogla sastaviti, a drugo, to je previše nisko razine biblioteka da bi se jednostavno mogla skompajlirati u JS. I nije problem samo u assemblerinim umetanjima — vjerojatno, ako se malo zakomplicira, za neke calling conventions može se i bez njih formirati potrebne argumente na steku i pozvati funkciju. No, Emscripten je komplicirana stvar: da bi generirani kod izgledao poznato optimizatoru JS-motora preglednika, koriste se neki trikovi. Konkretno, takozvano relooping — kodogenerator prema dobivenom LLVM IR s nekim apstraktnim instrukcijama prijelaza pokušava rekonstruirati vjerodostojne if-ove, petlje itd. A kako se argumenti funkcijama predaju? Naravno, kao argumenti JS-funkcija, što znači, ako je moguće, ne kroz stek.

Na početku je bila misao samo napisati zamjenu za libffi u JS i proći standardne testove, ali na kraju sam se zbunio kako napraviti svoje zaglavne datoteke da rade s postojećim kodom — što da radim, kako kažu, "Ili su zadatci tako složeni, ili smo mi tako glupi". Morao sam portirati libffi na još jednu arhitekturu, ako se tako može izraziti — srećom, u Emscriptenu postoje makroi za inline assembly (na javascriptskom, da — pa koja arhitektura, takav i assembler), kao i mogućnost pokretanja generiranog koda u hodu. U suštini, nakon što sam se malo igrao s platformno-zavisnim fragmentima libffi, dobio sam neki kod koji se može kompajlirati, i testirao sam ga na prvom testiranju. Na moje iznenađenje, test je prošao uspješno. Zapanjen svojom genijalnošću — šala nije, radilo je iz prvog pokušaja — još uvijek ne vjerujući svojim očima, ponovno sam pogledao dobiveni kod, procijenio kamo dalje kopati. Tada sam opet bio zapanjen — jedino što je moja funkcija ffi_call — to je prijavljivala o uspješnom pozivu. Samog poziva nije bilo. Tako sam poslao svoj prvi pull request, koji je ispravljao očiglednu grešku u testu — stvarne brojeve ne treba uspoređivati kao a == b i čak kao a - b < EPS — treba još zaboraviti modul, inače će 0 biti baš jednak 1/3… Uglavnom, dobio sam neku biblioteku libffi koja prolazi najjednostavnije testove, i uz nju se kompajlira glib — odlučio sam, ako bude trebalo, kasnije ću dodati. Da ne skraćujem, reći ću da, kako se ispostavilo, konačni kod funkcije libffi kompajler čak nije uključio.

Ali, kao što sam već rekao, postoje neka ograničenja, i među slobodnom upotrebom raznog neodređenog ponašanja pojavila se jedna osobina koja nije baš prijatna — JavaScript po dizajnu ne podržava višestruko izvođenje s zajedničkom memorijom. U osnovi, to se obično može nazvati čak i dobrom idejom, ali ne kada je u pitanju portiranje koda koji je arhitektonski vezan za C-ove niti. Uopšteno govoreći, u Firefoxu se eksperimentiše s podrškom za shared workers, i implementacija pthread za njih postoji u Emscriptenu, ali nisam hteo da zavisim od toga. Morao sam polako ukloniti višestruko pokretanje iz koda Qemu — to jest, pronaći gde se pokreću niti, izdvojiti telo petlje koja se izvršava u toj niti u posebnu funkciju, i redom pozivati takve funkcije iz glavne petlje.

Drugi pokušaj

U nekom trenutku postalo je jasno da je tu i dalje, i da neuredno razbacivanje loših rešenja po kodu ne vodi ničemu dobrom. Zaključak: treba nekako sistematizovati proces dodavanja rešenja. Stoga je uzeta sveža verzija 2.4.1 (ne 2.5.0, jer, ko zna, možda imaju još neuhvaćenih grešaka nove verzije, a meni i svojih grešaka dovoljno), i prvo je na siguran način prepisana thread-posix.c. Pa, kako sigurno: ako je neko pokušao izvršiti operaciju koja dovodi do blokade, odmah se pozivala funkcija abort() — naravno, to nije odmah rešilo sve probleme, ali barem, to je nekako prijatnije nego tiho dobijati nekonzistentnost podataka.

Uopšteno, portiranje koda na JS vrlo pomažu opcije Emscripten -s ASSERTIONS=1 -s SAFE_HEAP=1 — one hvataju neke vrste neodređenog ponašanja poput pristupa neporavnatom adresom (što se uopšte ne slaže s kodom za typed arrays poput HEAP32[addr >> 2] = 1) ili poziv funkcije s pogrešnim brojem argumenata.

Usput, pogreške u poravnanju su posebna tema. Kao što sam već spomenuo, u Qemu postoji "degenerisani" interpretativni backend za generiranje koda TCI (tiny code interpreter), i da bi se sastavio i pokrenuo Qemu na novoj arhitekturi, ako budemo imali sreće, dovoljan je C kompajler. Ključne riječi "ako budemo imali sreće". Meni baš i nije pošlo za rukom, i ispostavilo se da TCI pri razradi svog bajt-koda koristi neporavnat pristup. To jest, na raznim ARM i drugim arhitekturama koje zahtijevaju poravnat pristup Qemu se kompajlira zato što za njih postoji normalan TCG backend koji generira nativni kod, a hoće li TCI raditi na njima — pitanje je. Međutim, kao što se ispostavilo, u dokumentaciji za TCI nešto slično je jasno naznačeno. Na kraju su u kod dodani pozivi funkcija za neporavnat čitanje, koji su otkriveni u drugom dijelu Qemu.

Uništavanje hrpe

Na kraju, neporavnat pristup u TCI je ispravljen, napravljen je glavni ciklus koji redom poziva procesor, RCU i nešto sitno. I tako pokrećem Qemu s opcijom -d exec,in_asm,out_asm, što znači da treba naznačiti koji se blokovi koda izvršavaju, a također u trenutku prevođenja zapisivati koji je gostujući kod bio, koji je host kod postao (u ovom slučaju, bajt-kod). Pokreće se, izvršava nekoliko blokova prevođenja, piše moje ostavljeno dijagnostičko poruku da će se sada pokrenuti RCU i… pada na abort() unutra funkcije free(). Pokušajem istraživanja funkcije free() uspjelo je utvrditi da je u zaglavlju bloka hrpe, koje leži osam bajtova prije dodijeljene memorije, umjesto veličine bloka ili nečega slično, bio otpad.

Uništavanje hrpe — kako slatko... U takvom slučaju postoji korisno sredstvo — iz (ako je moguće) istih izvornih kodova sastaviti nativni binarni program i pokrenuti ga pod Valgrindom. Nakon nekog vremena binarni program je bio spreman. Pokrećem s istim opcijama — ponovno se ruši za vrijeme inicijalizacije, ne došavši do, zapravo, izvršenja. Neugodno, naravno — očigledno, izvorni kodovi nisu bili sasvim isti, što nije iznenađujuće, s obzirom da je configure otkrio nekoliko drugih opcija, ali ja imam Valgrind — prvo ću popraviti ovu grešku, a onda, ako budem imao sreće, možda će se i izvorna pojaviti. Pokrećem sve to pod Valgrindom... U-uh, ono se pokrenulo, normalno je prošlo inicijalizaciju i nastavilo dalje mimo izvornog baga bez jedne jedine poruke o neispravnom pristupu memoriji, a da ne govorim o rušenju. Na to me život, kako se kaže, nije pripremio — program koji se ruši prestaje se rušiti pri pokretanju pod Valgrindom. Što je to bilo — zagonetka. Moja hipoteza je da pošto u okolini trenutne instrukcije nakon rušenja pri inicijalizaciji gdb je pokazivao rad memset-a s validnim pokazivačem uz korištenje ili mmx, ili xmm registara, onda je to možda bila neka greška u poravnanju, iako svejedno slabo vjerujem.

U redu, Valgrind ovdje, čini se, nije od pomoći. I ovdje je počelo najgore - sve, čini se, čak radi, ali pada zbog apsolutno nepoznatih razloga zbog događaja koji se mogao dogoditi milijune instrukcija unatrag. Dugo nije bilo jasno kako se uopće približiti problemu. Na kraju je bilo potrebno sjesti i debagirati. Ispis onoga što je prepisano u zaglavlju pokazao je da to nije broj, već vjerojatno neki binarni podaci. I, o čudo, ti binarni nizovi pronađeni su u datoteci s BIOS-om - što znači da se sada moglo s dovoljno sigurnosti reći da se radilo o preljevu bafera i čak je bilo jasno što se upisivalo u ovaj bafer. Pa, dalje je nekako ovako - u Emscriptenu, srećom, nema randomizacije adresnog prostora, nema rupa u njemu, tako da se može napisati negdje u sredini koda ispis podataka putem pokazivača s prethodnog pokretanja, pogledati na podatke, pogledati na pokazivač, i, ako se nije promijenio, dobiti informacije za razmišljanje. Istina, na povezivanje nakon svake promjene troši se nekoliko minuta, ali što da se radi. Kao rezultat, pronađen je konkretan niz koji kopira BIOS iz privremenog bafera u gostujuću memoriju - i, doista, u baferu nije bilo dovoljno prostora. Potraga za izvorom tog čudnog adresa bafera dovela je do funkcije. qemu_anon_ram_alloc u datoteci oslib-posix.c - logika je bila takva: ponekad može biti korisno poravnati adresu prema huge page veličine 2 MB, za to ćemo zatražiti mmap prvo nešto više, a onda višak ćemo vratiti uz pomoć munmap. A ako takvo poravnanje nije potrebno, onda ćemo umjesto 2 MB navesti rezultat getpagesize() — mmap svejedno će dati poravnatu adresu... Dakle, u Emscripten mmap jednostavno poziva malloc, a on, naravno, ne poravnava prema stranici. Uglavnom, greška koja me mučila nekoliko mjeseci ispravila se promjenom u dvije redovima.

Značajke pozivanja funkcija

I evo procesor nešto izračunava, Qemu ne pada, ali ekran se ne uključuje, a procesor se brzo ciklira, sudeći prema izlazu -d exec,in_asm,out_asm. Pojavila se hipoteza: prekidi timera ne dolaze (ili uopće svi prekidi). I doista, ako se od nativne verzije, koja je iz nekog razloga radila, uklone prekidi, dobiva se slična slika. No, misterija nije bila u tome: usporedba tragova izvršenja s navedenom opcijom pokazala je da se putanje izvršenja razdvajaju vrlo rano. Tu treba napomenuti da usporedba zapisa izvedenog putem pokretača emrun debug output s nativnom verzijom - nije posve mehanički proces. Ne znam točno kako pokrenuti program u pregledniku povezuje s emrun, ali se neke linije u izlazu čine zamijenjenima, pa razlika u diffu - to još nije razlog da se smatra da su se putanje razdvojile. Uglavnom, postalo je jasno da se prema uputama ljmpl prelazi na različite adrese, a i bajtkod se generira suštinski drugačije: u jednom postoji instrukcija za poziv C funkcije pomoćnika, dok u drugom - nema. Nakon pretraživanja instrukcija i proučavanja koda koji te instrukcije prevodi, postalo je jasno da, prije svega, neposredno do nje u registar cr0 bila je zapis - također preko pomoćnika - koja procesor prebacuje u zaštitni način rada, a drugo, da JS verzija nikada nije prešla u zaštitni način. A stvar je u tome da još jedna karakteristika Emscriptena je nepokolebljiva želja da ne trpi kod poput implementacije instrukcije call u TCI, koja svaki pokazivač na funkciju dovodi do tipa long long f(int arg0, .. int arg9) — funkcije moraju biti pozvane s pravim brojem argumenata. Kada se ovo pravilo prekrši, ovisno o postavkama za ispravljanje, program će ili pasti (što je dobro), ili će pozvati potpuno neku drugu funkciju (što će biti tužno za debugiranje). Postoji još jedna opcija - uključiti generiranje omotača koji dodaju / odbačaju argumente, ali ukupno ti omotači zauzimaju jako puno prostora, dok mi zapravo treba samo malo više od stotinu omotača. Ovo samo po sebi je vrlo tužno, no ispostavilo se da je ozbiljniji problem: u generiranom kodu funkcija-omotač argumenti su se konvertirali, konvertirali, samo što funkcija s generiranim argumentima ponekad nije pozvana - baš kao u mojoj implementaciji libffi. Drugim riječima, neki pomoćnici jednostavno nisu izvršeni.

Na sreću, Qemu sadrži mašinski čitljive liste helpere u obliku zaglavlja poput

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

Koriste se na prilično zabavan način: najpre se na najbizarniji način redefinišu makroi DEF_HELPER_n, a zatim se uključuje helper.h. Do te mere da se makro otkriva u inicijalizatoru strukture i zarezu, a zatim se definiše niz, a umesto elemenata — #include <helper.h> Kao rezultat, konačno se ukazala prilika da se proba biblioteka pyparsing, i napisan je skript koji generiše omotače upravo za te funkcije koje su potrebne.

I eto, nakon toga procesor kao da je radio. Kao da, jer ekran se nikako nije inicijalizovao, iako je u nativnoj verziji uspelo da se pokrene memtest86+. Treba napomenuti da je kod blokovnog ulaza-izlaza Qemu napisan na korutinama. U Emscripten postoji svoj prilično složen oblik, ali to je još trebalo podržati u kodu Qemu, a debagovati procesor može se već sada: Qemu podržava opcije -kernel, -initrd, -append, pomoću kojih je moguće učitati Linux ili, na primer, memtest86+, praktično ne koristeći blok uređaje. Ali problem je u tome: u nativnoj verziji mogla se posmatrati izlaz Linux kernela na konzolu s opcijom -nographic, a iz browsera nije bilo nikakvog izlaza u terminal odakle je bio pokrenut emrun, nije bilo. Drugim rečima, nije se znalo da li procesor ne radi ili izlaz grafike. A onda mi je palo na pamet da malo sačekam. Ispostavilo se da "procesor ne spava, već samo sporo trepće", i nakon pet minuta jezgro je na konzolu izbacilo hrpu poruka i nastavilo dalje sa zamrzavanjem. Postalo je jasno da procesor, u celini, radi, i da treba istražiti kod rada sa SDL2. Nažalost, nisam vešt u korišćenju ove biblioteke, pa sam u nekim trenucima morao da postupam nasumično. U jednom trenutku na ekranu se pojavila linija parallel0 na plavoj pozadini, što je sugerisalo određene misli. Na kraju se ispostavilo da je reč o tome da Qemu otvara nekoliko virtuelnih prozora u jednom fizičkom prozoru, između kojih se može prebacivati pomoću Ctrl-Alt-n: u nativnoj verziji to funkcioniše, u Emscriptenu — ne. Nakon što sam se oslobodio viška prozora pomoću opcija -monitor none -parallel none -serial none i naložio da se celokupan ekran ponovo crta u svakom okviru, sve je iznenada počelo da funkcioniše.

Korutine

Dakle, emulacija u pregledniku radi, ali ne može se pokrenuti ništa zanimljivo sa floppy diska jer nema blokovnog ulaza/izlaza — potrebno je implementirati podršku za korutine. U Qemu već postoji nekoliko coroutine backendova, ali zbog osobitosti JavaScripta i generatora koda Emscripten ne može se jednostavno uzeti i početi žonglirati stablima. Činilo bi se da "sve je propalo, gips se skida", ali programeri Emscriptena su se već pobrinuli za sve. Realizirano je to prilično zabavno: hajde da nazovemo sumnjiv poziv funkcije poput emscripten_sleep i nekoliko drugih, koje koriste mehanizam Asyncify, kao i pozive putem pokazatelja i pozive bilo koje funkcije, gdje može doći do jednog od prethodna dva slučaja. A sada, prije svakog sumnjivog poziva, izdvojimo async kontekst, a odmah nakon poziva - provjerimo je li došlo do asinkronog poziva, i ako jest, sačuvat ćemo sve lokalne varijable u ovaj async kontekst, odrediti kojoj funkciji proslijediti kontrolu kada bude potrebno nastaviti izvršenje, i izaći iz trenutne funkcije. Ovdje je prostor za proučavanje učinka rastrganosti — za potrebe nastavka izvršavanja koda nakon povratka iz asinkronog poziva, kompajler generira "odrezane" funkcije, koje počinju nakon sumnjivog poziva — tako: ako ima n sumnjivih poziva, funkcija će biti razdvojena negdje n/2 puta — to je još i bez uzimanja u obzir da u izvornoj funkciji treba nakon svakog potencijalno asinkronog poziva dodati pohranu dijela lokalnih varijabli. U budućnosti je čak bilo potrebno napisati jednostavan skript u Pythonu, koji, prema određenom skupu posebno rastrganih funkcija, koje, pretpostavljam, "ne propuštaju asinkronost kroz sebe" (tj. u njima ne dolazi do raspleta stablai svega onoga što sam upravo opisao), ukazuje, pozive putem pokazatelja u kojim funkcijama treba ignorirati kompajleru, kako te funkcije ne bi bile smatrane asinkronim. Jer, JS datoteke od 60 MB — to je već očito previse — neka budu barem 30. Iako, jednom sam postavljao skript za gradnju, i slučajno izbacio opcije linker-a, među kojima je bila i -O3. Pokrećem generirani kod, i Chromium troši memoriju i ruši se. Zatim sam slučajno pogledao što je pokušavao učitati… Pa, što da kažem, i ja bih se zamrznuo da me pitaju da detaljno proučavam i optimiziram JavaScript od 500+ MB.

Nažalost, provjere u kodu biblioteke podrške Asyncify nisu baš surađivale s longjmp-ima, koji se koriste u kodu virtualnog procesora, ali nakon malog patcha koji onemogućava te provjere i prisilno obnavlja kontekste kao da je sve u redu, kod je proradio. I tu su počeli čudni problemi: ponekad su se aktivirale provjere u kodu sinkronizacije — one koje hitno završavaju kod ako bi prema logici izvršavanja trebao biti blokiran — netko je pokušavao zgrabiti već zgrabljeni mutex. Na sreću, ispostavilo se da nije bilo logičkog problema u serijaliziranom kodu — jednostavno sam koristio ugrađenu funkcionalnost glavne petlje koju pruža Emscripten, ali ponekad je asinkroni poziv potpuno 'razvlačio' stog, a u tom trenutku se aktivirao setTimeout iz glavne petlje — tako da je kod ulazio u iteraciju glavne petlje, ne izlazeći iz prethodne iteracije. Prepisao sam na beskonačnu petlju i emscripten_sleep, i problemi s mutexima su prestali. Kod je čak postao logičniji — jer, u suštini, nemam nekog koda koji priprema sljedeći okvir animacije — jednostavno procesor nešto izračunava i ekran se povremeno ažurira. Ipak, problemi tu nisu stali: ponekad se izvršavanje Qemu jednostavno tiho završavalo bez ikakvih iznimki i grešaka. U tom trenutku sam to zanemario, ali da skratim, problem je bio u tome: kod korutina, zapravo, uopće ne koristi setTimeout (pa ili, barem, ne tako često kako bi se moglo pomisliti): funkcija emscripten_yield jednostavno postavlja oznaku asinkronog poziva. Bit je u tome što emscripten_coroutine_next nije asinkrona funkcija: unutar sebe provjerava oznaku, resetira je i predaje kontrolu gdje treba. Dakle, na njoj stavljanje stoga završava. Problem je bio u tome što je zbog use-after-free, koji se manifestirao kada je bio onemogućen pool korutina zbog toga što nisam kopirao važnu liniju koda iz postojećeg coroutine backenda, funkcija qemu_in_coroutine vraćala true, kada je zapravo trebala vratiti false. To je dovodilo do poziva emscripten_yield, iznad kojeg se nije penjalo po stogu emscripten_coroutine_next, stog se protezao do samog vrha, ali nikakvih setTimeout, kao što sam već rekao, nije bilo postavljeno.

Generiranje koda u JavaScriptu

Evo, dakle, i obećanog "okretanja mljevenog mesa unazad". Zapravo nije. Naravno, ako pokrenete Qemu u pregledniku, a unutra — Node.js, tada, naravno, nakon generiranja koda u Qemu, dobit ćemo potpuno drugačiji JavaScript. Ipak, kakvo-takvo, ali obrnuto konvertiranje.

Za početak, malo o tome kako Qemu radi. Odmah se ispričavam: nisam profesionalni programer Qemu i moji zaključci mogu biti donekle pogrešni. Kao što se kaže, "mišljenje studenta ne mora se poklapati s mišljenjem predavača, Peanovom aksiomatikom i zdravim razumom". Qemu ima određeni broj podržanih gostujućih arhitektura i za svaku postoji katalog poput target-i386. Prilikom sastavljanja može se odabrati podrška za više gostujućih arhitektura, ali rezultat će biti jednostavno nekoliko binarnih datoteka. Kod za podršku gostujućim arhitekturama, zauzvrat, generira određene unutarnje operacije Qemu, koje TCG (Tiny Code Generator) već pretvara u strojni kod domaće arhitekture. Kao što se navodi u readme datoteci u katalogu tcg, to je izvorno bila dio običnog C kompajlera, koji je kasnije prilagođen za JIT. Stoga, na primjer, ciljna arhitektura u terminima ovog dokumenta — to više nije gostujuća, već domaća arhitektura. U nekom trenutku se pojavio još jedna komponenta — Tiny Code Interpreter (TCI), koja bi trebala izvoditi kod (praktički iste unutarnje operacije) u odsutnosti generatora koda za određenu domaću arhitekturu. U stvarnosti, kako se navodi u njegovoj dokumentaciji, ovaj interpreter možda ne radi uvijek jednako dobro kao JIT generator koda, ne samo kvantitativno u pogledu brzine, već i kvalitativno. Iako nisam siguran da je njegovo opisivanje potpuno aktualno.

Isprva sam pokušavao napraviti punokrvni TCG backend, ali sam se brzo zbunio u izvorima i ne sasvim jasnom opisu uputa bajtkoda, pa sam odlučio omotati interpreter TCI. To je donijelo nekoliko prednosti odjednom:

  • prilikom implementacije generatora koda mogao sam gledati ne u opis uputa, već u kod interpretera.
  • Mogu generirati funkcije ne za svaki susretni blok transkripcije, već, na primjer, samo nakon stotog izvršenja.
  • U slučaju izmjene generiranog koda (a to je, očigledno, moguće, sudeći prema funkcijama s nazivima koji sadrže riječ patch), morat ću invalidirati generirani JS kod, ali ću barem imati iz čega ga regenerirati.

Što se tiče treće točke, nisam siguran je li patching moguć nakon što se kod prvi put izvrši, ali su prva dva točka već dovoljna.

Isprva se kod generirao u obliku velikog switch-a po adresi izvorne instrukcije bajtkoda, ali sam kasnije, prisjetivši se članka o Emscriptenu, optimizaciji generiranog JS-a i reloopingu, odlučio generirati 'ljudskiji' kod, tim više što se empirijski pokazalo da je jedina ulazna točka u blok transkripcije - njegov početak. Rečeno - učinjeno, nakon nekog vremena dobio sam generator koda koji generira kod s if-ovima (iako bez petlji). No, dogodila se neugodnost, srušio se s porukom da je instrukcija bila neke pogrešne duljine. Pri tome je posljednja instrukcija na ovom nivou rekurzije bila brcond. Dobro, dodat ću identičnu provjeru u generaciju ove instrukcije prije rekurzivnog poziva i nakon, i… nijedna od njih nije uspjela, ali smo nakon switch-a po assert-u ipak pali. Na kraju, proučivši generirani kod, shvatio sam da nakon switch-a pokazivač na trenutnu instrukciju ponovno učitava sa stoga i, vjerojatno, prepisuje se generiranim JavaScript kodom. Tako je i bilo. Povećanje bafera s jednog megabajta na deset nije dalo nikakav rezultat, i postalo je jasno da generator koda kruži u krug. Morao sam provjeriti nismo li izašli iz granica trenutnog TB-a, a ako jesmo, izbaciti adresu sljedećeg TB-a s negativnim znakom, kako bismo mogli nastaviti izvršavanje. Osim toga, to rješava problem "koje generirane funkcije invalidirati ako se ova mala sekcija bajtkoda promijenila?" - invalidirati treba samo onu funkciju koja odgovara ovom bloku transkripcije. Usput, iako sam sve debugirao u Chromu (budući da koristim Firefox i lakše mi je koristiti zasebni preglednik za eksperimente), Firefox mi je pomogao ispraviti nesukladnosti sa standardom asm.js, nakon čega je kod brže radio u Chromu.

Primjer generiranog koda

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čak

Dakle, posao još nije gotov, ali mi je dosadilo tajno usavršavati ovaj projekt. Stoga sam odlučio objaviti ono što imamo. Kod je na nekoliko mjesta pomalo strašan, s obzirom da je ovo eksperiment, i nije unaprijed jasno što treba raditi. Vjerojatno će kasnije biti potrebno urediti normalne atomske commitove preko nekakve modernije verzije Qemu. Za sada postoji grana u gitu u formatu bloga: svakom "nivo" koji je barem donekle završen dodan je detaljan komentar na hrvatskom jeziku. Ovaj članak zapravo je u velikoj mjeri prepričavanje izlaza git log.

Možete probati sve ovo ovdje (oprezno, promet).

Što već sada radi:

  • Radi virtualni procesor x86
  • Postoji radni prototip JIT-kodnog generatora s strojskog koda na JavaScript
  • Postoji osnova za izradu drugih 32-bitnih gostujućih arhitektura: možete upravo sada uživati u Linuكسu za MIPS arhitekturu koji se zamrzava u pregledniku tijekom učitavanja

Što se još može napraviti

  • Ubrzati emulaciju. Čak i u JIT modu, izgleda da radi sporije od Virtual x86 (ali potencijalno postoji cijeli Qemu s mnogo emuliranih hardverskih platformi i arhitektura)
  • Napraviti normalno sučelje — web-developer nisam baš najbolji, stoga sam za sada doradio standardnu Emscripten ljusku koliko sam znao
  • Probati pokrenuti složenije funkcije Qemu — mreža, migracija VM itd.
  • UPD: Bit će potrebno predati svoje brojne radove i bug-raporte u upstream Emscripten, kao što su radili prethodni porteri Qemu i drugih projekata. Hvala im što su omogućili korištenje njihovog doprinosa Emscriptenu u okviru mog zadatka.

Izvor: habr.com

Kupite pouzdan hosting za web stranice s DDoS zaštitom, VPS VDS servere 🔥 Kupite pouzdan hosting za web stranice s DDoS zaštitom, VPS VDS servere | ProHoster