Le altre risposte stanno prendendo in considerazione solo un NOP che effettivamente viene eseguito ad un certo punto - che viene usato abbastanza comunemente, ma non è l'unico uso di NOP.
Il NOP non eseguibile è anche molto utile quando si scrive codice che può essere patchato - fondamentalmente, si riempirà la funzione con alcuni NOP dopo l' RET
istruzione (o istruzioni simili). Quando devi patchare l'eseguibile, puoi facilmente aggiungere più codice alla funzione a partire dall'originale RET
e usando tutti quei NOP di cui hai bisogno (ad es. Per salti lunghi o persino codice inline) e finire con un altroRET
.
In questo caso d'uso, noöne non si aspetta mai che venga NOP
eseguito. L'unico punto è consentire il patching dell'eseguibile: in un eseguibile teorico non imbottito, dovresti effettivamente cambiare il codice della funzione stessa (a volte potrebbe adattarsi ai confini originali, ma abbastanza spesso avrai bisogno di un salto comunque ) - è molto più complicato, specialmente considerando l'assemblaggio scritto manualmente o un compilatore ottimizzante; devi rispettare salti e costrutti simili che potrebbero aver puntato su un pezzo importante di codice. Tutto sommato, piuttosto complicato.
Ovviamente, questo era molto più utilizzato ai vecchi tempi, quando era utile creare patch come queste piccole e online . Oggi distribuirai semplicemente un binario ricompilato e finirai con esso. Ci sono ancora alcuni che usano patch NOP (eseguendo o no, e non sempre letterali NOP
- ad esempio, Windows utilizza MOV EDI, EDI
per patch online - è il tipo in cui è possibile aggiornare una libreria di sistema mentre il sistema è effettivamente in esecuzione, senza bisogno di riavvii).
Quindi l'ultima domanda è: perché avere un'istruzione dedicata per qualcosa che in realtà non fa nulla?
- È un'istruzione reale, importante durante il debug o l'assemblaggio della codifica manuale. Le istruzioni come
MOV AX, AX
faranno esattamente lo stesso, ma non segnalano l'intento in modo così chiaro.
- Padding - "codice" che è lì solo per migliorare le prestazioni generali del codice che dipende dall'allineamento. Non è mai pensato per essere eseguito. Alcuni debugger nascondono semplicemente i NOP di riempimento nel loro smontaggio.
- Offre più spazio per l'ottimizzazione dei compilatori: il modello ancora usato è che hai due passaggi di compilazione, il primo è piuttosto semplice e produce un sacco di codice assembly inutile, mentre il secondo pulisce, ricollega i riferimenti dell'indirizzo e rimuove istruzioni estranee. Questo è spesso visto anche nei linguaggi compilati da JIT: sia il codice IL che .NET di JVM usano il codice byte
NOP
abbastanza; il vero codice assembly compilato non ne ha più. Va notato che quelli non sono x86- NOP
s, però.
- Semplifica il debug online sia per la lettura (la memoria pre-azzerata sarà tutto
NOP
, rendendo molto più facile la lettura dello smontaggio) sia per il hot patching (anche se preferisco di gran lunga Modifica e Continua in Visual Studio: P).
Per l'esecuzione di NOP, ci sono ovviamente alcuni altri punti:
- Prestazioni, ovviamente - questo non è il motivo per cui era nell'8085, ma anche l'80486 aveva già un'esecuzione di istruzioni pipeline, che rende "non fare nulla" un po 'più complicato.
- Come visto
MOV EDI, EDI
, ci sono altri NOP efficaci oltre al letterale NOP
. MOV EDI, EDI
ha le migliori prestazioni come un NOP a 2 byte su x86. Se hai usato due NOP
s, sarebbero due istruzioni da eseguire.
MODIFICARE:
In realtà, la discussione con @DmitryGrigoryev mi ha costretto a pensarci un po 'di più, e penso che sia una preziosa aggiunta a questa domanda / risposta, quindi lasciatemi aggiungere alcuni bit extra:
Innanzitutto, punto, ovviamente: perché dovrebbe esserci un'istruzione che fa qualcosa del genere mov ax, ax
? Ad esempio, diamo un'occhiata al caso del codice macchina 8086 (più vecchio persino del codice macchina 386):
- C'è un'istruzione NOP dedicata con opcode
0x90
. Questo è ancora il momento in cui molte persone hanno scritto in assemblea, intendiamoci. Quindi, anche se non ci fosse NOP
un'istruzione dedicata , la NOP
parola chiave (alias / mnemonico) sarebbe comunque utile e mapperebbe a quello.
- Istruzioni come in
MOV
realtà mappano a molti diversi codici operativi, perché ciò consente di risparmiare tempo e spazio, ad esempio mov al, 42
"sposta il byte immediato nel al
registro", che si traduce in 0xB02A
( 0xB0
essendo il codice operativo, 0x2A
essendo l'argomento "immediato"). Quindi ci vogliono due byte.
- Non esiste un codice operativo di scelta rapida per
mov al, al
(dal momento che è una cosa stupida da fare, in pratica), quindi dovrai usare il mov al, rmb
sovraccarico (rmb essendo "registro o memoria"). Ciò richiede in realtà tre byte. (anche se probabilmente userebbe invece il meno specifico mov rb, rmb
, che dovrebbe richiedere solo due byte mov al, al
- il byte dell'argomento viene usato per specificare sia il registro di origine che quello di destinazione; ora sai perché 8086 aveva solo 8 registri: D). Confronta con NOP
, che è un'istruzione a byte singolo! Ciò consente di risparmiare memoria e tempo, dal momento che leggere la memoria nell'8086 era ancora piuttosto costoso - per non parlare del caricamento di quel programma da un nastro o floppy o qualcosa del genere, ovviamente.
Quindi da dove xchg ax, ax
viene? Devi solo guardare i codici operativi delle altre xhcg
istruzioni. Vedrai 0x86
, 0x87
e infine, 0x91
- 0x97
. Quindi nop
con 0x90
sembra una soluzione abbastanza adatta per xchg ax, ax
(che, di nuovo, non è un xchg
"sovraccarico" - dovresti usare xchg rb, rmb
, a due byte). E in effetti, sono abbastanza sicuro che questo sia stato un piacevole effetto collaterale della microarchitettura dell'epoca - se ricordo bene, era facile mappare l'intera gamma di 0x90-0x97
"xchg, agendo su registri ax
e ax
- di
" ( essendo l'operando simmetrico, questo ti dava l'intera gamma, incluso il nop xchg ax, ax
; nota che l'ordine è ax, cx, dx, bx, sp, bp, si, di
- bx
è dopo dx
,ax
; ricorda, i nomi dei registri sono mnemonici, non nomi ordinati - accumulatore, contatore, dati, base, puntatore stack, puntatore base, indice sorgente, indice destinazione). Lo stesso approccio è stato utilizzato anche per altri operandi, ad esempio il mov someRegister, immediate
set. In un certo senso, potresti pensare a questo come se il codice operativo non fosse in realtà un intero byte - gli ultimi bit sono "un argomento" per l'operando "reale".
Tutto ciò detto, su x86, nop
potrebbe essere considerato una vera istruzione o no. La microarchitettura originale la trattava come una variante xchg
se ricordo bene, ma in realtà è stata nominata nop
nelle specifiche. E poiché xchg ax, ax
non ha davvero senso come istruzione, puoi vedere come i progettisti dell'8086 hanno risparmiato su transistor e percorsi nella decodifica delle istruzioni sfruttando il fatto che si 0x90
associa naturalmente a qualcosa che è del tutto "rumoroso".
D'altra parte, i8051 ha un codice operativo interamente progettato per nop
- 0x00
. Un po 'pratico. La progettazione delle istruzioni utilizza fondamentalmente il crocino alto per il funzionamento e il crocino basso per la selezione degli operandi - ad esempio, add a
è 0x2Y
e 0xX8
significa "registro 0 diretto", così 0x28
è add a, r0
. Risparmia molto sul silicio :)
Potrei ancora continuare, dal momento che il design della CPU (per non parlare del design del compilatore e del linguaggio) è un argomento piuttosto ampio, ma penso di aver mostrato molti punti di vista diversi che sono entrati nel design abbastanza bene come sono.