Kaua aega tagasi otsustasin nalja pärast tõestada protsessi vastupidavust ja õppida genereerima JavaScripti (täpsemalt, Asm.js) masinkoodist. Eksperimendi jaoks valiti QEMU, mõne aja pärast kirjutati Habrisse artikkel. Kommentaarides soovitati mul projekt WebAssembly'ks ümber teha, ja endal ei olnud kuidagi tahtmist lõpetada peaaegu lõpetatud projekti… Töö edenes, kuid tõeliselt aeglaselt, ja hiljuti ilmus selle artikli alla teemal «Ja kuidas kõik lõpuks läks?». Minu põhjalikule vastusele kuulsin «See on artikli vääriline». No, kui on vääriline, siis tuleb artikkel. Võib-olla on see kellelegi kasulik. Sellest artikklist saab lugeja teada teatud fakte QEMU koodigeneratsiooni tagakülgede ülesehituse kohta, samuti kuidas kirjutada Just-in-Time kompilaator veebirakendusele.
Ülesanded
Kuna ma olin juba õppinud «kuidagi» QEMU JavaScripti portima, siis seekord otsustasin teha asju korralikult ja mitte korrata vanu vigu.
Viga number üks: hargneda punktvälja
Minu esimene viga oli hargneda oma versioon upstream versioonist 2.4.1. Tol ajal tundus see hea mõttena: kui punktvälja eksisteerib, siis ilmselt on see stabiilsem kui tavaline 2.4, rääkimata harust master. Ja kuna ma plaanisin lisada hulga oma vigu, siis teiste virised mulle üldse ei sobinud. Nii see ilmselt läks. Kuid probleem oli selles, et QEMU ei seisa paigal, ja mingil hetkel kuulutati seal isegi välja genereeritud koodi optimeerimine 10%. «Ahah, nüüd ma saadan,» mõtlesin ma ja olin petetud. Siin tuleb teha kõrvalpöörde: seoses QEMU.js'iga, millel on ühesuunaline iseloom ja selle originaali puhul ei eeldata mitme-ühe töö puudumist (see tähendab, et objektiivne on samaaegne töö mitmete eraldiseisvate koodide tee, mitte lihtsalt «kasutada kõiki tuumasid»), peamiste funktsioonide vooge tuli «väljastada» selleks, et neid saaks väljastpoolt kutsuda. See tekitas loomulikult teatud probleeme liitmise korral. Küll aga asjaolu, et osa muutustest harust master, millega üritasin oma koodi liita, oli samuti cherry picked punktvälja (ja seega ka minu haru) ei ole ilmselt mugavust juurde lisanud.
Kokkuvõttes otsustasin, et prototüüp on ikka mõistlik välja visata, võtta lahti ning ehitada uus versioon nullist millegi uuema põhjal, ja nüüd juba master.
Viga number kaks: TLP-metoodika
Põhimõtteliselt ei ole see viga, pigem on see lihtsalt projektihalduse eripära, kui ei mõistetud, kuhu ja kuidas liikuda, kas üldse jõuame lõpuni. kärp-programmeerimine oli õigustatud valik, kuid loomulikult ei soovinud ma seda ilma vajaduseta korrata. Seekord tahtsin teha asju õigesti: aatomilised commitid, teadlikud koodimuudatused (mitte „tallad juhuslikke märke kokku, kuni see kompileerib (hoiatustega)”, nagu kunagi ütles Linus Torvalds, kui uskuda Viki tsitaate) jne.
Viga number kolm: mitte teadmatult vette hüpata
Sellest pole ma siiani täielikult vabanenud, kuid nüüd otsustasin, et ei lähe enam kõige väiksema vastupanuga teed ja teen „täiskasvanu moodi”, nimelt kirjutan oma TCG backend'i nullist, et hiljem mitte öelda: „Jah, see on muidugi aeglane, aga ma ei saa kõike juhtida - TCI on nii kirjutatud...“. Lisaks tundus see esialgu ilmne lahendus, kuna ma genereerin binaarkoodi. Nagu öeldakse, „Kogusin Gent’i, aga vale”: kood on muidugi binaarne, kuid juhtimist sinna lihtsalt nii ei edastata - see tuleb selgelt brauserisse kompileerimiseks suruda, saades tulemuseks mingi JS maailma objekti, mille tuleb veel kuhugi salvestada. Kuid normaalsel RISC-architektuuril on, kui ma õigesti aru saan, tüüpiline olukord, et on vajalik selgelt tühistada juhiste vahemälu uuesti genereeritud koodi jaoks - isegi kui see ei ole see, mida me vajame, on see igal juhul lähedal. Lisaks olen oma eelmise katse käigus õppinud, et juhtimine ei edastata ülemisse tõlkeplokki, seega ei ole bytecode, mida seletatakse igasuguses nihkes, meile eriti vajalik, ja saame lihtsalt genereerida funktsiooni TB-le.onTuli ja koputas
Kuigi hakkasin koodi ümber kirjutama juba juulis, siis maagiline tõuge tuli ootamatult: tavaliselt tulevad GitHub'i e-kirjad nagu teated vastustele Issues ja Pull requests'idesse, aga seekord,
ühtäkki mainimine teemas Binaryen kui QEMU backend Binaryen teisele poole viia. kuidas seda kuidagi parandada (ma ei tea: võib-olla vahetada, võib-olla kahekordne litsentsimine, võib-olla midagi muud…). See tegi mind muidugi rõõmsaks, sest olin juba mitu korda selle peale mõelnud WebAssembly, ja ma tundsin end kuidagi kurvalt ja arusaamatult. Siin oli raamatukogu, mis suutis nii põhielemendid koos siirdediagrammidega kasutada, kui ka väljastada baiti koodi ning isegi käivitada selle tõlgis, kui see oli vajalik.
Hiljem oli seal veel QEMU meililistis, kuid see on juba pigem küsimus, "Kellele see üldse vajalik on?". Ja see oli mainimine teemas, osutus vajalikuks. Vähemalt saab välja mõelda selliseid kasutusvõimalusi, kui see töötab enam-vähem kiiresti:
- mida iganes harivat käivitada ilma paigaldamiseta
- virtualiseerimine iOS-is, kus kuulujutud väidavad, et ainus rakendus, mis on õigustatud koodigeneratsiooniks reaalajas, on JS mootori (kas see on tõsi?)
- mini-OS-i demonstreerimine — ühe ketta, integreeritud, igasugused püsiprogrammid jne...
Brauserikäitlusmoodulite eripära
Nagu ma juba ütlesin, on QEMU üles ehitatud mitme tahvliga, aga brauseris pole seda. Jah, nagu pole… Alguses ei olnud seda üldse, hiljem ilmusid WebWorkers — kuivõrd ma aru saan, siis see on mitme tahvli ülesanne, mis põhineb sõnumite edastamisel ilma ühiskasutatavate muutujaid. Loomulikult tekitab see olulisi probleeme olemasoleva koodi portimisega, mis põhineb jagatud mälu mudelil. Siis avalikkuse surve all see rakendati ja see toodi välja nimetusega SharedArrayBuffers. Seda hakati järk-järgult välja tooma, tähistati selle käivitamist erinevates brauserites, siis tähistati uut aastat ja seejärel Meltdown… Pärast seda jõuti järeldusele, et olgu, mõõtmise ajaga, kas naturaalne aeg või mitte, aga jagatud mälu ja voolu kaudu, mis suurendab loenduri arvu, ikkagi . Nii et nad lülitasid välja mitme tahvli koos jagatud mäluga. Tundub, et nad lülitasid selle hiljem tagasi sisse, aga nagu selgus esimesest eksperimendist, eksisteerib elu ka ilma selleta, ja seetõttu proovime seda teha, loodamata mitme tahvlile.
Teine omadus seisneb madala taseme manipulatsioonide võimatustes virnaga: ei saa lihtsalt võtta, salvestada praegust konteksti ja vahetada uue peene virnaga. Kutsumise virna haldab JS virtuaalmasin. Tundub, et milles on probleem, kui me plaanime juhtida end endiste voogude kaudu täielikult käsitsi? Asi on selles, et QEMU plokk-sisestus/virtuaalmasin on rakendatud korutiinide kaudu, ja seal oleks meile abiks madala taseme manipuleerimised virnaga. Õnneks sisaldab Emscripten juba mehhanismi asünkroonsete operatsioonide jaoks, isegi kahte: ja . Esimene töötab märkimisväärse JavaScripti koodi paisumise kaudu ja ei ole enam toetatud. Teine on praegu "õige viis" ja töötab oma interpreteerija jaoks bytecode'i genereerimise kaudu. Töötab, muidugi, aeglaselt, kuid vähemalt ei paisuta koodi. Tõsi, selle mehhanismi korutiinide toetust tuli ise panustada (seal olid juba korutiinid, mis olid kirjutatud Asyncify alla ning olid rakendused enam-vähem sama API jaoks Emterpreter'i jaoks, tuli need lihtsalt ühendada).
Praeguseks pole ma veel suutnud jagada koodi WASM-iks kompileeritavaks ja Emterpreter'i kaudu tõlgitavaks, mistõttu plokiseadmed ei tööta veel (vaadake järgmistes osades, nagu öeldakse...). Nii et lõpuks peaks saama selline naljakas kihtide koosseis:
- tõlgitav plokk-sisestus. Noh, kas tõesti ootasite emuleeritud NVMe originaaljõudlusega? 🙂
- staatiliselt kompileeritud QEMU põhikood (tõlkija, ülejäänud emuleeritud seadmed jne)
- dünaamiliselt WASM-iks kompileeritav külaliskood
QEMU allikate eripärad
Kuidas te, ilmselt, juba arvasite, on külaliste arhitektuuride emulatsiooni kood ja hosti masinainete genereerimise kood QEMU-s eraldatud. Tegelikult on seal isegi natuke keerulisem:
- on külaliste arhitektuurid
- on kiirendajad, nimelt KVM, mis võimaldab riistvaralist virtualiseerimist Linuxis (ühilduvate külaliste ja hostide süsteemide vahel), TCG JIT-koodigeneerimiseks igal pool. Alates QEMU 2.9 on toetatud HAXM riistvaralise virtualiseerimise standart Windowsis ()
- kui kasutatakse TCG-d, mitte riistvaralist virtualiseerimist, siis on sellel iga hostiarhitektuuri jaoks eraldi koodigeneerimise tugi ning ka universaalsele tõlgendajale.
- … ja kõik selle ümber — emuleeritud perifeeria, kasutajaliides, migratsioon, record-replay jne.
Ütlesin muide, et teate: QEMU suudab emuleerida mitte ainult tervet arvutit, vaid ka protsessorit eraldi kasutaja protsessile hosti tuumas, mida näiteks kasutab fuzzing-tööriist AFL binaaride instrumenteerimiseks. Võib-olla tahab keegi QEMU töötamise režiimi JS-ile portida? 😉
Nagu enamik ammu eksisteerinud vabavaralisi programme, kompileeritakse QEMU üleskutsest. konfigureeri ja teeOletame, et otsustasite midagi juurde lisada: TCG-taust, lõime teisi vooge, midagi muud. Ärge kiirustage rõõmustama / õudust tundma (vajalik allajoonida) suhtlemise võimaluse üle Autoconfigiga — tegelikult, konfigureeri näib, et QEMU-l on oma määramatu ja mitte millestki genereeritud variant.
WebAssembly
Kuid mis asi on WebAssembly (ka WASM)? See on Asm.js asendaja, mis enam ei näe välja nagu valideeritud JavaScripti kood. Vastupidi, see on puhtalt binaarne ja optimeeritud, ning isegi lihtsalt terves arvu kirjutamine ei ole kuigi lihtne: see salvestatakse kompaktsuse jaoks formaadis. .
Võib-olla olete kuulnud Asm.js reloopingu algoritmist — see on «kõrgetaseme» juhtimisjõudude taastamine (st if-then-else, tsüklid jne), mille jaoks JS-mootorid on kohandatud, madalama taseme LLVM IR-ist, mis on lähemal protsessori täitmiseks mõeldud masinkoodile. Loomulikult on QEMU vahepealne esitus lähemal teisele. Tundub, et siin on byte-kood, kannatuste lõpp… Ja siis plokid, if-then-else ja tsüklid!.
Ja see on veel üks põhjus, miks Binaryen on kasulik: see võib loomulikult vastu võtta kõrgema taseme plokke, mis on lähedased sellele, mis salvestatakse WASM-is. Kuid ta suudab ka genereerida koodi baasplokkide ja nende vaheliste üleminekute graafist. Ja nagu ma juba ütlesin, peidab ta WebAssembly salvestusformaadi mugava C/C++ API taga.
TCG (Tiny Code Generator)
TCG C-kompilaatori tagaküljel. Hiljem tundus, et ta ei suutnud GCC-ga konkureerida, kuid leidis lõpuks oma koha QEMU-s, kus ta on koduplatvormi koodigeneratsiooni mehhanism. Samuti on olemas TCG-tagakülg, mis genereerib mingisuguse abstraktse binaarkoodi, mida interpretatiivne tõlgendaja kohe täidab, kuid seekord otsustasin selle kasutamisest loobuda. Ent see, et QEMU-s on juba võimalik lülituda genereeritud TB-le läbi funktsiooni tcg_qemu_tb_exec, oli mulle üsna õigeaegne.
Uue TCG-tagakülje lisamiseks QEMU-sse tuleb luua alamkaust tcg/ (selles osas, tcg/binaryen), ning selles peab olema kaks faili: tcg-target.h ja tcg-target.inc.c ja kõik see asi konfigureeri. Saate sinna paigutada ka teisi faile, kuid nagu nende kahe nime põhjal arvata võib, sisaldavad nad mõlemad kuskile linkimiseks: üks tavapärase pealkirjafailina (seda linkitakse tcg/tcg.h, ja seejärel teistesse failidesse kataloogides tcg, accel ja mitte ainult), teine — ainult kui koodi fragment tcg/tcg.c, kuid tal on ligipääs selle staatilistele funktsioonidele.
Otsustades, et kulutan liiga palju aega, et süsteemi põhjalikult mõista, lihtsalt kopeerisin nende kahe faili «skeletid» teise tagakülje teostusest, märkides ausalt selle litsentsi pealkirjas.
Fail tcg-target.h peamiselt sisaldab konfiguratsiooni kujul #define-sid:
- kui palju registreid ja millise laiusega on sihtarhitektuuril (meie puhul — kui palju soovime, nii palju on — küsimus on rohkem selles, millist efektiivset koodi genereerib brauser «täiesti sihtarhitektuuril…)
- hosti käskude joondamine: x86-l, ja ka TCI-s, käskusid ei joondatud, mina aga katan koodibuffrisse mitte käskusid, vaid viiteid Binaryeni teek struktuuridele, seega ütlen: 4 baiti
- milliseid valikulisi käske tagakülg genereerida võib — lülitame sisse kõik, mida leidsime Binaryenist, ülejäänud las korraldaja jagab lihtsamateks ise.
- milline on umbkaudne TLB-mälupaandi suurus, mida backend nõuab. Asi on selles, et QEMU puhul on kõik tõsiselt: kuigi on olemas abifunktsioonid, mis teevad laadimise/salvestamise arvesse võttes külalise MMU (kuidas muidu ilma selleta?), on nende translatsiooniväli salvestatud struktuurina, mille töötlemine on mugav otse translatsiooniblokkidesse integreerida. Küsimus on aga selles, milline nihke struktuuris on kõige efektiivsem väikeste ja kiirete käskude jadas.
- siin saab reguleerida ühe või kahe reserveeritud registri määramist, aktiveerida TB kutsumise funktsiooni ja valida võimalusel paar väikest.
inline-funktsioone naguflush_icache_range(aga see ei ole meie juhtum)
Fail tcg-target.inc.c, loomulikult on see tavaliselt suuruselt palju suurem ja sisaldab mitmeid kohustuslikke funktsioone:
- algatamine, mis näitab muu hulgas piiranguid selle kohta, milline käsk koos milliste operandidega võib töötada. See on julmalt kopeeritud minu poolt teisest backendist.
- funktsioon, mis võtab vastu ühe sisemise байткода käsu.
- siia saab paigutada ka abifunktsioone ning siin saab kasutada ka staatilisi funktsioone.
tcg/tcg.c
Olen valinud järgmist strateegiat: iga translatsioonibloki alguses salvestasin neli osutajat: algusmark (mingi väärtus vahemikus 0xFFFFFFFF, mille järgi määrati TB praegune olek), kontekst, genereeritud moodul ja maagiline number silumise jaoks. Alguses oli mark seatud 0xFFFFFFFF - n, kus n — väike positiivne number, ja iga täitmise korral tõsteti see läbi tõlgendaja ühe võrra üles. Kui see jõudis 0xFFFFFFFE, toimus kompileerimine, moodul salvestati funktsioonide tabelisse, mis imporditi väikese "käivitaja" sisse, kuhu töötlemine viidi. tcg_qemu_tb_exec, ja moodul eemaldati QEMU mälust.
Parafraseerides klassikat, "Puksiir, kui palju on selles helis programmeerija südame jaoks...". Sellegipoolest oli mälu kuhugi kadunud. Selle käigus oli see mälu, mida juhib QEMU! Mul oli kood, mis kirjutamise ajal järgmise käsu (noh, see tähendab, osutaja) ajal eemaldas selle, millele viidati varasemalt selles kohas, kuid see ei aidanud. Tegelikult eraldab QEMU kõige lihtsamal juhul käivitamisel mälu ja kirjutab sinna genereeritud koodi. Kui puhver saab otsa, visatakse kood välja ja järgmine hakkab selle asemele kirjutama.
Uuringt pärast koodi uurimist sain aru, et magic number'i kasutamine aitas vältida langemist mälu purunemisel, vabastades midagi valesti mitte-initsialiseeritud puhvris esimesel läbimisel. Aga kes kirjutab puhvri minu funktsiooni mööda üle? Nagu Emscripteni arendajad soovitavad, kui silmitsi probleemiga, portisin saadud koodi tagasi natiivses rakendusesse, suunates selle Mozilla Record-Replay'le... Kokkuvõttes sain ma aru, et iga ploki jaoks eraldatakse struktuur TranslationBlock selle kirjeldusega. Arvake, kus… Õige, otse enne ploki puhvris. Seda mõistes otsustasin, et loobun kostüümidest (vähemalt mõnedest) ja lihtsalt viskasin magic number'i minema, samas kui ülejäänud sõnumid kandsin struktuur TranslationBlock, loodud ühesuunaline loend, mille kaudu saab kiirelt liikuda translatsiooniketti lähtestades ja mälu vabastades.
Mõned kostüümid jäid alles: näiteks, märgitud näidikud koodipuhvris — osa neist on lihtsalt BinaryenExpressionRef, see tähendab, et nad vaatavad väljendeid, mis tuleb lineaarsetes baassegmentides paigutada, osa neist on tingimused plokkide vahel liikumiseks, osa — kuhu liikuda. Ja on juba ette valmistatud plokid Relooper'ile, mida tuleb ühendada tingimuste alusel. Nende eristamiseks kasutatakse eeldust, et kõik need on vähemalt neli baiti joondatud, seega võib julgelt kasutada kahte madalamat bitti tähiseks, lihtsalt ei tohi unustada seda vajadusel eemaldada. Üksikasjalikult, sellised tähised on juba QEMU-s kasutusel, et tähistada TCG tsüklist väljumise põhjust.
Binaryeni kasutamine
Modulid WebAssembly's sisaldavad funktsioone, millest igaühel on oma keha, mis on väljend. Väljendid — need on unaaroperatsioonid, binaaroperatsioonid, plokid, mis koosnevad teistest väljenditest, kontrollvoog jne. Nagu juba mainisin, on kontrollvoog siin korraldatud just nagu kõrgetasemelised jaotused, tsüklid, funktsioonide kutsumised jne. Funktsioonide argumendid edastatakse mitte virnas, vaid selgelt, nagu JS-is. On ka globaalsed muutujad, kuid ma ei kasutanud neid, seega ei räägi ma neist.
Funktsioonidel on samuti nullist algavad numeeritud lokaalsed muutujad, mille tüüp on: int32 / int64 / float / double. Samas on esimesed n lokaalsed muutujad funktsioonile edastatud argumendid. Pange tähele, et kuigi see ei ole täiesti madalal tasemel juhtimist kontrollides, ei kanna täisarvud endas siiski märgi/ilma märgita tähist: kuidas number käitub, sõltub operatsioonikoodist.
Üldiselt pakub Binaryen : loote mooduli, mille sees loote avaldisi — unaarseid, binaarseid, muid avaldiste blokke, juhtimise voogu jne. Siis loote funktsiooni, mille kehas tuleb täpsustada avaldis. Kui teil on, nagu ka minul, madala taseme ülemineku graaf — aitab teid komponent relooper. Kuidas ma aru saan, on kõrgetasemeline juhtimise voog bloki sees lubatud, kuni see ei ületa bloki piire — näiteks, saate teha sisemist haru kiire tee / aeglane tee integreeritud TLB-vahemälu töötlemiskoodi sees, kuid ei tohi sekkuda „välisesse“ juhtimise voogu. Kui vabastate relooperi, vabastatakse selle blokid; kui vabastate mooduli, kaovad avaldised, funktsioonid jne, mis on selle areenis..
Siiski, kui soovite koode reaalajas tõlgendada ilma liigsete tõlgendaja eksemplaride loomise ja eemaldamiseta, võib olla mõistlik viia see loogika C++ faili ja juhendada sealt otse kogu C++ API-d, mööda valmis mähiseid.
Seega, et genereerida koodi, on vaja
// настроить глобальные параметры (можно поменять потом)
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);… kui midagi unustasin — vabandust, see on lihtsalt, et esindada ulatust, ja üksikasjad — need on dokumentatsioonis.
Nüüd algab kriks-feks-peks, ligikaudu järgmine:
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,
\/\/ ...
}
);
\/\/ ja nüüd on teil уже instance!
}, buf, sz);Kuidas siduda QEMU maailma ja JS omavahel, samas kiiresti pääseda kompileeritud funktsioonidele, loodi massiiv (importimise funktsioonide tabel), kuhu paigutati genereeritud funktsioonid. Kiiresti indeksi arvutamiseks kasutati algselt nullsõna translation block indeksi, kuid hiljem arvutatud indeks lihtsalt kirjutati vastavasse väljale. struktuur TranslationBlock.
Muide, (hetkel udune litsentsiga) töötab korralikult ainult Firefoxis. Chrome'i arendajad olid kunagi valmis selleks, et keegi soovib luua üle tuhande WebAssembly mooduli instantsi, nii et nad jagasid lihtsalt ühe gigabaidi virtuaalse aadressiruumi igale…
Hetkel on kõik. Võib-olla tuleb veel üks artikkel, kui see kedagi huvitab. Nimelt jäi alles vähemalt ainult pane tööle blokk-seadmed. Võib-olla on mõttekas ka WebAssembly moodulite kompileerimine teha asünkroonselt, nagu see on JS maailmas tavaks, kuna meil on ikkagi tõlgendaja, mis suudab kõik need toimingud teostada, kuni natiivne moodul on valmis.
Viimaseks mõistatus: te tegite binaari 32-bitisele arhitektuurile, kuid kood, mis tegeleb mäluga, läheb Binaryeni, kuhugi stack'i või kuhugi ülemistesse 2 GB 32-bitises aadressiruumis. Probleem seisneb selles, et Binaryeni vaatepunktist on see pöördumine liiga suurele tulemusaadressile. Kuidas seda vältida?
Administraatorlikult
Ma seda lõpuks ei testinud, kuid esimene mõte oli "Aga mis siis, kui panna 32-bitine Linux?" Sel juhul on aadressiruumi ülemine osa hõivatud tuumaga. Küsimus on ainult selles, kui palju on hõivatud: 1 või 2 GB.
Programmeerijana (praktikate variant)
Me puhume mullit ülemises aadressiruumis. Ma ei saa isegi aru, miks see töötab — seal on ju juba peaks olema stack. Aga "meie praktikud: meil kõik töötab, aga keegi ei tea, miks…".
// 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'iga tõele ei ole ühilduv, kuid õnneks tõukab Valgrind sealt kõiki välja 🙂
Võib-olla keegi pakub paremat seletust, kuidas see minu kood töötab...
Allikas: habr.com
