As outras respostas estão considerando apenas um NOP que realmente é executado em algum momento - que é usado com bastante frequência, mas não é o único uso do NOP.
O NOP não em execução também é bastante útil ao escrever código que pode ser corrigido - basicamente, você preencherá a função com alguns NOPs após a RET
(ou instrução similar). Quando você precisar corrigir o executável, poderá adicionar facilmente mais código à função, começando pelo original RET
e usando quantos NOPs forem necessários (por exemplo, para saltos longos ou mesmo código embutido) e terminando com outro RET
.
Nesse caso de uso, ninguém espera que a NOP
execução seja executada. O único ponto é permitir o patch do executável - em um executável teórico não acolchoado, você teria que realmente alterar o código da função (às vezes, ele pode se ajustar aos limites originais, mas muitas vezes você precisará de um salto de qualquer maneira ) - isso é muito mais complicado, especialmente considerando a montagem escrita manualmente ou um compilador de otimização; você deve respeitar saltos e construções semelhantes que possam ter apontado em algum trecho de código importante. Em suma, bastante complicado.
Obviamente, isso era muito mais usado nos velhos tempos, quando era útil fazer correções como essas pequenas e online . Hoje, você apenas distribuirá um binário recompilado e pronto. Ainda existem alguns que usam NOPs de patches (executando ou não, e nem sempre literais NOP
s - por exemplo, o Windows usa MOV EDI, EDI
para patches on-line - é o tipo em que você pode atualizar uma biblioteca do sistema enquanto o sistema está realmente em execução, sem a necessidade de reiniciar).
Portanto, a última pergunta é: por que ter uma instrução dedicada para algo que realmente não faz nada?
- É uma instrução real - importante ao depurar ou codificar manualmente a montagem. Instruções como
MOV AX, AX
farão exatamente o mesmo, mas não sinalizam a intenção com tanta clareza.
- Preenchimento - "código" disponível apenas para melhorar o desempenho geral do código que depende do alinhamento. Nunca foi feito para executar. Alguns depuradores simplesmente ocultam os NOPs de preenchimento em sua desmontagem.
- Ele oferece mais espaço para otimizar compiladores - o padrão ainda usado é que você tem duas etapas de compilação, a primeira sendo bastante simples e produzindo muito código de montagem desnecessário, enquanto a segunda limpa, renova as referências de endereço e remove instruções estranhas. Isso também é visto também em linguagens compiladas por JIT - o IL do .NET e o código de bytes da JVM usam
NOP
bastante; o código de montagem compilado real não tem mais isso. Deve-se notar que esses não são x86- NOP
s.
- Isso facilita a depuração on-line, tanto para a leitura (a memória pré-zerada será tudo
NOP
, facilitando muito a leitura da desmontagem) quanto para o hot-patch (embora eu prefira, de longe, Editar e Continuar no Visual Studio: P).
Para executar NOPs, é claro que existem mais alguns pontos:
- Desempenho, é claro - não foi por isso que ocorreu em 8085, mas mesmo o 80486 já tinha uma execução de instruções em pipeline, o que torna o "não fazer nada" um pouco mais complicado.
- Como visto
MOV EDI, EDI
, há outros NOPs efetivos que não o literal NOP
. MOV EDI, EDI
tem o melhor desempenho como um NOP de 2 bytes no x86. Se você usasse dois NOP
s, seriam duas instruções para executar.
EDITAR:
Na verdade, a discussão com @DmitryGrigoryev me forçou a pensar um pouco mais sobre isso, e acho que é uma adição valiosa a esta pergunta / resposta, então deixe-me adicionar alguns bits extras:
Primeiro, ponto, obviamente - por que haveria uma instrução que faça algo assim mov ax, ax
? Por exemplo, vejamos o caso do código de máquina 8086 (mais antigo que o código de máquina 386):
- Há uma instrução NOP dedicada com opcode
0x90
. Ainda é o momento em que muitas pessoas escreveram em assembléia, lembre-se. Portanto, mesmo se não houvesse uma NOP
instrução dedicada , a NOP
palavra - chave (alias / mnemonic) ainda seria útil e seria mapeada para isso.
- Instruções como
MOV
na verdade são mapeadas para muitos opcodes diferentes, porque isso economiza tempo e espaço - por exemplo, mov al, 42
é "mover byte imediato para o al
registrador", que se traduz em 0xB02A
( 0xB0
sendo o opcode, 0x2A
sendo o argumento "imediato"). Então isso leva dois bytes.
- Não existe um código de atalho para
mov al, al
(já que isso é uma coisa estúpida de se fazer, basicamente), então você terá que usar a mov al, rmb
sobrecarga (rmb sendo "registradora ou memória"). Na verdade, isso leva três bytes. (embora provavelmente use o menos específico mov rb, rmb
, que deve levar apenas dois bytes para mov al, al
- o argumento byte é usado para especificar o registro de origem e o destino; agora você sabe por que o 8086 tinha apenas 8 registros: D). Compare com NOP
, que é uma instrução de byte único! Isso economiza memória e tempo, já que a leitura da memória no 8086 ainda era muito cara - sem mencionar o carregamento desse programa a partir de uma fita ou disquete ou algo do tipo, é claro.
Então, de onde xchg ax, ax
vem? Você apenas precisa olhar os códigos de código das outras xhcg
instruções. Você verá 0x86
, 0x87
e, finalmente, 0x91
- 0x97
. Portanto, nop
com isso 0x90
parece um bom ajuste para xchg ax, ax
(o que, novamente, não é uma xchg
"sobrecarga" - você precisaria usar xchg rb, rmb
, em dois bytes). Na verdade, tenho certeza de que esse foi um bom efeito colateral da microarquitetura da época - se bem me lembro, foi fácil mapear todo o intervalo de 0x90-0x97
"xchg, agindo sobre registros ax
e ax
- di
" ( o operando é simétrico, isso ofereceu a faixa completa, incluindo o nop xchg ax, ax
; observe que a ordem é ax, cx, dx, bx, sp, bp, si, di
- bx
é depois dx
,ax
; lembre-se, os nomes dos registradores são mnemônicos, não os pedidos - acumulador, contador, dados, base, ponteiro de pilha, ponteiro de base, índice de origem, índice de destino). A mesma abordagem também foi usada para outros operandos, por exemplo, o mov someRegister, immediate
conjunto. De certa forma, você poderia pensar nisso como se o código de operação não fosse realmente um byte completo - os últimos bits são "um argumento" para o operando "real".
Tudo isso dito, no x86, nop
pode ser considerado uma instrução real, ou não. A microarquitetura original tratou-a como uma variante de, xchg
se bem me lembro, mas na verdade foi nomeada nop
na especificação. E como xchg ax, ax
realmente não faz sentido como uma instrução, você pode ver como os projetistas do 8086 economizaram em transistores e caminhos na decodificação de instruções, explorando o fato de que 0x90
mapeia naturalmente algo que é totalmente "desagradável".
Por outro lado, o i8051 possui um código de operação totalmente projetado para nop
- 0x00
. Meio prático. O design das instruções está basicamente usando a mordidela alta para operação e a mordidela baixa para selecionar os operandos - por exemplo, add a
é 0x2Y
e 0xX8
significa "registre 0 direto", assim 0x28
é add a, r0
. Economiza muito em silício :)
Eu ainda poderia continuar, já que o design da CPU (para não mencionar o design do compilador e o design da linguagem) é um tópico bastante amplo, mas acho que mostrei muitos pontos de vista diferentes que entraram no design muito bem.