Qemu.js cu suport pentru JIT: carnea totuși poate fi trecută înapoi

Câțiva ani în urmă, Fabric Bellar a scris jslinux — un emulator PC, scris în JavaScript. După aceea, a fost cel puțin Virtual x86. Dar toate acestea, din câte știu, erau interpretoare, în timp ce Qemu, scris cu mult înainte tot de Fabric Bellar, și probabil orice emulator modern respectabil, folosește compilarea JIT a codului guest în codul sistemului host. Mi s-a părut că este momentul să implementez sarcina inversă față de cea pe care o rezolvă browserele: compilarea JIT a codului mașină în JavaScript, pentru care părea logic să portăm Qemu. S-ar putea să te întrebi de ce tocmai Qemu, când există emulatoare mai simple și mai prietenoase cu utilizatorul — precum VirtualBox, de exemplu — l-ai instalat și funcționează. Dar Qemu are câteva caracteristici interesante

  • surse deschise
  • posibilitatea de a funcționa fără driver de kernel
  • posibilitatea de a funcționa în modul interpretator
  • sprijin pentru un număr mare de arhitecturi atât host, cât și guest

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

Și iată, după doi ani de analiză lentă a surselor Qemu în timpul liber, a apărut un prototip funcțional, în care deja poți porni, de exemplu, Kolibri OS.

Ce este Emscripten

În zilele noastre au apărut multe compilatoare, ale căror rezultate finale sunt JavaScript. Unele, precum TypeScript, au fost gândite de la bun început ca fiind cea mai bună modalitate de a scrie pentru web. În același timp, Emscripten este o modalitate de a lua cod existent în C sau C++ și de a-l compila într-o formă inteligibilă pentru browser. Pe această pagină s-au realizat multe porturi ale programelor cunoscute: aici, de exemplu, poți vedea PyPy — de altfel, se afirmă că au deja JIT. De fapt, nu orice program poate fi pur și simplu compilat și rulat în browser — există o serie de caracteristici, cu care trebuie să te confrunți, totuși, așa cum spune inscripția de pe această pagină "Emscripten poate fi folosit pentru a compila aproape orice portabil Cod C/C++ în JavaScript. Adică există o serie de operațiuni care sunt comportamente nedefinite conform standardului, dar care în general funcționează pe x86 — de exemplu, accesul nealiniat la variabile, care în anumite arhitecturi este complet interzis. În general, Qemu este un program cross-platform și, ar fi bine să credem, nu conține multe comportamente nedefinite — ia-l și compilează, apoi puțin de muncă cu JIT — și e gata! Dar nu a fost așa…

Prima încercare

În general, nu sunt primul care a avut ideea de a porta Qemu pe JavaScript. Pe forumul ReactOS a fost ridicată întrebarea, este posibilă aceasta cu ajutorul Emscripten? Cu ceva timp înainte, au existat zvonuri că Fabrice Bellard ar fi realizat personal acest lucru, dar era vorba despre jslinux, care, după cât îl știu, este o încercare manuală de a obține o performanță suficientă pe JS, scris de la zero. Ulterior a fost scris Virtual x86 — sursele neobfuscate au fost publicate, iar, conform afirmațiilor, 'realismul' mai mare al emulării a permis utilizarea SeaBIOS ca firmware. De asemenea, a existat cel puțin o încercare de a porta Qemu cu ajutorul Emscripten — cineva a încercat să facă acest lucru socketpair, dar, din câte am înțeles, dezvoltarea a fost suspendată.

Așadar, părea că iată sursele, iată Emscripten — ia-le și compilează. Dar există și biblioteci de care Qemu depinde, și biblioteci de care depind acele biblioteci etc., iar una dintre ele este libffi, de care depinde glib. Pe internet circulau zvonuri că în marele arsenal de porturi de biblioteci pentru Emscripten există și aceasta, dar părea greu de crezut: pe de o parte, nu era compilată cu noul compilator, pe de altă parte, este o bibliotecă prea de nivel jos pentru a putea fi compilată pur și simplu în JS. Și nu este vorba doar despre inserțiile în assembler — probabil, dacă te străduiești, poți forma argumentele necesare pe stivă și apela funcția pentru unele convenții de apelare fără ele. Dar Emscripten este o chestiune complicată: pentru ca codul generat să arate familiar pentru optimizatorul motorului JS al browserului, se folosesc unele trucuri. În special, așa-numitul relooping — generatorul de cod încearcă, pe baza IR LLVM obținut, cu unele instrucțiuni abstracte de salturi, să recreeze if-urile și ciclurile plauzibile etc. Și cum se transmit argumentele în funcții? Evident, ca argumente ale funcțiilor JS, adică, pe cât posibil, nu prin stivă.

La început, am avut gândul să scriu pur și simplu o înlocuire a libffi cu JS și să rulez testele standard, dar, în cele din urmă, m-am pierdut în modul în care să îmi fac fișierele header, astfel încât acestea să funcționeze cu codul existent — ce să faci, cum ar zice cineva, "Ori sarcinile sunt atât de complexe, ori noi suntem atât de proști". A trebuit să portez libffi pe încă o arhitectură, dacă se poate spune așa — din fericire, în Emscripten există atât macro-uri pentru assembly inline (pe JavaScript, așa — ce arhitectură, așa și assembler), cât și posibilitatea de a rula cod generat on-the-fly. În general, după ce am muncit ceva timp cu fragmentele dependent de platformă din libffi, am obținut un cod compilabil și l-am rulat la primul test întâlnit. Spre surprinderea mea, testul a trecut cu succes. Aflându-mă în fața propriei mele genialități — nu e o glumă, a funcționat la prima rulare — eu, încă nefăcându-mi credință ochilor, am început să mă uit și o dată la codul rezultat, să evaluez unde mai e de săpat. Aici am fost din nou uimit — singurul lucru pe care funcția mea ffi_call — a raportat un apel reușit. Apelul propriu-zis nu a avut loc. Așa că am trimis prima mea solicitare pull, corectând o greșeală evidentă pentru orice olimpian în test — numerele reale nu trebuie comparate ca a == b și chiar ca a - b < EPS — trebuie să nu uit și modulul, altfel 0 se va dovedi a fi foarte asemănător cu 1/3… În general, am realizat un fel de port libffi, care trece cele mai simple teste și cu care se compilează glib — m-am gândit, voi adăuga mai târziu dacă va fi nevoie. Anticipând, voi spune că, după cum s-a dovedit, compilatorul nu a inclus chiar deloc codul final al funcției libffi.

Dar, așa cum am spus, există anumite limitări, iar printre utilizarea liberă a diverselor comportamente nedefinite s-a strecurat o caracteristică mai puțin plăcută — JavaScript, prin design, nu suportă multi-threading cu memorie comună. În principiu, acest lucru poate fi chiar considerat o idee bună, dar nu pentru portarea codului care are arhitectura bazată pe thread-urile C. În general, în Firefox se desfășoară experimente pentru suportul lucrătorilor partajați, iar implementarea pthread pentru ei există în Emscripten, dar nu voiam să depind de aceasta. A fost necesar să elimin treptat multi-threading din codul Qemu — adică, să caut unde se lansează thread-uri, să mut corpul buclei care se execută în acel thread într-o funcție separată și să chemăm pe rând funcțiile respective din bucla principală.

A doua încercare

Într-un moment, a devenit clar că transportul e tot acolo, și că dispersarea nesistematică a soluțiilor temporare în cod nu va duce la nimic bun. Concluzie: trebuie să sistematizez cumva procesul de adăugare a soluțiilor temporare. Așa că am luat versiunea 2.4.1 (nu 2.5.0, pentru că, cine știe, s-ar putea să existe bug-uri neidentificate în noua versiune, iar eu am destule bug-uri de rezolvat), și în primul rând am rescris-o într-un mod sigur. thread-posix.c. Adică, într-un mod sigur: dacă cineva încerca să efectueze o operațiune care ducea la blocare, se chema imediat funcția abort() — desigur, aceasta nu rezolva imediat toate problemele, dar, cel puțin, era mai plăcut decât să primești o incoerență a datelor în liniște.

În general, în portarea codului pe JS, opțiunile Emscripten sunt foarte utile -s ASSERTIONS=1 -s SAFE_HEAP=1 — ele prind anumite tipuri de comportament nedefinit, cum ar fi accesările la adrese nealiniate (ceea ce nu se potrivește deloc cu codul pentru array-uri tipate, cum ar fi HEAP32[addr >> 2] = 1) sau apelarea unei funcții cu un număr greșit de argumente.

Aproape că am uitat, erorile de aliniere sunt un subiect separat. Așa cum am menționat, în Qemu există un backend "degenerat" pentru interpretarea codului de generare TCI (tiny code interpreter), iar pentru a construi și rula Qemu pe o arhitectură nouă, dacă ai noroc, este suficient un compilator C. Cuvinte cheie "dacă ai noroc". Eu nu am avut noroc și s-a dovedit că TCI folosește acces nealiniat la descompunerea bytecode-ului său. Asta înseamnă că pe arhitecturi precum ARM și alte arhitecturi care necesită acces aliniat, Qemu este compilat pentru că pentru acestea există un backend TCG normal, care generează cod nativ, iar dacă TCI va funcționa pe ele - este încă o întrebare. Totuși, după cum s-a dovedit, în documentația TCI s-a menționat ceva similar. Ca rezultat, în cod au fost adăugate apeluri de funcții pentru citirea nealiniată, care au fost descoperite în altă parte a Qemu.

Distrugerea heap-ului

Ca urmare, accesul nealiniat în TCI a fost corectat, a fost făcut un ciclu principal care a apelat pe rând procesorul, RCU și câteva lucruri mărunte. Și așa, pornesc Qemu cu opțiunea -d exec,in_asm,out_asm, care înseamnă că trebuie să indici ce blocuri de cod sunt executate, precum și, în momentul traducerii, să scrii ce cod de gazdă era, ce cod de gazdă a devenit (în acest caz, bytecode). Acesta se pornește, execută câteva blocuri de traducere, scrie mesajul de depanare lăsat de mine, că RCU va începe acum și... se prăbușește pe abort() în interiorul funcției free(). Printr-o analiză a funcției free() am reușit să descopăr că în antetul blocului heap-ului, care se află în cele opt octeți care precedă memoria alocată, în loc de dimensiunea blocului sau altceva similar, s-a găsit gunoi.

Destrucția unei grămezi - cât de drăguț... Într-un astfel de caz, există un instrument util - să construiesc un binar nativ din (atât cât este posibil) aceleași surse și să-l rulez sub Valgrind. După un timp, binarul a fost gata. Lansez cu aceleași opțiuni - cade din nou la inițializare, fără a ajunge la, de fapt, execuție. Este neplăcut, desigur - se pare că sursele nu erau chiar aceleași, ceea ce nu e surprinzător, având în vedere că configure a detectat câteva opțiuni diferite, dar am Valgrind - întâi repar bug-ul acesta, iar apoi, dacă am noroc, poate și sursa se va manifesta. Rulând totul sub Valgrind... Uuuh, uuuh, ehh, a pornit, a trecut inițializarea normal și a avansat mai departe, fără nicio avertizare de acces incorect la memorie, și să nu mai vorbim despre prăbușiri. Viața nu m-a pregătit pentru așa ceva - un program care se prăbușește încetează să se prăbușească când este rulat sub Valgrind. Ce a fost asta - o enigmă. Ipoteza mea este că, având în vedere că în jurul instrucțiunii curente după prăbușire gdb arăta funcționarea memset-a cu un pointer valid folosind fie mmx, fie xmm registrelor, ar putea fi o eroare de aliniere, deși tot nu mă lasă să cred.

Ok, Valgrind nu pare să ajute aici. Și aici a început partea cea mai neplăcută — totul pare să se lanseze, dar se oprește din motive absolut necunoscute, din cauza unui eveniment care ar fi putut să aibă loc cu milioane de instrucțiuni în urmă. O perioadă a fost chiar dificil să ne dăm seama de unde să începem. În cele din urmă, a trebuit totuși să mă așez și să depanez. Tipărirea a ceea ce a fost scris în antet a arătat că nu părea a fi un număr, ci mai degrabă unele date binare. Și, oh, minune, acest șir binar a fost găsit în fișierul BIOS — adică acum se putea spune cu o oarecare certitudine că a fost o depășire a bufferului, și se știa chiar și ce a fost scris în acel buffer. Apoi, în Emscripten, din fericire, nu există întâmplătoare a spațiului de adresare, nu sunt găuri în acesta, așa că pot să scriu undeva în mijlocul codului pentru a ieși datele de la ultima rulare, să mă uit la date, să mă uit la pointer, și, dacă acesta nu s-a schimbat, să obțin informații pentru reflecție. Totuși, linking-ul după orice modificare durează câteva minute, dar asta e. În rezultat, a fost găsită o linie specifică care copiază BIOS-ul din bufferul temporar în memoria guest — și, într-adevăr, în buffer nu a fost suficient spațiu. Căutarea sursei acelui adresă ciudată a bufferului a dus la funcția qemu_anon_ram_alloc în fișierul oslib-posix.c — logica acolo era astfel: uneori poate fi util să aliniem adresa la o pagină mare de 2 MB, pentru asta să cerem de la mmap mai întâi puțin mai mult, iar apoi să returnăm surplusul cu ajutorul munmap. Și dacă o astfel de aliniere nu este necesară, vom specifica în loc de 2 MB rezultatul getpagesize() — mmap încă va da o adresă aliniată… Așa că în Emscripten mmap doar se apelează malloc, și acesta, desigur, nu aliniază la pagină. În general, bug-ul care m-a deranjat timp de câteva luni s-a rezolvat cu o modificare în două liniile.

Particularitățile apelării funcțiilor

Și acum procesorul calculează ceva, Qemu nu se blochează, dar ecranul nu se activează, iar procesorul se blochează rapid, judecând după ieșire. -d exec,in_asm,out_asm. A apărut o ipoteză: nu sosesc întreruperile temporizatorului (sau poate toate întreruperile). Și, într-adevăr, dacă se scot întreruperile din compilarea nativă, care dintr-un motiv necunoscut funcționa, se obține o imagine similară. Dar dezlegarea s-a dovedit a fi cu totul altceva: compararea trasărilor, emise cu opțiunea menționată mai sus, a arătat că traiectoriile de execuție se despart foarte devreme. Aici trebuie să menționez că compararea ieșirii de depanare înregistrate cu ieșirea compilării native — nu este deloc un proces mecanic. Nu știu exact cum programul rulat în browser se conectează cu emrun ieșirea de depanare, dar unele linii din ieșire ajung să fie aranjate în ordine greșită, ceea ce înseamnă că diferențele în dif comparativ nu sunt suficiente pentru a concluziona că traiectoriile s-au despărțit. În general, a devenit clar că, conform instrucțiunilor emrunljmpl se face tranziția între adrese diferite, iar bytecode-ul generat este fundamental diferit: în unul există instrucțiunea de apelare a funcției helper în C, în celălalt — nu. După ce am căutat instrucțiunea pe Google și am studiat codul care transpune aceste instrucțiuni, a devenit clar că, pe de o parte, înainte de aceasta în registrul cr0 se realiza o scriere — de asemenea cu ajutorul unui helper — care transforma procesorul în modul protejat, iar pe de altă parte, că versiunea js nu a trecut în modul protejat. Problema este că o altă caracteristică a Emscripten este reticența de a tolera codul de tipul implementării instrucțiunii în TCI, care aduce orice pointer la o funcție la tipul call long long f(int arg0, .. int arg9) — funcțiile trebuie apelate cu numărul corect de argumente. În cazul încălcării acestei reguli, în funcție de setările de depanare, programul va ceda (ceea ce este bine) sau va apela o funcție complet greșită (ceea ce va fi trist de depanat). Există și o a treia variantă — activarea generării wrapperelor care adaugă / elimină argumente, dar în total aceste wrapper-uri ocupă destul de mult spațiu, având în vedere că, de fapt, am nevoie de puțin mai mult de o sută de wrapper-uri. Doar aceasta este destul de tristă, dar s-a dovedit a fi o problemă mai gravă: în codul generat al funcțiilor-wrapper, argumentele erau convertite, dar funcția cu argumentele generate uneori nu era apelată — exact ca în implementarea mea a libffi. Adică, unele helpers pur și simplu nu erau apelate. .

Din fericire, Qemu dispune de liste de ajutoare citibile de mașină sub formă de fișier header, precum

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

Acestea sunt utilizate într-un mod destul de amuzant: mai întâi, macrocomenzile sunt redefinite într-un mod foarte ciudat DEF_HELPER_n, și apoi este inclus helper.h. Până acolo încât macrocomanda este extinsă într-un inițializator de structură și o virgulă, după care este definit un array, iar în locul elementelor — #include <helper.h> În consecință, în sfârșit a apărut ocazia de a încerca în practică biblioteca pyparsing, și a fost scris un script care generează wrapper-e exact pentru aceleași funcții pentru care este necesar.

Și astfel, după aceasta, procesorul părea că a început să funcționeze. Părea, deoarece ecranul nu s-a inițializat, deși în construcția nativă a reușit să pornească memtest86+. Este important de precizat că codul de intrare/ieșire blocabil Qemu este scris pe corutine. În Emscripten există o implementare destul de complicată, dar aceasta trebuie susținută deja în codul Qemu, iar procesorul poate fi depanat chiar acum: Qemu acceptă opțiunile -kernel, -initrd, -append, cu ajutorul cărora se poate încărca Linux sau, de exemplu, memtest86+, fără a folosi deloc dispozitive bloc. Dar iată o problemă: în construcția nativă se putea observa ieșirea kernel-ului Linux pe consolă cu opțiunea -nographic, iar din browser nu venea nicio ieșire în terminalul din care a fost lansat emrun, nu se primea. Adică nu era clar: procesorul nu funcționează sau ieșirea grafică. Și apoi mi-a venit în minte să aștept puțin. S-a dovedit că "procesorul nu doarme, ci doar clipește lent", și după aproximativ cinci minute, nucleul a aruncat pe consolă un lot de mesaje și a continuat să se blocheze. A devenit clar că procesorul, în general, funcționează, și trebuie să investighez codul care se ocupă de SDL2. Din păcate, nu știu să folosesc această bibliotecă, așa că a trebuit să acționez pe alocuri pe bază de intuiție. La un moment dat, pe ecran a apărut o linie parallel0 pe un fundal albastru, ceea ce a trezit unele gânduri. În cele din urmă, s-a dovedit că problema era că Qemu deschide mai multe feronii virtuale într-o singură fereastră fizică, între care se poate comuta cu Ctrl-Alt-n: în construcția nativă funcționează, în Emscripten — nu. După ce am eliminat feronile suplimentare cu opțiunile -monitor none -parallel none -serial none și am specificat să se redesenzeze întreaga fereastră la fiecare cadru, totul a început să funcționeze brusc.

Corutine

Așadar, emularea în browser funcționează, dar nu putem rula nimic interesant de pe un disc flop deoarece nu există input/output block-level — trebuie implementată suportul pentru corutine. În Qemu există deja câteva backend-uri pentru corutine, dar din cauza particularităților JavaScript și codului generat de Emscripten, nu putem pur și simplu să începem să jonglăm cu stivele. Ar părea că „totul este pierdut, gipsul se îndepărtează”, dar dezvoltatorii Emscripten s-au ocupat deja de tot. Implementarea acestuia este destul de amuzantă: să numim anumite apeluri de funcții, cum ar fi emscripten_sleep și câteva altele, care folosesc mecanismul Asyncify, precum și apeluri prin pointer și apeluri ale oricărei funcții, unde mai jos în stivă ar putea apărea unul dintre cele două cazuri anterioare. Acum, înainte de fiecare apel suspect, alocăm un context async, iar imediat după apel verificăm dacă a avut loc un apel asincron și, dacă s-a întâmplat, salvăm toate variabilele locale în acest context async, indicăm către ce funcție trecem controlul când va trebui să continuăm execuția și ieșim din funcția curentă. Aici se deschide un spațiu imens pentru studierea efectului fragmentării — pentru necesitățile continuării execuției codului după întoarcerea de la un apel asincron, compilatorul generează "fragmente" ale funcției, care încep după apelul suspect — astfel: dacă există n apeluri suspecte, funcția va fi fragmentată de aproximativ n/2 ori — asta dacă nu luăm în considerare faptul că în funcția de bază trebuie să adăugăm salvarea unei părți din variabilele locale după fiecare apel potențial asincron. Ulterior, a fost necesar chiar să scriu un script simplu în Python, care, în funcție de un set dat de funcții foarte fragmentate, care, presupus, „nu permit asincronitatea să treacă prin ele” (adică în ele nu se declanșează desfășurarea stivei și tot ce am descris anterior), indică ce apeluri prin pointer trebuie ignorate de compilator, astfel încât acele funcții să nu fie considerate asincrone. De asemenea, fișierele JS de 60 MB este deja o exagerare — să fie măcar 30. Totuși, odată am configurat un script de compilare și din greșeală am eliminat opțiunile linker-ului, printre care se număra și -O3. Îmi rulez codul generat, iar Chromium înghite memorie și se blochează. Mai târziu, am aruncat o privire la ceea ce încerca să încarce… Ce pot spune, aș fi rămas și eu blocat dacă mi s-ar fi cerut să studiez și să optimizez JavaScript-ul de peste 500 MB.

Din păcate, verificările din codul bibliotecii de suport Asyncify nu au fost prea prietenoase cu longjmp-urile folosite în codul procesorului virtual, dar după un mic patch care dezactiva aceste verificări și restabiliza forțat contexte ca și cum totul era în regulă, codul a început să funcționeze. Și atunci a început ciudățenia: uneori, verificările din codul de sincronizare se activau — acelea care finalizează forțat codul, dacă din logică ar trebui să se blocheze — cineva încerca să capteze deja un mutex capturat. Din fericire, aceasta nu a fost o problemă logică în codul serializat — pur și simplu am folosit funcționalitatea standard a buclei principale, oferită de Emscripten, dar uneori apelul asincron desfăcea complet stiva și în acel moment se activa setTimeout din bucla principală — astfel, codul intra în iterația buclei principale fără a ieși din iterația anterioară. Am reproiectat pe un ciclu infinit și emscripten_sleep, iar problemele cu mutexurile s-au oprit. Codul a devenit chiar mai logic — deoarece, de fapt, nu am un anumit cod care pregătește urmframele animației — pur și simplu procesorul calculează ceva iar ecranul se actualizează periodic. Cu toate acestea, problemele nu s-au oprit aici: uneori execuția Qemu pur și simplu se încheia fără nicio excepție sau eroare. În acel moment am lăsat-o baltă, dar pentru a anticipa, voi spune că problema era aceasta: codul corutinelor, de fapt, nu folosește deloc setTimeout (sau, cel puțin, nu atât de des pe cât ai putea crede): funcția emscripten_yield pur și simplu setează un semafor pentru apeluri asincrone. Toată sarea este că emscripten_coroutine_next nu este o funcție asincronă: în interiorul său verifică semaforul, îl resetează și transmite controlul unde trebuie. Adică acolo se finalizează desfășurarea stivei. Problema a fost că din cauza unui use-after-free, care se manifesta atunci când pool-ul de corutine era dezactivat, din cauza că nu am copiat o linie importantă de cod din backend-ul corutinei existent, funcția qemu_in_coroutine returna true, când de fapt ar fi trebuit să returneze false. Acest lucru ducea la apelul emscripten_yield, deasupra căruia nu exista o stivă emscripten_coroutine_next, stiva se desfășura până în vârf, dar nu erau setTimeout, așa cum am spus, nu era expusă.

Generarea de cod JavaScript

Iată, de fapt, promisa "întoarcere a cărnii pe dos". De fapt, nu. Desigur, dacă rulezi Qemu în browser și în el — Node.js, atunci, evident, după generarea codului în Qemu, vom obține un JavaScript complet diferit. Totuși, există totuși un fel de conversie inversă.

Pentru început, puțin despre cum funcționează Qemu. Te rog să mă ierți: nu sunt un dezvoltator profesionist Qemu, iar concluziile mele pot fi eronate în unele puncte. Așa cum se spune, "opinia studentului nu trebuie să corespundă cu opinia profesorului, axiomatica lui Peano și bunul simț". Qemu are un anumit număr de arhitecturi guest suportate și pentru fiecare dintre acestea există un director precum target-i386. La compunere, poți specifica suportul pentru mai multe arhitecturi guest, dar rezultatul va fi pur și simplu mai multe binare. Codul pentru suportul arhitecturii guest generează la rândul său anumite operațiuni interne Qemu, pe care TCG (Tiny Code Generator) le transformă în cod mașină pentru arhitectura gazdă. Așa cum se afirmă în fișierul readme situat în directorul tcg, inițial aceasta a fost o parte a unui compilator C obișnuit, care ulterior a fost adaptat pentru JIT. Prin urmare, de exemplu, arhitectura target în termenii acestui document — aceasta nu este o arhitectură guest, ci o arhitectură gazdă. Pe parcurs, a apărut un alt component — Tiny Code Interpreter (TCI), care ar trebui să execute codul (aproape aceleași operațiuni interne) în absența generatoarei de cod pentru arhitectura gazdă specifică. De fapt, așa cum se afirmă în documentația sa, acest interpret poate să nu funcționeze întotdeauna la fel de bine ca generatoarea de cod JIT, nu doar cantitativ în termeni de viteză, ci și calitativ. Deși nu sunt sigur că descrierea sa este complet actualizată.

La început am încercat să creez un backend TCG complet, dar m-am pierdut repede în sursele de cod și în descrierea neclară a instrucțiunilor bytecode, așa că am decis să învăluiesc interpretul TCI. Acest lucru mi-a oferit imediat câteva avantaje:

  • în implementarea generatoarelor de cod, se putea consulta codul interpretului, nu descrierea instrucțiunilor.
  • poți genera funcții nu pentru fiecare bloc întâlnit de translație, ci de exemplu, doar după cea de-a sută execuție
  • în cazul modificării codului generat (ceea ce pare să fie posibil, judecând după funcțiile cu numele care conțin cuvântul patch) va trebui să invalidez codul JS generat, dar cel puțin voi avea de unde să-l regenererez

În legătură cu al treilea punct nu sunt sigur că patcharea este posibilă după ce codul a fost executat prima dată, dar primele două puncte sunt suficiente.

Inițial, codul era generat sub forma unui switch mare pe adresa instrucțiunii de bytecode, dar apoi, amintindu-mi de articolul despre Emscripten, optimizarea JS-ului generat și relooping, am decis să generez un cod mai uman, mai ales că empiric se dovedea că singura intrare în blocul de translație este începutul său. Spus și făcut, după un timp a rezultat un generator de cod care genera cod cu if-uri (deși fără bucle). Dar iată că a apărut o problemă, acesta se oprea, dând un mesaj că instrucțiunea se dovedea a fi de o lungime greșită. În același timp, ultima instrucțiune la acest nivel de recurs era brcond. Bine, voi adăuga o verificare identică în generarea acestei instrucțiuni înainte de apelul recursiv și după și... niciuna dintre ele nu a fost executată, dar după switch pe assert totuși am căzut. În cele din urmă, studiind codul generat, am înțeles că după switch, pointerul la instrucțiunea curentă era resetat de pe stivă și, probabil, suprascris de codul JavaScript generat. Așa a fost. Creșterea buffer-ului de la un megabyte la zece nu a dus la nimic, și a devenit clar că generatorul de cod se rotea pe loc. A trebuit să verific că nu am depășit limitele TB-ului curent, iar dacă am depășit, să emit adresa următorului TB cu semnul minus, pentru a putea continua execuția. În plus, aceasta rezolvă problema „ce funcții generate trebuie invalidate, dacă această bucată de bytecode s-a schimbat?” — trebuie să invalidăm doar funcția care corespunde acestui bloc de translație. Apropo, deși am debugit totul în Chromium (deoarece folosesc Firefox și îmi este mai ușor să folosesc un browser separat pentru experimente), Firefox m-a ajutat să repar incompatibilitățile cu standardul asm.js, după care codul a început să funcționeze mai repede în Chromium.

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

Așadar, munca nu este încă finalizată, dar m-am săturat să perfecționez acest proiect pe ascuns. De aceea, am decis să public temporar ceea ce am. Codul este, în unele locuri, destul de terifiant, deoarece este un experiment, iar nu este clar dinainte ce trebuie făcut. Probabil că ar trebui să fac apoi comenzi atomice normale peste o versiune mai modernă a Qemu. Până acum, există un branch în git sub formă de blog: pentru fiecare "nivel" parcurs, am adăugat un comentariu detaliat în limba română. Practic, acest articol este, într-o mare măsură, o reîntoarcere a concluziilor git log.

Puteți încerca toate acestea aici (atenție, trafic).

Ce funcționează deja acum:

  • Funcționează procesorul virtual x86
  • Există un prototype funcțional de JIT-codogenetizator de la cod mașină la JavaScript
  • Există un șablon pentru compilarea altor arhitecturi gazdă de 32 de biți: puteți chiar acum să admirați Linux-ul pentru arhitectura MIPS agățându-se în browser în timpul încărcării

Ce se mai poate face

  • Accelerați emularea. Chiar și în modul JIT, pare că funcționează mai lent decât Virtual x86 (dar are potențial un întreg Qemu cu mult mai mult hardware și arhitecturi emulate)
  • Să fac un interfață decentă — din păcate, nu sunt un mare dezvoltator web, așa că, pentru moment, am refăcut interfața standard Emscripten cât am putut
  • Încercați să rulați funcții mai complexe Qemu — rețea, migrarea VM etc.
  • UPD: Va trebui să trimit în upstream Emscripten puținele mele contribuții și raporturi de bug-uri, așa cum au făcut anterior portatorii Qemu și alte proiecte. Le mulțumesc pentru că am avut ocazia să folosesc indirect contribuția lor în Emscripten în cadrul sarcinii mele.

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster