Qemu.js kun JIT-subteno: plenigaĵo ankoraŭ povas esti returnita

Antaŭ kelkaj jaroj Fabrice Bellard skribita de jslinux estas komputila emulilo skribita en JavaScript. Post tio estis almenaŭ pli Virtuala x86. Sed ĉiuj ili, laŭ mia scio, estis interpretistoj, dum Qemu, verkita multe pli frue de la sama Fabrice Bellard, kaj, verŝajne, ajna memrespekta moderna emulilo, uzas JIT-kompilon de gastokodo en gastigan sistemkodon. Ŝajnis al mi, ke estas tempo efektivigi la kontraŭan taskon rilate al tiu, kiun retumiloj solvas: JIT-kompilo de maŝinkodo en JavaScript, por kiu ŝajnis plej logike porti Qemu. Ŝajnus, kial Qemu, ekzistas pli simplaj kaj afablaj emuliloj - la sama VirtualBox, ekzemple - instalitaj kaj funkcias. Sed Qemu havas plurajn interesajn trajtojn

  • malferma fonto
  • kapablo labori sen kerna pelilo
  • kapablo labori en interpretista reĝimo
  • subteno por granda nombro da kaj gastigaj kaj gastaj arkitekturoj

Koncerne la trian punkton, mi nun povas klarigi, ke fakte, en TCI-reĝimo, ne la gastmaŝino instrukcioj mem estas interpretataj, sed la bajtokodo akirita de ili, sed tio ne ŝanĝas la esencon - por konstrui kaj ruli. Qemu pri nova arkitekturo, se vi bonŝancas, C-kompilo sufiĉas - verkado de kodgeneratoro povas esti prokrastita.

Kaj nun, post du jaroj trankvile tuŝante la fontkodon Qemu en mia libera tempo, aperis funkcianta prototipo, en kiu oni jam povas ruli, ekzemple, Kolibri OS.

Kio estas Emscripten

Nuntempe aperis multaj kompililoj, kies fina rezulto estas JavaScript. Iuj, kiel Type Script, origine estis intencitaj por esti la plej bona maniero skribi por la reto. Samtempe, Emscripten estas maniero preni ekzistantan C aŭ C++-kodon kaj kompili ĝin en foliumile legeblan formon. On ĉi tiu paĝo Ni kolektis multajn havenojn de konataj programoj: tieEkzemple, vi povas rigardi PyPy - cetere ili asertas, ke ili jam havas JIT. Fakte, ne ĉiu programo povas esti simple kompilita kaj rulita en retumilo - ekzistas nombro Trajtoj, kiun vi tamen devas elteni, ĉar la surskribo sur la sama paĝo diras “Emscripten povas esti uzata por kompili preskaŭ ajnan porteblaj C/C++-kodo al JavaScript". Tio estas, ekzistas kelkaj operacioj kiuj estas nedifinita konduto laŭ la normo, sed kutime funkcias sur x86 - ekzemple, nevicigita aliro al variabloj, kiu estas ĝenerale malpermesita en kelkaj arkitekturoj. Ĝenerale , Qemu estas plurplatforma programo kaj , mi volis kredi, kaj ĝi ne jam enhavas multan nedifinitan konduton - prenu ĝin kaj kompilu, poste tuŝu iomete per JIT - kaj vi finis! Sed tio ne estas la kazo...

Unue provu

Ĝenerale, mi ne estas la unua persono, kiu elpensis la ideon porti Qemu al JavaScript. Estis demando demandita en la ReactOS-forumo ĉu tio eblis uzante Emscripten. Eĉ pli frue, estis onidiroj, ke Fabrice Bellard faris tion persone, sed ni parolis pri jslinux, kiu, laŭ mia scio, estas nur provo mane atingi sufiĉan rendimenton en JS, kaj estis skribita de nulo. Poste, Virtuala x86 estis skribita - nemalklarigitaj fontoj estis afiŝitaj por ĝi, kaj, kiel dirite, la pli granda "realismo" de la emulado ebligis uzi SeaBIOS kiel firmvaro. Krome, estis almenaŭ unu provo porti Qemu per Emscripten - mi provis fari tion socketparo, sed evoluo, laŭ mia kompreno, estis frostigita.

Do, ŝajnus, jen la fontoj, jen Emscripten - prenu ĝin kaj kompilu. Sed ekzistas ankaŭ bibliotekoj, de kiuj dependas Qemu, kaj bibliotekoj, de kiuj dependas tiuj bibliotekoj ktp., kaj unu el ili estas libffi, de kiu glib dependas. Estis onidiroj en la Interreto, ke ekzistas unu en la granda kolekto de havenoj de bibliotekoj por Emscripten, sed estis iel malfacile kredi: unue, ĝi ne intencis esti nova kompililo, due, ĝi estis tro malaltnivela. biblioteko por simple preni, kaj kompili al JS. Kaj ne temas nur pri kunigaĵoj - verŝajne, se vi tordas ĝin, por iuj vokaj konvencioj vi povas generi la necesajn argumentojn sur la stako kaj voki la funkcion sen ili. Sed Emscripten estas malfacila afero: por ke la generita kodo aspektu familiara al la retumilo JS-motoroptimumigo, kelkaj lertaĵoj estas uzataj. Aparte, la tielnomita relooping - kodgeneratoro uzanta la ricevitan LLVM IR kun kelkaj abstraktaj transirinstrukcioj provas rekrei kredindajn sejn, buklojn, ktp. Nu, kiel la argumentoj estas transdonitaj al la funkcio? Nature, kiel argumentoj al JS-funkcioj, tio estas, se eble, ne tra la stako.

Komence estis ideo simple skribi anstataŭaĵon por libffi per JS kaj ruli normajn testojn, sed finfine mi konfuziĝis pri kiel fari miajn kapdosierojn por ke ili funkciu kun la ekzistanta kodo - kion mi povas fari, kiel ili diras, "Ĉu la taskoj estas tiel kompleksaj "Ĉu ni estas tiel stultaj?" Mi devis porti libffi al alia arkitekturo, por tiel diri - feliĉe, Emscripten havas ambaŭ makroojn por enlinia asembleo (en Javascript, jes - nu, kia ajn la arkitekturo, do la asemblero), kaj la kapablon ruli kodon generitan sur la flugo. Ĝenerale, post iom da tempo tuŝante platform-dependajn libffi-fragmentojn, mi akiris iom da kompilebla kodo kaj kuris ĝin dum la unua testo, kiun mi renkontis. Je mia surprizo, la testo sukcesis. Mirigita de mia genio - sen ŝerco, ĝi funkciis ekde la unua lanĉo - mi, ankoraŭ ne kredante al miaj okuloj, iris denove rigardi la rezultan kodon, por taksi kie poste fosi. Jen mi duan fojon freneziĝis - la sola afero, kiun mia funkcio faris, estis ffi_call - ĉi tio raportis sukcesan vokon. Ne estis voko mem. Do mi sendis mian unuan tiran peton, kiu korektis eraron en la testo, kiu estas klara por iu ajn olimpika studento - realaj nombroj ne estu komparitaj kiel a == b kaj eĉ kiel a - b < EPS - Vi ankaŭ devas memori la modulon, alie 0 montros tre egala al 1/3... Ĝenerale, mi elpensis certan havenon de libffi, kiu trapasas la plej simplajn provojn, kaj kun kiu glib estas kompilita - mi decidis, ke ĝi estos necesa, mi aldonos ĝin poste. Rigardante antaŭen, mi diros ke, kiel montriĝis, la kompililo eĉ ne inkludis la libffi-funkcion en la fina kodo.

Sed, kiel mi jam diris, estas iuj limigoj, kaj inter la libera uzo de diversaj nedifinitaj kondutoj kaŝiĝis pli malagrabla trajto - JavaScript laŭ dezajno ne subtenas multfadenadon kun komuna memoro. Principe, ĉi tio kutime eĉ povas esti nomata bona ideo, sed ne por porti kodon, kies arkitekturo estas ligita al C-fadenoj. Ĝenerale, Fajrovulpo eksperimentas kun subtenado de komunaj laboristoj, kaj Emscripten havas pthread-efektivigon por ili, sed mi ne volis dependi de ĝi. Mi devis malrapide elradikigi multfadenadon el la kodo Qemu - tio estas, ekscii, kie la fadenoj funkcias, movi la korpon de la buklo kuranta en ĉi tiu fadeno en apartan funkcion kaj voki tiajn funkciojn unu post alia el la ĉefa buklo.

Dua provo

En iu momento, evidentiĝis, ke la problemo ankoraŭ ekzistas, kaj ke hazarde ŝovi lambastonojn ĉirkaŭ la kodo ne kondukus al io bono. Konkludo: ni devas iel sistemigi la procezon de aldono de lambastonoj. Tial, versio 2.4.1, kiu estis freŝa en tiu tempo, estis prenita (ne 2.5.0, ĉar, kiu scias, estos cimoj en la nova versio kiuj ankoraŭ ne estis kaptitaj, kaj mi havas sufiĉe da miaj propraj cimoj. ), kaj la unua afero estis reverki ĝin sekure thread-posix.c. Nu, tio estas, kiel sekura: se iu provis fari operacion kondukantan al blokado, la funkcio tuj estis vokita abort() — kompreneble tio ne solvis ĉiujn problemojn samtempe, sed almenaŭ ĝi estis iel pli agrable ol trankvile ricevi malkonsekvencan datumojn.

Ĝenerale, Emscripten-opcioj estas tre helpemaj por porti kodon al JS -s ASSERTIONS=1 -s SAFE_HEAP=1 - ili kaptas iujn specojn de nedifinita konduto, kiel vokojn al nevicigita adreso (kiu tute ne kongruas kun la kodo por tajpitaj tabeloj kiel HEAP32[addr >> 2] = 1) aŭ voki funkcion kun malĝusta nombro da argumentoj.

Cetere, eraroj de vicigo estas aparta afero. Kiel mi jam diris, Qemu havas "degeneritan" interpretan backend por kodgenerado TCI (eta koda interpretisto), kaj por konstrui kaj ruli Qemu sur nova arkitekturo, se vi bonŝancas, C-kompililo sufiĉas. "se vi estas bonŝanca". Mi estis malbonŝanca, kaj montriĝis, ke TCI uzas nevican aliron kiam analizas ĝian bajtkodon. Tio estas, sur ĉiaj ARM kaj aliaj arkitekturoj kun nepre ebenigita aliro, Qemu kompilas ĉar ili havas normalan TCG-backend kiu generas indiĝenan kodon, sed ĉu TCI funkcios pri ili estas alia demando. Tamen, kiel montriĝis, la TCI-dokumentado klare indikis ion similan. Kiel rezulto, funkciovokoj por nevicigita legado estis aldonitaj al la kodo, kiuj estis malkovritaj en alia parto de Qemu.

Heap detruo

Kiel rezulto, nevicigita aliro al TCI estis korektita, ĉefa buklo estis kreita kiu en victurno nomis la procesoro, RCU kaj iuj aliaj malgrandaj aĵoj. Kaj do mi lanĉas Qemu kun la opcio -d exec,in_asm,out_asm, kio signifas, ke vi devas diri, kiuj blokoj de kodo estas ekzekutitaj, kaj ankaŭ en la momento de la elsendo skribi kia gastkodo estis, kia gastiga kodo fariĝis (ĉi-kaze bajtokodo). Ĝi komenciĝas, ekzekutas plurajn tradukblokojn, skribas la sencimigan mesaĝon, kiun mi lasis, ke RCU nun komenciĝos kaj... kraŝas abort() ene de funkcio free(). Per lupado kun la funkcio free() Ni sukcesis ekscii, ke en la kaplinio de la amasbloko, kiu kuŝas en la ok bajtoj antaŭ la asignita memoro, anstataŭ la blokgrandeco aŭ io simila, estis rubo.

Detruo de la amaso - kiel bela... En tia kazo, estas utila rimedo - de (se eble) la samaj fontoj, kunvenu denaskan duuman kaj rulu ĝin sub Valgrind. Post iom da tempo, la binaro estis preta. Mi lanĉas ĝin kun la samaj opcioj - ĝi kraŝas eĉ dum pravalorigo, antaŭ ol efektive atingi ekzekuto. Kompreneble estas malagrabla - ŝajne, la fontoj ne estis ĝuste la samaj, kio ne estas surpriza, ĉar agordi esploris iomete malsamajn eblojn, sed mi havas Valgrind - unue mi riparos ĉi tiun cimon, kaj poste, se mi bonŝancas. , la originala aperos. Mi prizorgas la samon sub Valgrind... Y-y-y, y-y-y, uh-uh, ĝi komenciĝis, trairis inicialigon normale kaj pluiris preter la origina cimo sen ununura averto pri malĝusta memoraliro, sen mencii pri faloj. La vivo, kiel oni diras, ne preparis min por ĉi tio - kraŝa programo ĉesas kraŝi kiam estas lanĉita sub Walgrind. Kio ĝi estis estas mistero. Mia hipotezo estas, ke unufoje en la ĉirkaŭaĵo de la nuna instrukcio post kraŝo dum inicialigo, gdb montris laboron memset-a kun valida montrilo uzante ĉu mmx, aŭ xmm registroj, tiam eble ĝi estis ia viciga eraro, kvankam ĝi estas ankoraŭ malfacile kredi.

Bone, Valgrind ŝajnas ne helpi ĉi tie. Kaj ĉi tie komenciĝis la plej abomena afero - ĉio ŝajnas eĉ komenciĝi, sed kraŝas pro absolute nekonataj kialoj pro evento kiu povus okazi antaŭ milionoj da instrukcioj. Dum longa tempo, eĉ ne estis klare kiel alproksimiĝi. Fine mi ankoraŭ devis sidiĝi kaj sencimigi. Presi per kio la kaplinio estis reverkita montris, ke ĝi ne aspektis kiel nombro, sed prefere ia binara datumo. Kaj jen, ĉi tiu binara ĉeno estis trovita en la BIOS-dosiero - tio estas, nun eblis diri kun prudenta fido, ke ĝi estis bufro-superfluo, kaj estas eĉ klare, ke ĝi estis skribita al ĉi tiu bufro. Nu, do io tia - en Emscripten, feliĉe, ne ekzistas hazardo de la adresspaco, ankaŭ ne estas truoj en ĝi, do vi povas skribi ie meze de la kodo por eligi datumojn per montrilo de la lasta lanĉo, rigardu la datumojn, rigardu la montrilon, kaj, se ĝi ne ŝanĝiĝis, pripensu. Vere, necesas kelkaj minutoj por ligi post iu ajn ŝanĝo, sed kion vi povas fari? Kiel rezulto, specifa linio estis trovita, kiu kopiis la BIOS de la provizora bufro al la gastmemoro - kaj, efektive, ne estis sufiĉe da spaco en la bufro. Trovi la fonton de tiu stranga bufroadreso rezultigis funkcion qemu_anon_ram_alloc en dosiero oslib-posix.c - la logiko estis jena: foje povas esti utile vicigi la adreson al grandega paĝo de 2 MB en grandeco, por tio ni petos mmap unue iom pli, kaj poste ni redonos la troon kun la helpo munmap. Kaj se tia vicigo ne estas postulata, tiam ni indikos la rezulton anstataŭ 2 MB getpagesize() - mmap ĝi ankoraŭ disdonos vicigitan adreson... Do en Emscripten mmap nur vokoj malloc, sed kompreneble ĝi ne viciĝas sur la paĝo. Ĝenerale, cimo, kiu frustris min dum kelkaj monatoj, estis korektita per ŝanĝo en двух linioj.

Trajtoj de vokado de funkcioj

Kaj nun la procesoro kalkulas ion, Qemu ne kraŝas, sed la ekrano ne ŝaltas, kaj la procesoro rapide iras en buklojn, juĝante laŭ la eligo. -d exec,in_asm,out_asm. Aperis hipotezo: temporizaj interrompoj (aŭ, ĝenerale, ĉiuj interrompoj) ne alvenas. Kaj efektive, se vi malŝraŭbas la interrompojn de la indiĝena asembleo, kiu ial funkciis, vi ricevas similan bildon. Sed ĉi tio tute ne estis la respondo: komparo de la spuroj eldonitaj kun la supra opcio montris, ke la ekzekuttrajektorioj tre frue diverĝis. Ĉi tie oni devas diri, ke komparo de tio, kio estis registrita uzante la lanĉilon emrun sencimiga eligo kun la eligo de la denaska asembleo ne estas tute mekanika procezo. Mi ne scias precize kiel programo funkcianta en retumilo konektas al emrun, sed kelkaj linioj en la eligo rezultas esti rearanĝitaj, do la diferenco en la diferenco ankoraŭ ne estas kialo por supozi ke la trajektorioj diverĝis. Ĝenerale evidentiĝis, ke laŭ la instrukcioj ljmpl estas transiro al malsamaj adresoj, kaj la bajtokodo generita estas fundamente malsama: unu enhavas instrukcion por voki helpan funkcion, la alia ne. Guglinte la instrukciojn kaj studinte la kodon, kiu tradukas ĉi tiujn instrukciojn, evidentiĝis, ke unue, tuj antaŭ ĝi en la registro. cr0 oni faris registradon - ankaŭ uzante helpanton - kiu ŝanĝis la procesoron al protektita reĝimo, kaj due, ke la js-versio neniam ŝanĝis al protektita reĝimo. Sed la fakto estas, ke alia trajto de Emscripten estas ĝia malemo toleri kodon kiel la efektivigon de instrukcioj call en TCI, kiun iu ajn funkciomontrilo rezultigas tipon long long f(int arg0, .. int arg9) - funkcioj devas esti nomitaj kun la ĝusta nombro da argumentoj. Se ĉi tiu regulo estas malobservita, depende de la sencimigaj agordoj, la programo aŭ kraŝos (kio estas bona) aŭ entute vokos la malĝustan funkcion (kiu estos malĝoja sencimigi). Ekzistas ankaŭ tria opcio - ebligi la generacion de envolvaĵoj, kiuj aldonas/forigas argumentojn, sed entute ĉi tiuj envolvaĵoj okupas multe da spaco, malgraŭ tio, ke fakte mi bezonas nur iom pli ol cent envolvaĵojn. Ĉi tio sole estas tre malĝoja, sed montriĝis pli serioza problemo: en la generita kodo de la envolvaĵfunkcioj, la argumentoj estis konvertitaj kaj konvertitaj, sed foje la funkcio kun la generitaj argumentoj ne estis nomita - nu, same kiel en mia efektivigo de libffi. Tio estas, iuj helpantoj simple ne estis ekzekutitaj.

Feliĉe, Qemu havas maŝinlegeblajn listojn de helpantoj en formo de kapdosiero kiel

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

Ili estas uzataj sufiĉe amuzaj: unue, makrooj estas redifinitaj en la plej bizara maniero DEF_HELPER_n, kaj poste ŝaltas helper.h. Laŭ la mezuro, ke la makroo estas vastigita en strukturan inicialigilon kaj komon, kaj tiam tabelo estas difinita, kaj anstataŭ elementoj - #include <helper.h> Kiel rezulto, mi finfine havis ŝancon provi la bibliotekon ĉe la laboro piparsing, kaj skripto estis skribita kiu generas ĝuste tiujn envolvaĵojn por ĝuste la funkcioj por kiuj ili estas bezonataj.

Kaj do, post tio la procesoro ŝajnis funkcii. Ŝajnas esti ĉar la ekrano neniam estis pravigita, kvankam memtest86+ povis funkcii en la indiĝena asembleo. Ĉi tie necesas klarigi, ke la Qemu-bloka I/O-kodo estas skribita en korutinoj. Emscripten havas sian propran tre malfacilan efektivigon, sed ĝi ankoraŭ bezonis esti subtenata en la Qemu-kodo, kaj vi povas sencimigi la procesoron nun: Qemu subtenas opciojn -kernel, -initrd, -append, per kiu vi povas lanĉi Linukson aŭ, ekzemple, memtest86+, tute sen uzi blokajn aparatojn. Sed jen la problemo: en la denaska asembleo oni povus vidi la eliron de Linukso-kerno al la konzolo kun la opcio -nographic, kaj neniu eligo de la retumilo al la terminalo de kie ĝi estis lanĉita emrun, ne venis. Tio estas, ĝi ne estas klara: la procesoro ne funkcias aŭ la grafika eligo ne funkcias. Kaj tiam venis al mi en la kapon atendi iomete. Montriĝis, ke "la procesoro ne dormas, sed simple palpebrumas malrapide", kaj post ĉirkaŭ kvin minutoj la kerno ĵetis amason da mesaĝoj sur la konzolon kaj daŭre pendis. Evidentiĝis, ke la procesoro ĝenerale funkcias, kaj ni devas fosi la kodon por labori kun SDL2. Bedaŭrinde, mi ne scias kiel uzi ĉi tiun bibliotekon, do kelkloke mi devis agi hazarde. En iu momento, la linio paralela0 ekbrilis sur la ekrano sur blua fono, kio sugestis kelkajn pensojn. En la fino, montriĝis, ke la problemo estis, ke Qemu malfermas plurajn virtualajn fenestrojn en unu fizika fenestro, inter kiuj vi povas ŝanĝi per Ctrl-Alt-n: ĝi funkcias en la denaska konstruo, sed ne en Emscripten. Post forigi nenecesajn fenestrojn uzante opciojn -monitor none -parallel none -serial none kaj instrukcioj por forte redesegni la tutan ekranon sur ĉiu kadro, ĉio subite funkciis.

Korutinoj

Do, emulado en la retumilo funkcias, sed vi ne povas ruli ion interesan unu-disketon en ĝi, ĉar ne ekzistas bloko I/O - vi devas efektivigi subtenon por korutinoj. Qemu jam havas plurajn korutinajn backends, sed pro la naturo de JavaScript kaj la Emscripten-kodgeneratoro, vi ne povas simple komenci ĵongli stakojn. Ŝajnus, ke "ĉio malaperis, la gipso estas forigita", sed la programistoj de Emscripten jam prizorgis ĉion. Ĉi tio estas efektivigita sufiĉe amuza: ni nomu funkcion kiel ĉi tiun suspektinda emscripten_sleep kaj pluraj aliaj uzante la Asyncify-mekanismon, same kiel montrilvokojn kaj vokojn al iu ajn funkcio kie unu el la antaŭaj du kazoj povas okazi pli malsupre en la stako. Kaj nun, antaŭ ĉiu suspektinda voko, ni elektos nesinkronan kuntekston, kaj tuj post la alvoko, ni kontrolos ĉu nesinkrona voko okazis, kaj se ĝi okazis, ni konservos ĉiujn lokajn variablojn en ĉi tiu nesinkrona kunteksto, indiku kian funkcion. transdoni kontrolon al kiam ni devas daŭrigi la ekzekuton, kaj eliri la nunan funkcion. Ĉi tie estas amplekso por studi la efikon malŝpari — por la bezonoj de daŭrigo de la ekzekuto de kodo post reveno de nesinkrona voko, la kompililo generas "stubojn" de la funkcio komenciĝantaj post suspektinda voko - jene: se estas n suspektindaj vokoj, tiam la funkcio estos vastigita ie n/2 fojojn — ĉi tio ankoraŭ estas, se ne Memoru, ke post ĉiu eble nesinkrona voko, vi devas aldoni konservadon de iuj lokaj variabloj al la originala funkcio. Poste, mi eĉ devis skribi simplan skripton en Python, kiu, surbaze de donita aro de precipe tro uzataj funkcioj, kiuj supozeble "ne permesas al malsinkronio trapasi sin" (tio estas, stakpromocio kaj ĉio, kion mi ĵus priskribis, ne faras labori en ili), indikas vokojn per montriloj en kiuj funkcioj devus esti ignoritaj de la kompililo por ke ĉi tiuj funkcioj ne estu konsiderataj nesinkronaj. Kaj tiam JS-dosieroj malpli ol 60 MB estas klare tro - ni diru almenaŭ 30. Kvankam, iam mi starigis kunigskripton, kaj hazarde forĵetis la ligilojn, inter kiuj estis -O3. Mi kuras la generitan kodon, kaj Chromium manĝas memoron kaj kraŝas. Mi tiam hazarde rigardis tion, kion li provis elŝuti... Nu, kion mi povas diri, ankaŭ mi frostiĝus, se oni petus min pripense studi kaj optimumigi 500+ MB Javascript.

Bedaŭrinde, la ĉekoj en la Asyncify-subtena biblioteko-kodo ne estis tute amika longjmp-s kiuj estas uzataj en la virtuala procesorokodo, sed post malgranda diakilo kiu malŝaltas ĉi tiujn kontrolojn kaj forte restarigas kuntekstojn kvazaŭ ĉio estus bona, la kodo funkciis. Kaj tiam komenciĝis stranga afero: foje ekfunkciis kontroloj en la sinkroniga kodo - la samaj, kiuj frakasas la kodon se, laŭ la ekzekutlogiko, ĝi devus esti blokita - iu provis kapti jam kaptitan mutekson. Feliĉe, tio montriĝis ne logika problemo en la seriigita kodo - mi simple uzis la norman ĉefan buklofunkcion disponigitan de Emscripten, sed foje la nesinkrona alvoko tute malvolvus la stakon, kaj en tiu momento ĝi malsukcesus. setTimeout de la ĉefbuklo - tiel, la kodo eniris la ĉefbukloripeton sen forlasado de la antaŭa ripeto. Reskribite sur senfina buklo kaj emscripten_sleep, kaj la problemoj kun muteksoj ĉesis. La kodo eĉ fariĝis pli logika - finfine, fakte, mi ne havas kodon, kiu preparas la sekvan animacian kadron - la procesoro nur kalkulas ion kaj la ekrano estas periode ĝisdatigita. Tamen, la problemoj ne ĉesis tie: foje Qemu-ekzekuto simple finiĝis silente sen iuj esceptoj aŭ eraroj. En tiu momento mi rezignis pri ĝi, sed, rigardante antaŭen, mi diros, ke la problemo estis jena: la korutina kodo, fakte, ne uzas setTimeout (aŭ almenaŭ ne tiom ofte kiel vi povus pensi): funkcio emscripten_yield simple metas la nesinkronan alvokan flagon. La tuta afero estas tio emscripten_coroutine_next ne estas nesinkrona funkcio: interne ĝi kontrolas la flagon, restarigas ĝin kaj transdonas kontrolon tien, kie ĝi estas bezonata. Tio estas, la promocio de la stako finiĝas tie. La problemo estis, ke pro uzo-post-libera, kiu aperis kiam la korutina aro estis malŝaltita pro tio, ke mi ne kopiis gravan linion de kodo el la ekzistanta korutina backend, la funkcio qemu_in_coroutine revenis vera kiam fakte ĝi devus esti reveninta malvera. Ĉi tio kondukis al voko emscripten_yield, super kiu estis neniu sur la stako emscripten_coroutine_next, la stako disvolviĝis ĝis la supro, sed ne setTimeout, kiel mi jam diris, ne estis elmontrita.

JavaScript-kodogenerado

Kaj ĉi tie, fakte, estas la promesita "returni la pikitan viandon". Ne vere. Kompreneble, se ni rulas Qemu en la retumilo, kaj Node.js en ĝi, tiam, nature, post kodgenerado en Qemu ni ricevos tute malĝustan JavaScript. Sed tamen, ia inversa transformo.

Unue, iomete pri kiel Qemu funkcias. Bonvolu tuj pardoni min: mi ne estas profesia programisto de Qemu kaj miaj konkludoj povas esti eraraj kelkloke. Kiel oni diras, "la opinio de la studento ne devas koincidi kun la opinio de la instruisto, la aksiomatiko kaj komuna racio de Peano." Qemu havas certan nombron da subtenataj gastaj arkitekturoj kaj por ĉiu estas dosierujo kiel target-i386. Konstruante, vi povas specifi subtenon por pluraj gastaj arkitekturoj, sed la rezulto estos nur pluraj binaroj. La kodo por subteni la gastan arkitekturon, siavice, generas kelkajn internajn Qemu-operaciojn, kiujn la TCG (Ty Code Generator) jam igas maŝinkodon por la gastiga arkitekturo. Kiel dirite en la readme-dosiero situanta en la tcg-dosierujo, tio estis origine parto de regula C-kompililo, kiu poste estis adaptita por JIT. Tial, ekzemple, cela arkitekturo laŭ ĉi tiu dokumento ne plu estas gasta arkitekturo, sed gastiga arkitekturo. En iu momento, alia komponanto aperis - Tiny Code Interpreter (TCI), kiu devus ekzekuti kodon (preskaŭ la samajn internajn operaciojn) en foresto de kodgeneratoro por specifa gastiga arkitekturo. Fakte, kiel ĝia dokumentaro deklaras, ĉi tiu interpretisto eble ne ĉiam funkcias same kiel JIT-kodgeneratoro, ne nur kvante laŭ rapideco, sed ankaŭ kvalite. Kvankam mi ne certas, ke lia priskribo estas tute trafa.

Komence mi provis fari plenrajtan TCG-backend, sed rapide konfuziĝis en la fontkodo kaj ne tute klara priskribo de la bajtokodaj instrukcioj, do mi decidis envolvi la TCI-interpretilon. Ĉi tio donis plurajn avantaĝojn:

  • dum efektivigo de kodogeneratoro, vi povus rigardi ne la priskribon de instrukcioj, sed la interpretistokodon
  • vi povas generi funkciojn ne por ĉiu tradukbloko renkontita, sed, ekzemple, nur post la centa ekzekuto
  • se la generita kodo ŝanĝiĝas (kaj tio ŝajnas esti ebla, se juĝante laŭ la funkcioj kun nomoj enhavantaj la vorton flikaĵo), mi devos malvalidigi la generitan JS-kodon, sed almenaŭ mi havos ion por regeneri ĝin.

Koncerne la trian punkton, mi ne certas, ke flikado eblas post kiam la kodo estas efektivigita unuafoje, sed la unuaj du punktoj sufiĉas.

Komence, la kodo estis generita en la formo de granda ŝaltilo ĉe la adreso de la originala bajtokoda instrukcio, sed poste, rememorante la artikolon pri Emscripten, optimumigo de generita JS kaj relooping, mi decidis generi pli homan kodon, precipe ĉar empirie ĝi montriĝis, ke la sola enirpunkto en la tradukblokon estas ĝia Komenco. Apenaŭ dirite ol farite, post iom da tempo ni havis kodon generatoro kiu generis kodon kun se (kvankam sen bukloj). Sed malbonŝanco, ĝi kraŝis, donante mesaĝon ke la instrukcioj estis de iu malĝusta longo. Krome, la lasta instrukcio ĉe ĉi tiu rekursia nivelo estis brcond. Bone, mi aldonos identan kontrolon al la generacio de ĉi tiu instrukcio antaŭ kaj post la rekursiva alvoko kaj... neniu el ili estis ekzekutita, sed post la aserta ŝaltilo ili ankoraŭ malsukcesis. Fine, studinte la generitan kodon, mi rimarkis, ke post la ŝaltilo, la montrilo al la nuna instrukcio estas reŝargita el la stako kaj verŝajne estas anstataŭita de la generita JavaScript-kodo. Kaj tiel rezultis. Pligrandigi la bufron de unu megabajto al dek ne kondukis al io ajn, kaj evidentiĝis, ke la kodgeneratoro funkcias ronde. Ni devis kontroli, ke ni ne transiris la limojn de la nuna TB, kaj se ni faris, tiam elsendi la adreson de la sekva TB kun minussigno por ke ni povu daŭrigi la ekzekuton. Krome, ĉi tio solvas la problemon "kiu generitaj funkcioj estu malvalidigitaj se ĉi tiu bajtekskodo ŝanĝiĝis?" — nur la funkcio, kiu respondas al ĉi tiu tradukbloko, devas esti malvalidigita. Cetere, kvankam mi elpurigis ĉion en Chromium (ĉar mi uzas Firefox kaj estas por mi pli facile uzi apartan retumilon por eksperimentoj), Firefox helpis min korekti nekongruojn kun la normo asm.js, post kio la kodo ekfunkciis pli rapide en Kromo.

Ekzemplo de generita kodo

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

konkludo

Do, la laboro ankoraŭ ne estas finita, sed mi tedas sekrete perfektigi ĉi tiun longdaŭran konstruon. Tial mi decidis publikigi tion, kion mi havas nuntempe. La kodo estas iom timiga kelkloke, ĉar ĉi tio estas eksperimento, kaj ne klaras antaŭe, kio devas esti farita. Verŝajne, tiam indas elsendi normalajn atomajn kommitaĵojn aldone al iu pli moderna versio de Qemu. Intertempe estas fadeno en la Gita en bloga formato: por ĉiu "nivelo", kiu estis almenaŭ iel trapasita, aldoniĝis detala komento en la rusa. Efektive, ĉi tiu artikolo estas grandparte rerakonto de la konkludo git log.

Vi povas provi ĉion tie (atentu pri trafiko).

Kio jam funkcias:

  • x86 virtuala procesoro funkcianta
  • Estas funkcianta prototipo de JIT-kodgeneratoro de maŝinkodo ĝis JavaScript
  • Estas ŝablono por kunmeti aliajn 32-bitajn gastajn arkitekturojn: nun vi povas admiri Linukso por la arkitekturo MIPS, kiu frostas en la retumilo ĉe la ŝarĝa etapo.

Kion alian vi povas fari

  • Akceli la emuladon. Eĉ en JIT-reĝimo ĝi ŝajnas funkcii pli malrapide ol Virtuala x86 (sed eble ekzistas tuta Qemu kun multe da kopiitaj aparataro kaj arkitekturoj)
  • Por fari normalan interfacon - sincere, mi ne estas bona retejo-programisto, do nuntempe mi refaris la norman Emscripten-ŝelon kiel eble plej bone.
  • Provu lanĉi pli kompleksajn Qemu-funkciojn - reton, VM-migradon ktp.
  • UPS: vi devos sendi viajn malmultajn evoluojn kaj cimraportojn al Emscripten kontraŭflue, kiel antaŭaj portistoj de Qemu kaj aliaj projektoj faris. Dankon al ili pro povi implicite uzi sian kontribuon al Emscripten kiel parto de mia tasko.

fonto: www.habr.com

Aldoni komenton