Codice automodificante - Self-modifying code
In informatica , il codice automodificante è un codice che altera le proprie istruzioni durante l' esecuzione , di solito per ridurre la lunghezza del percorso dell'istruzione e migliorare le prestazioni o semplicemente per ridurre codice altrimenti ripetitivamente simile, semplificando così la manutenzione. L'auto-modifica è un'alternativa al metodo di "impostazione dei flag" e alla ramificazione condizionale del programma, utilizzata principalmente per ridurre il numero di volte in cui una condizione deve essere verificata. Il termine viene solitamente applicato solo al codice in cui l'auto-modifica è intenzionale, non in situazioni in cui il codice si modifica accidentalmente a causa di un errore come un buffer overflow .
Il metodo viene spesso utilizzato per richiamare in modo condizionale il codice di test/debug senza richiedere un sovraccarico computazionale aggiuntivo per ogni ciclo di input/output .
Le modifiche possono essere eseguite:
- solo durante l'inizializzazione – in base ai parametri di input (quando il processo è più comunemente descritto come ' configurazione ' software ed è in qualche modo analogo, in termini hardware, all'impostazione dei ponticelli per i circuiti stampati ). L'alterazione dei puntatori di ingresso del programma è un metodo indiretto equivalente di automodifica, ma richiede la coesistenza di uno o più percorsi di istruzione alternativi, aumentando la dimensione del programma .
- durante l'esecuzione ("al volo") - in base a particolari stati del programma che sono stati raggiunti durante l'esecuzione
In entrambi i casi, le modifiche possono essere eseguite direttamente sulle istruzioni del codice macchina stesse, sovrapponendo nuove istruzioni a quelle esistenti (ad esempio: modificando un confronto e un salto in un salto incondizionato o in alternativa un ' NOP ').
Nel set di istruzioni IBM/360 e Z/Architecture , un'istruzione EXECUTE (EX) sovrappone logicamente il secondo byte della sua istruzione di destinazione con gli 8 bit di ordine inferiore del registro 1. Ciò fornisce l'effetto di auto-modifica sebbene l'istruzione effettiva in deposito non viene alterato.
Applicazione in linguaggi di basso e alto livello
L'auto-modifica può essere eseguita in vari modi a seconda del linguaggio di programmazione e del suo supporto per i puntatori e/o l'accesso ai "motori" del compilatore dinamico o dell'interprete:
- sovrapposizione di istruzioni esistenti (o parti di istruzioni come codice operativo, registro, flag o indirizzo) o
- creazione diretta di intere istruzioni o sequenze di istruzioni in memoria
- creazione o modifica di istruzioni del codice sorgente seguite da una "mini compilazione" o da un'interpretazione dinamica (vedi istruzione eval )
- creare dinamicamente un intero programma e poi eseguirlo
linguaggio assembly
Il codice automodificante è abbastanza semplice da implementare quando si utilizza il linguaggio assembly . Le istruzioni possono essere create dinamicamente in memoria (oppure sovrapposte al codice esistente nella memoria di programma non protetta), in una sequenza equivalente a quelle che un compilatore standard può generare come codice oggetto . Con i processori moderni, possono verificarsi effetti collaterali indesiderati sulla cache della CPU che devono essere considerati. Il metodo è stato spesso utilizzato per testare le condizioni di "prima volta", come in questo esempio di assembler IBM/360 opportunamente commentato . Utilizza la sovrapposizione delle istruzioni per ridurre la lunghezza del percorso dell'istruzione di (N × 1)-1 dove N è il numero di record sul file (-1 è l' overhead per eseguire la sovrapposizione).
SUBRTN NOP OPENED FIRST TIME HERE?
* The NOP is x'4700'<Address_of_opened>
OI SUBRTN+1,X'F0' YES, CHANGE NOP TO UNCONDITIONAL BRANCH (47F0...)
OPEN INPUT AND OPEN THE INPUT FILE SINCE IT'S THE FIRST TIME THRU
OPENED GET INPUT NORMAL PROCESSING RESUMES HERE
...
Il codice alternativo potrebbe comportare il test di un "flag" ogni volta. Il ramo incondizionato è leggermente più veloce di un'istruzione di confronto, oltre a ridurre la lunghezza complessiva del percorso. Nei sistemi operativi successivi per i programmi che risiedono nella memoria protetta questa tecnica non poteva essere utilizzata e quindi sarebbe stata utilizzata la modifica del puntatore alla subroutine . Il puntatore risiederebbe nella memoria dinamica e potrebbe essere modificato a piacimento dopo il primo passaggio per bypassare l'OPEN (dovendo caricare prima un puntatore invece di un ramo diretto e il collegamento alla subroutine aggiungerebbe N istruzioni alla lunghezza del percorso - ma ci sarebbe una corrispondente riduzione di N per il ramo incondizionato che non sarebbe più necessario).
Di seguito è riportato un esempio in linguaggio assembly Zilog Z80 . Gli incrementi di codice registrano "B" nell'intervallo [0,5]. L'istruzione di confronto "CP" viene modificata ad ogni loop.
;======================================================================
ORG 0H
CALL FUNC00
HALT
;======================================================================
FUNC00:
LD A,6
LD HL,label01+1
LD B,(HL)
label00:
INC B
LD (HL),B
label01:
CP $0
JP NZ,label00
RET
;======================================================================
Il codice automodificante viene talvolta utilizzato per superare i limiti nel set di istruzioni di una macchina. Ad esempio, nel set di istruzioni Intel 8080 , non è possibile immettere un byte da una porta di input specificata in un registro. La porta di ingresso è codificata staticamente nell'istruzione stessa, come il secondo byte di un'istruzione a due byte. Utilizzando il codice automodificante, è possibile memorizzare il contenuto di un registro nel secondo byte dell'istruzione, quindi eseguire l'istruzione modificata per ottenere l'effetto desiderato.
Linguaggi di alto livello
Alcuni linguaggi compilati consentono esplicitamente il codice automodificante. Ad esempio, il verbo ALTER in COBOL può essere implementato come istruzione di salto che viene modificata durante l'esecuzione. Alcune tecniche di programmazione batch implicano l'uso di codice automodificante. Clipper e SPITBOL forniscono anche strutture per l'auto-modifica esplicita. Il compilatore Algol sui sistemi B6700 offriva un'interfaccia al sistema operativo per cui l'esecuzione del codice poteva passare una stringa di testo o un file disco denominato al compilatore Algol ed era quindi in grado di invocare la nuova versione di una procedura.
Con i linguaggi interpretati, il "codice macchina" è il testo sorgente e può essere suscettibile di modifiche al volo: in SNOBOL le istruzioni sorgente in esecuzione sono elementi di un array di testo. Altri linguaggi, come Perl e Python , consentono ai programmi di creare nuovo codice in fase di esecuzione ed eseguirlo utilizzando una funzione eval , ma non consentono la modifica del codice esistente. L'illusione della modifica (anche se nessun codice macchina viene realmente sovrascritto) si ottiene modificando i puntatori a funzione, come in questo esempio JavaScript:
var f = function (x) {return x + 1};
// assign a new definition to f:
f = new Function('x', 'return x + 2');
Le macro Lisp consentono anche la generazione di codice di runtime senza analizzare una stringa contenente codice di programma.
Il linguaggio di programmazione Push è un sistema di programmazione genetica esplicitamente progettato per creare programmi auto-modificanti. Sebbene non sia un linguaggio di alto livello, non è di basso livello come il linguaggio assembly.
Modifica del composto
Prima dell'avvento di più finestre, i sistemi a riga di comando potevano offrire un sistema di menu che implicava la modifica di uno script di comando in esecuzione. Supponiamo che un file di script DOS (o "batch") Menu.bat contenga quanto segue:
:StartAfresh <-A line starting with a colon marks a label. ShowMenu.exe
All'avvio di Menu.bat dalla riga di comando, ShowMenu presenta un menu su schermo, con possibili informazioni di aiuto, esempi di utilizzo e così via. Alla fine l'utente effettua una selezione che richiede l'esecuzione di un comando somename : ShowMenu esce dopo aver riscritto il file Menu.bat per contenere
:StartAfresh ShowMenu.exe CALL C:\Commands\somename.bat GOTO StartAfresh
Poiché l'interprete dei comandi DOS non compila un file di script e quindi lo esegue, né legge l'intero file in memoria prima di iniziare l'esecuzione, né si basa ancora sul contenuto di un buffer di record, quando ShowMenu esce, l'interprete dei comandi trova un nuovo comando da eseguire (si tratta di invocare il file di script somename , in una posizione di directory e tramite un protocollo noto a ShowMenu), e dopo che il comando è stato completato, torna all'inizio del file di script e riattiva ShowMenu pronto per la selezione successiva . Se la scelta del menu dovesse essere l'uscita, il file verrebbe riscritto al suo stato originale. Sebbene questo stato di partenza non abbia alcuna utilità per l'etichetta, essa o una quantità equivalente di testo è richiesta, poiché l'interprete dei comandi DOS richiama la posizione in byte del comando successivo quando deve avviare il comando successivo, quindi il file riscritto deve mantenere l'allineamento affinché il punto di inizio del comando successivo sia effettivamente l'inizio del comando successivo.
A parte la comodità di un sistema di menu (e possibili funzioni ausiliarie), questo schema significa che il sistema ShowMenu.exe non è in memoria quando viene attivato il comando selezionato, un vantaggio significativo quando la memoria è limitata.
Tabelle di controllo
Gli interpreti della tabella di controllo possono essere considerati, in un certo senso, "auto-modificati" dai valori dei dati estratti dalle voci della tabella (piuttosto che specificamente codificati a mano in istruzioni condizionali della forma "IF inputx = 'yyy'").
Programmi del canale
Alcuni metodi di accesso IBM tradizionalmente utilizzavano programmi di canale automodificanti , in cui un valore, ad esempio un indirizzo del disco, viene letto in un'area a cui fa riferimento un programma di canale, dove viene utilizzato da un successivo comando di canale per accedere al disco.
Storia
L' IBM SSEC , dimostrato nel gennaio 1948, aveva la capacità di modificare le sue istruzioni o comunque di trattarle esattamente come dati. Tuttavia, la capacità è stata utilizzata raramente nella pratica. Agli albori dei computer, il codice automodificante veniva spesso utilizzato per ridurre l'uso della memoria limitata o migliorare le prestazioni o entrambe le cose. A volte veniva anche utilizzato per implementare chiamate e ritorni di subroutine quando il set di istruzioni forniva solo semplici istruzioni di salto o diramazione per variare il flusso di controllo . Questo uso è ancora rilevante in alcune architetture ultra- RISC , almeno teoricamente; vedere per esempio un computer con set di istruzioni . L' architettura MIX di Donald Knuth utilizzava anche codice automodificante per implementare chiamate di subroutine.
utilizzo
Il codice automodificante può essere utilizzato per vari scopi:
- Ottimizzazione semiautomatica di un ciclo dipendente dallo stato.
- Run-time generazione di codice, o la specializzazione di un algoritmo in fase di esecuzione o loadtime (che è popolare, per esempio, nel campo della grafica in tempo reale), come ad esempio un programma di utilità generale sorta - preparare il codice per eseguire il confronto chiave descritto in una specifica invocazione.
- Alterare di inline stato di un oggetto , o simulare la costruzione di alto livello di chiusure .
- Patching della chiamata dell'indirizzo di subroutine ( pointer ), solitamente come eseguito al momento del caricamento/inizializzazione delle librerie dinamiche , oppure ad ogni chiamata, patchando i riferimenti interni della subroutine ai suoi parametri in modo da utilizzare i loro indirizzi effettivi. (cioè 'automodificazione' indiretta).
- Sistemi informatici evolutivi come la neuroevoluzione , la programmazione genetica e altri algoritmi evolutivi .
- Nascondere il codice per impedire il reverse engineering (mediante un disassemblatore o un debugger ) o per eludere il rilevamento da parte di software di scansione virus/spyware e simili.
- Riempire il 100% della memoria (in alcune architetture) con uno schema a rotazione di codici operativi ripetuti , per cancellare tutti i programmi e i dati o per eseguire il burn-in dell'hardware.
- Compressione del codice da decomprimere ed eseguire in fase di runtime, ad esempio, quando la memoria o lo spazio su disco sono limitati.
- Alcuni set di istruzioni molto limitati non lasciano altra scelta che utilizzare il codice automodificante per eseguire determinate funzioni. Ad esempio, un computer con un set di istruzioni (OISC) che utilizza solo l'"istruzione" sottrai e ramifica se negativo non può eseguire una copia indiretta (qualcosa come l'equivalente di "*a = **b" nel C language ) senza utilizzare il codice automodificante.
- Avvio . I primi microcomputer usavano spesso codice automodificante nei loro bootloader. Poiché il bootloader veniva digitato tramite il pannello frontale ad ogni accensione, non importava se il bootloader si modificava. Tuttavia, anche oggi molti caricatori bootstrap si spostano da soli e alcuni si modificano anche da soli.
- Alterare le istruzioni per la tolleranza agli errori.
Ottimizzazione di un ciclo dipendente dallo stato
Esempio di pseudocodice :
repeat N times {
if STATE is 1
increase A by one
else
decrease A by one
do something with A
}
Il codice automodificante, in questo caso, sarebbe semplicemente questione di riscrivere il ciclo in questo modo:
repeat N times {
increase A by one
do something with A
when STATE has to switch {
replace the opcode "increase" above with the opcode to decrease, or vice versa
}
}
Nota che la sostituzione a due stati dell'opcode può essere facilmente scritta come 'xor var at address con il valore "opcodeOf(Inc) xor opcodeOf(dec)"'.
La scelta di questa soluzione deve dipendere dal valore di N e dalla frequenza del cambiamento di stato.
Specializzazione
Supponiamo che un insieme di statistiche come media, estremi, posizione degli estremi, deviazione standard, ecc. debba essere calcolato per un insieme di dati di grandi dimensioni. In una situazione generale, potrebbe esserci la possibilità di associare pesi ai dati, quindi ogni x i è associato a aw i e anziché verificare la presenza di pesi ad ogni valore dell'indice, potrebbero esserci due versioni del calcolo, una da utilizzare con pesi e uno no, con una prova in partenza. Consideriamo ora un'ulteriore opzione, che ad ogni valore possa essere associato un booleano per indicare se quel valore deve essere saltato o meno. Questo potrebbe essere gestito producendo quattro batch di codice, uno per ogni permutazione e risultati di codice bloat. In alternativa, il peso e gli array skip potrebbero essere uniti in un array temporaneo (con pesi zero per i valori da saltare), al costo dell'elaborazione e comunque c'è troppo. Tuttavia, con la modifica del codice, al modello per il calcolo delle statistiche potrebbe essere aggiunto a seconda dei casi il codice per saltare i valori indesiderati e per l'applicazione dei pesi. Non ci sarebbero test ripetuti delle opzioni e l'array di dati sarebbe accessibile una volta, così come gli array di peso e di salto, se coinvolti.
Usa come camuffamento
Il codice automodificante è stato utilizzato per nascondere le istruzioni di protezione dalla copia nei programmi basati su disco degli anni '80 per piattaforme come IBM PC e Apple II . Ad esempio, su un PC IBM (o compatibile ), l' istruzione di accesso all'unità disco floppyint 0x13 non apparirà nell'immagine del programma eseguibile ma verrà scritta nell'immagine di memoria dell'eseguibile dopo l'avvio dell'esecuzione del programma.
Il codice automodificante viene talvolta utilizzato anche da programmi che non vogliono rivelare la loro presenza, come virus informatici e alcuni shellcode . I virus e gli shellcode che utilizzano codice automodificante lo fanno principalmente in combinazione con codice polimorfico . La modifica di una parte del codice in esecuzione viene utilizzata anche in alcuni attacchi, come i buffer overflow .
Sistemi di apprendimento automatico autoreferenziali
I tradizionali sistemi di apprendimento automatico hanno un algoritmo di apprendimento fisso e preprogrammato per regolare i loro parametri . Tuttavia, dagli anni '80 Jürgen Schmidhuber ha pubblicato diversi sistemi automodificanti con la capacità di modificare il proprio algoritmo di apprendimento. Evitano il pericolo di auto-riscritture catastrofiche assicurandosi che le auto-modifiche sopravvivano solo se sono utili in base a una funzione di fitness , errore o ricompensa data dall'utente.
Sistemi operativi
A causa delle implicazioni sulla sicurezza del codice automodificante, tutti i principali sistemi operativi sono attenti a rimuovere tali vulnerabilità non appena vengono note. La preoccupazione in genere non è che i programmi si modifichino intenzionalmente da soli, ma che possano essere modificati in modo dannoso da un exploit .
Come conseguenza dei problemi che possono essere causati da questi exploit, è stata sviluppata una funzionalità del sistema operativo chiamata W^X (che sta per "scrivi xor esegui") che proibisce a un programma di rendere scrivibile ed eseguibile qualsiasi pagina di memoria. Alcuni sistemi impediscono che una pagina scrivibile venga modificata per renderla eseguibile, anche se l'autorizzazione alla scrittura viene rimossa. Altri sistemi forniscono una sorta di " porta sul retro ", che consente a più mappature di una pagina di memoria di avere permessi diversi. Un modo relativamente portatile per aggirare W^X è creare un file con tutti i permessi, quindi mappare il file in memoria due volte. Su Linux, si può usare un flag di memoria condivisa SysV non documentato per ottenere memoria condivisa eseguibile senza bisogno di creare un file.
Indipendentemente da ciò, a un meta-livello , i programmi possono ancora modificare il proprio comportamento modificando i dati memorizzati altrove (vedi metaprogrammazione ) o tramite l'uso del polimorfismo .
Interazione tra cache e codice automodificante
Su architetture senza dati accoppiati e cache istruzioni (alcuni core ARM e MIPS) la sincronizzazione della cache deve essere eseguita esplicitamente dal codice di modifica (svuota la cache dei dati e invalida la cache delle istruzioni per l'area di memoria modificata).
In alcuni casi brevi sezioni di codice automodificante vengono eseguite più lentamente sui processori moderni. Questo perché un processore moderno di solito cerca di mantenere blocchi di codice nella sua memoria cache. Ogni volta che il programma riscrive una parte di se stesso, la parte riscritta deve essere nuovamente caricata nella cache, il che si traduce in un leggero ritardo, se il codelet modificato condivide la stessa riga di cache con il codice modificato, come nel caso della memoria modificata l'indirizzo si trova entro pochi byte da quello del codice di modifica.
Il problema dell'invalidazione della cache sui processori moderni di solito significa che il codice auto-modificante sarebbe ancora più veloce solo quando la modifica si verificherà raramente, come nel caso di un cambio di stato all'interno di un ciclo interno.
La maggior parte dei processori moderni carica il codice macchina prima di eseguirlo, il che significa che se un'istruzione troppo vicina al puntatore dell'istruzione viene modificata, il processore non se ne accorgerà, ma eseguirà invece il codice come era prima che fosse modificato. Vedere la coda di input di prelettura (PIQ). I processori per PC devono gestire correttamente il codice automodificante per motivi di compatibilità con le versioni precedenti, ma sono tutt'altro che efficienti nel farlo.
Il kernel di sintesi di Massalin
Il kernel di sintesi presentato nel dottorato di ricerca di Alexia Massalin . thesis è un minuscolo kernel Unix che adotta un approccio strutturato , o addirittura orientato agli oggetti , al codice automodificante, in cui il codice viene creato per quaject individuali , come i filehandle. La generazione di codice per attività specifiche consente al kernel di sintesi (come potrebbe un interprete JIT) di applicare una serie di ottimizzazioni come il ripiegamento costante o l' eliminazione di sottoespressioni comuni .
Il kernel di Synthesis era molto veloce, ma era scritto interamente in assembly. La conseguente mancanza di portabilità ha impedito alle idee di ottimizzazione di Massalin di essere adottate da qualsiasi kernel di produzione. Tuttavia, la struttura delle tecniche suggerisce che potrebbero essere catturate da un linguaggio di livello superiore , sebbene più complesso dei linguaggi di medio livello esistenti. Tale linguaggio e compilatore potrebbero consentire lo sviluppo di sistemi operativi e applicazioni più veloci.
Paul Haeberli e Bruce Karsh si sono opposti alla "marginalizzazione" del codice automodificante, e all'ottimizzazione in generale, a favore della riduzione dei costi di sviluppo.
Vantaggi
- È possibile stabilire percorsi veloci per l'esecuzione di un programma, riducendo alcuni rami condizionali altrimenti ripetitivi .
- Il codice automodificante può migliorare l' efficienza algoritmica .
Svantaggi
Il codice automodificante è più difficile da leggere e mantenere perché le istruzioni nell'elenco del programma sorgente non sono necessariamente le istruzioni che verranno eseguite. L'auto-modifica che consiste nella sostituzione dei puntatori di funzione potrebbe non essere così criptica, se è chiaro che i nomi delle funzioni da chiamare sono segnaposto per le funzioni da identificare in seguito.
Il codice automodificante può essere riscritto come codice che verifica un flag e si dirama a sequenze alternative in base all'esito del test, ma il codice automodificante viene in genere eseguito più velocemente.
Sui processori moderni con una pipeline di istruzioni , il codice che si modifica frequentemente può essere eseguito più lentamente se modifica le istruzioni che il processore ha già letto dalla memoria nella pipeline. Su alcuni di questi processori, l'unico modo per garantire che le istruzioni modificate vengano eseguite correttamente è svuotare la pipeline e rileggere molte istruzioni.
Il codice automodificante non può essere utilizzato affatto in alcuni ambienti, come i seguenti:
- Il software applicativo in esecuzione su un sistema operativo con rigorosa sicurezza W^X non può eseguire istruzioni nelle pagine in cui è consentito scrivere: solo il sistema operativo è autorizzato sia a scrivere istruzioni in memoria che a eseguirle successivamente.
- Molti microcontrollori dell'architettura Harvard non possono eseguire istruzioni in memoria di lettura-scrittura, ma solo istruzioni in memoria su cui non è possibile scrivere, ROM o memoria flash non autoprogrammabile .
- Un'applicazione multithread può avere diversi thread che eseguono la stessa sezione di codice auto-modificante, con conseguenti errori di calcolo e errori dell'applicazione.
Guarda anche
- Codice polimorfico
- Motore polimorfico
- Struttura dati persistente
- Codice AARD
- Efficienza algoritmica
- dichiarazione di valutazione
- IBM 1130 (esempio)
- Compilazione just-in-time : questa tecnica può spesso offrire agli utenti molti dei vantaggi del codice automodificante (eccetto la dimensione della memoria) senza gli svantaggi.
- Eliminazione dinamica del codice morto
- omoiconicità
- PCASTL
- Quine (informatica)
- Auto-replica
- Riflessione (informatica)
- Monkey patch : una modifica al codice runtime che non influisce sul codice sorgente originale di un programma
- Programmazione estensibile : un paradigma di programmazione in cui un linguaggio di programmazione può modificare la propria sintassi
- Virus informatico
- Self-hosting