Qemu.js med JIT-stöd: du kan fortfarande vända färsen bakåt

För några år sedan Fabrice Bellard skriven av jslinux är en PC-emulator skriven i JavaScript. Efter det var det åtminstone mer Virtuell x86. Men alla av dem, så vitt jag vet, var tolkar, medan Qemu, skriven mycket tidigare av samma Fabrice Bellard, och, förmodligen, vilken modern emulator som helst med självrespekt, använder JIT-kompilering av gästkod till värdsystemskod. Det tycktes mig att det var dags att implementera den motsatta uppgiften i förhållande till den som webbläsare löser: JIT-kompilering av maskinkod till JavaScript, för vilket det verkade mest logiskt att porta Qemu. Det verkar, varför Qemu, det finns enklare och användarvänliga emulatorer - samma VirtualBox, till exempel - installerade och fungerar. Men Qemu har flera intressanta funktioner

  • öppen källa
  • förmåga att arbeta utan en kärndrivrutin
  • förmåga att arbeta i tolkläge
  • stöd för ett stort antal både värd- och gästarkitekturer

När det gäller den tredje punkten kan jag nu förklara att det faktiskt, i TCI-läge, är det inte själva gästmaskininstruktionerna som tolkas, utan bytekoden som erhålls från dem, men detta ändrar inte essensen - för att bygga och köra Qemu på en ny arkitektur, om du har tur räcker det med en C-kompilator - att skriva en kodgenerator kan skjutas upp.

Och nu, efter två år av lugnt pysslande med Qemu-källkoden på fritiden, dök en fungerande prototyp upp, där du redan kan köra till exempel Kolibri OS.

Vad är Emscripten

Nuförtiden har många kompilatorer dykt upp, vars slutresultat är JavaScript. Vissa, som Type Script, var ursprungligen avsedda att vara det bästa sättet att skriva för webben. Samtidigt är Emscripten ett sätt att ta befintlig C- eller C++-kod och kompilera den till en webbläsbar form. På den här sidan Vi har samlat många portar av välkända program: härDu kan till exempel titta på PyPy – de påstår sig förresten redan ha JIT. Faktum är att inte alla program helt enkelt kan kompileras och köras i en webbläsare - det finns ett antal funktioner, som du dock får stå ut med, eftersom inskriptionen på samma sida säger ”Emscripten kan användas för att kompilera nästan alla portabel C/C++-kod till JavaScript". Det vill säga, det finns ett antal operationer som är odefinierat beteende enligt standarden, men som vanligtvis fungerar på x86 - till exempel ojusterad åtkomst till variabler, vilket generellt är förbjudet på vissa arkitekturer. Generellt , Qemu är ett plattformsoberoende program och , ville jag tro, och det innehåller inte redan en massa odefinierat beteende - ta det och kompilera, mixtra lite med JIT - och du är klar! Men det är inte fall...

Första försöket

Generellt sett är jag inte den första personen som kom på idén att porta Qemu till JavaScript. Det ställdes en fråga på ReactOS-forumet om detta var möjligt med Emscripten. Ännu tidigare gick det rykten om att Fabrice Bellard gjorde detta personligen, men vi pratade om jslinux, som så vitt jag vet bara är ett försök att manuellt uppnå tillräcklig prestanda i JS, och skrevs från grunden. Senare skrevs Virtual x86 - obfuskerade källor postades för det, och som sagt, den större "realismen" i emuleringen gjorde det möjligt att använda SeaBIOS som firmware. Dessutom gjordes det minst ett försök att porta Qemu med Emscripten - jag försökte göra detta socketpar, men utvecklingen var, såvitt jag förstår, frusen.

Så, det verkar, här är källorna, här är Emscripten - ta den och kompilera. Men det finns också bibliotek som Qemu är beroende av, och bibliotek som dessa bibliotek är beroende av etc., och ett av dem är libffi, vilket glib beror på. Det gick rykten på Internet om att det fanns en i den stora samlingen av bibliotekshamnar för Emscripten, men det var på något sätt svårt att tro: för det första var det inte tänkt att vara en ny kompilator, för det andra var den för låg nivå. biblioteket att bara hämta och kompilera till JS. Och det är inte bara en fråga om monteringsinsatser - förmodligen, om du vrider på det, för vissa anropskonventioner kan du generera de nödvändiga argumenten på stacken och anropa funktionen utan dem. Men Emscripten är en knepig sak: för att få den genererade koden att se bekant ut för webbläsarens JS-motoroptimerare används några knep. I synnerhet försöker den så kallade relooping - en kodgenerator som använder den mottagna LLVM IR med några abstrakta övergångsinstruktioner att återskapa rimliga oms, loopar, etc. Tja, hur överförs argumenten till funktionen? Naturligtvis, som argument till JS-funktioner, det vill säga, om möjligt, inte genom stacken.

I början var det en idé att helt enkelt skriva en ersättning för libffi med 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 den befintliga koden - vad kan jag göra, som de säger, "Är uppgifterna så komplexa "Är vi så dumma?" Jag var tvungen att porta libffi till en annan arkitektur så att säga - lyckligtvis har Emscripten både makron för inline assemblering (i Javascript, ja - ja, oavsett arkitektur, alltså assemblern), och möjligheten att köra kod som genereras i farten. I allmänhet, efter att ha mixtrat med plattformsberoende libffi-fragment under en tid, fick jag lite kompilerbar kod och körde den på det första testet jag stötte på. Till min förvåning var testet lyckat. Förbluffad av mitt geni - inget skämt, det fungerade från första lanseringen - jag, som fortfarande inte trodde mina ögon, gick för att titta på den resulterande koden igen för att utvärdera var jag skulle gräva härnäst. Här blev jag galen för andra gången - det enda min funktion gjorde var ffi_call - Detta rapporterade ett lyckat samtal. Det fanns inget samtal i sig. Så jag skickade min första pull-förfrågan, som korrigerade ett fel i testet som är tydligt för alla Olympiadstudenter - verkliga siffror ska inte jämföras som a == b och till och med hur a - b < EPS - du måste också komma ihåg modulen, annars kommer 0 att visa sig vara mycket lika med 1/3... I allmänhet slutade jag med en viss port av libffi, som klarar de enklaste testerna, och med vilken glib är sammanställd - jag bestämde mig för att det skulle vara nödvändigt, jag lägger till det senare. När jag ser framåt kommer jag att säga att kompilatorn, som det visade sig, inte ens inkluderade libffi-funktionen i den slutliga koden.

Men, som jag redan sa, det finns vissa begränsningar, och bland den fria användningen av olika odefinierade beteenden har en mer obehaglig funktion gömts - JavaScript av design stöder inte multithreading med delat minne. I princip kan detta vanligtvis till och med kallas en bra idé, men inte för portering av kod vars arkitektur är knuten till C-trådar. Generellt sett experimenterar Firefox med att stödja delade arbetare, och Emscripten har en pthread-implementering för dem, men jag ville inte vara beroende av det. Jag var tvungen att sakta rota ut multithreading från Qemu-koden - det vill säga ta reda på var trådarna körs, flytta kroppen av slingan som körs i denna tråd till en separat funktion och anropa sådana funktioner en efter en från huvudslingan.

Andra försöket

Vid något tillfälle stod det klart att problemet fortfarande fanns där, och att det inte skulle leda till någon nytta av att på måfå knuffa kryckor runt koden. Slutsats: vi måste på något sätt systematisera processen att lägga till kryckor. Därför togs version 2.4.1, som var färsk vid den tiden, (inte 2.5.0, för vem vet, det kommer att finnas buggar i den nya versionen som ännu inte har fångats, och jag har nog av mina egna buggar ), och det första var att skriva om det säkert thread-posix.c. Jo, det vill säga lika säkert: om någon försökte utföra en operation som ledde till blockering anropades funktionen omedelbart abort() – Det här löste förstås inte alla problem på en gång, men det var åtminstone på något sätt trevligare än att tyst ta emot inkonsekventa data.

I allmänhet är Emscripten-alternativen till stor hjälp för att porta kod till JS -s ASSERTIONS=1 -s SAFE_HEAP=1 - de fångar vissa typer av odefinierat beteende, till exempel anrop till en ojusterad adress (vilket inte alls överensstämmer med koden för typmatriser som HEAP32[addr >> 2] = 1) eller anropa en funktion med fel antal argument.

Justeringsfel är förresten en separat fråga. Som jag redan har sagt har Qemu en "degenererad" tolkningsbackend för kodgenerering TCI (liten kodtolk), och för att bygga och köra Qemu på en ny arkitektur, om du har tur räcker det med en C-kompilator. Nyckelord "om du har tur". Jag hade otur, och det visade sig att TCI använder ojusterad åtkomst när den analyserar sin bytekod. Det vill säga, på alla möjliga typer av ARM och andra arkitekturer med nödvändigtvis jämn åtkomst kompilerar Qemu eftersom de har en normal TCG-backend som genererar inbyggd kod, men om TCI kommer att fungera på dem är en annan fråga. Det visade sig dock att TCI-dokumentationen tydligt indikerade något liknande. Som ett resultat lades funktionsanrop för ojusterad läsning till koden, som hittades i en annan del av Qemu.

Hög förstörelse

Som ett resultat korrigerades ojusterad åtkomst till TCI, en huvudslinga skapades som i sin tur kallade processorn, RCU och några andra småsaker. Och så startar jag Qemu med alternativet -d exec,in_asm,out_asm, vilket innebär att du måste säga vilka kodblock som exekveras, och även vid sändningstillfället att skriva vad gästkoden var, vilken värdkod som blev (i det här fallet bytecode). Den startar, kör flera översättningsblock, skriver felsökningsmeddelandet jag lämnade att RCU nu kommer att starta och... kraschar abort() inuti en funktion free(). Genom att mixtra med funktionen free() Vi lyckades ta reda på att i headern för heapblocket, som ligger i de åtta byten som föregår det tilldelade minnet, i stället för blockstorleken eller något liknande, fanns skräp.

Förstörelse av högen - vad söt... I ett sådant fall finns det ett användbart botemedel - från (om möjligt) samma källor, sätt ihop en inbyggd binär och kör den under Valgrind. Efter en tid var binären klar. Jag startar den med samma alternativ - den kraschar även under initiering, innan den faktiskt når exekvering. Det är obehagligt, naturligtvis - uppenbarligen var källorna inte exakt desamma, vilket inte är förvånande, eftersom konfigurering spanade ut lite olika alternativ, men jag har Valgrind - först ska jag fixa det här felet, och sedan, om jag har tur , kommer originalet att visas. Jag kör samma sak under Valgrind... Y-y-y, y-y-y, eh-uh, den började, gick igenom initialiseringen normalt och gick vidare förbi den ursprungliga buggen utan en enda varning om felaktig minnesåtkomst, för att inte tala om fall. Livet, som de säger, förberedde mig inte för detta - ett kraschande program slutar krascha när det startas under Walgrind. Vad det var är ett mysterium. Min hypotes är att en gång i närheten av den aktuella instruktionen efter en krasch under initialiseringen visade gdb arbete memset-a med en giltig pekare med antingen mmx, eller xmm register, så kanske det var något slags inriktningsfel, även om det fortfarande är svårt att tro.

Okej, Valgrind verkar inte hjälpa här. Och här började det äckligaste – allt verkar till och med börja, men kraschar av absolut okända anledningar på grund av en händelse som kunde ha hänt för miljontals instruktioner sedan. Länge var det inte ens klart hur man skulle närma sig. Till slut var jag fortfarande tvungen att sätta mig ner och felsöka. Att skriva ut vad rubriken skrevs om med visade att den inte såg ut som ett nummer, utan snarare någon sorts binär data. Och se och se, denna binära sträng hittades i BIOS-filen - det vill säga nu var det möjligt att med rimlig säkerhet säga att det var ett buffertspill, och det var till och med tydligt att det skrevs till denna buffert. Nåväl, något sånt här - i Emscripten finns det lyckligtvis ingen randomisering av adressutrymmet, det finns inga hål i det heller, så du kan skriva någonstans i mitten av koden för att mata ut data med pekare från senaste lanseringen, titta på data, titta på pekaren och, om den inte har ändrats, skaffa dig en tankeställare. Det tar visserligen ett par minuter att länka efter en ändring, men vad kan du göra? Som ett resultat hittades en specifik rad som kopierade BIOS från den tillfälliga bufferten till gästminnet - och det fanns faktiskt inte tillräckligt med utrymme i bufferten. Att hitta källan till den konstiga buffertadressen resulterade i en funktion qemu_anon_ram_alloc i fil oslib-posix.c - logiken där var denna: ibland kan det vara användbart att anpassa adressen till en enorm sida på 2 MB, för detta kommer vi att fråga mmap först lite till, och sedan återbetalar vi överskottet med hjälp munmap. Och om sådan justering inte krävs, kommer vi att ange resultatet istället för 2 MB getpagesize() - mmap det kommer fortfarande att ge ut en anpassad adress... Så i Emscripten mmap bara ringer malloc, men det stämmer naturligtvis inte in på sidan. I allmänhet korrigerades en bugg som frustrerade mig i ett par månader genom en förändring i двух rader.

Funktioner för anropsfunktioner

Och nu räknar processorn något, Qemu kraschar inte, men skärmen slås inte på, och processorn går snabbt in i loopar, att döma av utdata -d exec,in_asm,out_asm. En hypotes har uppstått: timeravbrott (eller i allmänhet alla avbrott) kommer inte fram. Och faktiskt, om du skruvar av avbrotten från den infödda församlingen, som av någon anledning fungerade, får du en liknande bild. Men detta var inte svaret alls: en jämförelse av spåren som utfärdats med alternativet ovan visade att avrättningsbanorna divergerade mycket tidigt. Här måste det sägas att jämförelse av vad som spelades in med hjälp av launcher emrun att felsöka utdata med utdata från den inbyggda församlingen är inte en helt mekanisk process. Jag vet inte exakt hur ett program som körs i en webbläsare ansluter till emrun, men vissa linjer i utgången visar sig vara omarrangerade, så skillnaden i diff är ännu inte en anledning att anta att banorna har divergerat. I allmänhet blev det klart att enligt instruktionerna ljmpl det sker en övergång till olika adresser, och den genererade bytekoden är fundamentalt annorlunda: den ena innehåller en instruktion att anropa en hjälpfunktion, den andra inte. Efter att ha googlat instruktionerna och studerat koden som översätter dessa instruktioner, blev det klart att, för det första, omedelbart före det i registret cr0 en inspelning gjordes - också med hjälp av en hjälpreda - som kopplade processorn till skyddat läge, och för det andra att js-versionen aldrig bytte till skyddat läge. Men faktum är att en annan egenskap hos Emscripten är dess ovilja att tolerera kod som implementering av instruktioner call i TCI, vilket valfri funktionspekare resulterar i typ long long f(int arg0, .. int arg9) - funktioner måste anropas med rätt antal argument. Om denna regel bryts, beroende på felsökningsinställningarna, kommer programmet antingen att krascha (vilket är bra) eller anropa fel funktion alls (vilket kommer att vara tråkigt att felsöka). Det finns också ett tredje alternativ – aktivera genereringen av omslag som lägger till/tar bort argument, men totalt tar dessa omslag mycket plats, trots att jag faktiskt bara behöver lite mer än hundra omslag. Bara detta är väldigt tråkigt, men det visade sig vara ett allvarligare problem: i den genererade koden för wrapperfunktionerna konverterades och konverterades argumenten, men ibland anropades inte funktionen med de genererade argumenten - ja, precis som i min libffi-implementering. Det vill säga att vissa medhjälpare helt enkelt inte avrättades.

Lyckligtvis har Qemu maskinläsbara listor med hjälpare i form av en header-fil som

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

De används ganska roligt: ​​för det första omdefinieras makron på det mest bisarra sättet DEF_HELPER_n, och slås sedan på helper.h. I den mån makrot expanderas till en strukturinitierare och ett kommatecken, och sedan en array definieras, och istället för element - #include <helper.h> Som ett resultat fick jag äntligen chansen att prova biblioteket på jobbet parsning, och ett skript skrevs som genererar exakt dessa omslag för exakt de funktioner som de behövs för.

Och så, efter det verkade processorn fungera. Det verkar bero på att skärmen aldrig initierades, även om memtest86+ kunde köras i den ursprungliga församlingen. Här är det nödvändigt att klargöra att Qemu-blockets I/O-kod är skriven i koroutiner. Emscripten har sin egen mycket knepiga implementering, men den behövde fortfarande stödjas i Qemu-koden, och du kan felsöka processorn nu: Qemu stöder alternativ -kernel, -initrd, -append, med vilken du kan starta upp Linux eller till exempel memtest86+, utan att använda blockenheter alls. Men här är problemet: i den inbyggda församlingen kunde man se Linux-kärnan-utgången till konsolen med alternativet -nographic, och ingen utmatning från webbläsaren till terminalen där den startades emrun, kom inte. Det vill säga, det är inte klart: processorn fungerar inte eller grafikutgången fungerar inte. Och så kom det mig att vänta lite. Det visade sig att "processorn inte sover, utan bara blinkar långsamt", och efter ungefär fem minuter kastade kärnan ett gäng meddelanden på konsolen och fortsatte att hänga. Det blev tydligt att processorn i allmänhet fungerar, och vi måste gräva i koden för att arbeta med SDL2. Tyvärr vet jag inte hur man använder det här biblioteket, så på vissa ställen var jag tvungen att agera på måfå. Vid något tillfälle blinkade linjen parallell0 på skärmen på en blå bakgrund, vilket antydde några tankar. Till slut 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: det fungerar i den inbyggda builden, men inte i Emscripten. Efter att ha blivit av med onödiga fönster med hjälp av alternativ -monitor none -parallel none -serial none och instruktioner om att kraftfullt rita om hela skärmen på varje bildruta, allt fungerade plötsligt.

Coroutiner

Så, emulering i webbläsaren fungerar, men du kan inte köra något intressant enstaka diskett i den, eftersom det inte finns någon block-I/O - du måste implementera stöd för coroutines. Qemu har redan flera coroutine-backends, men på grund av JavaScript och Emscriptens kodgenerator kan du inte bara börja jonglera med stackar. Det verkar som att "allt är borta, gipset tas bort", men Emscripten-utvecklarna har redan tagit hand om allt. Detta är implementerat ganska roligt: ​​låt oss kalla ett funktionsanrop som detta misstänkt emscripten_sleep och flera andra som använder Asyncify-mekanismen, samt pekaranrop och anrop till valfri funktion där ett av de två föregående fallen kan inträffa längre ner i stacken. Och nu, före varje misstänkt anrop, kommer vi att välja ett asynkront sammanhang, och omedelbart efter anropet kommer vi att kontrollera om ett asynkront anrop har inträffat, och om det har skett, kommer vi att spara alla lokala variabler i detta asynkrona sammanhang, ange vilken funktion för att överföra kontrollen till när vi behöver fortsätta körningen och avsluta den aktuella funktionen. Det är här det finns utrymme för att studera effekten slösande — för behoven av fortsatt kodexekvering efter att ha återvänt från ett asynkront anrop, genererar kompilatorn "stubbar" av funktionen som börjar efter ett misstänkt anrop — så här: om det finns n misstänkta anrop, kommer funktionen att utökas någonstans n/2 gånger — detta är fortfarande, om inte Tänk på att efter varje potentiellt asynkront anrop måste du lägga till att spara några lokala variabler till den ursprungliga funktionen. Därefter var jag till och med tvungen att skriva ett enkelt skript i Python, som, baserat på en given uppsättning särskilt överanvända funktioner som förment "inte tillåter asynkron att passera genom sig själva" (det vill säga stackpromotion och allt jag just beskrev inte arbete i dem), indikerar anrop genom pekare där funktioner ska ignoreras av kompilatorn så att dessa funktioner inte anses vara asynkrona. Och sedan är JS-filer under 60 MB helt klart för mycket - låt oss säga åtminstone 30. Även om jag en gång höll på att sätta upp ett monteringsskript och av misstag kastade ut länkalternativen, bland annat -O3. Jag kör den genererade koden och Chromium äter upp minne och kraschar. Jag tittade då av misstag på vad han försökte ladda ner... Tja, vad kan jag säga, jag skulle också ha frusit om jag hade blivit ombedd att eftertänksamt studera och optimera ett 500+ MB Javascript.

Tyvärr var kontrollerna i Asyncify-stödbibliotekskoden inte helt vänliga mot longjmp-s som används i den virtuella processorkoden, men efter en liten patch som inaktiverar dessa kontroller och kraftfullt återställer sammanhang som om allt var bra, fungerade koden. Och så började en märklig sak: ibland utlöstes kontroller i synkroniseringskoden - samma som kraschar koden om den enligt exekveringslogiken skulle blockeras - någon försökte ta tag i en redan infångad mutex. Lyckligtvis visade det sig att detta inte var ett logiskt problem i den serialiserade koden - jag använde helt enkelt den vanliga huvudslingfunktionaliteten som tillhandahålls av Emscripten, men ibland skulle det asynkrona samtalet packa upp stacken helt och i det ögonblicket skulle det misslyckas setTimeout från huvudslingan - alltså gick koden in i huvudsling-iterationen utan att lämna den föregående iterationen. Omskrev på en oändlig slinga och emscripten_sleep, och problemen med mutexes upphörde. Koden har till och med blivit mer logisk - trots allt har jag faktiskt ingen kod som förbereder nästa animationsram - processorn beräknar bara något och skärmen uppdateras med jämna mellanrum. Men problemen slutade inte där: ibland avslutades Qemu-exekveringen helt enkelt tyst utan några undantag eller fel. I det ögonblicket gav jag upp det, men när jag ser framåt kommer jag säga att problemet var detta: Coroutine-koden använder faktiskt inte setTimeout (eller åtminstone inte så ofta som du kanske tror): funktion emscripten_yield ställer helt enkelt den asynkrona samtalsflaggan. Hela poängen är den emscripten_coroutine_next är inte en asynkron funktion: internt kontrollerar den flaggan, återställer den och överför kontrollen dit den behövs. Det vill säga, marknadsföringen av stacken slutar där. Problemet var att på grund av use-after-free, som dök upp när coroutine-poolen inaktiverades på grund av att jag inte kopierade en viktig kodrad från den befintliga coroutine-backend, funktionen qemu_in_coroutine returnerade sant när det i själva verket borde ha returnerats falskt. Detta ledde till ett samtal emscripten_yield, över vilken det inte fanns någon på traven emscripten_coroutine_next, stapeln vecklades ut till toppen, men nej setTimeout, som jag redan sa, var inte utställd.

JavaScript-kodgenerering

Och här är faktiskt den utlovade "vända tillbaka köttfärsen." Inte riktigt. Naturligtvis, om vi kör Qemu i webbläsaren och Node.js i den, så får vi naturligtvis helt fel JavaScript efter kodgenerering i Qemu. Men ändå, någon form av omvänd transformation.

Först lite om hur Qemu fungerar. Förlåt mig genast: Jag är inte en professionell Qemu-utvecklare och mina slutsatser kan vara felaktiga på vissa ställen. Som de säger, "elevens åsikt behöver inte sammanfalla med lärarens åsikt, Peanos axiomatik och sunt förnuft." Qemu har ett visst antal gästarkitekturer som stöds och för varje finns det en katalog som target-i386. När du bygger kan du ange stöd för flera gästarkitekturer, men resultatet blir bara flera binärer. Koden för att stödja gästarkitekturen genererar i sin tur vissa interna Qemu-operationer, som TCG (Tiny Code Generator) redan förvandlar till maskinkod för värdarkitekturen. Som det står i readme-filen som finns i tcg-katalogen var denna ursprungligen en del av en vanlig C-kompilator, som senare anpassades för JIT. Därför är till exempel målarkitektur i termer av detta dokument inte längre en gästarkitektur, utan en värdarkitektur. Vid något tillfälle dök en annan komponent upp - Tiny Code Interpreter (TCI), som ska köra kod (nästan samma interna operationer) i avsaknad av en kodgenerator för en specifik värdarkitektur. Faktum är att, som det står i dokumentationen, kanske denna tolk inte alltid fungerar lika bra som en JIT-kodgenerator, inte bara kvantitativt när det gäller hastighet utan också kvalitativt. Även om jag inte är säker på att hans beskrivning är helt relevant.

Först försökte jag göra en fullfjädrad TCG-backend, men blev snabbt förvirrad i källkoden och en inte helt tydlig beskrivning av bytekod-instruktionerna, så jag bestämde mig för att slå in TCI-tolken. Detta gav flera fördelar:

  • När du implementerar en kodgenerator kan du inte titta på beskrivningen av instruktioner, utan på tolkkoden
  • du kan generera funktioner inte för varje översättningsblock som påträffas, utan till exempel först efter den hundrade exekveringen
  • om den genererade koden ändras (och detta verkar vara möjligt, att döma av funktionerna med namn som innehåller ordet patch), måste jag ogiltigförklara den genererade JS-koden, men jag kommer åtminstone att ha något att återskapa den från

När det gäller den tredje punkten är jag inte säker på att patchning är möjlig efter att koden exekveras för första gången, men de två första punkterna räcker.

Ursprungligen genererades koden i form av en stor switch på adressen till den ursprungliga bytekodinstruktionen, men sedan, med tanke på artikeln om Emscripten, optimering av genererad JS och relooping, bestämde jag mig för att generera mer mänsklig kod, särskilt eftersom det empiriskt visade sig att den enda ingången till översättningsblocket är dess Start. Inte tidigare sagt än gjort, efter ett tag hade vi en kodgenerator som genererade kod med ifs (om än utan loopar). Men otur, den kraschade och gav ett meddelande om att instruktionerna var av någon felaktig längd. Dessutom var den sista instruktionen på denna rekursionsnivå brcond. Okej, jag lägger till en identisk kontroll för genereringen av den här instruktionen före och efter det rekursiva anropet och... inte en av dem exekverades, men efter påståendeväxlingen misslyckades de fortfarande. Till slut, efter att ha studerat den genererade koden, insåg jag att efter bytet laddas pekaren till den aktuella instruktionen om från stacken och skrivs förmodligen över av den genererade JavaScript-koden. Och så blev det. Att öka bufferten från en megabyte till tio ledde inte till någonting, och det blev tydligt att kodgeneratorn körde i cirklar. Vi var tvungna att kontrollera att vi inte gick över gränserna för den nuvarande TB, och om vi gjorde det, utfärda adressen till nästa TB med ett minustecken så att vi kunde fortsätta avrättningen. Dessutom löser detta problemet "vilka genererade funktioner bör ogiltigförklaras om denna bit av bytekod har ändrats?" — endast den funktion som motsvarar detta översättningsblock behöver ogiltigförklaras. Förresten, även om jag felsökte allt i Chromium (eftersom jag använder Firefox och det är lättare för mig att använda en separat webbläsare för experiment), hjälpte Firefox mig att rätta till inkompatibiliteter med asm.js-standarden, varefter koden började fungera snabbare i Krom.

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 slutfört, men jag är trött på att i hemlighet föra denna långsiktiga konstruktion till perfektion. Därför bestämde jag mig för att publicera det jag har för nu. Koden är lite skrämmande på sina ställen, eftersom det här är ett experiment och det är inte klart i förväg vad som behöver göras. Förmodligen, då är det värt att utfärda normala atomära åtaganden ovanpå någon mer modern version av Qemu. Under tiden finns det en tråd i Gita i ett bloggformat: för varje "nivå" som åtminstone på något sätt har passerats, har en detaljerad kommentar på ryska lagts till. Egentligen är den här artikeln till stor del en återberättelse av slutsatsen git log.

Du kan prova allt här (akta dig för trafik).

Vad som redan fungerar:

  • x86 virtuell processor igång
  • Det finns en fungerande prototyp av en JIT-kodgenerator från maskinkod till JavaScript
  • Det finns en mall för att montera andra 32-bitars gästarkitekturer: just nu kan du beundra Linux för MIPS-arkitekturen som fryser i webbläsaren vid laddningsstadiet

Vad mer kan du göra

  • Snabba upp emuleringen. Även i JIT-läge verkar det gå långsammare än Virtual x86 (men det finns potentiellt en hel Qemu med mycket emulerad hårdvara och arkitekturer)
  • För att göra ett normalt gränssnitt - ärligt talat, jag är ingen bra webbutvecklare, så för nu har jag gjort om standard Emscripten-skalet så gott jag kan
  • Försök att starta mer komplexa Qemu-funktioner - nätverk, VM-migrering, etc.
  • UPD: du kommer att behöva skicka in dina få utvecklingar och felrapporter till Emscripten uppströms, som tidigare portörer av Qemu och andra projekt gjorde. Tack till dem för att de implicit kan använda deras bidrag till Emscripten som en del av mitt uppdrag.

Källa: will.com

Lägg en kommentar