Por que precisamos da instrução “nop” Ie No operation no microprocessador 8085?

19

Na instrução do microprocessador 8085, há uma operação de controle da máquina "nop" (sem operação). Minha pergunta é: por que precisamos de uma operação sem? Quero dizer, se tivermos que encerrar o programa, usaremos o HLT ou o RST 3. Ou se queremos passar para a próxima instrução, forneceremos as próximas instruções. Mas por que nenhuma operação? Qual é a necessidade?

Demietra95
fonte
5
O NOP é freqüentemente usado na depuração e atualização do seu programa. Se, em uma data posterior, você desejar adicionar algumas linhas ao seu programa, basta substituir o NOP. Caso contrário, você terá que inserir linhas, e inserir significa mudar o programa inteiro. Também instruções defeituosas (incorretas) podem ser substituídas (simplesmente substituídas) pelo NOP seguindo os mesmos argumentos.
Contrabandista de plutônio
Ohkay. Mas usar nop também está aumentando o espaço. Considerando que nosso objetivo principal é fazer com que isso ocorra pouco.
Demitra95
* Quero dizer que nosso objetivo é diminuir um problema. Então, isso também não se torna um problema?
Demietra95
2
É por isso que deve ser usado com sabedoria. Caso contrário, todo o seu programa será apenas um monte de NOPs.
Contrabandista de plutônio

Respostas:

34

Um uso da instrução NOP (ou NOOP, sem operação) em CPUs e MCUs é inserir um pequeno atraso previsível no seu código. Embora os NOPs não realizem nenhuma operação, leva algum tempo para processá-los (a CPU precisa buscar e decodificar o código de operação, portanto, precisa de algum tempo para fazer isso). Tão pouco quanto 1 ciclo de CPU é "desperdiçado" para executar uma instrução NOP (o número exato pode ser deduzido da folha de dados da CPU / MCU, geralmente), portanto, colocar N NOPs em seqüência é uma maneira fácil de inserir um atraso previsível:

tdeeuumay=NTceuockK

onde K é o número de ciclos (na maioria das vezes 1) necessário para o processamento de uma instrução NOP e é o período do relógio.Tceuock

Por que você faria isso? Pode ser útil forçar a CPU a esperar um pouco pelos dispositivos externos (talvez mais lentos) para concluir seu trabalho e relatar dados à CPU, ou seja, o NOP é útil para fins de sincronização.

Veja também a página da Wikipedia relacionada no NOP .

Outro uso é alinhar o código em determinados endereços da memória e outros "truques de montagem", conforme explicado também neste tópico no Programmers.SE e neste outro tópico no StackOverflow .

Outro artigo interessante sobre o assunto .

Este link para uma página de livro do Google refere-se especialmente à CPU 8085. Excerto:

Cada instrução NOP usa quatro relógios para buscar, decodificar e executar.

EDIT (para resolver uma preocupação expressa em um comentário)

Se você estiver preocupado com a velocidade, lembre-se de que a eficiência (tempo) é apenas um parâmetro a considerar. Tudo depende da aplicação: se você deseja calcular o décimo bilionésimo número de , talvez sua única preocupação seja a velocidade. Por outro lado, se você deseja registrar dados de sensores de temperatura conectados a um MCU por meio de um ADC, a velocidade geralmente não é tão importante, mas é essencial aguardar a quantidade certa de tempo para permitir que o ADC conclua corretamente cada leitura . Nesse caso, se o MCU não esperar o suficiente, corre o risco de obter dados completamente não confiáveis ​​(eu admito que os dados seriam mais rápidos , porém: o).π

Lorenzo Donati apoia Monica
fonte
3
Muitas coisas (especialmente as saídas que direcionam os ICs externos para o uC) estão sujeitas a restrições de tempo como 'o tempo mínimo entre D sendo estável e o limite do relógio é de 100 us' ou 'o LED IR deve piscar em 1 MHz'. Portanto, atrasos (precisos) são frequentemente necessários.
Wouter van Ooijen
6
Os NOPs podem ser úteis para acertar a sincronização ao bater um protocolo serial em bits. Eles também podem ser úteis para preencher o espaço de código não utilizado seguido de um salto para o vetor de inicialização a frio para situações raras, se o contador do programa for corrompido (por exemplo, falha na PSU, impacto de um evento raro de raios gama, etc.) e começar a executar o código em um caso contrário, esvazie parte do espaço do código.
Techydude
6
No Atari 2600 Video Computer System (o segundo console de videogame a executar programas armazenados em cartuchos), o processador executava exatamente 76 ciclos por linha de varredura e muitas operações precisavam ser executadas com um número exato de ciclos após o início de um linha de digitalização. Nesse processador, a instrução "NOP" documentada leva dois ciclos, mas também é comum o código fazer uso de uma instrução de três ciclos inútil para reduzir um atraso a um número preciso de ciclos. A execução de código mais rápido produziria uma exibição totalmente distorcida.
Supercat 31/05
1
O uso de NOPs para atrasos pode fazer sentido, mesmo em sistemas não em tempo real, nos casos em que um dispositivo de E / S impõe um tempo mínimo entre operações consecutivas, mas não um máximo. Por exemplo, em muitos controladores, mudar um byte para fora de uma porta SPI levará oito ciclos de CPU. O código que não faz nada além de buscar bytes da memória e enviá-los para a porta SPI pode correr um pouco rápido demais, mas adicionar lógica para testar se a porta SPI estava pronta para cada byte o tornaria desnecessariamente lento. A adição de um NOP ou dois pode permitir que o código atinja a velocidade máxima disponível ... #
308
1
... no caso em que não há interrupções. Se ocorrer uma interrupção, os NOPs desnecessariamente perderão tempo, mas o tempo desperdiçado por um ou dois NOPs seria menor que o tempo necessário para determinar se uma interrupção os tornava desnecessários.
supercat
8

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 RETe 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 NOPexecuçã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 NOPs - por exemplo, o Windows usa MOV EDI, EDIpara 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, AXfarã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 NOPbastante; o código de montagem compilado real não tem mais isso. Deve-se notar que esses não são x86- NOPs.
  • 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, EDItem o melhor desempenho como um NOP de 2 bytes no x86. Se você usasse dois NOPs, 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 NOPinstrução dedicada , a NOPpalavra - chave (alias / mnemonic) ainda seria útil e seria mapeada para isso.
  • Instruções como MOVna 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 alregistrador", que se traduz em 0xB02A( 0xB0sendo o opcode, 0x2Asendo 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, rmbsobrecarga (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, axvem? Você apenas precisa olhar os códigos de código das outras xhcginstruções. Você verá 0x86, 0x87e, finalmente, 0x91- 0x97. Portanto, nopcom isso 0x90parece 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 axe 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, immediateconjunto. 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, noppode ser considerado uma instrução real, ou não. A microarquitetura original tratou-a como uma variante de, xchgse bem me lembro, mas na verdade foi nomeada nopna especificação. E como xchg ax, axrealmente 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 0x90mapeia 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é 0x2Ye 0xX8significa "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.

Luaan
fonte
Na verdade, NOPgeralmente é um apelido para MOV ax, ax, ADD ax, 0ou instruções semelhantes. Por que você projetaria uma instrução dedicada que não faz nada quando há muitas por aí?
Dmitry Grigoryev
@DmitryGrigoryev Isso realmente entra no design da linguagem da CPU (bem, na microarquitetura). A maioria das CPUs (e compiladores) tenderá a otimizar a MOV ax, axdistância; NOPsempre terá uma quantidade fixa de ciclos para execução. Mas não vejo como isso seja relevante para o que escrevi na minha resposta.
Luaan
As CPUs não podem realmente otimizar uma MOV ax, axdistância, porque quando sabem que é uma MOVinstrução já está no pipeline.
Dmitry Grigoryev
@DmitryGrigoryev Isso realmente depende do tipo de CPU que você está falando. As CPUs de desktop modernas fazem muitas coisas, não apenas o pipelining de instruções. Por exemplo, a CPU sabe que não precisa invalidar linhas de cache, etc, sabe que não precisa fazer nada (muito importante para o HyperThreading e até para os vários pipes envolvidos em geral). Eu não ficaria surpreso se isso também influenciasse a previsão do ramo (embora isso provavelmente seja o mesmo para NOPe MOV ax, ax). CPUs modernos são muito mais complexas do que os compiladores C oldschool :))
Luaan
1
+1 para otimizar ninguém para ninguém! Acho que todos devemos cooperar e voltar a esse estilo de ortografia! O Z80 (e, por sua vez, o 8080) possui 7 LD r, rinstruções onde rexiste um único registro, semelhante à sua MOV ax, axinstrução. O motivo é 7 e não 8 é porque uma das instruções está sobrecarregada para se tornar HALT. Portanto, o 8080 e o Z80 têm pelo menos 7 outras instruções que fazem o mesmo que NOP! Curiosamente, mesmo que essas instruções não sejam logicamente relacionadas por padrão de bits, todas elas levam 4 estados T para serem executadas, portanto não há razão para usar as LDinstruções!
CJ Dennis
5

No final dos anos 70, nós (eu era um jovem estudante de pesquisa na época) tínhamos um pequeno sistema de desenvolvimento (8080 se a memória serve) que rodava em 1024 bytes de código (ou seja, uma única UVEPROM) - ele tinha apenas quatro comandos para carregar (L ), salve (S), imprima (P) e outra coisa que não me lembro. Foi conduzido com um teletipo real e uma fita perfurada. Foi bem codificado!

Um exemplo de uso de NOOP estava em uma rotina de serviço de interrupção (ISR), espaçada em intervalos de 8 bytes. Essa rotina acabou tendo 9 bytes de comprimento, terminando com um salto (longo) para um endereço um pouco mais acima do espaço de endereço. Isso significava, dada a pequena ordem de bytes endian, que o byte de endereço alto era 00h e se encaixava no primeiro byte do próximo ISR, o que significava que ele (o próximo ISR) começava com NOOP, apenas para 'nós' caber o código no espaço limitado!

Portanto, o NOOP é útil. Além disso, suspeito que fosse mais fácil para a Intel codificá-lo dessa maneira - eles provavelmente tinham uma lista de instruções que queriam implementar e ela começou em '1', como todas as listas fazem (eram os dias de FORTRAN), então o zero O código NOOP se tornou um problema. (Eu nunca vi um artigo argumentando que um NOOP é uma parte essencial da teoria da Ciência da Computação (o mesmo Q: os matemáticos têm uma nul op, distinta do zero da teoria dos grupos?)

Philip Oakley
fonte
Nem todas as CPUs têm NOP codificado para 0x00 (embora o 8085, o 8080 e o CPU com os quais eu estou mais familiarizado, o Z80, todos tenham). No entanto, se eu estivesse projetando um processador, é onde eu o colocaria! Outra coisa útil é que a memória geralmente é inicializada para todos os 0x00; portanto, executar isso como código não fará nada até que a CPU alcance memória não zerada.
CJ Dennis
@CJDennis Eu expliquei por que as CPUs x86 não use 0x00para nopna minha resposta. Em resumo, economiza na decodificação de instruções - xchg ax, axflui naturalmente da maneira como a decodificação de instruções funciona e faz algo "desagradável"; então, por que não usar isso e chamá-lo nop, certo? :) Usado para salvar um pouco sobre o silício para a descodificação de instrução ...
Luaan
5

Em algumas arquiteturas, NOPé usado para ocupar slots de atraso não utilizados . Por exemplo, se a instrução de ramificação não limpar o pipeline, várias instruções após a execução serão executadas de qualquer maneira:

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

Mas e se você não tiver instruções úteis para ajustar após o JMP? Nesse caso, você terá que usar NOPs.

Os slots de atraso não se limitam a saltos. Em algumas arquiteturas, os riscos de dados no pipeline da CPU não são resolvidos automaticamente. Isso significa que, após cada instrução que modifica um registro, existe um slot em que o novo valor do registro ainda não está acessível. Se a próxima instrução precisar desse valor, o slot deve ser ocupado por NOP:

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

Além disso, algumas instruções de execução condicional ( If-True-False e similares) usam slots para cada condição e, quando uma condição específica não possui ações associadas, seu slot deve ser ocupado por 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
Dmitry Grigoryev
fonte
+1. Obviamente, eles só tendem a aparecer em arquiteturas que não se importam com a compatibilidade com versões anteriores - se o x86 tentasse algo assim ao introduzir o pipelining de instruções, quase todo mundo simplesmente chamaria isso errado (afinal, eles apenas atualizaram sua CPU e seus aplicativos pararam de funcionar!). Então x86 tem para garantir que o aplicativo não percebe quando as melhorias como este são adicionados à CPU - até que chegamos ao CPUs multi-core de qualquer maneira ...: D
Luaan
2

Outro exemplo de uso de um NOP de dois bytes : http://blogs.msdn.com/b/oldnewthing/archive/2011/09/21/10214405.aspx

A instrução MOV EDI, EDI é um NOP de dois bytes, que é apenas espaço suficiente para corrigir uma instrução de salto, para que a função possa ser atualizada em tempo real. A intenção é que a instrução MOV EDI, EDI seja substituída por uma instrução JMP $ -5 de dois bytes para redirecionar o controle para cinco bytes de espaço do patch que vem imediatamente antes do início da função. Cinco bytes são suficientes para uma instrução de salto completo, que pode enviar controle para a função de substituição instalada em outro lugar no espaço de endereço.

pjc50
fonte