Qemu.js з підтримкою JIT: фарш все ж таки можна провернути назад

Кілька років тому Фабріс Беллар написав jslinux - емулятор ПК, написаний на JavaScript. Після цього був ще як мінімум Virtual x86. Але всі вони, наскільки мені відомо, були інтерпретаторами, тоді як написаний значно раніше тим же Фабрисом Белларом Qemu, та й, напевно, будь-який сучасний емулятор, що поважає себе, використовує JIT-компіляцію гостьового коду в код хостової системи. Мені здалося, що саме час реалізувати зворотне завдання по відношенню до того, яке вирішують браузери: JIT-компіляцію машинного коду в JavaScript, для чого найлогічніше бачилося портувати Qemu. Здавалося б, чому саме Qemu, є ж простіші і user-friendly емулятори - той самий VirtualBox, наприклад - поставив і працює. Але Qemu має кілька цікавих особливостей

  • відкриті вихідні
  • можливість працювати без драйвера ядра
  • можливість працювати в режимі інтерпретатора
  • підтримка великої кількості як хостових, так і гостьових архітектур

На рахунок третього пункту тепер я вже можу пояснити, що насправді в режимі TCI інтерпретуються не самі гостьові машинні інструкції, а отриманий з них байткод, але це не змінює — щоб зібрати і запустити Qemu на новій архітектурі, якщо пощастить, достатньо компілятора C - написання кодогенератора можна відкласти.

І ось, після двох років повільного колупання у вільний час вихідників Qemu з'явився працюючий прототип, в якому вже можна запустити, наприклад, Kolibri OS.

Що таке Emscripten

В наші часи з'явилося багато компіляторів, кінцевим результатом роботи яких є JavaScript. Деякі, такі як Type Script, спочатку замислювалися як найкращий спосіб писати для Інтернету. В той же час, Emscripten - це спосіб взяти існуючий код на C або C + +, і скомпілювати його у вигляд, зрозумілий браузеру. на цій сторінці зібрано чимало портів відомих програм: тутНаприклад, можна подивитися на PyPy - до речі, як стверджується, у них вже є JIT. Насправді не будь-яку програму можна просто скомпілювати і запустити в браузері — є ряд особливостей, з якими доводиться миритися, втім, як напис на цій самій сторінці "Emscripten can be used to compile almost any портативний C/C++ code to JavaScript". Тобто існує ряд операцій, які є невизначеною поведінкою за стандартом, але зазвичай працюють на x86 - наприклад, невирівняний доступ до змінних, який на деяких архітектурах взагалі заборонений. Загалом, Qemu - програма кросплатформова і , хотілося вірити, і так не містить великої кількості невизначеної поведінки - бери і компілюй, потім трохи повозитися з JIT - і говото!

Перша спроба

Взагалі кажучи, я не перший, кому спала на думку ідея портувати Qemu на JavaScript. На форумі ReactOS ставилося питання, чи це можливо за допомогою Emscripten. Ще раніше ходили чутки, що це зробив особисто Фабріс Беллар, але йшлося про jslinux, який, наскільки мені відомо, є саме спробою вручну досягти JS достатньої продуктивності, і написаний з нуля. Пізніше був написаний Virtual x86 — до нього були викладені необфусцовані вихідні джерела, і, як стверджувалася, велика "реалістичність" емуляції дозволила використовувати SeaBIOS як firmware. Крім того, була як мінімум одна спроба портувати Qemu за допомогою Emscripten – це намагався зробити розеткаАле розробка, наскільки я зрозумів, була заморожена.

Отже, здавалося б, ось вихідники, ось Emscripten – бери та компілюй. Але є ще й бібліотеки, від яких Qemu залежить, і бібліотеки від яких залежать ті бібліотеки і т.д., причому одна з них libffiвід якої залежить glib. В інтернеті були чутки, що у великій колекції портів бібліотек під Emscripten є і вона, але вірилося якось насилу: по-перше, новим компілятором вона не збиралася, по-друге, це занадто низькорівнева бібліотека, щоб просто так взяти, і скомпілюватися в JS. І річ навіть не тільки в асемблерних вставках — напевно, якщо перекрутитися, то для деяких calling conventions можна і без них сформувати потрібні аргументи на стеку та викликати функцію. Ось тільки Emscripten - штуковина хитра: для того, щоб згенерований код виглядав звичним для оптимізатора JS-движка браузера, використовуються деякі трюки. Зокрема, так званий relooping - кодогенератор по отриманому LLVM IR з абстрактними інструкціями переходів намагається відтворити правдоподібні if-и, цикли і т.д. Ну а аргументи у функції передаються як? Звичайно, як аргументи JS-функцій, тобто по можливості не через стек.

На початку була думка просто написати заміну libffi на JS і прогнати штатні тести, але зрештою я заплутався в тому, як зробити свої заголовні файли, щоб вони працювали з існуючим кодом — що вже вдієш, як то кажуть, "Чи завдання такі складні , чи ми такі тупі ". Довелося портувати libffi на ще одну архітектуру, якщо можна так висловитися - на щастя, в Emscripten є як макроси для inline assembly (на джаваскрипті, ага - ну, яка архітектура, такий і асемблер), так і можливість запустити код, що згенерував на ходу. Загалом, повозившись деякий час з платформно-залежними фрагментами libffi, я отримав якийсь код, що компілюється, і прогнав його на першому тесті, що попався. На мій подив, тест пройшов успішно. Офігєв від своєї геніальності - чи жарт, запрацювало з першого запуску - я, все ще не вірячи своїм очам, поліз ще раз подивитися на код, що вийшов, оцінити, куди копати далі. Тут я офігел вдруге - єдине, що робила моя функція ffi_call - це рапортувала про успішний виклик. Самого виклику не було. Так я відправив свій перший pull request, який виправляв зрозумілу будь-якому олімпіаднику помилку в тесті - речові числа не слід порівнювати як a == b і навіть як a - b < EPS - Треба ще модуль не забути, а то 0 виявиться дуже навіть дорівнює 1/3 ... Загалом, у мене вийшов якийсь порт libffi, який проходить найпростіші тести, і з яким компілюється glib - вирішив, треба буде, потім допишу. Забігаючи наперед скажу, що, як виявилося, до фінального коду функції libffi компілятор навіть не включив.

Але, як я вже казав, є деякі обмеження, і серед вільного використання різноманітної невизначеної поведінки затесалася особливість неприємніше - JavaScript by design не підтримує багатопоточність із загальною пам'яттю. У принципі це зазвичай можна навіть назвати непоганою ідеєю, але не для портування коду, чия архітектура зав'язана на сишні потоки. Взагалі кажучи, Firefox йдуть експерименти з підтримки shared workers, і реалізація pthread для них в Emscripten є, але залежати від цього не хотілося. Довелося потихеньку викорчовувати багатопоточність з коду Qemu - тобто шукати, де запускаються потоки, виносити тіло циклу, що виконується в цьому потоці в окрему функцію, і по черзі викликати такі функції з основного циклу.

друга спроба

Якоїсь миті стало зрозуміло, що віз і нині там, і що безсистемне розпихування милиць за кодом до добра не доведе. Висновок: треба якось систематизувати процес додавання милиць. Тому було взято свіжу на той момент версію 2.4.1 (не 2.5.0, тому що, мало, там виявляться ще не відловлені баги нової версії, а мені і своїх багів вистачить), і насамперед був безпечно переписаний thread-posix.c. Ну як безпечним: якщо хтось намагався виконати операцію, що призводить до блокування, відразу викликалася функція abort() — звичайно, це не вирішувало одразу всіх проблем, але, як мінімум, це якось приємніше, ніж тихо отримувати неконсистентність даних.

Взагалі, в портуванні коду на JS дуже допомагають опції Emscripten -s ASSERTIONS=1 -s SAFE_HEAP=1 — вони відловлюють деякі види undefined behavior на зразок звернень за не вирівняною адресою (що зовсім не узгоджується з кодом для typed arrays на кшталт HEAP32[addr >> 2] = 1) або виклик функції з неправильною кількістю аргументів.

До речі, помилки вирівнювання – окрема тема. Як я вже казав, у Qemu є "вироджений" інтерпретуючий бекенд кодогенерації TCI (tiny code interpreter), і щоб зібрати та запустити Qemu на новій архітектурі, якщо пощастить, достатньо компілятора C. "якщо пощастить". Мені ось не пощастило, і виявилося, що TCI при розборі свого байт-коду використовує не вирівняний доступ. Тобто на будь-яких там ARM та інших архітектурах з обов'язково вирівняним доступом Qemu компілюється тому, що для них є нормальний TCG-бекенд, що генерує нативний код, а чи запрацює на них TCI — це ще питання. Втім, як виявилось, у документації на TCI щось подібне явно вказувалося. У результаті в код були додані дзвінки функцій для не вирівняного читання, які з'явилися в іншій частині Qemu.

Руйнування купи

У результаті, не вирівняний доступ у TCI було виправлено, зроблено головний цикл, що по черзі викликав процесор, RCU і щось по дрібниці. І ось я запускаю Qemu з опцією -d exec,in_asm,out_asm, Що означає, що треба говорити, які блоки коду виконуються, і навіть у момент трансляції писати, який гостьовий код був, який хостовий код став (у разі, байткод). Воно запускається, виконує кілька блоків трансляції, пише залишене мною налагоджувальне повідомлення, що зараз запуститься RCU і… падає по abort() всередині функції free(). Шляхом колупання функції free() вдалося з'ясувати, що в заголовку блоку купи, який лежить у восьми байтах, що передують виділеній пам'яті, замість розміру блоку або чогось подібного виявилося сміття.

Руйнування купи — як мило… У такому разі є корисний засіб — із (по можливості) тих же вихідників зібрати нативний бінарник і прогнати під Valgrind. За деякий час бінарник був готовий. Запускаю з тими ж опціями - падає ще на ініціалізації, не дійшовши до, власне, виконання. Неприємно, звичайно - мабуть, вихідники були не зовсім ті ж, що не дивно, адже configure розвідав дещо інші опції, але в мене ж є Valgrind - спочатку цю багу полагоджу, а потім, якщо пощастить, і вихідна виявиться. Запускаю все те саме під Valgrind ... Ы-и-и, у-у-у, е-е-е, воно запустилося, нормально пройшло ініціалізацію і пішло далі повз вихідний баг без єдиного попередження про неправильний доступ до пам'яті, не кажучи вже про падіння. До такого життя мене, як кажуть, не готувала — програма, що падає, перестає падати при запуску під валгріндом. Що це було – загадка. Моя гіпотеза, що раз на околицях поточної інструкції після падіння під час ініціалізації gdb показував роботу memset-а з валідним покажчиком з використанням чи mmx, чи xmm регістрів, то, можливо, це була якась помилка вирівнювання, хоча все одно віриться слабко.

О-Кей, Valgrind тут, схоже, не помічник. І ось тут почалося найнеприємніше — все, начебто, навіть запускається, але падає з абсолютно невідомих причин через подію, яка могла статися мільйони інструкцій тому. Довгий час навіть підступитись було незрозуміло як. Зрештою довелося все-таки сісти та налагоджувати. Друк того, чим було переписано заголовок, показало, що це схоже не на число, а швидше на якісь бінарні дані. І, диво, цей бінарний рядок знайшовся у файлі з біосом — тобто тепер можна було з достатньою впевненістю сказати, що це було переповнення буфера, і навіть зрозуміло, що в цей буфер записувалося. Ну а далі якось так — в Emscripten, на щастя, рандомізації адресного простору немає, дірок у ньому теж немає, тому можна написати десь у середині коду виведення даних за вказівником з минулого запуску, подивитися на дані, подивитися на покажчик, і , якщо той не змінився, отримати інформацію для роздумів. Щоправда, на лінковку після будь-якої зміни витрачається кілька хвилин, але що поробиш. В результаті було знайдено конкретний рядок, що копіює BIOS з тимчасового буфера в гостьову пам'ять - і, дійсно, в буфері не вистачило достатнього місця. Пошук джерела тієї дивної адреси буфера привів у функцію qemu_anon_ram_alloc у файлі oslib-posix.c — логіка там була така: іноді може бути корисно вирівняти адресу за huge page розміром 2 Мб, для цього попросимо у mmap спочатку трохи більше, а потім зайве повернемо за допомогою munmap. А якщо таке вирівнювання не потрібне, то вкажемо замість 2 Мб результат getpagesize() - mmap все ж таки видасть вирівняну адресу… Так ось у Emscripten mmap просто викликає malloc, А той, звичайно, по сторінці не вирівнює. Загалом, бага, яка засмучувала мене пару місяців, виправилася зміною в двох рядках.

Особливості виклику функцій

І ось уже процесор щось вважає, Qemu не падає, але екран не вмикається, і процесор швидко зациклюється, судячи з висновку -d exec,in_asm,out_asm. З'явилася гіпотеза: не приходять переривання таймера (ну чи взагалі переривання). І справді, якщо від нативного складання, яке чомусь працювало, відкрутити переривання, виходить схожа картина. Але розгадка виявилася зовсім не в цьому: порівняння трасування, виданих із зазначеною вище опцією, показало, що траєкторії виконання розходяться дуже рано. Тут треба сказати, що порівняння записаного за допомогою запускатора emrun налагоджувального висновку з виведенням нативного складання — не зовсім механічний процес. Не знаю точно, як запущена у браузері програма з'єднується з emrunАле деякі рядки у висновку виявляються переставлені місцями, тому різниця в диффе - це ще не привід вважати, що траєкторії розійшлися. Загалом стало зрозуміло, що за інструкцією ljmpl відбувається перехід за різними адресами, та й байткод генерується принципово різний: в одному є інструкція виклику сишной функції-хелпера, в іншому - ні. Після ковтання інструкції та вивчення коду, який ці інструкції транслює, стало зрозуміло, що, по-перше, безпосередньо до неї в регістр cr0 проводилася запис - теж за допомогою хелпера, що переводить процесор в захищений режим, а по-друге, що js-версія в захищений режим так і не перейшла. А річ у тому, що ще однією особливістю Emscripten є небажання терпіти код на кшталт реалізації інструкції. 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 kernel на консоль з опцією -nographic, а з браузера жодного висновку до терміналу, звідки був запущений emrun, Не приходило. Тобто незрозуміло: процесор не працює чи виведення графіки. А потім мені спало на думку трохи почекати. Виявилося, що "процесор не спить, а просто повільно моргає", і хвилин за п'ять ядро ​​викинуло на консоль пачку повідомлень і пішло виснути далі. Стало зрозуміло, що процесор загалом працює, і копати потрібно в коді роботи з SDL2. Користуватися цією бібліотекою я, на жаль, не вмію, тому подекуди довелося діяти навмання. Якоїсь миті на екрані промайнув рядок parallel0 на синьому тлі, що наводило на деякі думки. У результаті виявилося, що справа була в тому, що Qemu відкриває кілька віртуальних вікон в одному фізичному вікні, між якими можна перемикатися по Ctrl-Alt-n: у нативному складанні воно працює, Emscripten - ні. Після позбавлення від зайвих вікон за допомогою опцій -monitor none -parallel none -serial none і вказівки примусово перемальовувати весь екран на кожному кадрі все раптово запрацювало.

Корутини

Отже, емуляція в браузері працює, але нічого цікавого однодискетного в ній не запустити, тому що немає блокового виводу-введення-потрібно реалізовувати підтримку корутин. У Qemu вже є кілька coroutine backend-ів, але через особливості JavaScript і кодогенератора Emscripten не можна просто взяти і почати жонглювати стеками. Здавалося б, "все зникло, гіпс знімають", але розробники Emscripten вже потурбувалися про все. Реалізовано це досить смішно: а давайте назвемо підозрілим виклик функції на кшталт emscripten_sleep та кількох інших, які використовують механізм Asyncify, а також виклики за вказівником та виклики будь-якої функції, де нижче за стеком може статися один із попередніх двох випадків. А тепер перед кожним підозрілим викликом виділимо async context, а відразу після виклику - перевіримо, чи не відбувся асинхронний виклик, і якщо стався, то збережемо всі локальні змінні в цей async context, вкажемо, на яку функцію передавати управління, коли потрібно буде продовжити виконання і вийдемо з поточної функції. Ось де простір для вивчення ефекту розтарощування - Для потреб продовження виконання коду після повернення з асинхронного виклику компілятор генерує "обрубки" функції, що починаються після підозрілого виклику - ось так: якщо є n підозрілих викликів, то функція буде розірвана десь у n/2 разів - це ще, якщо не враховувати, що у вихідну функцію потрібно після кожного потенційно асинхронного виклику додати збереження частини локальних змінних. Згодом навіть довелося писати нехитрий скрипт на Пітоні, який за заданою множиною особливо розбещених функцій, які, ймовірно, "не пропускають асинхронність крізь себе" (тобто в них не спрацьовує розкрутка стека і все те, що я щойно описав), вказує, виклики через покажчики у яких функціях потрібно ігнорувати компілятору, щоб ці функції не розглядалися як асинхронні. А то JS-файли під 60 Мб - це вже явно перебір - нехай вже хоча б 30. Хоча, якось я налаштовував складальний скрипт, і випадково викинув опції лінковника, серед яких була і -O3. Запускаю згенерований код і Chromium вижирає пам'ять і падає. Я потім випадково подивився на те, що він намагався завантажити… Ну, що я можу сказати, я теж завис, якби мене попросили вдумливо вивчити і оптимізувати джаваскрипт на 500+ Мб.

На жаль, перевірки в коді бібліотеки підтримки Asyncify не зовсім дружили з longjmp-ами, які використовуються в коді віртуального процесора, але після невеликого патчу, що відключає ці перевірки і примусово відновлює контексти так, начебто все добре, код запрацював. І тут почалося дивне: іноді спрацьовували перевірки в коді синхронізації — ті, що аварійно завершують код, якщо за логікою виконання він має заблокуватися — хтось намагався захопити вже захоплений м'ютекс. На щастя, це виявилася не логічною проблемою в серіалізованому коді — просто я використовував штатну функціональність main loop, що надається Emscripten, але іноді асинхронний виклик повністю розгортав стек, а в цей момент спрацьовував setTimeout від main loop - таким чином код заходив в ітерацію головного циклу, не вийшовши з попередньої ітерації. Переписав на нескінченному циклі та emscripten_sleep, та проблеми з м'ютексами припинилися. Код навіть логічніше став — адже, по суті, я не маю якогось коду, який готує черговий кадр анімації — просто процесор щось вважає і екран періодично оновлюється. Втім, проблеми на цьому не припинилися: іноді виконання Qemu просто тихо завершувалося без будь-яких винятків і помилок. У той момент я на це забив, але, забігаючи наперед, скажу, що проблема була ось у чому: код корутин, насправді, взагалі не використовує setTimeout (ну або принаймні не так часто, як можна подумати): функція emscripten_yield просто виставляє прапорець асинхронного дзвінка. Вся сіль у тому, що emscripten_coroutine_next не є асинхронною функцією: у собі вона перевіряє прапорець, скидає його і передає управління куди треба. Тобто на ній розкручування стека закінчується. Проблема була в тому, що через use-after-free, який виявлявся при відключеному пулі корутин через те, що я не докопіював важливу позицію коду з існуючого coroutine backend, функція qemu_in_coroutine повертала true, коли насправді мала повернути false. Це призводило до виклику emscripten_yield, вище якого за стеком не було emscripten_coroutine_next, Стек розгортався до самого верху, але ніяких setTimeoutЯк я вже казав, не виставлялося.

Кодогенерація JavaScript

А ось, власне, і обіцяне "провертання фаршу назад". Насправді ні. Звичайно, якщо запустити в браузері Qemu, а в ньому - Node.js, то, природно, після кодогенерації Qemu ми отримаємо зовсім не той JavaScript. Але все-таки, якесь, а зворотне перетворення.

Спочатку трохи про те, як працює Qemu. Відразу прошу мене пробачити: я не професійний розробник Qemu і мої висновки можуть бути місцями помилкові. Як кажуть, "думка студента не повинна співпадати з думкою викладача, аксіоматикою Пеано та здоровим глуздом". У Qemu є кілька підтримуваних гостьових архітектур і для кожної є каталог начебто target-i386. При складанні можна вказати підтримку кількох гостьових архітектур, але в результаті вийде кілька бінарників. Код підтримки гостьової архітектури, своєю чергою, генерує деякі внутрішні операції Qemu, які TCG (Tiny Code Generator) вже перетворює на машинний код хостової архітектури. Як стверджується в readme-файлі, що лежить в каталозі tcg, це була частина звичайного компілятора C, яку потім пристосували під JIT. Тому, наприклад, target architecture у термінах цього документа – це вже не гостьова, а хостова архітектура. У якийсь момент з'явився ще один компонент - Tiny Code Interpreter (TCI), який повинен виконувати код (практично ті ж внутрішні операції) без кодогенератора під конкретну хостову архітектуру. Насправді, як мовиться в його документації, цей інтерпретатор може не завжди працювати так само добре, як JIT-кодогенератор не лише кількісно у плані швидкості, а й якісно. Хоча не впевнений, що його опис є цілком актуальним.

Спочатку я намагався зробити повноцінний TCG backend, але швидко заплутався у вихідниках та не цілком зрозумілому описі інструкцій байткоду, тому вирішив обернути інтерпретатор TCI. Це дало відразу кілька переваг:

  • при реалізації кодогенератора можна було дивитися не в опис інструкцій, а в код інтерпретатора
  • можна генерувати функції не для кожного зустрінутого блоку трансляції, а наприклад, тільки після сотого виконання
  • у разі зміни згенерованого коду (а таке, мабуть, можливо, судячи з функцій з назвами, що містять слово patch), мені потрібно буде інвалідувати згенерований JS-код, але у мене хоча б буде, з чого його перегенерувати

На рахунок третього пункту не впевнений, що патчінг можливий після того, як код буде вперше виконаний, але й перших двох пунктів достатньо.

Спочатку код генерувався у вигляді великого switch за адресою вихідної інструкції байткоду, але потім, згадавши статтю про Emscripten, оптимізацію JS і relooping, що генерується, вирішив генерувати більш людський код, тим більше, що емпірично виходило, що єдина точка входу в блок трансляції — це його початок. Сказано - зроблено, через деякий час вийшов кодогенератор, що генерує код з if-ами (хоч і без циклів). Але невдача, він падав, видаючи повідомлення про те, що інструкція виявилася якоюсь неправильною довжиною. При цьому остання інструкція на цьому рівні рекурсії була brcond. Добре, додам ідентичну перевірку в генерацію цієї інстукції до рекурсивного виклику і після і ... не одна з них не виконалася, але після свіча по assert-у все ж таки впали. Зрештою, вивчивши згенерований код, я зрозумів, що після switch-а покажчик на поточну інструкцію перезавантажується зі стека і, ймовірно, перетирається JavaScript-кодом, що генерується. Так воно й виявилось. Збільшення буфера з одного мегабайта до десяти ні до чого не призвело, і стало зрозумілим, що кодогенератор бігає по колу. Довелося перевіряти, що ми не вийшли за межі поточного ТБ, і якщо вийшли, то видавати адресу наступного ТБ зі знаком мінус, щоб можна було продовжити виконання. До того ж це вирішує проблему "які згенеровані функції інвалідувати, якщо змінився цей шматочок байткоду?" - Інвалідувати потрібно тільки ту функцію, яка відповідає цьому блоку трансляції. До речі, хоча налагоджував я все в Chromium (оскільки користуюся Firefox і мені простіше використовувати окремий браузер для експериментів), але Firefox мені допоміг виправити несумісності зі стандартом asm.js, після чого код став швидше працювати в Хроміумі.

Приклад генерованого коду

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-бітових гостьових архітектур: ви прямо зараз можете помилуватися на Лінукс, що висне в браузері на етапі завантаження, для архітектури MIPS

Що ще можна зробити

  • Прискорити емуляцію. Навіть у режимі JIT воно працює, схоже, повільніше, ніж Virtual x86 (зате потенційно є цілий Qemu з великою кількістю емульованого заліза та архітектур)
  • Зробити нормальний інтерфейс - веб-розробник з мене, прямо скажемо, так собі, тому поки що переробив стандартну оболонку Emscripten, як зумів
  • Спробувати запустити складніші функції Qemu - мережу, міграцію VM і т.д.
  • UPD: треба буде віддати в апстрім Emscripten свої нечисленні напрацювання та баг-репорти, як це робили попередні портувальники Qemu та інших проектів. Дякуємо їм за те, що була можливість неявно користуватися їхнім вкладом у Emscripten у рамках свого завдання.

Джерело: habr.com

Додати коментар або відгук