Qemu.js met JIT-ondersteuning: vulsel kan steeds teruggedraai word

'n Paar jaar gelede Fabrice Bellard geskryf deur jslinux is 'n rekenaar-emulator wat in JavaScript geskryf is. Daarna was daar darem meer Virtuele x86. Maar almal van hulle, sover ek weet, was tolke, terwyl Qemu, baie vroeër geskryf deur dieselfde Fabrice Bellard, en waarskynlik enige selfrespekvolle moderne emulator, JIT-samestelling van gaskode in gasheerstelselkode gebruik. Dit het vir my gelyk of dit tyd was om die teenoorgestelde taak te implementeer in verhouding tot die een wat blaaiers oplos: JIT samestelling van masjienkode in JavaScript, waarvoor dit die logiesste gelyk het om Qemu oor te dra. Dit wil voorkom, hoekom Qemu, daar is eenvoudiger en gebruikersvriendelike emulators - dieselfde VirtualBox, byvoorbeeld - geïnstalleer en werk. Maar Qemu het verskeie interessante kenmerke

  • oop bron
  • vermoë om sonder 'n kernbestuurder te werk
  • vermoë om in tolkmodus te werk
  • ondersteuning vir 'n groot aantal gasheer- en gasargitekture

Wat die derde punt betref, kan ek nou verduidelik dat dit in werklikheid, in TCI-modus, nie die gasmasjien-instruksies self is wat geïnterpreteer word nie, maar die greepkode wat daaruit verkry word, maar dit verander nie die essensie nie - om te bou en te laat loop Qemu op 'n nuwe argitektuur, as jy gelukkig is, is 'n C-samesteller genoeg - die skryf van 'n kodegenerator kan uitgestel word.

En nou, na twee jaar se rustige gepeuter met die Qemu-bronkode in my vrye tyd, het 'n werkende prototipe verskyn, waarin jy reeds byvoorbeeld Kolibri OS kan gebruik.

Wat is Emscripten

Deesdae het baie samestellers verskyn, waarvan die eindresultaat JavaScript is. Sommige, soos Type Script, was oorspronklik bedoel om die beste manier te wees om vir die web te skryf. Terselfdertyd is Emscripten 'n manier om bestaande C- of C++-kode te neem en dit saam te stel in 'n blaaier-leesbare vorm. Aan Hierdie bladsy Ons het baie hawens van bekende programme versamel: hierJy kan byvoorbeeld na PyPy kyk – terloops, hulle beweer dat hulle reeds JIT het. Trouens, nie elke program kan eenvoudig saamgestel en in 'n blaaier uitgevoer word nie - daar is 'n aantal kenmerke, wat jy egter moet verdra, aangesien die inskripsie op dieselfde bladsy sê “Emscripten kan gebruik word om byna enige draagbare C/C++-kode na JavaScript". Dit wil sê, daar is 'n aantal bewerkings wat ongedefinieerde gedrag volgens die standaard is, maar gewoonlik op x86 werk - byvoorbeeld ongelynde toegang tot veranderlikes, wat oor die algemeen op sommige argitekture verbied word. Oor die algemeen , Qemu is 'n kruisplatformprogram en , wou ek glo, en dit bevat nie reeds baie ongedefinieerde gedrag nie - neem dit en stel saam, peuter dan 'n bietjie met JIT - en jy is klaar! Maar dit is nie die geval...

Eerste probeer

Oor die algemeen is ek nie die eerste persoon wat met die idee vorendag gekom het om Qemu na JavaScript oor te dra nie. Daar is 'n vraag op die ReactOS-forum gevra of dit moontlik was met Emscripten. Selfs vroeër was daar gerugte dat Fabrice Bellard dit persoonlik gedoen het, maar ons het gepraat van jslinux, wat, sover ek weet, net 'n poging is om handmatig voldoende werkverrigting in JS te bereik, en van nuuts af geskryf is. Later is Virtual x86 geskryf - onverbloemde bronne is daarvoor geplaas, en, soos gesê, het die groter "realisme" van die emulasie dit moontlik gemaak om SeaBIOS as firmware te gebruik. Daarbenewens was daar ten minste een poging om Qemu oor te dra met Emscripten - ek het dit probeer doen sokpaar, maar ontwikkeling, sover ek verstaan, was gevries.

So, wil dit voorkom, hier is die bronne, hier is Emscripten - neem dit en stel saam. Maar daar is ook biblioteke waarvan Qemu afhanklik is, en biblioteke waarvan daardie biblioteke afhanklik is, ens., en een van hulle is libffi, waarvan glib afhang. Daar was gerugte op die internet dat daar een in die groot versameling hawens van biblioteke vir Emscripten was, maar dit was op een of ander manier moeilik om te glo: eerstens was dit nie bedoel om 'n nuwe samesteller te wees nie, tweedens was dit te lae-vlak 'n biblioteek om net op te tel, en saam te stel na JS. En dit is nie net 'n kwessie van samestelling-insetsels nie - waarskynlik, as jy dit draai, kan jy vir sommige oproepkonvensies die nodige argumente op die stapel genereer en die funksie daarsonder oproep. Maar Emscripten is 'n moeilike ding: om die gegenereerde kode bekend te laat lyk vir die blaaier JS-enjinoptimeerder, word 'n paar truuks gebruik. In die besonder, die sogenaamde herloop - 'n kodegenerator wat die ontvangde LLVM IR met 'n paar abstrakte oorgangsinstruksies gebruik, probeer om geloofwaardige ifs, lusse, ens. Wel, hoe word die argumente na die funksie oorgedra? Natuurlik, as argumente vir JS-funksies, dit is, indien moontlik, nie deur die stapel nie.

Aan die begin was daar 'n idee om bloot 'n plaasvervanger vir libffi met JS te skryf en standaardtoetse uit te voer, maar op die ou end het ek verward geraak oor hoe om my koplêers te maak sodat hulle met die bestaande kode sal werk - wat kan ek doen, soos hulle sê, "Is die take so kompleks "Is ons so dom?" Ek moes libffi by wyse van spreke na 'n ander argitektuur oordra - gelukkig het Emscripten beide makro's vir inlynsamestelling (in Javascript, ja - wel, wat ook al die argitektuur, dus die samesteller), en die vermoë om kode wat onmiddellik gegenereer word, te laat loop. Oor die algemeen, nadat ek vir 'n geruime tyd met platform-afhanklike libffi-fragmente gepeuter het, het ek 'n paar saamstelbare kode gekry en dit uitgevoer met die eerste toets wat ek teëgekom het. Tot my verbasing was die toets suksesvol. Ek was verstom oor my genialiteit - geen grap nie, dit het van die eerste bekendstelling af gewerk - het ek, steeds nie my oë geglo nie, weer na die gevolglike kode gaan kyk, om te evalueer waar om volgende te grawe. Hier het ek vir die tweede keer mal geraak – die enigste ding wat my funksie gedoen het, was ffi_call - dit het 'n suksesvolle oproep gerapporteer. Daar was geen oproep self nie. Ek het dus my eerste trekversoek gestuur, wat 'n fout in die toets reggestel het wat vir enige Olimpiade-student duidelik is - reële getalle moet nie vergelyk word as a == b en selfs hoe a - b < EPS - jy moet ook die module onthou, anders sal 0 baie gelyk wees aan 1/3... Oor die algemeen het ek met 'n sekere port van libffi vorendag gekom, wat die eenvoudigste toetse slaag, en waarmee glib is saamgestel - ek het besluit dit sal nodig wees, ek sal dit later byvoeg. As ek vorentoe kyk, sal ek sê dat, soos dit geblyk het, die samesteller nie eens die libffi-funksie in die finale kode ingesluit het nie.

Maar, soos ek reeds gesê het, is daar 'n paar beperkings, en onder die gratis gebruik van verskeie ongedefinieerde gedrag is 'n meer onaangename kenmerk versteek - JavaScript deur ontwerp ondersteun nie multithreading met gedeelde geheue nie. In beginsel kan dit gewoonlik selfs 'n goeie idee genoem word, maar nie vir oordragkode waarvan die argitektuur aan C-drade gekoppel is nie. Oor die algemeen eksperimenteer Firefox met die ondersteuning van gedeelde werkers, en Emscripten het 'n pthread-implementering vir hulle, maar ek wou nie daarop staatmaak nie. Ek moes multithreading stadig uit die Qemu-kode uitroei - dit wil sê, uitvind waar die drade loop, die liggaam van die lus wat in hierdie draad loop, in 'n aparte funksie skuif, en sulke funksies een vir een uit die hooflus oproep.

Tweede poging

Op 'n stadium het dit duidelik geword dat die probleem steeds daar was, en dat die lukraak stoot van krukke om die kode nie tot enige goeie lei nie. Gevolgtrekking: ons moet die proses om krukke by te voeg, op een of ander manier sistematiseer. Daarom is weergawe 2.4.1, wat op daardie stadium vars was, geneem (nie 2.5.0 nie, want wie weet, daar sal foute in die nuwe weergawe wees wat nog nie gevang is nie, en ek het genoeg van my eie foute ), en die eerste ding was om dit veilig te herskryf thread-posix.c. Wel, dit wil sê so veilig: as iemand probeer het om 'n operasie uit te voer wat tot blokkering gelei het, is die funksie dadelik ontbied abort() - dit het natuurlik nie al die probleme gelyktydig opgelos nie, maar dit was ten minste op een of ander manier aangenamer as om stilweg inkonsekwente data te ontvang.

Oor die algemeen is Emscripten-opsies baie nuttig om kode na JS oor te dra -s ASSERTIONS=1 -s SAFE_HEAP=1 - hulle vang sekere tipes ongedefinieerde gedrag op, soos oproepe na 'n nie-belynde adres (wat glad nie ooreenstem met die kode vir getikte skikkings soos HEAP32[addr >> 2] = 1) of 'n funksie met die verkeerde aantal argumente aanroep.

Terloops, belyningsfoute is 'n aparte kwessie. Soos ek reeds gesê het, het Qemu 'n "gedegenereerde" interpretatiewe agterkant vir kodegenerering TCI (klein kode-interpreter), en om Qemu op 'n nuwe argitektuur te bou en te laat loop, as jy gelukkig is, is 'n C-samesteller genoeg. "as jy gelukkig is". Ek was ongelukkig, en dit het geblyk dat TCI ongelynde toegang gebruik wanneer sy greepkode ontleed word. Dit wil sê, op allerhande ARM- en ander argitekture met noodwendig gelyke toegang, stel Qemu saam omdat hulle 'n normale TCG-agterkant het wat inheemse kode genereer, maar of TCI daarop sal werk, is 'n ander vraag. Soos dit egter geblyk het, het die TCI-dokumentasie duidelik iets soortgelyks aangedui. Gevolglik is funksie-oproepe vir ongelynde lees by die kode gevoeg, wat in 'n ander deel van Qemu gevind is.

Hoop vernietiging

As gevolg hiervan is ongelynde toegang tot TCI reggestel, 'n hooflus is geskep wat op sy beurt die verwerker, RCU en 'n paar ander klein dingetjies genoem het. En so begin ek Qemu met die opsie -d exec,in_asm,out_asm, wat beteken dat jy moet sê watter blokke kode uitgevoer word, en ook ten tyde van uitsending om te skryf wat gaskode was, watter gasheerkode geword het (in hierdie geval, bytecode). Dit begin, voer verskeie vertaalblokke uit, skryf die ontfoutingsboodskap wat ek gelos het dat RCU nou sal begin en... crashes abort() binne 'n funksie free(). Deur te peuter met die funksie free() Ons het daarin geslaag om uit te vind dat daar in die kop van die hoopblok, wat lê in die agt grepe wat die toegekende geheue voorafgaan, in plaas van die blokgrootte of iets soortgelyks, vullis was.

Vernietiging van die hoop - hoe oulik ... In so 'n geval is daar 'n nuttige middel - van (indien moontlik) dieselfde bronne, versamel 'n inheemse binêre en voer dit onder Valgrind. Na 'n rukkie was die binêre gereed. Ek begin dit met dieselfde opsies - dit stort ineen selfs tydens inisialisering, voordat dit eintlik uitvoering bereik. Dit is natuurlik onaangenaam - blykbaar was die bronne nie presies dieselfde nie, wat nie verbasend is nie, want konfigurasie het effens verskillende opsies uitgesoek, maar ek het Valgrind - eers sal ek hierdie fout regmaak, en dan, as ek gelukkig is , sal die oorspronklike een verskyn. Ek loop dieselfde ding onder Valgrind ... Y-y-y, y-y-y, uh-uh, dit het begin, normaalweg deur initialisering gegaan en verby die oorspronklike fout beweeg sonder 'n enkele waarskuwing oor verkeerde geheuetoegang, om nie te praat van valle nie. Die lewe, soos hulle sê, het my nie hierop voorberei nie - 'n program wat ineenstort, stop wanneer dit onder Walgrind geloods word. Wat dit was, is 'n raaisel. My hipotese is dat gdb, een keer in die omgewing van die huidige instruksie na 'n ongeluk tydens inisialisering, werk gewys het memset-a met 'n geldige wyser met behulp van óf mmx, of xmm registers, dan was dit miskien 'n soort belyningsfout, hoewel dit steeds moeilik is om te glo.

Goed, dit lyk nie of Valgrind hier help nie. En hier het die walglikste ding begin - dit lyk asof alles selfs begin, maar om absoluut onbekende redes ineenstort as gevolg van 'n gebeurtenis wat miljoene instruksies gelede kon gebeur het. Vir 'n lang tyd was dit nie eens duidelik hoe om te benader nie. Op die ou end moes ek nog gaan sit en ontfout. Die druk waarmee die kopskrif herskryf is, het gewys dat dit nie soos 'n nommer lyk nie, maar eerder 'n soort binêre data. En kyk en kyk, hierdie binêre string is in die BIOS-lêer gevind - dit wil sê, dit was nou moontlik om met redelike vertroue te sê dat dit 'n buffer-oorloop was, en dit is selfs duidelik dat dit na hierdie buffer geskryf is. Wel, dan so iets - in Emscripten is daar gelukkig geen randomisering van die adresspasie nie, daar is ook geen gate in nie, so jy kan iewers in die middel van die kode skryf om data deur wyser uit te voer vanaf die laaste bekendstelling, kyk na die data, kyk na die wyser, en, as dit nie verander het nie, kry stof tot nadenke. Dit neem weliswaar 'n paar minute om te skakel na enige verandering, maar wat kan jy doen? As gevolg hiervan is 'n spesifieke lyn gevind wat die BIOS van die tydelike buffer na die gasgeheue gekopieer het - en daar was inderdaad nie genoeg spasie in die buffer nie. Om die bron van daardie vreemde bufferadres te vind, het 'n funksie tot gevolg gehad qemu_anon_ram_alloc in lêer oslib-posix.c - die logika daar was dit: soms kan dit nuttig wees om die adres in lyn te bring met 'n groot bladsy van 2 MB groot, hiervoor sal ons vra mmap eers 'n bietjie meer, en dan sal ons die bybetaling met die hulp teruggee munmap. En as so 'n belyning nie nodig is nie, sal ons die resultaat in plaas van 2 MB aandui getpagesize() - mmap dit sal steeds 'n belynde adres gee ... So in Emscripten mmap bel net malloc, maar dit pas natuurlik nie op die bladsy nie. Oor die algemeen is 'n fout wat my vir 'n paar maande gefrustreer het, reggestel deur 'n verandering in двух lyne.

Kenmerke van oproepfunksies

En nou tel die verwerker iets, Qemu crash nie, maar die skerm skakel nie aan nie, en die verwerker gaan vinnig in lusse, te oordeel aan die uitset -d exec,in_asm,out_asm. 'n Hipotese het na vore gekom: tydonderbrekings (of, in die algemeen, alle onderbrekings) kom nie aan nie. En inderdaad, as jy die onderbrekings van die inboorlingvergadering losdraai, wat om een ​​of ander rede gewerk het, kry jy 'n soortgelyke prentjie. Maar dit was glad nie die antwoord nie: 'n vergelyking van die spore wat met die bogenoemde opsie uitgereik is, het getoon dat die uitvoeringstrajekte baie vroeg uiteengesit het. Hier moet gesê word dat vergelyking van wat met die lanseerder aangeteken is emrun ontfouting van uitset met die uitset van die inheemse samestelling is nie 'n heeltemal meganiese proses nie. Ek weet nie presies hoe 'n program wat in 'n blaaier loop, aansluit nie emrun, maar sommige lyne in die uitset blyk herrangskik te wees, so die verskil in die verskil is nog nie 'n rede om aan te neem dat die bane gedivergeer het nie. In die algemeen het dit duidelik geword dat volgens die instruksies ljmpl daar is 'n oorgang na verskillende adresse, en die greepkode wat gegenereer word, is fundamenteel anders: een bevat 'n instruksie om 'n helperfunksie te roep, die ander nie. Nadat u die instruksies gegoogle het en die kode bestudeer het wat hierdie instruksies vertaal, het dit duidelik geword dat, eerstens, onmiddellik voor dit in die register cr0 'n opname is gemaak - ook met behulp van 'n helper - wat die verwerker na beskermde modus oorgeskakel het, en tweedens dat die js-weergawe nooit na beskermde modus oorgeskakel het nie. Maar die feit is dat 'n ander kenmerk van Emscripten sy onwilligheid is om kode soos die implementering van instruksies te duld. call in TCI, wat enige funksiewyser lei tot tipe long long f(int arg0, .. int arg9) - funksies moet met die korrekte aantal argumente opgeroep word. As hierdie reël oortree word, afhangende van die ontfoutinstellings, sal die program óf ineenstort (wat goed is) óf die verkeerde funksie enigsins oproep (wat hartseer sal wees om te ontfout). Daar is ook 'n derde opsie - aktiveer die generering van omhulsels wat argumente byvoeg / verwyder, maar in totaal neem hierdie omhulsels baie spasie op, ten spyte van die feit dat ek eintlik net 'n bietjie meer as honderd omhulsels benodig. Dit alleen is baie hartseer, maar daar blyk 'n ernstiger probleem te wees: in die gegenereerde kode van die omhulfunksies is die argumente omgeskakel en omgeskakel, maar soms is die funksie met die gegenereerde argumente nie genoem nie - wel, net soos in my libffi implementering. Dit wil sê, sommige helpers is eenvoudig nie tereggestel nie.

Gelukkig het Qemu masjienleesbare lyste van helpers in die vorm van 'n koplêer soos

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

Hulle word nogal snaaks gebruik: eerstens word makro's op die mees bisarre manier herdefinieer DEF_HELPER_n, en skakel dan aan helper.h. In die mate dat die makro uitgebrei word na 'n struktuurinitialiseerder en 'n komma, en dan word 'n skikking gedefinieer, en in plaas van elemente - #include <helper.h> Gevolglik het ek uiteindelik 'n kans gehad om die biblioteek by die werk te probeer pyparsing, en 'n skrif is geskryf wat presies daardie omhulsels genereer vir presies die funksies waarvoor dit nodig is.

En so, daarna het die verwerker gelyk of dit werk. Dit blyk te wees omdat die skerm nooit geïnisialiseer is nie, alhoewel memtest86+ in die inheemse vergadering kon werk. Hier is dit nodig om te verduidelik dat die Qemu-blok I/O-kode in koroutines geskryf is. Emscripten het sy eie baie moeilike implementering, maar dit moes steeds in die Qemu-kode ondersteun word, en jy kan die verwerker nou ontfout: Qemu ondersteun opsies -kernel, -initrd, -append, waarmee jy Linux of, byvoorbeeld, memtest86+ kan selflaai, sonder om enigsins bloktoestelle te gebruik. Maar hier is die probleem: in die inheemse samestelling kan 'n mens die Linux-kernuitvoer na die konsole sien met die opsie -nographic, en geen uitvoer vanaf die blaaier na die terminaal van waar dit geloods is nie emrun, het nie gekom nie. Dit wil sê, dit is nie duidelik nie: die verwerker werk nie of die grafiese uitvoer werk nie. En toe kom dit by my op om 'n bietjie te wag. Dit het geblyk dat "die verwerker nie slaap nie, maar bloot stadig knip," en na ongeveer vyf minute het die kern 'n klomp boodskappe op die konsole gegooi en aangehou om te hang. Dit het duidelik geword dat die verwerker oor die algemeen werk, en ons moet in die kode grawe om met SDL2 te werk. Ongelukkig weet ek nie hoe om hierdie biblioteek te gebruik nie, so op sommige plekke moes ek lukraak optree. Op 'n stadium het die lyn parallel0 op 'n blou agtergrond op die skerm geflits, wat 'n paar gedagtes voorgestel het. Op die ou end het dit geblyk dat die probleem was dat Qemu verskeie virtuele vensters in een fisiese venster oopmaak, waartussen jy kan wissel met Ctrl-Alt-n: dit werk in die oorspronklike bou, maar nie in Emscripten nie. Na ontslae te raak van onnodige vensters met behulp van opsies -monitor none -parallel none -serial none en instruksies om die hele skerm op elke raam met geweld oor te teken, alles het skielik gewerk.

Coroutines

So, emulasie in die blaaier werk, maar jy kan niks interessants enkel-floppy daarin laat loop nie, want daar is geen blok I/O nie - jy moet ondersteuning vir koroutines implementeer. Qemu het reeds verskeie coroutine-agtergronde, maar as gevolg van die aard van JavaScript en die Emscripten-kodegenerator, kan jy nie net met stapels begin jongleren nie. Dit wil voorkom asof "alles weg is, die gips word verwyder," maar die Emscripten-ontwikkelaars het reeds vir alles gesorg. Dit is nogal snaaks geïmplementeer: kom ons noem 'n funksie-oproep soos hierdie verdag emscripten_sleep en verskeie ander wat die Asyncify-meganisme gebruik, sowel as wyseroproepe en oproepe na enige funksie waar een van die vorige twee gevalle verder af in die stapel kan voorkom. En nou, voor elke verdagte oproep, sal ons 'n asinkroniese konteks kies, en onmiddellik na die oproep sal ons kyk of 'n asynchrone oproep plaasgevind het, en as dit het, sal ons alle plaaslike veranderlikes in hierdie asinchroniese konteks stoor, aandui watter funksie om beheer oor te dra na wanneer ons moet voortgaan met uitvoering, en die huidige funksie verlaat. Dit is waar daar ruimte is om die effek te bestudeer verkwisting - vir die behoeftes van voortgesette kode-uitvoering na terugkeer van 'n asynchrone oproep, genereer die samesteller "stubs" van die funksie wat begin na 'n verdagte oproep - soos volg: as daar n verdagte oproepe is, dan sal die funksie iewers uitgebrei word n/2 keer — dit is steeds, indien nie. Hou in gedagte dat jy na elke potensieel asynchrone oproep, die stoor van 'n paar plaaslike veranderlikes by die oorspronklike funksie moet voeg. Daarna moes ek selfs 'n eenvoudige skrif in Python skryf, wat gebaseer is op 'n gegewe stel besonder oorgebruikte funksies wat kwansuis "nie toelaat dat asinchronie deur hulself gaan nie" (dit wil sê, stapelbevordering en alles wat ek sopas beskryf het, nie werk in hulle), dui oproepe deur middel van wysers aan waarin funksies deur die samesteller geïgnoreer moet word sodat hierdie funksies nie asinchronies beskou word nie. En dan is JS-lêers onder 60 MB duidelik te veel - kom ons sê ten minste 30. Alhoewel ek een keer 'n samestellingskrip opgestel het en per ongeluk die skakelopsies uitgegooi het, waaronder was -O3. Ek hardloop die gegenereerde kode, en Chromium eet geheue op en val ineen. Ek het toe per ongeluk gekyk na wat hy probeer aflaai... Wel, wat kan ek sê, ek sou ook gevries het as ek gevra is om deeglik 'n 500+ MB Javascript te bestudeer en te optimaliseer.

Ongelukkig was die tjeks in die Asyncify-ondersteuningsbiblioteekkode nie heeltemal vriendelik nie longjmp-s wat in die virtuele verwerkerkode gebruik word, maar na 'n klein pleister wat hierdie kontroles deaktiveer en kontekste kragtig herstel asof alles goed was, het die kode gewerk. En toe begin 'n vreemde ding: soms is tjeks in die sinchronisasiekode geaktiveer - dieselfde wat die kode laat neerstort as dit volgens die uitvoeringslogika geblokkeer moet word - iemand het probeer om 'n reeds vasgelê mutex te gryp. Gelukkig het dit geblyk dat dit nie 'n logiese probleem in die reekskode was nie - ek het bloot die standaard hooflusfunksionaliteit gebruik wat deur Emscripten verskaf word, maar soms het die asynchrone oproep die stapel heeltemal ontvou, en op daardie oomblik sou dit misluk setTimeout vanaf die hooflus - dus het die kode die hooflusiterasie binnegegaan sonder om die vorige iterasie te verlaat. Herskryf op 'n oneindige lus en emscripten_sleep, en die probleme met mutexes het gestop. Die kode het selfs meer logies geword - ek het immers nie een of ander kode wat die volgende animasieraamwerk voorberei nie - die verwerker bereken net iets en die skerm word periodiek opgedateer. Die probleme het egter nie daar opgehou nie: soms het Qemu-uitvoering eenvoudig stilweg beëindig sonder enige uitsonderings of foute. Op daardie oomblik het ek dit opgegee, maar as ek vorentoe kyk, sal ek sê dat die probleem dit was: die Coroutine-kode gebruik in werklikheid nie setTimeout (of ten minste nie so dikwels as wat jy dalk dink nie): funksie emscripten_yield stel bloot die asynchrone oproepvlag. Die hele punt is dit emscripten_coroutine_next is nie 'n asynchrone funksie nie: intern kontroleer dit die vlag, stel dit terug en dra beheer oor na waar dit nodig is. Dit wil sê, die bevordering van die stapel eindig daar. Die probleem was dat as gevolg van gebruik-na-vry, wat verskyn het toe die coroutine-poel gedeaktiveer is as gevolg van die feit dat ek nie 'n belangrike reël kode vanaf die bestaande coroutine-agterkant gekopieer het nie, die funksie qemu_in_coroutine het waar teruggegee terwyl dit in werklikheid vals moes teruggegee het. Dit het gelei tot 'n oproep emscripten_yield, waarbo daar niemand op die stapel was nie emscripten_coroutine_next, die stapel ontvou tot heel bo, maar nee setTimeout, soos ek reeds gesê het, is nie uitgestal nie.

JavaScript-kode generering

En hier is eintlik die beloofde “om die maalvleis terug te draai”. Nie regtig nie. Natuurlik, as ons Qemu in die blaaier laat loop, en Node.js daarin, sal ons natuurlik na die generering van kode in Qemu heeltemal verkeerde JavaScript kry. Maar tog, 'n soort omgekeerde transformasie.

Eerstens 'n bietjie oor hoe Qemu werk. Vergewe my asseblief dadelik: ek is nie 'n professionele Qemu-ontwikkelaar nie en my gevolgtrekkings kan op sommige plekke foutief wees. Soos hulle sê, "die student se mening hoef nie saam te stem met die onderwyser se mening, Peano se aksiomatiek en gesonde verstand nie." Qemu het 'n sekere aantal ondersteunde gasargitekture en vir elkeen is daar 'n gids soos target-i386. Wanneer jy bou, kan jy ondersteuning vir verskeie gas-argitekture spesifiseer, maar die resultaat sal net verskeie binaries wees. Die kode om die gasargitektuur te ondersteun, genereer op sy beurt 'n paar interne Qemu-operasies, wat die TCG (Tiny Code Generator) reeds in masjienkode vir die gasheerargitektuur verander. Soos genoem in die readme-lêer wat in die tcg-gids geleë is, was dit oorspronklik deel van 'n gewone C-samesteller, wat later vir JIT aangepas is. Daarom is teikenargitektuur byvoorbeeld in terme van hierdie dokument nie meer 'n gasargitektuur nie, maar 'n gasheerargitektuur. Op 'n stadium het 'n ander komponent verskyn - Tiny Code Interpreter (TCI), wat kode (byna dieselfde interne bewerkings) moet uitvoer in die afwesigheid van 'n kodegenerator vir 'n spesifieke gasheerargitektuur. Trouens, soos sy dokumentasie sê, kan hierdie tolk nie altyd so goed presteer soos 'n JIT-kodegenerator nie, nie net kwantitatief in terme van spoed nie, maar ook kwalitatief. Alhoewel ek nie seker is dat sy beskrywing heeltemal relevant is nie.

Ek het eers probeer om 'n volwaardige TCG-agtergrond te maak, maar het vinnig deurmekaar geraak in die bronkode en 'n nie heeltemal duidelike beskrywing van die greepkode-instruksies nie, so ek het besluit om die TCI-tolk toe te vou. Dit het verskeie voordele gegee:

  • wanneer u 'n kodegenerator implementeer, kan u nie na die beskrywing van instruksies kyk nie, maar na die tolkkode
  • jy kan funksies genereer nie vir elke vertaalblok wat teëgekom word nie, maar byvoorbeeld eers na die honderdste uitvoering
  • as die gegenereerde kode verander (en dit blyk moontlik te wees, te oordeel aan die funksies met name wat die woord pleister bevat), sal ek die gegenereerde JS-kode ongeldig moet maak, maar ten minste sal ek iets hê om dit van te hergenereer

Wat die derde punt betref, is ek nie seker dat pleistering moontlik is nadat die kode vir die eerste keer uitgevoer is nie, maar die eerste twee punte is genoeg.

Aanvanklik is die kode gegenereer in die vorm van 'n groot skakelaar by die adres van die oorspronklike greepkode-instruksie, maar toe, met die onthou van die artikel oor Emscripten, optimalisering van gegenereerde JS en herloop, het ek besluit om meer menslike kode te genereer, veral aangesien dit empiries het geblyk dat die enigste toegangspunt tot die vertaalblok sy Begin is. Nie gou gesê as gedaan nie, na 'n rukkie het ons 'n kodegenerator gehad wat kode met ifs (alhoewel sonder lusse) gegenereer het. Maar slegte geluk, dit het neergestort, wat 'n boodskap gegee het dat die instruksies van 'n verkeerde lengte was. Boonop was die laaste instruksie op hierdie rekursievlak brcond. Goed, ek sal 'n identiese tjek by die generering van hierdie instruksie voeg voor en na die rekursiewe oproep en ... nie een van hulle is uitgevoer nie, maar na die assert-skakelaar het hulle steeds misluk. Op die ou end, nadat ek die gegenereerde kode bestudeer het, het ek besef dat na die skakelaar die wyser na die huidige instruksie uit die stapel herlaai word en waarskynlik deur die gegenereerde JavaScript-kode oorskryf word. En so het dit geblyk. Die verhoging van die buffer van een megagreep tot tien het tot niks gelei nie, en dit het duidelik geword dat die kodegenerator in sirkels loop. Ons moes seker maak dat ons nie oor die grense van die huidige TB gaan nie, en as ons dit wel doen, dan die adres van die volgende TB met 'n minus teken uitreik sodat ons kon voortgaan met teregstelling. Daarbenewens los dit die probleem op "watter gegenereerde funksies moet ongeldig gemaak word as hierdie stukkie greepkode verander het?" — slegs die funksie wat met hierdie vertaalblok ooreenstem, moet ongeldig gemaak word. Terloops, hoewel ek alles in Chromium ontfout het (aangesien ek Firefox gebruik en dit vir my makliker is om 'n aparte blaaier vir eksperimente te gebruik), het Firefox my gehelp om onversoenbaarheid met die asm.js-standaard reg te stel, waarna die kode vinniger begin werk het in Chroom.

Voorbeeld van gegenereerde kode

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

Gevolgtrekking

So, die werk is steeds nie voltooi nie, maar ek is moeg daarvoor om hierdie langtermyn-konstruksie in die geheim tot volmaaktheid te bring. Daarom het ek besluit om te publiseer wat ek het vir nou. Die kode is plek-plek 'n bietjie skrikwekkend, want dit is 'n eksperiment, en dit is nie vooraf duidelik wat gedoen moet word nie. Waarskynlik, dan is dit die moeite werd om normale atoomverbintenisse uit te reik bo-op 'n meer moderne weergawe van Qemu. Intussen is daar 'n draad in die Gita in 'n blogformaat: vir elke "vlak" wat op een of ander manier geslaag is, is 'n gedetailleerde kommentaar in Russies bygevoeg. Eintlik is hierdie artikel in 'n groot mate 'n hervertelling van die gevolgtrekking git log.

Jy kan dit alles probeer hier (pasop vir verkeer).

Wat reeds werk:

  • x86 virtuele verwerker loop
  • Daar is 'n werkende prototipe van 'n JIT-kodegenerator van masjienkode tot JavaScript
  • Daar is 'n sjabloon vir die samestelling van ander 32-bis gas argitekture: op die oomblik kan jy Linux bewonder vir die MIPS argitektuur wat in die blaaier vries tydens die laai stadium

Wat anders kan jy doen

  • Bespoedig nabootsing. Selfs in JIT-modus lyk dit of dit stadiger loop as Virtual x86 (maar daar is moontlik 'n hele Qemu met baie nagebootste hardeware en argitekture)
  • Om 'n normale koppelvlak te maak - om eerlik te wees, ek is nie 'n goeie webontwikkelaar nie, so vir nou het ek die standaard Emscripten-dop so goed moontlik hervorm
  • Probeer om meer komplekse Qemu-funksies te begin - netwerk, VM-migrasie, ens.
  • UPS: jy sal jou paar ontwikkelings en foutverslae stroomop by Emscripten moet indien, soos vorige portiers van Qemu en ander projekte gedoen het. Dankie aan hulle dat hulle hul bydrae tot Emscripten implisiet as deel van my taak kon gebruik.

Bron: will.com

Voeg 'n opmerking