QEMU.js. այժմ լուրջ և WASM-ի հետ

Ժամանակին ես որոշեցի զվարճանալու համար ապացուցել գործընթացի շրջելիությունը և սովորեք, թե ինչպես ստեղծել JavaScript (ավելի ճիշտ՝ Asm.js) մեքենայի կոդից։ Փորձի համար ընտրվեց QEMU-ն, իսկ որոշ ժամանակ անց հոդված գրվեց Habr-ում։ Մեկնաբանություններում ինձ խորհուրդ տվեցին վերաիմաստավորել նախագիծը WebAssembly-ում և նույնիսկ ինքս լքել գրեթե ավարտված Ես ինչ-որ կերպ չէի ուզում նախագիծը... Աշխատանքն ընթանում էր, բայց շատ դանդաղ, և հիմա, վերջերս այդ հոդվածում հայտնվեց. մեկնաբանություն «Ուրեմն ինչպե՞ս ավարտվեց ամեն ինչ» թեմայով: Ի պատասխան իմ մանրամասն պատասխանի՝ ես լսեցի «Սա հոդվածի նման է հնչում»։ Դե եթե կարող ես հոդված կլինի։ Միգուցե ինչ-որ մեկին դա օգտակար կլինի: Դրանից ընթերցողը կսովորի որոշ փաստեր QEMU կոդերի ստեղծման հետին պլանների նախագծման մասին, ինչպես նաև այն մասին, թե ինչպես գրել Just-in-Time կոմպիլյատոր վեբ հավելվածի համար:

խնդիրները

Քանի որ ես արդեն սովորել էի, թե ինչպես «ինչ-որ կերպ» տեղափոխել QEMU JavaScript-ին, այս անգամ որոշվեց դա անել խելամտորեն և չկրկնել հին սխալները:

Սխալ թիվ մեկ. ճյուղավորվում է կետի թողարկումից

Իմ առաջին սխալը 2.4.1-ի վերին հոսանքով տարբերակից պատառաքաղելն էր: Հետո ինձ թվում էր լավ գաղափար. եթե կա կետի թողարկում, ապա այն հավանաբար ավելի կայուն է, քան պարզ 2.4-ը, և նույնիսկ ավելին, ճյուղը: master. Եվ քանի որ ես նախատեսում էի ավելացնել իմ սեփական սխալների բավականին մեծ քանակություն, ես ընդհանրապես ուրիշի կարիքը չունեի: Հավանաբար այդպես է ստացվել։ Բայց ահա բանը. QEMU-ն տեղում չի կանգնում, և ինչ-որ պահի նրանք նույնիսկ հայտարարեցին 10 տոկոսով գեներացված կոդի օպտիմալացում: «Այո, հիմա ես սառեցնելու եմ», - մտածեցի ես և կոտրվեցի: Այստեղ մենք պետք է շեղում կատարենք. QEMU.js-ի միալարային բնույթի և այն փաստի պատճառով, որ սկզբնական QEMU-ն չի ենթադրում բազմաշերտության բացակայություն (այսինքն՝ միաժամանակ մի քանի անկապ կոդերի ուղի գործարկելու հնարավորություն, և ոչ միայն «օգտագործել բոլոր միջուկները») դրա համար կարևոր է, թելերի հիմնական գործառույթները ես ստիպված էի «պարզել», որպեսզի կարողանայի դրսից կանչել: Սա միաձուլման ժամանակ որոշ բնական խնդիրներ ստեղծեց: Այնուամենայնիվ, այն, որ որոշ փոփոխություններ մասնաճյուղից master, որի հետ ես փորձեցի միաձուլել իմ կոդը, նույնպես ընտրված էին բալը կետի թողարկումում (և հետևաբար իմ մասնաճյուղում) նույնպես, հավանաբար, չէր ավելացնի հարմարություն:

Ընդհանրապես, ես որոշեցի, որ դեռ իմաստ ունի դուրս նետել նախատիպը, ապամոնտաժել այն մասերի համար և զրոյից կառուցել նոր տարբերակ՝ հիմնվելով ավելի թարմ բանի վրա և այժմ master.

Սխալ թիվ երկու. TLP մեթոդաբանություն

Սա, ըստ էության, սխալ չէ, ընդհանուր առմամբ, դա ուղղակի նախագիծ ստեղծելու հատկանիշ է թե «որտե՞ղ և ինչպե՞ս շարժվել» և ընդհանրապես «կհասնե՞նք այնտեղ» լիակատար թյուրիմացության պայմաններում։ Այս պայմաններում անշնորհք ծրագրավորում արդարացված տարբերակ էր, բայց, բնականաբար, չուզեցի անտեղի կրկնել։ Այս անգամ ես ուզում էի դա անել խելամտորեն. ատոմային պարտավորություններ, գիտակցված կոդի փոփոխություններ (և ոչ թե «պատահական նիշերը միասին շարել, մինչև այն կազմվի (նախազգուշացումներով)», ինչպես Լինուս Տորվալդսը մի անգամ ասել է ինչ-որ մեկի մասին, ըստ Վիքիքաղվածքի) և այլն։

Սխալ թիվ երեք՝ ջուր մտնել՝ առանց ֆորդին իմանալու

Ես դեռ ամբողջությամբ չեմ ազատվել սրանից, բայց հիմա որոշել եմ ընդհանրապես չգնալ նվազագույն դիմադրության ճանապարհով և դա անել «մեծահասակի կարգավիճակում», այսինքն՝ գրել զրոյից իմ TCG ֆոնդը, որպեսզի չգնամ: պետք է ավելի ուշ ասեմ. «Այո, սա, իհարկե, դանդաղ է, բայց ես չեմ կարող վերահսկել ամեն ինչ, այսպես է գրված TCI...»: Ավելին, սա ի սկզբանե թվում էր ակնհայտ լուծում, քանի որ Ես ստեղծում եմ երկուական կոդ. Ինչպես ասում են՝ «Գենտը հավաքվեցуկոդը, իհարկե, երկուական է, բայց հսկողությունը չի կարող պարզապես փոխանցվել դրան. այն պետք է բացահայտորեն մտցվի բրաուզերի մեջ՝ կոմպիլյացիայի համար, ինչի արդյունքում JS աշխարհից որոշակի օբյեկտ է առաջանում, որը դեռ պետք է ինչ-որ տեղ փրկվել. Այնուամենայնիվ, սովորական RISC ճարտարապետություններում, որքան ես հասկանում եմ, տիպիկ իրավիճակ է վերականգնված կոդի համար հրահանգների քեշը հստակորեն վերականգնելու անհրաժեշտությունը, եթե դա այն չէ, ինչ մեզ պետք է, ապա, ամեն դեպքում, այն մոտ է: Բացի այդ, իմ վերջին փորձից ես իմացա, որ վերահսկողությունը կարծես թե չի փոխանցվում թարգմանության բլոկի կեսին, այնպես որ մենք իրականում կարիք չունենք որևէ օֆսեթից մեկնաբանված բայթկոդի, և մենք կարող ենք պարզապես այն ստեղծել տուբերկուլյոզի վրա գործող ֆունկցիայից: .

Եկան, ոտքերով խփեցին

Թեև ես սկսեցի վերաշարադրել կոդը դեռ հուլիսին, մի կախարդական հարված սողաց աննկատ. հանկարծակի նշեք թեմայում Binaryen որպես qemu backend համատեքստում՝ «Նման բան է արել, գուցե մի բան ասի»։ Մենք խոսում էինք Emscripten-ի հարակից գրադարանից օգտվելու մասին Երկուական ստեղծել WASM JIT: Դե, ես ասացի, որ դուք այնտեղ ունեք Apache 2.0 լիցենզիա, և QEMU-ն ամբողջությամբ տարածված է GPLv2-ի տակ, և դրանք այնքան էլ համատեղելի չեն: Հանկարծ պարզվեց, որ լիցենզիա կարող է լինել ինչ-որ կերպ շտկել (Ես չգիտեմ. միգուցե փոխել այն, գուցե երկակի լիցենզավորում, գուցե մեկ այլ բան…): Սա, իհարկե, ուրախացրեց ինձ, քանի որ այդ ժամանակ ես արդեն ուշադիր նայել էի երկուական ձևաչափ WebAssembly, և ես ինչ-որ կերպ տխուր և անհասկանալի էի: Նաև կար գրադարան, որը կխժռեր հիմնական բլոկները անցումային գրաֆիկով, կարտադրեր բայթկոդը և նույնիսկ անհրաժեշտության դեպքում գործարկեց այն հենց թարգմանիչում:

Հետո ավելին կար նամակը QEMU փոստային ցուցակում, բայց սա ավելի շատ վերաբերում է այն հարցին, թե «ում է այն ամեն դեպքում պետք»: Եվ այդպես է հանկարծակի, պարզվեց, որ դա անհրաժեշտ էր։ Նվազագույնը, դուք կարող եք քերել միասին օգտագործման հետևյալ հնարավորությունները, եթե այն քիչ թե շատ արագ աշխատի.

  • ուսումնական ինչ-որ բան գործարկել առանց որևէ տեղադրման
  • վիրտուալացում iOS-ում, որտեղ, ըստ լուրերի, միակ հավելվածը, որն իրավունք ունի արագորեն կոդ ստեղծելու, JS շարժիչն է (դա ճի՞շտ է):
  • mini-OS-ի ցուցադրում` մեկ անգործունյա, ներկառուցված, բոլոր տեսակի որոնվածային ծրագրերի և այլն...

Բրաուզերի Runtime-ի առանձնահատկությունները

Ինչպես արդեն ասացի, QEMU-ն կապված է բազմաթելային, բայց զննարկիչը չունի այն: Դե, այսինքն՝ ոչ... Սկզբում այն ​​ընդհանրապես գոյություն չուներ, հետո հայտնվեց WebWorkers-ը, որքան ես հասկացա, սա բազմաշերտ է՝ հիմնված հաղորդագրությունների փոխանցման վրա: առանց ընդհանուր փոփոխականների. Բնականաբար, դա զգալի խնդիրներ է ստեղծում, երբ փոխադրվում է գոյություն ունեցող կոդը՝ հիմնված ընդհանուր հիշողության մոդելի վրա: Հետո հանրային ճնշման տակ այն իրականացվեց անվան տակ SharedArrayBuffers. Այն աստիճանաբար ներմուծվեց, տարբեր բրաուզերներում նշում էին դրա մեկնարկը, հետո նշում էին Նոր տարին, հետո Meltdown... Որից հետո եկան այն եզրակացության, որ ժամանակի չափումը կոպիտ կամ կոպիտ է, բայց ընդհանուր հիշողության և ա. թելն ավելացնում է հաշվիչը, միեւնույն է դա բավականին ճշգրիտ կստացվի. Այսպիսով, մենք անջատեցինք բազմալեզու աշխատանքը ընդհանուր հիշողությամբ: Թվում է, թե նրանք ավելի ուշ նորից միացրեցին այն, բայց, ինչպես պարզ դարձավ առաջին փորձից, կյանք կա առանց դրա, և եթե այո, մենք կփորձենք դա անել՝ առանց հենվելու բազմաթելային:

Երկրորդ հատկանիշը ստեկի հետ ցածր մակարդակի մանիպուլյացիաների անհնարինությունն է. դուք չեք կարող պարզապես վերցնել, պահպանել ընթացիկ համատեքստը և անցնել նորին նոր կույտով: Զանգերի կույտը կառավարվում է JS վիրտուալ մեքենայի կողմից: Կարծես թե ո՞րն է խնդիրը, քանի որ մենք դեռ որոշել ենք նախկին հոսքերը ամբողջությամբ ձեռքով կառավարել։ Փաստն այն է, որ QEMU-ում I/O-ի բլոկն իրականացվում է կորուտինների միջոցով, և հենց այստեղ են ցածր մակարդակի stack-ի մանիպուլյացիաները: Բարեբախտաբար, Emscipten-ն արդեն պարունակում է ասինխրոն գործողությունների մեխանիզմ, նույնիսկ երկու. Ասինկիզացնել и Կամարկիչ. Առաջինն աշխատում է առաջացած JavaScript կոդում զգալի փչումով և այլևս չի աջակցվում: Երկրորդը ներկայիս «ճիշտ ճանապարհն» է և աշխատում է բայթկոդերի ստեղծման միջոցով հայրենի թարգմանչի համար: Այն աշխատում է, իհարկե, դանդաղ, բայց չի փչում կոդը: Ճիշտ է, այս մեխանիզմի համար cooutines-ի աջակցությունը պետք է իրականացվեր ինքնուրույն (Արդեն կային կորուտիններ գրված Asyncify-ի համար և կար մոտավորապես նույն API-ի ներդրում Emterpreter-ի համար, պարզապես անհրաժեշտ էր դրանք միացնել):

Այս պահին ինձ դեռ չի հաջողվել WASM-ում կազմված և Emterpreter-ի միջոցով մեկնաբանված կոդը բաժանել, այնպես որ բլոկ սարքերը դեռ չեն աշխատում (տես հաջորդ սերիայում, ինչպես ասում են...): Այսինքն, վերջում դուք պետք է ստանաք այս զվարճալի շերտավոր բանի նման մի բան.

  • մեկնաբանված բլոկ I/O. Դե, իսկապե՞ս սպասում էիք նմանակված NVMe-ին հայրենի կատարմամբ: 🙂
  • ստատիկ կերպով կազմված հիմնական QEMU կոդը (թարգմանիչ, այլ նմանակված սարքեր և այլն)
  • դինամիկ կերպով կազմված հյուրի կոդը WASM-ում

QEMU աղբյուրների առանձնահատկությունները

Ինչպես հավանաբար արդեն կռահեցիք, հյուրի ճարտարապետությունների նմանակման կոդը և հյուրընկալող մեքենայի հրահանգների ստեղծման կոդը առանձնացված են QEMU-ում: Իրականում, դա նույնիսկ մի փոքր ավելի բարդ է.

  • կան հյուր ճարտարապետներ
  • կա արագացուցիչներ, մասնավորապես՝ KVM՝ Linux-ում ապարատային վիրտուալիզացիայի համար (հյուր և հյուրընկալող համակարգերի համար, որոնք համատեղելի են միմյանց հետ), TCG՝ JIT կոդի ստեղծման համար՝ ցանկացած վայրում։ Սկսած QEMU 2.9-ից, հայտնվեց Windows-ում HAXM ապարատային վիրտուալացման ստանդարտի աջակցությունը (Մանրամասն)
  • եթե օգտագործվում է TCG և ոչ թե ապարատային վիրտուալացում, ապա այն ունի կոդերի ստեղծման առանձին աջակցություն յուրաքանչյուր հոսթ ճարտարապետի, ինչպես նաև ունիվերսալ թարգմանչի համար:
  • ... և այս ամենի շուրջ՝ նմանակված ծայրամասային սարքեր, օգտատիրոջ ինտերֆեյս, միգրացիա, ձայնագրման կրկնություն և այլն:

Ի դեպ, դուք գիտեի՞ք. QEMU-ն կարող է ընդօրինակել ոչ միայն ամբողջ համակարգիչը, այլ նաև պրոցեսորը հյուրընկալող միջուկում օգտագործողի առանձին գործընթացի համար, որն օգտագործվում է, օրինակ, AFL fuzzer-ի կողմից երկուական գործիքավորման համար: Միգուցե ինչ-որ մեկը կցանկանար տեղափոխել QEMU-ի այս ռեժիմը JS-ում: 😉

Ինչպես վաղեմի անվճար ծրագրերի մեծ մասը, QEMU-ն ստեղծվում է զանգի միջոցով configure и make. Ենթադրենք՝ որոշել եք ինչ-որ բան ավելացնել՝ TCG backend, թելերի իրականացում, այլ բան: Մի շտապեք ուրախանալ/սարսափվել (ըստ անհրաժեշտության ընդգծեք) Autoconf-ի հետ շփվելու հեռանկարում, իրականում, configure QEMU-ները, ըստ երևույթին, ինքնագրված են և չեն առաջանում որևէ բանից:

Վեբ-հավաքույթ

Այսպիսով, ի՞նչ է այս բանը կոչվում WebAssembly (aka WASM): Սա փոխարինում է Asm.js-ին՝ այլևս չձևացնելով, որ վավեր JavaScript կոդ է: Ընդհակառակը, այն զուտ երկուական է և օպտիմիզացված, և նույնիսկ դրա մեջ ամբողջ թիվ գրելն այնքան էլ պարզ չէ. կոմպակտության համար այն պահվում է ձևաչափով: ԼԻԲ 128.

Հնարավոր է, որ դուք լսել եք Asm.js-ի վերամշակման ալգորիթմի մասին. սա հոսքի կառավարման «բարձր մակարդակի» հրահանգների վերականգնումն է (այսինքն, եթե-ապա-ուրիշ, հանգույցներ և այլն), որի համար նախատեսված են JS շարժիչները, սկսած: ցածր մակարդակի LLVM IR-ը, ավելի մոտ պրոցեսորի կողմից կատարված մեքենայի կոդին: Բնականաբար, QEMU-ի միջանկյալ ներկայացվածությունը ավելի մոտ է երկրորդին։ Թվում է, թե այստեղ է, բայթկոդը, տանջանքի վերջը... Եվ հետո կան բլոկներ, եթե-ապա-ուրիշ և օղակներ:

Եվ սա ևս մեկ պատճառ է, թե ինչու է Binaryen-ը օգտակար. այն բնականաբար կարող է ընդունել բարձր մակարդակի բլոկներ մոտ այն, ինչ կպահվի WASM-ում: Բայց այն կարող է նաև կոդ արտադրել հիմնական բլոկների գրաֆիկից և դրանց միջև անցումներից: Դե, ես արդեն ասացի, որ այն թաքցնում է WebAssembly պահեստավորման ձևաչափը հարմար C/C++ API-ի հետևում։

TCG (Tiny Code Generator)

GCT սկզբնապես էր backend-ը C կոմպիլյատորի համար: Այնուհետև, ըստ երևույթին, այն չկարողացավ դիմակայել GCC-ի հետ մրցակցությանը, բայց ի վերջո այն գտավ իր տեղը QEMU-ում՝ որպես հյուրընկալող հարթակի համար կոդերի ստեղծման մեխանիզմ: Գոյություն ունի նաև TCG backend, որը ստեղծում է որոշ վերացական բայթկոդ, որն անմիջապես գործարկվում է թարգմանչի կողմից, բայց ես որոշեցի այս անգամ խուսափել այն օգտագործելուց: Այնուամենայնիվ, այն փաստը, որ QEMU-ում արդեն հնարավոր է հնարավորություն տալ անցումը գեներացված տուբերկուլյոզին ֆունկցիայի միջոցով. tcg_qemu_tb_exec, ինձ համար շատ օգտակար է ստացվել։

QEMU-ին նոր TCG backend ավելացնելու համար դուք պետք է ստեղծեք ենթատեղեկատու tcg/<имя архитектуры> (այս դեպքում, tcg/binaryen), և այն պարունակում է երկու ֆայլ. tcg-target.h и tcg-target.inc.c и գրանցել ամեն ինչի մասին է configure. Դուք կարող եք այնտեղ տեղադրել այլ ֆայլեր, բայց, ինչպես կարող եք կռահել այս երկուսի անուններից, նրանք երկուսն էլ կներառվեն ինչ-որ տեղ. մեկը որպես սովորական վերնագրի ֆայլ (այն ներառված է tcg/tcg.h, և այդ մեկն արդեն կա գրացուցակների այլ ֆայլերում tcg, accel և ոչ միայն), մյուսը՝ միայն որպես կոդի հատված tcg/tcg.c, բայց այն հասանելի է իր ստատիկ գործառույթներին:

Որոշելով, որ ես չափազանց շատ ժամանակ կծախսեմ մանրամասն ուսումնասիրությունների վրա, թե ինչպես է այն աշխատում, ես պարզապես պատճենեցի այս երկու ֆայլերի «կմախքները» մեկ այլ հետին պլանի իրականացումից՝ անկեղծորեն նշելով դա լիցենզիայի վերնագրում:

ֆայլ tcg-target.h պարունակում է հիմնականում ձևի կարգավորումներ #define-ներ:

  • քանի ռեգիստր և ինչ լայնություն կա թիրախային ճարտարապետության վրա (մենք ունենք այնքան, որքան ուզում ենք, այնքան, որքան ուզում ենք. հարցն այն է, թե ինչ կստեղծվի ավելի արդյունավետ կոդով բրաուզերի կողմից «ամբողջովին թիրախային» ճարտարապետության վրա: ...)
  • հյուրընկալող հրահանգների հավասարեցում. x86-ում և նույնիսկ TCI-ում հրահանգները բոլորովին հավասարեցված չեն, բայց ես պատրաստվում եմ կոդի բուֆերի մեջ դնել ոչ թե հրահանգներ, այլ Binaryen գրադարանի կառուցվածքների ցուցիչներ, այնպես որ ես կասեմ. բայթեր
  • ինչ կամընտիր հրահանգներ կարող է ստեղծել backend-ը. մենք ներառում ենք այն ամենը, ինչ մենք գտնում ենք Binaryen-ում, թույլ ենք տալիս արագացուցչին ինքն իրեն բաժանել մնացածը ավելի պարզների:
  • Որքա՞ն է TLB քեշի մոտավոր չափը, որը պահանջվում է հետնամասի կողմից: Փաստն այն է, որ QEMU-ում ամեն ինչ լուրջ է. չնայած կան օգնական գործառույթներ, որոնք կատարում են բեռնում/պահում` հաշվի առնելով հյուրի MMU-ն (որտե՞ղ էինք մենք հիմա առանց դրա), նրանք պահպանում են իրենց թարգմանական քեշը կառուցվածքի տեսքով, որոնց մշակումը հարմար է ուղղակիորեն հեռարձակվող բլոկների մեջ տեղադրելու համար: Հարցն այն է, թե այս կառուցվածքում ո՞ր օֆսեթն է ամենաարդյունավետ մշակվում հրամանների փոքր և արագ հաջորդականությամբ:
  • այստեղ դուք կարող եք կսմթել մեկ կամ երկու վերապահված ռեգիստրների նպատակը, միացնել ֆունկցիայի միջոցով տուբերկուլյոզի զանգը և ցանկության դեպքում նկարագրել մի քանի փոքր inline- այնպիսի գործառույթներ, ինչպիսիք են flush_icache_range (բայց սա մեր դեպքը չէ)

ֆայլ tcg-target.inc.c, իհարկե, սովորաբար չափսերով շատ ավելի մեծ է և պարունակում է մի քանի պարտադիր գործառույթներ.

  • սկզբնավորումը, ներառյալ սահմանափակումները, թե որ օպերանդների վրա կարող են գործել հրահանգները: Իմ կողմից կոպտորեն պատճենված է մեկ այլ հետնամասից
  • ֆունկցիա, որը վերցնում է մեկ ներքին բայթկոդի հրահանգ
  • Այստեղ կարող եք նաև տեղադրել օժանդակ գործառույթներ, ինչպես նաև կարող եք օգտագործել ստատիկ գործառույթներ tcg/tcg.c

Ինքս ինձ համար ընտրեցի հետևյալ ռազմավարությունը. հաջորդ թարգմանության բլոկի առաջին բառերում ես գրեցի չորս ցուցիչ՝ մեկնարկային նշան (որոշակի արժեք մոտակայքում 0xFFFFFFFF, որը որոշեց տուբերկուլյոզի ներկայիս վիճակը), համատեքստը, գեներացված մոդուլը և վրիպազերծման կախարդական համարը: Սկզբում նշանը տեղադրվեց 0xFFFFFFFF - nՈրտեղ n - փոքր դրական թիվ, և ամեն անգամ, երբ այն կատարվում էր թարգմանչի միջոցով, ավելանում էր 1-ով: Երբ հասավ. 0xFFFFFFFEԿազմումը տեղի ունեցավ, մոդուլը պահպանվեց գործառույթների աղյուսակում, ներմուծվեց փոքրիկ «գործարկիչ», որի մեջ կատարվեց tcg_qemu_tb_exec, և մոդուլը հեռացվեց QEMU հիշողությունից:

Դասականներին վերափոխելու համար՝ «Հենակ, որքան է այս ձայնի մեջ միահյուսված պրոգերի սրտի համար...»: Այնուամենայնիվ, հիշողությունը ինչ-որ տեղ ծորում էր։ Ավելին, այն կառավարվում էր QEMU-ի կողմից: Ես ունեի կոդ, որը հաջորդ հրահանգը գրելիս (լավ, այսինքն՝ ցուցիչ), ջնջում էր նրան, ում հղումն ավելի վաղ այս տեղում էր, բայց դա չօգնեց։ Իրականում, ամենապարզ դեպքում, QEMU-ն տեղաբաշխում է հիշողությունը գործարկման ժամանակ և այնտեղ գրում է ստեղծված կոդը: Երբ բուֆերը վերջանում է, կոդը դուրս է նետվում, և հաջորդը սկսում է գրվել իր տեղում։

Կոդն ուսումնասիրելուց հետո ես հասկացա, որ կախարդական թվով հնարքն ինձ թույլ է տվել չտապալվել կույտի ոչնչացման ժամանակ՝ առաջին իսկ անցումում չնախաստորագրված բուֆերի վրա սխալ բան ազատելով: Բայց ո՞վ է վերագրում բուֆերը՝ հետագայում իմ ֆունկցիան շրջանցելու համար: Ինչպես խորհուրդ են տալիս Emscripten-ի ծրագրավորողները, երբ ես խնդիր հանդիպեցի, ստացված կոդը հետ տեղափոխեցի հայրենի հավելված, դրա վրա դրեցի Mozilla Record-Replay... Ընդհանրապես, վերջում ես հասկացա մի պարզ բան՝ յուրաքանչյուր բլոկի համար. ա struct TranslationBlock իր նկարագրությամբ։ Գուշակիր, որտեղ... Ճիշտ է, հենց բուֆերի մեջ գտնվող բլոկի առաջ: Հասկանալով դա՝ ես որոշեցի հրաժարվել հենակներ օգտագործելուց (առնվազն մի քանիսը) և պարզապես դուրս նետեցի կախարդական համարը և մնացած բառերը փոխանցեցի struct TranslationBlock, ստեղծելով միայնակ կապակցված ցուցակ, որը կարող է արագ անցնել, երբ թարգմանության քեշը վերակայվում է, և ազատել հիշողությունը:

Որոշ հենակներ մնում են. օրինակ՝ նշված ցուցիչներ կոդի բուֆերում. դրանցից մի քանիսը պարզապես BinaryenExpressionRef, այսինքն՝ նրանք նայում են արտահայտություններին, որոնք պետք է գծային կերպով տեղադրվեն գեներացված հիմնական բլոկի մեջ, մի մասը BB-ների միջև անցման պայմանն է, մի մասը՝ ուր գնալ։ Դե, Relooper-ի համար արդեն պատրաստված բլոկներ կան, որոնք պետք է միացվեն ըստ պայմանների։ Դրանք տարբերելու համար օգտագործվում է այն ենթադրությունը, որ դրանք բոլորը հավասարեցված են առնվազն չորս բայթով, այնպես որ դուք կարող եք ապահով օգտագործել պիտակի համար ամենաքիչ կարևոր երկու բիթերը, պարզապես անհրաժեշտ է հիշել, որ անհրաժեշտության դեպքում այն ​​հանեք: Ի դեպ, նման պիտակներ արդեն օգտագործվում են QEMU-ում՝ նշելու համար TCG հանգույցից դուրս գալու պատճառը։

Օգտագործելով Binaryen

WebAssembly-ի մոդուլները պարունակում են գործառույթներ, որոնցից յուրաքանչյուրը պարունակում է մարմին, որը արտահայտություն է: Արտահայտությունները միատարր և երկուական գործողություններ են, այլ արտահայտությունների ցուցակներից բաղկացած բլոկներ, հսկիչ հոսք և այլն: Ինչպես արդեն ասացի, այստեղ վերահսկման հոսքը կազմակերպվում է հենց որպես բարձր մակարդակի ճյուղեր, հանգույցներ, ֆունկցիաների կանչեր և այլն: Գործառույթների արգումենտները փոխանցվում են ոչ թե փաթեթի վրա, այլ բացահայտորեն, ինչպես JS-ում: Կան նաև գլոբալ փոփոխականներ, բայց ես դրանք չեմ օգտագործել, ուստի ես ձեզ չեմ պատմի դրանց մասին:

Ֆունկցիաներն ունեն նաև տեղային փոփոխականներ՝ համարակալված զրոյից, տեսակի՝ int32 / int64 / float / double: Այս դեպքում առաջին n տեղային փոփոխականները ֆունկցիային փոխանցված արգումենտներն են։ Խնդրում ենք նկատի ունենալ, որ չնայած այստեղ ամեն ինչ ամբողջովին ցածր մակարդակի չէ կառավարման հոսքի առումով, ամբողջ թվերը դեռ չեն կրում «ստորագրված/չստորագրված» հատկանիշը. այն, թե ինչպես է համարը վարվում, կախված է գործողության կոդից:

Ընդհանուր առմամբ, Binaryen ապահովում է պարզ C-APIԴուք ստեղծում եք մոդուլ, նրա մեջ ստեղծել արտահայտություններ՝ միատարր, երկուական, այլ արտահայտություններից բլոկներ, վերահսկիչ հոսք և այլն: Այնուհետև դուք ստեղծում եք ֆունկցիա, որի մարմինն է արտահայտությունը: Եթե ​​դուք, ինչպես ինձ, ունեք անցումային ցածր մակարդակի գրաֆիկ, ապա relooper բաղադրիչը կօգնի ձեզ: Որքան ես հասկանում եմ, հնարավոր է օգտագործել կատարողական հոսքի բարձր մակարդակի հսկողությունը բլոկում, քանի դեռ այն չի անցնում բլոկի սահմաններից, այսինքն՝ հնարավոր է ներքին արագ ուղի/դանդաղ դարձնել։ ճանապարհը ճյուղավորվում է ներկառուցված TLB քեշի մշակման կոդի ներսում, բայց չխանգարելու «արտաքին» կառավարման հոսքին: Երբ դուք ազատում եք 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);

... եթե ինչ-որ բան մոռացել եմ, կներեք, սա ընդամենը մասշտաբը ներկայացնելու համար է, իսկ մանրամասները փաստաթղթերում են:

Եվ հիմա սկսվում է crack-fex-pex-ը, մոտավորապես այսպես.

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-ի աշխարհները և միևնույն ժամանակ արագ մուտք գործենք կազմված գործառույթները, ստեղծվեց զանգված (գործառույթների աղյուսակ՝ գործարկիչ ներմուծելու համար), և այնտեղ տեղադրվեցին գեներացված գործառույթները։ Ցուցանիշն արագ հաշվարկելու համար սկզբում օգտագործվել է զրոյական բառի թարգմանության բլոկի ինդեքսը, սակայն այնուհետև այս բանաձևով հաշվարկված ցուցանիշը սկսել է պարզապես տեղավորվել դաշտում. struct TranslationBlock.

Ի դեպ, Demo (ներկայումս պղտոր լիցենզիայով) լավ է աշխատում միայն Firefox-ում: Chrome-ի մշակողները էին ինչ-որ կերպ պատրաստ չէ այն փաստին, որ ինչ-որ մեկը կցանկանար ստեղծել WebAssembly մոդուլների հազարից ավելի օրինակներ, ուստի նրանք պարզապես հատկացրին մեկ գիգաբայթ վիրտուալ հասցեի տարածք յուրաքանչյուրի համար...

Առայժմ այսքանը: Թերևս մեկ այլ հոդված կլինի, եթե որևէ մեկին հետաքրքրի։ Մասնավորապես, մնում է առնվազն արդար բլոկ սարքերը աշխատեցնել: Կարող է նաև իմաստ ունենալ WebAssembly մոդուլների կոմպիլյացիան դարձնել ասինխրոն, ինչպես ընդունված է JS աշխարհում, քանի որ դեռևս կա թարգմանիչ, որը կարող է անել այս ամենը, քանի դեռ մայրենի մոդուլը պատրաստ չէ:

Վերջապես մի հանելուկ. դուք երկուական եք կազմել 32-բիթանոց ճարտարապետության վրա, բայց կոդը, հիշողության գործողությունների միջոցով, բարձրանում է Binaryen-ից, ինչ-որ տեղ բուրգի վրա կամ ինչ-որ տեղ 2-բիթանոց հասցեների տարածության վերին 32 ԳԲ-ում: Խնդիրն այն է, որ Binaryen-ի տեսանկյունից սա չափազանց մեծ արդյունքի հասցե մուտք է գործում: Ինչպե՞ս շրջանցել սա:

Ադմինիստրատորի ձևով

Ես չփորձեցի սա, բայց իմ առաջին միտքն էր «Իսկ եթե ես տեղադրեի 32-բիթանոց Linux»: Այնուհետև հասցեի տարածության վերին մասը կզբաղեցնի միջուկը։ Հարցը միայն այն է, թե որքան կզբաղեցվի՝ 1 թե 2 Գբ։

Ծրագրավորողի ձևով (տարբերակ պրակտիկանտների համար)

Եկեք փուչիկ փչենք հասցեի տարածության վերևում: Ես ինքս չեմ հասկանում, թե ինչու է դա աշխատում, այնտեղ արդեն պետք է լինի կույտ: Բայց «մենք պրակտիկանտներ ենք. մեզ մոտ ամեն ինչ աշխատում է, բայց ոչ ոք չգիտի, թե ինչու…»:

// 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-ը շատ արդյունավետ կերպով դուրս է մղում բոլորին այնտեղից :)

Միգուցե ինչ-որ մեկը ավելի լավ բացատրի, թե ինչպես է աշխատում իմ այս կոդը...

Source: www.habr.com

Добавить комментарий