Qemu.js cu suport JIT: puteți în continuare să întoarceți tocată înapoi

Acum câțiva ani Fabrice Bellard scris de jslinux este un emulator de PC scris în JavaScript. După aceea au fost cel puțin mai multe Virtual x86. Dar toți, din câte știu eu, au fost interpreți, în timp ce Qemu, scris mult mai devreme de același Fabrice Bellard și, probabil, orice emulator modern care se respectă, folosește compilarea JIT a codului invitaților în codul sistemului gazdă. Mi s-a părut că era timpul să implementez sarcina opusă în raport cu cea pe care o rezolvă browserele: compilarea JIT a codului mașinii în JavaScript, pentru care mi s-a părut cel mai logic să port Qemu. S-ar părea, de ce Qemu, există emulatori mai simpli și ușor de utilizat - același VirtualBox, de exemplu - instalat și funcționează. Dar Qemu are mai multe caracteristici interesante

  • sursa deschisa
  • capacitatea de a lucra fără un driver de kernel
  • capacitatea de a lucra în modul interpret
  • suport pentru un număr mare de arhitecturi gazdă și oaspete

În ceea ce privește al treilea punct, acum pot să explic că, de fapt, în modul TCI, nu instrucțiunile mașinii invitate în sine sunt interpretate, ci bytecode-ul obținut din ele, dar acest lucru nu schimbă esența - pentru a construi și a rula Qemu pe o nouă arhitectură, dacă aveți noroc, un compilator C este suficient - scrierea unui generator de cod poate fi amânată.

Și acum, după doi ani în care am manipulat pe îndelete codul sursă Qemu în timpul meu liber, a apărut un prototip funcțional, în care puteți rula deja, de exemplu, Kolibri OS.

Ce este Emscripten

În zilele noastre au apărut multe compilatoare, al căror rezultat final este JavaScript. Unele, cum ar fi Type Script, au fost inițial destinate să fie cea mai bună modalitate de a scrie pentru web. În același timp, Emscripten este o modalitate de a prelua codul C sau C++ existent și de a-l compila într-o formă care poate fi citită de browser. Pe această pagină Am colectat multe porturi de programe cunoscute: aiciDe exemplu, vă puteți uita la PyPy - apropo, ei pretind că au deja JIT. De fapt, nu orice program poate fi pur și simplu compilat și rulat într-un browser - există un număr Caracteristici, pe care trebuie să îl suportați, totuși, deoarece inscripția de pe aceeași pagină spune „Emscripten poate fi folosit pentru a compila aproape orice portabil Cod C/C++ la JavaScript". Adică există o serie de operații care sunt comportament nedefinit conform standardului, dar funcționează de obicei pe x86 - de exemplu, acces nealiniat la variabile, care este în general interzis pe unele arhitecturi. În general , Qemu este un program multi-platformă și, am vrut să cred, și nu conține deja o mulțime de comportament nedefinit - luați-l și compilați, apoi schimbați puțin cu JIT - și ați terminat! Dar asta nu este caz...

Prima încercare

În general, nu sunt prima persoană care a venit cu ideea de a porta Qemu la JavaScript. A fost pusă o întrebare pe forumul ReactOS dacă acest lucru a fost posibil folosind Emscripten. Chiar și mai devreme, au existat zvonuri că Fabrice Bellard a făcut asta personal, dar vorbeam despre jslinux, care, din câte știu eu, este doar o încercare de a obține manual performanțe suficiente în JS și a fost scris de la zero. Mai târziu, Virtual x86 a fost scris - au fost postate surse neobfuscate pentru acesta și, după cum sa menționat, „realismul” mai mare al emulării a făcut posibilă utilizarea SeaBIOS ca firmware. În plus, a existat cel puțin o încercare de a porta Qemu folosind Emscripten - am încercat să fac asta pereche de prize, dar dezvoltarea, din câte am înțeles, a fost înghețată.

Deci, s-ar părea, aici sunt sursele, aici este Emscripten - luați-l și compilați. Dar există și biblioteci de care depinde Qemu și biblioteci de care depind acele biblioteci etc., iar una dintre ele este libffi, de care depinde glib. Au existat zvonuri pe Internet că ar exista unul în marea colecție de porturi de biblioteci pentru Emscripten, dar era cumva greu de crezut: în primul rând, nu era intenționat să fie un compilator nou, în al doilea rând, era un nivel prea scăzut. bibliotecă pentru a prelua și compila în JS. Și nu este doar o chestiune de inserții de asamblare - probabil, dacă o răsuciți, pentru unele convenții de apelare puteți genera argumentele necesare pe stivă și puteți apela funcția fără ele. Dar Emscripten este un lucru complicat: pentru a face codul generat să pară familiar pentru optimizatorul de motor JS al browserului, sunt folosite câteva trucuri. În special, așa-numita relooping - un generator de cod care utilizează LLVM IR primit cu unele instrucțiuni abstracte de tranziție încearcă să recreeze if-uri, bucle plauzibile etc. Ei bine, cum sunt transmise argumentele funcției? Desigur, ca argumente pentru funcțiile JS, adică, dacă este posibil, nu prin stivă.

La început a fost ideea de a scrie pur și simplu un înlocuitor pentru libffi cu JS și de a rula teste standard, dar în cele din urmă am fost confuz cu privire la cum să-mi fac fișierele de antet, astfel încât să funcționeze cu codul existent - ce pot face, așa cum spun ei, „Sunt sarcinile atât de complexe „Suntem atât de proști?” A trebuit să port libffi la o altă arhitectură, ca să zic așa - din fericire, Emscripten are atât macrocomenzi pentru asamblarea inline (în Javascript, da - ei bine, indiferent de arhitectură, deci asamblatorul), și capacitatea de a rula cod generat din mers. În general, după ce m-am chinuit de ceva timp cu fragmente libffi dependente de platformă, am primit un cod compilabil și l-am rulat la primul test pe care l-am întâlnit. Spre surprinderea mea, testul a avut succes. Uimit de geniul meu – nu glumă, a funcționat încă de la prima lansare – eu, încă ne-mi cred ochilor, m-am dus să mă uit din nou la codul rezultat, să evaluez unde să sap mai departe. Aici am înnebunit pentru a doua oară - singurul lucru pe care l-a făcut funcția mea a fost ffi_call - aceasta a raportat un apel reușit. Nu a existat nicio chemare în sine. Așa că am trimis prima mea cerere de tragere, care a corectat o eroare în test care este clară pentru orice student la Olimpiada - numerele reale nu trebuie comparate ca a == b și chiar cum a - b < EPS - trebuie să vă amintiți și modulul, altfel 0 se va dovedi a fi foarte mult egal cu 1/3... În general, am venit cu un anumit port al libffi, care trece cele mai simple teste și cu care glib este compilat - am decis că va fi necesar, îl voi adăuga mai târziu. Privind în viitor, voi spune că, după cum sa dovedit, compilatorul nici măcar nu a inclus funcția libffi în codul final.

Dar, așa cum am spus deja, există unele limitări și, printre utilizarea gratuită a diferitelor comportamente nedefinite, a fost ascunsă o caracteristică mai neplăcută - JavaScript prin design nu acceptă multithreading cu memorie partajată. În principiu, aceasta poate fi numită chiar și o idee bună, dar nu pentru portarea codului a cărui arhitectură este legată de firele C. În general, Firefox experimentează cu sprijinirea lucrătorilor partajați, iar Emscripten are o implementare pthread pentru ei, dar nu am vrut să depind de asta. A trebuit să scot încet multithreading din codul Qemu - adică să aflu unde rulează firele, să muți corpul buclei care rulează în acest fir într-o funcție separată și să apelez astfel de funcții una câte una din bucla principală.

A doua încercare

La un moment dat, a devenit clar că problema era încă acolo și că împingerea la întâmplare a cârjelor în jurul codului nu ar duce la nimic bun. Concluzie: trebuie să sistematizăm cumva procesul de adăugare a cârjelor. Prin urmare, a fost luată versiunea 2.4.1, care era proaspătă la acel moment (nu 2.5.0, pentru că, nu se știe niciodată, vor exista bug-uri în noua versiune care nu au fost încă prinse și am destule ale mele bug-uri), iar primul lucru pe care l-am făcut a fost să-l rescriu în siguranță thread-posix.c. Ei bine, adică la fel de sigur: dacă cineva a încercat să efectueze o operațiune care duce la blocare, funcția era apelată imediat abort() - desigur, asta nu a rezolvat toate problemele deodată, dar cel puțin a fost cumva mai plăcut decât primirea în liniște a datelor inconsistente.

În general, opțiunile Emscripten sunt foarte utile în portarea codului în JS -s ASSERTIONS=1 -s SAFE_HEAP=1 - prind unele tipuri de comportament nedefinit, cum ar fi apelurile către o adresă nealiniată (care nu este deloc în concordanță cu codul pentru matricele tastate, cum ar fi HEAP32[addr >> 2] = 1) sau apelarea unei funcții cu un număr greșit de argumente.

Apropo, erorile de aliniere sunt o problemă separată. După cum am spus deja, Qemu are un backend interpretativ „degenerat” pentru generarea de cod TCI (interpret de cod mic), iar pentru a construi și rula Qemu pe o nouă arhitectură, dacă aveți noroc, un compilator C este suficient. "dacă ești norocos". Am avut ghinion și s-a dovedit că TCI folosește acces nealiniat atunci când își analizează bytecode. Adică, pe tot felul de arhitecturi ARM și alte arhitecturi cu acces neapărat nivelat, Qemu se compilează deoarece au un backend normal TCG care generează cod nativ, dar dacă TCI va funcționa pe ele este o altă întrebare. Cu toate acestea, după cum sa dovedit, documentația TCI a indicat în mod clar ceva similar. Drept urmare, la cod au fost adăugate apeluri de funcție pentru citirea nealiniată, care au fost descoperite într-o altă parte a Qemu.

Distrugerea grămezilor

Ca urmare, accesul nealiniat la TCI a fost corectat, a fost creată o buclă principală care, la rândul său, a numit procesor, RCU și alte câteva lucruri mici. Și așa lansez Qemu cu opțiunea -d exec,in_asm,out_asm, ceea ce înseamnă că trebuie să spuneți ce blocuri de cod sunt executate și, de asemenea, în momentul difuzării, să scrieți ce cod de invitat a fost, ce cod de gazdă a devenit (în acest caz, bytecode). Pornește, execută mai multe blocuri de traducere, scrie mesajul de depanare pe care l-am lăsat că RCU va porni acum și... se blochează abort() în interiorul unei funcţii free(). Schimbând cu funcția free() Am reușit să aflăm că în antetul blocului heap, care se află în cei opt octeți precedați memoriei alocate, în loc de dimensiunea blocului sau ceva asemănător, era gunoi.

Distrugerea mormanului - ce drăguț... Într-un astfel de caz, există un remediu util - din aceleași surse (dacă este posibil), asamblați un binar nativ și rulați-l sub Valgrind. După ceva timp, binarul era gata. Îl lansez cu aceleași opțiuni - se blochează chiar și în timpul inițializării, înainte de a ajunge efectiv la execuție. Desigur, este neplăcut - aparent, sursele nu erau exact aceleași, ceea ce nu este surprinzător, deoarece configurarea a scos opțiuni ușor diferite, dar am Valgrind - mai întâi voi remedia acest bug și apoi, dacă am noroc. , va apărea cea originală. Rulez același lucru sub Valgrind... Y-y-y, y-y-y, uh-uh, a început, a trecut prin inițializare normal și a trecut de bug-ul original fără un singur avertisment despre accesul incorect la memorie, ca să nu mai vorbim despre căderi. Viața, după cum se spune, nu m-a pregătit pentru asta - un program de blocare se oprește când este lansat sub Walgrind. Ce a fost este un mister. Ipoteza mea este că, odată în apropierea instrucțiunii curente, după o blocare în timpul inițializării, gdb a arătat funcționalitate memset-a cu un pointer valid folosind oricare mmx, sau xmm registre, atunci poate că a fost un fel de eroare de aliniere, deși este încă greu de crezut.

Bine, Valgrind nu pare să ajute aici. Și aici a început cel mai dezgustător lucru - totul pare chiar să înceapă, dar se prăbușește din motive absolut necunoscute din cauza unui eveniment care s-ar fi putut întâmpla cu milioane de instrucțiuni în urmă. Multă vreme nici nu a fost clar cum să abordăm. Până la urmă, mai trebuia să mă așez și să depanez. Imprimarea cu ce a fost rescris antetul a arătat că nu arăta ca un număr, ci mai degrabă un fel de date binare. Și, iată, acest șir binar a fost găsit în fișierul BIOS - adică acum se putea spune cu încredere rezonabilă că a fost o depășire a bufferului și chiar este clar că a fost scris în acest buffer. Ei bine, atunci ceva de genul acesta - în Emscripten, din fericire, nu există o randomizare a spațiului de adresă, nici nu există găuri în el, așa că puteți scrie undeva în mijlocul codului pentru a scoate date prin indicator de la ultima lansare, uitați-vă la date, uitați-vă la indicator și, dacă nu s-a schimbat, găsiți de gândit. Adevărat, este nevoie de câteva minute pentru a conecta după orice modificare, dar ce poți face? Ca rezultat, a fost găsită o linie specifică care a copiat BIOS-ul din buffer-ul temporar în memoria oaspeților - și, într-adevăr, nu era suficient spațiu în buffer. Găsirea sursei acelei adrese tampon ciudate a dus la o funcție qemu_anon_ram_alloc în dosar oslib-posix.c - logica a fost aceasta: uneori poate fi util să aliniezi adresa la o pagină uriașă de 2 MB, pentru asta vom cere mmap mai întâi puțin, apoi vom returna excesul cu ajutorul munmap. Și dacă o astfel de aliniere nu este necesară, atunci vom indica rezultatul în loc de 2 MB getpagesize() - mmap va da în continuare o adresă aliniată... Deci în Emscripten mmap doar apeluri malloc, dar bineînțeles că nu se aliniază pe pagină. În general, o eroare care m-a frustrat timp de câteva luni a fost corectată printr-o schimbare în двух linii.

Caracteristici ale funcțiilor de apelare

Și acum procesorul numără ceva, Qemu nu se blochează, dar ecranul nu pornește, iar procesorul intră rapid în bucle, judecând după ieșire -d exec,in_asm,out_asm. A apărut o ipoteză: întreruperile timer (sau, în general, toate întreruperile) nu sosesc. Și într-adevăr, dacă deșurubați întreruperile din ansamblul nativ, care din anumite motive a funcționat, obțineți o imagine similară. Dar acesta nu a fost deloc răspunsul: o comparație a urmelor emise cu opțiunea de mai sus a arătat că traiectorii de execuție au diverjat foarte devreme. Aici trebuie spus că comparația a ceea ce a fost înregistrat cu ajutorul lansatorului emrun depanarea ieșirii cu rezultatul ansamblului nativ nu este un proces complet mecanic. Nu știu exact cum se conectează un program care rulează într-un browser emrun, dar unele linii din ieșire se dovedesc a fi rearanjate, așa că diferența de diferență nu este încă un motiv pentru a presupune că traiectoriile au divergentat. În general, a devenit clar că, conform instrucțiunilor ljmpl există o tranziție la adrese diferite, iar bytecode-ul generat este fundamental diferit: unul conține o instrucțiune pentru a apela o funcție de ajutor, celălalt nu. După ce am căutat pe Google instrucțiunile și am studiat codul care traduce aceste instrucțiuni, a devenit clar că, în primul rând, imediat înaintea acestuia în registru cr0 s-a făcut o înregistrare - folosind și un ajutor - care a trecut procesorul în modul protejat și, în al doilea rând, că versiunea js nu a trecut niciodată în modul protejat. Dar adevărul este că o altă caracteristică a Emscripten este reticența sa de a tolera cod, cum ar fi implementarea instrucțiunilor call în TCI, în care orice indicator de funcție are ca rezultat long long f(int arg0, .. int arg9) - funcțiile trebuie apelate cu numărul corect de argumente. Dacă această regulă este încălcată, în funcție de setările de depanare, programul fie se va bloca (ceea ce este bine), fie va apela deloc funcția greșită (ceea ce va fi trist de depanat). Există și o a treia opțiune - activați generarea de wrapper-uri care adaugă/elimină argumente, dar în total aceste wrapper-uri ocupă mult spațiu, în ciuda faptului că de fapt am nevoie doar de puțin mai mult de o sută de wrapper-uri. Numai acest lucru este foarte trist, dar s-a dovedit a fi o problemă mai serioasă: în codul generat al funcțiilor wrapper, argumentele au fost convertite și convertite, dar uneori funcția cu argumentele generate nu a fost numită - ei bine, la fel ca în implementarea mea libffi. Adică, unii ajutoare pur și simplu nu au fost executați.

Din fericire, Qemu are liste de ajutoare care pot fi citite de mașină sub forma unui fișier antet, cum ar fi

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

Sunt folosite destul de amuzant: mai întâi, macrourile sunt redefinite în cel mai bizar mod DEF_HELPER_n, apoi se aprinde helper.h. În măsura în care macro-ul este extins într-un inițializator de structură și o virgulă, apoi este definită o matrice și în loc de elemente - #include <helper.h> Drept urmare, am avut în sfârșit șansa să încerc biblioteca la serviciu pyparsing, și a fost scris un script care generează exact acele wrapper-uri pentru exact funcțiile pentru care sunt necesare.

Și așa, după aceea, procesorul părea să funcționeze. Se pare că ecranul nu a fost niciodată inițializat, deși memtest86+ a putut rula în ansamblul nativ. Aici este necesar să se clarifice faptul că codul I/O bloc Qemu este scris în coroutine. Emscripten are propria sa implementare foarte complicată, dar încă trebuia să fie suportat în codul Qemu și puteți depana procesorul acum: Qemu acceptă opțiuni -kernel, -initrd, -append, cu care puteți porni Linux sau, de exemplu, memtest86+, fără a utiliza deloc dispozitive blocate. Dar iată problema: în ansamblul nativ se poate vedea ieșirea kernel-ului Linux către consolă cu opțiunea -nographic, și nicio ieșire din browser către terminalul de unde a fost lansat emrun, nu a venit. Adică, nu este clar: procesorul nu funcționează sau ieșirea grafică nu funcționează. Și apoi mi-a trecut prin cap să aștept puțin. S-a dovedit că „procesorul nu doarme, ci pur și simplu clipește încet” și, după aproximativ cinci minute, nucleul a aruncat o grămadă de mesaje pe consolă și a continuat să se blocheze. A devenit clar că procesorul, în general, funcționează și trebuie să cercetăm codul pentru lucrul cu SDL2. Din păcate, nu știu cum să folosesc această bibliotecă, așa că în unele locuri a trebuit să acționez la întâmplare. La un moment dat, linia paralel0 a fulgerat pe ecran pe un fundal albastru, ceea ce a sugerat câteva gânduri. În cele din urmă, s-a dovedit că problema a fost că Qemu deschide mai multe ferestre virtuale într-o singură fereastră fizică, între care puteți comuta folosind Ctrl-Alt-n: funcționează în versiunea nativă, dar nu și în Emscripten. După ce ați scăpat de ferestrele inutile folosind opțiuni -monitor none -parallel none -serial none și instrucțiuni pentru a redesena cu forță întregul ecran pe fiecare cadru, totul a funcționat brusc.

Coroutine

Deci, emularea în browser funcționează, dar nu puteți rula nimic interesant cu un singur floppy, deoarece nu există bloc I/O - trebuie să implementați suport pentru coroutine. Qemu are deja mai multe backend-uri, dar datorită naturii JavaScript și a generatorului de cod Emscripten, nu puteți începe pur și simplu să jonglați cu stive. S-ar părea că „totul a dispărut, tencuiala este îndepărtată”, dar dezvoltatorii Emscripten s-au ocupat deja de tot. Acest lucru este implementat destul de amuzant: să numim un apel de funcție ca acesta suspect emscripten_sleep și alte câteva utilizând mecanismul Asyncify, precum și apeluri pointer și apeluri la orice funcție în care unul dintre cele două cazuri anterioare poate apărea mai jos în stivă. Și acum, înainte de fiecare apel suspect, vom selecta un context asincron, iar imediat după apel, vom verifica dacă a avut loc un apel asincron, iar dacă a avut loc, vom salva toate variabilele locale în acest context asincron, indicând ce funcție pentru a transfera controlul când trebuie să continuăm execuția și pentru a părăsi funcția curentă. Aici există spațiu pentru a studia efectul risipă — pentru nevoile de a continua execuția codului după revenirea dintr-un apel asincron, compilatorul generează „stub-uri” ale funcției care pornesc după un apel suspect - astfel: dacă există n apeluri suspecte, atunci funcția va fi extinsă undeva n/2 ori — aceasta este încă, dacă nu. Rețineți că după fiecare apel potențial asincron, trebuie să adăugați salvarea unor variabile locale la funcția originală. Ulterior, chiar a trebuit să scriu un script simplu în Python, care, pe baza unui set dat de funcții deosebit de suprautilizate, care se presupune că „nu permit asincroniei să treacă prin ele însele” (adică promovarea stivei și tot ceea ce tocmai am descris nu fac lucrează în ele), indică apeluri prin pointeri în care funcțiile ar trebui ignorate de compilator, astfel încât aceste funcții să nu fie considerate asincrone. Și apoi fișierele JS sub 60 MB sunt în mod clar prea multe - să spunem cel puțin 30. Deși, odată am configurat un script de asamblare și am aruncat accidental opțiunile de linker, printre care se număra -O3. Rulez codul generat, iar Chromium consumă memorie și se blochează. M-am uitat apoi din greșeală la ceea ce încerca să descarce... Ei bine, ce să spun, și eu m-aș fi blocat dacă mi s-ar fi cerut să studiez și să optimizez cu grijă un JavaScript de peste 500 MB.

Din păcate, verificările din codul bibliotecii de asistență Asyncify nu au fost în totalitate prietenoase longjmp-s care sunt folosite în codul procesorului virtual, dar după un mic patch care dezactivează aceste verificări și restaurează forțat contextele ca și cum totul ar fi bine, codul a funcționat. Și apoi a început un lucru ciudat: uneori au fost declanșate verificări în codul de sincronizare - aceleași care blochează codul dacă, conform logicii de execuție, ar trebui să fie blocat - cineva a încercat să apuce un mutex deja capturat. Din fericire, aceasta s-a dovedit a nu fi o problemă logică în codul serializat - pur și simplu foloseam funcționalitatea standard de buclă principală furnizată de Emscripten, dar uneori apelul asincron ar desface complet stiva și în acel moment ar eșua. setTimeout din bucla principală - astfel, codul a intrat în iterația buclei principale fără a părăsi iterația anterioară. Rescris pe o buclă infinită și emscripten_sleep, iar problemele cu mutexurile au încetat. Codul a devenit chiar mai logic - la urma urmei, de fapt, nu am un cod care să pregătească următorul cadru de animație - procesorul doar calculează ceva și ecranul este actualizat periodic. Cu toate acestea, problemele nu s-au oprit aici: uneori, execuția Qemu se termina pur și simplu fără excepții sau erori. În acel moment am renunțat la el, dar, privind în viitor, voi spune că problema a fost aceasta: codul corutine, de fapt, nu folosește setTimeout (sau cel puțin nu atât de des pe cât ați putea crede): funcția emscripten_yield pur și simplu setează indicatorul de apel asincron. Ideea este că emscripten_coroutine_next nu este o funcție asincronă: intern verifică steag-ul, îl resetează și transferă controlul acolo unde este necesar. Adică promovarea stivei se termină acolo. Problema a fost că din cauza use-after-free, care a apărut atunci când pool-ul de coroutine a fost dezactivat din cauza faptului că nu am copiat o linie importantă de cod din backend-ul coroutine existent, funcția qemu_in_coroutine returnat true când de fapt ar fi trebuit să returneze false. Acest lucru a dus la un apel emscripten_yield, deasupra căreia nu era nimeni pe stivă emscripten_coroutine_next, teancul s-a desfășurat chiar sus, dar nu setTimeout, după cum am spus deja, nu a fost expusă.

Generarea codului JavaScript

Și iată, de fapt, promisiunea „întoarcerea cărnii tocate înapoi”. Nu chiar. Desigur, dacă rulăm Qemu în browser și Node.js în el, atunci, firește, după generarea codului în Qemu vom obține JavaScript complet greșit. Dar totuși, un fel de transformare inversă.

În primul rând, puțin despre cum funcționează Qemu. Vă rog să mă iertați imediat: nu sunt un dezvoltator Qemu profesionist și concluziile mele pot fi eronate în unele locuri. După cum se spune, „opinia elevului nu trebuie să coincidă cu opinia profesorului, cu axiomatica lui Peano și cu bunul simț”. Qemu are un anumit număr de arhitecturi invitate suportate și pentru fiecare există un director asemănător target-i386. Când construiți, puteți specifica suport pentru mai multe arhitecturi invitate, dar rezultatul va fi doar câteva binare. Codul care suportă arhitectura oaspete generează, la rândul său, unele operații interne Qemu, pe care TCG (Tiny Code Generator) le transformă deja în cod de mașină pentru arhitectura gazdă. După cum se menționează în fișierul readme situat în directorul tcg, acesta a fost inițial parte a unui compilator C obișnuit, care a fost adaptat ulterior pentru JIT. Prin urmare, de exemplu, arhitectura țintă în ceea ce privește acest document nu mai este o arhitectură oaspete, ci o arhitectură gazdă. La un moment dat, a apărut o altă componentă - Tiny Code Interpreter (TCI), care ar trebui să execute cod (aproape aceleași operațiuni interne) în absența unui generator de cod pentru o anumită arhitectură gazdă. De fapt, așa cum se arată în documentația sa, acest interpret poate să nu funcționeze întotdeauna la fel de bine ca un generator de cod JIT, nu numai cantitativ în ceea ce privește viteza, ci și calitativ. Deși nu sunt sigur că descrierea lui este complet relevantă.

La început am încercat să fac un backend TCG cu drepturi depline, dar m-am încurcat rapid în codul sursă și o descriere nu complet clară a instrucțiunilor de cod de octet, așa că am decis să închei interpretul TCI. Acest lucru a oferit mai multe avantaje:

  • atunci când implementați un generator de cod, ați putea să vă uitați nu la descrierea instrucțiunilor, ci la codul interpretului
  • puteți genera funcții nu pentru fiecare bloc de traducere întâlnit, ci, de exemplu, numai după a suta execuție
  • dacă codul generat se modifică (și acest lucru pare a fi posibil, judecând după funcțiile cu nume care conțin cuvântul patch), va trebui să invalidez codul JS generat, dar măcar voi avea ceva din care să-l regenerez

În ceea ce privește al treilea punct, nu sunt sigur că patch-ul este posibil după ce codul este executat pentru prima dată, dar primele două puncte sunt suficiente.

Inițial, codul a fost generat sub forma unui comutator mare la adresa instrucțiunii originale bytecode, dar apoi, amintindu-mi articolul despre Emscripten, optimizarea JS-ului generat și relooping, am decis să generez mai mult cod uman, mai ales că empiric s-a dovedit că singurul punct de intrare în blocul de traducere este Start. Nu mai devreme de spus decât de făcut, după un timp am avut un generator de cod care genera cod cu ifs (deși fără bucle). Dar ghinion, s-a prăbușit, dând un mesaj că instrucțiunile erau de o lungime incorectă. Mai mult, ultima instrucțiune la acest nivel de recursivitate a fost brcond. Bine, voi adăuga o verificare identică la generarea acestei instrucțiuni înainte și după apelul recursiv și... niciunul dintre ele nu a fost executat, dar după comutarea assert au eșuat. În cele din urmă, după ce am studiat codul generat, mi-am dat seama că după comutare, indicatorul către instrucțiunea curentă este reîncărcat din stivă și este probabil suprascris de codul JavaScript generat. Și așa s-a dovedit. Creșterea tamponului de la un megaoctet la zece nu a dus la nimic și a devenit clar că generatorul de cod rula în cerc. Trebuia să verificăm că nu am depășit limitele actualului TB, iar dacă am făcut-o, atunci emitem adresa următorului TB cu semnul minus pentru a putea continua execuția. În plus, acest lucru rezolvă problema „ce funcții generate ar trebui să fie invalidate dacă această bucată de bytecode s-a schimbat?” — trebuie invalidată doar funcția care corespunde acestui bloc de traducere. Apropo, deși am depanat totul în Chromium (din moment ce folosesc Firefox și îmi este mai ușor să folosesc un browser separat pentru experimente), Firefox m-a ajutat să corectez incompatibilitățile cu standardul asm.js, după care codul a început să funcționeze mai repede în Crom.

Exemplu de cod generat

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

Concluzie

Deci, lucrarea încă nu este finalizată, dar m-am săturat să duc în secret această construcție pe termen lung la perfecțiune. Prin urmare, am decis să public ceea ce am deocamdată. Codul este puțin înfricoșător pe alocuri, deoarece acesta este un experiment și nu este clar în prealabil ce trebuie făcut. Probabil, atunci merită să emiti comite atomice normale pe lângă o versiune mai modernă a Qemu. Între timp, există un thread în Gita în format blog: pentru fiecare „nivel” care a fost cel puțin trecut cumva, a fost adăugat un comentariu detaliat în limba rusă. De fapt, acest articol este în mare măsură o repovestire a concluziei git log.

Puteți încerca pe toate aici (atentie la trafic).

Ce funcționează deja:

  • procesor virtual x86 rulează
  • Există un prototip funcțional al unui generator de cod JIT de la codul mașinii la JavaScript
  • Există un șablon pentru asamblarea altor arhitecturi guest pe 32 de biți: chiar acum puteți admira Linux pentru arhitectura MIPS care se îngheață în browser în faza de încărcare

Ce altceva poti face

  • Accelerează emularea. Chiar și în modul JIT, pare să ruleze mai lent decât Virtual x86 (dar există potențial un întreg Qemu cu o mulțime de hardware și arhitecturi emulate)
  • Pentru a face o interfață normală - sincer, nu sunt un dezvoltator web bun, așa că deocamdată am refăcut shell-ul standard Emscripten cât am putut.
  • Încercați să lansați funcții Qemu mai complexe - rețea, migrare VM etc.
  • UPD: va trebui să trimiteți câteva dezvoltări și rapoarte de erori către Emscripten în amonte, așa cum au făcut portatorii anteriori ai Qemu și ai altor proiecte. Le mulțumesc pentru că au putut folosi implicit contribuția lor la Emscripten ca parte a sarcinii mele.

Sursa: www.habr.com

Adauga un comentariu