JIT қолдауы бар Qemu.js: сіз әлі де тартқышты артқа бұра аласыз

Бірнеше жыл бұрын Фабрис Беллард jslinux жазған JavaScript тілінде жазылған ДК эмуляторы болып табылады. Осыдан кейін кем дегенде көп болды Виртуалды x86. Бірақ олардың барлығы, менің білуімше, аудармашылар болды, ал Кэму, сол Фабрис Беллардпен әлдеқайда бұрын жазылған және, мүмкін, кез келген өзін-өзі құрметтейтін заманауи эмулятор, қонақ кодының JIT компиляциясын хост жүйесінің кодына қолданады. Менің ойымша, браузерлер шешетін тапсырмаға қатысты қарама-қарсы тапсырманы жүзеге асырудың уақыты келді: JIT машина кодын JavaScript-ке құрастыру, ол үшін Qemu порты ең қисынды болып көрінді. Неліктен Qemu, қарапайым және ыңғайлы эмуляторлар бар сияқты - дәл сол VirtualBox, мысалы - орнатылған және жұмыс істейді. Бірақ Qemu-ның бірнеше қызықты мүмкіндіктері бар

  • ашық дереккөз
  • ядро драйверінсіз жұмыс істеу мүмкіндігі
  • аудармашы режимінде жұмыс істеу мүмкіндігі
  • хосттың да, қонақтың да архитектурасының үлкен санын қолдау

Үшінші тармаққа келетін болсақ, мен қазір түсіндіре аламын, шын мәнінде, TCI режимінде қонақ машинасының нұсқаулары емес, олардан алынған байт код түсіндіріледі, бірақ бұл мәнді өзгертпейді - құру және іске қосу үшін Qemu жаңа архитектурада, егер сәттілік болса, C компиляторы жеткілікті - код генераторын жазуды кейінге қалдыруға болады.

Енді, екі жыл бойы бос уақытымда Qemu бастапқы кодымен айналысқаннан кейін, мысалы, Kolibri OS іске қосуға болатын жұмыс прототипі пайда болды.

Emscripten дегеніміз не

Қазіргі уақытта көптеген компиляторлар пайда болды, олардың соңғы нәтижесі JavaScript болып табылады. Кейбіреулер, мысалы, Type Script, бастапқыда веб үшін жазудың ең жақсы тәсілі болуға арналған. Сонымен қатар, Emscripten бар C немесе C++ кодын алып, оны браузерде оқылатын пішінге құрастыру тәсілі болып табылады. Қосулы Бұл бет Біз танымал бағдарламалардың көптеген порттарын жинадық: осындаМысалы, сіз PyPy-ге қарасаңыз болады - айтпақшы, олар JIT бар деп мәлімдейді. Шындығында, кез келген бағдарламаны жай ғана құрастырып, браузерде іске қосуға болмайды - олардың саны бар Ерекшеліктер, дегенмен, сол бетте жазуда «Emscripten кез келген дерлік құрастыру үшін пайдаланылуы мүмкін» делінген. портативті JavaScript-ке C/C++ коды". Яғни, стандартқа сәйкес анықталмаған мінез-құлық болып табылатын, бірақ әдетте x86-де жұмыс істейтін бірқатар операциялар бар - мысалы, кейбір архитектураларда әдетте тыйым салынған айнымалыларға теңестірілмеген қатынас. Жалпы алғанда. , Qemu - бұл кросс-платформалық бағдарлама және мен сенгім келді және ол әлі анықталмаған мінез-құлықтарды қамтымайды - оны алыңыз және құрастырыңыз, содан кейін JIT-пен аздап айналысыңыз - және сіз аяқтадыңыз! Бірақ бұл емес! іс...

Бірінші әрекет

Жалпы айтқанда, мен Qemu-ді JavaScript-ке көшіру идеясын ұсынған бірінші адам емеспін. ReactOS форумында бұл Emscripten көмегімен мүмкін бе деген сұрақ қойылды. Бұған дейін Фабрис Беллард мұны жеке өзі жасады деген қауесет тараған болатын, бірақ біз jslinux туралы айтып отырмыз, менің білуімше, бұл JS-те жеткілікті өнімділікке қолмен қол жеткізу әрекеті және нөлден бастап жазылған. Кейінірек Virtual x86 жазылды - ол үшін жасырын емес көздер орналастырылды және айтылғандай эмуляцияның үлкен «шынайылығы» SeaBIOS-ты микробағдарлама ретінде пайдалануға мүмкіндік берді. Сонымен қатар, Emscripten көмегімен Qemu портына кем дегенде бір әрекет жасалды - мен мұны істеуге тырыстым розетка жұбы, бірақ менің түсінуімше, даму қатып қалды.

Сонымен, бұл жерде дереккөздер, міне Emscripten - оны алыңыз және құрастырыңыз. Бірақ сонымен қатар Qemu тәуелді кітапханалар және сол кітапханалар тәуелді кітапханалар және т.б. бар және олардың бірі либфи, қай глипке байланысты. Интернетте Emscripten кітапханаларының үлкен коллекциясында біреуі бар деген қауесет тарады, бірақ оған сену қиын болды: біріншіден, ол жаңа компилятор болуға арналмаған, екіншіден, ол тым төмен деңгейлі болды. кітапхананы жай ғана алып, JS-ке құрастырыңыз. Бұл жай ғана құрастыру кірістірулері мәселесі емес - бәлкім, егер сіз оны бұрасаңыз, кейбір шақыру конвенциялары үшін стекте қажетті аргументтерді жасай аласыз және оларсыз функцияны шақыра аласыз. Бірақ Emscripten - бұл қиын нәрсе: жасалған кодты JS қозғалтқышын оңтайландырушы браузеріне таныс ету үшін кейбір трюктар қолданылады. Атап айтқанда, қайта құру деп аталатын - кейбір абстрактілі өту нұсқаулары бар алынған LLVM IR пайдаланатын код генераторы ықтимал ifs, ілмектер және т.б. қайта құруға тырысады. Ал, аргументтер функцияға қалай беріледі? Әрине, JS функцияларына дәлел ретінде, яғни мүмкін болса, стек арқылы емес.

Бастапқыда JS көмегімен libffi орнына жай жазу және стандартты сынақтарды орындау идеясы болды, бірақ соңында мен тақырып файлдарын бар кодпен жұмыс істейтіндей етіп қалай жасауға болатынын білмедім - мен не істей аламын? Олар айтқандай, «Біз соншалықты ақымақпыз ба?» Тапсырмалар соншалықты күрделі ме? Маған libffi-ді басқа архитектураға порттау керек болды, былайша айтқанда - бақытымызға орай, Emscripten-де кірістірілген құрастыруға арналған макростар да (Javascript-те, иә - архитектура қандай болса да, ассемблер де) және жылдам жасалған кодты іске қосу мүмкіндігі бар. Жалпы, платформаға тәуелді libffi фрагменттерімен біраз уақыт айналысқаннан кейін мен компиляцияланатын кодты алдым және оны бірінші кездестірген сынақта іске қостым. Менің таң қалғаным, сынақ сәтті өтті. Менің данышпаным таң қалды - әзіл-қалжың жоқ, ол бірінші ұшырудан бастап жұмыс істеді - мен әлі де өз көзіме сенбей, алынған кодты қайта қарауға, әрі қарай қайда қазу керектігін бағалауға бардым. Міне, мен екінші рет жаңғақ болдым - менің функциям жасаған жалғыз нәрсе болды ffi_call - бұл сәтті қоңырау туралы хабарлады. Өзі қоңырау болған жоқ. Сондықтан мен бірінші сұрауымды жібердім, ол кез келген олимпиада оқушысына түсінікті тесттегі қатені түзетті - нақты сандарды салыстыруға болмайды. a == b және тіпті қалай a - b < EPS - модульді де есте сақтау керек, әйтпесе 0 1/3-ке тең болып шығады... Жалпы, мен қарапайым сынақтардан өтетін және glib болатын белгілі бір libffi портын ойлап таптым. құрастырылған - бұл қажет деп шештім, мен оны кейінірек қосамын. Алға қарайтын болсақ, компилятор соңғы кодқа libffi функциясын да қоспағанын айтамын.

Бірақ, жоғарыда айтқанымдай, кейбір шектеулер бар және әртүрлі анықталмаған мінез-құлықтарды еркін пайдаланудың арасында одан да жағымсыз мүмкіндік жасырылды - дизайн бойынша JavaScript ортақ жадымен көп ағынды қолдамайды. Негізінде, бұл әдетте жақсы идея деп атауға болады, бірақ архитектурасы C ағындарымен байланыстырылған кодты тасымалдау үшін емес. Жалпы айтқанда, Firefox ортақ жұмысшыларға қолдау көрсетумен тәжірибе жасауда, және Emscripten-де олар үшін тізілімді енгізу бар, бірақ мен оған тәуелді болғым келмеді. Мен Qemu кодынан көп ағынды баяу жоюға тура келді - яғни ағындардың қай жерде жұмыс істейтінін анықтау, осы ағында жұмыс істейтін циклдің денесін бөлек функцияға жылжыту және негізгі циклден осындай функцияларды бір-бірден шақыру.

Екінші рет көріңіз

Бір кездері мәселе әлі де бар екені және кодты кездейсоқ балдақпен серуендеу жақсылыққа апармайтыны белгілі болды. Қорытынды: біз балдақтарды қосу процесін қандай да бір түрде жүйелеуіміз керек. Сондықтан, сол кезде жаңа шыққан 2.4.1 нұсқасы алынды (2.5.0 емес, өйткені, кім біледі, жаңа нұсқада әлі ұсталмаған қателер болады, менде өз қателерім жеткілікті. ) және бірінші нәрсе оны қауіпсіз түрде қайта жазу болды thread-posix.c. Яғни, қауіпсіз: егер біреу блоктауға әкелетін операцияны орындауға әрекеттенсе, функция дереу шақырылды abort() - әрине, бұл барлық мәселелерді бірден шешпеді, бірақ, кем дегенде, сәйкес келмейтін деректерді тыныш қабылдаудан гөрі жағымдырақ болды.

Жалпы, Emscripten опциялары кодты JS жүйесіне тасымалдауда өте пайдалы -s ASSERTIONS=1 -s SAFE_HEAP=1 - олар анықталмаған әрекеттің кейбір түрлерін ұстайды, мысалы, реттелмеген мекенжайға қоңыраулар (бұл терілген массивтердің кодымен мүлдем сәйкес келмейді HEAP32[addr >> 2] = 1) немесе дұрыс емес аргумент саны бар функцияны шақыру.

Айтпақшы, туралау қателері бөлек мәселе. Жоғарыда айтып өткенімдей, Qemu-да TCI (кішкентай код интерпретаторы) кодты генерациялауға арналған «тозған» интерпретативті сервер бар және Qemu-ны жаңа архитектурада құру және іске қосу үшін, егер сәттілік болса, C компиляторы жеткілікті. «Егер бақытты болсаң». Менің жолым болмады және TCI байт кодты талдау кезінде теңестірілмеген қатынасты пайдаланатыны анықталды. Яғни, ARM-ның барлық түрлерінде және міндетті түрде теңестірілген қол жеткізуге ие басқа архитектураларда Qemu компиляциялайды, себебі оларда жергілікті кодты жасайтын қалыпты TCG сервері бар, бірақ TCI оларда жұмыс істей ме, жоқ па, бұл басқа мәселе. Алайда, белгілі болғандай, TCI құжаттамасында ұқсас нәрсе анық көрсетілген. Нәтижесінде, Qemu-ның басқа бөлігінде табылған кодқа теңестірілмеген оқуға арналған функция шақырулары қосылды.

Үйіндінің жойылуы

Нәтижесінде TCI-ге теңестірілмеген қол жеткізу түзетілді, өз кезегінде процессор, RCU және басқа да шағын нәрселер деп аталатын негізгі цикл жасалды. Сондықтан мен Qemu опциясын іске қосамын -d exec,in_asm,out_asm, бұл дегеніміз, қандай код блоктары орындалып жатқанын айту керек, сондай-ақ хабар тарату кезінде қонақ коды қандай болды, қандай хост коды болды (бұл жағдайда байт код). Ол басталады, бірнеше аударма блоктарын орындайды, RCU енді іске қосылатыны туралы мен қалдырған жөндеу хабарын жазады және... бұзылады abort() функцияның ішінде free(). Функциямен айналысу арқылы free() Бөлінген жадтың алдындағы сегіз байтта орналасқан үйме блогының тақырыбында блок өлшемі немесе ұқсас нәрсенің орнына қоқыс бар екенін анықтадық.

Үйіндінің жойылуы - қандай сүйкімді ... Мұндай жағдайда пайдалы құрал бар - (мүмкіндігінше) бірдей көздерден, жергілікті екілік файлды жинап, оны Valgrind астында іске қосыңыз. Біраз уақыттан кейін екілік дайын болды. Мен оны бірдей опциялармен іске қосамын - ол инициализация кезінде, тіпті орындауға дейін істен шығады. Бұл, әрине, жағымсыз - мүмкін, көздер бірдей емес еді, бұл таңқаларлық емес, өйткені конфигурация сәл басқа нұсқаларды қарастырды, бірақ менде Valgrind бар - алдымен мен бұл қатені түзетемін, содан кейін жолым болса. , түпнұсқасы пайда болады. Мен дәл сол нәрсені Valgrind астында іске қосып жатырмын... Y-y-y, y-y-y, uh-uh, ол басталды, қалыпты түрде инициализациядан өтті және құлау туралы айтпағанда, жадқа дұрыс кіру туралы бірде-бір ескертусіз бастапқы қатеден өтті. Өмір, олар айтқандай, мені бұған дайындамады - апатты бағдарлама Walgrind астында іске қосылған кезде істен шығуды тоқтатады. Оның не болғаны жұмбақ. Менің болжамым бойынша, инициализация кезінде апаттан кейін ағымдағы нұсқаулықтың жанында бір рет gdb жұмысын көрсетті. memset-a екеуін де қолданатын жарамды көрсеткіші бар mmx, немесе xmm регистрлер, содан кейін бұл туралаудың қандай да бір қателігі болуы мүмкін, бірақ әлі де сену қиын.

Жарайды, Валгринд бұл жерде көмектеспейтін сияқты. Міне, ең жиіркенішті нәрсе басталды - бәрі тіпті басталатын сияқты, бірақ миллиондаған нұсқаулар бұрын болуы мүмкін оқиғаға байланысты мүлдем белгісіз себептермен бұзылады. Ұзақ уақыт бойы қалай жақындау керектігі де түсініксіз болды. Ақыр соңында, мен әлі де отыруға және жөндеуге тура келді. Тақырыптың қайта жазылғанын басып шығару оның санға емес, екілік деректердің қандай да бір түріне ұқсайтынын көрсетті. Міне, міне, бұл екілік жол BIOS файлында табылды - яғни, енді ол буфердің толып кетуі деп сенімді түрде айтуға болады, тіпті бұл буферге жазылғаны да анық. Ал, содан кейін осындай нәрсе - Emscripten-де, бақытымызға орай, мекенжай кеңістігінің рандомизациясы жоқ, онда да тесіктер жоқ, сондықтан сіз соңғы іске қосудан бастап көрсеткіш арқылы деректерді шығару үшін кодтың ортасында бір жерде жаза аласыз, деректерге қараңыз, көрсеткішке қараңыз және егер ол өзгермеген болса, ойға азық алыңыз. Рас, кез келген өзгерістен кейін байланыстыруға бірнеше минут кетеді, бірақ сіз не істей аласыз? Нәтижесінде BIOS-ны уақытша буферден қонақ жадына көшіретін белгілі бір жол табылды - және шынымен де буферде орын жеткіліксіз болды. Біртүрлі буфер мекенжайының көзін табу функцияға әкелді qemu_anon_ram_alloc файлда oslib-posix.c - логика мынадай болды: кейде мекенжайды өлшемі 2 МБ үлкен бетке теңестіру пайдалы болуы мүмкін, бұл үшін біз сұраймыз. mmap алдымен тағы біраз, сосын артығын көмекпен қайтарамыз munmap. Ал егер мұндай туралау қажет болмаса, онда біз 2 МБ орнына нәтижені көрсетеміз getpagesize() - mmap ол әлі де тураланған мекенжайды береді... Сонымен Emscripten-де mmap жай ғана қоңыраулар malloc, бірақ, әрине, ол бетте тураланбайды. Жалпы, мені екі ай бойы ренжіткен қатені өзгерту арқылы түзетілді екі дана сызықтар.

Шақыру функцияларының ерекшеліктері

Ал қазір процессор бірдеңені санап жатыр, Qemu бұзылмайды, бірақ экран қосылмайды, ал процессор шығысына қарай тез циклдарға өтеді. -d exec,in_asm,out_asm. Гипотеза пайда болды: таймер үзілістері (немесе, жалпы алғанда, барлық үзілістер) келмейді. Шынында да, егер сіз қандай да бір себептермен жұмыс істеген жергілікті жиналыстан үзілістерді бұрап алсаңыз, сіз ұқсас суретті аласыз. Бірақ бұл мүлдем жауап болмады: жоғарыда келтірілген нұсқамен берілген іздерді салыстыру орындалу траекторияларының өте ерте бөлінгенін көрсетті. Бұл жерде қосқыштың көмегімен жазылғанды ​​салыстыру керек emrun Түпнұсқа жинақтың шығысымен отладка шығару толығымен механикалық процесс емес. Мен браузерде жұмыс істейтін бағдарламаның қалай қосылатынын білмеймін emrun, бірақ шығыстағы кейбір жолдар қайта реттеледі, сондықтан дифференциядағы айырмашылық траекториялар алшақтады деп болжауға әлі негіз емес. Жалпы, нұсқаулыққа сәйкес екені белгілі болды ljmpl әртүрлі адрестерге көшу бар, ал генерацияланған байт-код түбегейлі ерекшеленеді: біреуінде көмекші функцияны шақыру нұсқауы бар, екіншісінде жоқ. Нұсқауларды іздеуден және осы нұсқауларды аударатын кодты зерделеуден кейін, біріншіден, тізілімде оның алдында тікелей cr0 Процессорды қорғалған режимге ауыстырған, екіншіден, JS нұсқасы ешқашан қорғалған режимге ауыспаған көмекші арқылы жазба жасалды. Бірақ Эмскриптеннің тағы бір ерекшелігі - оның нұсқауларды орындау сияқты кодқа шыдамауы. call TCI-де, кез келген функция көрсеткіші түрін береді long long f(int arg0, .. int arg9) - функциялар аргументтердің дұрыс санымен шақырылуы керек. Егер бұл ереже бұзылса, отладтау параметрлеріне байланысты бағдарлама бұзылады (бұл жақсы) немесе мүлде қате функцияны шақырады (оны жөндеу өкінішті болады). Сондай-ақ үшінші нұсқа бар - аргументтерді қосатын/жойатын орауыштарды генерациялауды қосыңыз, бірақ шын мәнінде маған жүзден сәл астам орауыш қажет болғанына қарамастан, жалпы алғанда бұл орауыштар көп орын алады. Бұл өте өкінішті, бірақ одан да маңызды мәселе болды: орауыш функцияларының құрылған кодында аргументтер түрлендірілді және түрленді, бірақ кейде жасалған аргументтері бар функция шақырылмады - жақсы, дәл сол сияқты менің libffi іске асыруым. Яғни, кейбір көмекшілер жай ғана өлім жазасына кесілген жоқ.

Бақытымызға орай, Qemu-да тақырып файлы түріндегі машина оқылатын көмекшілер тізімдері бар.

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

Олар өте күлкілі қолданылады: біріншіден, макростар ең оғаш жолмен қайта анықталады DEF_HELPER_n, содан кейін қосылады helper.h. Макрос құрылымды инициализаторға және үтірге кеңейтіліп, содан кейін массив анықталады және элементтердің орнына - #include <helper.h> Нәтижесінде мен жұмыста кітапхананы сынап көру мүмкіндігіне ие болдым пипаринг, және сценарий жазылды, ол дәл сол орауыштарды қажет функциялар үшін жасайды.

Содан кейін процессор жұмыс істегендей болды. Бұл memtest86+ жергілікті жинақта жұмыс істей алғанымен, экран ешқашан инициализацияланбаған сияқты. Мұнда Qemu блогының енгізу/шығару коды корутиндерде жазылғанын нақтылау қажет. Emscripten-де өзінің өте күрделі іске асырылуы бар, бірақ оған Qemu кодында әлі де қолдау көрсету қажет болды және процессорды қазір жөндеуге болады: Qemu опцияларды қолдайды. -kernel, -initrd, -append, оның көмегімен Linux немесе, мысалы, memtest86+, блоктық құрылғыларды мүлде пайдаланбай жүктей аласыз. Бірақ мәселе мынада: жергілікті жинақта консольге Linux ядросының шығуын опциямен көруге болады. -nographic, және браузерден ол іске қосылған жерден терминалға шықпайды emrun, келмеді. Яғни, түсініксіз: процессор жұмыс істемейді немесе графикалық шығыс жұмыс істемейді. Сосын біраз күту ойыма келді. «Процессор ұйықтап жатқан жоқ, жай ғана жыпылықтайды» және бес минуттан кейін ядро ​​​​консольге көптеген хабарламаларды тастап, іліп қоюды жалғастырды. Процессордың жалпы жұмыс істейтіні белгілі болды және біз SDL2-мен жұмыс істеу үшін кодты қазып алуымыз керек. Өкінішке орай, мен бұл кітапхананы қалай пайдалану керектігін білмеймін, сондықтан кейбір жерлерде кездейсоқ әрекет етуге тура келді. Бір кезде көк фонда экранда параллель0 сызығы жыпылықтайды, бұл кейбір ойларды ұсынды. Соңында, мәселе Qemu бір физикалық терезеде бірнеше виртуалды терезелерді ашатыны анықталды, олардың арасында Ctrl-Alt-n арқылы ауысуға болады: ол жергілікті құрылымда жұмыс істейді, бірақ Emscripten-де емес. Опцияларды пайдаланып қажетсіз терезелерден құтылғаннан кейін -monitor none -parallel none -serial none және әрбір кадрдағы бүкіл экранды күштеп қайта салу нұсқаулары, бәрі кенеттен жұмыс істеді.

Корутиндер

Сонымен, браузердегі эмуляция жұмыс істейді, бірақ сіз ондағы қызықты бір дискетканы іске қоса алмайсыз, өйткені енгізу/шығару блогы жоқ - сізге корутиндерге қолдау көрсету қажет. Qemu-да қазірдің өзінде бірнеше корутиндік серверлер бар, бірақ JavaScript және Emscripten код генераторының табиғатына байланысты стектерді жонглёрлеуді бастау мүмкін емес. «Бәрі кетіп қалды, сылақ алынып жатыр» сияқты көрінетін, бірақ Emscripten әзірлеушілері бәрін шешіп қойған. Бұл өте күлкілі орындалды: осы күдікті сияқты функция шақыруын шақырайық emscripten_sleep және Asyncify механизмін пайдаланатын бірнеше басқалары, сондай-ақ көрсеткішті шақырулар мен алдыңғы екі жағдайдың бірі стектен төменірек орын алуы мүмкін кез келген функцияға шақырулар. Ал енді, әрбір күдікті шақыру алдында біз асинхронды контекстті таңдаймыз және қоңыраудан кейін бірден асинхронды шақырудың болған-болмағанын тексереміз, егер ол бар болса, біз барлық жергілікті айнымалы мәндерді осы асинхронды контексте сақтаймыз, қай функцияны көрсетіңіз. басқаруды орындауды жалғастыру және ағымдағы функциядан шығу қажет болған уақытқа беру үшін. Бұл жерде әсерді зерттеуге мүмкіндік бар ысырап ету — асинхронды шақырудан қайтқаннан кейін кодты орындауды жалғастыру қажеттіліктері үшін компилятор күдікті шақырудан кейін басталатын функцияның «қоқыстарын» жасайды — келесідей: егер n күдікті шақыру болса, онда функция бір жерде n/2 кеңейтіледі. рет — бұл әлі де, егер жоқ болса, әрбір ықтимал асинхронды шақырудан кейін бастапқы функцияға кейбір жергілікті айнымалы мәндерді сақтауды қосу керек екенін есте сақтаңыз. Кейіннен, мен тіпті Python-да қарапайым сценарий жазуға тура келді, ол «асинхронияның өздігінен өтуіне жол бермейтін» (яғни, стек жылжыту және мен жаңа ғана сипаттағандардың бәрі емес) ерекше қолданылған функциялардың берілген жиынтығына негізделген. олардағы жұмыс), бұл функциялар асинхронды болып саналмауы үшін компилятор функцияларды елемеуі керек болатын көрсеткіштер арқылы шақыруларды көрсетеді. Содан кейін 60 МБ-тан төмен JS файлдары анық тым көп – кем дегенде 30 делік. Дегенмен, бір кездері мен құрастыру сценарийін орнатып жатқанда, кездейсоқ сілтеме опцияларын тастадым, олардың арасында -O3. Мен жасалған кодты іске қосамын және Chromium жадты жейді және бұзылады. Мен оның жүктеп алмақшы болғанын байқамай көрдім... Не айтамын, егер маған 500+ МБ Javascript-ті мұқият оқып, оңтайландыруды сұраса, мен де қатып қалар едім.

Өкінішке орай, Asyncify қолдау кітапханасының кодындағы тексерулер толығымен сәйкес келмеді longjmp-s виртуалды процессор кодында пайдаланылады, бірақ бұл тексерулерді өшіретін және контекстерді күштеп қалпына келтіретін шағын патчтан кейін бәрі жақсы сияқты, код жұмыс істеді. Содан кейін таңқаларлық нәрсе басталды: кейде синхрондау кодындағы тексерулер іске қосылды - кодты бұзатындар, егер орындау логикасына сәйкес, оны бұғаттау керек болса - біреу әлдеқашан түсірілген мутексті басып алуға тырысты. Бақытымызға орай, бұл серияланған кодта логикалық мәселе емес болып шықты - мен жай ғана Emscripten ұсынған стандартты негізгі цикл функциясын қолдандым, бірақ кейде асинхронды қоңырау стекті толығымен ашады және сол сәтте ол сәтсіз болады. setTimeout негізгі циклден - осылайша, код алдыңғы итерациядан шықпай негізгі цикл итерациясына енді. Шексіз циклде қайта жазу және emscripten_sleep, және мутекстермен проблемалар тоқтатылды. Код одан да қисынды болды - шын мәнінде, менде келесі анимациялық кадрды дайындайтын код жоқ - процессор жай ғана бір нәрсені есептейді және экран мезгіл-мезгіл жаңартылып отырады. Дегенмен, проблемалар мұнымен тоқтап қалмады: кейде Qemu орындауы ешқандай ерекшеліктер немесе қателерсіз жай ғана үнсіз аяқталады. Сол сәтте мен одан бас тарттым, бірақ алға қарай, мәселе мынада екенін айтамын: корутиндік код, шын мәнінде, пайдаланбайды. setTimeout (немесе, кем дегенде, сіз ойлағандай жиі емес): функция emscripten_yield жай ғана асинхронды шақыру жалауын орнатады. Мәселе мынада emscripten_coroutine_next асинхронды функция емес: іштей ол жалаушаны тексереді, оны қалпына келтіреді және басқаруды қажет жерге жібереді. Яғни, стекті жылжыту сонда аяқталады. Мәселе мынада болды: мен бар корутиндік серверден кодтың маңызды жолын көшірмегендіктен корутин пулы өшірілген кезде пайда болған use-after-free функциясына байланысты qemu_in_coroutine шын мәнісінде ол false мәнін қайтару керек болғанда true мәнін қайтарды. Бұл қоңырауға әкелді emscripten_yield, оның үстінде стекте ешкім болмады emscripten_coroutine_next, стек ең жоғарғы жағына дейін ашылды, бірақ жоқ setTimeout, жоғарыда айтқанымдай, көрмеге қойылған жоқ.

JavaScript кодын жасау

Міне, шын мәнінде, уәде етілген «таршты кері қайтару». Онша емес. Әрине, егер біз Qemu браузерін және онда Node.js іске қоссақ, Qemu-да кодты жасағаннан кейін, әрине, қате JavaScript аламыз. Бірақ бәрібір, кері түрлендірудің қандай да бір түрі.

Алдымен, Qemu қалай жұмыс істейтіні туралы аздап. Мені дереу кешіріңіз: мен кәсіби Qemu әзірлеушісі емеспін және менің тұжырымдарым кейбір жерлерде қате болуы мүмкін. Олар айтқандай, «оқушының пікірі мұғалімнің пікірімен, Пеано аксиоматикасымен және парасаттылықпен сәйкес келмеуі керек». Qemu-да қолдау көрсетілетін қонақ архитектураларының белгілі саны бар және әрқайсысы үшін осындай каталог бар target-i386. Құру кезінде сіз бірнеше қонақ архитектурасына қолдау көрсете аласыз, бірақ нәтиже тек бірнеше екілік болады. Қонақ архитектурасын қолдауға арналған код, өз кезегінде, TCG (Tiny Code Generator) хост архитектурасы үшін машиналық кодқа айналатын кейбір ішкі Qemu операцияларын жасайды. tcg каталогында орналасқан readme файлында айтылғандай, бұл бастапқыда әдеттегі C компиляторының бөлігі болды, кейінірек ол JIT үшін бейімделген. Сондықтан, мысалы, осы құжат тұрғысынан мақсатты архитектура енді қонақ архитектурасы емес, хост архитектурасы болып табылады. Бір сәтте тағы бір компонент пайда болды - Tiny Code Interpreter (TCI), ол белгілі бір хост архитектурасы үшін код генераторы болмаған кезде кодты (дерлік бірдей ішкі операциялар) орындауы керек. Шындығында, оның құжаттамасында айтылғандай, бұл интерпретатор JIT кодының генераторы сияқты жылдамдықты ғана емес, сонымен қатар сапалық жағынан да жақсы жұмыс істей алмайды. Оның сипаттамасы толығымен сәйкес келетініне сенімді болмасам да.

Алдымен мен толыққанды TCG серверін жасауға тырыстым, бірақ бастапқы кодта және байт-код нұсқауларының толығымен анық емес сипаттамасында тез шатастырдым, сондықтан мен TCI аудармашысын орауды шештім. Бұл бірнеше артықшылықтар берді:

  • код генераторын енгізу кезінде нұсқаулардың сипаттамасына емес, аудармашы кодына қарауға болады
  • сіз кез келген аударма блогы үшін емес, мысалы, жүзінші орындалудан кейін функцияларды жасай аласыз.
  • егер жасалған код өзгерсе (және бұл мүмкін сияқты, патч сөзі бар атаулары бар функцияларға қарағанда), мен жасалған JS кодын жарамсыз деп тануға тура келеді, бірақ кем дегенде менде оны қалпына келтіретін нәрсе болады.

Үшінші тармаққа келетін болсақ, кодты бірінші рет орындағаннан кейін түзету мүмкін болатынына сенімді емеспін, бірақ алғашқы екі тармақ жеткілікті.

Бастапқыда код бастапқы байт-код нұсқаулығының мекенжайында үлкен коммутатор түрінде жасалды, бірақ содан кейін Emscripten, жасалған JS оңтайландыру және қайта құру туралы мақаланы еске түсіріп, мен көбірек адам кодын жасауды шештім, әсіресе эмпирикалық түрде бұл аударма блогына жалғыз кіру нүктесі оның Бастауы болып шықты. Біраз уақыттан кейін бізде ifs көмегімен код жасайтын код генераторы пайда болды (бірақ циклдарсыз). Бірақ сәтсіздікке ұшырады, ол апатқа ұшырап, нұсқаулардың дұрыс емес ұзындығы туралы хабарлама берді. Сонымен қатар, осы рекурсия деңгейіндегі соңғы нұсқау болды brcond. Жарайды, мен осы нұсқаудың генерациясына рекурсивті қоңырауға дейін және одан кейін бірдей тексеру қосамын және... олардың ешқайсысы орындалмады, бірақ бекітуді ауыстырғаннан кейін олар әлі де сәтсіз болды. Соңында, құрылған кодты зерттегеннен кейін, коммутатордан кейін ағымдағы нұсқаулыққа көрсеткіш стектен қайта жүктелетінін және жасалған JavaScript коды арқылы қайта жазылатынын түсіндім. Міне, солай болды. Буферді бір мегабайттан онға дейін ұлғайту ештеңеге әкелмеді және код генераторы шеңберлерде жұмыс істейтіні белгілі болды. Біз қазіргі ТБ шекарасынан шықпағанымызды тексеруіміз керек еді, егер барсақ, орындауды жалғастыру үшін келесі ТБ мекенжайын минус белгісімен беріңіз. Бұған қоса, бұл «Байтекодтың осы бөлігі өзгерген жағдайда қандай функциялар жарамсыз болуы керек?» деген мәселені шешеді. — тек осы аударма блогына сәйкес функцияны жарамсыз деп тану керек. Айтпақшы, мен барлығын Chromium-да түзетсем де (мен Firefox-ты қолданамын және эксперименттер үшін бөлек браузерді пайдалану маған оңайырақ), Firefox маған asm.js стандартымен үйлесімсіздіктерді түзетуге көмектесті, содан кейін код тезірек жұмыс істей бастады. Chromium.

Жасалған кодтың мысалы

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

қорытынды

Сонымен, жұмыс әлі аяқталған жоқ, бірақ мен бұл ұзақ мерзімді құрылысты жасырын түрде жетілдіруден шаршадым. Сондықтан мен әзірше қолда барды жариялауды жөн көрдім. Код кейбір жерлерде қорқынышты, өйткені бұл эксперимент және не істеу керектігі алдын ала белгісіз. Мүмкін, Qemu-ның заманауи нұсқасының үстіне қалыпты атомдық міндеттемелерді шығарған жөн. Бұл арада Гитада блог форматындағы ағын бар: кем дегенде қандай да бір жолмен өткен әрбір «деңгейге» орыс тіліндегі егжей-тегжейлі түсініктеме қосылды. Шындығында, бұл мақала негізінен қорытындыны қайталау болып табылады git log.

Барлығын сынап көруге болады осында (жол қозғалысын сақтаңыз).

Қазірдің өзінде не жұмыс істейді:

  • x86 виртуалды процессоры жұмыс істейді
  • JIT код генераторының машиналық кодтан JavaScript-ке дейінгі жұмыс прототипі бар
  • Басқа 32-биттік қонақ архитектурасын құрастыруға арналған үлгі бар: дәл қазір сіз Linux-ті жүктеу кезеңінде браузерде қатып қалған MIPS архитектурасына таңдана аласыз.

Басқа не істей аласың

  • Эмуляцияны жылдамдату. Тіпті JIT режимінде ол Virtual x86-ге қарағанда баяу жұмыс істейтін сияқты (бірақ көптеген эмуляцияланған аппараттық құралдар мен архитектуралары бар тұтас Qemu болуы мүмкін)
  • Қалыпты интерфейс жасау үшін - шынымды айтсам, мен жақсы веб-әзірлеуші ​​емеспін, сондықтан мен стандартты Emscripten қабығын мүмкіндігінше қайта жасадым.
  • Күрделі Qemu функцияларын іске қосып көріңіз - желіні құру, VM көшіру және т.б.
  • UPD: Qemu және басқа жобалардың алдыңғы портерлері сияқты, сізге бірнеше әзірлемелер мен қателер туралы есептерді Emscripten upstream-ге жіберу керек болады. Менің тапсырмамның бөлігі ретінде Emscripten-ге қосқан үлестерін жанама түрде пайдалана алғандары үшін оларға рахмет.

Ақпарат көзі: www.habr.com

пікір қалдыру