JIT дэмжлэгтэй Qemu.js: та татсан махаа хойш эргүүлж болно

Хэдэн жилийн өмнө Фабрис Беллард jslinux бичсэн нь JavaScript дээр бичигдсэн компьютерийн эмулятор юм. Үүний дараа наад зах нь илүү их байсан Виртуал x86. Гэхдээ миний мэдэж байгаагаар тэд бүгд орчуулагч байсан бол Кэму нь ижил Фабрис Беллардын бичсэн бөгөөд магадгүй өөрийгөө хүндэтгэдэг орчин үеийн эмулятор нь JIT-ийн зочдын кодыг хост системийн код болгон ашигладаг. Хөтөчүүдийн шийддэгтэй холбоотой эсрэг даалгаврыг хэрэгжүүлэх цаг нь болсон юм шиг санагдлаа: JIT машин кодыг JavaScript-д эмхэтгэх, үүний тулд Qemu порт хийх нь хамгийн логик юм шиг санагдсан. Qemu-д илүү энгийн бөгөөд хэрэглэгчдэд ээлтэй эмуляторууд байдаг - жишээлбэл, ижил VirtualBox - суулгаж, ажилладаг бололтой. Гэхдээ Кему хэд хэдэн сонирхолтой онцлогтой

  • нээлттэй эх сурвалж
  • цөмийн драйвергүйгээр ажиллах чадвар
  • орчуулагчийн горимд ажиллах чадвар
  • олон тооны хост болон зочдын архитектурыг дэмжих

Гурав дахь цэгийн тухайд би одоо тайлбарлаж болно, үнэндээ 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-ийг програм хангамж болгон ашиглах боломжтой болгосон. Нэмж дурдахад, Kemu-г Emscripten ашиглан порт хийх гэж ядаж нэг удаа оролдсон - би үүнийг хийхийг оролдсон залгуур, гэхдээ миний ойлгосноор хөгжил царцсан байсан.

Тиймээс, эх сурвалжууд энд байна, энд Эмскриптен байна - үүнийг аваад эмхэтгээрэй. Гэхдээ Кэмүгийн хамааралтай номын сангууд, тэдгээр номын сангаас хамаардаг номын сангууд гэх мэт байдаг бөгөөд тэдгээрийн нэг нь libffi, аль glib хамаарна. Интернетэд Emscripten-д зориулсан номын сангуудын томоохон цуглуулгад байдаг гэсэн цуу яриа гарч байсан ч үүнд итгэхэд хэцүү байсан: нэгдүгээрт, энэ нь шинэ хөрвүүлэгч байхаар төлөвлөөгүй, хоёрдугаарт, энэ нь хэтэрхий доогуур түвшний програм байсан юм. Номын санг аваад JS рүү хөрвүүлнэ. Энэ нь зөвхөн угсралтын оруулгатай холбоотой асуудал биш юм - хэрэв та үүнийг мушгивал зарим дуудлагын конвенцид шаардлагатай аргументуудыг стек дээр үүсгэж, функцийг түүнгүйгээр дуудаж болно. Гэхдээ Emscripten бол төвөгтэй зүйл юм: үүсгэсэн кодыг JS хөдөлгүүрийг оновчтой болгох хөтөч дээр танил болгохын тулд зарим заль мэхийг ашигладаг. Ялангуяа дахин давталт гэж нэрлэгддэг - хүлээн авсан LLVM IR-г ашиглан зарим хийсвэр шилжилтийн заавар бүхий код үүсгэгч нь боломжит if, гогцоо гэх мэтийг дахин үүсгэхийг оролддог. За, аргументуудыг функцэд хэрхэн дамжуулдаг вэ? Мэдээжийн хэрэг, JS функцүүдийн аргументууд, өөрөөр хэлбэл боломжтой бол стекээр дамждаггүй.

Эхэндээ зүгээр л JS-тэй libffi-г орлуулж бичээд стандарт тест хийх санаа байсан боловч эцэст нь би толгой файлуудаа одоо байгаа кодтой ажиллахын тулд хэрхэн хийх талаар эргэлзэж байсан - би юу хийж чадах вэ? Тэдний хэлснээр "Бид ийм тэнэг юм уу?" Даалгаврууд тийм төвөгтэй байдаг гэж үү? Би libffi-г өөр архитектурт порт хийх шаардлагатай болсон тул аз болоход Emscripten нь шугаман угсралтын макро (Javascript-д, тийм ээ - ямар ч архитектур, тиймээс ассемблер) хоёуланг нь хоёуланг нь агуулж байгаа бөгөөд шууд үүсгэсэн кодыг ажиллуулах чадвартай. Ерөнхийдөө, платформоос хамааралтай libffi фрагментүүдтэй хэсэг хугацааны турш эргэлдсэний дараа би хөрвүүлж болох кодыг авч, тааралдсан анхны тест дээрээ ажиллуулсан. Туршилт амжилттай болсон нь намайг гайхшрууллаа. Миний суут ухаанд гайхсан - хошигнол биш, энэ нь анхны хөөргөлтөөс л ажилласан - би нүдэндээ ч итгэсэнгүй, гарч ирсэн кодыг дахин харж, дараа нь хаана ухахаа дүгнэхээр явлаа. Энд би хоёр дахь удаагаа галзуурсан - миний үйл ажиллагааны цорын ганц зүйл бол ffi_call - Энэ дуудлага амжилттай болсон гэж мэдээлсэн. Өөрөө дуудлага байгаагүй. Тиймээс би анхны татах хүсэлтээ илгээсэн бөгөөд энэ нь аливаа олимпиадын сурагчдад ойлгомжтой тестийн алдааг зассан - бодит тоог харьцуулж болохгүй. a == b тэр ч байтугай яаж a - b < EPS - та модулийг бас санах хэрэгтэй, эс тэгвээс 0 нь 1/3-тай тэнцүү байх болно ... Ерөнхийдөө би хамгийн энгийн туршилтуудыг давдаг, glib-тэй тодорхой libffi порттой болсон. эмхэтгэсэн - Би үүнийг шаардлагатай гэж шийдсэн, би үүнийг дараа нэмнэ. Урагшаа харахад хөрвүүлэгч нь 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 backend-тэй байдаг, гэхдээ TCI тэдгээр дээр ажиллах эсэх нь өөр асуулт юм. Гэсэн хэдий ч TCI баримт бичигт үүнтэй төстэй зүйлийг тодорхой зааж өгсөн байна. Үүний үр дүнд Qemu-ийн өөр хэсэгт нээсэн кодонд жигд бус унших функцийн дуудлага нэмэгдсэн.

Бөөн сүйрэл

Үүний үр дүнд TCI-д тэгш бус хандалтыг засч, процессор, RCU болон бусад жижиг зүйлс гэж нэрлэгддэг үндсэн гогцоо үүсгэсэн. Тиймээс би Qemu-г сонголтоор ажиллуулж байна -d exec,in_asm,out_asm, энэ нь та ямар блок кодыг гүйцэтгэж байгааг хэлэх хэрэгтэй, мөн нэвтрүүлгийн үеэр ямар зочин код байсан, ямар хост код болсныг бичих хэрэгтэй (энэ тохиолдолд байт код). Энэ нь эхэлж, хэд хэдэн орчуулгын блокуудыг ажиллуулж, RCU одоо эхлэх болно гэж миний үлдээсэн дибаг хийх мессежийг бичиж, ... эвдэрсэн. abort() функц дотор free(). Функцийг эргэцүүлэн бодох замаар free() Хуваарилагдсан санах ойн өмнөх найман байт дотор байрлах овоолгын блокийн толгой хэсэгт блокийн хэмжээ эсвэл үүнтэй төстэй зүйлийн оронд хог хаягдал байгааг бид олж мэдсэн.

Нуруулсныг устгах - ямар хөөрхөн ... Ийм тохиолдолд ашигтай арга байдаг - (боломжтой бол) ижил эх сурвалжаас уугуул хоёртын файлыг цуглуулж, Valgrind дор ажиллуулна уу. Хэсэг хугацааны дараа хоёртын хувилбар бэлэн болсон. Би үүнийг ижил сонголтоор ажиллуулдаг - энэ нь гүйцэтгэлд хүрэхээс өмнө эхлүүлэх үед ч гацдаг. Энэ нь мэдээжийн хэрэг тааламжгүй юм - эх сурвалжууд нь яг адилхан биш байсан нь гайхмаар зүйл биш юм, учир нь тохируулга нь арай өөр хувилбаруудыг хайж байсан, гэхдээ надад Valgrind байгаа - эхлээд би энэ алдааг засах болно, дараа нь би азтай бол , анхных нь гарч ирнэ. Би Valgrind-ийн доор ижил зүйлийг ажиллуулж байна... Y-y-y, y-y-y, uh-uh, энэ нь эхэлж, эхлүүлэх горимд шилжсэн бөгөөд санах ойн буруу хандалтын талаар нэг ч анхааруулгагүйгээр анхны алдааг давсан. Тэдний хэлснээр амьдрал намайг үүнд бэлтгээгүй - Walgrind-ийн удирдлаган дор ажиллахад сүйрсэн програм ажиллахаа больсон. Энэ нь юу байсан нь нууц юм. Миний таамаглал бол эхлүүлэх явцад эвдрэл гарсны дараа одоогийн зааварчилгааны ойролцоо нэг удаа gdb ажиллаж байгааг харуулсан. memset-а аль алиныг нь ашиглан зөв заагчтай mmx, эсвэл xmm Бүртгүүлсэн бол энэ нь ямар нэгэн байдлаар тохируулсан алдаа байж магадгүй ч итгэхэд бэрх хэвээр байна.

За, Валгринд энд тус болохгүй бололтой. Эндээс хамгийн жигшүүртэй зүйл эхэлсэн - бүх зүйл бүр эхэлж байгаа мэт боловч сая сая зааврын өмнө тохиолдож болох үйл явдлын улмаас огт үл мэдэгдэх шалтгаанаар сүйрэв. Хэрхэн ойртох нь ч тодорхойгүй удлаа. Эцэст нь би суугаад дибаг хийх шаардлагатай болсон. Толгой хэсгийг дахин бичсэн зүйлийг хэвлэх нь энэ нь тоо шиг биш, харин ямар нэгэн хоёртын өгөгдөлтэй төстэй болохыг харуулсан. Харагтун, энэ хоёртын мөр нь BIOS файлаас олдсон - өөрөөр хэлбэл, энэ нь буферийн халилт байсан гэж хангалттай итгэлтэйгээр хэлэх боломжтой байсан бөгөөд энэ буферт бичигдсэн нь тодорхой болсон. За, тэгвэл үүнтэй төстэй зүйл - Emscripten-д аз болоход хаягийн орон зайд санамсаргүй хуваарилалт байхгүй, дотор нь нүх байхгүй тул та кодын дунд хаа нэгтээ бичиж, сүүлийн эхлүүлснээс хойш заагчаар өгөгдлийг гаргаж болно. Өгөгдлийг хар, заагчийг хар, хэрэв өөрчлөгдөөгүй бол бодоод үзээрэй. Аливаа өөрчлөлтийн дараа холбоход хэдэн минут шаардагдах нь үнэн, гэхдээ та юу хийж чадах вэ? Үүний үр дүнд BIOS-ийг түр зуурын буферээс зочны санах ой руу хуулсан тодорхой мөр олдсон бөгөөд үнэхээр буферт хангалттай зай байхгүй байв. Тэр хачирхалтай буфер хаягийн эх сурвалжийг олсноор функц гарч ирэв qemu_anon_ram_alloc файлд oslib-posix.c - Логик нь ийм байсан: заримдаа хаягийг 2 МБ хэмжээтэй асар том хуудсанд зэрэгцүүлэх нь ашигтай байдаг тул бид үүнийг асуух болно. mmap эхлээд бага зэрэг, дараа нь бид илүүдлийг нь тусламжаар буцааж өгнө munmap. Хэрэв ийм тохируулга хийх шаардлагагүй бол бид 2 МБ биш харин үр дүнг харуулах болно getpagesize() - mmap Энэ нь зэрэгцүүлсэн хаягийг өгөх болно ... Тиймээс Emscripten дээр mmap зүгээр л залгадаг malloc, гэхдээ энэ нь мэдээж хуудсан дээр таарахгүй. Ерөнхийдөө хэдэн сарын турш намайг бухимдуулж байсан алдааг өөрчилснөөр зассан двух шугамууд.

Дуудлага хийх функцүүдийн онцлог

Одоо процессор ямар нэг зүйлийг тоолж байна, Qemu гацахгүй, гэхдээ дэлгэц асахгүй, процессор хурдан гогцоонд орж, гаралтаас харахад -d exec,in_asm,out_asm. Нэг таамаглал гарч ирэв: таймерын тасалдал (эсвэл ерөнхийдөө бүх тасалдал) ирдэггүй. Үнэн хэрэгтээ, хэрэв та ямар нэг шалтгаанаар ажиллаж байсан уугуул чуулганы тасалдлыг салгавал үүнтэй төстэй зураг гарч ирнэ. Гэхдээ энэ нь огт хариулт байсангүй: дээрх хувилбартай гаргасан ул мөрийг харьцуулах нь гүйцэтгэлийн замнал маш эрт ялгаатай болохыг харуулж байна. Эхлүүлэгч ашиглан тэмдэглэсэн зүйлийг харьцуулж үзэхийг энд хэлэх ёстой emrun эх угсралтын гаралттай дибаг хийх нь бүрэн механик процесс биш юм. Хөтөч дээр ажиллаж байгаа програм яг яаж холбогддогийг би мэдэхгүй emrun, гэхдээ гаралт дахь зарим мөрүүд өөрчлөгддөг тул ялгааны зөрүү нь траекторууд зөрсөн гэж үзэх үндэслэл хараахан биш юм. Ерөнхийдөө зааврын дагуу гэдэг нь тодорхой болсон ljmpl өөр өөр хаяг руу шилжих шилжилт байгаа бөгөөд үүсгэсэн байт код нь үндсэндээ өөр: нэг нь туслах функцийг дуудах заавар агуулсан, нөгөө нь байхгүй. Зааврыг хайж, эдгээр зааврыг орчуулсан кодыг судалсны дараа, нэгдүгээрт, бүртгэлд шууд орох нь тодорхой болсон. cr0 Процессорыг хамгаалалттай горимд шилжүүлсэн, хоёрдугаарт, js хувилбар хэзээ ч хамгаалалттай горимд шилждэггүй гэсэн бичлэгийг мөн туслагч ашиглан хийсэн. Гэхдээ баримт бол 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 кодыг coroutines дээр бичсэн гэдгийг тодруулах шаардлагатай. 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 механизм, түүнчлэн заагч дуудлагууд болон өмнөх хоёр тохиолдлын аль нэг нь стекийн доор тохиолдож болох аливаа функц руу залгах. Одоо, сэжигтэй дуудлага бүрийн өмнө бид асинхрон контекстийг сонгох бөгөөд дуудлагын дараа шууд асинхрон дуудлага гарсан эсэхийг шалгах бөгөөд хэрэв байгаа бол бид энэ асинхрон контекст дэх бүх локал хувьсагчдыг хадгалах бөгөөд аль функцийг зааж өгөх болно. гүйцэтгэлийг үргэлжлүүлэх шаардлагатай үед хяналтыг шилжүүлж, одоогийн функцээс гарна. Энд л үр нөлөөг нь судлах боломж бий үрэн таран хийх — асинхрон дуудлагаас буцаж ирсний дараа кодыг үргэлжлүүлэн гүйцэтгэх хэрэгцээнд зориулж хөрвүүлэгч сэжигтэй дуудлагын дараа эхэлж буй функцийн "subbs" -ийг үүсгэдэг - үүнтэй адил: хэрэв n сэжигтэй дуудлага байвал функц хаа нэгтээ n/2 өргөжинө. удаа - энэ хэвээр байна, хэрэв тийм биш бол боломжит асинхрон дуудлага бүрийн дараа та анхны функцэд зарим локал хувьсагчдыг хадгалах хэрэгтэй гэдгийг санаарай. Дараа нь би Python хэл дээр энгийн скрипт бичих шаардлагатай болсон бөгөөд энэ нь "асинхроныг өөрсөддөө нэвтрүүлэхийг зөвшөөрдөггүй" (өөрөөр хэлбэл стек сурталчилгаа болон миний саяхан тайлбарласан бүх зүйл тийм биш юм) гэж үздэг, хэт их ашиглагддаг функцуудын өгөгдсөн багц дээр үндэслэсэн. тэдгээрт ажиллах), эдгээр функцуудыг асинхрон гэж үзэхгүйн тулд хөрвүүлэгчийн функцийг үл тоомсорлож болох заагчаар дамжуулан дуудлагыг заана. Дараа нь 60 МБ-аас доош хэмжээтэй JS файлууд хэтэрхий их байна - дор хаяж 30 гэж бодъё. Хэдий нэг удаа би угсралтын скрипт тохируулж байгаад санамсаргүйгээр холбогч сонголтуудыг хаясан. -O3. Би үүсгэсэн кодыг ажиллуулж, Chromium санах ойг идэж, гацдаг. Дараа нь би санамсаргүйгээр түүний татаж авах гэж байгаа зүйлийг харлаа... За, би юу хэлэх вэ, хэрэв надаас 500+ MB Javascript-ийг сайтар судалж, оновчтой болгохыг хүссэн бол би ч бас царцах байсан.

Харамсалтай нь, Asyncify дэмжлэгийн номын сангийн кодыг шалгах нь тийм ч таатай биш байсан longjmp-s нь виртуал процессорын кодонд ашиглагддаг боловч эдгээр шалгалтыг идэвхгүй болгож, бүх зүйл зүгээр байгаа мэт контекстийг хүчээр сэргээдэг жижиг засвар хийсний дараа код ажилласан. Дараа нь хачирхалтай зүйл эхэлсэн: заримдаа синхрончлолын кодыг шалгах ажиллагаа идэвхждэг - хэрэв гүйцэтгэх логикийн дагуу үүнийг хаах шаардлагатай бол кодыг гацаадаг - хэн нэгэн аль хэдийн баригдсан мутексийг барьж авахыг оролдсон. Аз болоход энэ нь цуваа кодонд логик асуудал биш болсон - би зүгээр л Emscripten-ийн өгсөн стандарт үндсэн давталтын функцийг ашиглаж байсан, гэхдээ заримдаа асинхрон дуудлага нь стекийг бүрэн задлах бөгөөд тэр үед энэ нь бүтэлгүйтэх болно. setTimeout үндсэн давталтаас - ингэснээр код нь өмнөх давталтаас гарахгүйгээр үндсэн давталт руу орсон. Хязгааргүй давталт дээр дахин бичсэн ба emscripten_sleep, мөн мутексуудтай холбоотой асуудлууд зогссон. Код нь бүр илүү логик болсон - үнэндээ надад дараагийн хөдөлгөөнт дүрсийг бэлтгэх код байхгүй - процессор зүгээр л ямар нэг зүйлийг тооцоолж, дэлгэцийг үе үе шинэчилдэг. Гэсэн хэдий ч асуудал үүгээр зогссонгүй: заримдаа Qemu гүйцэтгэл нь ямар ч үл хамаарах зүйл, алдаагүйгээр чимээгүйхэн зогсдог. Тэр үед би үүнийг орхисон, гэхдээ урагшаа харахад асуудал нь ийм байсан гэж хэлэх болно: корутин код нь үнэндээ үүнийг ашигладаггүй. setTimeout (эсвэл ядаж таны бодож байгаа шиг олон удаа биш): функц emscripten_yield зүгээр л асинхрон дуудлагын тугийг тохируулна. Гол санаа нь үүнд л байгаа юм emscripten_coroutine_next нь асинхрон функц биш: дотооддоо энэ нь тугийг шалгаж, дахин тохируулж, хяналтыг шаардлагатай газарт шилжүүлдэг. Өөрөөр хэлбэл, стекийн сурталчилгаа тэнд дуусдаг. Асуудал нь одоо байгаа coroutine backend-ээс чухал мөрийн кодын хуулбарлаагүйн улмаас coroutine pool идэвхгүй болсон үед гарч ирсэн, after free-ийн улмаас функц qemu_in_coroutine үнэн гэж буцсан бол үнэн хэрэгтээ худал буцах ёстой байсан. Энэ нь дуудлага хийхэд хүргэсэн emscripten_yield, түүнээс дээш стек дээр хэн ч байсангүй emscripten_coroutine_next, стек хамгийн дээд тал руугаа дэлгэгдсэн боловч үгүй setTimeout, би аль хэдийн хэлсэнчлэн, үзэсгэлэнд ороогүй.

JavaScript код үүсгэх

Энд үнэндээ "татсан махыг эргүүлэх" амласан зүйл байна. Үнэхээр биш. Мэдээжийн хэрэг, хэрэв бид Qemu-г хөтөч дээр ажиллуулж, Node.js-г түүн дээр ажиллуулбал Qemu-д код үүсгэсний дараа бид огт буруу JavaScript-ийг авах болно. Гэсэн хэдий ч зарим төрлийн урвуу өөрчлөлт.

Эхлээд Qemu хэрхэн ажилладаг талаар бага зэрэг. Намайг даруй уучлаарай: Би мэргэжлийн Qemu хөгжүүлэгч биш бөгөөд миний дүгнэлт зарим газарт алдаатай байж магадгүй. Тэдний хэлснээр "Оюутны үзэл бодол нь багшийн үзэл бодол, Пеаногийн аксиоматик, эрүүл ухаантай давхцах албагүй." Qemu нь тодорхой тооны дэмжигдсэн зочин архитектуртай бөгөөд тус бүрд нь үүнтэй төстэй лавлах байдаг target-i386. Барилга хийхдээ та хэд хэдэн зочин архитектурын дэмжлэгийг зааж өгч болно, гэхдээ үр дүн нь зөвхөн хэд хэдэн хоёртын хувилбар байх болно. Зочны архитектурыг дэмжих код нь эргээд Qemu-ийн зарим дотоод үйлдлүүдийг үүсгэдэг бөгөөд TCG (Tiny Code Generator) нь аль хэдийн хост архитектурын машины код болж хувирдаг. tcg директорт байрлах readme файлд дурдсанчлан энэ нь анхандаа ердийн C хөрвүүлэгчийн нэг хэсэг байсан бөгөөд хожим нь JIT-д тохируулсан. Тиймээс, жишээлбэл, энэ баримт бичгийн хувьд зорилтот архитектур нь зочин архитектур байхаа больсон, харин хост архитектур юм. Хэзээ нэгэн цагт өөр нэг бүрэлдэхүүн хэсэг гарч ирэв - Tiny Code Interpreter (TCI) бөгөөд энэ нь тодорхой хост архитектурт код үүсгэгч байхгүй тохиолдолд кодыг (бараг ижил дотоод үйлдлүүд) гүйцэтгэх ёстой. Үнэн хэрэгтээ, түүний баримт бичигт дурдсанчлан, энэ орчуулагч нь зөвхөн хурдны хувьд төдийгүй чанарын хувьд JIT код үүсгэгч шиг сайн ажиллаж чаддаггүй. Хэдийгээр түүний тайлбар бүрэн хамааралтай гэдэгт би итгэлгүй байна.

Эхлээд би бүрэн хэмжээний TCG backend хийх гэж оролдсон боловч эх код болон байт кодын зааврын бүрэн тодорхой бус тайлбарт хурдан андуурч, TCI орчуулагчийг боохоор шийдсэн. Энэ нь хэд хэдэн давуу талыг өгсөн:

  • Код үүсгэгчийг хэрэгжүүлэхдээ та зааврын тайлбарыг биш харин орчуулагчийн кодыг харж болно
  • Та тулгарсан орчуулгын блок бүрт функцийг үүсгэхгүй, жишээлбэл, зуу дахь гүйцэтгэлийн дараа л функцийг үүсгэж болно.
  • Хэрэв үүсгэсэн код өөрчлөгдвөл (мөн нөхөөсийг агуулсан нэр бүхий функцээс харахад энэ нь боломжтой юм шиг санагдаж байна) би үүсгэсэн JS кодыг хүчингүй болгох хэрэгтэй болно, гэхдээ ядаж надад үүнийг дахин үүсгэх зүйл байх болно.

Гурав дахь цэгийн тухайд, кодыг анх удаа ажиллуулсны дараа засвар хийх боломжтой гэдэгт би итгэлгүй байна, гэхдээ эхний хоёр цэг хангалттай.

Эхэндээ кодыг анхны байт кодын зааврын хаягаар том шилжүүлэгч хэлбэрээр үүсгэсэн боловч дараа нь Emscripten, үүсгэсэн JS-ийг оновчтой болгох, дахин давтагдах тухай нийтлэлийг санаж, би илүү олон хүний ​​код үүсгэхээр шийдсэн, ялангуяа энэ нь эмпирик юм. Орчуулгын блок руу орох цорын ганц цэг нь түүний Эхлэл юм. Хэсэг хугацааны дараа бид ifs-тэй (гогцоогүй ч) код үүсгэдэг код үүсгэгчтэй болсон. Гэвч азгүйтэж, энэ нь сүйрч, заавар нь буруу урттай байсан гэсэн мессежийг өгчээ. Түүгээр ч барахгүй энэ рекурсын түвшний сүүлчийн заавар нь байсан brcond. За, би энэ зааварчилгааг рекурсив дуудлагын өмнө болон дараа үүсгэхэд ижил чек нэмж оруулах ба... тэдгээрийн нэг нь ч гүйцэтгэгдээгүй, гэхдээ баталгаажуулалтын дараа тэдгээр нь амжилтгүй болсон. Эцэст нь үүсгэсэн кодыг судалсны дараа би шилжүүлсний дараа одоогийн зааврын заагч нь стекээс дахин ачаалагдах бөгөөд магадгүй үүсгэсэн JavaScript кодоор дарж бичигдсэн болохыг ойлгосон. Тэгээд ийм зүйл болсон. Буферийг нэг мегабайтаас арав хүртэл нэмэгдүүлснээр ямар ч үр дүнд хүрээгүй бөгөөд код үүсгэгч тойрог хэлбэрээр ажиллаж байгаа нь тодорхой болсон. Бид одоогийн сүрьеэгийн хил хязгаараас хэтрээгүй эсэхийг шалгах ёстой байсан бөгөөд хэрэв гарсан бол дараагийн сүрьеэгийн хаягийг хасах тэмдэгтэй болгож, гүйцэтгэлийг үргэлжлүүлэх боломжтой болсон. Нэмж дурдахад, энэ нь "энэ байт кодын хэсэг өөрчлөгдсөн тохиолдолд аль үүсгэсэн функцийг хүчингүй болгох ёстой вэ?" гэсэн асуудлыг шийддэг. - зөвхөн энэ орчуулгын блокт тохирох функцийг хүчингүй болгох шаардлагатай. Дашрамд хэлэхэд, би Chromium дээрх бүх зүйлийг дибаг хийсэн ч (би Firefox ашигладаг бөгөөд туршилт хийхэд тусдаа хөтөч ашиглах нь надад илүү хялбар байдаг) Firefox надад asm.js стандарттай нийцэхгүй байгааг засахад тусалсан бөгөөд үүний дараа код илүү хурдан ажиллаж эхэлсэн. Chromium.

Үүсгэсэн кодын жишээ

Compiling 0x15b46d0:
CompiledTB[0x015b46d0] = function(stdlib, ffi, heap) {
"use asm";
var HEAP8 = new stdlib.Int8Array(heap);
var HEAP16 = new stdlib.Int16Array(heap);
var HEAP32 = new stdlib.Int32Array(heap);
var HEAPU8 = new stdlib.Uint8Array(heap);
var HEAPU16 = new stdlib.Uint16Array(heap);
var HEAPU32 = new stdlib.Uint32Array(heap);

var dynCall_iiiiiiiiiii = ffi.dynCall_iiiiiiiiiii;
var getTempRet0 = ffi.getTempRet0;
var badAlignment = ffi.badAlignment;
var _i64Add = ffi._i64Add;
var _i64Subtract = ffi._i64Subtract;
var Math_imul = ffi.Math_imul;
var _mul_unsigned_long_long = ffi._mul_unsigned_long_long;
var execute_if_compiled = ffi.execute_if_compiled;
var getThrew = ffi.getThrew;
var abort = ffi.abort;
var qemu_ld_ub = ffi.qemu_ld_ub;
var qemu_ld_leuw = ffi.qemu_ld_leuw;
var qemu_ld_leul = ffi.qemu_ld_leul;
var qemu_ld_beuw = ffi.qemu_ld_beuw;
var qemu_ld_beul = ffi.qemu_ld_beul;
var qemu_ld_beq = ffi.qemu_ld_beq;
var qemu_ld_leq = ffi.qemu_ld_leq;
var qemu_st_b = ffi.qemu_st_b;
var qemu_st_lew = ffi.qemu_st_lew;
var qemu_st_lel = ffi.qemu_st_lel;
var qemu_st_bew = ffi.qemu_st_bew;
var qemu_st_bel = ffi.qemu_st_bel;
var qemu_st_leq = ffi.qemu_st_leq;
var qemu_st_beq = ffi.qemu_st_beq;

function tb_fun(tb_ptr, env, sp_value, depth) {
  tb_ptr = tb_ptr|0;
  env = env|0;
  sp_value = sp_value|0;
  depth = depth|0;
  var u0 = 0, u1 = 0, u2 = 0, u3 = 0, result = 0;
  var r0 = 0, r1 = 0, r2 = 0, r3 = 0, r4 = 0, r5 = 0, r6 = 0, r7 = 0, r8 = 0, r9 = 0;
  var r10 = 0, r11 = 0, r12 = 0, r13 = 0, r14 = 0, r15 = 0, r16 = 0, r17 = 0, r18 = 0, r19 = 0;
  var r20 = 0, r21 = 0, r22 = 0, r23 = 0, r24 = 0, r25 = 0, r26 = 0, r27 = 0, r28 = 0, r29 = 0;
  var r30 = 0, r31 = 0, r41 = 0, r42 = 0, r43 = 0, r44 = 0;
    r14 = env|0;
    r15 = sp_value|0;
  START: do {
    r0 = HEAPU32[((r14 + (-4))|0) >> 2] | 0;
    r42 = 0;
    result = ((r0|0) != (r42|0))|0;
    HEAPU32[1445307] = r0;
    HEAPU32[1445321] = r14;
    if(result|0) {
    HEAPU32[1445322] = r15;
    return 0x0345bf93|0;
    }
    r0 = HEAPU32[((r14 + (16))|0) >> 2] | 0;
    r42 = 8;
    r0 = ((r0|0) - (r42|0))|0;
    HEAPU32[(r14 + (16)) >> 2] = r0;
    r1 = 8;
    HEAPU32[(r14 + (44)) >> 2] = r1;
    r1 = r0|0;
    HEAPU32[(r14 + (40)) >> 2] = r1;
    r42 = 4;
    r0 = ((r0|0) + (r42|0))|0;
    r2 = HEAPU32[((r14 + (24))|0) >> 2] | 0;
    HEAPU32[1445307] = r0;
    HEAPU32[1445308] = r1;
    HEAPU32[1445309] = r2;
    HEAPU32[1445321] = r14;
    HEAPU32[1445322] = r15;
    qemu_st_lel(env|0, r0|0, r2|0, 34, 22759218);
if(getThrew() | 0) abort();
    r0 = 3241038392;
    HEAPU32[1445307] = r0;
    r0 = qemu_ld_leul(env|0, r0|0, 34, 22759233)|0;
if(getThrew() | 0) abort();
    HEAPU32[(r14 + (24)) >> 2] = r0;
    r1 = HEAPU32[((r14 + (12))|0) >> 2] | 0;
    r2 = HEAPU32[((r14 + (40))|0) >> 2] | 0;
    HEAPU32[1445307] = r0;
    HEAPU32[1445308] = r1;
    HEAPU32[1445309] = r2;
    qemu_st_lel(env|0, r2|0, r1|0, 34, 22759265);
if(getThrew() | 0) abort();
    r0 = HEAPU32[((r14 + (24))|0) >> 2] | 0;
    HEAPU32[(r14 + (40)) >> 2] = r0;
    r1 = 24;
    HEAPU32[(r14 + (52)) >> 2] = r1;
    r42 = 0;
    result = ((r0|0) == (r42|0))|0;
    if(result|0) {
    HEAPU32[1445307] = r0;
    HEAPU32[1445308] = r1;
    }
    HEAPU32[1445307] = r0;
    HEAPU32[1445308] = r1;
    return execute_if_compiled(22759392|0, env|0, sp_value|0, depth|0) | 0;
    return execute_if_compiled(23164080|0, env|0, sp_value|0, depth|0) | 0;
    break;
  } while(1); abort(); return 0|0;
}
return {tb_fun: tb_fun};
}(window, CompilerFFI, Module.buffer)["tb_fun"]

дүгнэлт

Тиймээс ажил дуусаагүй байгаа ч би энэ урт хугацааны бүтээн байгуулалтыг нууцаар төгс төгөлдөрт хүргэхээс залхаж байна. Тиймээс одоо байгаа зүйлээ нийтлэхээр шийдлээ. Энэ код нь зарим газарт бага зэрэг аймшигтай байдаг, учир нь энэ бол туршилт бөгөөд юу хийх нь тодорхойгүй байна. Магадгүй, Кэмүгийн илүү орчин үеийн хувилбар дээр ердийн атомын амлалтуудыг гаргах нь зүйтэй болов уу. Энэ хооронд Gita-д блог форматтай нэг сэдэв бий: ямар нэгэн байдлаар давсан "түвшин" бүрийн хувьд орос хэл дээрх дэлгэрэнгүй тайлбарыг нэмж оруулсан болно. Үнэн хэрэгтээ энэ нийтлэл нь дүгнэлтийг дахин тайлбарлах явдал юм git log.

Та бүгдийг туршиж үзэж болно энд (замын хөдөлгөөнөөс болгоомжил).

Аль хэдийн ажиллаж байгаа зүйл:

  • x86 виртуал процессор ажиллаж байна
  • Машины кодоос JavaScript хүртэлх JIT код үүсгэгчийн ажлын загвар байдаг
  • Бусад 32 битийн зочны архитектурыг угсрах загвар бий: яг одоо та ачаалах үе шатанд хөтөч дээр хөлддөг MIPS архитектурын хувьд Linux-ийг биширч чадна.

Та өөр юу хийж чадах вэ

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

Эх сурвалж: www.habr.com

сэтгэгдэл нэмэх