Qemu.js z obsługą JIT: nadal możesz odwrócić mielone mięso do tyłu

Kilka lat temu Fabrice Bellard napisany przez jslinux to emulator komputera PC napisany w JavaScript. Potem było co najmniej więcej Wirtualny x86. Ale wszystkie z nich, o ile wiem, były interpreterami, podczas gdy Qemu, napisany znacznie wcześniej przez tego samego Fabrice'a Bellarda i prawdopodobnie każdy szanujący się nowoczesny emulator, wykorzystuje kompilację JIT kodu gościa do kodu systemu hosta. Wydawało mi się, że przyszedł czas na realizację zadania odwrotnego w stosunku do tego, które rozwiązują przeglądarki: kompilacja JIT kodu maszynowego do JavaScript, dla którego najbardziej logiczne wydawało się przeniesienie Qemu. Wydawałoby się, dlaczego Qemu, istnieją prostsze i przyjazne dla użytkownika emulatory - na przykład ten sam VirtualBox - zainstalowane i działa. Ale Qemu ma kilka interesujących funkcji

  • otwarte źródło
  • możliwość pracy bez sterownika jądra
  • możliwość pracy w trybie tłumacza
  • obsługa dużej liczby architektur hosta i gościa

Odnosząc się do trzeciego punktu, mogę już wyjaśnić, że tak naprawdę w trybie TCI interpretowane są nie same instrukcje maszyny-gościa, ale uzyskany z nich kod bajtowy, ale nie zmienia to istoty - aby zbudować i uruchomić Qemu na nowej architekturze, jeśli masz szczęście, wystarczy kompilator AC - pisanie generatora kodu można odłożyć w czasie.

A teraz, po dwóch latach spokojnego majsterkowania w wolnym czasie przy kodzie źródłowym Qemu, pojawił się działający prototyp, w którym można już uruchomić np. Kolibri OS.

Co to jest Emscripten

Obecnie pojawiło się wiele kompilatorów, których efektem końcowym jest JavaScript. Niektóre, jak Type Script, pierwotnie miały być najlepszym sposobem pisania dla Internetu. Jednocześnie Emscripten umożliwia pobranie istniejącego kodu C lub C++ i skompilowanie go w formie czytelnej dla przeglądarki. NA aktualizacja Zebraliśmy wiele portów znanych programów: tutajMożesz na przykład spojrzeć na PyPy - nawiasem mówiąc, twierdzą, że mają już JIT. Tak naprawdę nie każdy program można po prostu skompilować i uruchomić w przeglądarce – jest ich wiele cechy, z czym jednak trzeba się pogodzić, jak głosi napis na tej samej stronie: „Emscripten można wykorzystać do skompilowania niemal każdego przenośny Kod C/C++ do JavaScript”. Oznacza to, że istnieje szereg operacji, których zachowanie zgodnie ze standardem jest niezdefiniowane, ale zwykle działają na platformie x86 - na przykład niewyrównany dostęp do zmiennych, co jest ogólnie zabronione w niektórych architekturach. Ogólnie , Qemu jest programem wieloplatformowym i, chciałem w to wierzyć, i że nie zawiera już wielu niezdefiniowanych zachowań - weź to i skompiluj, a potem majstruj trochę przy JIT - i gotowe! Ale to nie jest to sprawa...

Pierwsza próba

Ogólnie rzecz biorąc, nie jestem pierwszą osobą, która wpadła na pomysł przeniesienia Qemu na JavaScript. Na forum ReactOS zadano pytanie, czy jest to możliwe przy użyciu Emscripten. Jeszcze wcześniej krążyły plotki, że Fabrice Bellard zrobił to osobiście, ale my rozmawialiśmy o jslinux, który z tego co wiem jest jedynie próbą ręcznego osiągnięcia wystarczającej wydajności w JS i został napisany od podstaw. Później napisano Virtual x86 - zamieszczono dla niego niezakłócone źródła i, jak stwierdzono, większy „realizm” emulacji umożliwił wykorzystanie SeaBIOS jako oprogramowania sprzętowego. Ponadto była co najmniej jedna próba przeniesienia Qemu przy użyciu Emscripten - próbowałem to zrobić para gniazd, ale rozwój, o ile rozumiem, został zamrożony.

Wygląda na to, że oto źródła, oto Emscripten - weź je i skompiluj. Ale są też biblioteki, od których zależy Qemu, i biblioteki, od których te biblioteki zależą itd., a jedna z nich jest libffi, od którego zależy glib. W internecie krążyły plotki, że znalazła się taka w dużym zbiorze portów bibliotek dla Emscripten, ale jakoś trudno było w to uwierzyć: po pierwsze nie miał to być nowy kompilator, po drugie był to kompilator zbyt niskiego poziomu bibliotekę, którą można po prostu pobrać i skompilować do JS. I nie jest to tylko kwestia wstawek asemblera - prawdopodobnie, jeśli to przekręcisz, dla niektórych konwencji wywoływania możesz wygenerować niezbędne argumenty na stosie i wywołać funkcję bez nich. Ale Emscripten to skomplikowana sprawa: aby wygenerowany kod wyglądał znajomo dla optymalizatora silnika JS przeglądarki, stosuje się pewne sztuczki. W szczególności tzw. relooping – generator kodu wykorzystujący odebrany LLVM IR z pewnymi abstrakcyjnymi instrukcjami przejścia próbuje odtworzyć wiarygodne if, pętle itp. W jaki sposób argumenty są przekazywane do funkcji? Naturalnie jako argumenty funkcji JS, czyli jeśli to możliwe, nie przez stos.

Na początku był pomysł, żeby po prostu napisać zamiennik libffi za pomocą JS i uruchomić standardowe testy, ale ostatecznie pogubiłem się, jak zrobić moje pliki nagłówkowe, aby działały z istniejącym kodem - co mogę zrobić, jak mówią: „Czy zadania są aż tak skomplikowane?” „Czy jesteśmy aż tak głupi?” Musiałem, że tak powiem, przenieść libffi na inną architekturę - na szczęście Emscripten ma zarówno makra do montażu inline (w JavaScript, tak - cóż, niezależnie od architektury, więc asemblera), jak i możliwość uruchamiania kodu wygenerowanego w locie. Ogólnie rzecz biorąc, po pewnym czasie majstrowania przy fragmentach libffi zależnych od platformy, udało mi się uzyskać kod do skompilowania i uruchomiłem go w pierwszym teście, na jaki się natknąłem. Ku mojemu zdziwieniu test zakończył się pomyślnie. Oszołomiony moim geniuszem – to nie żart, zadziałało od pierwszego uruchomienia – wciąż nie wierząc własnym oczom, poszedłem jeszcze raz spojrzeć na powstały kod, aby ocenić, gdzie dalej kopać. Tutaj po raz drugi oszalałem - jedyne, co zrobiła moja funkcja, to ffi_call - to raport o udanym połączeniu. Nie było żadnego wezwania. Wysłałem więc moją pierwszą prośbę o ściągnięcie, która poprawiła błąd w teście, który jest jasny dla każdego ucznia Olimpiady – liczb rzeczywistych nie należy porównywać z a == b a nawet jak a - b < EPS - trzeba też pamiętać o module, inaczej 0 okaże się bardzo równe 1/3... Ogólnie rzecz biorąc, wymyśliłem pewien port libffi, który przechodzi najprostsze testy i z którym glib jest skompilowany - zdecydowałem, że będzie to konieczne, dodam to później. Patrząc w przyszłość, powiem, że jak się okazało, kompilator nawet nie uwzględnił funkcji libffi w ostatecznym kodzie.

Ale tak jak już mówiłem, istnieją pewne ograniczenia, a wśród swobodnego korzystania z różnych niezdefiniowanych zachowań ukryta została bardziej nieprzyjemna funkcja - JavaScript z założenia nie obsługuje wielowątkowości z pamięcią współdzieloną. W zasadzie można to nawet nazwać dobrym pomysłem, ale nie w przypadku przenoszenia kodu, którego architektura jest powiązana z wątkami C. Ogólnie rzecz biorąc, Firefox eksperymentuje ze obsługą współdzielonych pracowników, a Emscripten ma dla nich implementację pthread, ale nie chciałem na tym polegać. Musiałem powoli wykorzenić wielowątkowość z kodu Qemu - czyli dowiedzieć się, gdzie biegną wątki, przenieść ciało pętli działającej w tym wątku do osobnej funkcji i wywoływać takie funkcje po kolei z głównej pętli.

Druga próba

W pewnym momencie stało się jasne, że problem nadal istnieje i przypadkowe przesuwanie kul wokół kodu nie prowadzi do niczego dobrego. Wniosek: musimy w jakiś sposób usystematyzować proces dodawania kul. Dlatego została wzięta świeża wówczas wersja 2.4.1 (a nie 2.5.0, bo kto wie, w nowej wersji będą błędy, których jeszcze nie wyłapałem, a własnych błędów mam dość) ), a pierwszą rzeczą było bezpieczne przepisanie go thread-posix.c. No cóż, w miarę bezpiecznie: jeśli ktoś próbował wykonać operację prowadzącą do zablokowania, funkcja była natychmiast wywoływana abort() - oczywiście nie rozwiązało to wszystkich problemów od razu, ale przynajmniej było w jakiś sposób przyjemniejsze niż ciche otrzymywanie niespójnych danych.

Ogólnie opcje Emscripten są bardzo pomocne w przenoszeniu kodu do JS -s ASSERTIONS=1 -s SAFE_HEAP=1 - wychwytują niektóre typy niezdefiniowanych zachowań, takie jak wywołania niewyrównanego adresu (co nie jest wcale spójne z kodem tablic z typem, np. HEAP32[addr >> 2] = 1) lub wywołanie funkcji z niewłaściwą liczbą argumentów.

Nawiasem mówiąc, błędy w wyrównaniu to osobna kwestia. Jak już powiedziałem, Qemu ma „zdegenerowany” backend interpretacyjny do generowania kodu TCI (mały interpreter kodu) oraz do budowania i uruchamiania Qemu na nowej architekturze, jeśli masz szczęście, wystarczy kompilator C. "Jeśli masz szczęście". Miałem pecha i okazało się, że TCI wykorzystuje dostęp niewyrównany podczas analizowania kodu bajtowego. Oznacza to, że na wszystkich rodzajach architektur ARM i innych z koniecznie wyrównanym dostępem, Qemu kompiluje, ponieważ ma normalny backend TCG, który generuje natywny kod, ale to, czy TCI będzie na nich działać, to inna kwestia. Jak się jednak okazało, dokumentacja TCI wyraźnie wskazywała na coś podobnego. W efekcie do kodu dodano wywołania funkcji niewyrównanego odczytu, które odkryto w innej części Qemu.

Zniszczenie sterty

W rezultacie poprawiono nierówny dostęp do TCI, powstała pętla główna, która z kolei nazywała się procesorem, RCU i kilkoma innymi drobiazgami. I tak uruchamiam Qemu z opcją -d exec,in_asm,out_asm, co oznacza, że ​​trzeba powiedzieć, które bloki kodu są wykonywane, a także w momencie transmisji napisać, jaki był kod gościa, jaki stał się kod hosta (w tym przypadku kod bajtowy). Uruchamia się, wykonuje kilka bloków tłumaczeń, zapisuje komunikat debugowania, który zostawiłem, że RCU się teraz uruchomi i... zawiesza się abort() wewnątrz funkcji free(). Majsterkując przy funkcji free() Udało nam się dowiedzieć, że w nagłówku bloku sterty, który znajduje się w ośmiu bajtach poprzedzających przydzieloną pamięć, zamiast rozmiaru bloku lub czegoś podobnego, znajdowały się śmieci.

Zniszczenie sterty - jakie urocze... W takim przypadku istnieje przydatne rozwiązanie - z (jeśli to możliwe) tych samych źródeł zmontuj natywny plik binarny i uruchom go pod Valgrindem. Po pewnym czasie plik binarny był gotowy. Uruchamiam go z tymi samymi opcjami - zawiesza się nawet podczas inicjalizacji, przed faktycznym osiągnięciem wykonania. Jest to oczywiście nieprzyjemne - najwyraźniej źródła nie były dokładnie takie same, co nie jest zaskakujące, ponieważ konfiguracja wypatrzyła nieco inne opcje, ale mam Valgrinda - najpierw naprawię ten błąd, a potem, jeśli mi się poszczęści , pojawi się oryginał. Używam tego samego pod Valgrindem... Y-y-y, y-y-y, uh-uh, zaczęło się, normalnie przeszło inicjalizację i przeszło obok pierwotnego błędu bez ani jednego ostrzeżenia o nieprawidłowym dostępie do pamięci, nie mówiąc już o upadkach. Życie, jak mówią, nie przygotowało mnie na to - awaria programu przestaje się zawieszać po uruchomieniu pod Walgrindem. Co to było, pozostaje tajemnicą. Moja hipoteza jest taka, że ​​gdy znalazłem się w pobliżu bieżącej instrukcji po awarii podczas inicjalizacji, gdb pokazało pracę memset-a z prawidłowym wskaźnikiem, używając jednego z nich mmx, lub xmm rejestrów, to być może był to jakiś błąd w zestrojeniu, chociaż wciąż trudno w to uwierzyć.

OK, Valgrind tu nie pomaga. I tu zaczęła się najbardziej obrzydliwa rzecz - wszystko wydaje się nawet zaczynać, ale ulega awarii z zupełnie nieznanych powodów w wyniku zdarzenia, które mogło nastąpić miliony instrukcji temu. Przez długi czas nie było nawet jasne, jak podejść. W końcu nadal musiałem usiąść i debugować. Wydrukowanie tego, czym został przepisany nagłówek, pokazało, że nie wyglądał on jak liczba, ale raczej coś w rodzaju danych binarnych. I oto ten ciąg binarny został znaleziony w pliku BIOS-u - to znaczy teraz można było z rozsądną pewnością stwierdzić, że było to przepełnienie bufora, a nawet jest jasne, że został zapisany w tym buforze. No to coś w tym stylu - w Emscripten na szczęście nie ma randomizacji przestrzeni adresowej, dziur też w niej nie ma, więc można napisać gdzieś w środku kodu, żeby po wskaźniku wyprowadzić dane z ostatniego uruchomienia, spójrz na dane, spójrz na wskaźnik i, jeśli się nie zmienił, daj do myślenia. To prawda, że ​​połączenie po każdej zmianie zajmuje kilka minut, ale co możesz zrobić? W rezultacie znaleziono konkretną linię, która skopiowała BIOS z bufora tymczasowego do pamięci gościa - i rzeczywiście w buforze nie było wystarczającej ilości miejsca. Znalezienie źródła tego dziwnego adresu bufora spowodowało powstanie funkcji qemu_anon_ram_alloc w pliku oslib-posix.c - logika była taka: czasami przydatne może być wyrównanie adresu do ogromnej strony o rozmiarze 2 MB, w tym celu poprosimy mmap najpierw trochę więcej, a potem zwrócimy nadmiar z pomocą munmap. A jeśli takie wyrównanie nie jest wymagane, zamiast 2 MB wskażemy wynik getpagesize() - mmap nadal będzie podawać wyrównany adres... Więc w Emscripten mmap po prostu dzwoni malloc, ale oczywiście nie jest wyrównany na stronie. Ogólnie rzecz biorąc, błąd, który frustrował mnie przez kilka miesięcy, został naprawiony poprzez zmianę двух linie.

Funkcje wywoływania funkcji

A teraz procesor coś liczy, Qemu nie ulega awarii, ale ekran się nie włącza, a procesor szybko wpada w pętle, sądząc po wyjściu -d exec,in_asm,out_asm. Pojawiła się hipoteza: przerwania czasowe (lub ogólnie wszystkie przerwania) nie docierają. I rzeczywiście, jeśli odkręcisz przerwania od natywnego zestawu, co z jakiegoś powodu zadziałało, otrzymasz podobny obraz. Ale to wcale nie była odpowiedź: porównanie śladów wydanych w ramach powyższej opcji wykazało, że trajektorie wykonania rozeszły się bardzo wcześnie. Tutaj trzeba powiedzieć, że porównanie tego, co zostało nagrane za pomocą programu uruchamiającego emrun debugowanie danych wyjściowych za pomocą danych wyjściowych zestawu natywnego nie jest procesem całkowicie mechanicznym. Nie wiem dokładnie, w jaki sposób łączy się program działający w przeglądarce emrun, ale okazuje się, że niektóre linie na wyjściu są przestawione, więc różnica w różnicy nie jest jeszcze powodem do założenia, że ​​trajektorie się rozeszły. Ogólnie rzecz biorąc, stało się jasne, że zgodnie z instrukcją ljmpl następuje przejście do różnych adresów, a wygenerowany kod bajtowy jest zasadniczo inny: jeden zawiera instrukcję wywołania funkcji pomocniczej, drugi nie. Po przejrzeniu instrukcji w Google i przestudiowaniu kodu tłumaczącego te instrukcje stało się jasne, że po pierwsze bezpośrednio przed nią w rejestrze cr0 dokonano nagrania - również przy użyciu pomocnika - które przełączyło procesor w tryb chroniony, a po drugie, że wersja js nigdy nie przełączyła się w tryb chroniony. Ale faktem jest, że inną cechą Emscripten jest niechęć do tolerowania kodu takiego jak implementacja instrukcji call w TCI, którego dowolny wskaźnik funkcji daje typ long long f(int arg0, .. int arg9) - funkcje muszą być wywoływane z odpowiednią liczbą argumentów. Jeśli ta zasada zostanie naruszona, w zależności od ustawień debugowania, program albo ulegnie awarii (co jest dobre), albo w ogóle wywoła niewłaściwą funkcję (co będzie smutne przy debugowaniu). Jest też trzecia opcja - włącz generowanie wrapperów, które dodają/usuwają argumenty, ale w sumie te wrappery zajmują sporo miejsca, mimo że tak naprawdę potrzebuję tylko trochę ponad stu wrapperów. Już samo to jest bardzo smutne, ale okazało się, że jest poważniejszy problem: w wygenerowanym kodzie funkcji wrapperowych argumenty były konwertowane i konwertowane, ale czasami nie wywoływano funkcji z wygenerowanymi argumentami - cóż, tak jak w moja implementacja libffi. Oznacza to, że niektórzy pomocnicy po prostu nie zostali straceni.

Na szczęście Qemu ma czytelne maszynowo listy pomocników w formie pliku nagłówkowego, np

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

Używa się ich dość zabawnie: po pierwsze, makra są redefiniowane w najdziwniejszy sposób DEF_HELPER_n, a następnie włącza się helper.h. O ile makro jest rozwinięte do inicjatora struktury i przecinka, a następnie zdefiniowana jest tablica, a zamiast elementów - #include <helper.h> Dzięki temu wreszcie miałem okazję wypróbować bibliotekę w pracy piparsowaniei napisano skrypt, który generuje dokładnie te opakowania dla dokładnie tych funkcji, do których są potrzebne.

I tak po tym procesor zdawał się działać. Wydaje się, że dzieje się tak dlatego, że ekran nigdy nie został zainicjowany, chociaż memtest86+ mógł działać w natywnym zestawie. W tym miejscu konieczne jest wyjaśnienie, że kod we/wy bloku Qemu jest zapisany we współprogramach. Emscripten ma swoją własną, bardzo trudną implementację, ale nadal musiał być obsługiwany w kodzie Qemu i możesz teraz debugować procesor: Qemu obsługuje opcje -kernel, -initrd, -append, za pomocą którego możesz uruchomić Linuksa lub na przykład memtest86+, w ogóle bez użycia urządzeń blokowych. Ale tutaj jest problem: w natywnym zestawie można było zobaczyć wyjście jądra Linuksa na konsolę z opcją -nographici brak danych wyjściowych z przeglądarki do terminala, z którego została uruchomiona emrun, nie przyszedł. Oznacza to, że nie jest jasne: procesor nie działa lub wyjście graficzne nie działa. I wtedy przyszło mi do głowy, żeby trochę poczekać. Okazało się, że „procesor nie śpi, ale po prostu powoli mruga” i po około pięciu minutach jądro wyrzuciło na konsolę kilka komunikatów i nadal się zawieszało. Stało się jasne, że procesor ogólnie działa i musimy zagłębić się w kod do pracy z SDL2. Niestety nie wiem jak korzystać z tej biblioteki, więc w niektórych miejscach musiałem działać losowo. W pewnym momencie na ekranie na niebieskim tle rozbłysła linia równoległa0, co podsunęło pewne przemyślenia. Ostatecznie okazało się, że problem polegał na tym, że Qemu otwiera kilka wirtualnych okien w jednym fizycznym oknie, pomiędzy którymi można przełączać się za pomocą Ctrl-Alt-n: działa to w natywnej kompilacji, ale nie w Emscripten. Po pozbyciu się niepotrzebnych okien za pomocą opcji -monitor none -parallel none -serial none i instrukcje, aby na siłę przerysować cały ekran w każdej klatce, wszystko nagle zadziałało.

Współprogramy

Zatem emulacja w przeglądarce działa, ale nie da się w niej uruchomić nic ciekawego na pojedynczej dyskietce, bo nie ma blokowych wejść/wyjść - trzeba zaimplementować obsługę współprogramów. Qemu ma już kilka backendów współprogramowych, ale ze względu na naturę JavaScript i generator kodu Emscripten nie można po prostu zacząć żonglować stosami. Wydawać by się mogło, że „wszystko zniknęło, tynk jest usuwany”, jednak deweloperzy Emscripten już o wszystko zadbali. Zaimplementowano to dość zabawnie: nazwijmy takie wywołanie funkcji podejrzanym emscripten_sleep i kilka innych korzystających z mechanizmu Asyncify, a także wywołania wskaźników i wywołania dowolnej funkcji, w której jeden z dwóch poprzednich przypadków może wystąpić niżej na stosie. I teraz przed każdym podejrzanym wywołaniem wybierzemy kontekst asynchroniczny, a zaraz po wywołaniu sprawdzimy, czy doszło do wywołania asynchronicznego, a jeśli tak, to zapiszemy wszystkie zmienne lokalne w tym kontekście asynchronicznym, wskażemy jaka funkcja aby przekazać kontrolę, kiedy musimy kontynuować wykonywanie i wyjść z bieżącej funkcji. W tym miejscu istnieje możliwość zbadania efektu trwonienie — na potrzeby kontynuacji wykonywania kodu po powrocie z wywołania asynchronicznego kompilator generuje „odcinki” funkcji rozpoczynającej się po podejrzanym wywołaniu — na przykład: jeśli będzie n podejrzanych wywołań, to funkcja zostanie rozwinięta gdzieś n/2 razy — to nadal jest, jeśli nie. Należy pamiętać, że po każdym potencjalnie asynchronicznym wywołaniu trzeba dodać zapisanie niektórych zmiennych lokalnych do oryginalnej funkcji. Później musiałem nawet napisać prosty skrypt w Pythonie, który w oparciu o dany zestaw szczególnie nadużywanych funkcji, które rzekomo „nie pozwalają na przejście asynchronii przez siebie” (czyli promocja stosu i wszystko, co właśnie opisałem, nie w nich pracować), wskazuje wywołania poprzez wskaźniki, w których funkcje powinny być ignorowane przez kompilator, aby funkcje te nie były uważane za asynchroniczne. A wtedy pliki JS poniżej 60 MB to zdecydowanie za dużo - powiedzmy co najmniej 30. Chociaż kiedyś konfigurowałem skrypt asemblera i przez przypadek wyrzuciłem opcje linkera, wśród których była -O3. Uruchamiam wygenerowany kod, a Chromium zużywa pamięć i ulega awarii. Potem przypadkowo spojrzałem na to, co próbował pobrać... Cóż, co mogę powiedzieć, też bym zamarł, gdyby poproszono mnie o dokładne przestudiowanie i zoptymalizowanie ponad 500-MB Javascriptu.

Niestety, kontrole w kodzie biblioteki pomocniczej Asyncify nie były do ​​końca przyjazne longjmp-s, które są używane w kodzie procesora wirtualnego, ale po małej poprawce, która wyłącza te kontrole i na siłę przywraca konteksty, jakby wszystko było w porządku, kod zadziałał. I wtedy zaczęła się dziwna rzecz: czasami uruchamiały się sprawdzenia kodu synchronizacji - te same, które powodują awarię kodu, jeśli zgodnie z logiką wykonania powinien on zostać zablokowany - ktoś próbował przechwycić już przechwycony muteks. Na szczęście okazało się, że nie jest to logiczny problem w serializowanym kodzie - po prostu korzystałem ze standardowej funkcjonalności pętli głównej dostarczonej przez Emscripten, ale czasami wywołanie asynchroniczne całkowicie rozpakowywało stos i w tym momencie kończyło się niepowodzeniem setTimeout z głównej pętli - tym samym kod wszedł do głównej iteracji pętli bez opuszczania poprzedniej iteracji. Przepisano w nieskończonej pętli i emscripten_sleepi problemy z muteksami ustały. Kod stał się nawet bardziej logiczny – w końcu tak naprawdę nie mam jakiegoś kodu, który przygotuje kolejną klatkę animacji – procesor po prostu coś oblicza, a ekran jest okresowo aktualizowany. Jednak na tym problemy się nie kończyły: czasami wykonanie Qemu po prostu kończyło się po cichu, bez żadnych wyjątków i błędów. W tym momencie zrezygnowałem, ale patrząc w przyszłość, powiem, że problem był następujący: kod współprogramowy w rzeczywistości nie używa setTimeout (a przynajmniej nie tak często, jak mogłoby się wydawać): funkcja emscripten_yield po prostu ustawia flagę wywołania asynchronicznego. Cały sens w tym emscripten_coroutine_next nie jest funkcją asynchroniczną: wewnętrznie sprawdza flagę, resetuje ją i przekazuje kontrolę tam, gdzie jest potrzebna. Oznacza to, że na tym kończy się promocja stosu. Problem polegał na tym, że ze względu na użycie po zwolnieniu, które pojawiło się, gdy pula współprogramów została wyłączona ze względu na fakt, że nie skopiowałem ważnej linii kodu z istniejącego zaplecza współprogramów, funkcja qemu_in_coroutine zwróciło wartość true, podczas gdy w rzeczywistości powinno zwrócić wartość false. To doprowadziło do wezwania emscripten_yield, nad którym na stosie nie było nikogo emscripten_coroutine_next, stos rozłożony na samą górę, ale nie setTimeoutjak już mówiłem, nie był wystawiany.

Generowanie kodu JavaScript

I tutaj faktycznie jest obiecane „odwrócenie mięsa mielonego”. Nie bardzo. Oczywiście jeśli w przeglądarce uruchomimy Qemu, a w niej Node.js to oczywiście po wygenerowaniu kodu w Qemu otrzymamy zupełnie błędny JavaScript. Ale wciąż jakiś rodzaj odwrotnej transformacji.

Na początek trochę o działaniu Qemu. Proszę o natychmiastowe wybaczenie: nie jestem profesjonalnym programistą Qemu i moje wnioski mogą być w niektórych miejscach błędne. Jak mówią: „opinia ucznia nie musi pokrywać się z opinią nauczyciela, aksjomatami Peano i zdrowym rozsądkiem”. Qemu ma pewną liczbę obsługiwanych architektur gościnnych i dla każdej z nich istnieje katalog podobny do target-i386. Podczas budowania możesz określić obsługę kilku architektur gościnnych, ale wynikiem będzie tylko kilka plików binarnych. Z kolei kod obsługujący architekturę gościa generuje pewne wewnętrzne operacje Qemu, które TCG (Tiny Code Generator) przekształca już w kod maszynowy dla architektury hosta. Jak stwierdzono w pliku Readme znajdującym się w katalogu tcg, był on pierwotnie częścią zwykłego kompilatora C, który później został zaadaptowany dla JIT. Dlatego na przykład architektura docelowa w rozumieniu tego dokumentu nie jest już architekturą gościa, ale architekturą hosta. W pewnym momencie pojawił się kolejny komponent – ​​Tiny Code Interpreter (TCI), który powinien wykonać kod (prawie te same operacje wewnętrzne) w przypadku braku generatora kodu dla konkretnej architektury hosta. W rzeczywistości, jak stwierdza jego dokumentacja, ten interpreter nie zawsze może działać tak dobrze, jak generator kodu JIT, nie tylko pod względem ilościowym pod względem szybkości, ale także jakościowym. Chociaż nie jestem pewien, czy jego opis jest w pełni istotny.

Na początku próbowałem stworzyć pełnoprawny backend TCG, ale szybko pogubiłem się w kodzie źródłowym i nie do końca jasnym opisie instrukcji kodu bajtowego, więc zdecydowałem się zawinąć interpreter TCI. Dało to kilka korzyści:

  • implementując generator kodu, nie można patrzeć na opis instrukcji, ale na kod interpretera
  • możesz generować funkcje nie dla każdego napotkanego bloku translacji, ale np. dopiero po setnym wykonaniu
  • jeśli wygenerowany kod ulegnie zmianie (a wydaje się to możliwe, sądząc po funkcjach o nazwach zawierających słowo patch), będę musiał unieważnić wygenerowany kod JS, ale przynajmniej będę miał z czego go zregenerować

Jeśli chodzi o trzeci punkt, nie jestem pewien, czy łatanie jest możliwe po pierwszym wykonaniu kodu, ale pierwsze dwa punkty wystarczą.

Początkowo kod był generowany w postaci dużego przełącznika pod adresem oryginalnej instrukcji bytecode, jednak potem, pamiętając artykuł o Emscripten, optymalizacji wygenerowanego JS i reloopowaniu, zdecydowałem się wygenerować więcej ludzkiego kodu, tym bardziej, że empirycznie jest to okazało się, że jedynym punktem wejścia do bloku translacji jest jego Początek. Ledwo powiedziane, a już zrobione, po pewnym czasie mieliśmy generator kodu, który generował kod za pomocą ifs (aczkolwiek bez pętli). Ale pech, zawiesił się, wyświetlając komunikat, że instrukcje były nieprawidłowej długości. Co więcej, ostatnią instrukcją na tym poziomie rekurencji było brcond. OK, dodam identyczną kontrolę do generowania tej instrukcji przed i po wywołaniu rekurencyjnym i... żadna z nich nie została wykonana, ale po przełączeniu potwierdzenia nadal nie powiodło się. Ostatecznie po przestudiowaniu wygenerowanego kodu zdałem sobie sprawę, że po przełączeniu wskaźnik do aktualnej instrukcji jest przeładowywany ze stosu i prawdopodobnie zostaje nadpisany przez wygenerowany kod JavaScript. I tak się okazało. Zwiększanie bufora z jednego megabajta do dziesięciu nie dało żadnego efektu i stało się jasne, że generator kodu kręcił się w kółko. Trzeba było sprawdzić, czy nie przekroczyliśmy granic obecnego TB, a jeśli tak, to wydać adres kolejnego TB ze znakiem minus, abyśmy mogli kontynuować realizację. Ponadto rozwiązuje to problem „które wygenerowane funkcje powinny zostać unieważnione, jeśli ten fragment kodu bajtowego uległ zmianie?” — należy unieważnić tylko funkcję odpowiadającą temu blokowi translacji. Swoją drogą, choć wszystko debugowałem w Chromium (ponieważ korzystam z Firefoksa i łatwiej mi używać osobnej przeglądarki do eksperymentów), Firefox pomógł mi skorygować niezgodności ze standardem asm.js, po czym kod zaczął działać szybciej w Chrom.

Przykład wygenerowanego kodu

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"]

wniosek

Tak więc praca wciąż nie jest ukończona, ale jestem zmęczony potajemnym doprowadzaniem tej długoterminowej konstrukcji do perfekcji. Dlatego też zdecydowałem się opublikować to, co na chwilę obecną posiadam. Kod jest miejscami trochę straszny, ponieważ jest to eksperyment i nie jest z góry jasne, co należy zrobić. Prawdopodobnie wtedy warto wydać normalne zatwierdzenia atomowe na jakiejś bardziej nowoczesnej wersji Qemu. W międzyczasie w Gicie pojawił się wątek w formie bloga: dla każdego „poziomu”, który przynajmniej w jakiś sposób został przebyty, dodany został szczegółowy komentarz w języku rosyjskim. W rzeczywistości ten artykuł jest w dużej mierze powtórzeniem wniosków git log.

Możesz spróbować wszystkiego tutaj (uważaj na ruch uliczny).

Co już działa:

  • Działa procesor wirtualny x86
  • Istnieje działający prototyp generatora kodu JIT z kodu maszynowego na JavaScript
  • Dostępny jest szablon do montażu innych 32-bitowych architektur gościnnych: w tej chwili można podziwiać Linuksa dla architektury MIPS zawieszającej się w przeglądarce na etapie ładowania

Co jeszcze możesz zrobić

  • Przyspiesz emulację. Nawet w trybie JIT wydaje się działać wolniej niż Virtual x86 (ale potencjalnie istnieje całe Qemu z dużą ilością emulowanego sprzętu i architektur)
  • Aby stworzyć normalny interfejs - szczerze mówiąc, nie jestem dobrym programistą stron internetowych, więc na razie przerobiłem standardową powłokę Emscripten najlepiej jak potrafię
  • Spróbuj uruchomić bardziej złożone funkcje Qemu - sieć, migracja maszyn wirtualnych itp.
  • UPD: będziesz musiał przesłać kilka raportów o rozwoju i błędach do Emscripten, tak jak zrobili to poprzedni porterzy Qemu i innych projektów. Dziękuję im za możliwość pośredniego wykorzystania ich wkładu w Emscripten w ramach mojego zadania.

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

Dodaj komentarz