QEMU.js: ora serio e con WASM

Una volta decisi per divertimento dimostrare la reversibilità del processo e scopri come generare JavaScript (più precisamente Asm.js) dal codice macchina. QEMU fu scelto per l'esperimento e qualche tempo dopo fu scritto un articolo su Habr. Nei commenti mi è stato consigliato di rifare il progetto in WebAssembly e persino di licenziarmi quasi finito In qualche modo non volevo il progetto... Il lavoro andava avanti, ma molto lentamente, e ora, di recente, è apparso quell'articolo commento sull'argomento "Allora come è finito tutto?" In risposta alla mia risposta dettagliata, ho sentito "Sembra un articolo". Bene, se puoi, ci sarà un articolo. Forse qualcuno lo troverà utile. Da esso il lettore apprenderà alcuni fatti sulla progettazione dei backend di generazione del codice QEMU, nonché su come scrivere un compilatore Just-in-Time per un'applicazione web.

compiti

Dato che avevo già imparato come portare "in qualche modo" QEMU su JavaScript, questa volta si è deciso di farlo saggiamente e di non ripetere vecchi errori.

Errore numero uno: diramazione dal punto di rilascio

Il mio primo errore è stato quello di creare un fork della mia versione dalla versione upstream 2.4.1. Allora mi è sembrata una buona idea: se esiste un punto di rilascio, probabilmente è più stabile del semplice 2.4, e ancor di più del ramo master. E poiché avevo intenzione di aggiungere una discreta quantità di bug miei, non avevo bisogno di quelli di nessun altro. Probabilmente è così che è andata a finire. Ma il punto è questo: QEMU non si ferma e ad un certo punto hanno addirittura annunciato un'ottimizzazione del codice generato del 10%... "Sì, ora mi blocco", ho pensato e sono crollato. Qui dobbiamo fare una digressione: a causa della natura a thread singolo di QEMU.js e del fatto che il QEMU originale non implica l'assenza di multi-threading (cioè la capacità di gestire simultaneamente diversi percorsi di codice non correlati, e non solo "usa tutti i kernel") è fondamentale per questo, le funzioni principali dei thread che ho dovuto "spegnere" per poterle chiamare dall'esterno. Ciò ha creato alcuni problemi naturali durante la fusione. Tuttavia, il fatto che alcuni cambiamenti dal ramo master, con cui ho provato a unire il mio codice, fossero stati scelti anche nel punto di rilascio (e quindi nel mio ramo) probabilmente non avrebbero aggiunto comodità.

In generale, ho deciso che ha ancora senso buttare via il prototipo, smontarlo per parti e costruire una nuova versione da zero basata su qualcosa di più fresco e ora da master.

Errore numero due: metodologia TLP

In sostanza, questo non è un errore, in generale, è solo una caratteristica della creazione di un progetto in condizioni di completa incomprensione sia su "dove e come muoverci?", sia in generale "ci arriveremo?" In queste condizioni programmazione goffa era un'opzione giustificata, ma, naturalmente, non volevo ripeterla inutilmente. Questa volta volevo farlo saggiamente: commit atomici, modifiche consapevoli del codice (e non "mettere insieme caratteri casuali finché non viene compilato (con avvisi)", come disse una volta Linus Torvalds di qualcuno, secondo Wikiquote), ecc.

Errore numero tre: entrare in acqua senza conoscere il guado

Non me ne sono ancora liberato del tutto, ma ora ho deciso di non seguire affatto la strada di minor resistenza e di farlo "da adulto", ovvero scrivere il mio backend TCG da zero, in modo da non dover dire più tardi: "Sì, ovviamente, lentamente, ma non posso controllare tutto - è così che è scritto TCI..." Inoltre, inizialmente questa sembrava una soluzione ovvia Genero codice binario. Come si suol dire, “Gand si è riunitoу, ma non quello”: il codice è, ovviamente, binario, ma il controllo non può essere semplicemente trasferito ad esso - deve essere esplicitamente inserito nel browser per la compilazione, risultando in un determinato oggetto dal mondo JS, che deve ancora essere essere salvato da qualche parte. Tuttavia, sulle normali architetture RISC, per quanto ho capito, una situazione tipica è la necessità di reimpostare esplicitamente la cache delle istruzioni per il codice rigenerato: se questo non è ciò di cui abbiamo bisogno, in ogni caso è vicino. Inoltre, dal mio ultimo tentativo, ho imparato che il controllo non sembra essere trasferito al centro del blocco di traduzione, quindi non abbiamo davvero bisogno del bytecode interpretato da qualsiasi offset, e possiamo semplicemente generarlo dalla funzione su TB .

Sono venuti e hanno preso a calci

Anche se ho iniziato a riscrivere il codice a luglio, una magia si è insinuata inosservata: di solito le lettere da GitHub arrivano come notifiche sulle risposte ai problemi e alle richieste pull, ma qui, improvvisamente menzione nel thread Binaryen come backend qemu nel contesto: "Ha fatto qualcosa del genere, forse dirà qualcosa". Stavamo parlando dell'utilizzo della libreria correlata di Emscripten binario per creare WASM JIT. Bene, ho detto che lì hai una licenza Apache 2.0 e QEMU nel suo insieme è distribuito sotto GPLv2 e non sono molto compatibili. All'improvviso si è scoperto che una licenza può essere sistemarlo in qualche modo (Non lo so: forse cambiarlo, forse la doppia licenza, forse qualcos'altro...). Questo, ovviamente, mi ha reso felice, perché a quel punto avevo già guardato da vicino formato binario WebAssembly, ed ero in qualche modo triste e incomprensibile. C'era anche una libreria che avrebbe divorato i blocchi di base con il grafico di transizione, avrebbe prodotto il bytecode e, se necessario, lo avrebbe persino eseguito nell'interprete stesso.

Poi c'era di più una lettera sulla mailing list QEMU, ma si tratta più della domanda: "Chi ne ha bisogno comunque?" E questo è improvvisamente, si è scoperto che era necessario. Come minimo, puoi mettere insieme le seguenti possibilità di utilizzo, se funziona più o meno velocemente:

  • lanciare qualcosa di educativo senza alcuna installazione
  • virtualizzazione su iOS, dove, secondo alcune indiscrezioni, l'unica applicazione che ha il diritto di generare codice al volo è un motore JS (è vero?)
  • dimostrazione del mini-OS: floppy singolo, integrato, tutti i tipi di firmware, ecc...

Funzionalità di esecuzione del browser

Come ho già detto, QEMU è legato al multithreading, ma il browser non ce l'ha. Beh, cioè no... All'inizio non esisteva affatto, poi sono comparsi i WebWorkers - per quanto ho capito, si tratta di multithreading basato sullo scambio di messaggi senza variabili condivise. Naturalmente, ciò crea problemi significativi durante il porting del codice esistente basato sul modello di memoria condivisa. Poi, sotto la pressione dell'opinione pubblica, è stato implementato anche con questo nome SharedArrayBuffers. È stato introdotto gradualmente, hanno festeggiato il suo lancio in diversi browser, poi hanno festeggiato il nuovo anno, e poi il Meltdown... Dopo di che sono giunti alla conclusione che grossolana o grossolana è la misurazione del tempo, ma con l'aiuto della memoria condivisa e di un thread incrementando il contatore, è tutto uguale funzionerà in modo abbastanza accurato. Quindi abbiamo disabilitato il multithreading con memoria condivisa. Sembra che in seguito l'abbiano riattivato, ma, come è apparso chiaro dal primo esperimento, senza di esso c'è vita e, in tal caso, proveremo a farlo senza fare affidamento sul multithreading.

La seconda caratteristica è l'impossibilità di manipolazioni di basso livello con lo stack: non puoi semplicemente prendere, salvare il contesto corrente e passare a uno nuovo con un nuovo stack. Lo stack di chiamate è gestito dalla macchina virtuale JS. Sembrerebbe, qual è il problema, dal momento che abbiamo comunque deciso di gestire i flussi precedenti in modo completamente manuale? Il fatto è che l'I/O dei blocchi in QEMU è implementato tramite coroutine, ed è qui che le manipolazioni dello stack di basso livello potrebbero tornare utili. Fortunatamente Emscipten contiene già un meccanismo per le operazioni asincrone, addirittura due: Asincronizzare и Interprete. Il primo funziona in modo significativo nel codice JavaScript generato e non è più supportato. Il secondo è l'attuale "modo corretto" e funziona tramite la generazione di bytecode per l'interprete nativo. Funziona, ovviamente, lentamente, ma non gonfia il codice. È vero, il supporto per le coroutine per questo meccanismo doveva essere fornito in modo indipendente (c'erano già coroutine scritte per Asyncify e c'era un'implementazione più o meno della stessa API per Emterpreter, dovevi solo collegarle).

Al momento non sono ancora riuscito a dividere il codice in uno compilato in WASM e interpretato utilizzando Emterpreter, quindi i dispositivi a blocchi non funzionano ancora (vedi nella prossima serie, come si suol dire...). Cioè, alla fine dovresti ottenere qualcosa come questa divertente cosa a strati:

  • I/O del blocco interpretato. Bene, ti aspettavi davvero un NVMe emulato con prestazioni native? 🙂
  • Codice QEMU principale compilato staticamente (traduttore, altri dispositivi emulati, ecc.)
  • codice ospite compilato dinamicamente in WASM

Caratteristiche delle sorgenti QEMU

Come probabilmente avrai già intuito, il codice per emulare le architetture guest e il codice per generare le istruzioni della macchina host sono separati in QEMU. In realtà, è anche un po’ più complicato:

  • ci sono architetture ospiti
  • c'è acceleratori, ovvero KVM per la virtualizzazione dell'hardware su Linux (per sistemi guest e host compatibili tra loro), TCG per la generazione di codice JIT ovunque. A partire da QEMU 2.9, è apparso il supporto per lo standard di virtualizzazione hardware HAXM su Windows (Dettagli)
  • se viene utilizzato TCG e non la virtualizzazione hardware, allora ha un supporto separato per la generazione di codice per ciascuna architettura host, nonché per l'interprete universale
  • ... e tutto questo: periferiche emulate, interfaccia utente, migrazione, riproduzione di record, ecc.

A proposito, sapevi: QEMU può emulare non solo l'intero computer, ma anche il processore per un processo utente separato nel kernel host, utilizzato, ad esempio, dal fuzzer AFL per la strumentazione binaria. Forse qualcuno vorrebbe portare questa modalità operativa di QEMU su JS? 😉

Come la maggior parte del software libero di lunga data, QEMU è costruito tramite la chiamata configure и make. Diciamo che decidi di aggiungere qualcosa: un backend TCG, un'implementazione del thread, qualcos'altro. Non abbiate fretta di essere felici/inorriditi (sottolineate se opportuno) alla prospettiva di comunicare con Autoconf - infatti, configure QEMU è apparentemente autoscritto e non è generato da nulla.

WebAssembly

Allora cos'è questa cosa chiamata WebAssembly (nota anche come WASM)? Questo sostituisce Asm.js e non pretende più di essere un codice JavaScript valido. Al contrario, è puramente binario e ottimizzato, e anche semplicemente scrivervi un intero non è semplicissimo: per compattezza, viene memorizzato nel formato LEB128.

Potresti aver sentito parlare dell'algoritmo di relooping per Asm.js: si tratta del ripristino delle istruzioni di controllo del flusso di "alto livello" (ovvero if-then-else, loop, ecc.), per le quali sono progettati i motori JS, da il LLVM IR di basso livello, più vicino al codice macchina eseguito dal processore. Naturalmente, la rappresentazione intermedia del QEMU è più vicina alla seconda. Sembrerebbe che eccola qui, bytecode, la fine del tormento... E poi ci sono blocchi, if-then-else e loop!..

E questo è un altro motivo per cui Binaryen è utile: può naturalmente accettare blocchi di alto livello vicini a quelli che verrebbero archiviati in WASM. Ma può anche produrre codice da un grafico di blocchi di base e transizioni tra di essi. Ebbene, ho già detto che nasconde il formato di archiviazione WebAssembly dietro la comoda API C/C++.

TCG (piccolo generatore di codici)

TCG era originariamente backend per il compilatore C. Quindi, a quanto pare, non ha potuto resistere alla concorrenza con GCC, ma alla fine ha trovato il suo posto in QEMU come meccanismo di generazione del codice per la piattaforma host. C'è anche un backend TCG che genera del bytecode astratto, che viene immediatamente eseguito dall'interprete, ma questa volta ho deciso di evitare di usarlo. Resta comunque valido il fatto che in QEMU sia già possibile abilitare il passaggio al TB generato tramite la funzione tcg_qemu_tb_exec, mi è risultato molto utile.

Per aggiungere un nuovo backend TCG a QEMU, è necessario creare una sottodirectory tcg/<имя архитектуры> (in questo caso, tcg/binaryen) e contiene due file: tcg-target.h и tcg-target.inc.c и prescrivere è tutta una questione di configure. Puoi inserire altri file lì, ma, come puoi intuire dai nomi di questi due, saranno entrambi inclusi da qualche parte: uno come normale file di intestazione (è incluso in tcg/tcg.he quello è già in altri file nelle directory tcg, accel e non solo), l'altro - solo come frammento di codice in tcg/tcg.c, ma ha accesso alle sue funzioni statiche.

Avendo deciso che avrei dedicato troppo tempo ad indagini dettagliate su come funziona, ho semplicemente copiato gli “scheletri” di questi due file da un'altra implementazione del backend, indicandolo onestamente nell'intestazione della licenza.

file tcg-target.h contiene principalmente le impostazioni nel modulo #define-S:

  • quanti registri e quale larghezza ci sono sull'architettura di destinazione (ne abbiamo quanti ne vogliamo, quanti ne vogliamo - la domanda è più su cosa verrà generato in codice più efficiente dal browser sull'architettura "completamente target" ...)
  • allineamento delle istruzioni dell'host: su x86, e anche in TCI, le istruzioni non sono affatto allineate, ma inserirò nel buffer del codice non istruzioni, ma puntatori alle strutture della libreria Binaryen, quindi dirò: 4 byte
  • quali istruzioni opzionali il backend può generare: includiamo tutto ciò che troviamo in Binaryen, lasciamo che l'acceleratore suddivida il resto in istruzioni più semplici
  • Qual è la dimensione approssimativa della cache TLB richiesta dal backend. Il fatto è che in QEMU tutto è serio: sebbene ci siano funzioni di supporto che eseguono il caricamento/archiviazione tenendo conto della MMU guest (dove saremmo senza di essa adesso?), salvano la loro cache di traduzione sotto forma di una struttura, la la cui elaborazione è conveniente incorporare direttamente nei blocchi broadcast. La domanda è: quale offset in questa struttura viene elaborato in modo più efficiente da una sequenza piccola e veloce di comandi?
  • qui puoi modificare lo scopo di uno o due registri riservati, abilitare la chiamata TB tramite una funzione e facoltativamente descrivere un paio di piccoli inline-funzioni come flush_icache_range (ma non è il nostro caso)

file tcg-target.inc.c, ovviamente, è solitamente di dimensioni molto più grandi e contiene diverse funzioni obbligatorie:

  • inizializzazione, comprese le restrizioni su quali istruzioni possono operare e su quali operandi. Copiato palesemente da me da un altro backend
  • funzione che accetta un'istruzione bytecode interna
  • Puoi anche inserire qui funzioni ausiliarie e puoi anche utilizzare funzioni statiche da tcg/tcg.c

Per quanto mi riguarda, ho scelto la seguente strategia: nelle prime parole del successivo blocco di traduzione, ho scritto quattro puntatori: un segno di inizio (un certo valore in prossimità 0xFFFFFFFF, che determina lo stato corrente della TB), contesto, modulo generato e numero magico per il debug. Inizialmente è stato apposto il marchio 0xFFFFFFFF - nDove n - un piccolo numero positivo e ogni volta che veniva eseguito tramite l'interprete aumentava di 1. Quando veniva raggiunto 0xFFFFFFFE, avvenuta la compilazione, il modulo è stato salvato nella tabella delle funzioni, importato in un piccolo “launcher”, nel quale è passata l'esecuzione tcg_qemu_tb_exece il modulo è stato rimosso dalla memoria QEMU.

Per parafrasare i classici, “Stampella, quanto si intreccia in questo suono per il cuore del proger...”. Tuttavia, la memoria perdeva da qualche parte. Inoltre, la memoria era gestita da QEMU! Avevo un codice che, quando scrivevo l'istruzione successiva (beh, cioè un puntatore), cancellava quello il cui collegamento era in questo posto prima, ma questo non ha aiutato. In realtà, nel caso più semplice, QEMU alloca memoria all'avvio e scrive lì il codice generato. Quando il buffer si esaurisce, il codice viene espulso e al suo posto inizia a essere scritto quello successivo.

Dopo aver studiato il codice, mi sono reso conto che il trucco con il numero magico mi permetteva di non fallire nella distruzione dell'heap liberando qualcosa di sbagliato su un buffer non inizializzato al primo passaggio. Ma chi riscrive il buffer per bypassare la mia funzione in seguito? Come consigliano gli sviluppatori di Emscripten, quando ho riscontrato un problema, ho riportato il codice risultante nell'applicazione nativa, ho impostato Mozilla Record-Replay su di esso... In generale, alla fine ho capito una cosa semplice: per ogni blocco, UN struct TranslationBlock con la sua descrizione. Indovina dove... Esatto, proprio prima del blocco, proprio nel buffer. Rendendomi conto di ciò, ho deciso di smettere di usare le stampelle (almeno alcune) e ho semplicemente buttato via il numero magico e ho trasferito le parole rimanenti in struct TranslationBlock, creando un elenco collegato singolarmente che può essere attraversato rapidamente quando la cache di traduzione viene reimpostata e liberare memoria.

Rimangono alcune stampelle: ad esempio, puntatori contrassegnati nel buffer del codice - alcuni di essi sono semplicemente BinaryenExpressionRef, cioè esaminano le espressioni che devono essere inserite linearmente nel blocco base generato, in parte è la condizione per la transizione tra BB, in parte è dove andare. Bene, ci sono già dei blocchi pronti per Relooper che devono essere collegati a seconda delle condizioni. Per distinguerli si parte dal presupposto che siano tutti allineati di almeno quattro byte, quindi si possono tranquillamente utilizzare i due bit meno significativi per l'etichetta, basta ricordarsi di rimuoverli se necessario. A proposito, tali etichette sono già utilizzate in QEMU per indicare il motivo dell'uscita dal ciclo del TCG.

Utilizzando Binaryen

I moduli in WebAssembly contengono funzioni, ognuna delle quali contiene un corpo, che è un'espressione. Le espressioni sono operazioni unarie e binarie, blocchi costituiti da elenchi di altre espressioni, flusso di controllo, ecc. Come ho già detto, il flusso di controllo qui è organizzato proprio come rami di alto livello, loop, chiamate di funzione, ecc. Gli argomenti alle funzioni non vengono passati nello stack, ma esplicitamente, proprio come in JS. Ci sono anche variabili globali, ma non le ho usate, quindi non te ne parlerò.

Le funzioni hanno anche variabili locali, numerate da zero, di tipo: int32/int64/float/double. In questo caso, le prime n variabili locali sono gli argomenti passati alla funzione. Tieni presente che sebbene tutto qui non sia del tutto di basso livello in termini di flusso di controllo, gli interi non portano ancora l'attributo "signed/unsigned": il modo in cui si comporta il numero dipende dal codice dell'operazione.

In generale, Binaryen fornisce API C semplice: crei un modulo, in lui creare espressioni: unarie, binarie, blocchi da altre espressioni, flusso di controllo, ecc. Quindi crei una funzione con un'espressione come corpo. Se tu, come me, hai un grafico di transizione di basso livello, il componente relooper ti aiuterà. Per quanto ho capito, è possibile utilizzare il controllo di alto livello del flusso di esecuzione in un blocco, purché non vada oltre i confini del blocco, ovvero sia possibile creare un percorso interno veloce/lento ramificazione del percorso all'interno del codice di elaborazione della cache TLB integrato, ma non per interferire con il flusso di controllo "esterno". Quando liberi un relooper, i suoi blocchi vengono liberati; quando liberi un modulo, le espressioni, le funzioni, ecc. ad esso assegnate scompaiono arena.

Tuttavia, se si desidera interpretare il codice al volo senza la creazione e l'eliminazione non necessarie di un'istanza dell'interprete, potrebbe avere senso inserire questa logica in un file C++ e da lì gestire direttamente l'intera API C++ della libreria, ignorando ready- realizzato involucri.

Quindi, per generare il codice che ti serve

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

... se ho dimenticato qualcosa, scusate, questo è solo per rappresentare la scala, e i dettagli sono nella documentazione.

E ora inizia il crack-fex-pex, qualcosa del genere:

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

Per connettere in qualche modo i mondi di QEMU e JS e allo stesso tempo accedere rapidamente alle funzioni compilate, è stato creato un array (una tabella di funzioni da importare nel launcher) e lì sono state posizionate le funzioni generate. Per calcolare rapidamente l'indice, inizialmente è stato utilizzato l'indice del blocco di traduzione con parole zero, ma poi l'indice calcolato utilizzando questa formula ha iniziato a adattarsi semplicemente al campo in struct TranslationBlock.

Tra l'altro, dimostrazione (attualmente con una licenza oscura) funziona bene solo con Firefox. Gli sviluppatori di Chrome lo erano in qualche modo non è pronto al fatto che qualcuno vorrebbe creare più di mille istanze di moduli WebAssembly, quindi ha semplicemente allocato un gigabyte di spazio di indirizzi virtuali per ciascuno...

È tutto per ora. Forse ci sarà un altro articolo se qualcuno è interessato. Vale a dire, rimane almeno solo far funzionare i dispositivi a blocchi. Potrebbe anche avere senso rendere asincrona la compilazione dei moduli WebAssembly, come è consuetudine nel mondo JS, poiché esiste ancora un interprete che può fare tutto questo finché il modulo nativo non è pronto.

Infine un indovinello: hai compilato un binario su un'architettura a 32 bit, ma il codice, attraverso operazioni di memoria, sale da Binaryen, da qualche parte nello stack o da qualche altra parte nei 2 GB superiori dello spazio degli indirizzi a 32 bit. Il problema è che dal punto di vista di Binaryen questo significa accedere a un indirizzo risultante troppo grande. Come aggirare questo problema?

In modo amministrativo

Non ho finito per testarlo, ma il mio primo pensiero è stato "E se installassi Linux a 32 bit?" Quindi la parte superiore dello spazio degli indirizzi sarà occupata dal kernel. L'unica domanda è quanto sarà occupato: 1 o 2 Gb.

Come un programmatore (opzione per i professionisti)

Facciamo esplodere una bolla nella parte superiore dello spazio degli indirizzi. Io stesso non capisco perché funzioni lì già ci deve essere una pila. Ma «siamo praticanti: tutto funziona per noi, ma nessuno sa perché...»

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

... è vero che non è compatibile con Valgrind, ma fortunatamente Valgrind stesso spinge tutti fuori da lì in modo molto efficace :)

Forse qualcuno spiegherà meglio come funziona questo mio codice...

Fonte: habr.com

Aggiungi un commento