QEMU.js: teraz poważnie i z WASM

Dawno temu, dla zabawy postanowiłem udowodnić odwracalność procesu i nauczyć się generować JavaScript (a dokładniej, Asm.js) z kodu maszynowego. Do eksperymentu wybrano QEMU, a jakiś czas później napisałem artykuł na Habrze. W komentarzach zasugerowano mi, bym przerobił projekt na WebAssembly, a samemu rezygnować z prawie zakończonego projektu jakoś nie chciało się... Prace trwały, ale niezwykle powoli, a oto ostatnio w tym artykule pojawił się komentarz temat „I co z tego wynikło?”. Na moją rozwiniętą odpowiedź usłyszałem: „To nadaje się na artykuł”. Cóż, skoro się nadaje, to będzie artykuł. Może przyda się komuś. Czytelnik dowie się z niego kilku faktów o strukturze backendów generacji kodu QEMU, a także jak napisać kompilator Just-in-Time dla aplikacji webowej.

Zadania

Ponieważ „na chybił trafił” już nauczyłem się portować QEMU na JavaScript, tym razem postanowiono działać z głową i nie powtarzać starych błędów.

Błąd numer jeden: odgałęzić się od wydania poprawkowego

Moim pierwszym błędem było odgałęzienie swojej wersji od wersji upstream 2.4.1. Wtedy wydawało mi się to dobrym pomysłem: jeśli istnieje wydanie poprawkowe, to pewnie jest stabilniejsze od zwykłej 2.4, a tym bardziej od gałęzi master. A ponieważ planowałem dodać znaczną ilość swoich błędów, obce zupełnie mi były zbędne. No i chyba tak to wyszło. Ale oto problem: QEMU nie stoi w miejscu, a w pewnym momencie ogłoszono nawet optymalizację generowanego kodu o 10%. „Aha, teraz włączę” pomyślałem i się zawiodłem. Tu trzeba zrobić dygresję: ze względu na jednowątkowy charakter QEMU.js i fakt, że oryginalny QEMU nie przewiduje braku wielowątkowości (to znaczy dla niego krytyczna jest możliwość równoczesnego działania kilku niespowiązanych ścieżek kodu, a nie tylko „użycia wszystkich rdzeni”), główne funkcje wątków musiałem „wykręcić” w celu możliwości wywołania z zewnątrz. To stworzyło pewne naturalne problemy przy scalaniu. Niemniej jednak fakt, że część zmian z gałęzi master, z którą próbowałem scalić swój kod, również był cherry picked w wydaniu poprawkowym (a co za tym idzie, i w mojej gałęzi) raczej nie przydałby wygody.

W sumie, postanowiłem, że prototyp mimo wszystko ma sens wyrzucić, rozebrać na części i zbudować nową wersję od podstaw na bazie czegoś nowszego, a teraz już z master.

Błąd numer dwa: metodologia TLP

W rzeczywistości to nie do końca błąd, to po prostu cecha tworzenia projektu w warunkach całkowitego braku zrozumienia, zarówno „dokąd i jak się poruszać?”, jak i ogólnie „czy w ogóle dotrzemy?”. W takich warunkach, programowanie "na szybko" było uzasadnioną opcją, ale oczywiście zupełnie nie chciało się tego powtarzać bez potrzeby. Tym razem chciałem to zrobić z głową: atomowe commity, świadome zmiany w kodzie (a nie „spajanie losowych znaków, aby to się skompilowało (z ostrzeżeniami)”, jak kiedyś powiedział o kimś Linus Torvalds, jeśli wierzyć Wiki cytatom) itd.

Błąd numer trzy: nie znając wody, wchodzić do niej

Od tego do tej pory jeszcze się nie uwolniłem, ale teraz postanowiłem podążać nie najłatwiejszą drogą i zrobić to „jak dorośli”, tzn. napisać swój backend TCG od podstaw, żeby potem nie mówić: „Tak, to oczywiście wolno, ale nie mogę wszystkiego kontrolować – TCI tak był napisany...”. Ponadto początkowo wydawało się to oczywistym rozwiązaniem, ponieważ generuję kod binarny.Jak to mówią, „Zbudowałem Gentu, ale nie ten”: kod jest wprawdzie binarny, ale zarządzanie nim nie jest takie proste — trzeba go jawnie wcisnąć do przeglądarki na kompilację, otrzymując w wyniku jakiś obiekt ze świata JS, który jeszcze trzeba gdzieś zapisać. Zresztą na normalnych architekturach RISC, o ile rozumiem, typową sytuacją jest konieczność jawnego zresetowania pamięci podręcznej instrukcji dla ponownie generowanego kodu — jeśli to nie jest dokładnie to, czego potrzebujemy, to w każdym razie jest bliskie. Ponadto, z mojej poprzedniej próby nauczyłem się, że zarządzanie nie jest przekazywane do środka bloku translacji, więc bajtkod, interpretowany z dowolnego przesunięcia, w szczególności nie jest nam potrzebny, można po prostu generować funkcję na TB.

Przyszli i kopnęli

Chociaż zacząłem przepisywać kod jeszcze w lipcu, to magiczny kopniak przyszedł niespodziewanie: zwykle wiadomości z GitHub przychodzą jako powiadomienia o odpowiedziach na kwestie i żądania pull, a tutaj, nagle wzmianka w wątku Binaryen jako backend qemu w kontekście, „Zrobił coś podobnego, może coś powie”. Mowa była o wykorzystaniu powiązanej z Emscriptenem biblioteki Binaryen do tworzenia WASM JIT. No i powiedziałem, że macie tam licencję Apache 2.0, a QEMU jako całość jest udostępniane na licencji GPLv2, co nie jest zbyt kompatybilne. Nagle okazało się, że licencję można jakoś poprawić (nie wiem: może zmienić, może podwójne licencjonowanie, może coś innego…). To mnie oczywiście ucieszyło, bo już kilka razy wcześniej przyglądałem się formatowi binarnemu WebAssembly, i byłem jakoś smutny i zdezorientowany. Tutaj była biblioteka, która i podstawowe bloki z grafem przejść wchłonie, i bajtkod wyda, i nawet sama go uruchomi w interpreterze, jeśli będzie to potrzebne.

Potem było jeszcze pismo na liście mailingowej QEMU, ale to już raczej do pytania, „A komu to w ogóle potrzebne?”. A jednak nagle, okazało się, że jest potrzebne. Przynajmniej, można zebrać takie możliwości zastosowania, jeśli będzie to działać w miarę sprawnie:

  • uruchomienie czegoś edukacyjnego całkowicie bez instalacji
  • wirtualizacja na iOS, gdzie podobno jedyną aplikacją, mającą prawo do generacji kodu w locie — to silnik JS (czy to prawda?)
  • demonstracja mini-OS — jednopłytkowe, wbudowane, różne firmware'y itd…

Specyfika środowiska wykonawczego przeglądarki

Jak już mówiłem, QEMU jest związane z wielowątkowością, a w przeglądarce jej nie ma. No, to znaczy jak nie ma… Na początku w ogóle jej nie było, potem pojawiły się WebWorkers — o ile dobrze rozumiem, to wielowątkowość oparta na przesyłaniu wiadomości bez współdzielonych zmiennych. Oczywiście, to stwarza znaczne problemy przy portowaniu istniejącego kodu, opartego na modelu shared memory. Potem pod presją opinii publicznej została wdrożona i ona pod nazwą SharedArrayBuffers. Stopniowo zostały wprowadzone, celebrowano ich uruchomienie w różnych przeglądarkach, potem świętowano nowy rok, a potem Meltdown… Po czym doszli do wniosku, że niezależnie od tego, jak się mierzy czas, z pomocą shared memory i wątku, inkrementującego licznik, i tak dosyć dokładnie to wyjdzie. I tak wyłączono wielowątkowość z wspólną pamięcią. Wydaje się, że potem ją włączano z powrotem, ale, jak stało się jasne z pierwszego eksperymentu, i bez niej życie jest, a skoro tak, to spróbujmy zrobić, nie zakładając na wielowątkowość.

Drugą cechą jest niemożność niskopoziomowych manipulacji ze stosem: nie można po prostu wziąć, zapisać aktualny kontekst i przełączyć się na nowy z nowym stosem. Stos wywołań jest zarządzany przez maszynę wirtualną JS. Wydawałoby się, w czym problem, skoro i tak zdecydowaliśmy się zarządzać wcześniejszymi wątkami całkowicie ręcznie? Chodzi o to, że blokowe wejście-wyjście w QEMU jest realizowane poprzez korutyny, i tutaj przydałyby nam się niskopoziomowe manipulacje stosem. Na szczęście, Emscripten już zawiera mechanizm do operacji asynchronicznych, a nawet dwa: Asyncify i Emterpreter. Pierwszy działa poprzez znaczne powiększenie generowanego kodu JavaScript i nie jest już wspierany. Drugi jest aktualnie "właściwym sposobem" i działa poprzez generację bajtkodu dla własnego interpretera. Działa, rzecz jasna, wolno, ale przynajmniej nie powiększa kodu. Prawda, że wsparcie dla korutyn w tym mechanizmie musiałem dostarczyć samodzielnie (były już korutyny napisane pod Asyncify i była implementacja mniej więcej tego samego API dla Emterpreter, trzeba było je tylko połączyć).

Na chwilę obecną jeszcze nie zdążyłem podzielić kodu na kompilowany do WASM i interpretowany za pomocą Emterpreter, dlatego blokowe urządzenia jeszcze nie działają (zobaczycie w następnych odcinkach, jak to mówią…). To znaczy, w końcu powinno wyjść coś takiego jak zabawne warstwowe coś:

  • interpretowane blokowe wejście-wyjście. No a co, naprawdę oczekiwałeś emulowanego NVMe z natywną wydajnością? 🙂
  • statycznie skompilowany podstawowy kod QEMU (translator, pozostałe emulowane urządzenia itd.)
  • dynamicznie kompilowany do WASM kod gościa

Cechy źródeł QEMU

Jak pewnie już się domyśliliście, kod emulacji architektur gości i kod generowania maszynowych instrukcji hosta w QEMU jest podzielony. W rzeczywistości jest to nawet trochę sprytniejsze:

  • są architektury gości
  • jest akceleratory, a konkretnie KVM dla wirtualizacji sprzętowej w systemie Linux (dla systemów gości i hostów kompatybilnych ze sobą), TCG dla JIT-kodogenereacji wszędzie. Od QEMU 2.9 wprowadzono wsparcie dla standardu wirtualizacji sprzętowej HAXM w systemie Windows (szczegóły)
  • jeśli używamy TCG, a nie wirtualizacji sprzętowej, to ma on osobne wsparcie dla generacji kodu dla każdej architektury hosta, jak również dla uniwersalnego interpretatora
  • … a wokół tego wszystkiego – emulowane peryferia, interfejs użytkownika, migracja, rejestracja-odtwarzanie itd.

Przy okazji, czy wiecie: QEMU może emulować nie tylko cały komputer, ale także procesor dla pojedynczego procesu użytkownika w jądrze hosta, co wykorzystuje na przykład fuzzer AFL do instrumentacji binariów. Może ktoś zechce przenieść ten tryb działania QEMU na JS? 😉

Jak większość istniejących od dawna wolnych programów, QEMU buduje się przez wywołanie configure i make. Załóżmy, że postanowiłeś coś dodać: backend TCG, implementację wątków, coś jeszcze. Nie spiesz się z radością/strachowaniem (wymagane podkreślenie) na perspektywę komunikacji z Autoconf – w rzeczywistości, configure QEMU, zdaje się, ma własny kod i nie jest generowany z niczego.

WebAssembly

Czym zatem jest WebAssembly (znane również jako WASM)? To zamiennik Asm.js, który już nie udaje, że jest poprawnym kodem JavaScript. Przeciwnie, jest to czysto binarne i zoptymalizowane, a nawet zapisanie w nim liczby całkowitej nie jest takie proste: dla kompaktowości jest przechowywane w formacie LEB128.

Możliwe, że słyszałeś o algorytmie relooping dla Asm.js – to odzyskiwanie 'wysokopoziomowych' instrukcji sterujących przepływem wykonań (czyli if-then-else, pętle itd.), pod które są dostosowane silniki JS, z niskopoziomowego LLVM IR, bliższego kodowi maszynowemu, wykonywanemu przez procesor. Naturalnie, pośrednia reprezentacja QEMU jest bliższa drugiemu. Wydawałoby się, oto on, kod bajtowy, koniec męczarni… A tutaj bloki, if-then-else i pętle!..

I to jest jeszcze jeden powód, dla którego Binaryen jest przydatny: naturalnie może przyjmować wysokopoziomowe bloki, bliskie temu, co zostanie zapisane w WASM. Może także generować kod z grafu podstawowych bloków i przejść między nimi. A o tym, co ukrywa za wygodnym interfejsem C/C++ format przechowywania WebAssembly, już wspomniałem.

TCG (Tiny Code Generator)

TCG pierwotnie był backend dla kompilatora C. Potem, jak się okazuje, nie wytrzymał konkurencji z GCC, ale w końcu znalazł swoje miejsce w QEMU jako mechanizm generacji kodu dla platformy hosta. Istnieje także backend TCG, który generuje pewien abstrakcyjny bajtkod, który od razu wykonuje interpreter, ale zdecydowałem się tym razem z niego zrezygnować. Niemniej jednak fakt, że w QEMU już istnieje możliwość przejścia na wygenerowany TB przez funkcję tcg_qemu_tb_exec, okazał się dla mnie bardzo przydatny.

Aby dodać nowy backend TCG do QEMU, musisz utworzyć podkatalog tcg/ (w tym przypadku, tcg/binaryen), a w nim dwa pliki: tcg-target.h i tcg-target.inc.c i należy wpisać wszystko to w configure. Można tam także umieścić inne pliki, ale, jak można się domyślić z nazw tych dwóch, będą one gdzieś dołączane: jeden jako zwykły plik nagłówkowy (dołączany do tcg/tcg.h, a drugi do innych plików w katalogach tcg, accel i nie tylko), drugi — tylko jako fragment kodu w tcg/tcg.c, ale ma dostęp do jego funkcji statycznych.

Decydując, że spędzę zbyt dużo czasu na szczegółowych rozważaniach, jak to działa, po prostu skopiowałem „szkielety” tych dwóch plików z innej realizacji backendu, uczciwie wskazując to w nagłówku licencji.

Plik tcg-target.h zawiera głównie ustawienia w postaci #define-ów:

  • ile rejestrów i jakiej szerokości są na docelowej architekturze (w naszym przypadku — ile chcemy, tyle mamy — kwestia bardziej tego, jaki kod będzie generowany w bardziej efektywny sposób przez przeglądarkę na „zupełnie docelowej” architekturze...)
  • wyrównanie instrukcji hosta: na x86, a także w TCI, instrukcje w ogóle nie są wyrównywane, zamierzam wrzucić do bufora kodu nie instrukcje, lecz wskaźniki na struktury biblioteki Binaryen, dlatego powiem: 4 bajty
  • jakie opcjonalne instrukcje może generować backend — włączamy wszystko, co znajdziemy w Binaryen, resztę niech akcelerator dzieli na prostsze sam.
  • Jaki jest przybliżony rozmiar pamięci podręcznej TLB, który żąda backend. Chodzi o to, że w QEMU wszystko jest na poważnie: chociaż są funkcje pomocnicze realizujące operacje load/store z uwzględnieniem gościnnego MMU (a gdzie teraz bez tego?), to jednak swoją pamięć podręczną translacji przechowują w postaci struktury, której przetwarzanie wygodnie wbudować bezpośrednio w bloki translacji. Pytanie brzmi, które przesunięcie w tej strukturze jest najwydajniej przetwarzane przez małą i szybką sekwencję poleceń.
  • Tutaj można również dostosować przeznaczenie jednego lub dwóch zarezerwowanych rejestrów, włączyć wywołanie TB przez funkcję i opcjonalnie opisać kilka drobnych. inline-funkcje takie jak flush_icache_range (ale to nie nasz przypadek)

Plik tcg-target.inc.c, naturalnie, jest zazwyczaj znacznie większe i zawiera kilka obowiązkowych funkcji:

  • inicjalizacja, wskazująca m.in. ograniczenia dotyczące tego, jakie instrukcje mogą działać na jakich operandach. Zuchwale skopiowane przeze mnie z innego backendu.
  • funkcja przyjmująca jedną instrukcję wewnętrznego bajtkodu.
  • Tutaj można również umieścić funkcje pomocnicze, a także korzystać z funkcji statycznych z tcg/tcg.c

Dla siebie wybrałem następującą strategię: na początku każdego bloku translacji zapisywałem cztery wskaźniki: znacznik początku (jakiejś wartości w okolicy 0xFFFFFFFF, według którego określano bieżący stan TB), kontekst, wygenerowany moduł oraz magiczna liczba do debugowania. Początkowo znacznik był ustawiany na 0xFFFFFFFF - n, gdzie n — niewielką liczbą dodatnią, a przy każdym wykonaniu przez interpreter zwiększał się o 1. Kiedy osiągał 0xFFFFFFFE, następowała kompilacja, moduł był zapisywany w tabeli funkcji, zaimportowanej do małego "uruchamiacza", do którego trwało wykonywanie z tcg_qemu_tb_exec, a moduł był usuwany z pamięci QEMU.

Przeredagowując klasykę, „Kołek, jak wiele w tym dźwięku dla serca programisty się splata…”. Niemniej jednak pamięć gdzieś umykała. I to była pamięć zarządzana przez QEMU! Miałem kod, który przy zapisie kolejnej instrukcji (no, to znaczy, wskaźnika) usuwał tę, do której odniesienie było wcześniej w tym miejscu, ale to nie pomagało. W rzeczywistości w najprostszym przypadku QEMU alokuje pamięć przy uruchomieniu i zapisuje tam generowany kod. Kiedy bufor się kończy, kod jest usuwany, a w jego miejsce zaczyna się zapisywać następny.

Po przeanalizowaniu kodu, zrozumiałem, że obejście z magic number zapobiegało awarii podczas zwolnienia pamięci, uwalniając coś nieprawidłowego w niezinicjalizowanym buforze podczas pierwszego przejścia. Ale kto potem zapisuje bufor omijając moją funkcję? Jak sugerują deweloperzy Emscripten, gdy napotkałem problem, przekonwertowałem uzyskany kod z powrotem na aplikację natywną i użyłem Mozilla Record-Replay… W każdym razie, w końcu zrozumiałem prostą rzecz: dla każdego bloku przydzielana jest struktura TranslationBlock z jego opisem. Zgadnijcie, gdzie… Tak jest, bezpośrednio przed blokiem w buforze. Uświadomiwszy sobie to, postanowiłem zakończyć z obejściami (choćby niektórymi) i po prostu usunąłem magic number, a pozostałe słowa przeniosłem do struktura TranslationBlock, tworząc listę jednokierunkową, po której można szybko przejść podczas resetowania pamięci podręcznej translacji i zwolnić pamięć.

Niektóre obejścia pozostały: na przykład, oznaczone wskaźniki w buforze kodu — część z nich to po prostu BinaryenExpressionRef, co oznacza, że odnoszą się do wyrażeń, które należy w liniowy sposób umieścić w generowanym bloku bazowym, część — to warunki przejścia między BB, część — miejsca, do których należy przechodzić. Już przygotowane bloki dla Reloopera muszą być połączone według warunków. Aby je odróżnić, przyjmuje się, że wszystkie są wyrównane przynajmniej do czterech bajtów, więc spokojnie można używać dwóch najmłodszych bitów pod etykietę, trzeba tylko pamiętać o jej usunięciu w razie potrzeby. Tak przy okazji, takie etykiety są już używane w QEMU do oznaczania przyczyny wyjścia z pętli TCG.

Użycie Binaryen

Moduły w WebAssembly zawierają funkcje, z których każda ma ciała reprezentujące wyrażenie. Wyrażenia to operacje unarne i binarne, bloki składające się z list innych wyrażeń, kontrola przepływu itp. Jak już mówiłem, kontrola przepływu jest organizowana tutaj jako wysokopoziomowe rozgałęzienia, pętle, wywołania funkcji itd. Argumenty funkcjom przekazywane są jawnie, a nie na stosie, tak jak w JS. Są też zmienne globalne, ale ich nie używałem, więc o nich nie opowiem.

Funkcje mają również numerowane od zera lokalne zmienne, które są typów: int32 / int64 / float / double. Przy czym pierwsze n lokalnych zmiennych to argumenty przekazane do funkcji. Należy zauważyć, że chociaż nie jest to do końca niski poziom w kontekście przepływu sterowania, całkowite liczby nie zawierają w sobie oznaczenia „znakowe/bez znaku”: jak liczba się zachowa, zależy od kodu operacji.

Ogólnie rzecz biorąc, Binaryen oferuje proste API C: tworzysz moduł, w nim tworzysz wyrażenia — unarne, binarne, bloki z innych wyrażeń, kontrola przepływu itd. Następnie tworzysz funkcję, której ciało musi zawierać wyrażenie. Jeśli masz, tak jak ja, niski poziom grafu przejść — komponent relooper może być pomocny. Z tego co rozumiem, można stosować wysokopoziomowe zarządzanie przepływem wykonania w bloku, dopóki nie wychodzi ono poza jego granice — to znaczy można zrobić wewnętrzne rozgałęzienie fast path / slow path wbudowanego kodu obsługi TLB cache, ale nie można ingerować w „zewnętrzny” przepływ sterowania. Gdy zwalniasz relooper, zwalniają się jego bloki, gdy zwalniasz moduł — znikają wyrażenia, funkcje itd., przydzielone w jego arenie..

Jednak jeśli chcesz interpretować kod na bieżąco bez zbędnego tworzenia i usuwania instancji interpretera, może warto wynieść tę logikę do pliku C++ i tam bezpośrednio zarządzać całym API C++ biblioteki, omijając gotowe opakowania.

W związku z tym, aby wygenerować kod, musisz

// настроить глобальные параметры (можно поменять потом)
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);

… jeśli coś pominąłem — przepraszam, to tylko aby zobrazować rozmiary, a szczegóły — są w dokumentacji.

A teraz zaczyna się kraks-feks-peks, mniej więcej taki:

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,
          // ...
      }
  );
  // i już masz instancję!
}, buf, sz);

Aby jakoś połączyć świat QEMU z JS i szybko uzyskiwać dostęp do skompilowanych funkcji, stworzono tablicę (tabelę funkcji do importu w launcherze), do której trafiły generowane funkcje. Aby szybko obliczać indeks, początkowo używano indeksu zerowego słowa bloku tłumaczenia, ale później obliczony indeks według takiej formuły po prostu wpisywano w pole w struktura TranslationBlock.

Przy okazji, demo (jak dotąd z niejasną licencją) działa poprawnie tylko w Firefoxie. Programiści Chrome byli trochę nieprzygotowani na to, że ktoś będzie chciał stworzyć ponad tysiąc instancji modułów WebAssembly, dlatego po prostu przydzielali po gigabajcie wirtualnej przestrzeni adresowej na każdy…

Na razie to wszystko. Może będzie jeszcze jeden artykuł, jeśli kogoś to interesuje. A konkretnie, zostało jeszcze co najmniej zaledwie sprawić, by działały urządzenia blokowe. Może warto również uczynić kompilację modułów WebAssembly asynchroniczną, jak to jest w świecie JS, skoro i tak mamy interpreter, który może to wszystko wykonywać, dopóki natywny moduł nie jest gotowy.

Na koniec zagadka: skompilowałeś binarkę na architekturze 32-bitowej, ale kod przez operacje z pamięcią wychodzi gdzieś z Binaryen na stos lub gdzie indziej w górne 2 GB 32-bitowej przestrzeni adresowej. Problem w tym, że z perspektywy Binaryen to odwołanie jest zbyt dużym rezultatem adresu. Jak to obejść?

Po administracyjnie

Ostatecznie tego nie testowałem, ale pierwsza myśl była: „A co, jeśli zainstalować 32-bitowego Linuksa?” Wtedy górna część przestrzeni adresowej będzie zajęta przez jądro. Tylko pytanie, ile będzie zajęte: 1 czy 2 GB.

Po programistycznie (wersja dla praktyków)

Napompujemy bańkę w górnej części przestrzeni adresowej. Sam nie rozumiem, dlaczego to działa — tam powinien być stos. Ale „my praktycy: u nas wszystko działa, ale nikt nie wie dlaczego…”. już … z Valgrindem, prawda, niekompatybilne, ale na szczęście Valgrind sam bardzo efektywnie wszystkich stamtąd wypycha 🙂

// 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));
}

Może ktoś da lepsze wyjaśnienie, jak działa ten mój kod…

Dawno temu dla zabawy postanowiłem udowodnić odwrotność procesu i nauczyć się generować JavaScript (a dokładniej, Asm.js) z kodu maszynowego.

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster