JIT колдоосу менен Qemu.js: сиз дагы эле фаршты артка бура аласыз

Бир нече жыл мурун Фабрис Беллард jslinux тарабынан жазылган JavaScript менен жазылган PC эмулятору. Андан кийин жок дегенде дагы көп болду Виртуалдык x86. Бирок алардын баары, менин билишимче, котормочулар болгон, ал эми Qemu, ошол эле Фабрис Беллард тарабынан бир топ мурда жазылган жана, кыязы, ар кандай өзүн сыйлаган заманбап эмулятор, конок кодун 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 - аны алып, түзүңүз. Бирок ошондой эле Кэму көз каранды болгон китепканалар жана ошол китепканалар көз каранды болгон китепканалар жана башкалар бар жана алардын бири libffi, кайсы глибден көз каранды. Интернетте Emscripten үчүн китепкана портторунун чоң коллекциясында бирөө бар деген имиштер бар болчу, бирок ага ишенүү кыйын: биринчиден, ал жаңы компилятор болуу үчүн арналган эмес, экинчиден, ал өтө төмөн деңгээлдеги программа болгон. китепкананы алып, JSге компиляциялоо үчүн. Жана бул жөн гана монтаждык кыстармалар маселеси эмес - балким, эгер сиз аны бурсаңыз, кээ бир чакыруу конвенциялары үчүн стекте керектүү аргументтерди жаратып, аларсыз функцияны чакыра аласыз. Бирок Emscripten татаал нерсе: түзүлгөн код браузердин JS кыймылдаткычынын оптимизаторуна тааныш болушу үчүн, кээ бир амалдар колдонулат. Тактап айтканда, кайра айлануу деп аталган - кээ бир абстракттуу өткөөл инструкциялары менен алынган LLVM IRди колдонгон код генератору ишенүүгө татыктуу ifs, циклдерди жана башкаларды кайра жаратууга аракет кылат. Аргументтер функцияга кантип берилет? Албетте, JS функцияларына аргумент катары, башкача айтканда, мүмкүн болсо, стек аркылуу эмес.

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

Бирок, мен буга чейин айткандай, кээ бир чектөөлөр бар жана ар кандай аныкталбаган жүрүм-турумду бекер колдонуунун арасында дагы бир жагымсыз өзгөчөлүк жашырылган - дизайн боюнча JavaScript жалпы эс тутум менен көп агымды колдобойт. Негизи, муну адатта жакшы идея деп атоого болот, бирок архитектурасы C жиптерине байланган кодду көчүрүү үчүн эмес. Жалпысынан алганда, Firefox жалпы жумушчуларды колдоо менен эксперимент жүргүзүп жатат жана Emscripten алар үчүн pthread ишке ашырууга ээ, бирок мен ага көз каранды болгум келген жок. Мен 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, ал башталып, кадимкидей инициализациядан өттү жана кулаганды айтпаганда да, туура эмес эстутумга жетүү жөнүндө бир да эскертүүсүз баштапкы катадан өттү. Жашоо, алар айткандай, мени буга даярдаган жок - кыйроого учураган программа Уолгринддин тушунда ишке киргенде иштебей калат. Бул эмне болгон табышмак. Менин гипотеза боюнча, инициализация учурунда кыйроодон кийин учурдагы инструкциянын жанында бир жолу gdb ишин көрсөттү. memset-a же колдонуучу жарактуу көрсөткүч менен mmx, же xmm регистрлер, анда балким, бул кандайдыр бир тегиздөө катасы болгондур, бирок ага ишенүү дагы деле кыйын.

Макул, Valgrind бул жерде жардам бербейт окшойт. Бул жерде эң жийиркеничтүү нерсе башталды - баары башталып жаткандай сезилет, бирок миллиондогон көрсөтмөлөр мурун болушу мүмкүн болгон окуядан улам такыр белгисиз себептерден улам бузулат. Көптөн бери кантип жакындаш керек да белгисиз. Акыр-аягы, мен дагы эле отуруп, каталарды оңдоого туура келди. Баш аты эмне менен кайра жазылганын басып чыгаруу ал санга окшош эмес, экилик маалыматтарга окшош экенин көрсөттү. Мына, мына, бул экилик сап 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 версиясы эч качан корголгон режимге өтпөгөнүн жазуу, ошондой эле жардамчы аркылуу жасалган. Бирок, 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 блогунун I/O коду корутиндерде жазылганын тактоо керек. Emscriptenдин өзүнүн өтө татаал ишке ашырылышы бар, бирок ал дагы эле Qemu кодунда колдоого алынышы керек болчу жана сиз азыр процессорду оңдоого болот: Qemu параметрлерди колдойт -kernel, -initrd, -append, анын жардамы менен сиз Linux же, мисалы, memtest86+, блоктук түзмөктөрдү такыр колдонбостон жүктөй аласыз. Бирок бул жерде көйгөй бар: түпнуска ассамблеяда консолго Linux ядросунун чыгышын көрүүгө болот. -nographic, жана браузерден ал ишке киргизилген жерден терминалга эч кандай чыгуу жок emrun, келген жок. Башкача айтканда, бул түшүнүксүз: процессор иштебей жатат же графикалык чыгаруу иштебей жатат. Анан бир аз күтө кетейин деген ой келди. Көрсө, "процессор уктап жаткан жок, жөн гана жай ирмеп жатат" жана беш мүнөттөн кийин ядро ​​​​консолго бир топ билдирүүлөрдү ыргытып, илинип кала берген. Процессор жалпысынан иштей турганы айкын болду жана биз SDL2 менен иштөө үчүн кодду казып алышыбыз керек. Тилекке каршы, мен бул китепкананы кантип колдонууну билбейм, андыктан кээ бир жерлерде туш келди аракет кылууга туура келди. Кайсы бир убакта көк фондо экранда параллель0 сызыгы жарк этти, бул кандайдыр бир ойлорду сунуштады. Акыр-аягы, көйгөй Qemu бир физикалык терезеде бир нече виртуалдык терезелерди ачканда, алардын ортосунда Ctrl-Alt-n аркылуу которула аласыз: ал жергиликтүү түзүлүштө иштейт, бирок Emscriptenде эмес. Параметрлерди колдонуу менен керексиз терезелерден арылуудан кийин -monitor none -parallel none -serial none жана ар бир кадрга бүт экранды күч менен кайра тартуу боюнча көрсөтмөлөр, баары күтүлбөгөн жерден иштеди.

Корутиндер

Ошентип, браузерде эмуляция иштейт, бирок анда сиз эч кандай кызыктуу бир дискетти иштете албайсыз, анткени I/O блогу жок - сиз корутиндерди колдоону ишке ашырууңуз керек. 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, жана mutexes менен көйгөйлөр токтоду. Код ого бетер логикалуу болуп калды - чындыгында, менде кийинки анимация кадрын даярдай турган код жок - процессор жөн гана бир нерсени эсептейт жана экран мезгил-мезгили менен жаңыланып турат. Бирок, көйгөйлөр ушуну менен эле токтоп калган жок: кээде Qemu аткаруу эч кандай өзгөчөлүктөр же каталарсыз унчукпай токтоп калат. Ошол учурда мен андан баш тарттым, бирок, алдыга карап, мен көйгөй бул жерде деп айтам: корутин коду, чындыгында, колдонбойт. setTimeout (же жок дегенде сиз ойлогондой көп эмес): функция emscripten_yield жөн гана асинхрондук чакыруу желегин орнотот. Кептин баары мына ушунда emscripten_coroutine_next асинхрондук функция эмес: ичине ал желекти текшерип, аны баштапкы абалга келтирет жана башкарууну керектүү жерге өткөрүп берет. Башкача айтканда, стекти жылдыруу ошол жерде бүтөт. Маселе, корутиндик бассейн өчүрүлгөндө пайда болгон free-after-функциясына байланыштуу, мен учурдагы coroutine backendден коддун маанилүү сабын көчүрүп албагандыктан, функция qemu_in_coroutine Чындыгында ал жалган деп кайтарылышы керек болчу. Бул чакырууга алып келди emscripten_yield, анын үстүндө стек эч ким жок болчу emscripten_coroutine_next, стек эң чокусуна чейин ачылды, бирок жок setTimeout, буга чейин айткандай, көргөзмөгө коюлган эмес.

JavaScript кодун түзүү

Бул жерде, чындыгында, убада кылынган "фаршты артка кайтаруу". Жок эле. Албетте, эгер биз Qemu браузеринде жана андагы Node.js иштетсек, анда Qemu коддорун түзгөндөн кийин, албетте, JavaScript таптакыр туура эмес болуп калат. Бирок дагы эле кандайдыр бир тескери трансформация.

Биринчиден, Qemu кантип иштээри жөнүндө бир аз. Сураныч, мени дароо кечириңиз: мен Qemu программасынын профессионал иштеп чыгуучусу эмесмин жана менин корутундуларым кээ бир жерлерде ката болушу мүмкүн. Алар айткандай, "окуучунун пикири мугалимдин пикири, Пеанонун аксиоматикасы жана акыл-эси менен дал келбеши керек". Qemu колдоого алынган конок архитектураларынын белгилүү санына ээ жана ар бири үчүн окшош каталог бар target-i386. Куруп жатканда, сиз бир нече конок архитектурасына колдоо көрсөтсөңүз болот, бирок натыйжада бир нече бинардык болот. Конок архитектурасын колдоо коду, өз кезегинде, TCG (Tiny Code Generator) хост архитектурасы үчүн машина кодуна айланган кээ бир ички Qemu операцияларын жаратат. tcg каталогунда жайгашкан readme файлында айтылгандай, бул алгач JIT үчүн ылайыкташтырылган кадимки C компиляторунун бир бөлүгү болгон. Ошондуктан, мисалы, бул документтин жагынан максаттуу архитектура мындан ары конок архитектурасы эмес, хост архитектурасы. Кайсы бир учурда, дагы бир компонент пайда болду - Tiny Code Interpreter (TCI), ал белгилүү бир хост архитектурасы үчүн код генератору жок болгон учурда кодду (дээрлик бирдей ички операцияларды) аткарышы керек. Чынында, анын документтеринде айтылгандай, бул котормочу дайыма эле JIT кодунун генератору сыяктуу эле, ылдамдык жагынан гана эмес, сапаттык жактан да жакшы аткара бербейт. Мен анын сүрөттөлүшү толугу менен тиешелүү экенине ишенбесем да.

Адегенде мен толук кандуу TCG бэкэндин түзүүгө аракет кылдым, бирок булак кодунда жана байт-код көрсөтмөлөрүнүн так эмес сүрөттөлүшүнө бат эле чаташып кеттим, ошондуктан TCI котормочусун ороп коюуну чечтим. Бул бир нече артыкчылыктарды берди:

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

Үчүнчү пунктка келсек, код биринчи жолу аткарылгандан кийин жамоо мүмкүн экенине ишенбейм, бирок биринчи эки пункт жетиштүү.

Башында, код баштапкы байт-код нускамасынын дареги боюнча чоң которгуч түрүндө түзүлгөн, бирок андан кийин Emscripten, түзүлгөн JSти оптималдаштыруу жана кайра иштетүү жөнүндө макаланы эстеп, мен көбүрөөк адам кодун жаратууну чечтим, айрыкча эмпирикалык түрдө ал котормо блогуна бирден-бир кирүү чекити анын башталышы экени белгилүү болду. Бир аздан кийин бизде ifs менен код жараткан код генератору пайда болду (илмексиз болсо да). Бирок ийгиликсиздиктен, ал кыйрап, көрсөтмөлөр туура эмес узундукта болгон деген кабарды берди. Мындан тышкары, бул рекурсия деңгээлиндеги акыркы нускама болгон brcond. Макул, мен рекурсивдүү чалууга чейин жана андан кийин бул нускаманын муунга окшош текшерүүнү кошом жана... алардын бири да аткарылган жок, бирок assert которулгандан кийин алар дагы эле ишке ашпай калды. Акырында, түзүлгөн кодду изилдеп чыккандан кийин, которуштуруудан кийин, учурдагы нускамага көрсөткүч стектен кайра жүктөлөрүн жана, балким, түзүлгөн 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-бит конок архитектураларын чогултуу үчүн шаблон бар: азыр сиз жүктөө стадиясында браузерде тоңуп калган MIPS архитектурасына Linux суктансаңыз болот.

Дагы эмне кыла аласың

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

Source: www.habr.com

Комментарий кошуу