Esistono casi intelligenti di modifica del codice runtime?


119

Riesci a pensare a qualche uso legittimo (intelligente) per la modifica del codice in runtime (programma che modifica il proprio codice in fase di esecuzione)?

I sistemi operativi moderni sembrano disapprovare i programmi che lo fanno poiché questa tecnica è stata utilizzata dai virus per evitare il rilevamento.

Tutto quello a cui riesco a pensare è una sorta di ottimizzazione del runtime che rimuoverebbe o aggiungerebbe del codice conoscendo qualcosa in runtime che non può essere conosciuto in fase di compilazione.


8
Sulle architetture moderne, interferisce gravemente con la memorizzazione nella cache e la pipeline di istruzioni: il codice di auto-modifica finirebbe per non modificare la cache, quindi avresti bisogno di barriere e questo probabilmente rallenterebbe il tuo codice. E non puoi modificare il codice che è già nella pipeline delle istruzioni. Pertanto, qualsiasi ottimizzazione basata sul codice che si modifica automaticamente deve essere eseguita molto prima che il codice venga eseguito per avere un impatto sulle prestazioni superiore, ad esempio, a un controllo di runtime.
Alexandre C.

7
@Alexandre: è comune che il codice auto-modificante apporti modifiche raramente (ad esempio una, due volte) nonostante venga eseguito un numero arbitrario di volte, quindi il costo una tantum può essere insignificante.
Tony Delroy

7
Non sono sicuro del motivo per cui questo è etichettato C o C ++, poiché nessuno dei due ha alcun meccanismo per questo.
MSalters

4
@Alexandre: Microsoft Office è noto per fare esattamente questo. Di conseguenza (?) Tutti i processori x86 hanno un eccellente supporto per l'auto-modifica del codice. Su altri processori è necessaria una costosa sincronizzazione che rende il tutto meno attraente.
Mackie Messer

3
@Cawas: di solito il software di aggiornamento automatico scaricherà nuovi assembly e / o eseguibili e sovrascriverà quelli esistenti. Quindi riavvierà il software. Questo è ciò che fanno Firefox, Adobe, ecc. L'auto-modifica in genere significa che durante il runtime il codice viene riscritto in memoria dall'applicazione a causa di alcuni parametri e non necessariamente persistito su disco. Ad esempio, potrebbe ottimizzare interi percorsi di codice se è in grado di rilevare in modo intelligente quei percorsi che non verrebbero esercitati durante questa particolare esecuzione per accelerare l'esecuzione.
NotMe

Risposte:


117

Esistono molti casi validi per la modifica del codice. La generazione di codice in fase di esecuzione può essere utile per:

  • Alcune macchine virtuali utilizzano la compilazione JIT per migliorare le prestazioni.
  • La generazione di funzioni specializzate al volo è stata a lungo comune nella computer grafica. Vedi ad esempio Rob Pike e Bart Locanthi e John Reiser Hardware Software Tradeoffs for Bitmap Graphics on the Blit (1984) o questo intervento (2006) di Chris Lattner sull'uso di LLVM da parte di Apple per la specializzazione del codice runtime nel loro stack OpenGL.
  • In alcuni casi il software ricorre a una tecnica nota come trampolino che implica la creazione dinamica di codice sullo stack (o in un altro posto). Esempi sono le funzioni annidate di GCC e il meccanismo dei segnali di alcuni Unix.

A volte il codice viene tradotto in codice in fase di esecuzione (questa è chiamata traduzione binaria dinamica ):

  • Emulatori come Rosetta di Apple utilizzano questa tecnica per accelerare l'emulazione. Un altro esempio è il software di code morphing di Transmeta .
  • Debugger e profiler sofisticati come Valgrind o Pin lo usano per strumentare il tuo codice durante l'esecuzione.
  • Prima che venissero apportate estensioni al set di istruzioni x86, il software di virtualizzazione come VMWare non poteva eseguire direttamente codice x86 privilegiato all'interno delle macchine virtuali. Invece doveva tradurre al volo qualsiasi istruzione problematica in un codice personalizzato più appropriato.

La modifica del codice può essere utilizzata per aggirare i limiti del set di istruzioni:

  • C'è stato un tempo (molto tempo fa, lo so), in cui i computer non avevano istruzioni per tornare da una subroutine o per indirizzare indirettamente la memoria. Il codice auto-modificante era l'unico modo per implementare subroutine, puntatori e array .

Altri casi di modifica del codice:

  • Molti debugger sostituiscono le istruzioni per implementare i punti di interruzione .
  • Alcuni linker dinamici modificano il codice in fase di esecuzione. Questo articolo fornisce alcune informazioni di base sul riposizionamento in fase di esecuzione delle DLL di Windows, che è effettivamente una forma di modifica del codice.

10
Questo elenco sembra mescolare esempi di codice che modifica se stesso e codice che modifica altro codice, come i linker.
AShelly

6
@ AShelly: Beh, se consideri il linker / caricatore dinamico come parte del codice, allora si modifica da solo. Vivono nello stesso spazio degli indirizzi, quindi penso che sia un punto di vista valido.
Mackie Messer

1
Ok, l'elenco ora distingue tra programmi e software di sistema. Spero che questo abbia un senso. Alla fine qualsiasi classificazione è discutibile. Tutto si riduce a ciò che includi esattamente nella definizione di programma (o codice).
Mackie Messer

35

Ciò è stato fatto in computer grafica, in particolare in rendering di software per scopi di ottimizzazione. In fase di esecuzione viene esaminato lo stato di molti parametri e viene generata una versione ottimizzata del codice rasterizzatore (potenzialmente eliminando molti condizionali) che consente di rendere le primitive grafiche, ad esempio i triangoli, molto più velocemente.


5
Una lettura interessante sono gli articoli Pixomatic in 3 parti di Michael Abrash su DDJ: drdobbs.com/architecture-and-design/184405765 , drdobbs.com/184405807 , drdobbs.com/184405848 . Il secondo link (Part2) parla del saldatore di codice Pixomatic per la pipeline di pixel.
typo.pl

1
Un articolo molto carino sull'argomento. Dal 1984, ma ancora una buona lettura: Rob Pike e Bart Locanthi e John Reiser. Compromessi software hardware per grafica bitmap su Blit .
Mackie Messer

5
Charles Petzold spiega un esempio di questo tipo in un libro intitolato "Beautiful Code": amazon.com/Beautiful-Code-Leading-Programmers-Practice/dp/…
Nawaz

3
Questa risposta parla della generazione del codice, ma la domanda riguarda la modifica del codice ...
Timwi

3
@Timwi - ha modificato il codice. Piuttosto che gestire una grande catena di if, ha analizzato la forma una volta e ha riscritto il renderer in modo che fosse impostato per il tipo corretto di forma senza dover controllare ogni volta. È interessante notare che questo è ora comune con il codice Opencl: poiché è compilato al volo, puoi riscriverlo per il caso specifico in fase di esecuzione
Martin Beckett

23

Una ragione valida è perché il set di istruzioni asm manca di alcune istruzioni necessarie, che potresti costruire tu stesso. Esempio: su x86 non c'è modo di creare un interrupt a una variabile in un registro (es. Make interrupt con numero di interrupt in ax). Erano consentiti solo i numeri const codificati nel codice operativo. Con il codice automodificante si potrebbe emulare questo comportamento.


Giusto. C'è qualche uso di questa tecnica? Sembra pericoloso.
Alexandre C.

4
@Alexandre C .: Se ricordo bene, molte librerie di runtime (C, Pascal, ...) avevano a DOS volte una funzione per eseguire chiamate di interrupt. Poiché una tale funzione ottiene il numero di interrupt come parametro che dovevi fornire tale funzione (ovviamente se il numero fosse costante avresti potuto generare il codice giusto, ma non era garantito). E tutte le librerie lo hanno implementato con codice automodificante.
flolo

È possibile utilizzare un caso switch per farlo senza modificare il codice. Il ridimensionamento è che il codice di output sarà più grande
phuclv

17

Alcuni compilatori lo usavano per l'inizializzazione di variabili statiche, evitando il costo di un condizionale per gli accessi successivi. In altre parole, implementano "esegui questo codice solo una volta" sovrascrivendo quel codice con no-op la prima volta che viene eseguito.


1
Molto carino, soprattutto se evita i blocchi / sblocchi mutex.
Tony Delroy

2
Veramente? Come funziona questo per il codice basato su ROM o per il codice eseguito nel segmento di codice protetto da scrittura?
Ira Baxter

1
@Ira Baxter: qualsiasi compilatore che emette codice rilocabile sa che il segmento di codice è scrivibile, almeno durante l'avvio. Quindi l'istruzione "alcuni compilatori l'hanno usata" è ancora possibile.
MSalters

17

Ci sono molti casi:

  • I virus usano comunemente codice auto-modificante per "deoffuscare" il codice prima dell'esecuzione, ma questa tecnica può anche essere utile per frustrare il reverse engineering, il cracking e gli hacker indesiderati
  • In alcuni casi, può esserci un punto particolare durante il runtime (ad es. Immediatamente dopo aver letto il file di configurazione) in cui è noto che - per il resto della vita del processo - un particolare ramo verrà sempre o mai preso: piuttosto che inutilmente controllando alcune variabili per determinare in che modo eseguire il branch, l'istruzione branch stessa potrebbe essere modificata di conseguenza
    • Ad esempio, potrebbe essere noto che verrà gestito solo uno dei possibili tipi derivati, in modo tale che l'invio virtuale possa essere sostituito con una chiamata specifica
    • Dopo aver rilevato quale hardware è disponibile, l'uso di un codice corrispondente potrebbe essere codificato
  • Il codice non necessario può essere sostituito con istruzioni non operative o un salto su di esso, oppure spostare direttamente il bit successivo di codice in posizione (più semplice se si utilizzano codici operativi indipendenti dalla posizione)
  • Il codice scritto per facilitare il proprio debug potrebbe iniettare un'istruzione trap / segnale / interrupt prevista dal debugger in una posizione strategica.
  • Alcune espressioni del predicato basate sull'input dell'utente potrebbero essere compilate in codice nativo da una libreria
  • Inlining alcune semplici operazioni che non sono visibili fino al runtime (ad esempio dalla libreria caricata dinamicamente) ...
  • Aggiunta condizionale di passaggi di auto-strumentazione / profilazione
  • I crack possono essere implementati come librerie che modificano il codice che li carica (non si "auto" modificando esattamente, ma necessita delle stesse tecniche e autorizzazioni).
  • ...

I modelli di sicurezza di alcuni sistemi operativi significano che il codice che si modifica automaticamente non può essere eseguito senza i privilegi di root / amministratore, rendendolo poco pratico per un uso generico.

Da Wikipedia:

Il software applicativo in esecuzione in un sistema operativo con protezione W ^ X rigorosa non può eseguire istruzioni nelle pagine su cui è consentito scrivere: solo il sistema operativo stesso può sia scrivere istruzioni in memoria sia eseguirle successivamente.

Su tali sistemi operativi, anche programmi come Java VM necessitano dei privilegi di root / amministratore per eseguire il loro codice JIT. (Vedi http://en.wikipedia.org/wiki/W%5EX per maggiori dettagli)


2
Non sono necessari i privilegi di root per modificare automaticamente il codice. Nemmeno la Java VM.
Mackie Messer

Non sapevo che alcuni sistemi operativi fossero così rigidi. Ma ha sicuramente senso in alcune applicazioni. Mi chiedo tuttavia se l'esecuzione di Java con privilegi di root aumenti effettivamente la sicurezza ...
Mackie Messer

@Mackie: penso che debba diminuirlo, ma forse può impostare alcuni permessi di memoria quindi cambiare l'uid effettivo in qualche account utente ...?
Tony Delroy

Sì, mi aspetto che abbiano un meccanismo a grana fine per concedere autorizzazioni per accompagnare il rigoroso modello di sicurezza.
Mackie Messer

15

Il sistema operativo Synthesis sostanzialmente valutato parzialmente il programma in relazione alle chiamate API e ha sostituito il codice del sistema operativo con i risultati. Il vantaggio principale è che molti controlli degli errori sono stati eliminati (perché se il tuo programma non chiede al sistema operativo di fare qualcosa di stupido, non ha bisogno di controllare).

Sì, questo è un esempio di ottimizzazione del runtime.


Non riesco a vedere il punto. Se diciamo che una chiamata di sistema sarà vietata dal sistema operativo, probabilmente riceverai un errore che dovrai controllare nel codice, vero? Mi sembra che modificare l'eseguibile invece di restituire un codice di errore sia una specie di overengineering.
Alexandre C.

@Alexandre C.: potresti essere in grado di eliminare i controlli del puntatore nullo in questo modo. Spesso è banalmente ovvio per il chiamante che un argomento è valido.
MSalters

@Alexandre: puoi leggere la ricerca al link. Penso che abbiano ottenuto accelerazioni abbastanza impressionanti, e questo sarebbe il punto: -}
Ira Baxter

2
Per le chiamate di sistema relativamente banali e non associate a I / O, i risparmi sono significativi. Ad esempio, se stai scrivendo un demone per Unix, ci sono un sacco di chiamate di sistema boilerplate che fai per disconnettere stdio, impostare vari gestori di segnali, ecc. Se sai che i parametri di una chiamata sono costanti e che il i risultati saranno sempre gli stessi (chiudendo stdin, per esempio), molto del codice che esegui nel caso generale non è necessario.
Mark Bessey

1
Se leggi la tesi, il capitolo 8 contiene alcuni numeri davvero impressionanti sull'I / O in tempo reale non banale per l'acquisizione dei dati. Ricordi che questa è una tesi della metà degli anni '80 e la macchina su cui girava era 10? Mhz 68000, è stato in grado di acquisire nel software dati audio di qualità CD (44.000 campioni al secondo) con un semplice software vecchio. Ha affermato che le workstation Sun (Unix classico) potevano raggiungere solo 1/5 di quella velocità. Sono un vecchio programmatore in linguaggio assembly di quei giorni, e questo è piuttosto spettacolare.
Ira Baxter

9

Molti anni fa ho trascorso una mattinata cercando di eseguire il debug di un codice che si modificava automaticamente, un'istruzione ha cambiato l'indirizzo di destinazione dell'istruzione seguente, cioè stavo calcolando un indirizzo di filiale. Era scritto in linguaggio assembly e funzionava perfettamente quando ho eseguito il programma un'istruzione alla volta. Ma quando ho eseguito il programma non è riuscito. Alla fine, mi sono reso conto che la macchina stava recuperando 2 istruzioni dalla memoria e (poiché le istruzioni erano disposte in memoria) l'istruzione che stavo modificando era già stata recuperata e quindi la macchina stava eseguendo la versione non modificata (errata) dell'istruzione. Ovviamente, durante il debug, eseguivo solo un'istruzione alla volta.

Il mio punto, il codice auto-modificante può essere estremamente sgradevole da testare / eseguire il debug e spesso ha presupposti nascosti sul comportamento della macchina (sia hardware che virtuale). Inoltre, il sistema non potrebbe mai condividere le code page tra i vari thread / processi in esecuzione sulle (ora) macchine multi-core. Ciò vanifica molti dei vantaggi della memoria virtuale, ecc. Inoltre, invaliderebbe le ottimizzazioni dei rami eseguite a livello di hardware.

(Nota - non ho incluso JIT nella categoria del codice auto-modificante. JIT sta traducendo da una rappresentazione del codice a una rappresentazione alternativa, non sta modificando il codice)

Tutto sommato, è solo una cattiva idea - davvero chiara, davvero oscura, ma davvero cattiva.

naturalmente - se tutto ciò che hai è un 8080 e ~ 512 byte di memoria potresti dover ricorrere a tali pratiche.


1
Non lo so, il bene e il male non sembrano essere le categorie giuste per pensarci. Ovviamente dovresti sapere davvero cosa stai facendo e anche perché lo stai facendo. Ma il programmatore che ha scritto quel codice probabilmente non voleva che tu vedessi cosa stava facendo il programma. Ovviamente è brutto se devi eseguire il debug di codice in questo modo. Ma quel codice era molto probabilmente pensato per essere così.
Mackie Messer

Le moderne CPU x86 hanno un rilevamento SMC più potente di quanto richiesto sulla carta: osservando il recupero delle istruzioni obsolete su x86 con codice auto-modificante . E sulla maggior parte delle CPU non x86 (come ARM), la cache delle istruzioni non è coerente con le cache dei dati, quindi è necessario lo svuotamento / sincronizzazione manuale prima che i byte appena memorizzati possano essere eseguiti in modo affidabile come istruzioni. community.arm.com/processors/b/blog/posts/… . In ogni caso, le prestazioni di SMC sono terribili sulle CPU moderne, a meno che non si modifichi una volta e si esegua molte volte.
Peter Cordes,

7

Dal punto di vista di un kernel del sistema operativo, ogni Just In Time Compiler e Linker Runtime esegue l'auto modifica del testo del programma. Un esempio importante sarebbe l'interprete di script ECMA V8 di Google.


5

Un altro motivo per modificare il codice (in realtà un codice "autogenerante") è implementare un meccanismo di compilazione Just-In-time per le prestazioni. Ad esempio, un programma che legge un'espressione algebrica e la calcola su una gamma di parametri di input può convertire l'espressione in codice macchina prima di dichiarare il calcolo.


5

Sapete il vecchio castagno che non c'è differenza logica tra hardware e software ... si può anche dire che non c'è differenza logica tra codice e dati.

Cos'è il codice automodificante? Codice che inserisce valori nel flusso di esecuzione in modo che possa essere interpretato non come dati ma come comando. Sicuramente c'è il punto di vista teorico nei linguaggi funzionali secondo cui non c'è davvero differenza. Sto dicendo che posso farlo in modo diretto in linguaggi imperativi e compilatori / interpreti senza la presunzione di uno status uguale.

Ciò a cui mi riferisco è nel senso pratico che i dati possono alterare i percorsi di esecuzione del programma (in un certo senso questo è estremamente ovvio). Sto pensando a qualcosa di simile a un compilatore-compilatore che crea una tabella (un array di dati) che si attraversa durante l'analisi, spostandosi da stato a stato (e anche modificando altre variabili), proprio come un programma si sposta da comando a comando , modificando le variabili nel processo.

Quindi, anche nella solita istanza in cui un compilatore crea lo spazio del codice e fa riferimento a uno spazio dati completamente separato (l'heap), è comunque possibile modificare i dati per cambiare esplicitamente il percorso di esecuzione.


4
Nessuna differenza logica, vero. Tuttavia, non ho visto troppi circuiti integrati automodificanti.
Ira Baxter

@ Mitch, IMO che cambia il percorso exec non ha nulla a che fare con la (auto) modifica del codice. Inoltre, confondi i dati con le informazioni. Non posso rispondere al tuo commento alla mia risposta in LSE b / c Sono bandito lì, dal febbraio, per 3 anni (1.000 giorni) per aver espresso in meta-LSE il mio punto di vista che americani e britannici non possiedono l'inglese.
Gennady Vanin Геннадий Ванин

4

Ho implementato un programma utilizzando evolution per creare il miglior algoritmo. Ha usato il codice auto-modificante per modificare il progetto del DNA.


2

Un caso d'uso è il file di test EICAR che è un file COM eseguibile DOS legittimo per testare i programmi antivirus.

X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*

Deve utilizzare la modifica del codice automatico perché il file eseguibile deve contenere solo caratteri ASCII stampabili / digitabili nell'intervallo [21h-60h, 7Bh-7Dh], il che limita in modo significativo il numero di istruzioni codificabili

I dettagli sono spiegati qui


Viene anche utilizzato per l'invio di operazioni in virgola mobile in DOS

Alcuni compilatori emetteranno CD xxcon xx compreso tra 0x34-0x3B al posto delle istruzioni x87 in virgola mobile. Poiché CDè il codice operativo per l' intistruzione, salterà nell'interrupt 34h-3Bh ed emulerà quell'istruzione nel software se il coprocessore x87 non è disponibile. Altrimenti il ​​gestore di interrupt sostituirà quei 2 byte con in 9B Dxmodo che le esecuzioni successive verranno gestite direttamente da x87 senza emulazione.

Qual è il protocollo per l'emulazione in virgola mobile x87 in MS-DOS?


1

Il kernel Linux ha moduli caricabili del kernel che fanno proprio questo.

Anche Emacs ha questa capacità e la uso sempre.

Tutto ciò che supporta un'architettura plug-in dinamica lo sta essenzialmente modificando il codice in fase di esecuzione.


4
difficilmente. avere una libreria caricabile dinamicamente che non è sempre residente ha molto poco a che fare con il codice che si modifica automaticamente.
Dov

1

Eseguo analisi statistiche su un database in continuo aggiornamento. Il mio modello statistico viene scritto e riscritto ogni volta che il codice viene eseguito per accogliere i nuovi dati che diventano disponibili.


0

Lo scenario in cui può essere utilizzato è un programma di apprendimento. In risposta all'input dell'utente, il programma apprende un nuovo algoritmo:

  1. cerca la base di codice esistente per un algoritmo simile
  2. se nessun algoritmo simile è nel codice base, il programma aggiunge semplicemente un nuovo algoritmo
  3. se esiste un algoritmo simile, il programma (magari con qualche aiuto da parte dell'utente) modifica l'algoritmo esistente per essere in grado di servire sia il vecchio scopo che il nuovo scopo

C'è una domanda su come farlo in Java: quali sono le possibilità di auto-modifica del codice Java?


-1

La versione migliore di questo potrebbe essere Lisp Macros. A differenza delle macro C che sono solo un preprocessore, Lisp ti consente di avere accesso all'intero linguaggio di programmazione in ogni momento. Questa è la funzionalità più potente di lisp e non esiste in nessun'altra lingua.

Non sono affatto un esperto, ma fai parlare uno dei ragazzi lisp! C'è una ragione per cui dicono che il Lisp è il linguaggio più potente in circolazione e le persone intelligenti no che probabilmente hanno ragione.


2
Questo crea effettivamente codice auto modificante o è solo un preprocessore più potente (uno che genererà funzioni)?
Brendan Long

@Brendan: in effetti, ma esso è il modo giusto per eseguire la preelaborazione. Non ci sono modifiche al codice di runtime qui.
Alexandre C.
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.