• Nenhum resultado encontrado

Capítulo 8. Arquitetura de processadores

N/A
N/A
Protected

Academic year: 2021

Share "Capítulo 8. Arquitetura de processadores"

Copied!
75
0
0

Texto

(1)

Capítulo

8

Arquitetura de

processadores

Mostraremos neste capítulo alguns conceitos importantes sobre o funcionamento interno dos processadores. Tomaremos como exemplo os processadores Intel, e com eles você entenderá conceitos como execução especulativa, pipeline, previsão de desvio, paralelismo, micro-operações, linguagem assembly, memória virtual, paginação e outros termos complexos. O assunto é difícil, mas vale a pena, pois o leitor passará a ter um conhecimento mais profundo sobre o que se passa dentro de um processador.

Registradores internos do processador

Para entender como um processador executa programas, precisamos conhecer a sua arquitetura interna, do ponto de vista de software. Dentro de um processador existem vários circuitos chamados de registradores. Os registradores funcionam como posições de memória, porém o seu acesso é extremamente rápido, muito mais veloz que o da cache L1. O número de bits dos registradores depende do processador.

 Processadores de 8 bits usam registradores de 8 bits  Processadores de 16 bits usam registradores de 16 bits  Processadores de 32 bits usam registradores de 32 bits  Processadores de 64 bits usam registradores de 64 bits

A figura 1 mostra os registradores internos dos processadores 8086, 8088 e 80286, todos de 16 bits. Todos os processadores têm uma linguagem baseada

(2)

em códigos numéricos na memória. Cada código significa uma instrução. Por exemplo, podemos ter uma instrução para somar o valor de AX com o valor de BX e guardar o resultado em AX. As instruções do processador que encontramos na memória são o que chamamos de linguagem de máquina. Nenhum programador consegue criar programas complexos usando a linguagem de máquina, pois ela é formada por códigos numéricos. É verdade que alguns programadores conseguem fazer isso, mas não para programas muito longos, pois tornam-se difíceis de entender e de gerenciar. Ao invés disso, são utilizados códigos representados por siglas. As siglas são os nomes das instruções, e os operandos dessas instruções são os registradores, valores existentes na memória e valores constantes.

Figura 8.1

Registradores internos do processador 8086.

Por exemplo, a instrução que acabamos de citar, que soma o valor dos registradores AX e BX e guarda o resultado em AX, é representada por: ADD AX,BX

Esta instrução é representada na memória pelo seguinte código de máquina: 01 D8

Portanto a instrução ADD AX,BX é representada na memória por dois bytes, com valores 01 e D8 (em hexadecimal). Os bytes na memória que formam os programas são o que chamamos de linguagem de máquina. Esse códigos são lidos e executados pelo processador. Já as representações por siglas, como “ADD AX,BX”, formam o que chamamos de linguagem assembly. Quando programamos em linguagem assembly, estamos utilizando

(3)

as instruções nativas do processador. A linguagem assembly é usada para escrever programas que têm contato direto com o hardware, como o BIOS e drivers. O assembly também é chamado linguagem de baixo nível, pois interage intimamente com o hardware. Programas que não necessitam deste contato direto com o hardware não precisam ser escritos em assembly, e são em geral escritos em linguagens como C, Pascal, Delphi, Basic e diversas outras. Essas são chamadas linguagens de alto nível. Nas linguagens de alto nível, não nos preocupamos com os registradores do processador, nem com a sua arquitetura interna. Os programas pensam apenas em dados, matrizes, arquivos, telas, etc.

Apresentamos abaixo um pequeno trecho de um programa em linguagem assembly. Em cada linha deste programa temos na parte esquerda, os endereços, formados por duas partes (segmento e offset). A seguir temos as instruções em códigos de máquina, e finalmente as instruções em assembly.

Endereço Código Assembly

--- --- ---1B8D:0100 01D8 ADD AX,BX

1B8D:0102 C3 RET

1B8D:0103 16 PUSH SS 1B8D:0104 B03A MOV AL,3A 1B8D:0106 380685D5 CMP [D585],AL 1B8D:010A 750E JNZ 011A

1B8D:010C 804E0402 OR BYTE PTR [BP+04],02 1B8D:0110 BF86D5 MOV DI,D586

1B8D:0113 C6460000 MOV BYTE PTR [BP+00],00 1B8D:0117 E85F0B CALL 0C79

1B8D:011A 8B7E34 MOV DI,[BP+34] 1B8D:011D 007C1B ADD [SI+1B],BH

Quando estamos programando em linguagem assembly, escrevemos apenas os nomes das instruções. Depois de escrever o programa, usando um editor de textos comum, usamos um programa chamado compilador de linguagem assembly, ou simplesmente, Assembler. O que este programa faz é ler o arquivo com as instruções (arquivo fonte) e gerar um arquivo contendo apenas os códigos das instruções, em linguagem de máquina (arquivo objeto). O arquivo objeto passa ainda por um processo chamado link edição, e finalmente se transforma em um programa, que pode ser executado pelo processador. O Assembler também gera um arquivo de impressão, contendo os endereços, códigos e instruções em assembly, como no trecho de listagem que mostramos acima. O programador pode utilizar esta listagem para depurar o programa, ou seja, testar o seu funcionamento.

(4)

Os códigos hexadecimais que representam as instruções do processador são chamados de opcodes. As siglas que representam essas instruções são chamadas de mnemônicos.

Daremos neste capítulo, noções básicas da linguagem assembly dos processadores modernos. Não ensinaremos a linguagem a fundo, mas o suficiente para você entender como os processadores trabalham. Como a programação nos processadores modernos é relativamente complexa, começaremos com o 8080, de 8 bits. A arquitetura do 8080 deu origem à do 8086, que por sua vez deu origem ao 386 e aos processadores modernos. Entendendo o 8080, que é bem mais simples, será mais fácil entender os processadores modernos.

Linguagem Assembly 8080

Aprender assembly do 8080 não é uma inutilidade, por duas razões. Primeiro porque você entenderá com muito mais facilidade o assembly dos processadores modernos, que afinal foram inspirados no 8080. Segundo que nem só de PCs vive um especialista em hardware. Você poderá trabalhar com placas controladoras que são baseadas nos processadores 8051 e Z80. Ambos são de 8 bits e também derivados do 8080, e são bastante utilizados em projetos modernos.

A figura 2 mostra os registradores internos do 8080. São registradores de 8 bits, com exceção do PC (Program Counter) e do SP (Stack Pointer), que têm 16 bits.

Figura 8.2

Registradores internos do 8080.

O registrador mais importante é o acumulador. Ele é o valor de saída da unidade lógica e aritmética (ALU), na qual são realizadas todas as operações. Processadores atuais permitem fazer operações com todos os registradores, mas no 8080, o acumulador deve obrigatoriamente ter um dos operandos, e sempre é onde ficam os resultados.

Os registradores B, C, D, E, H e L são de uso geral. Servem como operandos nas operações lógicas e aritméticas envolvendo o acumulador. O PC é um registrador de 16 bits, e seus valores são usados para formar o barramento de

(5)

endereços do processador durante as buscas de instruções. O PC tem sempre o endereço da próxima instrução a ser executada.

O SP (Stack Pointer) é muito importante. Ele serve para endereçar uma área de memória chamada stack (pilha). A pilha serve para que os programas possam usar o que chamamos de subrotinas, que são trechos de programa que podem ser usados em vários pontos diferentes. Por exemplo, se em um programa é preciso enviar caracteres para o vídeo, não é preciso usar em vários pontos deste programa, as diversas instruções que fazem este trabalho. Basta fazer uma subrotina com essas funções e “chamá-la” onde for necessária. A subrotina deve terminar com uma instrução RET, que faz o programa retornar ao ponto no qual a subrotina foi chamada. Para chamar uma subrotina, basta usar a instrução CALL. Quando esta instrução é executada, é automaticamente armazenado na pilha, o endereço da instrução imediatamente posterior à instrução CALL (endereço de retorno). Subrotinas podem chamar outras subrotinas, permitindo assim criar programas mais complexos. O Stack Pointer sempre aponta para o topo da pilha, e é automaticamente corrigido à medida em que são usadas instruções CALL e RET. A instrução RET consiste em obter o endereço de retorno existente no topo da pilha e copiá-lo para o PC (Program Counter). Isso fará com que o programa continue a partir da instrução posterior à instrução CALL.

Os FLAGS são um conjunto de 8 bits que representam resultados de operações aritméticas e lógicas. São os seguintes esses bits:

Símbolo Nome Descrição

Z Zero Indica se o resultado da operação foi zero

CY Carry Indica se uma operação aritmética teve “vai um” ou “pede emprestado” P Parity Indica a paridade do resultado da operação.

S Signal Indica o sinal de uma operação, se foi positivo ou negativo AC Aux. Carry Carry auxiliar, em algumas instruções especiais.

Apesar de ser um processador de 8 bits, o 8080 é capaz de realizar algumas operações de 16 bits. Nessas operações, os registradores B e C são tratados como um valor de 16 bits. O mesmo ocorre com o par D/E e H/L.

Além de manipular os registadores, o 8080 também permite obter valores na memória. Esses valores podem ser de 8 ou 16 bits, e nas instruções que fazem esses acessos, basta indicar o endereço de 16 bits da posição de memória que desejamos acessar. Além disso é possivel usar os registradores HL, BC e DE como apontadores para posições de memória. Nas instruções do assembly do 8080, o pseudo registrador M é na verdade a posição de memória (8 bits) cujo endereço está em HL.

(6)

Programar em assembly do 8080 consiste em utilizar suas instruções, manipulando seus registradores para executar as funções que desejamos.

Instruções de movimentação de dados

MOV: Move dados entre dois registradores diferentes. Assim como na maioria das instruções que envolvem registradores, podemos usar M como sendo a posição de memória apontada por HL. Exemplos:

MOV A,C ; A=C MOV C,E ; C=E

MOV D,M ; D=M, ou seja, a posição de memória indicada ; por HL

MOV M,A ; M=A

Note que quando escrevemos programas em assembly, podemos usar comentários em cada linha, bastando usar um ponto-e-vírgula após a instrução. Tudo o que estiver depois do ponto-e-vírgula será ignorado pelo assembler. Aqui aproveitamos este convenção para colocar também comentários explicativos nas instruções de nossos exemplos.

MVI: Carrega um valor constante de 8 bits em um registrador de 8 bits ou na posição de memória apontada por HL. Exemplos:

MVI C,200 ; Carrega o registrador C com 200 (decimal) MVI A,15h ; Carrega o acumulador com 15 hexadecimal MVI M,150 ; Armazena o valor 150 em [HL] MVI L,32 ; Carrega o registrador L com 32 em decimal

Aproveitamos para além de exemplificar essas instruções, apresentar mais algumas convenções usadas na linguagem assembly. Os números podem ser representados nos formatos binário, octal, hexadecimal ou decimal. Quando não usamos sufixos após os números, considera-se que são números decimais. Para números hexadecimais, usamos o sufixo H. Quando um número hexadecimal começa com A, B, C, E, E ou F, temos que usar um “0” no início, para que o assembler não pense que se trata de uma variável, e não um número. Números binários devem terminar com “b”, e números octais devem terminar com “q”. Exemplos:

190 10010111b 325q 8BC3h

(7)

Os quatro números acima estão expressos respectivamente em decimal, binário, octal e hexadecimal.

Outra convenção que vamos introduzir aqui é usar o símbolo [HL] para indicar a posição de memória cujo endereço é dado por HL. Na linguagem assembly do 8080, este é o papel do símbolo M. Não usamos [HL], porém esta convenção foi adotada no assembly do 8086 e outros processadores mais novos. Da mesma forma vamos usar os símbolos [BC] e [DE] para indicar as posições de memória apontadas por BC e por DE.

LXI: Carrega um valor constante de 16 bits em um dos pares BC, DE, HL e no Stack Pointer. Exemplos:

LXI H,35AFh ; Carega HL com o valor 35AF hexadecimal LXI D,25100 ; Carrega DE com o valor 25100 decimal LXI B,0 ; Carrega BC com 0

LXI SP,200 ; Carrega o Stack Pointer com 200 decimal

Note que os números de 8 bits podem assumir valores inteiros positivos de 0 a 255 decimal (ou de 0 a FF em hexadecimal). Os números inteiros positivos de 16 bits podem assumir valores entre 0 e 65.535 decimal (ou 0 a FFFF hex).

Obseve a instrução LXI H, 35AFh. Este valor 35AF é formado por 16 bits, sendo que os 8 bits mais significativos têm o valor 35 hex, e os 8 bits menos significativos têm o valor AF hex. No par HL, o registrador H é o mais significativo, e o registrador L é o menos significativo. Sendo assim o registrador H ficará com 35 hex e o registrador L ficará com AF hex.

LDA e STA: A instrução LDA carrega o acumulador (registrador A) com o valor que está no endereço de memória especificado. A instrução STA faz o inverso, ou seja, guarda o valor de A na posição de memória especificada. Exemplos:

LDA 1000h ; Carrega A com o valor existente em [1000h] STA 2000h ; Guarda o valor de A em [2000h]

Estamos utilizando a partir de agora a notação [nnnn] para indicar a posição de memória cujo endereço é nnnn. Esta notação não é usada no assembly para 8080, mas é usada no assembly do 8086 e superiores.

LHLD e SHLD: A instrução LHLD carrega nos registradores H e L, o valor de 16 bits existente nas duas células de memória cujo endereço é especificado. A instrução SHLD faz o inverso. Exemplos:

(8)

LHLD 1000h ; Faz L=[1000h] e H=[1001h] SHLD 2000h ; Guarda L em [2000h] e H em [2001h]

Aqui vai mais um conceito importante. A memória do 8080 é uma sucessão de bytes, mas podemos também acessar words, ou seja, grupos de 16 bits. A operação envolve dos bytes consecutivos, e nas instruções indicamos apenas o endereço do primeiro byte. Os 8 bits menos significativos estão relacionados com a posição de memória indicada, e os 8 bits seguintes estão relacionados com a próxima posição. A figura 3 ilustra o que ocorre ao usarmos a instrução SHLD 2000h, levando em conta que H está com o valor 35h e L com o valor 8Ch.

Figura 8.3

Armazenando HL em [2000h].

Esta convenção é utilizada por todos os processadores Intel. Sempre que é feita uma leitura ou escrita na memória, as partes menos significativas dizem respeito aos endereço menores, e as partes mais significativas correspondem aos endereços maiores.

LDAX e STAX: Essas instruções fazem respectivamente operações de load (carrega) e store (guarda) do acumulador, usando a posição de memória cujo endereço está no par BC ou DE. Exemplos:

LDAX D ; A = [DE] STAX B ; [BC] = A LDAX B ; A = [BC] STAX D ; [DE] = A

Note que estamos usando as notações [BC] e [DE] para indicar as posições de memória cujos endereços são dados por BC e DE. Observe que as instruções LDAX H e STAX H não existem, mas em seu lugar temos “MOV A,M” e “MOV M,A” que fazem a mesma coisa.

(9)

XCHG: Troca o valor de HL com o valor de DE. Esta instrução só é usada na forma:

XCHG ; DE  HL

Trecho de programa com movimentação de dados

Mostraremos agora uma seqüência de instruções de movimentação de dados apresentadas aqui. Usaremos depois de cada instrução, um ponto-e-vírgula, seguido de comentários. Esta é uma prática comum nos programas em assembly e em outras linguagens. O comentário não produz instruções para o processador, apenas serve para o programador explicar melhor o seu programa.

INIC: MVI A,100 ; Carrega A com o valor 100 decimal MOV C,A ; Copia o valor de A para o registrador C

LXI H, 300h ; Carrega HL com o valor 300h. H ficará com 03 e L ficará com 00 MVI M,40 ; Armazena no endereço 300h (apontado por HL), o valor 40 LXI D, 1000h ; Carrega DE com o valor 1000h

MOV A,M ; Move para A o valor armazanedo no endreço 300h STAX D ; Guarda o valor de A na posição 1000h da memória SHLD 2000h ; Guarda o valor de HL nas posições 2000 e 2001 XCHG ; Troca os valores de DE e HL

Na listagem acima, “INIC:” é o que chamamos de LABEL. Ele será entendido pelo assembler como um endereço que deverá ser utilizado posteriormente em alguma instrução.

Instruções aritméticas

ADD: Soma com A, o valor do registrador especificado, ou da posição de memória apontada por HL (M). O resultado da operação é armazenado em A. Exemplos:

ADD B ; A = A+B ADD C ; A = A=C ADD L ; A = A+L ADD M ; A = A+[HL]

Assim como ocorre com todas as instruções aritméticas e lógicas, os flags (Z, CY, P, S e AC) são atualizados de acordo com o resultado da operação. Por exemplo, se somarmos C8h com 72h, o resultado será 13Ah. Este valor não cabe em 8 bits, portanto o resultado será 3Ah e o bit Carry será ligado para indicar que ocorreu um “vai 1”.

(10)

ADI. Soma com A, o valor constante especificado. O resultado fica armazenado em A. Exemplos:

ADI 90 ; A = A+90 ADI 35 ; A = A+35

ADC: Semelhante à instrução ADD, exceto que o bit Carry também é adicionado. Esta operação serve para fazer somas com “vai 1”. Desta foram podemos dividir números grande em valores de 8 bits, e somar 8 bits de cada vez. Sempre que fazemos uma soma, o Carry ficará com o “vai 1” da operação, e assim poderá ser usado para somar a parcela seguinte. Exemplos:

ADC L ; A = A+L+carry ADC D ; A = A+D+carry ADC M ; A = A+[HL]+carry

ACI: Semelhante à instrução ADI, exceto pelo Carry também entrar na soma. Exemplos:

ACI 90 ; A = A+90+carry ACI 84 ; A = A+84+carry

SUB: Faz uma subtração de A com o registrador (A = A-registrador) ou com M. Exemplos:

SUB D ; A = A-D SUB C ; A = A-C SUB M ; A = A-[HL]

Nesta operação, o carry é ligado para indicar o resultado negativo, o que serve para fazer cálculos com vários dígitos, usando o método de “pedir emprestado”.

SUI: Subtrai do acumulador, o número especificado. Por exemplo:

SUI 20 ; A = A-20 SUI 82 ; A = A-82 SUI 0DFh ; A = A-DF (hex)

SBB: Similar à instrução SUB, exceto que leva em conta o valor do carry. Serve para fazer cálculos com o método de “pedir emprestado”. Exemplos:

SBB C ; A = A-C-carry SBB L ; A = A-L-carry SBB M ; A = A-[HL]-carry

(11)

SBI: Similar à instrução SUI, exceto que leva em conta o valor do carry. Serve para fazer cálculos com o método de “pedir emprestado”. Exemplos:

SBI 2Fh ; A = A-2Fh-carry SBI 73h ; A = A-73h-carry

INR: Incrementa em uma unidade o valor do registrador especificado. Serve para implementar contadores. Exemplos:

INR A ; A = A+1 INR C ; C = C+1 INR D ; D = D+1 INR L ; L = L+1 INR M ; [HL] = [HL]+1

Quando o valor do registrador é FF e usamos esta instrução, ele passará a ficar com o valor 00 e o bit Carry será ligado. O bit Zero também será ligado, indicando que o resultado da operação foi zero.

DCR: Diminui de uma unidade o conteúdo do registrado especificado. Esta instrução é usada para implementar contagens decrescentes. Exemplos:

DCR A ; A = A-1 DCR C ; C = C-1 DCR D ; D = D-1 DCR H ; H = H-1 DCR M ; [HL] = [HL]-1

Quando o registrador tem o valor 1 e usamos esta instrução, o seu conteúdo passará a ser 00. O bit Zero será ligado, indicando que o resultado da operação foi 0. Se o registrador estiver com o valor 00 e usarmos novamente esta instrução, seu valor passará a ser FF. O bit Carry será ligado, para indicar que o resultado tem valor negativo.

INX e DCX: Essas instruções são similares às instruções INR e DCR, exceto que operam com pares de registradores (BC, DE e HL) e com o Stack Pointer. Não podem ser usadas diretamente para implementar contadores, pois elas não afetam os valores dos flags, ou seja, não “avisam” se o resultado foi zero, positivo ou negativo. Essas instruções não tinham objetivo de fazer contagem, mas sim de usar os registradores como ponteiros para a memória. Ao lançar o 8086, a Intel corrigiu este “deslize”. As instruções correspondentes nos processadores de 16 bits afetam os valores dos flags, o que é importante para tomar decisões posteriores em função do resultado da contagem. Exemplos:

(12)

INX H ; HL = HL+1 INX D ; DE = DE+1 DCX B ; BC = BC-1 INX SP ; SP = SP+1

Note que apesar do 8080 ser um processador de 8 bits, INX e DCX são consideradas instruções de 16 bits.

DAA: Esta instrução é usada na manipulação de números codificados no formato BCD (Bynary Coded Decimal). Nesta representação, um valor de 8 bits é dividido em 2 grupos de 4 bits. Um grupo de 4 bits representa o dígito das unidades e o outro grupo representa o dígito das dezenas, no formato decimal. Números neste formato podem ser somados e subtraídos pelas mesmas instruções que manipulam números binários. A única diferença é que no final da operação é preciso usar a instrução DAA para fazer o ajuste decimal. Por exemplo, se quisermos somar os números 48 e 36 (BCD), usamos as instruções comuns (ADD, ADI, etc.) e encontraremos o resultado 7E (hex). Entretanto o resultado esperado, considerando o formado BCD, seria 84 (pensando em decimal). Logo após fazer a soma, se usarmos a instrução DAA, aquele valor 7E resultará em 84.

DAD: Esta é uma operação de soma em 16 bits. Soma o valor de 16 bits presente em HL com o par de registradores especificado. Este “par” pode ser BC, DE, HL ou SP. O resultado é colocado em HL, e o bit Carry é afetado, refletindo um eventual “vai 1”. Exemplos:

DAD B ; HL = HL+BC DAD D ; HL = HL+DE DAD H ; HL = HL+HL DAD SP ;HL = HL+SP

Para exemplificar as instruções apresentadas até aqui, vamos mostrar um exemplo no qual movemos 30 bytes localizados a partir do endereço 1000h para o endereço 2000h.

LXI H, 1000h ; HL vai apontar para a origem LXI D, 2000h ; DE vai apontar para o destino MVI C, 30 ; C será usado como contador: TRAN: MOV A,M ; Pega o byte da origem

STAD X ; Guarda no destino INX H ; Aponta para o próximo byte INX D ; Aponte para o próximo byte DCR C ; Decrementa o contador

(13)

Além das instruções já conhecidas, estamos usando a instrução JNZ (Jump if not Zero). Este instrução é um exemplo de desvio condicional. O programa continuará a partir do endereço TRAN caso o bit ZERO não esteja ativado, ou seja, se o resultado da operação anterior (C=C-1) não foi zero. Quando a contagem chegar a zero, a instrução JNZ não provocará o desvio, e o programa continuará com a instrução seguinte.

Instruções lógicas

As instruções lógicas são necessárias para que os programas possam tomar decisões em função dos dados. São instruções que realizam operações AND, OR, XOR (ou exclusivo) e NOT (negação). Existem ainda instruções de comparação, instruções para manipular o bit carry e instruções para rotação de bits.

Para entender o funcionamento dessas instruções, temos que lembrar as tabelas verdade dos operadores NOT, AND, OR e XOR:

X NOT X X Y X AND Y X Y X OR Y

0 1 0 0 0 0 0 0

1 0 0 1 0 0 1 1

1 0 0 1 0 1

1 1 1 1 1 1

Como vemos na tabela acima, o operador NOT faz a inversão do bit sobre o qual atua. O operador AND dará resultado 1 apenas quando ambos os bits forem 1, e dará 0 em caso contrário. O operador OR dará resultado 0 somente quando ambos os bits forem 0. O operador XOR dará resultado 1 se os dois bits forem diferentes, e 0 se ambos os bits forem iguais.

X Y X XOR Y

0 0 0

0 1 1

1 0 1

1 1 0

Essas operações são envolvem apenas um bit, mas nas instruções lógicas dos processadores, atuam individualmente sobre cada um dos bits. Por exemplo, se calcularmos 10111110 AND 10010101, teremos o seguinte resultado: 10111110

10010101 AND ---10010100

(14)

Note que o primeiro bit do resultado é obtido fazendo a operação AND com os primeiros bits das duas parcelas, e assim por diante.

ANA e ANI: Realiza uma operação AND, bit a bit, do acumulador com o registrador especificado. O resultado da operação fica no acumulador. A instrução ANI faz o AND do acumulador com um valor constante.

ANA B ; A = A AND B ANA C ; A = A AND C ANA A ; A = A AND A ANA M ; A = A AND [HL] ANI 3Fh ; A = A AND 3F

Uma das várias aplicações desta instrução é testar se determinados bits são zero ou 1. Por exemplo, se fizermos ANI 00000100b, podemos usar a seguir uma instrução JZ ou JNZ que causarão desvio ou não dependendo do fato do bit 2 estar ligado ou desligado.

Chegou a hora de apresentar mais um conceito: a identificação dos bits. Em um grupo de 8 bits, chamamos cada um desses bits, da direita para a esquerda, de bit 0, bit 1, até bit 7, ou seja:

bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0

ORA, ORI, XRA, XRI: ORA faz a operação OR do acumulador com o registrador especificado; ORI faz o mesmo com um valor constante; XRA faz a operação XOR (OU Exclusivo) do acumulador com o registrador especificado, XRI faz o mesmo com um valor constante. Exemplos:

XRA B ; A = A XOR B XRA C ; A = A XOR C ORA L ; A = A XOR L ORI 20h ; A = A OR 20h XRI 04h ; A = A XOR 04h XRA A ; A = A XOR A

Aproveitamos para mostrar alguns macetes de programação assembly. A instrução ORI serve para ligar um bit selecionado. Para ligar os bits 7, 6, 5, 4, 3, 2, 1 e 0 basta fazer um ORI com valores 80h, 40h, 20h, 10h, 8, 4, 2 e 1, respectivamente. A instrução XRI fará a inversão do bit correspondente (use os mesmos valores que indicamos para a instrução ORI). A instrução XRA A tem o efeito de zerar o acumulador.

CMP, CPI: A instrução CMP compara o acumulador com outros registradores. A instrução CPI compara o acumulador com um valor

(15)

constante de 8 bits. O resultado do acumulador não é afetado. As instruções apenas afetam os bits Zero e Carry. Após essas instruções podem ser usados desvios condicionais que testam esses dois bits. Esses bits ficarão ligados ou desligados de acordo com os valores comparados:

A maior que Valor Z=0 Cy=0 A igual a Valor Z=1 Cy=0 A menor que Valor Z=0 Cy=1 Exemplos:

CMP C ; Compara A com C CMP L ; Compara A com L CMP M ; Compara A com [HL] CPI 4Ch ; Compara A com 4C

RLC, RRC: Essas duas instruções são usadas para deslocar os bits do acumulador. RLC desloca para esquerda e RRC desloca para a direita. A operação é mostrada na figura 4.

Figura 8.4

Instruções RLC e RRC.

Na instrução RLC, cada bit assume o lugar do bit imediatamente à sua esquerda (ou seja, o bit imediatamente mais significativo). O bit 7 é transferido para o bit 0, e uma cópia do bit 7 é feita no Carry. Na instrução RRC, o deslocamento é feito de forma inversa. O bit 0 é copiado para o bit 7 e para o Carry. Essas instruções têm diversas aplicações, entre as quais, a implementação de operações de multiplicação e divisão, já que o 8080 não as possui no seu conjunto de instruções.

Essas instruções não têm parâmetros. São usadas simplesmente nas formas RRC e RLC.

RAL, RAR: Também fazem deslocamentos dos bits do acumulador, para a esquerda e para a direita. A diferença é que neste caso, a rotação é feita com

(16)

9 bits, sendo 8 do acumulador e mais o Carry. A operação dessas instruções é mostrada na figura 5.

Figura 8.5

Instruções RAL e RAR.

CMA: Complementa o acumulador, ou seja, faz a inversão de todos os seus bits.

STC, CMC: Essas instruções servem para carregar valores no Carry. A instrução STC faz Carry=1, e a instrução CMC inverte o valor do Carry. Note que não existe uma instrução para zerar o Carry. Ao invés dela, podemos usar STC seguida de CMC, ou então usar a instrução ANA A ou ORA A, que não alteram o valor de A mas zeram o Carry.

Instruções de desvio

As instruções de desvio são importantíssimas, e são executadas o tempo todo. O processador tende a seguir uma seqüência de instruções, na mesma ordem na qual são encontradas na memória, ou seja, depois de cada instrução é executada a instrução seguinte. Um programa que só executa instruções na sequência não tem muita utilidade. Todos os processadores precisam de insruções de desvio, que fazem com que a execução seja continuada a partir de um outro ponto qualquer do programa. Já mostramos um exemplo de trecho de programa que usa a instrução JNZ (jump if not zero) para implementar a repetição de um trecho um certo número de vezes. Um trecho de programa que é executado diversas vezes é chamado de LOOP.

JMP: Esta é a principal e mais simples instrução de desvio. É o que chamamos de desvio incondicional, ou seja, sempre será executada, não importa em que condições. Por exemplo, ao encontrar a instrução JMP 8000h, o processador continuará a execução do programa a partir das instruções localizadas no endereço 8000h.

(17)

CALL e RET: A instrução CALL também é um desvio, mas bem diferente do JMP. É uma chamada de rotina. Uma rotina é um trecho de programa que pode ser chamado de vários pontos de um programa principal. No final de uma rotina deve exitir uma instrução RET, que faz o processador retornar ao ponto imediatamente após a instrução CALL.

Comparando as instruções CALL e JMP, a única diferença é que no caso da instrução CALL, o endereço da próxima instrução (endereço de retorno) é guardado no topo da pilha. O valor do SP (stack pointer é atualizado para permitir novos empilhamentos). A instrução RET simplesmente obtem o endereço no topo da pilha e o coloca em PC (Program Counter), fazendo com que a execução continue de onde parou.

Mostraremos novamente nosso pequeno trecho de programa que move um certo número de bytes de uma parte para outra da memória, mas desta vez usando uma rotina. O trecho começa no endereço 1000, onde carregamos o par HL com o endereço da origem, DE com o endereço do destino e o registrador C com o número de bytes (no caso são 16 bytes, que corresponde a 10 em hexadecimal). A seguir é chamada uma rotina que está no endereço 1020. Esta é a rotina responsável pela movimentação. Depois que a rotina é chamada, a próxima instrução a ser executada é JMP 0, que está no endereço 100A.

1000: 21 00 81 LXI H, 8100 1003: 11 00 82 LXI D, 8200 1006: 0E 10 MVI C,10 1008: CD 20 10 CALL 1020 100A: C3 00 00 JMP 0 ... 1020: 7E MOV A,M 1021: 12 STAX D 1022: 23 INX H 1023: 13 INX D 1024: 0D DCR C 1025: C2 20 10 JNZ 1020 1028: C9 RET

Observe que a rotina de movimentação localizada no endereço 1020 é genérica. Ela pode mover dados entre duas posições quaisquer de memória, dadas por HL e DE. O número de bytes também pode ser qualquer (de 1 a 255), e deve ser dado em C. Dizemos então que HL, DE e C são os parâmetros desta rotina.

É importante entender o que acontece com a stack ao executarmos instruções CALL e RET. Na instrução CALL, o endereço de retorno e empilhado. Na instrução RET, o endereço de retorno é desempilhado. No

(18)

trecho de programa mostrado acima, a instrução CALL causará o empilhamento do endereço 100A, que é o endereço da instrução seguinte, e será o endereço de retorno.

Figura 8.6

Empilhamento de um endereço de retorno na stack, feito por uma instrução CALL.

A figura 6 ilustra o que está ocorrendo. Digamos que o registrador SP (Stack Pointer) esteja com o valor inicial 0100. A stack aumenta para trás, ou seja, para endereços menores. Ao executar a instrução CALL, o processador empilhará o endrereço 100A nos bytes imediatamente anteriores ao endereço indicado por SP. Portanto ocupará os endereços 00FF e 00FE. O SP será atualizado para 00FE, que será o novo topo da pilha. Assim novos endereços poderão ser empilhados quando forem executadas outras instruções CALL.

A instrução RET fará exatamente o inverso do mostrado na figura 6. O Stack Pointer estará com o valor 00FE, portanto irá obter o endereço de retorno nas posições 00FE e 00FF da memória, e encontrará 100A. O Stack Poitner será então atualizado para 0100, que será o novo topo da pilha.

JMPs, CALLs e RETs condicionais – Além das instruções JMP, CALL e RET, que são incondicionais, existem suas versões condicionais, que são executadas apenas quando uma determinada condição é satisfeita. Essas condições são baseadas nos flags: Zero, Carry, Parity e Signal. São elas:

Instrução Ação Interpretação

JZ Pula se Zero está ligado Pula se o resultado é zero, Pula se iguais

JNZ Pula se Zero está desligado Pula se o resultado não é zero, Pula se diferentes

JC Pula se Carry está ligado Pula se menor, pula se carry JNC Pula se Carry está desligado Pula se maior ou igual, pula se não carry JPE Pula se paridade Par Pula se número de bits 1 é par JPO Pula se paridade Ímpar Pula se número de bits 1 é ímpar JP Pula se sinal positivo Pula se resultado positivo ou zero JM Pula se sinal negativo Pula se resultado negativo

(19)

Nesta tabela mostramos a ação de cada uma desas instruções, e ainda uma interpretação dessas ações. Por exemplo, a instrução JZ pode ser usada logo depois uma operação aritmética e queremos que seja feito o desvio se o resultado foi zero. Pode ainda ser usada depois de uma comparação e queremos que o desvio seja feito se os valores comparados forem iguais. Da mesma forma existem as chamadas condicionais de rotinas e os retornos condicionais:

CZ, CNZ, CC, CNC, CPE, CPO, CP, CM RZ, RNZ, RC, RNC, RPE, RPO, RP, RM

RST n: Esta instrução é similar a uma instrução CALL. A diferença é que ela não precisa que seja indicado o endereço, pois está implícito. Podemos usá-la de 8 formas diferentes:

RST 0 / RST 1 / RST 2 / RST 3 / RST 4 / RST 5 / RST 6 / RST 7 Essas instruções têm o mesmo efeito que:

CALL 0000 / CALL 0008 / CALL 0010 / CALL 0018 / … / CALL 0038 O objetivo dessas instruções é economizar bytes, já que ao invés dos 3 bytes ocupados por uma instrução CALL, usa apenas um byte. Quando uma certa rotina é usada muitas vezes em um programa, podemos colocá-la a partir de um desses endereços e chamá-las através das instruções RST. Note que esses endereços estão separados apenas por 8 bytes, portanto não é possível colocar diretamente neles, rotinas maiores que este tamanho. O que normalmente fazemos é usar nesses endereços, instruções de JMP para outra área onde ficam as rotinas.

PCHL: Carrega em PC o valor existente em HL. Isto é equivalente a executar um JMP para o endereço especificado por HL. É útil quando queremos desviar para um local variável, em função do valor em HL, obtido por exemplo, de uma tabela de endereços.

Operações com a pilha, E/S e controle

Para completar o conjunto de instruções do 8080, falta apenas uma pequena miscelânea de instruções para para manipulação da stack, entrada/saída e controle.

(20)

PUSH e POP: Já vimos como a stack é usada para emplilhar e desempilhar endereços nas instruções CALL e RET. São sempre valores de 16 bits. Além de endereços, podemos ainda empilar e desempilhar dados na stack. Por exemplo, a instrução PUSH H guardará o valor de HL no topo da stack. A instrução POP H fará o inverso, ou seja, retirará da pilha o valor do seu topo e o copiará para HL. As instruções PUSH e POP podem ser usadas com os parâmetros B, D e H, que operam com os pares BC, DE e HL. Podemos ainda usar PUSH PSW, que salva em um grupo de 16 bits, o valor do acumulador e dos flags. A instrução POP PSW faz o inverso.

XTHL: Lembra da instrução XCHG, que troca os valores de HL e DE? A instrução XTHL faz algo parecido. Troca os valores de HL e do topo da pilha.

SPHL: Já vimos também a instrução “LXI SP, Valor”, que carrega um valor fixo no stack pointer. Isto é necessário na inicialização dos programas, quando temos que definir onde ficará a pilha. A instrução SPHL é mais flexível. Ela cria a stack em qualquer ponto da memória, bastando indicar seu endereço em HL.

IN, OUT: São instruções importantíssimas que servem para o processador trocar dados com o mundo exterior. Através da instrução IN podemos obter dados provenientes de interfaces que estão ligadas aos periféricos. O dado lido ficará no acumulador. A instrução OUT faz o inverso, ou seja, transfere para o endereço de E/S especificado, o valor que está no acumulador. Exemplos:

IN 70h ; Lê dado que está no endereço de E/S 70h OUT 40h ; Envia para o endereço de E/S 40h, o valor de A

DI, EI: Essas instruções controlam as interrupções de hardware. DI faz com que as interrupções sejam desabilitadas. EI faz com que sejam habilitadas novamente. São úteis durante o atendimento a interrupções de hardware e em certos trechos críticos que não podem ser interrompidos.

HLT: Halt. Faz o processador parar de executar instruções. O processador só sai deste estado quando ocorre uma interrupção.

NOP: No Oparation. Esta instrução não faz nada. É usada quando queremos fazer uma pausa entre duas instruções seguidas. Normalmente isso é necessário quando temos um programa que faz controle direto do hardware.

(21)

Isto pode ser necessário, por exemplo, para fazer o processador esperar um pouco mais pela execução das funções de certos circuitos lentos.

Um pequeno programa para 8080

Finalizamos a apresentação das instruções do 8080, mostrando um pequeno programa. Este programa faz o recebimento dos caracteres do teclado e os coloca na memória a partir do endereço 1000h. O número máximo de caracteres que poderá ser recebido é 80. Quando terminarmos de digitar a linha, devemos teclar ENTER, cujo código hexadecimal é 0Dh. Estamos supondo aqui que o computador tem um console (teclado/vídeo combinados) ligado em uma interface serial que ocupa os endereços de E/S 80h e 81h. O endereço 80h é a porta de dados, que envia caracteres do para o vídeo (escrita) e lê caracteres do teclado (leitura). A porta 81h é usada como status. Seus bits 0 e 1 indicam respectivamente se a interface tem um dado vindo do teclado e se está pronta para enviar um dado para o vídeo.

LXI H, 1000h ; Aponta para a área de memória MVI C,0 ; Zera o contador de bytes LECH: CALL INCHAR ; Lê caractere do teclado

CPI 0Dh ; Testa se foi ENTER JZ FIM ; Vai para o fim se teclou ENTER MOV B,A ; Se não foi enter, guarda caracter em B MOV A,C ; Pega o contador de caracteres CPI 80 ; Testa se chegou a 80

JZ LECH ; Se há chegarm 80, ignora e volta a ler MOV A,B ; Se não chegou a 80, pega o caracter MOV M,A ; Guarda caracter na memória CALL OUTCHAR ; Envia o caracter para o vídeo INR C ; Incrementa o contador de caracteres INX H ; Incrementa o ponteiro

JMP LECH ; Vai ler o próximo caracter

FIM: JMP 0 ; Pula para 0000 quando terminar o programa ; Rotina de leitura de caracter

INCHAR: IN 81h ; Lê o status da porta serial ANI 01 ; Testa se o bit 0 está ligado JZ INCHAR ; Se está desligado continua tentando IN 80h ; Lê o código do caracter

RET ; e retorna com o caracter em A ; Rotina que envia para o vídeo, caracter ; que está em A

OUTCHAR: PUSH B ; Salva para BC na pilha MOV B,A ; Guarda em B o caracter OUTC1: IN 81h ; Lê o status da porta serial

ANI 02 ; Testa o bit 1

JZ OUTC1 ; Se bit 1 está zerado, continua esperando MOV A,B ; Pega o caracter

OUT 80h ; Envia o caracter

POP B ; Restaura o valor original de BC RET ; e retorna

(22)

Códigos das instruções do 8080

Apresentamos a seguir uma tabela com os códigos de todas as instruções do 8080. Não que você vá programar 8080, mas para que você tenha uma idéia da relação entre as instruções e os seus códigos. Na tabela que se segue, temos as seguintes convenções:

 D8 representa um dado constante de 8 bits  D16 representa um dado constante de 16 bits  Addr representa um endereço de 16 bits

Op Cod

e Mnemonic Op

Code Mnemonic OpCod

e Mnemonic Op Cod

e Mnemonic Op

Code Mnemonic OpCode Mnemonic 00 NOP 2B DCX H 56 MOV D,M 81 ADD C AC XRA H D7 RST 2 01 LXI B,D16 2C INR L 57 MOV D,A 82 ADD D AD XRA L D8 RC 02 STAX B 2D DCR L 58 MOV E,B 83 ADD E AE XRA M D9 -03 INX B 2E MVI L,D8 59 MOV E,C 84 ADD H AF XRA A DA JC ADDR 04 INR B 2F CMA 5A MOV E,D 85 ADD L B0 ORA B DB IN D8 05 DCR B 30 - 5B MOV E,E 86 ADD M B1 ORA C DC CC ADDR 06 MVI B,D8 31 LXI SP,d16 5C MOV E,H 87 ADD A B2 ORA D DD -07 RLC 32 STA ADDR 5D MOV E,L 88 ADC B B3 ORA E DE SBI D8 08 - 33 INX SP 5E MOV E,M 89 ADC C B4 ORA H DF RST 3 09 DAD B 34 INR M 5F MOV E,A 8A ADC D B5 ORA L E0 POR 0A LDAX B 35 DCR M 60 MOV H,B 8B ADC E B6 ORA M E1 POP H 0B DCX B 36 MVI M,D8 61 MOV H,C 8C ADC H B7 ORA A E2 JPO ADDR 0C INR C 37 STC 62 MOV H,D 8D ADC L B8 CMP B E3 XTHL 0D DCR C 38 - 63 MOV H,E 8E ADC M B9 CMP C E4 CPO ADDR 0E MVI C,D8 39 DAD SP 64 MOV H,H 8F ADC A BA CMP D E5 PUSH H 0F RRC 3A LDA ADDR 65 MOV H,L 90 SUB B BB CMP E E6 ANI D8 10 - 3B DCX SP 66 MOV H,M 91 SUB C BC CMP H E7 RST 4 11 LXI D,D16 3C INR A 67 MOV H,A 92 SUB D BD CMP L E8 RPE 12 STAX D 3D DCR A 68 MOV L,B 93 SUB E BE CMP M E9 PCHL 13 INX D 3E MVI A,D8 69 MOV L,C 94 SUB H BF CMP A EA JPE ADDR 14 INR D 3F CMC 6A MOV L,D 95 SUB L C0 RNZ EB XCHG 15 DCR D 40 MOV B,B 6B MOV L,E 96 SUB M C1 POP B EC CPE ADDR 16 MVI D,D8 41 MOV B,C 6C MOV L,H 97 SUB A C2 JNZ ADDR ED -17 RAL 42 MOV B,D 6D MOV L,L 98 SBB B C3 JMP ADDR EE XRI D8 18 - 43 MOV B,E 6E MOV L,M 99 SBB C C4 CNZ ADDR EF RST 5 19 DAD D 44 MOV B,H 6F MOV L,A 9A SBB D C5 PUSH B F0 RP 1A LDAX D 45 MOV B,L 70 MOV M,B 9B SBB E C6 ADI D8 F1 POP PSW 1B DCX D 46 MOV B,M 71 MOV M,C 9C SBB H C7 RST 0 F2 JP ADDR 1C INR E 47 MOV B,A 72 MOV M,D 9D SBB L C8 RZ F3 DI 1D DCR E 48 MOV C,B 73 MOV M,E 9E SBB M C9 RET F4 CP ADDR 1E MVI E,D8 49 MOV C,C 74 MOV M,H 9F SBB A CA JZ ADDR F5 PUSH PSW 1F RAR 4A MOV C,D 75 MOV M,L A0 ANA B CB - F6 ORI D8 20 - 4B MOV C,E 76 HLT A1 ANA C CC CZ ADDR F7 RST 6 21 LXI H,D16 4C MOV C,H 77 MOV M,A A2 ANA D CD CALL Addr F8 RM 22 SHLD ADDR 4D MOV C,L 78 MOV A,B A3 ANA E CE ACI D8 F9 SPHL 23 INX H 4E MOV C,M 79 MOV A,C A4 ANA H CF RST 1 FA JM ADDR 24 INR H 4F MOV C,A 7A MOV A,D A5 ANA L D0 RNC FB EI 25 DCR H 50 MOV D,B 7B MOV A,E A6 ANA M D1 POP D FC CM ADDR 26 MVI H,D8 51 MOV D,C 7C MOV A,H A7 ANA A D2 JNC ADDR FD -27 DAA 52 MOV D,D 7D MOV A,L A8 XRA B D3 OUT D8 FE CPI D8

(23)

28 - 53 MOV D,E 7E MOV A,M A9 XRA C D4 CNC ADDR FF RST 7 29 DAD H 54 MOV D,H 7F MOV A,A AA XRA D D5 PUSH D

2A LHLD ADDR 55 MOV D,L 80 ADD B AB XRA E D6 SUI D8

Observe que alguns códigos, ao serem recebidos pelo processador, não representam instrução alguma. No caso do 8080, esses códigos são:

08, 10, 18, 20, 28, 30, 38, CB, D9, DD, ED e FD.

Ao encontrar uma dessas instruções inválidas, o 8080 não fazia nada. Alguns ciriosos descobriram que certos códigos inválidos eram na verdade instruções não documentadas da Intel, porém nenhum programador sério ousava utilizá-las. Como eram instruções não oficiais, não era garantido que fossem implementadas em todas as versões do processador. No 8085, uma evolução do 8080, a Intel utilizou duas novas instruções: RIM (20h) e SIM (30h). A Zilog utilizou esses códigos para usar com as novas instruções do seu processador Z80.

Nos processadores modernos, não é permitido executar instruções inválidas. Quando isso ocorre, o próprio processador gera uma interrupção e indica operação ilegal. No Windows, isso resulta em uma mensagem como: Erro o programa xxxx executou uma operação ilegal em ...”

Linguagem Assembly do 8086

Depois desta breve apresentação do assembly do processador 8080, estamos finalmente entrando na era dos PCs, com o assembly do processador 8086. Os seus registradores internos são de 16 bits, mas foram inspirados nos registradores do 8080. Na figura 7, os registradores indicados em branco são “herdados” do 8080, enquanto os indicados em cinza são novos, próprios do 8086.

(24)

Figura 8.7

Registradores internos do 8086.

Os regisradores AX, BC, CX e DX são de 16 bits, mas podem ser tratados como duas partes de 8 bits. AX é dividido em AH e AL, BX é dividido em BH e BL, e assim por diante. AX é o acumulador, portanto AL corresponde ao registrador A do 8080. O registrador BX do 8086 corresponde ao par HL do 8080 (assim como BH corresponde a H e BL corresponde a L). Da mesma forma, CX corresponde ao par BC e DX corresponde ao par DE. O contador de programa (PC) do 8080 é chamado no 8080 de IP (Instruction Pointer). O Stack Pointer (SP) é similar, e os flags (Cy, Z, AC, P e S) ficam em um registrador de flags, com 16 bits.

Esta correspondência entre os registradores do 8086 e do 8080 foi proposital. Permitiu que programas escritos em assembly do 8080 fossem rapidamente convertidos para o 8086, mesmo que não da forma mais eficiente. Por exemplo, as instruções MOV D,B / MOV E,C podiam ser diretamente traduzidas por MOV DH,CH / MOV DL,CL. Entretanto é muito melhor usar os recursos de 16 bits, com a instrução MOV DX,CX. Depois de converter os antigos programas assembly de 8080 para 8086, os produtores de software passaram a criar programas novos já usando os recursos mais avançados do 8086, resultando em programas mais eficientes. Programas em linguagem de alto nível (C, Pascal, etc.) podiam ser convertidos com mais facilidade, já que eram desvinculados do assembly.

Novas instruções

Além de ter todas as instruções do 8080 ou instruções similares, o 8086 trouxe novas instruções bem mais avançadas, com execução mais rápida. Alguns exemplos:

(25)

 Multiplicacão de números inteiros de 16 bits  Divisão de números inteiros de 32 bits  Rotações com qualquer número de bits  Movimentação e comparação de strings  Instruções para manipulação direta de bits  Instruções de repetição

Registradores BX, BP, SI e DI

Esses registradores permitem várias operações comuns em outros registradores, e além delas, podem ser usados como índices. Por exemplo, podemos usá-los para apontar posições de memíria usando expressões como: [BX+valor] [BX+SI+valor]

[BP+valor] [BX+DI+valor] [SI+valor] [BP+SI+valor] [DI+valor] [BP+DI+valor] Exemplos:

MOV BX,1000h ; Aponta para o endereço 1000h MOV AL,[BX+15h] ; Lê para AL o valor que está em 1015h MOV BX,2000h

MOV SI,100h

MOV AL,[BX+SI+20h] ; Lê para AL o valor que está em 2120h

O uso de índices torna a programação extremamente mais simples quando temos que lidar com estruturas de dados na memória, como strings e matrizes.

Registradores de segmento

O 8086 podia endereçar 1 MB de memória, muito mais que os 64 kB permitidos pelo 8080. No 8080, toda a memória era tratada como uma única coleção de bytes, contendo instruções, dados e stack. No 8086, esses elementos também ficam nesta mesma memória, apesar de maior. Apesar da memória ser homogênea do ponto de vista físico, seu uso é dividido em áreas chamados segmentos. Instruções devem ficar no segmento de código, dados devem ficar no segmento de dados ou no segmento extra, e a stack deve ficar no segmento de stack. Para manter essas 4 áreas de memória diferentes, o 8086 possui 4 registradores de segmento, que são:

(26)

DS: Data segment ES: Extra segment SS: Stack segment

Cada segmento no 8086 é uma área de memória com 64 kB. Os registradores de segmento indicam o endereço inicial dos respectivos segmentos. Note que esses registradores têm 16 bits, enquanto os endereços de memória do 8086 têm 20 bits. O processador obtém o endereço inicial de cada segmento adicionando 4 bits zero (ou um dígito 0 hexadecimal) à direita do valor existente no regitrador de segmento. Por exemplo, se o registrador CS está com o valor 7BC3, então o segmento de dados começa no endereço 7BC30.

Figura 8.8

Regitradores de segmento indicam os inícios dos respectivos segmentos.

A figura 9 mostra como é feito o endereçamento da memória dentro de um segmento. Todos os acessos a instruções são feitas automaticamente no segmento de dados. Digamos que CS esteja armazenando o valor 2800h, o que indica que o segmento de dados começa em 28000h. Digamos que o registrador IP (Instriction Pointer) esteja com o valor 0153h. Para obter o endereço de memória, fazemos a seguinte conta: adicionar um zero à direita do valor do segmento e somar este resultado com o offset, que no caso é o valor de IP. Encontramos então 28000h+0153h=28153h.

(27)

Figura 8.9

Determinação de um endereço absoluto a partir do segmento e do offset.

Todos os endereços do 8080 são compostos desta forma. O endereço usado para acessar a memória (de 00000 a FFFFF) é o que chamamos de endereço absoluto. O endereço absoluto sempre é formado por um valor de segmento e um offset. O valor do segmento é adicionado de um zero hexadecimal à sua direita e somado com o valor do offset, resultando no endereço absoluto. Vejamos um outro exemplo. Digamos que tenhamos DS=8A9Fh e BX=7CB6h. A instrução MOV AL,[BX] buscará um byte do endereço absoluto dado por:

8A9F0h +7CB6h = 926A6h

Observe que cada posição de memória pode ser endereçada de várias outras formas. Por exemplo, o mesmo endereço absoluto 926A6H pode ser obtido fazendo DS=9000h e BX=26A6h.

Todas as instruções a serem executadas são buscadas no segmento de código, portanto o registrador CS é usado na determinação do endereço absoluto. Todos os acessos a dados são feitos no segmento de dados, portanto o processador usa o valor de DS no cálculo do endereço absoluto. Certas instruções que manipulam strings utilizam o segmento extra (ES é a base para o cálculo), e as operações com a stack são feitas no segmento de stack, determinado por SS.

Usando 4 segmentos de 64kB (código, dados, stack e extra), somos levados a concluir erradamente que um programa de 8086 pode ter no máximo 64 kB. Na prática não é isso o que ocorre. Para programas pequenos, não é necessário usar integralmente os 64 kB de cada segmento, portanto pode

(28)

ocorrer interseção entre os segmentos. Além disso, instruções especiais alteram automaticamente o valor de CS em operações de desvio e chamadas de rotinas, resultando em programas de maior tamanho, podendo até mesmo usar toda a memória disponível. Um mesmo programa pode ter múltiplos segmentos de código e de dados, manipulando assim quantidades maiores de memória.

Modos de endereçamento

O 8086 possui vários modos de endereçamento: Imediato: Opera com valores constantes. Exemplos:

MOV AX,0 ; Carrega AX com 0 MOV BX,1000h ; Carrega BX com 1000h MOV DL,20h ; Carrega DL com 20h MOV SI,3500h ; Carrega SI com 3500h

Registrador: Quando envolve apenas registradores. Exemplos:

MOV AX,BX ; Copia BX em AX MOV CX,SI ; Copia SI em CX MOV DS,AX ; Copia AX em DS

OR BX,CX ; Faz um “OR” de BX com CX. Resultado fica em BX

Direto: Qundo faz referência a um endereço fixo de memória. Exemplos:

MOV AX,[1000h] ; Carrega o valor do endereço 1000h em AL e do endereço 1001h em AH ADD CX,[2000h] ; Carrega o valor de 16 bits dos endereços 2000h/2001h em CX CMP SI,[1020h] ; Carrega o valor de 16 bits dos endereços 1020h/1021h em SI

Indexado: Este é o modo de endereçamento mais flexível. Usa os registradores BX, BP, SI e DI como índices. Os índices podem ser usados sozinhos ou combinados, ou seja, o valor da soma de BX ou BP com SI ou DI. Sobre este valor ainda pode ser adicionada uma constante. Exemplos:

MOV CL,[BX] MOV DL,[BP] MOV AX,[SI] MOV AH,[DI] MOV CX,[BX+5] MOV DL,[BP+50] MOV AL,[SI+100] MOV AX,[DI+1200] MOV AX,[BX+SI] MOV CL,[BX+SI+200] MOV AH,[BP+DI] MOV DX,[BP+DI+300] MOV CX,[DI+4800]

(29)

MOV DX,[BP+SI] MOV AH,[BP+SI+2000] MOV AL,[BP+DI] MOV DX,[BP+DI+700]

Note que não é permitido usar em uma única instrução, dois endereçamentos à memória. Por exemplo, não podemos usar MOV [SI],[DI]. Apesar disso podemos mover dados entre quaisquer resitradores e quaisquer formas de endereçamento da memória (coisa que não era permitida no 8080). No caso do 8086, existem algumas raras exceções. Por exemplo, não podemos usar livremente os registradores de segmento com todas as operações que são suportadas pelos demais registradores. Não podemos usar, por exemplo, ADD DS,AX. Os registradores de segmento permitem apenas instruções de movimentação de dados.

Instruções de movimentação de dados

MOV: Move dados entre dois locais quaisquer. Podem ser usados nesta instrução, qualquer um dos modos de endereçamento já citados. Exemplos:

MOV AX,BX MOV DI,1000h MOV [BX+SI],20 MOV CL,19 MOV SI,[BX] MOV [BP+DI],CX

Note que o 8086 não tem instruções equivalentes a STAX e LDAX do 8080, que usam pares BC e DE para indexar a memória, já que não existem os modos de endreçamento [CX] e [DX].

XCHG: No 8080 esta instrução permutava os valores de DE e HL. No 8086, quaisquer valores podem ser permutados, o que engloba todos os registradores e a memória, endereçada por todos os modos válidos. É permitido inclusive usá-la com elementos de 8 bits. Exemplos:

XCHG BX,DX XCHG AX,SI XCHG AL,BH XCHG CX,[BX+SI]

XLAT: Esta é uma instrução bastante especializada. É útil para implementar traduções de códigos. Digamos que tenhamos na memória uma tabela de 256 valores, e queremos obter o valor desta tabela, cujo índice está em AL. A instrução XLAT faz isso, uma operação equivalente a MOV AL,[BX+AL].

(30)

Instruções aritméticas

NEG: Inverte o sinal do valor aritmético especificado. Se o número for positivo, tornar-se-a negativo, e vice-versa. Note que números negativos necessitam de um bit (o mais significativo) para indicar o sinal, e os demais para indicar a magnitude. Números com sinal armazenados em 8 bits podem portanto variar entre –128 e +127. Com 16 bits, variam entre –32.768 e +32.767.

NEG AL NEG AX NEG BX NEG DX

NEG byte ptr [BX+SI] NEG word ptr [DI+4]

Estamos apresentando agora os prefixos byte ptr e word ptr. Esses prefixos são utilizados para informar ao assembler a real intenção do programador, se é acessar um dado de 8 ou de 16 bits. Por exemplo, na instrução MOV AL, [BX], o assembler sabe que o valor da memória a ser acessado é de 8 bits, já que o outro operando é AL, que é também de 8 bits. Já em instruções como NEG [BX], o assembler não saberia se a operação deve ser feita sobre o byte cujo endereço é dado por BX, ou se deve operar sobre os dois bytes (word) com este endereço. Usamos então os prefixos byte ptr e word ptr quando necessário para dar esta informação ao assembler.

ADD, ADC: Soma os dois operandos. O resultado é colocado no primeiro operando. A operação pode ser feita com 8 ou 16 bits, dependendo do operando. A instrução ADC soma os dois valores com o bit Carry, o que é usado para fazer o “vai 1”, agrupando dados de 16 bits para formar dados com maior número de bits. Exemplos:

ADD BX,SI ADD AX,[BX+DI] ADD CL,AH ADD DX,CX ADD [SI],DX ADC CX,[BX+SI] ADC AH,[BP+SI+3] ADC DX,BX ADC [SI],AX

SUB, SBB: Essas duas instruções utilizam os mesmos operandos das instruções ADD e ADC. Fazem a subtração de valores. A diferença entre elas é que a SBB subtrai também o valor do bit Carry, tornando possível a operação de “pedir emprestado”, o que é necessário para agrupar vários

(31)

dados de 16 bits, manipulando assim números inteiros com maior número de bits. Exemplos: SUB BX,DX SUB CX,[BP+DI] SUB CH,DL SUB CX,AX SUB [SI],BX SBB AX,[BX+DI] SBB CX,[BP+SI+3] SBB CX,AX SBB [SI],CX

MUL, IMUL: São as duas instruções de multiplicação do 8086. Ambas podem operar com 8 e 16 bits. A diferença é que MUL é usada para números sem sinal, somente positivos, enquanto IMUL aceita números inteiros, sejam positivos ou negativos. Nesta multiplicação, um dos fatores é sempre AX ou AL. O outro fator pode ser qualquer operando na memória ou um outro registrador, com 8 ou 16 bits. Ao multiplicarmos dois valores de 8 bits, o resultado é armazenado nos 16 bits de AX. Ao multiplicarmos dois valores de 16 bits, o resultado é armazenado em 32 bits, ficando os 16 menos significativos em AX e os 16 mais significativo em DX. Exemplos:

MUL CL MUL BX MUL byte ptr [SI] IMUL DX IMUL BX IMUL CL

DIV, IDIV: São as instruções de divisão. O dividendo pode ser de 16 ou 32 bits. Se for de 16 bits, é usado o valor de AX. Se for de 32 bits, é usado o valor obtido em DX e AX. O que definirá se o dividendo será de 16 ou 32 bits é o divisor. Se o divisor for de 8 bits, será considerado como dividendo, AX, o quociente ficará em AL e o resto em AH. Se o divisor for de 16 bits, será considerado como dividendo o número de 32 bits formado por DX e AX. O quociente ficará em AX e o resto em DX.

Note que esta instrução parte do princípio de que o resultado “caberá” no registrador destinado ao quociente. Se não couber, ocorrerá um erro chamado “estouro de divisão” (divide overflow). Por exemplo, ao fazer a conta 8000h dividido por 2, usando um divisor de 8 bits, o resultado será 4000h, que não cabe em 8 bits. Para não passar por este problema é melhor fazer a mesma conta usando o divisor 2 com 16 bits (MOV CX,2 / DIV CX). Assim como ocorre nas instruções MUL e IMUL, a instrução DIV opera

(32)

apenas com números inteiros positivos, e a IDIV opera tanto com positivos quanto com negativos.

INC, DEC: Incrementa de uma unidade e decrementa de uma unidade. Os bits Carry e Zero são afetados por essas operações, portanto podem ser usadas para implementar contadores. Por exemplo, para preencher a tela de um terminal de vídeo com 2000 caracteres em branco, podemos usar o seguinte trecho de programa:

MOV DX,2000 ; Número de bytes a serem enviados ENVIA: MOV AL, 20h ; 20h é o código do caracter “espaço”.

CALL OUTCHAR ; Envia o caracter para o terminal de video DEC DX ; Decrementa o contador

JNZ ENVIA ; Pula se não chegou a zero

Além de implementar contadores, as instruções INC e DEC também podem ser usadas para implementar ponteiros para posições de memória, o que e útil quando queremos manipular dados seqüenciais.

Instruções lógicas

NOT: Inverte todos os bits do dado especificado. Cada bit 1 se transforma em 0, e cada bit 0 se transforma em 1. Exemplos:

NEG AX NEG SI NEG DL NEG byte ptr [BX] NEG word ptr [BP+DI] NEG byte ptr [1000h]

AND, OR, XOR: São os tradicionais operadores lógicos “E”, “OU” e “OU Exclusivo”. Não há necessidade de apresentar novamente a tabela verdade desses operadores, já mostradas quando apresentamos as instruções do 8080. A diferença aqui é que essas operações podem ser feitas com 8 ou 16 bits, e os dois operandos podem ser quaisquer, desde que ambos sejam do mesmo tipo (ou ambos são byte, ou ambos são word). O resultado da operação ficará armazenado no primeiro operando.

AND AX,SI AND CX,[BX+DI] AND DL,CH OR [SI],AL OR AX,1040h OR byte ptr[SI],20h XOR BX,DX XOR [SI+2],AL XOR AL,AH

(33)

Shifts e Rotates

O 8086 tem diversas instruções para deslocar bits para a esquerda e para a direita. São chamadas de shifts e rotates. As instruções SHL e SHR são exemplos de shifts. Provocam o deslocamento de todos os bits para a esquerda e para a direita, respectivamente. Bits 0 são introduzidos à direita e à esquerda. A operação dessas duas instruções é mostrada na figura 10. Note que no 8086, qualquer registrador ou posição de memória pode ser usada com esta instrução. Podemos aplicar um deslocamento de um só bit ou de múltiplos bits, como mostraremos mais adiante.

Figura 8.10

Instruções SHL e SHR.

A diferença entre um shift e um rotate é que o shift introduz um bit 0 ou 1 no no bit mais ou no bit menos significativo, como é o caso das instruções SHL e SHR mostradas na figura 10. Uma instrução de rotate forma uma espécie de “anel”, na qual o bit que sai em uma extremidade é recolocado na otura extremidade. A figura 11 mostra as instruções ROL e ROR (rotate left e rotate right). O exemplo da figura mostra a rotação de valores de 8 bits, mas também podem ser usados operandos de 16 bits. Note que na instrução ROL, o bit 7 é realimentado no lugar do bit 0. Na instrução ROR o bit 0 é realimentado no lugar do bit 7. Em ambas as instruções, o bit que é realimentado também é copiado no Carry. Este método de rotação é o mesmo das instruções RLC e RRC do 8080.

Figura 8.11

(34)

As instruções RCL e RCR operam de forma similar, exceto pelo fato do bit Carry fazer parte da rotação, ao invés de simplesmente ficar com uma cópia do bit realimentado. A figura 12 mostra o funcionamento dessas duas instruções, que são exemplos de rotates. Este é o mesmo método de rotação usado pelas instruções RAL e RAR do 8080.

Figura 8.12

Instruções RCL e RCR.

Finalmente apresentamos as instruções SAL e SAR, que também são shifts, da mesma forma como as instruções SHL e SHR já apresentadas. Note que deslocar os bits uma posição para a esquerda, introduzindo zeros, equivale a multiplicar o valor por 2, e deslocar os bits uma posição para a direita equivale a dividir o valor por 2. Isto funciona para números positivos, mas quando os números podem ter sinal (o sinal é representado pelo bit mais significativo; 1 significa negativo e 0 significa positivo), é preciso que as instruções de shift preservem este sinal. Para isso servem as instruções SAL e SAR, que são chamados shifts aritméticos (assim como SHR e SHL são chamados shifts lógicos). O funcionamento dessas duas instruções é mostrado na figura 13.

Figura 8.13

Instruções SAL e SAR.

A instrução SAL é idêntica à instrução SHL, com a introdução de zeros. Já a instrução SAR tem uma diferença. Ao invés de serem introduzidos zeros no bit mais significativo, este é realimentado em si próprio, ou seja, é copiado para o bit seguinte mas o seu próprio valor permanece inalterado. Esta

(35)

alteração permite que números negativos continuem negativos ao serem deslocados para a direita (ou seja, divididos por 2).

Além da maior variedade de instruções de shifts e rotates, o 8086 permite operar não apenas com o acumulador, mas com qualquer outro registrador (exceto registradores de segmento), de 8 ou 16 bits. Também pode operar com posições de memória, de 8 ou 16 bits. Uma outra diferença importante é que o deslocamento pode ser feito apenas uma posição (como exemplificado nas figuras), ou com múltiplas posições. Por exemplo, podemos deslocar um valor 3 bits para a esquerda, o que equivale a usar 3 vezes consecutivas a mesma instrução. Para aplicar shifts e rotates múltiplos, basta carregar no registrador CL, o número de vezes que os bits devem ser deslocados. Exemplos:

SHR AX,1 ; Aplica um shift para a esquerda em AX, de 1 bit. MOV CL,4 ; Prepara CL com o número de bits a serem deslocados ROR BX,CL ; Roda BX 4 bits para a direita

SHL DL,1 ; Aplica um shift em DL de 1 posição para a esquerda

Desvios

As instruções de CALL, RET e JMP presentes no 8080 também estão presentes no 8086. Também temos as formas condicionais da instrução JMP, mas não temos formas condicionais das instruções CALL e RET. Por exemplo, não existe a instrução RC (Return if Carry), como no 8080. No seu lugar temos que fazer uma combinação das instruções JNC e RET.

As formas condicionais da instrução JMP estão representadas na tabela que se segue:

(36)

Note que muitas instruções possuem aliases, ou seja, sinônimos. Por exemplo, “pule se menor ou igual” é a mesma coisa que “pule se não é maior”, portanto existem duas instruções idênticas: JBE e JNA (jump if below or equal / jump if not above).

Uma outra instrução interessante é LOOP. Esta instrução faz o seguinte: decrementa o valor de CX, e se este registrador não chegou a zero, faz o desvio para um label especificado. Por exemplo:

MOV CX,10 ; Contador para 10 vezes

MOV SI,1000 ; SI aponta para endereço 1000 da memória MOV DI,2000 ; DI aponta para 2000

TRANSF: MOV AL,[SI] ; Pega um byte da origem MOV [DI],AL ; Guarda no destino INC SI ; Incrementa ponteiros INC DI

LOOP TRANSF ; Decrementa CX e se não chegou a zero vai para TRANSF

Neste pequeno trecho de programa as 4 instruções MOV AL,[SI] / MOV [DI],AL / INC SI / INC DI será executado 10 vezes, que é o valor inicial do contador CX. Observe que este exemplo é meramente explicativo, já que existe uma única instrução que faz tudo isso sozinha (REP MOVS), como veremos mais adiante. O objetivo deste exemplo foi mostrar como a instrução LOOP pode ser usada para implementar repetições.

Existem ainda as formas condicionais da instrução LOOP, que são LOOPE e LOOPNE (ou LOOPZ e LOOPNZ). Essas instruções fazem previamente um

(37)

teste no bit Zero, e executação uma instrução LOOP caso a condição seja satisfeita. Se a condição não for satisfeita, o loop será terminado. Podemos usar os loops condicionais para fazer uma comparação ou finalizar uma contagem antes imediatamente antes do final do loop, permitindo assim que o loop possa ser finalizado mesmo que o contador não tenha chegado a zero. Existe ainda a instrução JCXZ (jump if CX=0). Como o nome já diz, esta instrução executa um desvio caso o valor de CX tenha atingido o valor zero. Note que esta instrução, a instrução LOOP e suas formas condicionais, e as instruções de shifts e rotates que podem usar em CL o número de bits a serem deslocados, dão ao registrador CX uma espécie de “personalidade”. Este registrador é usado como contador em todas essas instruções citadas, e em outras que ainda vamos apresentar.

Rotinas e retornos

Como já abordamos, as instruções de chamadas e retornos de rotinas são CALL e RET, e não possuem formas condicionais. Existem entretanto outras instruções de chamadas e retornos.

A instrução INT é uma espécie de “interrupção de software”. Normalmente é usada para serviços do sistema operacional. Os primeiros 1024 bytes da memória são reservados para uma área chamada vetor de interrupções. Este vetor tem 256 elementos, e cada um desses elementos é composto de 4 bytes, sendo 2 para indicar um segmento e 2 para indicar um offset. Cada um desses 256 elementos é o endereço de uma função do sistema operacional encarregada de um determinado serviço. Cabe ao produtor do sistema operacional estipular como essas 256 interrupções serão usadas. Por exemplo, no MS-DOS, a instrução INT 21h é usada para várias chamadas de funções básicas de acesso a disco e E/S em geral.

Quando uma instrução CALL é executada, o endereço de IP é armazenado na stack. A operação inversa é feita pela instrução RET. Quando uma instrução INT é executada, os valores de CS e IP são armazenados na stack, já que serão carregados com novos valores encontrados no vetor de interrupções. O final de uma rotina de interrupção, seja ela de software ou de hardware, tem que terminar com uma instrução IRET. A diferença é que IRET obtém da stack, valores de CS e IP, enquanto uma instrução RET comum obtém apenas o valor de IP.

(38)

As instruções PUSH e POP são utilizadas respectivamente para armazenar e recuperar valores de 16 bits na pilha. Todos os registradores de 16 bits podem ser usados com essas instruções, bem como dados de 16 bits da memória. As instruções PUSHF e POPF são usadas para salvar e recuperar o registrador de flags. Exemplos:

PUSH BX PUSH SI PUSH BP PUSH DS POP AX POP CX POPF Interrupções e E/S

Várias instruções são usadas para o processador interagir com o hardware. As instruções STI e CLI são hadas para habilitar e desabilitar interrupções. Instruções IN e OUT fazem operações de entrada e saída com 8 ou 16 bits. Nas instruções de 8 bits é usado o registrador AL, e nas instruções de 16 bits é usado o registrador AX. Exemplos:

IN AL,80h ; Lê porta de 8 bits no endereço 80h IN AX,60h ; Lê porta de 16 bits no endereço 60h OUT 43h,AL ; Envia dado de AL para a porta 43h

OUT 40h,AX ; Envia AL para a porta 40h e AH para a porta 41h

Usadas neste modo, as instruções IN e OUT permitem endereçar portas na faixa de endereços de 00 a FF. Para endereçar portas em todo o espaço de endereçamento do processador (0000 a FFFF) é preciso colocar em DX o endereço da porta a ser acessada. Exemplos:

MOV DX,3F0h ; DX aponta para a porta 3F0 IN AL,DX ; Lê o valor da porta 3F0 MOV DX,278h ; Aponta para a porta 278h OUT DX,AL ; Envia dado de AL para a porta 278h

Manipulação de strings

O processador 8086 e seus sucessores têm a capacidade de manipular strings, que são cadeias de caracteres. Essas funções são importantes em processadores de texto e compiladores. Em todas as instruções de strings, os registradores SI e DI apontam para as strings envolvidas. SI aponta para a origem, localizada no segmento de dados. DI aponta para a string destino, localizada no segmento Extra. Portanto as strings de origem e destino estão em CS:SI e ES:DI, respectivamente. O registrador CX é usado como

Referências

Documentos relacionados

Note on the occurrence of the crebeater seal, Lobodon carcinophagus (Hombron & Jacquinot, 1842) (Mammalia: Pinnipedia), in Rio de Janeiro State, Brazil.. On May 12, 2003,

O desenvolvimento das interações entre os próprios alunos e entre estes e as professoras, juntamente com o reconhecimento da singularidade dos conhecimentos

Importante, nesse contexto, ressaltar que a PNAB é uma Portaria que foi publicada no ano de 2017, cujo objetivo é estabelecer a revisão de diretrizes para a organização da

Após a realização de todas as atividades teóricas e práticas de campo, pode-se concluir que não há grande erosão do conhecimento popular e tradicional de plantas medicinais, que

8- Bruno não percebeu (verbo perceber, no Pretérito Perfeito do Indicativo) o que ela queria (verbo querer, no Pretérito Imperfeito do Indicativo) dizer e, por isso, fez

2. Identifica as personagens do texto.. Indica o tempo da história. Indica o espaço da história. Classifica as palavras quanto ao número de sílabas. Copia do texto três

ensino superior como um todo e para o curso específico; desenho do projeto: a identidade da educação a distância; equipe profissional multidisciplinar;comunicação/interatividade

O valor da reputação dos pseudônimos é igual a 0,8 devido aos fal- sos positivos do mecanismo auxiliar, que acabam por fazer com que a reputação mesmo dos usuários que enviam