QEMU.js: зараз па-сур'ёзнаму і з WASM

Калісьці даўно я смеху дзеля вырашыў даказаць абарачальнасць працэсу і навучыцца генераваць JavaScript (а дакладней, Asm.js) з машыннага кода. Для эксперыменту быў абраны QEMU, праз некаторы час быў напісаны артыкул на Хабр. У каментарах мне параілі перарабіць праект на WebAssembly, ды і самому кідаць амаль скончаны праект неяк не хацелася… Праца ішла, але вельмі павольна, і вось, нядаўна ў тым артыкуле з'явіўся каментар на тэму "Так і чым усё скончылася?". На мой разгорнуты адказ я пачуў "Гэта цягне на артыкул". Ну, калі цягне, то будзе артыкул. Можа, каму спатрэбіцца. З яе чытач даведаецца некаторыя факты пра прыладу бэкенд кодагенерацыі QEMU, а таксама як напісаць Just-in-Time кампілятар для вэб-прыкладанні.

задачы

Паколькі «так-сяк» партаваць QEMU на JavaScript я ўжо навучыўся, у гэты раз было вырашана рабіць па розуме і не паўтараць старых памылак.

Памылка нумар разоў: адказаць ад point release

Першай маёй памылкай было адказаць сваю версію ад upstream-версіі 2.4.1. Тады мне здавалася гэта добрай ідэяй: калі point release існуе, значыць ён, мусіць, стабільней простага 2.4, а ўжо тым больш галінкі master. А паколькі я планаваў дадаць ладную колькасць сваіх багаў, то чужыя мне былі ну наогул не патрэбныя. Так яно, мусіць, і атрымалася. Але вось няўдача: QEMU не варта на месцы, а ў нейкі момант тамака нават анансавалі аптымізацыю генераванага кода адсоткаў на 10. «Ага, цяпер умержу» падумаў я і абламаўся. Тут трэба зрабіць адступ: у сувязі з аднаструменным характарам QEMU.js і тым, што арыгінальны QEMU не мяркуе адсутнасці шматструменнасці (гэта значыць для яго крытычная магчымасць адначасовай працы некалькіх нязвязаных code path, а не проста "заюзать усе ядры"), галоўныя функцыі патокаў прыйшлося "вывярнуць" для магчымасці выкліку звонку. Гэта стварыла нейкія натуральныя праблемы пры зліцці. Аднак той факт, што частка змен з галінкі master, З якой я спрабаваў зліць свой код, таксама былі cherry picked ў point release (а значыць, і ў маю галінку) таксама, верагодна, выгоды б не дадаў.

Увогуле, я вырашыў, што ўсё роўна прататып мае сэнс выкінуць разабраць на запчасткі і пабудаваць новую версію з нуля на базе чаго-небудзь пасвяжэй і зараз ужо з master.

Памылка нумар два: ТЛП-метадалогія

У сутнасці, гэта і не памылка, увогуле-то - проста асаблівасць стварэння праекта ва ўмовах поўнага неразумення як "куды і як рухацца?", так і наогул "а ці дойдзем?". У гэтых умовах цяп-ляп праграмаванне было апраўданым варыянтам, але, натуральна, зусім не жадалася гэта паўтараць без неабходнасці. У гэты раз хацелася зрабіць па розуме: атамарныя коміты, усвядомленыя змены кода (а не "stringing random characters together until to compiles (with warnings)", як пра кагосьці аднойчы сказаў Лінус Торвальдс, калі верыць Вікіцытатніку) і г.д.

Памылка нумар тры: не ведаючы броду лезці ў ваду

Ад гэтага я і цяпер да канца не пазбавіўся, але зараз вырашыў ісці не па шляху зусім ужо найменшага супраціву, і зрабіць "па дарослым", а менавіта, напісаць свой TCG backend з нуля, каб потым не казаць, маўляў "Так, гэта, вядома, павольна, але я ж не магу ўсё кантраляваць – TCI так напісаны…». Акрамя таго, першапачаткова гэта здавалася відавочным рашэннем, паколькі я ж бінарны код генерую. Як гаворыцца, «Сабраў Генту, Ды не тую »: код-то, вядома, бінарны, але кіраванне на яго проста так не перадаць - яго трэба відавочным чынам запхнуць у браўзэр на кампіляцыю, атрымаўшы ў выніку нейкі аб'ект са свету JS, які яшчэ трэба кудысьці захаваць. Зрэшты, на нармальных RISC-архітэктурах, наколькі я разумею, тыповай сітуацыяй з'яўляецца неабходнасць відавочна скінуць кэш інструкцый для перагенераванага кода – калі гэта і не тое, што нам трэба, то, ва ўсякім разе, блізка. Акрамя таго, з мінулай сваёй спробы я засвоіў, што кіраванне на сярэдзіну блока трансляцыі накшталт як не перадаецца, таму байткод, інтэрпрэтаваны з любога зрушэння, нам асоба і не патрэбен, і можна проста генераваць па функцыі на TB.

Прыйшлі і штурхнулі

Хоць перапісваць код я пачаў яшчэ ў ліпені, але чароўны пендэль падкраўся неўзаметку: звычайна лісты з GitHub прыходзяць як апавяшчэнні аб адказах на Issues і Pull requests, а тут, раптам згадка ў трэдзе Binaryen as a qemu backend у кантэксце, "Вось ён нешта падобнае рабіў, можа скажа што-небудзь". Гаворка ішла аб выкарыстанні роднаснай Emscripten-у бібліятэкі Binaryen для стварэння WASM JIT. Ну я і сказаў, што ў вас тамака ліцэнзія Apache 2.0, а QEMU як адзінае цэлае распаўсюджваецца пад GPLv2, і яны не вельмі сумяшчальныя. Раптам аказалася, што ліцэнзію можна неяк паправіць (не ведаю: можа, памяняць, можа, падвойнае ліцэнзаванне, можа, яшчэ нешта…). Гэта мяне, вядома, узрадавала, таму што я ўжо некалькі разоў да таго часу прыглядаўся да бінарным фармаце WebAssembly, і мне было неяк сумна і незразумела. Тут жа была бібліятэка, якая і базавыя блокі з графам пераходаў зжарэ, і байткод выдасць, і нават сама яго запусціць у інтэрпрэтатары, калі спатрэбіцца.

Потым яшчэ было ліст у спісе рассылання QEMU, але гэта ўжо хутчэй да пытання, "А каму яно наогул трэба?". А яно, раптам, Аказалася трэба. Як мінімум, можна наскрэбці такія магчымасці выкарыстання, калі яно будзе больш-менш хутка працаваць:

  • запуск чаго-небудзь навучалага наогул без усталёўкі
  • віртуалізацыя на iOS, дзе па чутках адзінае прыкладанне, якое мае права на кодогенерацию на лета - гэта JS-рухавічок (а ці праўда гэта?)
  • дэманстрацыя міні-АС - аднадыскетныя, убудаваныя, усякія прашыўкі і г.д…

Асаблівасці браузернага асяроддзя выканання

Як я ўжо казаў, QEMU завязаны на шматструменнасць, а ў браўзэры яе няма. Ну, гэта значыць як не… Спачатку яе не было наогул, потым з'явіліся WebWorkers – наколькі я разумею, гэта шматструменнасць, заснаваная на перадачы паведамленняў без сумесна зменлівых зменных. Натуральна, гэта стварае значныя праблемы пры партаванні існага кода, заснаванага на shared memory мадэлі. Потым пад ціскам грамадскасці была рэалізавана і яна пад назвай SharedArrayBuffers. Яе паступова ўвялі, адсвяткавалі яе запуск у розных браўзэрах, потым адсвяткавалі новы год, а потым Meltdown… Пасля чаго прыйшлі да высновы, што загрубляй-не загрубляй вымярэнне часу, а з дапамогай shared memory і струменя, які инкрементирует лічыльнік, усё роўна даволі дакладна атрымаецца. Так і адключылі шматструменнасць з агульнай памяццю. Як бы, яе потым уключалі зваротна, але, як стала зразумела з першага эксперыменту, і без яе жыццё ёсць, а раз так, то паспрабуем зрабіць, не закладаючыся на шматструменнасць.

Другая асаблівасць заключаецца ў немагчымасці нізкаўзроўневых маніпуляцый са стэкам: нельга проста ўзяць, захаваць бягучы кантэкст і пераключыцца на новы з новым стэкам. Стэк выклікаў кіруецца віртуальнай машынай JS. Здавалася б, у чым праблема, раз ужо мы ўсё роўна вырашылі спраўляцца з былымі плынямі цалкам уручную? Справа ў тым, што блокавы ўвод-вывад у QEMU рэалізаваны праз каруціны, і вось тут бы нам і спатрэбіліся нізкаўзроўневыя маніпуляцыі стэкам. На шчасце, Emscipten ужо змяшчае механізм для асінхронных аперацый, нават два: Asyncify и Emterpreter. Першы працуе праз значнае раздзіманне генераванага JavaScript-кода і ўжо не падтрымліваецца. Другі з'яўляецца бягучым "правільным спосабам" і працуе праз генерацыю байткода для ўласнага інтэрпрэтатара. Працуе, вядома, павольна, але затое не раздзімае код. Праўда, падтрымку каруцін для гэтага механізму прыйшлося контрибутить самастойна (там ужо былі каруціны, напісаныя пад Asyncify і была рэалізацыя прыблізна таго ж API для Emterpreter, трэба было проста іх злучыць).

На дадзены момант я яшчэ не паспеў падзяліць код на кампіляваны ў WASM і інтэрпрэтаваны з дапамогай Emterpreter, таму блокавыя прылады яшчэ не працуюць (глядзіце ў наступных серыях, як гаворыцца…). Гэта значыць, у выніку павінна атрымацца вось такое пацешнае слаістай нешта:

  • інтэрпрэтаваны блокавы ўвод-вывад. Ну а што, вы праўда чакалі эмуляваны NVMe з натыўнай прадукцыйнасцю? 🙂
  • статычна скампіляваны асноўны код QEMU (транслятар, астатнія эмуляваныя прылады і г.д.)
  • дынамічна кампіляваны ў WASM гасцявы код

Асаблівасці зыходнікаў QEMU

Як вы, мусіць, ужо здагадаліся, код эмуляцыі гасцявых архітэктур і код генерацыі хостовых машынных інструкцый у QEMU падзелены. Насамрэч, там нават яшчэ крыху хітрэй:

  • ёсць гасцявыя архітэктуры
  • ёсць акселератары, А менавіта, KVM для апаратнай віртуалізацыі на Linux (для сумяшчальных паміж сабой гасцявых і хаставых сістэм), TCG для JIT-кодагенерацыі дзе патрапіла. Пачынаючы з QEMU 2.9 з'явілася падтрымка стандарту апаратнай віртуалізацыі HAXM на Windows (падрабязнасці)
  • калі выкарыстоўваецца TCG, а не апаратная віртуалізацыя, то ў яго ёсць асобная падтрымка кодагенерацыі пад кожную хаставую архітэктуру, а таксама пад універсальны інтэрпрэтатар.
  • … а вакол усяго гэтага – эмуляваная перыферыя, карыстацкі інтэрфейс, міграцыя, record-replay, і г.д.

Дарэчы, ці ведаеце вы: QEMU можа эмуляваць не толькі кампутар цалкам, але і працэсар для асобнага карыстацкага працэсу ў хаставым ядры, чым карыстаецца, напрыклад, фазар AFL для інструментацыі бінарнікаў. Магчыма, нехта захоча партаваць гэты рэжым працы QEMU на JS? 😉

Як і большасць даўно існуючых свабодных праграм, QEMU збіраецца праз выклік configure и make. Выкажам здагадку, вы вырашылі нешта дадаць: TCG-бэкенд, рэалізацыю патокаў, нешта яшчэ. Не спяшаецеся цешыцца/жахацца (патрэбнае падкрэсліць) даляглядзе зносін з Autoconf насамрэч, configure у QEMU, па ўсёй бачнасці, самапісны і не з чаго не генеруецца.

WebAssembly

Дык што ж гэта за штука – WebAssembly (ён жа WASM)? Гэта замена Asm.js, зараз ужо не прыкідваецца валідным JavaScript кодам. Наадварот, яно асабліва бінарнае і аптымізаванае, і нават проста запісаць у яго цэлы лік не вельмі вось і проста: яно для кампактнасці захоўваецца ў фармаце ЛЕБ128.

Магчыма, вы чулі пра алгарытм relooping для Asm.js - гэта аднаўленне «высокаўзроўневых» інструкцый кіравання струменем выканання (гэта значыць if-then-else, цыклы і г.д.), пад якія заменчаны JS-рухавічкі, з нізкаўзроўневага LLVM IR, больш блізкага да машыннага кода, які выконваецца працэсарам. Натуральна, прамежкавае ўяўленне QEMU бліжэй да другога. Здавалася б, вось ён, байткод, канец пакут… І тут блокі, if-then-else і цыклы!

І ў гэтым заключаецца яшчэ адна прычына, чаму карысны Binaryen: ён, натуральна, можа прымаць высокаўзроўневыя блокі, блізкія да таго, што будзе захавана ў WASM. Але ён таксама можа выдаваць код з графа базавых блокаў і пераходаў паміж імі. Ну а пра тое, што ен хавае за зручным C/C++ API фармат захоўвання WebAssembly, я ўжо сказаў.

TCG (Tiny Code Generator)

TCG першапачаткова быў бэкэндам для кампілятара C. Потым ён, мабыць, не вытрымаў канкурэнцыі з GCC, але ў выніку знайшоў сваё месца ў складзе QEMU у якасці механізму кодагенерацыі пад хаставую платформу. Таксама ёсць і TCG-бэкенд, які генеруе нейкі абстрактны байткод, які тут жа і выконвае інтэрпрэтатар, але ад яго выкарыстання я вырашыў сысці ў гэты раз. Зрэшты, той факт, што ў QEMU ужо ёсць магчымасць уключыць пераход на згенераваны TB праз функцыю tcg_qemu_tb_exec, мне аказаўся вельмі дарэчы.

Каб дадаць новы TCG-бэкенд у QEMU, трэба стварыць падкаталог tcg/<имя архитектуры> (у дадзеным выпадку, tcg/binaryen), а ў ім два файлы: tcg-target.h и tcg-target.inc.c и прапісаць уся гэтая справа ў configure. Можна пакласці туды і іншыя файлы, але, як можна здагадацца з назваў гэтых двух, яны абодва будуць кудысьці ўключацца: адзін як звычайны загалоўкавыя файл (ён инклудится у tcg/tcg.h, а той ужо ў іншыя файлы ў каталогах tcg, accel і не толькі), іншы - толькі як code snippet у tcg/tcg.c, затое ён мае доступ да яго static-функцый.

Вырашыўшы, што я патрачу занадта шмат часу на дэталёвыя разгляды, як яно ўладкована, я проста скапіяваў "шкілеты" гэтых двух файлаў з іншай рэалізацыі бэкенда, сапраўды паказаўшы гэта ў загалоўку ліцэнзіі.

файл tcg-target.h змяшчае пераважна наладкі ў выглядзе #define-ов:

  • колькі рэгістраў і якой шырыні ёсць на мэтавай архітэктуры (у нас - колькі хочам, столькі і ёсць - пытанне больш за тое, што будзе генеравацца ў больш эфектыўны код браўзэрам на "зусім мэтавай" архітэктуры…)
  • выраўноўванне хостовых інструкцый: на x86, ды і ў TCI, інструкцыі наогул не выраўноўваюцца, я ж збіраюся класці ў буфер кода і не інструкцыі зусім, а паказальнікі на структуры бібліятэкі Binaryen, таму скажу: 4 байта
  • якія апцыянальныя інструкцыі можа генераваць бэкенд - уключаем усё, што знойдзем у Binaryen, астатняе хай акселератар разбівае на прасцейшыя сам
  • які прыкладна памер TLB-кеша запытвае бэкенд. Справа ў тым, што ў QEMU усё па-сур'ёзнаму: хоць і ёсць функцыі-памочнікі, якія ажыццяўляюць load/store з улікам гасцявога MMU (а куды зараз без яго?), але свой кэш трансляцыі яны захоўваюць у выглядзе структуры, апрацоўку якой зручна ўбудоўваць прама ў блокі трансляцыі. Пытанне ж у тым, якое зрушэнне ў гэтай структуры найболей эфектыўна апрацоўваецца маленькай і хуткай паслядоўнасцю каманд
  • тутака ж можна падкруціць прызначэнне аднаго-двух зарэзерваваных рэгістраў, уключыць выклік TB праз функцыю і апцыянальна апісаць пару дробных inline-функцый накшталт flush_icache_range (але гэта не наш выпадак)

файл tcg-target.inc.c, натуральна, звычайна нашмат больш па памеры і змяшчае некалькі абавязковых функцый:

  • ініцыялізацыя, якая паказвае ў тым ліку абмежаванні на тое, якая інструкцыя з якімі аперандамі можа працаваць. Нахабна скапіявана мною з іншага бэкенда
  • функцыя, якая прымае адну інструкцыю ўнутранага байткода
  • сюды ж можна пакласці дапаможныя функцыі, а таксама тут можна карыстацца статычнымі функцыямі з tcg/tcg.c

Для сябе я абраў наступную стратэгію: у першых словах чарговага блока трансляцыі я запісваў чатыры паказальнікі: пазнаку пачатку (нейкае значэнне ў наваколлі 0xFFFFFFFF, па якім вызначалася бягучы стан TB), кантэкст, згенераваны модуль, і magic number для адладкі. Спачатку пазнака выстаўлялася ў 0xFFFFFFFF - n, Дзе n - невялікі станоўчы лік, і пры кожным выкананні праз інтэрпрэтатар павялічвалася на 1. Калі яна даходзіла да 0xFFFFFFFE, адбывалася кампіляцыя, модуль захоўваўся ў табліцы функцый, імпартаванай у невялікі «запускатар», у які і сыходзіла выкананне з tcg_qemu_tb_exec, а модуль выдаляўся з памяці QEMU.

Перафразуючы класіку, "Мыца, як шмат у гэтым гуку для сэрца прагера сплялося…". Тым не менш, памяць кудысьці выцякала. Прычым гэта была памяць, якая кіруецца QEMU! У мяне быў код, які пры запісе чарговай інструкцыі (ну, гэта значыць, паказальніка) выдаляў тую, спасылка на якую была на гэтым месцы раней, але гэта не дапамагала. Наогул-то, у найпростым выпадку QEMU вылучае пры старце памяць і піша туды генераваны код. Калі буфер заканчваецца, код выкідваецца, і на яго месца пачынае запісвацца наступны.

Павучыўшы код, я зразумеў, што мыліца з magic number дазваляў не зваліцца на разбурэнні кучы, вызваліўшы што-небудзь не тое на неініцыялізаваным буферы пры першым праходзе. Але хто перапісвае буфер у абыход маёй функцыі потым? Як і раяць распрацоўшчыкі Emscripten, упёршыся ў праблему, я партаваў атрыманы код назад у натыўны дадатак, нацкаваў на яго Mozilla Record-Replay… Увогуле, у выніку я зразумеў простую рэч: для кожнага блока вылучаецца struct TranslationBlock з яго апісаннем. Адгадайце, дзе ... Правільна, непасрэдна перад блокам прама ў буферы. Усвядоміўшы гэта, я вырашыў завязваць з мыліцамі (хоць бы некаторымі), і проста выкінуў magic number, а пакінутыя словы перанёс у struct TranslationBlock, завёўшы аднасувязны спіс, па якім можна хутка прайсціся пры скідзе кэша трансляцыі, і вызваліць памяць.

Некаторыя мыліцы засталіся: напрыклад, пазначаныя паказальнікі ў буферы кода - частка з іх проста з'яўляюцца BinaryenExpressionRef, гэта значыць глядзяць на выразы, якія трэба лінейна пакласці ў генераваны базавы блок, частка - умова пераходу паміж ББ, частка - куды пераходзіць. Ды і ёсць ужо падрыхтаваныя блокі для Relooper, якія трэба злучыць па ўмовах. Каб іх адрозніваць, выкарыстоўваецца здагадка, што ўсе яны выраўнаваны хаця б на чатыры байта, таму можна спакойна выкарыстоўваць малодшыя два біты пад пазнаку, трэба толькі не забываць яе прыбіраць пры неабходнасці. Дарэчы, такія пазнакі ўжо выкарыстоўваюцца ў QEMU для абазначэння прычыны выхаду з цыкла TCG.

Выкарыстанне Binaryen

Модулі ў WebAssembly утрымоўваюць функцыі, кожная з якіх утрымоўвае цела, якое прадстаўляе з сябе выраз. Выразы - гэта ўнарныя і бінарныя аперацыі, блокі, якія складаюцца са спісаў іншых выразаў, control flow і г.д. Як я ўжо казаў, control flow тут арганізуецца менавіта як высокаўзроўневыя галіны, цыклы, выклікі функцый і г.д. Аргументы функцый перадаюцца не на стэку, а відавочна, як і ў JS. Ёсць і глабальныя зменныя, але я іх не выкарыстоўваў, таму пра іх не раскажу.

Таксама ў функцый ёсць нумараваныя з нуля лакальныя зменныя, якія маюць тып: int32/int64/float/double. Пры гэтым першыя n лакальных зменных - гэта перададзеныя функцыі аргументы. Звярніце ўвагу, што хоць тут усё і не зусім нізкаўзроўневае ў плане патоку кіравання, але цэлыя лікі ўсё ж не нясуць у сабе прыкмета "знакавы / беззнакавы": як будзе паводзіць сябе лік, залежыць ад кода аперацыі.

Наогул кажучы, Binaryen дае просты C-API: вы ствараеце модуль, у ім ствараеце выразы - унарныя, бінарныя, блокі з іншых выразаў, control flow і г.д. Потым вы ствараеце функцыю, у якасці цела якой трэба пазначыць выраз. Калі ў вас, як і ў мяне, ёсць нізкаўзроўневы граф пераходаў - вам дапаможа кампанент relooper. Наколькі я разумею, выкарыстоўваць высокаўзроўневы кіраванне струменем выканання ў блоку можна, пакуль яно не выходзіць за межы блока – гэта значыць зрабіць унутранае галінаванне . Калі вы вызваляеце relooper, вызваляюцца яго блокі, калі вызваляеце модуль - знікаюць выразы, функцыі і г.д., вылучаныя ў яго арэне.

Зрэшты, калі вы жадаеце інтэрпрэтаваць код на ходу без лішніх стварэнняў і выдаленняў асобніка інтэрпрэтатара, можа мець сэнс вынесці гэтую логіку ў файл на C++, і адтуль непасрэдна кіраваць усім C++ API бібліятэкі, абыходзячы гатовыя абгорткі.

Такім чынам, каб згенераваць код, трэба

// настроить глобальные параметры (можно поменять потом)
BinaryenSetAPITracing(0);

BinaryenSetOptimizeLevel(3);
BinaryenSetShrinkLevel(2);

// создать модуль
BinaryenModuleRef MODULE = BinaryenModuleCreate();

// описать типы функций (как создаваемых, так и вызываемых)
helper_type  BinaryenAddFunctionType(MODULE, "helper-func", BinaryenTypeInt32(), int32_helper_args, ARRAY_SIZE(int32_helper_args));
// (int23_helper_args приоб^Wсоздаются отдельно)

// сконструировать супер-мега выражение
// ... ну тут уж вы как-нибудь сами :)

// потом создать функцию
BinaryenAddFunction(MODULE, "tb_fun", tb_func_type, func_locals, FUNC_LOCALS_COUNT, expr);
BinaryenAddFunctionExport(MODULE, "tb_fun", "tb_fun");
...
BinaryenSetMemory(MODULE, (1 << 15) - 1, -1, NULL, NULL, NULL, NULL, NULL, 0, 0);
BinaryenAddMemoryImport(MODULE, NULL, "env", "memory", 0);
BinaryenAddTableImport(MODULE, NULL, "env", "tb_funcs");

// запросить валидацию и оптимизацию при желании
assert (BinaryenModuleValidate(MODULE));
BinaryenModuleOptimize(MODULE);

… калі што забыўся – прабачце, гэта проста каб прадстаўляць маштабы, а падрабязнасці – яны ў дакументацыі.

А зараз пачынаецца крэкс-фэкс-пекс, прыкладна такі:

static char buf[1 << 20];
BinaryenModuleOptimize(MODULE);
BinaryenSetMemory(MODULE, 0, -1, NULL, NULL, NULL, NULL, NULL, 0, 0);
int sz = BinaryenModuleWrite(MODULE, buf, sizeof(buf));
BinaryenModuleDispose(MODULE);
EM_ASM({
  var module = new WebAssembly.Module(new Uint8Array(wasmMemory.buffer, $0, $1));
  var fptr = $2;
  var instance = new WebAssembly.Instance(module, {
      'env': {
          'memory': wasmMemory,
          // ...
      }
  );
  // и вот уже у вас есть instance!
}, buf, sz);

Каб неяк звязаць паміж сабой свет QEMU і JS і пры гэтым заходзіць у скампіляваныя функцыі хутка, быў створаны масіў (табліца функцый для імпарту ў запускатар), і туды клаліся згенераваныя функцыі. Каб хутка вылічваць азначнік, у якасці яго першапачаткова выкарыстоўваўся азначнік нулявога слова translation block, але потым азначнік, вылічаны па такой формуле стаў проста ўпісвацца ў поле ў struct TranslationBlock.

Дарэчы, дэма (пакуль што з каламутнай ліцэнзіяй) працуе нармальна толькі ў Firefox. Распрацоўнікі Chrome былі неяк не гатовы да таго, што хтосьці захоча ствараць больш за тысячу інстансаў модуляў WebAssembly, таму проста вылучалі па гігабайце віртуальнай адраснай прасторы на кожны…

Пакуль што на гэтым усё. Магчыма, будзе яшчэ адзін артыкул, калі ён камусьці цікавы. А менавіта, засталося як мінімум ўсяго толькі прымусіць працаваць блокавыя прылады. Магчыма, мае сэнс таксама зрабіць кампіляцыю WebAssembly модуляў асінхроннай, як гэта і прынята ў свеце JS, раз ужо ўсё роўна маецца інтэрпрэтатар, які можа гэта ўсё выконваць, пакуль натыўны модуль не готаў.

Напрыканцы загадка: вы сабралі бінарнік на 32-бітнай архітэктуры, але код праз аперацыі з памяццю лезе з Binaryen, кудысьці на стэк ці яшчэ кудысьці ў верхнія 2 Гб 32-бітнай адраснай прасторы. Праблема ў тым, што з пункту гледжання Binaryen гэты зварот па занадта вялікім выніковым адрасе. Як гэта абысці?

Па-адмінску

Я гэта ў выніку не тэставаў, але першая думка была "А што, калі паставіць 32-бітны Linux?" Тады верхняя частка адраснай прасторы будзе занятая ядром. Пытанне толькі ў тым, колькі будзе занята: 1 ці 2 Gb.

Па-праграмісцку (варыянт для практыкаў)

Надзімаем бурбалку ў верхняй частцы адраснай прасторы. Я сам не разумею, чаму яно працуе - там жа ўжо павінен быць стэк. Але "мы практыкі: у нас усё працуе, але ніхто не ведае чаму…".

// 2gbubble.c
// Usage: LD_PRELOAD=2gbubble.so <program>

#include <sys/mman.h>
#include <assert.h>

void __attribute__((constructor)) constr(void)
{
  assert(MAP_FAILED != mmap(1u >> 31, (1u >> 31) - (1u >> 20), PROT_NONE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0));
}

… з Valgrind-ым, праўда, не сумяшчальна, але, на шчасце, Valgrind сам вельмі эфектыўна адтуль усіх выцясняе 🙂

Магчыма, нехта дасць лепшае тлумачэнне, як працуе гэты мой код…

Крыніца: habr.com

Дадаць каментар