Perché abbiamo bisogno del "no" Vale a dire Nessuna istruzione operativa nel microprocessore 8085?


19

Nell'istruzione del microprocessore 8085, esiste un'operazione di controllo della macchina "nop" (nessuna operazione). La mia domanda è: perché non abbiamo bisogno di un'operazione? Voglio dire, se dovessimo terminare il programma useremo HLT o RST 3. O se vogliamo passare all'istruzione successiva daremo le istruzioni successive. Ma perché nessuna operazione? Qual è la necessità?


5
NOP viene spesso utilizzato per il debug e l'aggiornamento del programma. Se in un secondo momento desideri aggiungere alcune righe al tuo programma, puoi semplicemente sovrascrivere il NOP. Altrimenti dovrai inserire delle righe e inserire significa spostare l'intero programma. Anche le istruzioni errate (errate) possono essere sostituite (semplicemente sovrascritte) da NOP seguendo gli stessi argomenti.
Contrabbandiere di plutonio

Ohkay. Ma usare nop aumenta anche lo spazio. Considerando che il nostro obiettivo principale è quello di farlo prendere poco spazio.
Demietra95

* Intendo che il nostro obiettivo è ridurre un problema. Quindi non diventa anche un problema?
Demietra95

2
Ecco perché dovrebbe essere usato saggiamente. Altrimenti l'intero programma sarà solo un gruppo di NOP.
Contrabbandiere di plutonio

Risposte:


34

Un uso dell'istruzione NOP (o NOOP, nessuna operazione) in CPU e MCU è quello di inserire un piccolo, prevedibile, ritardo nel codice. Sebbene i NOP non eseguano alcuna operazione, ci vuole del tempo per elaborarli (la CPU deve recuperare e decodificare il codice operativo, quindi è necessario un po 'di tempo per farlo). Per eseguire un'istruzione NOP basta "sprecare" solo 1 ciclo di CPU (il numero esatto può essere dedotto dal foglio dati CPU / MCU, di solito), pertanto inserire N NOP in sequenza è un modo semplice per inserire un ritardo prevedibile:

tdelay=NTclockK

Tclock

Perché dovresti farlo? Può essere utile forzare la CPU ad attendere un po 'i dispositivi esterni (forse più lenti) per completare il loro lavoro e riportare i dati alla CPU, ad esempio NOP è utile ai fini della sincronizzazione.

Vedi anche la relativa pagina Wikipedia su NOP .

Un altro uso è allineare il codice a determinati indirizzi in memoria e altri "trucchi di assemblaggio", come spiegato anche in questo thread su Programmers.SE e in questo altro thread su StackOverflow .

Un altro articolo interessante sull'argomento .

Questo link a una pagina del libro di Google riferisce in particolare alla CPU 8085. Estratto:

Ogni istruzione NOP utilizza quattro orologi per il recupero, la decodifica e l'esecuzione.

MODIFICARE (per rispondere a una preoccupazione espressa in un commento)

π


3
Molte cose (in particolare le uscite che pilotano circuiti integrati esterni all'uC) sono soggette a vincoli di temporizzazione come "il tempo minimo tra D è stabile e il limite di clock è 100 us" o "il LED IR deve lampeggiare a 1 MHz". Pertanto, sono spesso necessari ritardi (precisi).
Wouter van Ooijen,

6
I NOP ​​possono essere utili per ottenere il tempismo giusto quando si esegue il bit-bang di un protocollo seriale. Possono anche essere utili per riempire lo spazio di codice inutilizzato seguito da un salto al vettore di avvio a freddo per situazioni rare se il contatore del programma viene danneggiato (ad es. Glitch dell'alimentatore, impatto di un raro evento di raggi gamma, ecc.) E inizia l'esecuzione del codice in un altrimenti parte vuota dello spazio del codice.
Techydude,

6
Sul sistema video-computer Atari 2600 (la seconda console per videogiochi che esegue programmi memorizzati su cartucce), il processore eseguirà esattamente 76 cicli per riga di scansione e molte operazioni dovrebbero essere eseguite con un numero esatto di cicli dopo l'inizio di un linea di scansione. Su quel processore, l'istruzione "NOP" documentata richiede due cicli, ma è anche comune che il codice faccia uso di un'istruzione a tre cicli altrimenti inutile per compensare un ritardo a un numero preciso di cicli. L'esecuzione più veloce del codice avrebbe prodotto una visualizzazione totalmente confusa.
supercat,

1
L'uso dei NOP per i ritardi può avere senso anche su sistemi non in tempo reale nei casi in cui un dispositivo I / O impone un tempo minimo tra operazioni consecutive, ma non un massimo. Ad esempio, su molti controller, lo spostamento di un byte da una porta SPI richiederà otto cicli CPU. Il codice che non fa altro che recuperare i byte dalla memoria e inviarli alla porta SPI potrebbe essere eseguito un po 'troppo velocemente, ma l'aggiunta di logica per verificare se la porta SPI era pronta per ogni byte renderebbe inutilmente lento. L'aggiunta di uno o due NOP può consentire al codice di raggiungere la massima velocità disponibile ...
supercat

1
... nel caso in cui non vi siano interruzioni. Se si verifica un interrupt, i NOP perderebbero inutilmente tempo, ma il tempo perso da uno o due NOP sarebbe inferiore al tempo necessario per determinare se un interrupt li rendesse inutili.
supercat

8

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' RETistruzione (o istruzioni simili). Quando devi patchare l'eseguibile, puoi facilmente aggiungere più codice alla funzione a partire dall'originale RETe 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 NOPeseguito. 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, EDIper 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, AXfaranno 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 NOPabbastanza; il vero codice assembly compilato non ne ha più. Va notato che quelli non sono x86- NOPs, 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, EDIha le migliori prestazioni come un NOP a 2 byte su x86. Se hai usato due NOPs, 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 NOPun'istruzione dedicata , la NOPparola chiave (alias / mnemonico) sarebbe comunque utile e mapperebbe a quello.
  • Istruzioni come in MOVrealtà mappano a molti diversi codici operativi, perché ciò consente di risparmiare tempo e spazio, ad esempio mov al, 42"sposta il byte immediato nel alregistro", che si traduce in 0xB02A( 0xB0essendo il codice operativo, 0x2Aessendo 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, rmbsovraccarico (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, axviene? Devi solo guardare i codici operativi delle altre xhcgistruzioni. Vedrai 0x86, 0x87e infine, 0x91- 0x97. Quindi nopcon 0x90sembra 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 axe 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, immediateset. 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, noppotrebbe essere considerato una vera istruzione o no. La microarchitettura originale la trattava come una variante xchgse ricordo bene, ma in realtà è stata nominata nopnelle specifiche. E poiché xchg ax, axnon 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 0x90associa 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è 0x2Ye 0xX8significa "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.


A dire il vero, NOPdi solito è un alias a MOV ax, ax, ADD ax, 0o le istruzioni simili. Perché dovresti progettare un'istruzione dedicata che non fa nulla quando ce ne sono molti là fuori.
Dmitry Grigoryev il

@DmitryGrigoryev Questo va davvero nella progettazione del linguaggio CPU (beh, microarchitettura) stesso. La maggior parte delle CPU (e compilatori) tenderanno a ottimizzare l' MOV ax, axouts; NOPavrà sempre un numero fisso di cicli per l'esecuzione. Ma non vedo quanto sia rilevante per quello che ho scritto nella mia risposta comunque.
Luaan,

Le CPU non possono davvero ottimizzare una MOV ax, axvia, perché quando sanno che è MOVun'istruzione è già in cantiere.
Dmitry Grigoryev il

@DmitryGrigoryev Dipende molto dal tipo di CPU di cui stai parlando. Le moderne CPU desktop fanno un sacco di cose, non solo il pipeline di istruzioni. Ad esempio, la CPU sa che non deve invalidare le linee della cache, ecc., Sa che non deve fare nulla (molto importante per HyperThreading e persino per le pipe multiple coinvolte in generale). Non sarei sorpreso se influenzasse anche la previsione del ramo (anche se probabilmente sarebbe lo stesso per NOPe MOV ax, ax). Le CPU moderne sono molto più complesse dei compilatori C oldschool :))
Luaan

1
+1 per l'ottimizzazione di nessuno su noöne! Penso che dovremmo tutti collaborare e tornare a questo stile di ortografia! Lo Z80 (e, a sua volta, l'8080) ha 7 LD r, ristruzioni in cui rè presente un singolo registro, simile alle tue MOV ax, axistruzioni. Il motivo è 7 e non 8 è perché una delle istruzioni è sovraccarica per diventare HALT. Quindi 8080 e Z80 hanno almeno altre 7 istruzioni che fanno lo stesso NOP! È interessante notare che, sebbene queste istruzioni non siano logicamente correlate dal modello di bit, tutte richiedono 4 T State per essere eseguite, quindi non c'è motivo di usare le LDistruzioni!
CJ Dennis,

5

Alla fine degli anni '70, noi (ero un giovane studente di ricerca allora) avevamo un piccolo sistema di sviluppo (8080 se la memoria serve) che correva in 1024 byte di codice (cioè una singola UVEPROM) - aveva solo quattro comandi da caricare (L ), salva (S), stampa (P) e qualcos'altro che non ricordo. È stato guidato con un vero teletipo e nastro adesivo. Era strettamente codificato!

Un esempio di utilizzo NOOP è stato in una routine di servizio di interruzione (ISR), che sono stati distanziati a intervalli di 8 byte. Questa routine ha finito per essere lunga 9 byte e termina con un (lungo) salto a un indirizzo leggermente più in alto nello spazio degli indirizzi. Ciò significava, dato il piccolo ordine di byte endian, che il byte dell'indirizzo alto era 00h, e inserito nel primo byte del successivo ISR, il che significava che (il successivo ISR) era iniziato con NOOP, proprio così 'noi' potevamo adattarci il codice nello spazio limitato!

Quindi il NOOP è utile. Inoltre, ho il sospetto che per Intel sia stato più semplice codificarlo in quel modo - Probabilmente avevano un elenco di istruzioni che volevano implementare ed è iniziato da '1', come fanno tutti gli elenchi (erano i giorni di FORTRAN), quindi lo zero Il codice NOOP è diventato un fall out. (Non ho mai visto un articolo che sostenga che un NOOP è una parte essenziale della teoria della scienza informatica (la stessa Q di: i matematici hanno un nul op, distinto dallo zero della teoria dei gruppi?)


Non tutte le CPU hanno il NOP codificato su 0x00 (anche se l'8085, l'8080 e la CPU con cui ho più familiarità, lo Z80, lo fanno tutti). Tuttavia, se stavo progettando un processore è lì che lo metterei! Un'altra cosa utile è che la memoria è di solito inizializzata su tutti gli 0x00, quindi l'esecuzione come codice non farà nulla fino a quando la CPU non raggiungerà la memoria non azzerata.
CJ Dennis,

@CJDennis Ho spiegato perché le CPU x86 non usano 0x00per la nopmia risposta. In breve, risparmia sulla decodifica delle istruzioni - xchg ax, axscorre naturalmente dal modo in cui funziona la decodifica delle istruzioni e fa qualcosa di "rumoroso", quindi perché non usarlo e chiamarlo nop, giusto? :) Utilizzato per risparmiare un bel po 'sul silicio per la decodifica delle istruzioni ...
Luaan

5

Su alcune architetture, NOPviene utilizzato per occupare slot di ritardo non utilizzati . Ad esempio, se l'istruzione di diramazione non cancella la pipeline, vengono comunque eseguite diverse istruzioni dopo che è stata eseguita:

 JMP     .label
 MOV     R2, 1    ; these instructions start execution before the jump
 MOV     R2, 2    ; takes place so they still get executed

Ma cosa succede se non hai istruzioni utili per adattarti dopo il JMP? In tal caso dovrai usare NOPs.

Gli slot di ritardo non si limitano ai salti. In alcune architetture, i rischi relativi ai dati nella pipeline della CPU non vengono risolti automaticamente. Ciò significa che dopo ogni istruzione che modifica un registro è presente uno slot in cui il nuovo valore del registro non è ancora accessibile. Se l'istruzione successiva richiede quel valore, lo slot deve essere occupato da un NOP:

 ADD     R1, R1
 NOP               ; The new value of R1 is not ready yet
 ADD     R1, R3

Inoltre, alcune istruzioni di esecuzione condizionale ( If-True-False e simili) usano gli slot per ogni condizione e quando una condizione particolare non ha azioni associate ad essa, il suo slot dovrebbe essere occupato da NOP:

CMP     R0, R1       ; Compare R0 and R1, setting flags
ITF     GT           ; If-True-False on GT flag 
MOV     R3, R2       ; If 'greater than', move R2 to R3
NOP                  ; Else, do nothing

+1. Naturalmente, quelli tendono a presentarsi solo su architetture che non si preoccupano della retrocompatibilità - se x86 provasse qualcosa del genere quando introducevano il pipeline di istruzioni, quasi tutti lo avrebbero semplicemente sbagliato (dopo tutto, avevano appena aggiornato la loro CPU e il loro le applicazioni hanno smesso di funzionare!). Quindi x86 deve assicurarsi che l'applicazione non si accorga quando miglioramenti come questo vengono aggiunti alla CPU - fino a quando non arriviamo comunque alle CPU multi-core ...: D
Luaan

2

Un altro esempio di utilizzo per un NOP a due byte : http://blogs.msdn.com/b/oldnewthing/archive/2011/09/21/10214405.aspx

L'istruzione MOV EDI, EDI è un NOP a due byte, che è lo spazio sufficiente per eseguire il patch in un'istruzione di salto in modo che la funzione possa essere aggiornata al volo. L'intenzione è che l'istruzione MOV EDI, EDI venga sostituita con un'istruzione JMP $ -5 a due byte per reindirizzare il controllo a cinque byte di spazio patch che viene immediatamente prima dell'inizio della funzione. Cinque byte sono sufficienti per un'istruzione di salto completo, che può inviare il controllo alla funzione di sostituzione installata altrove nello spazio degli indirizzi.

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.