Qemu.js sa JIT podrškom: punjenje se i dalje može vratiti

Prije nekoliko godina Fabrice Bellard napisao jslinux je PC emulator napisan u JavaScript-u. Nakon toga je bilo barem više Virtualni x86. Ali svi su oni, koliko ja znam, bili interpretatori, dok Qemu, koji je mnogo ranije napisao isti Fabrice Bellard, i, vjerovatno, bilo koji moderni emulator koji poštuje sebe, koristi JIT kompilaciju gostujućeg koda u kod host sistema. Činilo mi se da je došlo vrijeme da implementiramo suprotan zadatak u odnosu na onaj koji pretraživači rješavaju: JIT kompilaciju mašinskog koda u JavaScript, za koji se činilo najlogičnijim portirati Qemu. Čini se, zašto Qemu, postoje jednostavniji i jednostavniji emulatori - isti VirtualBox, na primjer - instaliran i radi. Ali Qemu ima nekoliko zanimljivih karakteristika

  • open source
  • mogućnost rada bez drajvera kernela
  • sposobnost rada u režimu tumača
  • podrška za veliki broj i host i gostujućih arhitektura

Što se tiče treće tačke, sada mogu objasniti da se zapravo u TCI modu ne interpretiraju same instrukcije mašine za goste, već bajt kod koji se dobije od njih, ali to ne mijenja suštinu - da bi se izgradile i pokrenule Qemu na novoj arhitekturi, ako imate sreće, dovoljan je C kompajler - pisanje generatora koda može biti odloženo.

A sada, nakon dvije godine ležernog petljanja s Qemu izvornim kodom u slobodno vrijeme, pojavio se radni prototip u kojem već možete pokrenuti, na primjer, Kolibri OS.

Šta je Emscripten

Danas se pojavilo mnogo kompajlera čiji je krajnji rezultat JavaScript. Neki, poput Type Script, prvobitno su trebali biti najbolji način za pisanje za web. Istovremeno, Emscripten je način da se postojeći C ili C++ kod prevede u formu koja se može čitati u pretraživaču. On ovu stranicu Prikupili smo mnogo portova poznatih programa: ovdjeNa primjer, možete pogledati PyPy - usput, oni tvrde da već imaju JIT. U stvari, ne može se svaki program jednostavno kompajlirati i pokrenuti u pretraživaču - postoji broj karakteristike, s čime se, međutim, morate pomiriti, jer natpis na istoj stranici kaže „Emscripten se može koristiti za kompajliranje gotovo svih portabl C/C++ kod u JavaScript". To jest, postoji niz operacija koje su nedefinisano ponašanje prema standardu, ali obično rade na x86 - na primjer, neusklađeni pristup varijablama, što je općenito zabranjeno na nekim arhitekturama. Općenito , Qemu je multi-platformski program i , htio sam vjerovati, i već ne sadrži puno nedefiniranog ponašanja - uzmi ga i kompajliraj, pa malo poigraj JIT - i gotovi ste! Ali to nije slučaj...

Prvi pokušaj

Uopšteno govoreći, nisam prva osoba koja je došla na ideju da prenese Qemu na JavaScript. Na ReactOS forumu je postavljeno pitanje da li je to moguće pomoću Emscriptena. Još ranije su se šuškale da je to lično uradio Fabris Belard, ali smo pričali o jslinuxu, koji je, koliko ja znam, samo pokušaj da se ručno postigne dovoljne performanse u JS-u, a napisan je od nule. Kasnije je napisan Virtual x86 - za njega su objavljeni neobusknuti izvori, a, kako je navedeno, veći "realizam" emulacije omogućio je korištenje SeaBIOS-a kao firmvera. Osim toga, postojao je barem jedan pokušaj portiranja Qemu-a koristeći Emscripten - pokušao sam to učiniti socketpair, ali razvoj je, koliko sam shvatio, bio zamrznut.

Dakle, čini se, ovdje su izvori, ovdje je Emscripten - uzmite ga i kompajlirajte. Ali postoje i biblioteke od kojih zavisi Qemu, i biblioteke od kojih zavise te biblioteke itd., a jedna od njih je libffi, od kojeg glib zavisi. Na internetu su se šuškale da postoji jedan u velikoj kolekciji portova biblioteka za Emscripten, ali je nekako bilo teško povjerovati: prvo, nije bio namijenjen da bude novi kompajler, drugo, bio je previše niskog nivoa. biblioteku koju treba samo pokupiti i kompajlirati u JS. I nije stvar samo u umetcima asemblera - vjerojatno, ako ga okrenete, za neke konvencije pozivanja možete generirati potrebne argumente na steku i pozvati funkciju bez njih. Ali Emscripten je zeznuta stvar: kako bi generirani kod izgledao poznato optimizatoru JS motora pretraživača, koriste se neki trikovi. Konkretno, takozvani relooping - generator koda koji koristi primljeni LLVM IR sa nekim apstraktnim uputstvima za tranziciju pokušava ponovo stvoriti vjerodostojne if, petlje, itd. Pa, kako se argumenti prosljeđuju funkciji? Naravno, kao argumenti JS funkcijama, odnosno, ako je moguće, ne kroz stek.

Na početku je postojala ideja da jednostavno napišem zamenu za libffi sa JS-om i pokrenem standardne testove, ali na kraju sam se zbunio kako da napravim svoje zaglavlje tako da rade sa postojećim kodom - šta da radim, kako kažu, "Jesu li zadaci tako složeni "Jesmo li tako glupi?" Morao sam da prenesem libffi na drugu arhitekturu, da tako kažem – na sreću, Emscripten ima i makroe za inline asembler (u Javascriptu, da – pa, bez obzira na arhitekturu, dakle asembler), i mogućnost pokretanja koda generiranog u hodu. Općenito, nakon što sam neko vrijeme petljao sa fragmentima libffija zavisnim od platforme, dobio sam neki kompajlibilni kod i pokrenuo ga na prvom testu na koji sam naišao. Na moje iznenađenje, test je bio uspješan. Zapanjen svojom genijalnošću - bez šale, funkcioniralo je od prvog lansiranja - ja sam, još uvijek ne vjerujući svojim očima, otišao da ponovo pogledam rezultirajući kod, da procijenim gdje dalje kopati. Ovdje sam poludio po drugi put - jedino što je moja funkcija radila je ffi_call - ovo je prijavilo uspješan poziv. Nije bilo samog poziva. Tako sam poslao svoj prvi pull zahtjev, koji je ispravio grešku u testu koja je jasna svakom učeniku olimpijade - stvarne brojeve ne treba porediti kao a == b pa čak i kako a - b < EPS - također morate zapamtiti modul, inače će 0 ispasti vrlo jednako 1/3... Generalno, smislio sam određeni port za libffi, koji prolazi najjednostavnije testove i s kojim je glib kompajlirano - odlučio sam da će biti potrebno, dodaću kasnije. Gledajući unaprijed, reći ću da, kako se ispostavilo, kompajler nije čak ni uključio libffi funkciju u konačni kod.

Ali, kao što sam već rekao, postoje neka ograničenja, a među slobodnim korištenjem raznih nedefiniranih ponašanja, skrivena je još neugodnija karakteristika - JavaScript po dizajnu ne podržava višenitnost sa zajedničkom memorijom. U principu, ovo se obično može čak nazvati i dobrom idejom, ali ne i za prenos koda čija je arhitektura vezana za C niti. Općenito govoreći, Firefox eksperimentira s podrškom dijeljenih radnika, a Emscripten ima implementaciju pthread za njih, ali nisam htio ovisiti o tome. Morao sam polako iskorijeniti višenitnost iz Qemu koda - to jest, otkriti gdje se pokreću niti, premjestiti tijelo petlje koja se izvodi u ovoj niti u zasebnu funkciju i pozvati takve funkcije jednu po jednu iz glavne petlje.

Drugi pokušaj

U nekom trenutku postalo je jasno da je problem i dalje prisutan i da nasumično guranje štaka oko koda neće dovesti do ničega. Zaključak: potrebno je nekako sistematizirati proces dodavanja štaka. Stoga je uzeta verzija 2.4.1, koja je bila svježa u to vrijeme (ne 2.5.0, jer, ko zna, u novoj verziji će biti grešaka koje još nisu uhvaćene, a ja imam dovoljno svojih grešaka ), a prva stvar je bila da ga bezbedno prepišete thread-posix.c. Pa, to jest, kao sigurno: ako je neko pokušao izvršiti operaciju koja je dovela do blokiranja, funkcija je odmah pozvana abort() - naravno, ovo nije riješilo sve probleme odjednom, ali je barem bilo nekako ugodnije od tihog primanja nedosljednih podataka.

Općenito, Emscripten opcije su od velike pomoći u prijenosu koda na JS -s ASSERTIONS=1 -s SAFE_HEAP=1 - hvataju neke tipove nedefinisanog ponašanja, kao što su pozivi na neusklađenu adresu (što uopšte nije u skladu sa kodom za ukucane nizove kao što je HEAP32[addr >> 2] = 1) ili pozivanje funkcije s pogrešnim brojem argumenata.

Usput, greške u poravnanju su posebna tema. Kao što sam već rekao, Qemu ima "degeneriranu" interpretativnu pozadinu za generiranje koda TCI (maleni interpreter koda), a za izgradnju i pokretanje Qemu-a na novoj arhitekturi, ako imate sreće, dovoljan je kompajler C. Ključne riječi "ako budeš imao sreće". Nisam imao sreće i pokazalo se da TCI koristi neusklađeni pristup kada analizira svoj bajt kod. Odnosno, na svim vrstama ARM i drugih arhitektura sa nužno nivoiranim pristupom, Qemu kompajlira jer imaju normalan TCG backend koji generiše izvorni kod, ali da li će TCI raditi na njima je drugo pitanje. Međutim, kako se ispostavilo, TCI dokumentacija jasno ukazuje na nešto slično. Kao rezultat toga, kodu su dodani pozivi funkcija za neusklađeno čitanje, koji su pronađeni u drugom dijelu Qemu-a.

Heap destruction

Kao rezultat, ispravljen je neusklađeni pristup TCI-u, stvorena je glavna petlja koja je zauzvrat zvala procesor, RCU i još neke sitnice. I tako pokrećem Qemu sa opcijom -d exec,in_asm,out_asm, što znači da treba da kažete koji blokovi koda se izvršavaju, kao i da u trenutku emitovanja napišete šta je bio gostujući kod, koji je kod hosta postao (u ovom slučaju bajt kod). Pokreće se, izvršava nekoliko prijevodnih blokova, piše poruku za otklanjanje grešaka koju sam ostavio da će se RCU sada pokrenuti i... ruši abort() unutar funkcije free(). Petljanjem po funkciji free() Uspjeli smo otkriti da je u zaglavlju heap bloka, koje se nalazi u osam bajtova koji prethode dodijeljenoj memoriji, umjesto veličine bloka ili nečeg sličnog, bilo smeće.

Uništavanje gomile - kako simpatično... U takvom slučaju postoji koristan lijek - iz (ako je moguće) istih izvora, sastavite izvornu binarnu datoteku i pokrenite je pod Valgrind-om. Nakon nekog vremena, binarni program je bio spreman. Pokrećem ga sa istim opcijama - ruši se čak i tokom inicijalizacije, pre nego što zaista dođe do izvršenja. Neprijatno je, naravno - očigledno, izvori nisu bili potpuno isti, što i nije iznenađujuće, jer konfiguracija ima malo drugačije opcije, ali imam Valgrind - prvo ću popraviti ovu grešku, a onda, ako budem imao sreće , pojavit će se originalni. Pokrećem istu stvar pod Valgrind-om... Y-y-y, y-y-y, uh-uh, počelo je, normalno je prošlo kroz inicijalizaciju i krenulo dalje od originalne greške bez ijednog upozorenja o pogrešnom pristupu memoriji, da ne spominjemo padove. Život me, kako kažu, nije pripremio za ovo - program za rušenje prestaje da se ruši kada se pokrene pod Walgrindom. Šta je to bilo je misterija. Moja hipoteza je da je jednom u blizini trenutne instrukcije nakon pada tokom inicijalizacije, gdb pokazao rad memset-a sa važećim pokazivačem koristeći bilo koji mmx, ili xmm registre, onda je možda u pitanju neka vrsta greške u poravnanju, iako je još uvijek teško povjerovati.

Ok, izgleda da Valgrind tu ne pomaže. I tu je počelo ono najodvratnije - čini se da je sve počelo, ali se ruši iz apsolutno nepoznatih razloga zbog događaja koji se mogao dogoditi prije više miliona instrukcija. Dugo vremena nije bilo jasno ni kako pristupiti. Na kraju sam ipak morao sjesti i otkloniti greške. Ispis onoga čime je zaglavlje prepisano pokazalo je da ne izgleda kao broj, već neka vrsta binarnih podataka. I, eto, ovaj binarni niz je pronađen u BIOS datoteci - to jest, sada se moglo sa razumnom sigurnošću reći da se radi o prekoračenju bafera, a čak je jasno da je upisan u ovaj bafer. Pa, onda nešto ovako - u Emscriptenu, srećom, nema randomizacije adresnog prostora, nema ni rupa u njemu, tako da možete napisati negdje u sredini koda za izlaz podataka po pokazivaču od posljednjeg pokretanja, pogledajte podatke, pogledajte pokazivač, i, ako se nije promijenio, potražite hranu za razmišljanje. Istina, potrebno je nekoliko minuta za povezivanje nakon bilo kakve promjene, ali šta možete učiniti? Kao rezultat toga, pronađena je određena linija koja je kopirala BIOS iz privremenog međuspremnika u gostujuću memoriju - i zaista nije bilo dovoljno prostora u međuspremniku. Pronalaženje izvora te čudne adrese bafera rezultiralo je funkcijom qemu_anon_ram_alloc u fajlu oslib-posix.c - logika je bila sljedeća: ponekad može biti korisno poravnati adresu sa ogromnom stranicom veličine 2 MB, za to ćemo pitati mmap prvo još malo, a onda ćemo uz pomoć vratiti višak munmap. A ako takvo poravnanje nije potrebno, tada ćemo navesti rezultat umjesto 2 MB getpagesize() - mmap i dalje će izdati usklađenu adresu... Dakle, u Emscriptenu mmap samo zove malloc, ali naravno da se ne poravna na stranici. Općenito, greška koja me je frustrirala par mjeseci ispravljena je promjenom dvije linije.

Karakteristike pozivanja funkcija

I sada procesor nešto broji, Qemu se ne ruši, ali ekran se ne uključuje, a procesor brzo ide u petlje, sudeći po izlazu -d exec,in_asm,out_asm. Pojavila se hipoteza: prekidi tajmera (ili, općenito, svi prekidi) ne stižu. I zaista, ako odvrnete prekide s izvornog sklopa, koji je iz nekog razloga funkcionirao, dobit ćete sličnu sliku. Ali to uopće nije bio odgovor: poređenje izdanih tragova s ​​gornjom opcijom pokazalo je da su se putanje izvršenja vrlo rano razišle. Ovdje se mora reći da je poređenje onoga što je snimljeno pomoću lansera emrun otklanjanje grešaka izlaza sa izlazom izvornog sklopa nije potpuno mehanički proces. Ne znam tačno kako se povezuje program koji radi u pretraživaču emrun, ali neke linije na izlazu su preuređene, tako da razlika u diff-u još nije razlog za pretpostavku da su se putanje razišle. Općenito, postalo je jasno da prema uputama ljmpl postoji prijelaz na različite adrese, a generirani bajtkod je fundamentalno drugačiji: jedan sadrži instrukciju za pozivanje pomoćne funkcije, drugi ne. Nakon guglanja instrukcija i proučavanja koda koji prevodi ove upute, postalo je jasno da je, prvo, neposredno prije toga u registru cr0 napravljen je snimak - takođe uz pomoć pomoćnika - koji je prebacio procesor u zaštićeni režim, i drugo, da js verzija nikada nije prešla u zaštićeni režim. Ali činjenica je da je još jedna karakteristika Emscriptena njegova nevoljkost da toleriše kod kao što je implementacija instrukcija call u TCI, što bilo koji pokazivač funkcije rezultira u tipu long long f(int arg0, .. int arg9) - funkcije moraju biti pozvane s tačnim brojem argumenata. Ako se ovo pravilo prekrši, ovisno o postavkama za otklanjanje grešaka, program će se ili srušiti (što je dobro) ili će uopće pozvati pogrešnu funkciju (što će biti tužno za otklanjanje grešaka). Postoji i treća opcija - omogući generisanje omota koji dodaju/uklanjaju argumente, ali ukupno ti omoti zauzimaju dosta prostora, uprkos činjenici da mi u stvari treba samo nešto više od sto omotača. Ovo je samo po sebi vrlo tužno, ali se pokazalo da postoji ozbiljniji problem: u generiranom kodu funkcija omotača, argumenti su konvertirani i konvertirani, ali ponekad funkcija s generiranim argumentima nije pozvana - pa, baš kao u moja implementacija libffi. Odnosno, neki pomagači jednostavno nisu pogubljeni.

Na sreću, Qemu ima mašinski čitljive liste pomagača u obliku zaglavlja kao što je

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

Koriste se prilično smiješno: prvo, makroi se redefiniraju na najbizarniji način DEF_HELPER_n, a zatim se uključuje helper.h. U meri u kojoj je makro proširen u inicijalizator strukture i zarez, a zatim se definiše niz, a umesto elemenata - #include <helper.h> Kao rezultat toga, konačno sam imao priliku isprobati biblioteku na poslu pyparsing, i napisana je skripta koja generiše upravo one omote za točno one funkcije za koje su potrebni.

I tako, nakon toga se činilo da procesor radi. Čini se da je to zato što ekran nikada nije bio inicijaliziran, iako je memtest86+ mogao da radi u matičnom sklopu. Ovdje je potrebno pojasniti da je Qemu blok I/O kod napisan u korutinama. Emscripten ima svoju vrlo zeznutu implementaciju, ali je i dalje trebala biti podržana u Qemu kodu, a sada možete otkloniti greške u procesoru: Qemu podržava opcije -kernel, -initrd, -append, s kojim možete pokrenuti Linux ili, na primjer, memtest86+, a da uopće ne koristite blok uređaje. Ali evo problema: u matičnom sklopu se mogao vidjeti izlaz Linux kernela na konzolu s opcijom -nographic, i nema izlaza iz pretraživača na terminal odakle je pokrenut emrun, nije došao. Odnosno, nije jasno: procesor ne radi ili grafički izlaz ne radi. A onda mi je palo na pamet da sačekam malo. Ispostavilo se da "procesor ne spava, već samo polako trepće", a nakon otprilike pet minuta kernel je bacio gomilu poruka na konzolu i nastavio da visi. Postalo je jasno da procesor, generalno, radi, i moramo se ukopati u kod za rad sa SDL2. Nažalost, ne znam kako da koristim ovu biblioteku, pa sam na nekim mjestima morao djelovati nasumično. U nekom trenutku na ekranu je na plavoj pozadini bljesnula linija paralela0, što je nagovijestilo neke misli. Na kraju se ispostavilo da je problem u tome što Qemu otvara nekoliko virtuelnih prozora u jednom fizičkom prozoru, između kojih se možete prebacivati ​​pomoću Ctrl-Alt-n: radi u nativnoj verziji, ali ne i u Emscriptenu. Nakon što se riješite nepotrebnih prozora pomoću opcija -monitor none -parallel none -serial none i uputstva da se nasilno iscrta ceo ekran na svakom kadru, sve je odjednom proradilo.

Coroutines

Dakle, emulacija u pretraživaču radi, ali u njemu ne možete pokrenuti ništa zanimljivo sa jednom disketom, jer nema blok I/O - morate implementirati podršku za korutine. Qemu već ima nekoliko pozadinskih programa korutine, ali zbog prirode JavaScript-a i Emscripten generatora koda, ne možete tek tako početi žonglirati sa stogovima. Čini se da je "sve nestalo, gips se uklanja", ali programeri Emscriptena su se već pobrinuli za sve. Ovo je implementirano prilično smiješno: nazovimo poziv funkcije poput ovog sumnjivim emscripten_sleep i nekoliko drugih koji koriste mehanizam Asyncify, kao i pozive pokazivača i pozive na bilo koju funkciju u kojoj se jedan od prethodna dva slučaja može pojaviti niže niz stek. I sada ćemo prije svakog sumnjivog poziva odabrati asinkroni kontekst, a odmah nakon poziva provjeriti da li je došlo do asinkronog poziva, i ako jeste, sve lokalne varijable ćemo sačuvati u ovom asinkronom kontekstu, naznačiti koju funkciju da prenesemo kontrolu kada trebamo da nastavimo sa izvršavanjem i izađemo iz trenutne funkcije. Ovdje postoji prostor za proučavanje efekta rasipanje — za potrebe nastavka izvršavanja koda nakon povratka iz asinhronog poziva, kompajler generiše „stubove“ funkcije počevši nakon sumnjivog poziva — ovako: ako ima n sumnjivih poziva, funkcija će se proširiti negdje n/2 puta — ovo je još uvijek, ako ne. Imajte na umu da nakon svakog potencijalno asinhronog poziva, morate dodati spremanje nekih lokalnih varijabli u originalnu funkciju. Nakon toga, čak sam morao da napišem jednostavnu skriptu u Pythonu, koja, na osnovu datog skupa posebno preterano korišćenih funkcija koje navodno „ne dozvoljavaju da asinhronija prođe kroz sebe“ (odnosno, promocija steka i sve što sam upravo opisao ne rad u njima), označava pozive kroz pokazivače u kojima prevodilac treba zanemariti funkcije tako da se te funkcije ne smatraju asinhronim. A onda su JS fajlovi ispod 60 MB očito previše - recimo barem 30. Mada, jednom sam postavljao skriptu za sklapanje, i slučajno izbacio opcije linkera, među kojima je bilo -O3. Pokrećem generirani kod, a Chromium troši memoriju i ruši se. Onda sam slučajno pogledao šta pokušava da preuzme... Pa, šta da kažem, i ja bih se ukočio da su me zamolili da pažljivo proučim i optimizujem Javascript od 500+ MB.

Nažalost, provjere u kodu biblioteke podrške Asyncify nisu bile sasvim prijateljske longjmp-s koji se koriste u virtuelnom procesorskom kodu, ali nakon male zakrpe koja onemogućuje ove provjere i nasilno vraća kontekste kao da je sve u redu, kod je proradio. A onda je počela čudna stvar: ponekad su se pokretale provjere u kodu za sinhronizaciju - iste one koje ruše kod ako bi, prema logici izvršenja, trebao biti blokiran - neko je pokušao da zgrabi već uhvaćeni mutex. Srećom, pokazalo se da to nije logičan problem u serijaliziranom kodu - jednostavno sam koristio standardnu ​​funkcionalnost glavne petlje koju je omogućio Emscripten, ali ponekad bi asinhroni poziv potpuno razmotao stog i u tom trenutku bi propao setTimeout iz glavne petlje - tako je kod ušao u iteraciju glavne petlje bez napuštanja prethodne iteracije. Prepisano u beskonačnoj petlji i emscripten_sleep, i problemi sa mutexima su prestali. Kod je čak postao logičniji - uostalom, u stvari, ja nemam neki kod koji priprema sljedeći okvir animacije - procesor samo nešto izračuna i ekran se periodično ažurira. Međutim, problemi nisu tu stali: ponekad bi se izvršavanje Qemu-a jednostavno tiho prekinulo bez ikakvih izuzetaka ili grešaka. U tom trenutku sam odustao od toga, ali, gledajući unaprijed, reći ću da je problem bio sljedeći: kod korutine, zapravo, ne koristi setTimeout (ili barem ne tako često kao što mislite): funkcija emscripten_yield jednostavno postavlja oznaku asinhronog poziva. Čitava poenta je u tome emscripten_coroutine_next nije asinhrona funkcija: interno provjerava zastavicu, resetuje je i prenosi kontrolu tamo gdje je potrebna. To jest, promocija steka se tu završava. Problem je bio u tome što zbog use-after-free, koji se pojavio kada je spremište korutina onemogućeno zbog činjenice da nisam kopirao važnu liniju koda iz postojećeg pozadinskog dijela korutine, funkcija qemu_in_coroutine vratio true kada je u stvari trebao vratiti false. To je dovelo do poziva emscripten_yield, iznad koje nije bilo nikoga na hrpi emscripten_coroutine_next, hrpa se otvorila do samog vrha, ali ne setTimeout, kao što sam već rekao, nije bio izložen.

Generisanje JavaScript koda

A evo, u stvari, obećanog „vraćanja mljevenog mesa nazad“. Ne baš. Naravno, ako pokrenemo Qemu u pretraživaču, i Node.js u njemu, onda ćemo, naravno, nakon generisanja koda u Qemu-u dobiti potpuno pogrešan JavaScript. Ali ipak, neka vrsta obrnute transformacije.

Prvo, malo o tome kako Qemu funkcionira. Oprostite mi odmah: ja nisam profesionalni Qemu programer i moji zaključci mogu biti pogrešni na nekim mjestima. Kako kažu, "mišljenje učenika ne mora da se poklapa sa mišljenjem nastavnika, Peanovom aksiomatikom i zdravim razumom." Qemu ima određeni broj podržanih gostujućih arhitektura i za svaku postoji direktorij poput target-i386. Prilikom izgradnje, možete specificirati podršku za nekoliko gostujućih arhitektura, ali rezultat će biti samo nekoliko binarnih datoteka. Kod za podršku gostujuće arhitekture, zauzvrat, generiše neke interne Qemu operacije, koje TCG (Tiny Code Generator) već pretvara u mašinski kod za arhitekturu domaćina. Kao što je navedeno u readme datoteci koja se nalazi u tcg direktoriju, ovo je prvobitno bio dio običnog C kompajlera, koji je kasnije prilagođen za JIT. Stoga, na primjer, ciljna arhitektura u smislu ovog dokumenta više nije gostujuća, već host arhitektura. U nekom trenutku se pojavila još jedna komponenta - Tiny Code Interpreter (TCI), koja bi trebala izvršiti kod (skoro iste interne operacije) u odsustvu generatora koda za određenu arhitekturu hosta. Zapravo, kako se navodi u njegovoj dokumentaciji, ovaj interpreter možda neće uvijek raditi jednako dobro kao generator JIT koda, ne samo kvantitativno u smislu brzine, već i kvalitativno. Iako nisam siguran da je njegov opis potpuno relevantan.

U početku sam pokušao da napravim punopravni TCG backend, ali sam se brzo zabunio u izvornom kodu i ne sasvim jasnom opisu bajt koda instrukcija, pa sam odlučio da umotam TCI interpreter. To je dalo nekoliko prednosti:

  • kada implementirate generator koda, možete pogledati ne opis instrukcija, već kod interpretatora
  • možete generirati funkcije ne za svaki prijevodni blok na koji se naiđe, već, na primjer, tek nakon stotog izvršenja
  • ako se generirani kod promijeni (a to se čini mogućim, sudeći po funkcijama s nazivima koji sadrže riječ zakrpa), morat ću poništiti generirani JS kod, ali ću barem imati od čega da ga regeneriram

Što se tiče treće tačke, nisam siguran da je zakrpanje moguće nakon prvog izvršavanja koda, ali prve dvije točke su dovoljne.

U početku je kod generiran u obliku velikog prekidača na adresi originalne instrukcije bajt koda, ali onda sam, prisjetivši se članka o Emscriptenu, optimizaciji generiranog JS-a i ponovnom ponavljanju, odlučio generirati više ljudskog koda, pogotovo jer je empirijski pokazalo se da je jedina ulazna tačka u prevodni blok njegov početak. Tek što je rečeno nego urađeno, nakon nekog vremena imali smo generator koda koji je generirao kod sa ifs (iako bez petlji). Ali loša sreća, srušio se, dajući poruku da su upute bile neke pogrešne dužine. Štaviše, posljednja instrukcija na ovom nivou rekurzije je bila brcond. U redu, dodaću identičnu provjeru generiranju ove instrukcije prije i poslije rekurzivnog poziva i... nijedna od njih nije izvršena, ali nakon prekidača assert i dalje nije uspjela. Na kraju, nakon proučavanja generiranog koda, shvatio sam da se nakon prebacivanja pokazivač na trenutnu instrukciju ponovo učitava iz steka i vjerovatno je prepisan generiranim JavaScript kodom. I tako je ispalo. Povećanje bafera sa jednog megabajta na deset nije dovelo ni do čega, a postalo je jasno da generator koda radi u krugovima. Morali smo provjeriti da nismo izašli van granica trenutne TB, a ako jesmo, onda izdati adresu sljedećeg TB sa znakom minus kako bismo mogli nastaviti s izvođenjem. Osim toga, ovo rješava problem "koje generirane funkcije treba poništiti ako se ovaj dio bajtkoda promijenio?" — samo funkcija koja odgovara ovom bloku prijevoda treba biti poništena. Inače, iako sam sve ispravio u Chromiumu (pošto koristim Firefox i lakše mi je koristiti poseban preglednik za eksperimente), Firefox mi je pomogao da ispravim nekompatibilnosti sa standardom asm.js, nakon čega je kod počeo brže raditi u Chromium.

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š uvijek nije završen, ali umoran sam od potajnog dovođenja ove dugoročne konstrukcije do savršenstva. Stoga sam odlučio da objavim ono što za sada imam. Kod je na mjestima malo zastrašujući, jer je ovo eksperiment i nije unaprijed jasno šta treba učiniti. Vjerovatno je onda vrijedno izdavati normalne atomske urezivanja na vrhu neke modernije verzije Qemu-a. U međuvremenu, u Giti postoji nit u formatu bloga: za svaki "nivo" koji je barem nekako pređen, dodan je detaljan komentar na ruskom. Zapravo, ovaj članak je u velikoj mjeri ponavljanje zaključka git log.

Možete probati sve ovdje (čuvajte se saobraćaja).

Šta već radi:

  • x86 virtuelni procesor radi
  • Postoji radni prototip generatora JIT koda od mašinskog koda do JavaScripta
  • Postoji predložak za sastavljanje drugih 32-bitnih gostujućih arhitektura: trenutno se možete diviti Linuxu zbog zamrzavanja MIPS arhitekture u pretraživaču u fazi učitavanja

Šta drugo možete učiniti

  • Ubrzajte emulaciju. Čak i u JIT modu izgleda da radi sporije od Virtual x86 (ali potencijalno postoji cijeli Qemu s puno emuliranog hardvera i arhitekture)
  • Da napravim normalno sučelje - iskreno, nisam dobar web programer, pa sam za sada prepravio standardni Emscripten shell najbolje što mogu
  • Pokušajte pokrenuti složenije Qemu funkcije - umrežavanje, migracija VM-a, itd.
  • UPS: moraćete da pošaljete svoje nekoliko razvoja i izveštaja o greškama Emscripten-u uzvodno, kao što su radili prethodni porteri Qemu-a i drugih projekata. Hvala im što su mogli implicitno koristiti svoj doprinos Emscriptenu kao dio mog zadatka.

izvor: www.habr.com

Dodajte komentar