QEMU.js: teraz poważnie i z WASM

Kiedyś zdecydowałem się na zabawę wykazać odwracalność procesu i dowiedz się, jak wygenerować JavaScript (a dokładniej Asm.js) z kodu maszynowego. Do eksperymentu wybrano QEMU, a jakiś czas później powstał artykuł na temat Habr. W komentarzach doradzono mi, abym przerobił projekt w WebAssembly, a nawet zrezygnował prawie skończony Jakoś nie chciałem tego projektu... Prace trwały, ale bardzo powoli i teraz, niedawno, pojawił się ten artykuł komentarz na temat „Jak to się wszystko skończyło?” W odpowiedzi na moją szczegółową odpowiedź usłyszałem: „To brzmi jak artykuł”. Cóż, jeśli możesz, będzie artykuł. Może komuś się przyda. Z niego czytelnik dowie się kilku faktów na temat projektowania backendów generowania kodu QEMU, a także tego, jak napisać kompilator Just-in-Time dla aplikacji internetowej.

zadania

Ponieważ już nauczyłem się, jak „w jakiś sposób” przenieść QEMU na JavaScript, tym razem postanowiłem zrobić to mądrze i nie powtarzać starych błędów.

Błąd numer jeden: odgałęzienie od zwolnienia punktu

Moim pierwszym błędem było rozwidlenie mojej wersji z wcześniejszej wersji 2.4.1. Wtedy wydało mi się to dobrym pomysłem: jeśli istnieje wydanie punktowe, to prawdopodobnie jest ono stabilniejsze niż proste 2.4, a tym bardziej gałąź master. A ponieważ planowałem dodać sporo własnych błędów, w ogóle nie potrzebowałem cudzych błędów. Pewnie tak to wyszło. Ale rzecz w tym, że QEMU nie stoi w miejscu i w pewnym momencie zapowiedzieli nawet optymalizację wygenerowanego kodu o 10 procent. „No, teraz zamarznę” – pomyślałem i załamałem się. Tutaj musimy zrobić dygresję: ze względu na jednowątkowy charakter QEMU.js oraz fakt, że oryginalne QEMU nie implikuje braku wielowątkowości (czyli możliwości jednoczesnej obsługi kilku niepowiązanych ścieżek kodu i nie tylko „użyj wszystkich jąder”) jest dla niego krytyczne, główne funkcje wątków musiałem „wyłączyć”, aby móc wywoływać z zewnątrz. Stworzyło to pewne naturalne problemy podczas fuzji. Faktem jednak jest, że część zmian dotyczy branży master, z którym próbowałem połączyć mój kod, również zostały wybrane w wydaniu punktowym (a zatem w mojej branży) prawdopodobnie również nie zapewniłyby dodatkowej wygody.

Generalnie stwierdziłem, że nadal jest sens wyrzucić prototyp, rozebrać go na części i zbudować nową wersję od podstaw w oparciu o coś świeższego i teraz od master.

Błąd numer dwa: metodologia TLP

W zasadzie nie jest to błąd, w ogóle, to po prostu cecha tworzenia projektu w warunkach całkowitego niezrozumienia zarówno „gdzie i jak się przenieść?”, jak i w ogóle „czy tam dotrzemy?” W tych warunkach niezdarne programowanie była opcją uzasadnioną, ale oczywiście nie chciałem jej niepotrzebnie powtarzać. Tym razem chciałem zrobić to mądrze: atomowe zatwierdzenia, świadome zmiany w kodzie (a nie „łączenie losowych znaków razem, aż się skompiluje (z ostrzeżeniami)”, jak kiedyś o kimś powiedział Linus Torvalds, według Wikicytatów) itp.

Błąd numer trzy: wejście do wody bez znajomości brodu

Jeszcze się tego całkowicie nie pozbyłem, ale teraz zdecydowałem, że nie idę po najmniejszej linii oporu i zrobię to „dojrzale”, czyli napiszę od zera mój backend TCG, żeby nie żeby później powiedzieć: „Tak, to oczywiście dzieje się powoli, ale nie jestem w stanie wszystkiego kontrolować – tak się pisze TCI…” Co więcej, początkowo wydawało się to oczywistym rozwiązaniem, ponieważ Generuję kod binarny. Jak to mówią: „Zgromadziło się w Gandawieу, ale nie ten”: kod jest oczywiście binarny, ale nie można na niego po prostu przenieść kontroli - należy go jawnie wepchnąć do przeglądarki w celu kompilacji, w wyniku czego powstaje pewien obiekt ze świata JS, który trzeba jeszcze być gdzieś zapisany. Jednak na normalnych architekturach RISC, o ile rozumiem, typową sytuacją jest konieczność jawnego zresetowania pamięci podręcznej instrukcji dla zregenerowanego kodu - jeśli tego nie potrzebujemy, to w każdym razie jest blisko. Ponadto z mojej ostatniej próby dowiedziałem się, że kontrola nie wydaje się być przenoszona na środek bloku translacji, więc tak naprawdę nie potrzebujemy kodu bajtowego interpretowanego z jakiegokolwiek przesunięcia i możemy go po prostu wygenerować z funkcji na TB .

Przyszli i kopnęli

Chociaż zacząłem przepisywać kod w lipcu, magiczny kopniak nastąpił niezauważony: zazwyczaj listy z GitHuba przychodzą jako powiadomienia o odpowiedziach na problemy i żądania ściągnięcia, ale tutaj: nagle wspomnij w wątku Binaryen jako backend qemu w kontekście: „Zrobił coś takiego, może coś powie”. Mówiliśmy o korzystaniu z powiązanej biblioteki Emscripten Binarny aby stworzyć WASM JIT. Cóż, powiedziałem, że masz tam licencję Apache 2.0, a QEMU jako całość jest rozpowszechniane na licencji GPLv2 i nie są one zbyt kompatybilne. Nagle okazało się, że licencja może być naprawić to jakoś (nie wiem: może to zmienić, może podwójne licencje, może coś innego...). To oczywiście mnie uszczęśliwiło, bo już wtedy dokładnie się temu przyglądałem formacie binarnym WebAssembly i było mi jakoś smutno i niezrozumiało. Dostępna była także biblioteka, która pożerała podstawowe bloki z wykresem przejścia, tworzyła kod bajtowy, a nawet uruchamiała go w samym interpreterze, jeśli było to konieczne.

Potem było więcej list na liście mailingowej QEMU, ale tu bardziej chodzi o pytanie: „Po co to w ogóle potrzebne?” I to jest nagle, okazało się, że było to konieczne. Można zeskrobać co najmniej następujące możliwości użycia, jeśli zadziała to mniej więcej szybko:

  • uruchomienie czegoś edukacyjnego bez żadnej instalacji
  • wirtualizacja na iOS, gdzie według plotek jedyną aplikacją, która ma prawo do generowania kodu w locie jest silnik JS (czy to prawda?)
  • demonstracja mini-OS - pojedyncza dyskietka, wbudowany, wszelkiego rodzaju oprogramowanie sprzętowe, itp...

Funkcje wykonawcze przeglądarki

Jak już mówiłem, QEMU jest przywiązany do wielowątkowości, ale przeglądarka jej nie ma. No właśnie, nie... Na początku w ogóle tego nie było, potem pojawili się WebWorkers - o ile rozumiem, to jest wielowątkowość oparta na przekazywaniu wiadomości bez wspólnych zmiennych. Naturalnie stwarza to znaczne problemy podczas przenoszenia istniejącego kodu w oparciu o model pamięci współdzielonej. Następnie pod naciskiem opinii publicznej wprowadzono go także pod nazwą SharedArrayBuffers. Wprowadzano go stopniowo, świętowano jego uruchomienie w różnych przeglądarkach, potem świętowano Nowy Rok, a potem Meltdown... Po czym doszli do wniosku, że zgrubny lub zgrubny pomiar czasu, ale za pomocą współdzielonej pamięci i wątek zwiększający licznik, wszystko jest takie samo będzie to działać dość dokładnie. Dlatego wyłączyliśmy wielowątkowość z pamięcią współdzieloną. Wygląda na to, że później włączyli go ponownie, ale jak wynika z pierwszego eksperymentu, bez niego da się żyć, a jeśli tak, spróbujemy to zrobić bez polegania na wielowątkowości.

Drugą cechą jest niemożność manipulacji na niskim poziomie stosem: nie można po prostu wziąć, zapisać bieżącego kontekstu i przełączyć się na nowy z nowym stosem. Stos wywołań jest zarządzany przez maszynę wirtualną JS. Wydawałoby się, w czym tkwi problem, skoro nadal zdecydowaliśmy się na całkowicie ręczne zarządzanie poprzednimi przepływami? Faktem jest, że blokowe wejścia/wyjścia w QEMU są implementowane poprzez współprogramy i tutaj przydadzą się manipulacje stosem niskiego poziomu. Na szczęście Emscipten zawiera już mechanizm operacji asynchronicznych, nawet dwóch: Asynchronizuj и Tłumacz. Pierwszy z nich działa poprzez znaczne wzdęcia w wygenerowanym kodzie JavaScript i nie jest już obsługiwany. Drugi to obecny „poprawny sposób” i działa poprzez generowanie kodu bajtowego dla natywnego interpretera. Działa oczywiście powoli, ale nie powoduje rozdęcia kodu. To prawda, że ​​wsparcie dla współprogramów dla tego mechanizmu musiało być wniesione niezależnie (istnieły już współprogramy napisane dla Asyncify i była implementacja mniej więcej tego samego API dla Emterpretera, wystarczyło je połączyć).

Na ten moment nie udało mi się jeszcze rozbić kodu na jeden skompilowany w WASM i zinterpretowany za pomocą Emterpretera, więc urządzenia blokowe jeszcze nie działają (patrz w następnej serii, jak to mówią...). Oznacza to, że ostatecznie powinieneś otrzymać coś takiego, jak ta zabawna, warstwowa rzecz:

  • interpretowany blok we/wy. Czy naprawdę spodziewałeś się emulowanego NVMe z natywną wydajnością? 🙂
  • statycznie skompilowany główny kod QEMU (tłumacz, inne emulowane urządzenia itp.)
  • dynamicznie skompilowany kod gościa do WASM

Cechy źródeł QEMU

Jak już zapewne się domyślasz, kod emulujący architekturę gościa i kod generujący instrukcje maszyny hosta są w QEMU oddzielone. W rzeczywistości jest to jeszcze trochę trudniejsze:

  • istnieją architektury gościnne
  • jest akceleratory, mianowicie KVM do wirtualizacji sprzętu w systemie Linux (dla kompatybilnych ze sobą systemów gościa i hosta), TCG do generowania kodu JIT w dowolnym miejscu. Począwszy od QEMU 2.9, pojawiła się obsługa standardu wirtualizacji sprzętu HAXM w systemie Windows (szczegóły)
  • jeśli zamiast wirtualizacji sprzętowej używany jest TCG, wówczas ma on oddzielną obsługę generowania kodu dla każdej architektury hosta, a także dla uniwersalnego interpretera
  • ... i wokół tego wszystkiego - emulowane urządzenia peryferyjne, interfejs użytkownika, migracja, odtwarzanie nagrań itp.

Swoją drogą, czy wiedziałeś: QEMU potrafi emulować nie tylko cały komputer, ale także procesor dla osobnego procesu użytkownika w jądrze hosta, który jest używany np. przez fuzzer AFL do instrumentacji binarnej. Być może ktoś chciałby przenieść ten tryb działania QEMU do JS? 😉

Podobnie jak większość istniejącego od dawna bezpłatnego oprogramowania, QEMU jest budowane poprzez połączenie configure и make. Załóżmy, że zdecydujesz się coś dodać: backend TCG, implementację wątku lub coś innego. Nie spiesz się, aby być szczęśliwym/przerażonym (podkreśl odpowiednie) perspektywą komunikacji z Autoconfem – w rzeczywistości configure QEMU jest najwyraźniej napisany samodzielnie i nie jest z niczego generowany.

WebAssembly

Czym więc jest to coś, co nazywa się WebAssembly (aka WASM)? Jest to zamiennik Asm.js, który nie udaje już prawidłowego kodu JavaScript. Wręcz przeciwnie, jest czysto binarny i zoptymalizowany, a nawet samo wpisanie do niego liczby całkowitej nie jest zbyt proste: dla zwartości jest ona przechowywana w formacie LEB128.

Być może słyszałeś o algorytmie ponownego zapętlenia dla Asm.js - jest to przywrócenie instrukcji sterowania przepływem „wysokiego poziomu” (czyli jeśli-to-else, pętle itp.), dla których zaprojektowano silniki JS, z niskopoziomowy LLVM IR, bliżej kodu maszynowego wykonywanego przez procesor. Naturalnie pośrednia reprezentacja QEMU jest bliższa drugiej. Wydawać by się mogło, że już jest, kod bajtowy, koniec męki... A potem są bloki, if-then-else i pętle!..

Jest to kolejny powód, dla którego Binaryen jest przydatny: może naturalnie akceptować bloki wysokiego poziomu zbliżone do tych, które byłyby przechowywane w WASM. Ale może również generować kod na podstawie wykresu podstawowych bloków i przejść między nimi. Cóż, powiedziałem już, że ukrywa format przechowywania WebAssembly za wygodnym API C/C++.

TCG (generator małego kodu)

TCG był oryginalnie backend dla kompilatora C. Wtedy najwyraźniej nie wytrzymał konkurencji z GCC, ale ostatecznie znalazł swoje miejsce w QEMU jako mechanizm generowania kodu dla platformy hosta. Istnieje również backend TCG, który generuje abstrakcyjny kod bajtowy, który jest natychmiast wykonywany przez interpreter, ale tym razem zdecydowałem się uniknąć jego używania. Jednak fakt, że w QEMU jest już możliwe umożliwienie przejścia do wygenerowanego TB poprzez funkcję tcg_qemu_tb_exec, okazało się dla mnie bardzo przydatne.

Aby dodać nowy backend TCG do QEMU, musisz utworzyć podkatalog tcg/<имя архитектуры> (w tym przypadku, tcg/binaryen) i zawiera dwa pliki: tcg-target.h и tcg-target.inc.c и przepisać to wszystko o configure. Możesz tam umieścić inne pliki, ale jak można się domyślić z nazw tych dwóch, oba zostaną gdzieś uwzględnione: jeden jako zwykły plik nagłówkowy (jest zawarty w tcg/tcg.h, a ten znajduje się już w innych plikach w katalogach tcg, accel i nie tylko), drugi - tylko jako fragment kodu w tcg/tcg.c, ale ma dostęp do swoich funkcji statycznych.

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

plik tcg-target.h zawiera głównie ustawienia w formularzu #define-S:

  • ile rejestrów i jaka szerokość jest na architekturze docelowej (mamy tyle, ile chcemy, tyle, ile chcemy - pytanie bardziej dotyczy tego, co zostanie wygenerowane przez przeglądarkę w bardziej wydajny kod na architekturze „w pełni docelowej” ...)
  • wyrównanie instrukcji hosta: na x86, a nawet w TCI, instrukcje nie są w ogóle wyrównane, ale zamierzam umieścić w buforze kodu nie instrukcje, ale wskaźniki do struktur bibliotek Binaryen, więc powiem: 4 bajty
  • jakie opcjonalne instrukcje może wygenerować backend - włączamy wszystko, co znajdziemy w Binaryen, resztę niech akcelerator sam rozbije na prostsze
  • Jaki jest przybliżony rozmiar pamięci podręcznej TLB żądanej przez backend. Faktem jest, że w QEMU wszystko jest poważne: chociaż istnieją funkcje pomocnicze, które wykonują ładowanie/zapisywanie z uwzględnieniem gościa MMU (kim byśmy teraz byli bez niego?), zapisują swoją pamięć podręczną tłumaczeń w postaci struktury, których przetwarzanie wygodnie jest osadzać bezpośrednio w blokach rozgłoszeniowych. Pytanie brzmi, jakie przesunięcie w tej strukturze jest najskuteczniej przetwarzane przez małą i szybką sekwencję poleceń?
  • tutaj możesz dostosować przeznaczenie jednego lub dwóch zarezerwowanych rejestrów, umożliwić wywoływanie TB poprzez funkcję i opcjonalnie opisać kilka małych inline-działa jak flush_icache_range (ale to nie jest nasz przypadek)

plik tcg-target.inc.c, oczywiście, ma zwykle znacznie większy rozmiar i zawiera kilka obowiązkowych funkcji:

  • inicjalizacja, w tym ograniczenia dotyczące tego, które instrukcje mogą operować na jakich operandach. Rażąco skopiowane przeze mnie z innego backendu
  • funkcja pobierająca jedną wewnętrzną instrukcję kodu bajtowego
  • Możesz także umieścić tutaj funkcje pomocnicze, a także możesz użyć funkcji statycznych tcg/tcg.c

Dla siebie wybrałem następującą strategię: w pierwszych słowach kolejnego bloku tłumaczeniowego zapisałem cztery wskazówki: znak startu (pewna wartość w pobliżu 0xFFFFFFFF, który określa aktualny stan TB), kontekst, wygenerowany moduł i magiczną liczbę do debugowania. Początkowo znak został umieszczony 0xFFFFFFFF - nGdzie n - mała liczba dodatnia i za każdym razem, gdy była wykonywana przez interpreter, zwiększała się o 1. Kiedy została osiągnięta 0xFFFFFFFE, odbyła się kompilacja, moduł został zapisany w tablicy funkcji, zaimportowany do małego „launchera”, do którego poszło wykonanie tcg_qemu_tb_exec, a moduł został usunięty z pamięci QEMU.

Parafrazując klasykę: „Crutch, ile w tym brzmieniu kryje się dla serca progera…”. Jednak gdzieś pamięć przeciekała. Co więcej, była to pamięć zarządzana przez QEMU! Miałem kod, który podczas pisania kolejnej instrukcji (no, czyli wskaźnika) usunął tę, której link był w tym miejscu wcześniej, ale to nie pomogło. Właściwie w najprostszym przypadku QEMU przydziela pamięć przy uruchomieniu i zapisuje tam wygenerowany kod. Kiedy bufor się skończy, kod zostaje wyrzucony i na jego miejsce zaczyna się pisać następny.

Po przestudiowaniu kodu zdałem sobie sprawę, że sztuczka z magiczną liczbą pozwoliła mi uniknąć niepowodzenia przy zniszczeniu sterty poprzez uwolnienie czegoś złego w niezainicjowanym buforze przy pierwszym przebiegu. Ale kto przepisuje bufor, aby później ominąć moją funkcję? Jak radzą twórcy Emscripten, gdy napotkałem problem, przeportowałem powstały kod z powrotem do aplikacji natywnej, ustawiłem na niej Mozilla Record-Replay... Ogólnie rzecz biorąc, ostatecznie zrozumiałem prostą rzecz: dla każdego bloku, A struct TranslationBlock z jego opisem. Zgadnij gdzie... Zgadza się, tuż przed blokiem, w buforze. Zdając sobie z tego sprawę, zdecydowałem się zrezygnować z używania kul (przynajmniej niektórych) i po prostu wyrzuciłem magiczną liczbę, a pozostałe słowa przeniosłem na struct TranslationBlock, tworząc pojedynczo połączoną listę, którą można szybko przeglądać po zresetowaniu pamięci podręcznej tłumaczeń i zwalniając pamięć.

Niektóre kule pozostają: na przykład zaznaczone wskaźniki w buforze kodu - niektóre są po prostu BinaryenExpressionRef, to znaczy patrzą na wyrażenia, które należy liniowo umieścić w wygenerowanym bloku podstawowym, część jest warunkiem przejścia między kulkami, część określa miejsce, w którym należy się udać. Otóż ​​są już przygotowane bloki dla Reloopera, które trzeba połączyć zgodnie z warunkami. Aby je rozróżnić, przyjmuje się założenie, że wszystkie są wyrównane co najmniej o cztery bajty, więc spokojnie można wykorzystać w etykiecie dwa najmniej znaczące bity, trzeba tylko pamiętać o ich usunięciu w razie potrzeby. Nawiasem mówiąc, takie etykiety są już używane w QEMU w celu wskazania powodu wyjścia z pętli TCG.

Korzystanie z Binaryena

Moduły w WebAssembly zawierają funkcje, z których każda zawiera treść, która jest wyrażeniem. Wyrażenia to operacje jedno- i binarne, bloki składające się z list innych wyrażeń, przepływ sterowania itp. Jak już powiedziałem, przepływ sterowania jest tutaj zorganizowany dokładnie jako gałęzie wysokiego poziomu, pętle, wywołania funkcji itp. Argumenty funkcji nie są przekazywane na stosie, ale jawnie, tak jak w JS. Istnieją również zmienne globalne, ale z nich nie korzystałem, więc nie będę o nich mówił.

Funkcje posiadają również zmienne lokalne, numerowane od zera, typu: int32 / int64 / float / double. W tym przypadku pierwszych n zmiennych lokalnych jest argumentami przekazywanymi do funkcji. Należy pamiętać, że chociaż nie wszystko tutaj jest całkowicie niskie pod względem przepływu sterowania, liczby całkowite nadal nie mają atrybutu „ze znakiem/bez znaku”: sposób zachowania liczby zależy od kodu operacji.

Ogólnie rzecz biorąc, Binaryen zapewnia proste C-API: tworzysz moduł, w nim tworzyć wyrażenia - jednoargumentowe, binarne, bloki z innych wyrażeń, przepływ sterowania itp. Następnie tworzysz funkcję, której treścią jest wyrażenie. Jeśli tak jak ja masz wykres przejść niskiego poziomu, komponent relooper ci pomoże. O ile rozumiem, możliwe jest zastosowanie wysokiego poziomu kontroli przepływu wykonania w bloku, o ile nie wykracza on poza granice bloku - czyli możliwe jest wykonanie wewnętrznej szybkiej/wolnej ścieżki ścieżka rozgałęziająca się wewnątrz wbudowanego kodu przetwarzającego pamięć podręczną TLB, ale nie zakłócająca „zewnętrznego” przepływu sterowania. Kiedy zwalniasz relooper, jego bloki zostają zwolnione, kiedy zwalniasz moduł, wyrażenia, funkcje itp. przydzielone do niego znikają arena.

Jeśli jednak chcesz interpretować kod na bieżąco, bez niepotrzebnego tworzenia i usuwania instancji interpretera, sensownym może być umieszczenie tej logiki w pliku C++ i stamtąd bezpośrednio zarządzaj całym API C++ biblioteki, omijając gotowe- zrobiły opakowania.

Aby wygenerować potrzebny kod

// настроить глобальные параметры (можно поменять потом)
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 o czymś zapomniałem, przepraszam, to tylko przedstawienie skali, a szczegóły znajdują się w dokumentacji.

A teraz zaczyna się crack-fex-pex, coś takiego:

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,
          // ...
      }
  );
  // и вот уже у вас есть instance!
}, buf, sz);

Aby w jakiś sposób połączyć światy QEMU i JS, a jednocześnie uzyskać szybki dostęp do skompilowanych funkcji, stworzono tablicę (tabelę funkcji do zaimportowania do launchera) i tam umieszczono wygenerowane funkcje. Aby szybko obliczyć indeks, początkowo użyto indeksu bloku tłumaczenia słów zerowych, ale potem indeks obliczony za pomocą tego wzoru zaczął po prostu mieścić się w polu w struct TranslationBlock.

By the way, próbny (obecnie z mętną licencją) działa dobrze tylko w przeglądarce Firefox. Twórcy Chrome byli jakoś nie jestem gotowy do tego, że ktoś chciałby stworzyć ponad tysiąc instancji modułów WebAssembly, więc po prostu przydzielił każdemu gigabajt wirtualnej przestrzeni adresowej...

To wszystko na teraz. Być może, jeśli ktoś będzie zainteresowany, pojawi się kolejny artykuł. Mianowicie pozostaje przynajmniej tylko sprawić, by urządzenia blokowe działały. Sensowne może być również zapewnienie asynchronicznej kompilacji modułów WebAssembly, jak to jest w zwyczaju w świecie JS, ponieważ nadal istnieje interpreter, który może to wszystko zrobić, dopóki moduł natywny nie będzie gotowy.

Na koniec zagadka: skompilowałeś plik binarny w architekturze 32-bitowej, ale kod w wyniku operacji na pamięci wspina się z Binaryen, gdzieś na stosie lub gdzie indziej w górnych 2 GB 32-bitowej przestrzeni adresowej. Problem polega na tym, że z punktu widzenia Binaryena jest to dostęp do zbyt dużego adresu wynikowego. Jak to obejść?

Na sposób administratora

Nie skończyłem tego testować, ale moją pierwszą myślą było: „A co, jeśli zainstaluję 32-bitowego Linuksa?” Wtedy górna część przestrzeni adresowej zostanie zajęta przez jądro. Pytanie tylko ile będzie zajęte: 1 czy 2 Gb.

Po programiście (opcja dla praktyków)

Wypuśćmy bańkę na górze przestrzeni adresowej. Sam nie rozumiem, dlaczego to działa - tam już musi być stos. Ale „jesteśmy praktykami: u nas wszystko działa, tylko nikt nie wie dlaczego…”

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

...co prawda nie jest kompatybilny z Valgrindem, ale na szczęście sam Valgrind bardzo skutecznie wypycha stamtąd wszystkich :)

Być może ktoś lepiej wyjaśni, jak działa mój kod ...

Źródło: www.habr.com

Dodaj komentarz