Lang geleden besloot ik uit lolligheid de omkeerbaarheid van het proces te bewijzen en te leren JavaScript (specifiek, Asm.js) te genereren uit machinetaal. Voor het experiment koos ik QEMU, en enige tijd later schreef ik een artikel op Habr. In de reacties werd me aangeraden het project om te zetten naar WebAssembly, en zelf wilde ik het bijna afgeronde project ook niet zomaar laten vallen... Het werk ging door, maar het ging wel erg langzaam, en onlangs verscheen er een opmerking Aangezien ik al geleerd had om QEMU op een rudimentaire manier naar JavaScript te porteren, was het deze keer besloten om het op een degelijke manier aan te pakken en niet dezelfde fouten te herhalen.
Taken
Fout nummer één: afsplitsen van de point release
Mijn eerste fout was om mijn versie van de upstream-versie 2.4.1 af te splitsen. Destijds leek het me een goed idee: als er een point release bestaat, zal deze waarschijnlijk stabieler zijn dan de eenvoudige 2.4, en vooral van de branch
. En omdat ik van plan was om een aanzienlijk aantal eigen bugs toe te voegen, had ik geen behoefte aan bugs van anderen. Dit is waarschijnlijk ook gebeurd. Maar er was een probleem: QEMU blijft niet stil staan, en op een gegeven moment kondigden ze zelfs een optimalisatie van de gegenereerde code van ongeveer 10% aan. "Aha, dat krijg ik wel in mijn code verwerkt", dacht ik en ik viel door de mand. Hier moet ik een stap terug doen: vanwege de eentakige aard van QEMU.js en het feit dat de originele QEMU geen gebrek aan multithreading voorziet (dat wil zeggen, het is cruciaal voor hem om de mogelijkheid van gelijktijdige uitvoering van verschillende niet-verbonden codepaden te hebben, en niet gewoon "alle kernen te gebruiken"), moesten de belangrijkste functies van de threads "omgekeerd" worden voor externe aanroepbaarheid. Dit creëerde enkele natuurlijke problemen bij het samenvoegen. Echter, het feit dat een deel van de wijzigingen uit de branch master, waarmee ik mijn code probeerde te combineren, ook waren cherry-picked in de point release (en dus ook in mijn branch) zou waarschijnlijk geen extra gemak hebben opgeleverd. masterOver het geheel genomen besloot ik dat het nog steeds zinnig was om het prototype weg te gooien, uit elkaar te halen en een nieuwe versie vanaf nul op basis van iets nieuwers te bouwen, en dat al uit
Fout nummer twee: TLP-methodologie master.
Fout nummer twee: TLP-methodologie
In wezen is dit geen fout, maar eerder een eigenaardigheid van het creëren van een project in omstandigheden van volledige onduidelijkheid over "waarheen en hoe verder?", en of we überhaupt zullen komen. Onder deze omstandigheden slordig programmeren was een begrijpelijke optie, maar natuurlijk wilde ik dit niet zonder noodzaak herhalen. Deze keer wilde ik het goed doen: atomaire commits, bewuste codewijzigingen (en niet "random karakters aan elkaar rijgen totdat het compileert (met waarschuwingen)", zoals Linus Torvalds ooit over iemand zei, als we Wikipedia geloven) enzovoort.
Fout nummer drie: zonder te weten wat de situatie is, in het diepe springen.
Daarom ben ik daar nog steeds niet helemaal vanaf, maar nu heb ik besloten om niet de weg van de minste weerstand te volgen, maar om "volwassen" te werken, namelijk mijn eigen TCG backend vanaf nul te schrijven, zodat ik later niet hoef te zeggen: "Ja, het is natuurlijk traag, maar ik kan niet alles controleren - TCI is zo geschreven...". Daarnaast leek dit aanvankelijk de duidelijke oplossing omdat ik binair code genereer. Zoals men zegt: "Ik bouwde Gent‘, maar niet dat": de code is inderdaad binair, maar het beheer ervan kan niet zomaar worden overgedragen - het moet expliciet in de browser worden gestopt voor compilatie, waardoor je uiteindelijk een object uit de JS-wereld krijgt dat nog ergens moet worden opgeslagen. Echter, op normale RISC-architecturen, voor zover ik begrijp, is het gebruikelijk om de instructiecache expliciet te wissen voor opnieuw gegenereerde code - als dat niet precies is wat we nodig hebben, is het in ieder geval dichtbij. Bovendien heb ik van mijn vorige poging geleerd dat het beheer geen toegang krijgt tot het midden van de vertaalblok, dus bytecode die vanaf een willekeurige offset wordt geïnterpreteerd, is niet echt nodig en we kunnen eenvoudig genereren per functie op de TB.
Kwamen en schopten
Hoewel ik al in juli begon met het herschrijven van de code, kwam de magische duw ongemerkt: meestal komen e-mails van GitHub binnen als meldingen over antwoorden op Issues en Pull requests, maar hier, plotseling een vermelding in de thread in de context: "Hij deed iets soortgelijks, misschien zegt hij iets." Het ging over het gebruik van de verwante bibliotheek Emscripten, voor het creëren van WASM JIT. Dus ik zei dat jullie daar de Apache 2.0-licentie hebben, en QEMU als geheel wordt onder GPLv2 verspreid, en ze zijn niet echt compatibel. Het bleek plotseling dat de licentie op de een of andere manier kan worden aangepast. (ik weet het niet: misschien veranderen, misschien dubbele licenties, misschien iets anders…). Dit verraste me natuurlijk, want ik had op dat moment al meerdere keren gekeken naar WebAssembly, en ik voelde me daar een beetje triest en verward over. Hier was er echter een bibliotheek die de basisblokken met de overgangsgrafiek kon verwerken, bytecode kon genereren, en zelfs zelf de code in een interpreter kon uitvoeren, indien nodig.
Daarna was er nog in de mailinglijst van QEMU, maar dat was meer een vraag van: 'Wie heeft dit eigenlijk nodig?'. Maar het bleek, plotseling, dat het daadwerkelijk nodig was. In ieder geval, je kunt enkele toepassingen bedenken als het een beetje vlot werkt:
- het draaien van iets educatiefs zonder installatie
- virtualisatie op iOS, waar geruchten gaan dat de enige applicatie die het recht heeft op dynamische codegeneratie de JS-engine is (is dat waar?)
- een demonstratie van een mini-OS - enkele schijfjes, embedded, allerlei firmware, enzovoort…
Kenmerken van de browser runtime
Zoals ik al zei, is QEMU afhankelijk van multithreading, en in de browser is dat er niet. Nou ja, niet helemaal… In het begin was het er helemaal niet, daarna kwamen er WebWorkers - zo begrijp ik, is dit een vorm van multithreading die is gebaseerd op berichtenoverdracht zonder gedeelde variabelen. Dit creëert natuurlijk aanzienlijke problemen bij het porteren van bestaande code die is gebaseerd op het shared memory-model. Vervolgens, onder druk van het publiek, werd het geïmplementeerd en het kreeg de naam SharedArrayBuffers. Het werd geleidelijk geïntroduceerd, het starten ervan in verschillende browsers werd gevierd, daarna vierde men het nieuwe jaar, en toen kwam Meltdown… Na dat alles concludeerde men dat, ongeacht hoe je de tijd meet, met behulp van shared memory en een stroom die de teller verhoogt, je toch . Zo werd multithreading met gedeeld geheugen uitgeschakeld. Het lijkt erop dat het later weer werd ingeschakeld, maar zoals bleek uit het eerste experiment, was het leven ook zonder mogelijk, en als dat zo is, laten we het proberen te doen zonder afhankelijk te zijn van multithreading.
Een tweede kenmerk is het onvermogen om laag-niveau manipulaties met de stack uit te voeren: je kunt niet simpelweg de huidige context opslaan en overschakelen naar een nieuwe met een nieuwe stack. De call stack wordt beheerd door de JS virtuele machine. Wat is er aan de hand, nu we toch hebben besloten om met voormalige threads volledig handmatig om te gaan? Het probleem is dat block I/O in QEMU wordt geïmplementeerd via coroutines, waarbij we laag-niveau stackmanipulaties goed zouden kunnen gebruiken. Gelukkig bevat Emscripten al een mechanisme voor asynchrone operaties, zelfs twee: en . De eerste werkt door een aanzienlijke toename van de gegenereerde JavaScript-code en wordt al niet meer ondersteund. De tweede is de huidige "juiste manier" en werkt door bytecode te genereren voor zijn eigen interpreter. Natuurlijk werkt het langzaam, maar het zorgt er tenminste voor dat de code niet opblaast. Echter, de ondersteuning voor coroutines voor dit mechanisme moest ik zelf bijdragen (er waren al coroutines geschreven voor Asyncify en er was een implementatie van een vergelijkbare API voor Emterpreter, het moest gewoon worden samengevoegd).
Tot nu toe heb ik nog niet de tijd gehad om de code te splitsen in die voor compilatie naar WASM en die die door Emterpreter wordt geïnterpreteerd, dus block devices werken nog niet (zie in de volgende afleveringen, zoals ze zeggen…). Het moet dus uiteindelijk zo'n grappige gelaagde creatie opleveren:
- geïnterpreteerde block I/O. Maar echt, verwachtte je een nagebootste NVMe met native prestaties? 🙂
- statisch gecompileerde hoofdcode van QEMU (de vertaler, andere nagebootste apparaten, enz.)
- dynamisch gecompileerde gastcode naar WASM
Kenmerken van de QEMU-broncode
Zoals je waarschijnlijk al geraden hebt, is de code voor het emuleren van gastarchitecturen en de code voor het genereren van host machine-instructies in QEMU gescheiden. In werkelijkheid is het daar zelfs nog iets gecompliceerder:
- er zijn gastarchitecturen
- bestaande versnellers, namelijk KVM voor hardwarevirtualisatie op Linux (voor compatibele gast- en hostsystemen), TCG voor JIT-codegeneratie waar dan ook. Sinds QEMU 2.9 is er ondersteuning voor de standaard voor hardwarevirtualisatie HAXM op Windows ()
- als TCG wordt gebruikt in plaats van hardwarevirtualisatie, dan is er aparte ondersteuning voor codegeneratie voor elke hostarchitectuur, evenals voor een algemene interpreter.
- … en rondom dit alles — geëmuleerde randapparatuur, gebruikersinterface, migratie, record-replay, enz.
Trouwens, wist u dat: QEMU kan niet alleen een hele computer emuleren, maar ook een processor voor een afzonderlijk gebruikersproces in de hostkernel, wat bijvoorbeeld wordt gebruikt door de fuzzingtool AFL voor de instrumentatie van binaries. Misschien wil iemand deze modus van QEMU naar JS porteren? 😉
Net als de meeste al lang bestaande vrije software, wordt QEMU verzameld via de aanroep configure en make. Stel dat u iets wilt toevoegen: een TCG-backend, implementatie van threads, of iets anders. Wees niet te snel blij/angstig (onderstreep wat nodig is) over de vooruitzichten voor interactie met Autoconf — in feite, configure is de code van QEMU kennelijk zelfgeschreven en wordt niet gegenereerd uit iets.
WebAssembly
Dus wat is WebAssembly (ook wel WASM genoemd)? Het is een vervanging voor Asm.js, dat nu niet meer als valide JavaScript-code wordt vermomd. Sterker nog, het is puur binair en geoptimaliseerd, en zelfs het gewoon opslaan van een geheel getal is niet zo eenvoudig: het wordt compact opgeslagen in het formaat .
Misschien heeft u gehoord van het relooping-algoritme voor Asm.js — dit is het herstel van ‘high-level’ instructies voor stroombesturing (zoals if-then-else, lussen, enz.) waarvoor JS-engines zijn ontworpen, uit laag-niveau LLVM IR, dat dichter bij de machinecode is die door de processor wordt uitgevoerd. Uiteraard ligt de tussenversie van QEMU dichter bij de tweede. Het lijkt alsof hier de bytecode is, het einde van de kwellingen… En dan zijn er weer blokken, if-then-else en lussen!.
En hierin ligt nog een reden waarom Binaryen nuttig is: het kan natuurlijk high-level blokken aanvaarden die dicht bij de opslag in WASM zijn. Maar het kan ook code genereren uit een graf van basisblokken en de overgangen daartussen. Over hetgeen het verbergt achter de handige C/C++ API van het WebAssembly-opslagformaat heb ik al gesproken.
TCG (Tiny Code Generator)
TCG de backend voor de C-compiler. Het lijkt erop dat het niet kon concurreren met GCC, maar uiteindelijk vond het zijn plaats binnen QEMU als de codegeneratiemechanisme voor het hostplatform. Er is ook een TCG-backend, die een abstracte bytecode genereert die onmiddellijk door de interpreter wordt uitgevoerd, maar ik besluit deze keer af te zien van het gebruik ervan. Echter, het feit dat QEMU al de mogelijkheid heeft om over te schakelen naar de gegenereerde TB via de functie tcg_qemu_tb_exec, was voor mij zeer handig.
Om een nieuwe TCG-backend aan QEMU toe te voegen, moet je een subdirectory maken tcg/ (in dit geval, tcg/binaryen), en daarin twee bestanden: tcg-target.h en tcg-target.inc.c en dit allemaal in configure. Je kunt daar ook andere bestanden plaatsen, maar zoals je kunt raden uit de namen van deze twee, zullen beide ergens worden opgenomen: één als een gewone headerfile (die wordt opgenomen in tcg/tcg.h, en dat weer in andere bestanden in de directories tcg, accel en meer), de andere alleen als code snippet in tcg/tcg.c, maar het heeft toegang tot zijn static-functies.
Besluitend dat ik te veel tijd zou besteden aan gedetailleerde onderzoeken van hoe het werkt, heb ik eenvoudigweg de 'skelet'-bestanden van deze twee bestanden uit een andere backend-implementatie gekopieerd, waarbij ik dit eerlijk in de licentietitel heb vermeld.
Bestand tcg-target.h bevat voornamelijk instellingen in de vorm van #define-en:
- hoeveel registers en welke breedte er zijn in de doelsarchitectuur (in ons geval — wat wij willen, is er — het is meer de vraag wat er in effectievere code door de browser wordt gegenereerd op de 'echte doelsarchitectuur'...)
- uitlijning van hostinstructies: op x86, en zelfs in TCI, worden instructies helemaal niet uitgelijnd, ik ben van plan om geen instructies maar verwijzingen naar structuren van de Binaryen-bibliotheek in de codebuffer te plaatsen, dus ik zeg: 4 bytes
- welke optionele instructies de backend kan genereren — we zetten alles aan wat we in Binaryen vinden, de rest laat ik de accelerator in eenvoudigere instructies splitsen.
- Wat is ongeveer de grootte van de TLB-cache die de backend aanroept? Het punt is dat alles in QEMU serieus is: hoewel er helperfuncties zijn die load/store uitvoeren met inachtneming van de gast-MMU (en wie kan er nu nog zonder?), wordt hun translatiecache opgeslagen in de vorm van een struct, waarvan de verwerking handig in de translatieblokken kan worden geïntegreerd. De vraag is welke offset in deze structuur het meest efficiënt wordt verwerkt door een kleine en snelle instructiesequentie.
- Hier kun je ook de toewijzing van een of twee gereserveerde registers aanpassen, de TB-aanroep via een functie inschakelen en optioneel een paar kleine zaken beschrijven.
inline-functies zoalsflush_icache_range(maar dit is niet ons geval).
Bestand tcg-target.inc.c, doorgaans is dit uiteraard veel groter in omvang en bevat het verschillende verplichte functies:
- initialisatie, die onder andere beperkingen aangeeft op welke instructie met welke operand kan werken. Onbeschoft gekopieerd door mij van een andere backend.
- Een functie die één instructie van de interne bytecode accepteert.
- Hier kunnen ook hulpfuncties worden geplaatst, en hier kunnen statische functies uit worden gebruikt.
tcg/tcg.c
Voor mezelf heb ik de volgende strategie gekozen: in de eerste woorden van een nieuw translatieblok heb ik vier aanwijzers genoteerd: een label voor het begin (een zekere waarde in de buurt van 0xFFFFFFFF, waaraan de huidige staat van de TB werd bepaald), de context, de gegenereerde module en een magic number voor debugging. Eerst werd het label ingesteld op 0xFFFFFFFF - n, waar n — een klein positief getal, en bij elke uitvoering via de interpreter werd het met 1 verhoogd. Toen het dit bereikt had, 0xFFFFFFFE, vond de compilatie plaats, werd de module opgeslagen in de functietabel, geïmporteerd in een kleine 'launcher', waar de uitvoering naartoe ging uit tcg_qemu_tb_exec, en werd de module uit het geheugen van QEMU verwijderd.
Parafraserend klassiekers, 'De workaround, hoeveel is er in deze klank voor het hart van de programmeur verweven...'. Desalniettemin lekte het geheugen ergens naar toe. En het was geheugen dat door QEMU werd beheerd! Ik had code die bij het schrijven van de volgende instructie (nou ja, de aanwijzer) de verwijzing naar de locatie die daar eerder stond verwijderde, maar dat hielp niet. Eigenlijk allocateert QEMU in de eenvoudigste gevallen geheugen bij het opstarten en schrijft daar de gegenereerde code naar toe. Wanneer de buffer vol is, wordt de code weggegooid, en dus begint de volgende te worden geschreven.
Na het bestuderen van de code entend, dat de workaround met magic number ervoor zorgde dat de heap niet crashte doordat er iets verkeerds werd vrijgegeven op een niet-geïnitialiseerde buffer tijdens de eerste doorloop. Maar wie herschrijft de buffer om mijn functie heen daarna? Zoals aanbevolen door de ontwikkelaars van Emscripten, heb ik het probleem aangepakt door de resulterende code weer over te zetten naar een native applicatie en deze te testen met Mozilla Record-Replay... Kortom, uiteindelijk realiseerde ik me één eenvoudige zaak: voor elk blok wordt geheugen gealloceerd. struct TranslationBlock met de beschrijving ervan. Raad eens waar... Juist, direct voor het blok in de buffer. Toen ik dit besefte, besloot ik te stoppen met de workarounds (althans enkele), en gooide gewoon de magic number weg, terwijl ik de resterende woorden verplaatste naar struct TranslationBlock, door een enkelvoudige gelinkte lijst aan te leggen, waarmee ik snel kan doorlopen tijdens het leegmaken van de vertaalcache en geheugen kan vrijgeven.
Sommige workarounds blijven bestaan: bijvoorbeeld, gemarkeerde aanwijzers in de codebuffer — een deel van hen zijn gewoon BinaryenExpressionRef, wat betekent dat ze kijken naar uitdrukkingen die lineair in het gegenereerde basisblok moeten worden geplaatst, een deel is de overgangsvoorwaarde tussen de BB, en een deel is waarheen te gaan. En er zijn al voorbereide blokken voor Relooper die aan elkaar moeten worden verbonden op basis van voorwaarden. Om ze te onderscheiden, wordt aangenomen dat ze allemaal in ieder geval op vier bytes zijn uitgelijnd, zodat de twee laagste bits veilig kunnen worden gebruikt voor de label, waarbij je alleen niet moet vergeten deze te verwijderen wanneer dat nodig is. Overigens worden dergelijke labels al gebruikt in QEMU om de reden voor het verlaten van de TCG-lus aan te geven.
Gebruik van Binaryen
Modules in WebAssembly bevatten functies, waarvan elke een lichaam heeft dat een expressie vertegenwoordigt. Expressies zijn unaire en binaire operaties, blokken die bestaan uit lijsten van andere expressies, control flow, enz. Zoals ik al zei, is de control flow hier georganiseerd als hoog-niveau vertakkingen, lussen, functieaanroepen, enz. Argumenten worden niet op de stack doorgegeven, maar expliciet, zoals in JS. Er zijn ook globale variabelen, maar ik heb ze niet gebruikt, dus ik zal er niet over vertellen.
Daarnaast hebben functies genummerde lokale variabelen die beginnen vanaf nul, met de typen: int32 / int64 / float / double. Let op dat alhoewel hier niet alles volledig low-level is qua controle stroom, gehele getallen toch geen teken- of niet-tekenindicator bevatten: hoe een getal zich gedraagt, hangt af van de bewerkingscode.
Over het algemeen biedt Binaryen : je creëert een module, waar je uitdrukkingen maakt — unaire, binaire, blokken van andere uitdrukkingen, controle stromen, enzovoort. Vervolgens creëer je een functie waarvan het lichaam de uitdrukking moet zijn. Als jij, net als ik, een low-level overgangsgrafiek hebt — helpt de component relooper je. Voor zover ik begrijp, kun je high-level controle stromen binnen een blok gebruiken, zolang ze het blok niet verlaten — dat wil zeggen, je kunt interne vertakkingen van fast path / slow path binnen de ingebedde code voor TLB-cacheverwerking maken, maar niet ingrijpen in de 'buiten' controle stroom. Wanneer je de relooper vrijgeeft, worden zijn blokken vrijgegeven; wanneer je de module vrijgeeft, verdwijnen de uitdrukkingen, functies, enzovoort, die in zijn arena.
Als je echter de code ter plekke wilt interpreteren zonder onnodige creaties en verwijderingen van een instantie van de interpreter, kan het zinvol zijn om deze logica naar een C++-bestand te verplaatsen, en van daaruit rechtstreeks alle C++ API van de bibliotheek te beheren, zonder gebruik te maken van kant-en-klare wrappers.
Dus, om code te genereren, moet je
// настроить глобальные параметры (можно поменять потом)
BinaryenSetAPITracing(0);
BinaryenSetOptimizeLevel(3);
BinaryenSetShrinkLevel(2);
// создать модуль
BinaryenModuleRef MODULE = BinaryenModuleCreate();
// описать типы функций (как создаваемых, так и вызываемых)
helper_type BinaryenAddFunctionType(MODULE, "helper-func", BinaryenTypeInt32(), int32_helper_args, ARRAY_SIZE(int32_helper_args));
// (int23_helper_args приоб^Wсоздаются отдельно)
// сконструировать супер-мега выражение
// ... ну тут уж вы как-нибудь сами :)
// потом создать функцию
BinaryenAddFunction(MODULE, "tb_fun", tb_func_type, func_locals, FUNC_LOCALS_COUNT, expr);
BinaryenAddFunctionExport(MODULE, "tb_fun", "tb_fun");
...
BinaryenSetMemory(MODULE, (1 << 15) - 1, -1, NULL, NULL, NULL, NULL, NULL, 0, 0);
BinaryenAddMemoryImport(MODULE, NULL, "env", "memory", 0);
BinaryenAddTableImport(MODULE, NULL, "env", "tb_funcs");
// запросить валидацию и оптимизацию при желании
assert (BinaryenModuleValidate(MODULE));
BinaryenModuleOptimize(MODULE);… als ik iets ben vergeten — excuses, dit is alleen om de schaal te representeren, de details staan in de documentatie.
En nu begint de crux: iets als dit:
static char buf[1 << 20];
BinaryenModuleOptimize(MODULE);
BinaryenSetMemory(MODULE, 0, -1, NULL, NULL, NULL, NULL, NULL, 0, 0);
int sz = BinaryenModuleWrite(MODULE, buf, sizeof(buf));
BinaryenModuleDispose(MODULE);
EM_ASM({
var module = new WebAssembly.Module(new Uint8Array(wasmMemory.buffer, $0, $1));
var fptr = $2;
var instance = new WebAssembly.Instance(module, {
'env': {
'memory': wasmMemory,
// ...
}
);
// en nu heb je al een instance!
}, buf, sz);Om de wereld van QEMU met JS te verbinden en snel toegang te krijgen tot gecompileerde functies, is er een array (functietabel voor import in de launcher) gemaakt, waarin de gegenereerde functies werden geplaatst. Om de index snel te berekenen, werd aanvankelijk de index van het nulwoord van het vertaalblok gebruikt, maar later werd de berekende index gewoon in het veld ingevuld. struct TranslationBlock.
Overigens, (momenteel met een dubieuze licentie) werkt alleen goed in Firefox. De ontwikkelaars van Chrome waren helemaal niet voorbereid op het feit dat iemand meer dan duizend instanties van WebAssembly-modules zou willen maken, dus ze allocateerden eenvoudigweg een gigabyte virtuele adresruimte per stuk...
Dat is voorlopig alles. Er komt misschien nog een artikel, als iemand dat interessant vindt. Tot nu toe is het gewoon slechts het laten werken van blokapparaten. Het kan ook de moeite waard zijn om de compilatie van WebAssembly-modules asynchroon te maken, zoals gebruikelijk is in de wereld van JS, aangezien er toch een interpreter is die alles kan uitvoeren terwijl de native module nog niet klaar is.
Tot slot een raadsel: je hebt een binaire uitvoering op een 32-bits architectuur samengesteld, maar de code gaat via geheugengevoelige bewerkingen in Binaryen, ergens naar de stack of ergens anders in de bovenste 2 GB van het 32-bits adresruimte. Het probleem is dat dit vanuit het perspectief van Binaryen een toegang tot een te groot resulterend adres is. Hoe omzeilen we dit?
Vanuit adminperspectief
Ik heb dit uiteindelijk niet getest, maar mijn eerste gedachte was: "Wat als we 32-bits Linux installeren?" Dan zou het bovenste gedeelte van het adresruimte bezet zijn door de kernel. De vraag is alleen hoeveel dat zou zijn: 1 of 2 GB.
Vanuit programmeerperspectief (praktische optie)
We blazen een bubbel op in het bovenste gedeelte van het adresruimte. Ik begrijp zelf niet waarom het werkt — daar hoort toch al de stack te zijn. Maar "wij zijn praktici: wij werken en niemand weet waarom…".
// 2gbubble.c
// Usage: LD_PRELOAD=2gbubble.so <program>
#include <sys/mman.h>
#include <assert.h>
void __attribute__((constructor)) constr(void)
{
assert(MAP_FAILED != mmap(1u >> 31, (1u >> 31) - (1u >> 20), PROT_NONE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0));
}… het is echter niet compatibel met Valgrind, maar gelukkig stoot Valgrind iedereen daar heel efficiënt uit 🙂
Misschien kan iemand een beter uitleggen hoe mijn code werkt…
Bron: habr.com
