QEMU.js: acum serios și cu WASM

Am decis odată să mă distrez demonstra reversibilitatea procesului și aflați cum să generați JavaScript (mai precis, Asm.js) din codul mașinii. QEMU a fost ales pentru experiment, iar ceva timp mai târziu a fost scris un articol pe Habr. În comentarii am fost sfătuit să refac proiectul în WebAssembly și chiar să mă renunț aproape terminat Cumva nu am vrut proiectul... Lucrul mergea, dar foarte încet, iar acum, recent, în acel articol a apărut comentariu pe tema „Deci cum s-a terminat totul?” Ca răspuns la răspunsul meu detaliat, am auzit „Sună ca un articol”. Ei bine, dacă poți, va fi un articol. Poate cineva îl va găsi util. Din acesta cititorul va afla câteva fapte despre designul backend-urilor de generare a codului QEMU, precum și despre cum să scrie un compilator Just-in-Time pentru o aplicație web.

sarcini

Deoarece învățasem deja cum să port „cumva” QEMU la JavaScript, de data aceasta s-a decis să o fac cu înțelepciune și să nu repetam greșelile vechi.

Eroare numărul unu: ramificare de la eliberarea punctului

Prima mea greșeală a fost să fork versiunea mea din versiunea upstream 2.4.1. Atunci mi s-a părut o idee bună: dacă există lansare punct, atunci probabil că este mai stabilă decât simplul 2.4, și cu atât mai mult ramura master. Și din moment ce am plănuit să adaug o cantitate destul de mare din propriile erori, nu am avut nevoie deloc de ale altcuiva. Probabil așa a ieșit. Dar iată chestia: QEMU nu stă pe loc și, la un moment dat, chiar au anunțat optimizarea codului generat cu 10 la sută. „Da, acum o să îngheț”, m-am gândit și m-am întrerupt. Aici trebuie să facem o digresiune: datorită naturii cu un singur thread a QEMU.js și a faptului că QEMU original nu implică absența multi-threading-ului (adică capacitatea de a opera simultan mai multe căi de cod fără legătură și nu doar „utilizați toate nucleele”) este esențial pentru acesta, principalele funcții ale thread-urilor pe care trebuia să le „dezvoltam” pentru a putea apela din exterior. Acest lucru a creat unele probleme naturale în timpul fuziunii. Cu toate acestea, faptul că unele dintre modificările de la ramură master, cu care am încercat să-mi îmbin codul, au fost, de asemenea, culese în versiunea punct (și, prin urmare, în ramura mea), probabil că nu ar fi adăugat confort.

În general, am decis că încă mai are sens să arunc prototipul, să-l dezasamblam pentru piese și să construiesc o nouă versiune de la zero bazată pe ceva mai proaspăt și acum din master.

Greșeala numărul doi: metodologia TLP

În esență, aceasta nu este o greșeală, în general, este doar o caracteristică a creării unui proiect în condiții de înțelegere greșită completă atât a „unde și cum să ne mutăm?”, cât și, în general, „o să ajungem acolo?” In aceste conditii programare stângace a fost o opțiune justificată, dar, firește, nu am vrut să o repet inutil. De data aceasta am vrut să o fac cu înțelepciune: comiteri atomice, modificări conștiente de cod (și nu „înșiră de caractere aleatoare până când se compila (cu avertismente)”, așa cum a spus odată Linus Torvalds despre cineva, conform Wikiquote), etc.

Greșeala numărul trei: să intri în apă fără să cunoști vadul

Încă nu am scăpat complet de asta, dar acum am decis să nu urmez deloc calea celei mai mici rezistențe și să o fac „ca adult”, și anume, să-mi scriu backend-ul TCG de la zero, pentru a nu să trebuiască să spun mai târziu: „Da, asta se întâmplă, desigur, încet, dar nu pot controla totul - așa este scris TCI...” Mai mult, aceasta a părut inițial o soluție evidentă, deoarece Eu generez cod binar. După cum se spune, „S-a adunat Gentulу, dar nu acela”: codul este, desigur, binar, dar controlul nu poate fi pur și simplu transferat la el - trebuie să fie împins în mod explicit în browser pentru compilare, rezultând un anumit obiect din lumea JS, care încă trebuie să fi salvat undeva. Cu toate acestea, pe arhitecturile RISC normale, din câte am înțeles, o situație tipică este necesitatea de a reseta în mod explicit memoria cache de instrucțiuni pentru codul regenerat - dacă nu este ceea ce avem nevoie, atunci, în orice caz, este aproape. În plus, din ultima mea încercare, am învățat că controlul nu pare să fie transferat la mijlocul blocului de traducere, așa că nu prea avem nevoie de bytecode interpretat din orice offset și îl putem genera pur și simplu din funcția de pe TB .

Au venit și au dat cu piciorul

Deși am început să rescriu codul încă din iulie, o lovitură magică s-a strecurat neobservată: de obicei, scrisorile de la GitHub sosesc ca notificări despre răspunsurile la Probleme și solicitări Pull, dar aici, brusc mentioneaza in thread Binaryen ca backend qemu în context, „A făcut așa ceva, poate va spune ceva.” Vorbeam despre utilizarea bibliotecii aferente Emscripten Binaryen pentru a crea WASM JIT. Ei bine, am spus că aveți o licență Apache 2.0 acolo, iar QEMU în ansamblu este distribuit sub GPLv2 și nu sunt foarte compatibile. Deodată s-a dovedit că o licență poate fi repara cumva (Nu știu: poate schimba-l, poate dublă licență, poate altceva...). Acest lucru, desigur, m-a făcut fericit, pentru că până atunci mă uitasem deja cu atenție format binar WebAssembly și am fost cumva trist și de neînțeles. Exista și o bibliotecă care devora blocurile de bază cu graficul de tranziție, producea bytecode și chiar îl rula în interpretul însuși, dacă era necesar.

Apoi au fost mai multe o scrisoare pe lista de corespondență QEMU, dar aceasta este mai mult despre întrebarea „Cine are nevoie oricum?” Si e brusc, s-a dovedit că era necesar. Cel puțin, puteți combina următoarele posibilități de utilizare, dacă funcționează mai mult sau mai puțin rapid:

  • lansând ceva educativ fără nicio instalare
  • virtualizare pe iOS, unde, conform zvonurilor, singura aplicație care are dreptul de a genera cod din mers este un motor JS (este adevărat?)
  • demonstrație de mini-OS - single-floppy, built-in, tot felul de firmware, etc...

Funcții de rulare a browserului

După cum am spus deja, QEMU este legat de multithreading, dar browserul nu îl are. Ei bine, adică nu... La început nu a existat deloc, apoi a apărut WebWorkers - din câte am înțeles, acesta este multithreading bazat pe transmiterea mesajelor fără variabile partajate. Desigur, acest lucru creează probleme semnificative la portarea codului existent pe baza modelului de memorie partajată. Apoi, sub presiunea publicului, a fost implementat și sub numele SharedArrayBuffers. A fost introdus treptat, au sărbătorit lansarea în diferite browsere, apoi au sărbătorit Anul Nou, apoi Meltdown... După care au ajuns la concluzia că măsurarea timpului grosieră sau grosieră, dar cu ajutorul memoriei partajate și a unui fir incrementând contorul, e tot la fel va funcționa destul de precis. Așa că am dezactivat multithreading cu memorie partajată. Se pare că l-au reactivat ulterior, dar, așa cum a devenit clar de la primul experiment, există viață fără el și, dacă da, vom încerca să o facem fără a ne baza pe multithreading.

A doua caracteristică este imposibilitatea manipulărilor la nivel scăzut cu stiva: nu puteți pur și simplu să luați, să salvați contextul actual și să treceți la unul nou cu o stivă nouă. Stiva de apeluri este gestionată de mașina virtuală JS. S-ar părea, care este problema, din moment ce am decis totuși să gestionăm fostele fluxuri complet manual? Faptul este că bloc I/O în QEMU este implementat prin coroutine și aici ar fi utilă manipulările de stivă de nivel scăzut. Din fericire, Emscipten conține deja un mecanism pentru operații asincrone, chiar și două: Asyncify и Imparator. Primul funcționează prin umflare semnificativă în codul JavaScript generat și nu mai este acceptat. Al doilea este „modul corect” actual și funcționează prin generarea de bytecode pentru interpretul nativ. Funcționează, desigur, încet, dar nu umfla codul. Adevărat, suportul pentru corutine pentru acest mecanism a trebuit să fie contribuit independent (existau deja corutine scrise pentru Asyncify și a existat o implementare a aproximativ aceeași API pentru Emterpreter, trebuia doar să le conectați).

Momentan, nu am reușit încă să împart codul într-unul compilat în WASM și interpretat folosind Emterpreter, așa că dispozitivele bloc nu funcționează încă (vezi în seria următoare, cum se spune...). Adică, până la urmă ar trebui să obțineți ceva de genul asta amuzant stratificat:

  • bloc I/O interpretat. Ei bine, chiar te așteptai la un NVMe emulat cu performanță nativă? 🙂
  • codul QEMU principal compilat static (translator, alte dispozitive emulate etc.)
  • cod de invitat compilat dinamic în WASM

Caracteristicile surselor QEMU

După cum probabil ați ghicit deja, codul pentru emularea arhitecturilor oaspeților și codul pentru generarea instrucțiunilor mașinii gazdă sunt separate în QEMU. De fapt, este și puțin mai complicat:

  • există arhitecturi de oaspeți
  • există acceleratoare, și anume, KVM pentru virtualizarea hardware pe Linux (pentru sisteme guest și gazdă compatibile între ele), TCG pentru generarea de cod JIT oriunde. Începând cu QEMU 2.9, a apărut suportul pentru standardul de virtualizare hardware HAXM pe Windows (detaliile)
  • dacă se utilizează TCG și nu virtualizarea hardware, atunci are suport separat pentru generarea de cod pentru fiecare arhitectură gazdă, precum și pentru interpretul universal
  • ... și în jurul tuturor acestora - periferice emulate, interfață cu utilizatorul, migrare, record-replay etc.

Apropo, știai: QEMU poate emula nu numai întregul computer, ci și procesorul pentru un proces utilizator separat din nucleul gazdă, care este folosit, de exemplu, de fuzzer-ul AFL pentru instrumentarea binară. Poate cineva ar dori să port acest mod de operare al QEMU la JS? 😉

La fel ca majoritatea software-ului gratuit de lungă durată, QEMU este construit prin apel configure и make. Să presupunem că decideți să adăugați ceva: un backend TCG, implementare fir, altceva. Nu vă grăbiți să fiți fericiți/îngroziți (subliniați după caz) de perspectiva de a comunica cu Autoconf - de fapt, configure QEMU este aparent auto-scris și nu este generat din nimic.

WebAssembly

Deci, ce este chestia asta numită WebAssembly (aka WASM)? Acesta este un înlocuitor pentru Asm.js, nu se mai pretinde a fi cod JavaScript valid. Dimpotrivă, este pur binar și optimizat și chiar și pur și simplu scrierea unui număr întreg în el nu este foarte simplă: pentru compactitate, este stocat în format LEB128.

Poate că ați auzit despre algoritmul de relooping pentru Asm.js - aceasta este restaurarea instrucțiunilor de control al fluxului „la nivel înalt” (adică if-then-else, bucle etc.), pentru care sunt proiectate motoarele JS, de la LLVM IR de nivel scăzut, mai aproape de codul mașină executat de procesor. Desigur, reprezentarea intermediară a QEMU este mai aproape de a doua. S-ar părea că aici este, bytecode, sfârșitul chinului... Și apoi sunt blocuri, if-then-else și bucle!..

Și acesta este un alt motiv pentru care Binaryen este util: poate accepta în mod natural blocuri de nivel înalt apropiate de ceea ce ar fi stocat în WASM. Dar poate produce și cod dintr-un grafic de blocuri de bază și tranziții între ele. Ei bine, am spus deja că ascunde formatul de stocare WebAssembly în spatele API-ului convenabil C/C++.

TCG (Tiny Code Generator)

TCG a fost inițial backend pentru compilatorul C. Apoi, se pare, nu a putut rezista concurenței cu GCC, dar în cele din urmă și-a găsit locul în QEMU ca mecanism de generare de cod pentru platforma gazdă. Există, de asemenea, un backend TCG care generează un bytecode abstract, care este imediat executat de interpret, dar am decis să evit să-l folosesc de data aceasta. Cu toate acestea, faptul că în QEMU este deja posibilă activarea tranziției la TB generată prin intermediul funcției tcg_qemu_tb_exec, s-a dovedit a fi foarte util pentru mine.

Pentru a adăuga un nou backend TCG la QEMU, trebuie să creați un subdirector tcg/<имя архитектуры> (în acest caz, tcg/binaryen), și conține două fișiere: tcg-target.h и tcg-target.inc.c и prescrie totul este despre configure. Puteți pune și alte fișiere acolo, dar, după cum puteți ghici din numele acestor două, ambele vor fi incluse undeva: unul ca fișier de antet obișnuit (este inclus în tcg/tcg.h, iar acesta se află deja în alte fișiere din directoare tcg, accel și nu numai), celălalt - doar ca fragment de cod în tcg/tcg.c, dar are acces la funcțiile sale statice.

Decizând că voi petrece prea mult timp pe investigații detaliate despre cum funcționează, am copiat pur și simplu „scheletele” acestor două fișiere dintr-o altă implementare backend, indicând sincer acest lucru în antetul licenței.

fișier tcg-target.h conține în principal setări în formular #define-s:

  • câte registre și ce lățime sunt pe arhitectura țintă (avem câte vrem, câte vrem - întrebarea este mai mult despre ce va fi generat în cod mai eficient de către browser pe arhitectura „complet țintă” ...)
  • alinierea instrucțiunilor gazdă: pe x86 și chiar și în TCI, instrucțiunile nu sunt aliniate deloc, dar voi pune în buffer-ul de cod nu instrucțiuni deloc, ci pointeri către structurile bibliotecii Binaryen, așa că voi spune: 4 octeți
  • ce instrucțiuni opționale poate genera backend-ul - includem tot ce găsim în Binaryen, lăsăm acceleratorul să despartă restul în altele mai simple.
  • Care este dimensiunea aproximativă a memoriei cache TLB solicitată de backend. Cert este că în QEMU totul este serios: deși există funcții de ajutor care efectuează încărcare/stocare ținând cont de MMU-ul invitat (unde am fi noi fără el acum?), își salvează memoria cache de traducere sub forma unei structuri, procesarea cărora este convenabil să fie încorporat direct în blocurile de difuzare. Întrebarea este, ce offset din această structură este cel mai eficient procesat de o secvență mică și rapidă de comenzi?
  • aici puteți modifica scopul unuia sau a două registre rezervate, puteți activa apelarea TB printr-o funcție și, opțional, puteți descrie câteva inline-funcții ca flush_icache_range (dar nu este cazul nostru)

fișier tcg-target.inc.c, desigur, este de obicei mult mai mare și conține mai multe funcții obligatorii:

  • inițializare, inclusiv restricții privind instrucțiunile care pot funcționa pe ce operanzi. Copiat flagrant de mine dintr-un alt backend
  • funcție care primește o instrucțiune internă de cod octet
  • Puteți pune și funcții auxiliare aici și puteți utiliza și funcții statice din tcg/tcg.c

Pentru mine, am ales următoarea strategie: în primele cuvinte din următorul bloc de traducere, am notat patru indicatoare: un semn de început (o anumită valoare în vecinătate 0xFFFFFFFF, care a determinat starea curentă a TB), contextul, modulul generat și numărul magic pentru depanare. La început marca a fost plasată 0xFFFFFFFF - nUnde n - un mic număr pozitiv, iar de fiecare dată când a fost executat prin interpret a crescut cu 1. Când a ajuns 0xFFFFFFFE, a avut loc compilarea, modulul a fost salvat în tabelul de funcții, importat într-un mic „lansator”, în care execuția a trecut de la tcg_qemu_tb_exec, iar modulul a fost eliminat din memoria QEMU.

Pentru a parafraza clasicii, „Crutch, cât de mult se împletește în acest sunet pentru inima progerului...”. Cu toate acestea, memoria se scurgea pe undeva. Mai mult, era memorie gestionată de QEMU! Aveam un cod care, la scrierea următoarei instrucțiuni (ei bine, adică un pointer), îl ștergea pe cel al cărui link era în acest loc mai devreme, dar acest lucru nu a ajutat. De fapt, în cel mai simplu caz, QEMU alocă memorie la pornire și scrie acolo codul generat. Când tamponul se epuizează, codul este aruncat și următorul începe să fie scris în locul lui.

După ce am studiat codul, mi-am dat seama că trucul cu numărul magic mi-a permis să nu eșuez la distrugerea grămezilor eliberând ceva greșit pe un buffer neinițializat la prima trecere. Dar cine rescrie tamponul pentru a ocoli funcția mea mai târziu? După cum ne sfătuiesc dezvoltatorii Emscripten, atunci când am întâmpinat o problemă, am portat codul rezultat înapoi în aplicația nativă, am setat pe el Mozilla Record-Replay... În general, până la urmă mi-am dat seama de un lucru simplu: pentru fiecare bloc, A struct TranslationBlock cu descrierea ei. Ghici unde... Așa e, chiar înainte de bloc, chiar în buffer. Dându-mi seama de acest lucru, am decis să renunț să folosesc cârje (cel puțin unele) și pur și simplu am aruncat numărul magic și am transferat cuvintele rămase în struct TranslationBlock, creând o listă cu legături unice care poate fi parcursă rapid atunci când memoria cache a traducerii este resetată și eliberează memorie.

Rămân câteva cârje: de exemplu, indicatori marcați în tamponul de cod - unele dintre ele sunt pur și simplu BinaryenExpressionRef, adică se uită la expresiile care trebuie introduse liniar în blocul de bază generat, o parte este condiția pentru tranziția între BB-uri, o parte este unde să mergem. Ei bine, există deja blocuri pregătite pentru Relooper care trebuie conectate în funcție de condiții. Pentru a le distinge, se utilizează presupunerea că toți sunt aliniați cu cel puțin patru octeți, astfel încât să puteți utiliza în siguranță cei doi biți mai puțin semnificativi pentru etichetă, trebuie doar să vă amintiți să o eliminați dacă este necesar. Apropo, astfel de etichete sunt deja folosite în QEMU pentru a indica motivul ieșirii din bucla TCG.

Folosind Binaryen

Modulele din WebAssembly conțin funcții, fiecare dintre ele conține un corp, care este o expresie. Expresiile sunt operații unare și binare, blocuri formate din liste de alte expresii, flux de control etc. După cum am spus deja, fluxul de control aici este organizat exact ca ramuri de nivel înalt, bucle, apeluri de funcții etc. Argumentele la funcții nu sunt transmise în stivă, ci explicit, la fel ca în JS. Există și variabile globale, dar nu le-am folosit, așa că nu vă voi spune despre ele.

Funcțiile au și variabile locale, numerotate de la zero, de tip: int32 / int64 / float / double. În acest caz, primele n variabile locale sunt argumentele transmise funcției. Vă rugăm să rețineți că, deși totul aici nu este complet de nivel scăzut în ceea ce privește fluxul de control, numerele întregi încă nu poartă atributul „semnat/nesemnat”: modul în care se comportă numărul depinde de codul operației.

În general, Binaryen oferă C-API simplu: creezi un modul, în el creați expresii - unare, binare, blocuri din alte expresii, control flux etc. Apoi creați o funcție cu o expresie ca corp. Dacă, ca și mine, aveți un grafic de tranziție de nivel scăzut, componenta relooper vă va ajuta. Din câte am înțeles, este posibil să se utilizeze controlul la nivel înalt al fluxului de execuție într-un bloc, atâta timp cât acesta nu depășește limitele blocului - adică este posibil să se facă o cale internă rapidă / lentă ramificarea căii în interiorul codului de procesare cache TLB încorporat, dar să nu interfereze cu fluxul de control „extern”. Când eliberați un relooper, blocurile acestuia sunt eliberate, când eliberați un modul, expresiile, funcțiile etc. alocate acestuia dispar. arenă.

Cu toate acestea, dacă doriți să interpretați codul din mers fără crearea și ștergerea inutilă a unei instanțe de interpret, ar putea avea sens să puneți această logică într-un fișier C++ și de acolo să gestionați direct întregul API C++ al bibliotecii, ocolind gata- făcut ambalaje.

Deci pentru a genera codul de care aveți nevoie

// настроить глобальные параметры (можно поменять потом)
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);

... dacă am uitat ceva, scuze, asta este doar pentru a reprezenta scara, iar detaliile sunt în documentație.

Și acum începe crack-fex-pex, ceva de genul acesta:

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);

Pentru a conecta cumva lumile QEMU și JS și, în același timp, pentru a accesa rapid funcțiile compilate, a fost creat un tablou (un tabel de funcții pentru import în launcher), iar funcțiile generate au fost plasate acolo. Pentru a calcula rapid indexul, indexul blocului de traducere zero cuvânt a fost utilizat inițial ca acesta, dar apoi indexul calculat folosind această formulă a început să se potrivească pur și simplu în câmpul din struct TranslationBlock.

Apropo, demo (în prezent cu o licență tulbure) funcționează bine doar în Firefox. Dezvoltatorii Chrome au fost cumva nu gata la faptul că cineva ar dori să creeze mai mult de o mie de instanțe de module WebAssembly, așa că pur și simplu a alocat un gigabyte de spațiu de adrese virtuale pentru fiecare...

Asta este tot pentru acum. Poate că va mai fi un articol dacă este cineva interesat. Și anume, rămâne cel puțin numai faceți ca dispozitivele bloc să funcționeze. De asemenea, ar putea avea sens să faceți compilarea modulelor WebAssembly asincronă, așa cum este obișnuit în lumea JS, deoarece există încă un interpret care poate face toate acestea până când modulul nativ este gata.

In sfarsit o ghicitoare: ați compilat un binar pe o arhitectură de 32 de biți, dar codul, prin operațiuni de memorie, urcă din Binaryen, undeva pe stivă, sau altundeva în cei 2 GB de sus ai spațiului de adrese pe 32 de biți. Problema este că, din punctul de vedere al lui Binaryen, aceasta este accesarea unei adrese prea mari. Cum să ocoliți asta?

În felul administratorului

Nu am ajuns să testez asta, dar primul meu gând a fost „Dar dacă aș instala Linux pe 32 de biți?” Apoi partea superioară a spațiului de adrese va fi ocupată de kernel. Singura întrebare este cât va fi ocupat: 1 sau 2 Gb.

În felul unui programator (opțiune pentru practicieni)

Să aruncăm un balon în partea de sus a spațiului de adrese. Eu însumi nu înțeleg de ce funcționează - acolo deja trebuie să existe o stivă. Dar „noi suntem practicanți: totul funcționează pentru noi, dar nimeni nu știe de ce...”

// 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));
}

... este adevărat că nu este compatibil cu Valgrind, dar, din fericire, Valgrind în sine îi împinge foarte eficient pe toată lumea de acolo :)

Poate cineva va da o explicație mai bună despre cum funcționează acest cod al meu...

Sursa: www.habr.com

Adauga un comentariu