För nĂ„gra Ă„r sedan skrev Fabrice Bellard â en emulator av en PC, skriven i JavaScript. Efter det fanns det Ă„tminstone . Men alla dessa, sĂ„vitt jag vet, har varit tolkar, medan Qemu, som skrevs betydligt tidigare av samma Fabrice Bellard, och nog alla respektabla moderna emulatorer, anvĂ€nder JIT-kompilering av gĂ€stkod till vĂ€rdmaskinens kod. Jag kĂ€nde att det var dags att genomföra uppgiften omvĂ€nd, i förhĂ„llande till den som webblĂ€sare löser: JIT-kompilering av maskinkod till JavaScript, vilket kĂ€ndes mest logiskt nĂ€r man portade Qemu. Det kan tyckas, varför just Qemu, det finns ju enklare och mer anvĂ€ndarvĂ€nliga emulatorer â som VirtualBox till exempel â installerar och fungerar. Men Qemu har flera intressanta egenskaper
- öppna kÀllkoder
- möjlighet att arbeta utan kÀrndrivrutiner
- möjlighet att arbeta i tolkningslÀge
- stöd för mÄnga bÄde vÀrd- och gÀstarkitekturer
NĂ€r det gĂ€ller den tredje punkten kan jag nu förklara att i TCI-lĂ€get tolkas inte sjĂ€lva gĂ€stmaskininstruktionerna utan bytekoden som erhĂ„lls frĂ„n dem, men det Ă€ndrar inte essensen â för att bygga och köra Qemu pĂ„ en ny arkitektur, om man har tur, rĂ€cker det med en C-kompilator â att skriva en kodgenerator kan skjutas upp.
Och nu, efter tvÄ Ärs lÄngsam klurande med Qemus kÀllkod pÄ fritiden, har en fungerande prototyp dykt upp, dÀr man redan kan köra, till exempel, Kolibri OS.
Vad Àr Emscripten
Nuförtiden har det kommit mĂ„nga kompilatorer vars slutresultat Ă€r JavaScript. Vissa, sĂ„som TypeScript, var frĂ„n början avsedda som det bĂ€sta sĂ€ttet att skriva för webben. Samtidigt Ă€r Emscripten ett sĂ€tt att ta existerande kod i C eller C++ och kompilera den till ett format som webblĂ€saren kan förstĂ„. PĂ„ har mĂ„nga portar av kĂ€nda program skapats: , till exempel kan man titta pĂ„ PyPy â för övrigt sĂ€gs det att de redan har JIT. I verkligheten kan inte vilket program som helst enkelt kompileras och köras i webblĂ€saren â det finns en rad , som man fĂ„r leva med, men som det anges pĂ„ samma sida "Emscripten can be used to compile almost any bĂ€rbar C/C++-kod till JavaScript. Det finns en rad operationer som Ă€r osĂ€kra enligt standarden, men som vanligtvis fungerar pĂ„ x86 â till exempel, ojusterad Ă„tkomst till variabler, vilket pĂ„ vissa arkitekturer Ă€r helt förbjudet. I allmĂ€nhet Ă€r Qemu ett plattformsoberoende program och man hoppades att det inte innehöll mycket osĂ€ker beteende â ta och kompilera, sedan lite arbete med JIT â och klart! Men sĂ„ enkelt var det inteâŠ
Första försöket
Ăverhuvudtaget Ă€r jag inte den första som fĂ„tt idĂ©n att portera Qemu till JavaScript. PĂ„ ReactOS-forumet stĂ€lldes frĂ„gan om detta var möjligt med Emscripten. Redan tidigare hade det flutit rykten om att Fabrice Bellard personligen hade gjort detta, men det handlade om jslinux, vilket sĂ„vitt jag vet Ă€r ett försök att manuellt uppnĂ„ tillrĂ€cklig prestanda i JS, och som Ă€r skriven frĂ„n grunden. Senare skrevs Virtual x86 â det lades upp oklyvda kĂ€llkoder och pĂ„stods att den stora "realismen" i emuleringen möjliggjorde anvĂ€ndning av SeaBIOS som firmware. Dessutom fanns det Ă„tminstone ett försök att portera Qemu med Emscripten â detta försökte göras , men utvecklingen, sĂ„vitt jag förstod, har stannat av.
SĂ„, det verkar som om hĂ€r Ă€r kĂ€llkoderna, hĂ€r Ă€r Emscripten â ta och kompilera. Men det finns ocksĂ„ bibliotek som Qemu Ă€r beroende av, och bibliotek som dessa bibliotek Ă€r beroende av, och sĂ„ vidare, dĂ€ribland ett som Ă€r , som glib Ă€r beroende av. Det fanns rykten pĂ„ internet om att det fanns en sĂ„dan i den stora samlingen av portar till bibliotek för Emscripten, men det var svĂ„rt att tro: för det första, den samlades inte med den nya kompileraren, och för det andra, det Ă€r ett alltför lĂ„gnivĂ„bibliotek för att bara kunna kompileras till JS. Och det handlar inte bara om assemblerinsatser â förmodligen kan man, om man vrider och vĂ€nder pĂ„ det, för vissa calling conventions skapa nödvĂ€ndiga argument pĂ„ stacken och anropa funktionen utan dem. Men Emscripten Ă€r en knepig liten sak: för att den genererade koden ska se bekant ut för JS-motorens optimerare i webblĂ€saren, anvĂ€nds vissa trick. Bland annat sĂ„ kallad relooping â kodgeneratorn försöker Ă„terskapa trovĂ€rdiga if-satser, loopar etc. frĂ„n den erhĂ„llna LLVM IR med nĂ„gra abstrakta instruktioner. Hur överförs argumenten i funktionerna? SjĂ€lvklart som argument till JS-funktioner, det vill sĂ€ga sĂ„ mycket som möjligt inte via stacken.
I början var tanken att bara skriva en ersĂ€ttning för libffi i JS och köra standardtester, men till slut blev jag förvirrad över hur jag skulle göra mina header-filer sĂ„ att de skulle fungera med befintlig kod - vad kan man sĂ€ga, som man sĂ€ger, "Antingen Ă€r uppgifterna sĂ„ svĂ„ra, eller sĂ„ Ă€r vi sĂ„ dumma". Jag var tvungen att portera libffi till en annan arkitektur, om man nu kan kalla det sĂ„ â lyckligtvis finns det i Emscripten bĂ„de makron för inline assembly (i JavaScript, ja â vilken arkitektur, sĂ„ Ă€ven assemblern) och möjlighet att köra kod som genereras on-the-fly. Efter en tid av arbete med plattformsberoende fragment av libffi, lyckades jag fĂ„ fram en kompilerbar kod och körde den pĂ„ det första bĂ€sta testet. Till min förvĂ„ning, bestod testet. Jag var överraskad av min genialitet â Ă€r det ett skĂ€mt, det fungerade pĂ„ första försöket â jag, fortfarande oförmögen att tro mina egna ögon, gick tillbaks för att se pĂ„ den resulterande koden och bedöma vad jag skulle grĂ€va i nĂ€sta steg. HĂ€r blev jag Ă„terigen överraskad â det enda som min funktion gjorde ffi_call â var att rapportera om ett framgĂ„ngsrikt anrop. Det fanns inget anrop. SĂ„ jag skickade min första pull request, som rĂ€ttade ett uppenbart fel i testet â flyttal ska inte jĂ€mföras som a == b och inte ens som a - b < EPS â kom ihĂ„g att jag fortfarande mĂ„ste lĂ€gga till modulen, annars kommer 0 verkligen att bli lika med 1/3⊠Sammanfattningsvis har jag fĂ„tt fram en sorts libffi-port som klarar de enklaste testerna och som kan kompilera glib â jag tĂ€nkte att jag kan komplettera det senare. För att ligga steget före kan jag sĂ€ga att kompilatorn faktiskt inte inkluderade den slutliga koden för libffi-funktionen.
Men, som jag redan nĂ€mnt, finns det vissa begrĂ€nsningar, och bland det fria utnyttjandet av olika odefinierat beteende finns det en lite obehaglig sĂ€rskildhet â JavaScript stöder inte flertrĂ„dighet med delad minne som standard. I princip kan detta faktiskt kallas en ganska bra idĂ©, men inte för portering av kod vars arkitektur Ă€r baserad pĂ„ C-trĂ„dar. Generellt sett pĂ„gĂ„r det experiment i Firefox för att stödja delade arbetare, och implementationen av pthread för dem finns i Emscripten, men jag ville inte vara beroende av detta. Jag var tvungen att gradvis avlĂ€gsna flertrĂ„dighet frĂ„n Qemu-koden â det vill sĂ€ga att leta efter var trĂ„dar startas, flytta kropparna av looparna som körs i dessa trĂ„dar till en separat funktion, och vĂ€xelvis anropa sĂ„dana funktioner frĂ„n huvudloopen.
Andra försöket
Vid nĂ„got tillfĂ€lle blev det klart att lĂ€get var oförĂ€ndrat, och att oordnat spridande av tillfĂ€lliga lösningar i koden inte skulle leda till nĂ„got bra. Slutsatsen: Jag mĂ„ste pĂ„ nĂ„got sĂ€tt systematisera processen för att lĂ€gga till tillfĂ€lliga lösningar. DĂ€rför valde jag den senaste versionen 2.4.1 (inte 2.5.0, eftersom man aldrig vet, om det finns fler obesvarade buggar i den nya versionen, och jag har tillrĂ€ckligt med mina egna buggar), och som första steg skrev jag om thread-posix.c. SĂ„, hur sĂ€kert: om nĂ„gon försökte utföra en operation som skulle leda till en blockering, anropades direkt funktionen abort() â sjĂ€lvklart Ă„tgĂ€rdade detta inte alla problem direkt, men Ă„tminstone var det pĂ„ nĂ„got sĂ€tt bĂ€ttre Ă€n att tyst fĂ„ data inkonsistens.
Generellt sett, nĂ€r man porterar kod till JS, hjĂ€lper Emscripten-alternativen mycket -s ASSERTIONS=1 -s SAFE_HEAP=1 â de fĂ„ngar vissa typer av odefinierat beteende som felaktiga adresser (vilket helt strider mot koden för typed arrays som HEAP32[addr >> 2] = 1) eller anrop av funktioner med fel antal argument.
För övrigt, justering av fel Àr ett eget Àmne. Som jag nÀmnde finns det i Qemu en "degenererad" tolkande backend för kodgenerering TCI (tiny code interpreter), och för att bygga och köra Qemu pÄ en ny arkitektur, om man har tur, rÀcker det med en C-kompilator. Nyckelord "om man har tur". Jag hade inte tur, och det visade sig att TCI vid tolkning av sin bytekod anvÀnder ojusterad Ätkomst. Det vill sÀga, pÄ alla sÄdana ARM- och andra arkitekturer som krÀver justerad Ätkomst kompileras Qemu eftersom det finns en korrekt TCG-backend som genererar nativ kod, men om TCI kommer att fungera pÄ dem Àr fortfarande en frÄga. Det visade sig dock att nÄgot liknande tydligt angavs i dokumentationen för TCI. Sammanfattningsvis lades det till funktioner för ojusterad lÀsning, som upptÀcktes i en annan del av Qemu.
Krasch av heapen
Till slut ÄtgÀrdades den ojusterade Ätkomsten i TCI, en huvudloop skapades som turades om att kalla pÄ processorn, RCU och lite smÄtt. Och nu kör jag Qemu med alternativet -d exec,in_asm,out_asm, vilket innebÀr att det ska anges vilka kodblock som körs, samt vid översÀttningen skriva vilken gÀstkod som fanns och vilken vÀrdkod som blev (i det hÀr fallet bytekod). Det startar, kör nÄgra översÀttningsblock, skriver det debugmeddelande jag lÀmnade, att RCU nu ska startas och⊠kraschar vid abort() inuti funktionen free(). Genom att grÀva i funktionen free() utrettes att i huvudet pÄ heapblocket, som ligger Ätta byte före den allokerade minnet, fanns det skrÀp istÀllet för blockstorlek eller nÄgot liknande.
Att förstöra en hög - hur gulligt... I sÄdana fall finns det ett anvÀndbart medel - att, om möjligt, frÄn samma kÀllor bygga en nativ binÀr och köra den under Valgrind. Efter en tid var binÀren klar. Jag kör med samma alternativ - den kraschar fortfarande vid initialisering, utan att faktiskt nÄ utförandet. Obehagligt, sÄ klart - uppenbarligen var kÀllorna inte riktigt desamma, vilket inte Àr förvÄnande, eftersom configure upptÀckte nÄgra andra alternativ, men jag har ju Valgrind - först ska jag fixa den hÀr buggen, och sedan, om jag har tur, kan jag fÄ den ursprungliga att dyka upp. Jag kör allt som vanligt under Valgrind... Uh-oh, den startade, klarade initialiseringen och fortsatte förbi den ursprungliga buggen utan en enda varning om felaktig minnesÄtkomst, för att inte tala om kraschar. Till detta var livet, som man sÀger, inte förberett mig - ett kraschande program slutar krascha nÀr det startas under Valgrind. Vad var detta - en gÄta. Min hypotes Àr att eftersom det i omgivningarna av den aktuella instruktionen efter krasch vid initialisering gdb visade aktivitet memset-a med en giltig pekare med hjÀlp av antingen mmx, eller xmm register, sÄ kan det ha varit nÄgot fel med justeringen, Àven om det fortfarande Àr svÄrt att tro.
Okej, Valgrind verkar inte vara till hjĂ€lp hĂ€r. Och hĂ€r började det obehagligaste - allt verkar starta, men det kraschar av helt okĂ€nda skĂ€l pĂ„ grund av en hĂ€ndelse som kan ha intrĂ€ffat miljontals instruktioner tillbaka. Det var lĂ€nge oklart hur man ens skulle nĂ€rma sig problemet. Tills slut var jag tvungen att sĂ€tta mig ner och debuggra. Utskriften av vad som Ă„terstĂ€llde rubriken visade att det inte liknade ett nummer, utan snarare nĂ„got binĂ€rt data. Och, mirakulöst nog, hittades denna binĂ€ra strĂ€ng i BIOS-filen - vilket innebĂ€r att man nu med tillrĂ€cklig sĂ€kerhet kunde sĂ€ga att det handlade om ett buffertöverskridande, och man förstod till och med vad som skrevs till denna buffert. Vidare, i Emscripten, finns det lyckligtvis ingen randomisering av adresserummet, och det finns inga hĂ„l i det, sĂ„ det gĂ„r att skriva nĂ„gonstans mitt i koden och fĂ„ ut data via pekaren frĂ„n den tidigare körningen, se pĂ„ data, titta pĂ„ pekaren, och om den inte Ă€ndrats, fĂ„ information att fundera pĂ„. Problemet Ă€r att det tar nĂ„gra minuter att lĂ€nka efter vilket som helst Ă€ndring, men vad kan man göra. Resultatet var att en specifik strĂ€ng hittades, som kopierade BIOS frĂ„n en temporĂ€r buffert till gĂ€stminnet - och det visade sig att det inte fanns tillrĂ€ckligt med utrymme i bufferten. Sökandet efter kĂ€llan till den konstiga buffertadressen ledde mig till funktionen qemu_anon_ram_alloc i filen oslib-posix.c - logiken dĂ€r var att det ibland kan vara anvĂ€ndbart att justera adresser till en huge page pĂ„ 2 MB, för detta ber vi i mmap först lite mer, och sedan returnerar vi överskottet med hjĂ€lp av munmap. Om ingen justering krĂ€vs, sĂ„ anger vi istĂ€llet för 2 MB resultatet getpagesize() â mmap det kommer Ă€ndĂ„ ge ut en justerad adress... SĂ„ i Emscripten mmap anropas helt enkelt malloc, och den justerar naturligtvis inte pĂ„ sidan. PĂ„ det stora hela, buggen som irriterade mig i flera mĂ„nader, löstes med en Ă€ndring i tvĂ„ raderna.
SĂ€rskilda funktionsanrop
Och nu rĂ€knar processorn nĂ„got, Qemu kraschar inte, men skĂ€rmen tĂ€nds inte, och processorn fastnar snabbt, dömd av utskriften. -d exec,in_asm,out_asmDet har uppstĂ„tt en hypotes: det kommer inga timeravbrott (eller överhuvudtaget inga avbrott). Och visst, om man tar bort avbrotten frĂ„n den native versionen som av nĂ„gon anledning fungerade, blir bilden liknande. Men lösningen visade sig vara nĂ„got helt annat: jĂ€mförelsen av spĂ„rningen, utförd med den angivna instĂ€llningen, visade att körningsvĂ€garna avviker tidigt. HĂ€r mĂ„ste det nĂ€mnas att jĂ€mförelsen mellan den avlyssnade med hjĂ€lp av startprogrammet emrun och den native versionens debugutskrift Ă€r inte en helt mekanisk process. Jag vet inte exakt hur programmet som körs i webblĂ€saren kopplar till emrun, men vissa rader i utskriften hamnar pĂ„ andra platser, sĂ„ skillnaden i diffen Ă€r inte en anledning att tro att körningsvĂ€garna har avvikit. Sammanfattningsvis blev det tydligt att enligt instruktionerna ljmpl görs övergĂ„ngar till olika adresser, och dessutom genereras bytekoden pĂ„ ett helt annat sĂ€tt: i den ena finns en instruktion för att anropa en C-funktionshjĂ€lpare, i den andra finns den inte. Efter att ha googlat instruktioner och studerat koden som översĂ€tter dessa instruktioner, blev det klart att för det första sĂ„ skrevs direkt till registret cr0 ocksĂ„ med hjĂ€lp av en hjĂ€lpare â vilket förde processorn till skyddat lĂ€ge, och för det andra att js-versionen aldrig nĂ„dde skyddat lĂ€ge. Och detta beror pĂ„ att Ă€nnu en egenskap hos Emscripten Ă€r att den inte vill tolerera kod av typen implementering av instruktionen call i TCI, som varje pekare till en funktion konverterar till typen long long f(int arg0, .. int arg9) â funktioner mĂ„ste anropas med rĂ€tt antal argument. Vid brott mot denna regel kommer programmet antingen att krascha (vilket Ă€r bra), eller anropa en helt annan funktion (vilket Ă€r trist att felsöka). Det finns Ă€ven ett tredje alternativ â att aktivera genereringen av omslag som lĂ€gger till/för bort argument, men totalt sett tar dessa omslag upp vĂ€ldigt mycket plats, medan jag i praktiken bara behöver strax över hundra omslag. Detta Ă€r tyvĂ€rr en stor besvikelse, men det visade sig vara ett mer allvarligt problem: i den genererade koden för omslagfunktionerna konverterades argumenten, men funktionen med de genererade argumenten anropades ibland inte â precis som i min implementation av libffi. Det vill sĂ€ga, vissa hjĂ€lpare utförs helt enkelt inte.
Lyckligtvis har Qemu maskinlÀsbara listor över hjÀlpkomponenter i form av en headerfil som liknar
DEF_HELPER_0(lock, void)
DEF_HELPER_0(unlock, void)
DEF_HELPER_3(write_eflags, void, env, tl, i32)De anvĂ€nds pĂ„ ett ganska lustigt sĂ€tt: först omdefinieras makron pĂ„ de mest mĂ€rkliga sĂ€tten DEF_HELPER_n, och sedan inkluderas helper.h. För att makrot ska utökas i en strukturinitialiserare och komma med ett kommatecken, och sedan definieras en array, istĂ€llet för element â #include <helper.h> Som ett resultat fick jag Ă€ntligen tillfĂ€lle att försöka anvĂ€nda biblioteket , och en skript skapades som genererade wrappers precis för de funktioner som behövdes.
Och nu, efter detta, verkade processorn ha startat. Verkade, eftersom skÀrmen fortfarande inte initialiserades, Àven om jag lyckades köra memtest86+ i den inhemska versionen. HÀr bör det pÄpekas att Qemus block I/O-kod Àr skriven med korutiner. I Emscripten finns det en ganska komplicerad implementation, men den behövde ocksÄ stödjas i Qemus kod, medan processorn kan debuggas redan nu: Qemu stödjer alternativen -kernel, -initrd, -append, med hjÀlp av vilka man kan ladda Linux eller till exempel memtest86+ utan att anvÀnda blockenheter. Men förresten: i den inhemska versionen kunde man se Linux-kÀrnan till konsolen med alternativet -nographic, men frÄn webblÀsaren kom det ingen utdata till terminalen dÀr den startades emrun, vilket inte gav nÄgot svar. Det vill sÀga: Àr processorn trasig eller Àr det grafikutmatningen. Och dÄ kom jag pÄ att vÀnta lite. Det visade sig att "processorn inte sover, utan bara blinkar lÄngsamt", och efter fem minuter spottade kÀrnan ut en mÀngd meddelanden till konsolen och hÀngde sig sedan igen. Det blev klart att processorn fungerar men att jag behöver leta i SDL2-koden. TyvÀrr vet jag inte hur man anvÀnder detta bibliotek, sÄ jag fick ibland agera pÄ mÄfÄ. Vid ett tillfÀlle flimrade raden parallel0 pÄ en blÄ bakgrund, vilket vÀckte vissa tankar. I slutÀndan visade det sig att problemet var att Qemu öppnar flera virtuella fönster i ett fysiskt fönster, mellan vilka man kan vÀxla med Ctrl-Alt-n: i den inhemska versionen fungerar det, i Emscripten gör det inte. Efter att ha blivit av med onödiga fönster med hjÀlp av alternativen -monitor none -parallel none -serial none och tvingar till att rita om hela skÀrmen vid varje bild, fungerade plötsligt allt.
Korutiner
SĂ„ emulatorn i webblĂ€saren fungerar, men det finns inget intressant frĂ„n ett diskettdrivare att köra eftersom det inte finns nĂ„got blockin- och utdata â support för korutiner mĂ„ste implementeras. I Qemu finns det redan flera coroutine-backends, men pĂ„ grund av JavaScript:s och Emscripten-kodgeneratorns sĂ€rdrag kan man inte bara börja jonglera med stackar. Det skulle kunna verka som "allt Ă€r förlorat, gipset tas bort", men Emscripten-utvecklarna har redan tagit hand om allt. Det Ă€r genomfört pĂ„ ett ganska underhĂ„llande sĂ€tt: lĂ„t oss kalla en misstĂ€nkt funktionsanrop som emscripten_sleep och nĂ„gra andra, som anvĂ€nder Asyncify-mekanismen, samt anrop via pekare och funktionsanrop dĂ€r det kan intrĂ€ffa nĂ„got av de tidigare tvĂ„ fallen lĂ€ngre ner i stacken. SĂ„ nu, före varje misstĂ€nkt anrop, allokerar vi en asynkron kontext, och direkt efter anropet â kontrollerar vi om ett asynkront anrop intrĂ€ffade, och om det gjorde det, sparar vi alla lokala variabler i denna asynkrona kontext, anger vilken funktion som ska ta över utförandet nĂ€r vi behöver fortsĂ€tta, och vi lĂ€mnar den nuvarande funktionen. HĂ€r finns det verkligen utrymme för att studera effekten av â för att fortsĂ€tta utföra koden efter att ha Ă„tervĂ€nt frĂ„n ett asynkront anrop genererar kompilatorn "snuttar" av funktionen, som börjar efter det misstĂ€nkta anropet â sĂ„ hĂ€r: om det finns n misstĂ€nkta anrop, kommer funktionen att delas upp nĂ„gonstans i n/2 gĂ„nger â detta utan att ta hĂ€nsyn till att en del lokala variabler mĂ„ste sparas efter varje potentiellt asynkront anrop i den ursprungliga funktionen. Senare var jag till och med tvungen att skriva ett enkelt script i Python, som för ett givet antal speciellt splittrade funktioner som antas "inte slĂ€ppa asynkronitet", det vill sĂ€ga dĂ€r stapeluppvridning och allt jag just beskrev inte intrĂ€ffar, angav vilka anrop via pekare i vilka funktioner som kompilatorn skulle ignorera sĂ„ att dessa funktioner inte betraktades som asynkrona. För annars Ă€r 60 MB JS-filer redan uppenbart för mycket â lĂ„t dem Ă„tminstone vara 30. Ăven om jag en gĂ„ng stĂ€llde in ett byggscript och av misstag tog bort lĂ€nkarnas alternativ, bland vilka det var -O3. Jag kör den genererade koden och Chromium anvĂ€nder för mycket minne och kraschar. Jag rĂ„kade sedan titta pĂ„ vad den försökte ladda... Tja, vad ska jag sĂ€ga, jag skulle ocksĂ„ hĂ€nga mig om jag blev ombedd att noggrant granska och optimera JavaScript pĂ„ över 500 MB.
TyvĂ€rr fungerade kontrollerna i Asyncify-stödbibliotekets kod inte riktigt bra med longjmp-arna som anvĂ€ndes i koden för den virtuella processorn, men efter en liten patch som inaktiverade dessa kontroller och tvĂ„ngsmĂ€ssigt Ă„terstĂ€llde kontexter som om allt var bra, fungerade koden. Och dĂ„ började det konstiga: ibland utlöste kontroller i synkroniseringskoden â de som avslutar koden om den logiskt skulle blockeras â nĂ„gon försökte fĂ„nga en redan fĂ„ngad mutex. Lyckligtvis var det inte ett logiskt problem i den serialiserade koden â jag anvĂ€nde bara den standardfunktionalitet som tillhandahĂ„lls av Emscripten för huvudloopen, men ibland rullade det asynkrona anropet helt ut stacken, och vid det tillfĂ€llet utlöste setTimeout frĂ„n huvudloopen â sĂ„ koden kom in i iterationen av huvudloopen utan att ha avslutat den föregĂ„ende iterationen. Jag skrev om det i en oĂ€ndlig loop och emscripten_sleep, och problemen med mutexarna upphörde. Koden blev till och med mer logisk â eftersom jag egentligen inte har nĂ„gon kod som förbereder nĂ€sta animationsram â processorn berĂ€knar nĂ„got och skĂ€rmen uppdateras periodiskt. Men problemen slutade inte dĂ€r: ibland avslutades Qemu tyst utan nĂ„gra undantag eller fel. Vid det tillfĂ€llet gav jag upp, men för att förskjuta framĂ„t sĂ€ger jag att problemet lĂ„g i: kodkorutinerna anvĂ€nder egentligen inte setTimeout (eller Ă„tminstone inte sĂ„ ofta som man skulle kunna tro): funktionen emscripten_yield sĂ€tter bara en flagga för det asynkrona anropet. Det hela handlar om att emscripten_coroutine_next inte Ă€r en asynkron funktion: den kontrollerar sin flagga, nollstĂ€ller den och överlĂ€mnar kontrollen dit den behöver gĂ„. Det betyder att det Ă€r dĂ€r stackutvecklingen avslutas. Problemet var att pĂ„ grund av use-after-free, som uppstod nĂ€r coroutine-poolen var inaktiverad eftersom jag inte kopierade över en viktig rad kod frĂ„n den existerande coroutine-backend, returnerade funktionen qemu_in_coroutine true nĂ€r den egentligen borde ha returnerat false. Detta ledde till anropet emscripten_yield, över vilken det inte fanns nĂ„gon stack emscripten_coroutine_next, stacken strĂ€ckte sig Ă€nda till toppen, men inga setTimeout, som jag redan nĂ€mnt, angavs inte.
Kodgenerering JavaScript
Och hĂ€r Ă€r faktiskt den lovade "Ă„terstĂ€llningen av köttfĂ€rsen". Egentligen inte. Naturligtvis, om man kör Qemu i webblĂ€saren och i den â Node.js, kommer vi efter kodgenereringen i Qemu att fĂ„ en helt annan JavaScript. Men Ă€ndĂ„, en form av tillbakaöversĂ€ttning.
Först lite om hur Qemu fungerar. Jag ber om ursÀkt: jag Àr inte en professionell Qemu-utvecklare och mina slutsatser kan vara felaktiga pÄ vissa stÀllen. Som man sÀger, "en students Äsikt behöver inte stÀmma överens med lÀrarens Äsikt, Peanos axiom och sunt förnuft". Qemu har ett antal stödda gÀstarkitekturer och för varje finns en katalog som liknar target-i386. Vid kompilering kan man ange stöd för flera gÀstarkitekturer, men resultatet blir helt enkelt flera binÀra filer. Koden för att stödja gÀstarkitekturen genererar i sin tur nÄgra interna operationer i Qemu, som TCG (Tiny Code Generator) sedan omvandlar till maskinkod för vÀrdarkitekturen. Som anges i readme-filen som finns i katalogen tcg, var det ursprungligen en del av en vanlig C-kompilator som senare anpassades för JIT. SÄ exempelvis, gÀstarkitekturen i termer av detta dokument - det Àr redan vÀrdarkitekturen. Vid ett tillfÀlle tillkom ytterligare en komponent - Tiny Code Interpreter (TCI), som ska köra koden (nÀstan samma interna operationer) i avsaknad av kodgenerator för den specifika vÀrdarkitekturen. Faktum Àr att, som anges i dess dokumentation, denna tolk kanske inte alltid fungerar lika bra som JIT-kodgeneratorn, varken kvantitativt i frÄga om hastighet eller kvalitativt. Jag Àr dock osÀker pÄ om dess beskrivning fortfarande Àr helt aktuell.
I början försökte jag göra en fullstÀndig TCG-backend, men blev snabbt förvirrad av kÀllkoden och den oklara beskrivningen av bytekodens instruktioner, sÄ jag beslutade att kapsla in TCI-tolkaren. Det gav omedelbart flera fördelar:
- vid implementering av kodgeneratorn kunde jag titta inte i beskrivningen av instruktionerna utan i tolkens kod
- Funktioner kan genereras inte för varje mött block av översÀttning, utan till exempel bara efter den hundrade körningen.
- Vid Àndring av den genererade koden (och detta verkar möjligt med tanke pÄ funktioner som har "patch" i namnet) mÄste jag ogiltigförklara den genererade JS-koden, men Ätminstone kommer jag ha nÄgot att regenerera den ifrÄn.
Jag Àr osÀker angÄende det tredje punkten; jag tvekar pÄ om patchning Àr möjligt efter att koden har körts första gÄngen, men de första tvÄ punkterna Àr tillrÀckliga.
Ursprungligen genererades koden som en stor switch baserad pĂ„ adressen till de ursprungliga bytekodinstruktionerna, men efter att jag kom pĂ„ en artikel om Emscripten, optimering av den genererade JS:en och relooping, beslutade jag mig för att generera mer mĂ€nsklig lĂ€sbar kod. Det visade sig empiriskt att den enda ingĂ„ngspunkten till översĂ€ttningsblocket var i dess början. SĂ„ blev det. Efter en tid hade jag en kodgenerator som genererade kod med if-satser (Ă€ven om utan loopar). Men hĂ€r Ă€r kruxet, den föll med ett meddelande om att instruktionen hade fel lĂ€ngd. Den sista instruktionen pĂ„ den hĂ€r rekursionsnivĂ„n var brcond. Okej, jag lĂ€gger till en identisk kontroll vid genereringen av denna instruktion före det rekursiva anropet och efterĂ„t, och... ingen av dem utfördes, men efter switchen pĂ„ assert föll vi Ă€ndĂ„. Till slut insĂ„g jag, efter att ha studerat den genererade koden, att pekaren till den aktuella instruktionen laddades om frĂ„n stacken och antagligen överskrevs av den genererade JavaScript-koden. Och sĂ„ var det. Ăkningen av buffertens storlek frĂ„n en megabyte till tio ledde inte till nĂ„got resultat, och det stod klart att kodgeneratorn cirkulerade. Jag var tvungen att kontrollera att vi inte hade gĂ„tt utanför grĂ€nserna för den aktuella TB:n, och om vi hade, ge adressen till nĂ€sta TB med ett minustecken sĂ„ att vi kunde fortsĂ€tta köra. Dessutom löser detta problemet med "vilka genererade funktioner ska ogiltigförklaras om denna bit bytekod har Ă€ndrats?" â endast den funktion som motsvarar detta översĂ€ttningsblock mĂ„ste ogiltigförklaras. Förresten, Ă€ven om jag debuggade allt i Chromium (eftersom jag anvĂ€nder Firefox och det Ă€r lĂ€ttare för mig att ha en separat webblĂ€sare för experiment), hjĂ€lpte Firefox mig att Ă„tgĂ€rda inkompatibiliteter med asm.js-standarden, efter vilket koden började köra snabbare i Chromium.
Exempel pÄ genererad kod
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"]Slutsats
SÄ, arbetet Àr fortfarande inte klart, men jag har tröttnat pÄ att hemligt förfina detta lÄngsamma bygge. DÀrför bestÀmde jag mig för att publicera vad som finns sÄ lÄngt. Koden Àr stundtals lite skrÀmmande, eftersom detta Àr ett experiment, och det Àr oklart pÄ förhand vad som behöver göras. Kanske borde man senare skapa normala atomÀra commits över nÄgon mer modern version av Qemu. Just nu finns det en gren i git i bloggformat: till varje "nivÄ" man tagit sig igenom har jag lagt till en utförlig kommentar pÄ ryska. Egentligen Àr denna artikel i stor utstrÀckning en sammanfattning av slutsatsen. git log.
Detta kan de flesta testa (försiktigt, trafik).
Vad som redan fungerar:
- En virtuell x86-processor fungerar
- Det finns en fungerande prototyp av JIT-kodgenerator frÄn maskinkod till JavaScript
- Det finns en grund för att bygga andra 32-bitars gÀstarikitektur: ni kan just nu beundra den som hÀnger sig i webblÀsaren under lastning av Linux för MIPS-arkitekturen
Vad mer kan göras
- Accelerera emuleringen. Ăven i JIT-lĂ€ge verkar det fungera lĂ„ngsammare Ă€n Virtual x86 (men det finns Ă„tminstone en hel Qemu med mĂ„nga emulerade hĂ„rdvaror och arkitekturer)
- Skapa ett bra grÀnssnitt - jag Àr, Àrligt talat, ingen bra webbutvecklare, sÄ Àn sÄ lÀnge har jag bara omarbetat standardgrÀnssnittet i Emscripten sÄ gott jag kunnat
- Försöka köra mer komplexa funktioner i Qemu - nÀtverk, migrering av VM osv.
- UPD: Jag mÄste skicka in mina fÄ utvecklingsresultat och buggrapporter till upstream Emscripten, precis som tidigare portare av Qemu och andra projekt har gjort. Tack till dem för att jag fick möjlighet att obemÀrkt anvÀnda deras bidrag till Emscripten inom ramen för min uppgift.
KĂ€lla: habr.com
