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. Деякі, такі як Script Type, спочатку задумувалися як найкращий спосіб писати для інтернету. У той же час, Emscripten — це спосіб взяти існуючий код на C чи C++, і скомпілювати його у вигляд, що зрозумілий браузеру. цій сторінці зібрано чимало портів відомих програм: тут, наприклад, можна подивитися на PyPy — до речі, як стверджується, у них вже є JIT. Насправді, не будь-яку програму можна просто скомпілювати і запустити в браузері — є низка особливостей, з якими доводиться миритися, втім, як свідчить напис на цій же сторінці "Emscripten can be used to compile almost any portable C/C++ code to JavaScript". Тобто існує ряд операцій, які є невизначеним поведінкою за стандартом, але зазвичай працюють на x86 — приміром, невыровненный доступ до змінних, який на деяких архітектурах взагалі заборонений. Загалом, Qemu — платформна програма і хотілося вірити, і так не містить великої кількості неопредленного поведінки — бери і компилируй, потім трохи повозитися з JIT — і готове! Але не тут-то було...
Перша спроба
Взагалі кажучи, я не перший, кому прийшла в голову ідея перенести Qemu на JavaScript. На форумі ReactOS задавалося питання, чи можливо це з допомогою Emscripten. Ще раніше ходили чутки, що це зробив особисто Фабріс Беллар, але мова йшла про jslinux, який, наскільки мені відомо, є якраз спробою вручну домогтися на JS достатньої продуктивності, і написаний з нуля. Пізніше був написаний Virtual x86 — до нього були викладені необфусцированные исходники, і, як затверджувалась, велика "реалістичність" емуляції дозволила використовувати SeaBIOS як firmware. Крім того, була як мінімум одна спроба перенести Qemu з допомогою Emscripten — це намагався зробити socketpair, але розробка, наскільки я зрозумів, була заморожена.
Отже, здавалося б, ось исходники, ось 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>
В результаті, нарешті підвернувся привід спробувати в роботі бібліотеки pyparsing, і був написаний скрипт генерує обгортки рівно ті і рівне для тих функцій, для яких потрібно.
І ось, після цього процесор начебто заробив. Начебто, тому що екран так і не инициализировался, хоча в нативної збірці вдалося запустити 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-кодом. Так воно і виявилося. Збільшення буфера з одного мегабайта до десяти ні до чого не призвело, і стало зрозуміло, що кодогенератор бігає по колу. Довелося перевіряти, що ми не вийшли за межі поточного TB, і якщо вийшли, то видавати адреса наступного TB зі знаком мінус, щоб можна було продовжити виконання. До того ж це вирішує проблему "які згенеровані функції инвалидировать, якщо змінився ось цей шматочок байткода?" — инвалидировать потрібно тільки ту функцію, яка відповідає цьому блоку трансляції. До речі, хоча отлаживал я все 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 і т. д.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.