Qemu.js s podrškom za JIT: još uvijek možete okrenuti mljeveno meso unatrag

Prije nekoliko godina Fabrice Bellard napisao jslinux je PC emulator napisan u JavaScriptu. Nakon toga je bilo barem više Virtualni x86. Ali svi su oni, koliko ja znam, bili tumači, dok Qemu, koji je mnogo ranije napisao isti Fabrice Bellard, i, vjerojatno, bilo koji moderni emulator koji poštuje sebe, koristi JIT kompilaciju gostujućeg koda u kod glavnog sustava. Činilo mi se da je vrijeme da se implementira suprotan zadatak u odnosu na onaj koji preglednici rješavaju: JIT kompilacija strojnog koda u JavaScript, za što mi se činilo najlogičnije portirati Qemu. Čini se, zašto Qemu, postoje jednostavniji i user-friendly emulatori - isti VirtualBox, na primjer - instaliran i radi. Ali Qemu ima nekoliko zanimljivih značajki

  • otvoreni izvor
  • mogućnost rada bez upravljačkog programa kernela
  • sposobnost rada u načinu tumača
  • podrška za veliki broj host i guest arhitektura

Što se tiče treće točke, sada mogu objasniti da se zapravo u TCI načinu rada ne interpretiraju same instrukcije gostujućeg stroja, već bajt kod dobiven iz njih, ali to ne mijenja bit - kako bi se izgradilo i pokrenulo Qemu na novoj arhitekturi, ako imate sreće, dovoljan je C prevodilac - pisanje generatora koda može se odgoditi.

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

Što je Emscripten

Danas su se pojavili mnogi prevoditelji čiji je krajnji rezultat JavaScript. Neki, poput Type Scripta, izvorno su trebali biti najbolji način pisanja za web. U isto vrijeme, Emscripten je način da se postojeći C ili C++ kod prevede u oblik čitljiv pregledniku. Na ovu stranicu Prikupili smo mnogo portova poznatih programa: здесьNa primjer, možete pogledati PyPy - usput, oni tvrde da već imaju JIT. Zapravo, ne može se svaki program jednostavno kompajlirati i pokrenuti u pregledniku - ima ih nekoliko značajke, s čime se ipak morate pomiriti, jer natpis na istoj stranici kaže "Emscripten se može koristiti za sastavljanje gotovo bilo kojeg prenosiv C/C++ kod u JavaScript". Odnosno, postoji niz operacija koje se nedefiniraju 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 višeplatformski program i , htio sam vjerovati, već ne sadrži mnogo nedefiniranog ponašanja - uzmite ga i kompajlirajte, zatim malo petljajte s JIT-om - i gotovi ste! Ali to nije slučaj...

Prvi pokušaj

Općenito govoreći, nisam prva osoba koja je došla na ideju prenijeti Qemu na JavaScript. Na ReactOS forumu postavljeno je pitanje je li to moguće pomoću Emscriptena. I ranije su se šuškale da je to osobno napravio Fabrice Bellard, ali govorili smo o jslinuxu, koji je, koliko ja znam, samo pokušaj da se ručno postigne dovoljna izvedba u JS-u, a napisan je od nule. Kasnije je napisan Virtual x86 - za njega su objavljeni nemaskirani izvori, i, kao što je navedeno, veći "realizam" emulacije omogućio je korištenje SeaBIOS-a kao firmvera. Osim toga, postojao je barem jedan pokušaj portiranja Qemua pomoću Emscriptena - pokušao sam to učiniti par utičnica, ali razvoj je, koliko sam shvatio, bio zamrznut.

Dakle, čini se, ovdje su izvori, ovdje je Emscripten - uzmite ga i sastavite. Ali postoje i biblioteke o kojima Qemu ovisi, i biblioteke o kojima te biblioteke ovise, itd., a jedna od njih je libffi, o čemu glib ovisi. Na internetu su kružile glasine da postoji jedan u velikoj kolekciji portova knjižnica za Emscripten, ali bilo je nekako teško za povjerovati: prvo, nije trebao biti novi prevodilac, drugo, bio je preniske razine biblioteku samo uzeti i kompajlirati u JS. I to nije samo stvar umetanja sklopa - vjerojatno, ako okrenete, za neke konvencije pozivanja možete generirati potrebne argumente na stogu i pozvati funkciju bez njih. Ali Emscripten je lukava stvar: kako bi generirani kod izgledao poznato optimizatoru JS motora preglednika, koriste se neki trikovi. Konkretno, takozvani relooping - generator koda koji koristi primljeni LLVM IR s nekim apstraktnim uputama prijelaza pokušava ponovno stvoriti uvjerljive if-ove, petlje itd. Pa, kako se argumenti prosljeđuju funkciji? Naravno, kao argumente JS funkcijama, odnosno, ako je moguće, ne kroz stog.

U početku je postojala ideja da jednostavno napišem zamjenu za libffi s JS-om i pokrenem standardne testove, ali na kraju sam se zbunio oko toga kako napraviti svoje datoteke zaglavlja tako da rade s postojećim kodom - što mogu učiniti, kako kažu: "Jesu li zadaci tako složeni "Jesmo li tako glupi?" Morao sam prenijeti libffi na drugu arhitekturu, da tako kažem - na sreću, Emscripten ima i makronaredbe za inline asembler (u Javascriptu, da - pa, bez obzira na arhitekturu, tako asembler), i mogućnost pokretanja koda generiranog u hodu. Općenito, nakon što sam neko vrijeme petljao s fragmentima libffi-ja ovisnim o platformi, dobio sam neki kod koji se mogao kompilirati i pokrenuo ga na prvom testu na koji sam naišao. Na moje iznenađenje, test je bio uspješan. Zaprepašten svojom genijalnošću – bez šale, radilo je od prvog lansiranja – još sam ne vjerujući svojim očima, ponovno otišao pogledati dobiveni kod, da procijenim gdje dalje kopati. Ovdje sam poludio po drugi put – jedino što je moja funkcija učinila ffi_call - ovo je prijavilo uspješan poziv. Nije bilo samog poziva. Tako sam poslao svoj prvi zahtjev za povlačenjem, koji je ispravio pogrešku u testu koja je jasna svakom učeniku Olimpijade - stvarne brojeve ne treba uspoređivati ​​kao a == b pa čak i kako a - b < EPS - također morate zapamtiti modul, inače će 0 ispasti vrlo jednaka 1/3... Općenito, došao sam do određenog porta libffija, koji prolazi najjednostavnije testove, i s kojim je glib sastavljeno - odlučio sam da će biti potrebno, dodati ću ga kasnije. Gledajući unaprijed, reći ću da, kako se ispostavilo, prevodilac nije ni uključio funkciju libffi u konačni kod.

Ali, kao što sam već rekao, postoje neka ograničenja, a među besplatnom upotrebom raznih nedefiniranih ponašanja skrivena je još neugodnija značajka - JavaScript po dizajnu ne podržava višenitnost sa dijeljenom memorijom. U principu, to se obično čak može nazvati dobrom idejom, ali ne za portiranje koda čija je arhitektura vezana uz C niti. Općenito govoreći, Firefox eksperimentira s podrškom za dijeljene radnike, a Emscripten ima implementaciju pthreada za njih, ali nisam želio ovisiti o tome. Morao sam polako iskorijeniti višenitnost iz Qemu koda - to jest, saznati gdje se niti izvode, premjestiti tijelo petlje koja se izvodi u ovoj niti u zasebnu funkciju i pozivati ​​takve funkcije jednu po jednu iz glavne petlje.

Drugi pokušaj

U nekom trenutku postalo je jasno da problem još uvijek postoji i da nasumično guranje štaka oko koda neće dovesti do ničega dobrog. Zaključak: moramo nekako sistematizirati proces dodavanja štaka. Stoga je uzeta tada svježa verzija 2.4.1 (ne 2.5.0 jer tko zna hoće li u novoj verziji biti bugova koji još nisu uhvaćeni, a ja imam dovoljno svojih bugova ), a prva stvar bila je sigurno ga prepisati thread-posix.c. Pa, to je kao sigurno: ako je netko pokušao izvesti operaciju koja dovodi do blokiranja, funkcija je odmah pozvana abort() - naravno, to nije riješilo sve probleme odjednom, ali je barem bilo nekako ugodnije nego tiho primati nedosljedne podatke.

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

Usput, pogreške u poravnanju su zasebno pitanje. Kao što sam već rekao, Qemu ima "degeneriranu" interpretativnu pozadinu za generiranje koda TCI (sićušni interpreter koda), a za izgradnju i pokretanje Qemua na novoj arhitekturi, ako imate sreće, dovoljan je C kompajler. Ključne riječi "ako budeš imao sreće". Nisam imao sreće i ispostavilo se da TCI koristi neusklađeni pristup kada analizira svoj bajt kod. To jest, na svim vrstama ARM-a i drugih arhitektura s nužno izravnanim pristupom, Qemu kompajlira jer imaju normalnu TCG pozadinu koja generira izvorni kod, ali hoće li TCI raditi na njima drugo je pitanje. Međutim, kako se pokazalo, TCI dokumentacija jasno ukazuje na nešto slično. Kao rezultat toga, kodu su dodani funkcijski pozivi za neusklađeno čitanje, koji su otkriveni u drugom dijelu Qemua.

Uništavanje hrpe

Kao rezultat toga, ispravljen je neusklađen pristup TCI-ju, stvorena je glavna petlja koja je pak pozvala procesor, RCU i još neke sitnice. I tako pokrenem Qemu s opcijom -d exec,in_asm,out_asm, što znači da trebate reći koji se blokovi koda izvršavaju, a također u trenutku emitiranja napisati što je kod gosta, što je postao kod hosta (u ovom slučaju bytecode). Pokreće se, izvršava nekoliko blokova prijevoda, piše poruku za otklanjanje pogrešaka koju sam ostavio da će se RCU sada pokrenuti i... ruši se abort() unutar funkcije free(). Petljanjem po funkciji free() Uspjeli smo doznati da je u zaglavlju heap bloka, koji se nalazi u osam bajtova ispred dodijeljene memorije, umjesto veličine bloka ili nečeg sličnog, stajalo smeće.

Uništavanje hrpe - kako slatko... U takvom slučaju, postoji koristan lijek - iz (ako je moguće) istih izvora, sastavite izvornu binarnu datoteku i pokrenite je pod Valgrindom. Nakon nekog vremena, binarni je bio spreman. Pokrećem ga s istim opcijama - ruši se čak i tijekom inicijalizacije, prije nego što zapravo dođe do izvršenja. Neugodno je, naravno - očito izvori nisu bili potpuno isti, što nije iznenađujuće, jer je configure izvidio nešto drugačije opcije, ali ja imam Valgrind - prvo ću popraviti ovu grešku, a onda, ako budem imao sreće , pojavit će se originalni. Istu stvar pokrećem pod Valgrindom... I-i-i, i-i-i, uh-uh, krenulo je, normalno prošlo kroz inicijalizaciju i krenulo dalje od originalnog buga bez ijednog upozorenja o netočnom pristupu memoriji, da ne spominjem padove. Život me, kako kažu, nije pripremio za ovo - program koji se ruši prestaje se rušiti kada se pokrene pod Walgrindom. Što je to bilo, misterij je. Moja hipoteza je da je jednom u blizini trenutne instrukcije nakon pada tijekom inicijalizacije, gdb pokazao da radi memset-a s valjanim pokazivačem pomoću bilo kojeg mmx, ili xmm registara, onda se možda radi o nekakvoj pogrešci poravnanja, iako je u to još uvijek teško povjerovati.

U redu, Valgrind ovdje ne pomaže. I tu je počela najodvratnija stvar - čini se da se sve čak pokreće, ali se ruši iz apsolutno nepoznatih razloga zbog događaja koji se mogao dogoditi prije milijune uputa. Dugo nije bilo jasno ni kako pristupiti. Na kraju sam ipak morao sjesti i ispravljati pogreške. Ispis onoga čime je zaglavlje prepisano pokazalo je da to ne izgleda kao broj, već kao neka vrsta binarnih podataka. I, evo, ovaj binarni niz pronađen je u BIOS datoteci - to jest, sada je bilo moguće s razumnom pouzdanošću reći da se radi o prekoračenju međuspremnika, a čak je jasno da je zapisan u ovaj međuspremnik. 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 zadnjeg pokretanja, pogledajte podatke, pogledajte pokazivač i, ako se nije promijenio, uzmite hranu za razmišljanje. Istina, potrebno je nekoliko minuta za povezivanje nakon svake promjene, ali što 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 doista nije bilo dovoljno mjesta u međuspremniku. Pronalaženje izvora te čudne adrese međuspremnika rezultiralo je funkcijom qemu_anon_ram_alloc u spisu oslib-posix.c - logika je bila sljedeća: ponekad može biti korisno uskladiti adresu s ogromnom stranicom od 2 MB, za to ćemo tražiti mmap prvo još malo, a onda ćemo uz pomoć vratiti višak munmap. A ako takvo usklađivanje nije potrebno, tada ćemo navesti rezultat umjesto 2 MB getpagesize() - mmap svejedno će dati usklađenu adresu... Tako u Emscriptenu mmap samo pozivi malloc, ali naravno ne poravnava se na stranici. Općenito, greška koja me frustrirala nekoliko mjeseci ispravljena je promjenom u двух linije.

Značajke pozivanja funkcija

A 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 timera (ili, općenito, svi prekidi) ne stižu. I doista, ako odvrnete prekide iz izvornog sklopa, koji je iz nekog razloga radio, dobit ćete sličnu sliku. Ali to uopće nije bio odgovor: usporedba tragova izdanih s gornjom opcijom pokazala je da su se putanje izvršenja vrlo rano razišle. Ovdje se mora reći ta usporedba onoga što je snimljeno pomoću launchera emrun otklanjanje pogrešaka izlaza s izlazom izvornog sklopa nije potpuno mehanički proces. Ne znam točno kako se povezuje program koji radi u pregledniku emrun, ali ispada da su neki redovi u izlazu preuređeni, tako da razlika u razlici 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 bajt kod je bitno drugačiji: jedan sadrži instrukciju za pozivanje pomoćne funkcije, drugi ne. Nakon googlanja uputa i proučavanja koda koji prevodi te upute, postalo je jasno da je, prvo, neposredno prije njega u registru cr0 napravljena je snimka - također uz pomoć pomoćnika - koja je prebacila procesor u zaštićeni način rada, i drugo, da se js verzija nikada nije prebacila u zaštićeni način rada. Ali činjenica je da je još jedna značajka Emscriptena njegova nevoljkost da tolerira kod kao što je implementacija uputa call u TCI, što bilo koji pokazivač funkcije rezultira tipom long long f(int arg0, .. int arg9) - funkcije se moraju pozvati s točnim brojem argumenata. Ako se ovo pravilo prekrši, ovisno o postavkama otklanjanja pogrešaka, program će se ili srušiti (što je dobro) ili će uopće pozvati pogrešnu funkciju (što će biti tužno ispravljati pogreške). Postoji i treća opcija - omogućiti generiranje omotača koji dodaju/uklanjaju argumente, ali ukupno ti omotači zauzimaju puno prostora, unatoč činjenici da mi zapravo treba samo nešto više od stotinjak omotača. Samo ovo je vrlo tužno, ali pokazalo se da postoji ozbiljniji problem: u generiranom kodu funkcija omotača, argumenti su se pretvarali i pretvarali, ali ponekad funkcija s generiranim argumentima nije pozvana - pa, baš kao u moja libffi implementacija. Odnosno, neki pomagači jednostavno nisu pogubljeni.

Srećom, Qemu ima strojno čitljive popise pomagača u obliku datoteke zaglavlja poput

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, makronaredbe su redefinirane na najbizarniji način DEF_HELPER_n, a zatim se uključuje helper.h. U mjeri u kojoj se makronaredba proširuje u inicijalizator strukture i zarez, a zatim se definira niz, a umjesto elemenata - #include <helper.h> Kao rezultat toga, napokon sam imao priliku isprobati knjižnicu na poslu pyparsing, a napisana je skripta koja generira upravo te 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 zaslon nikada nije inicijaliziran, iako se memtest86+ mogao pokrenuti u izvornom sklopu. Ovdje je potrebno pojasniti da je Qemu blok I/O kod napisan u korutinama. Emscripten ima svoju vlastitu vrlo lukavu implementaciju, ali je još uvijek trebao biti podržan u Qemu kodu, a sada možete otkloniti pogreške procesora: Qemu podržava opcije -kernel, -initrd, -append, s kojim možete pokrenuti Linux ili, na primjer, memtest86+, bez korištenja blok uređaja uopće. Ali evo problema: u matičnom sklopu moglo se vidjeti izlaz jezgre Linuxa na konzolu s opcijom -nographic, i nema izlaza iz preglednika na terminal s kojeg 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 malo pričekam. Ispostavilo se da "procesor ne spava, već samo polako trepće", a nakon otprilike pet minuta kernel je bacio hrpu poruka na konzolu i nastavio visjeti. Postalo je jasno da procesor, općenito, radi i moramo kopati u kod za rad sa SDL2. Nažalost, ne znam kako koristiti ovu biblioteku, pa sam na nekim mjestima morao djelovati nasumično. U nekom trenutku na ekranu je na plavoj pozadini zabljesnula crta parallel0, što je nagovijestilo neka razmišljanja. Na kraju se pokazalo da je problem u tome što Qemu otvara nekoliko virtualnih 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 upute za prisilno ponovno crtanje cijelog zaslona na svakom okviru, sve je odjednom radilo.

Korutine

Dakle, emulacija u pregledniku radi, ali ne možete pokrenuti ništa zanimljivo na jednoj disketi, jer nema blok I/O - morate implementirati podršku za korutine. Qemu već ima nekoliko korutinskih pozadina, ali zbog prirode JavaScripta i Emscripten generatora koda, ne možete tek tako početi žonglirati snopovima. Čini se da je "sve nestalo, gips se uklanja", ali programeri Emscriptena već su se pobrinuli za sve. Ovo je implementirano prilično smiješno: nazovimo poziv funkcije kao što je ovaj sumnjivim emscripten_sleep i nekoliko drugih koji koriste mehanizam Asyncify, kao i pozive pokazivača i pozive bilo koje funkcije gdje se jedan od prethodna dva slučaja može pojaviti niže niz stog. I sada, prije svakog sumnjivog poziva, odabrat ćemo asinkroni kontekst, a odmah nakon poziva provjerit ćemo je li došlo do asinkronog poziva, i ako jest, spremit ćemo sve lokalne varijable u ovaj asinkroni kontekst, naznačiti koja funkcija za prijenos kontrole kada trebamo nastaviti s izvođenjem i izaći iz trenutne funkcije. Ovdje postoji prostor za proučavanje učinka rasipanje — za potrebe nastavka izvršavanja koda nakon povratka s asinkronog poziva, prevodilac generira “stubove” funkcije koja počinje nakon sumnjivog poziva — ovako: ako ima n sumnjivih poziva, tada će funkcija biti proširena negdje n/2 puta — to je još uvijek, ako ne Imajte na umu da nakon svakog potencijalno asinkronog poziva morate dodati spremanje nekih lokalnih varijabli izvornoj funkciji. Naknadno sam čak morao napisati jednostavnu skriptu u Pythonu, koja, na temelju zadanog skupa posebno pretjerano korištenih funkcija koje navodno "ne dopuštaju asinkroniji da prođe kroz sebe" (to jest, promicanje hrpe i sve što sam upravo opisao ne rad u njima), označava pozive kroz pokazivače u kojima prevoditelj treba zanemariti funkcije kako se te funkcije ne bi smatrale asinkronima. A onda su JS datoteke ispod 60 MB očito previše - recimo barem 30. Iako sam jednom postavljao asemblersku skriptu i slučajno izbacio opcije povezivača, među kojima je bila -O3. Pokrenem generirani kod, a Chromium pojede memoriju i ruši se. Zatim sam slučajno pogledao što je pokušavao preuzeti... Pa, što da kažem, i ja bih se smrznuo da su me zamolili da pažljivo proučim i optimiziram Javascript od 500+ MB.

Nažalost, provjere u kodu biblioteke za podršku Asyncify nisu bile sasvim prijateljske longjmp-s koji se koriste u kodu virtualnog procesora, ali nakon male zakrpe koja onemogućuje te provjere i nasilno vraća kontekste kao da je sve u redu, kod je radio. A onda je počela čudna stvar: ponekad su se aktivirale provjere u sinkronizacijskom kodu - iste one koje ruše kod ako ga, prema logici izvršavanja, treba blokirati - netko je pokušao dohvatiti već uhvaćeni mutex. Srećom, pokazalo se da to nije logičan problem u serijaliziranom kodu - jednostavno sam koristio standardnu ​​funkciju glavne petlje koju je omogućio Emscripten, ali ponekad bi asinkroni poziv potpuno odmotao stog i u tom trenutku ne bi uspio setTimeout iz glavne petlje - dakle, kod je ušao u iteraciju glavne petlje bez napuštanja prethodne iteracije. Prepisao na beskonačnoj petlji i emscripten_sleep, i problemi s muteksima su prestali. Kod je čak postao logičniji - uostalom, zapravo, nemam neki kod koji priprema sljedeći okvir animacije - procesor samo nešto izračuna i ekran se povremeno ažurira. Međutim, problemi tu nisu prestali: ponekad bi se izvršavanje Qemua jednostavno prekinulo tiho bez ikakvih iznimaka ili pogrešaka. U tom trenutku sam odustao od toga, ali, gledajući unaprijed, reći ću da je problem bio sljedeći: korutinski kod, zapravo, ne koristi setTimeout (ili barem ne tako često kao što možda mislite): funkcija emscripten_yield jednostavno postavlja oznaku asinkronog poziva. Cijela poanta je u tome emscripten_coroutine_next nije asinkrona funkcija: interno provjerava zastavu, resetira je i prenosi kontrolu tamo gdje je potrebna. Odnosno, promocija hrpe tu završava. Problem je bio u tome što je zbog korištenja nakon-slobodnog, koji se pojavio kada je skup korutina bio onemogućen zbog činjenice da nisam kopirao važnu liniju koda iz postojeće pozadine korutine, funkcija qemu_in_coroutine vratio true kada je zapravo trebao vratiti false. To je dovelo do poziva emscripten_yield, iznad kojeg nije bilo nikoga na stogu emscripten_coroutine_next, hrpa se razmotala do samog vrha, ali ne setTimeout, kao što sam već rekao, nije bio izložen.

Generiranje JavaScript koda

I evo, zapravo, obećanog "vraćanja mljevenog mesa". Ne baš. Naravno, ako pokrenemo Qemu u pregledniku, i Node.js u njemu, tada ćemo, naravno, nakon generiranja koda u Qemu dobiti potpuno pogrešan JavaScript. Ali ipak, neka vrsta obrnute transformacije.

Prvo, malo o tome kako Qemu radi. Molim vas da mi odmah oprostite: nisam profesionalni Qemu programer i moji zaključci mogu biti pogrešni na nekim mjestima. Kako kažu, “mišljenje učenika ne mora se poklapati s 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 navesti podršku za nekoliko gostujućih arhitektura, ali rezultat će biti samo nekoliko binarnih datoteka. Kôd koji podržava arhitekturu gosta zauzvrat generira neke interne Qemu operacije, koje TCG (Tiny Code Generator) već pretvara u strojni kod za arhitekturu glavnog računala. Kao što je navedeno u datoteci readme koja se nalazi u direktoriju tcg, ovo je izvorno 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 arhitektura gosta, već arhitektura domaćina. U nekom trenutku pojavila se još jedna komponenta - Tiny Code Interpreter (TCI), koja bi trebala izvršavati kod (gotovo iste interne operacije) u nedostatku generatora koda za određenu host arhitekturu. Zapravo, kao što njegova dokumentacija navodi, ovaj tumač možda neće uvijek raditi tako dobro kao JIT generator koda, ne samo kvantitativno u smislu brzine, već i kvalitativno. Iako nisam siguran da je njegov opis sasvim relevantan.

Isprva sam pokušao napraviti potpuni TCG backend, ali sam se brzo zbunio u izvornom kodu i ne sasvim jasnom opisu uputa za bajt kod, pa sam odlučio zamotati TCI interpreter. To je dalo nekoliko prednosti:

  • kada implementirate generator koda, ne biste mogli pogledati opis instrukcija, već kod tumača
  • možete generirati funkcije ne za svaki blok prijevoda na koji naiđete, već, na primjer, tek nakon stotog izvođenja
  • ako se generirani kod promijeni (a to se čini mogućim, sudeći po funkcijama čiji nazivi sadrže riječ patch), morat ću poništiti generirani JS kod, ali ću ga barem imati iz čega regenerirati

Što se tiče treće točke, nisam siguran da je patching moguć nakon što se kod prvi put izvrši, ali prve dvije točke su dovoljne.

U početku je kod bio generiran u obliku velikog prekidača na adresi izvorne instrukcije bajt koda, ali onda sam, sjetivši se članka o Emscriptenu, optimizaciji generiranog JS-a i ponovnom petlji, odlučio generirati više ljudskog koda, pogotovo jer je empirijski pokazalo se da je jedina ulazna točka u blok za prevođenje njegov Start. Rečeno, učinjeno, nakon nekog vremena imali smo generator koda koji je generirao kod s if-ovima (iako bez petlji). Ali loša sreća, srušio se, dajući poruku da su upute bile netočne duljine. Štoviše, zadnja instrukcija na ovoj razini rekurzije bila je brcond. U redu, dodat ću identičnu provjeru generiranju ove instrukcije prije i nakon rekurzivnog poziva i... niti jedna od njih nije izvršena, ali nakon assert switch-a ipak nisu uspjele. Na kraju, nakon proučavanja generiranog koda, shvatio sam da se nakon prebacivanja pokazivač na trenutnu instrukciju ponovno učitava sa stoga i vjerojatno ga prepisuje generirani JavaScript kod. I tako je ispalo. Povećanje međuspremnika s jednog megabajta na deset nije dovelo ni do čega i postalo je jasno da se generator koda vrti u krug. Morali smo provjeriti da nismo izašli izvan granica trenutnog TB, a ako jesmo, onda izdati adresu sljedećeg TB s predznakom minus kako bismo mogli nastaviti s izvršenjem. Osim toga, ovo rješava problem "koje generirane funkcije treba poništiti ako se ovaj dio bajt koda promijenio?" — samo funkcija koja odgovara ovom bloku prijevoda mora biti poništena. Usput, iako sam sve ispravio u Chromiumu (budući da koristim Firefox i lakše mi je koristiti zasebni preglednik za eksperimente), Firefox mi je pomogao ispraviti nekompatibilnosti sa standardom asm.js, nakon čega je kod počeo raditi brže u Krom.

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 dovršen, ali umoran sam od potajnog dovođenja ove dugotrajne konstrukcije do savršenstva. Stoga sam odlučio objaviti ono što za sada imam. Kod je na mjestima pomalo zastrašujući jer je ovo eksperiment i nije unaprijed jasno što treba učiniti. Vjerojatno je onda vrijedno izdati normalne atomske obveze povrh neke modernije verzije Qemua. U međuvremenu, postoji nit u Giti u formatu bloga: za svaku "razinu" koja je barem nekako prošla, dodan je detaljan komentar na ruskom. Zapravo, ovaj je članak u velikoj mjeri prepričavanje zaključka git log.

Sve možete probati здесь (čuvajte se prometa).

Što već radi:

  • x86 virtualni procesor radi
  • Postoji radni prototip generatora JIT koda iz strojnog koda u JavaScript
  • Postoji predložak za sastavljanje drugih 32-bitnih gostujućih arhitektura: trenutno se možete diviti Linuxu zbog MIPS arhitekture koja se smrzava u pregledniku u fazi učitavanja

Što drugo možete učiniti

  • Ubrzajte emulaciju. Čak iu JIT načinu rada čini se da radi sporije od Virtual x86 (ali potencijalno postoji cijeli Qemu s mnogo emuliranog hardvera i arhitektura)
  • Da napravim normalno sučelje - iskreno, nisam dobar web programer, pa sam za sada preradio standardnu ​​Emscripten shell najbolje što sam mogao
  • Pokušajte pokrenuti složenije Qemu funkcije - umrežavanje, VM migracija itd.
  • UPS: morat ćete podnijeti svojih nekoliko razvoja i izvješća o greškama Emscriptenu uzvodno, kao što su to činili prethodni prenositelji Qemua i drugih projekata. Hvala im što mogu implicitno koristiti svoj doprinos Emscriptenu kao dio mog zadatka.

Izvor: www.habr.com

Dodajte komentar