Baixe o app para aproveitar ainda mais
Prévia do material em texto
8086 MICROPROCESSADOR Linguagem Assembly Maria João Nicolau Universidade do Minho – Portugal http://piano.dsi.uminho.pt/~joao/Computacao2/node16.html Pesquisa realizada pelos alunos: CARLOS AUGUSTO PETRINI PAULO TEODORO PINTO JÚNIOR VALDECIR REINALDO TASCA 3° PD – Noturno Faculdade de Tecnologia de Americana Agosto/2002 O MICROPROCESSADOR 8086 Introdução Os computadores podem ser divididos em diferentes tipos tendo em conta o seu tamanho e capacidade. Os maiores e mais poderosos são os chamados mainframes e chegam por vezes a ocupar uma sala. No outro extremo estão os chamados microcomputadores, usados habitualmente como computadores pessoais. Os microprocessadores mais usados no fabrico de microcomputadores são os produzidos pela Intel, que constituem já uma família numerosa. O 8086 foi o primeiro membro desta família e apesar de ter sido há muito ultrapassado pelos seus sucessores continua a ser utilizado como um ponto de partida para o estudo dos microprocessadores. Durante as próximas sessões de trabalho vamos debruçar-nos sobre variados aspectos dos microprocessadores, utilizando o 8086 como caso prático. O objetivo deste texto de apoio é somente fazer a revisão de alguns conceitos já estudados e introduzir o microprocessador 8086, a sua arquitetura, registros, modelo de programação, etc. Arquitetura dos microcomputadores - Revisões Um microcomputador é constituído pelos seguintes componentes: ? O CPU (Central Processing Unit), que controla todo o funcionamento do computador. O CPU vai buscar instruções à memória, descodifica-as e executa-as. ? A memória, onde se guarda instruções e dados. ? A unidade de entradas/saídas, que permite ao computador receber e enviar dados para o exterior através de periféricos. Estas componentes estão ligadas entre si por conjuntos de linhas paralelas que se designam por barramentos. Os barramentos podem ser agrupados em três tipos: ? Barramento de Endereços. É através deste barramento que o CPU consegue endereçar as posições de memória de onde quer ler ou para onde quer escrever dados ou instruções. O número de linhas que constitui o barramento de endereços determina a quantidade de memória que o CPU consegue endereçar. Por exemplo, um CPU com 16 linhas de endereços consegue endereçar 64 kbytes de memória (216 = 65 536). ? Barramento de Dados. Este barramento é constituído por linhas bidirecionais e é usado pelo CPU para ler ou escrever dados na memória ou nas várias portas que constituem a unidade de entradas/saídas. ? Barramento de Controle. É através deste barramento que o CPU envia os sinais que lhe permitem ler e escrever em posições de memória e nas várias portas de entrada/saída. Exemplos típicos de sinais que o CPU envia através do barramento de controle são os sinais: memory read, memory write, I/O read e I/O write. A família de microprocessadores 8086 O microprocessador 8086 da Intel é um microprocessador de 16 bits que é suposto ser usado como CPU num microcomputador. Quando se diz que é um processador de 16 bits, quer-se dizer que a sua unidade lógica e aritmética, os seus registros internos, e a maior parte das suas instruções foram concebidos para trabalhar com palavras de 16 bits. Além disso o 8086 tem um barramento de dados de 16 bits, ou seja, pode ler e escrever na memória ou nas portas 16 bits de uma só vez. O barramento de endereços é de 20 bits, ou seja o 8086 consegue endereçar 1 Mb (220) posições de memória. Cada uma destas posições de memória é ocupada por um byte. Arquitetura O 8086 está dividido em duas unidades distintas: a BIU (Bus Interface Unit) e a EU (Execution Unit). A BIU envia endereços para o barramento de endereços, lê instruções da memória, lê e escreve dados nas portas e na memória, etc. Por outras palavras é a unidade responsável por todas as transferências de dados e endereços através dos barramentos. A EU diz à BIU onde é que há de ir buscar instruções ou dados, descodifica as instruções e executa-as. Registros No total este processador é composto por 14 registros de 16 bits cada. Registros Genéricos Oito desses registros são chamados registros genéricos e encontram-se divididos em dois grupos de quatro registros cada. O primeiro grupo contém os chamados data registers (AX, BX, CX e DX) e o segundo os index registers (SI e DI) e os pointer registers (BP e SP). Apesar de todos os registros deste microprocessador serem registros de 16 bits, os registros de dados, AX, BX, CX e DX podem ser utilizados como registros de oito bits. Utilizamos para isso o byte mais significativo e o byte menos significativo de cada registro (AH e AL respectivamente, para o caso do registro AX). Registro das Flags Este registro é um registro de condição que reflete em cada um dos seus bits situações que ocorrem no interior do microprocessador durante as operações aritméticas e lógicas. Registros de Segmento e Deslocamento O endereçamento de memória é feito a partir dos registros de segmento e registros de deslocamento (offset). Como já foi referido, os registros internos do 8086 são todos de 16 bits e, como também já sabemos, com 16 bits conseguimos apenas endereçar 216 bytes, ou seja 64 Kbytes. Para endereçar 1 MByte foi necessário recorrer a uma técnica designada por segmentação e que consiste no seguinte: em vez de um registro, são utilizados dois registros. Um desses registros aponta para um valor de memória chamado base do segmento, enquanto o outro determina o deslocamento em relação a essa base num valor máximo de 64 Kbytes. Os registros que são a base do segmento são os registros de segmento e os registros que determinam o deslocamento do endereço em relação a essa base chamam-se registros de deslocamento. Existem 4 registros de segmento, são eles: ? O CS (Code Segment) que endereça código (instruções que constituem programas); ? O DS (Data Segment) que endereça dados (áreas de memória que contém dados); ? o SS (Stack Segment) que endereça a zona de stack (zona de arquivo temporário do conteúdo de registros e variáveis); ? o ES (Extra Segment) que endereça dados; Os registros de deslocamento trabalham associados aos registros de segmento. ? O IP (Instruction Pointer) está associado ao registro CS; ? O SP (Stack Pointer) está associado ao registro SS; ? Associado aos registros DS e ES podem aparecer qualquer dos registros genéricos embora se usem com mais freqüência os index registers e os pointer registers. Um endereço representado a partir de dois valores de registros tem o nome de endereço lógico. O endereço físico ou endereço real é calculado a partir desse endereço lógico, deslocando o valor de base do endereço, 4 bits para a esquerda, e somando ao resultado o valor do deslocamento. Exercícios: ? Qual o endereço físico associado ao endereço lógico 0020h:0056h; ? Qual o endereço lógico associado ao endereço físico 02100h; Programação do 8086 - Introdução Introdução Para compreendermos a estrutura de um microcomputador e os seus circuitos internos, nada melhor do que recorrermos a programa desenvolvidos em linguagem Assembly, uma vez que esta linguagem está muito próxima dos circuitos eletrônicos da máquina. Como já foi referido os microcomputadores que vamos estudar são os baseados nos microprocessadores 8086 da Intel. Todos os modelos que se seguiram ao 8086 são compatíveis com este em instruções e modo de funcionamento. Os microprocessadores posteriores ao 8086 (286, 386, 496 e Pentium) podem trabalhar em modo real ou em modo protegido. O modo real, onde o endereçamentoda memória é feito usando a segmentação, é totalmente compatível com o modo de funcionamento do 8086. O objetivo desta sessão de trabalho é introduzir a linguagem Assembly do 8086. Os registros do 8086 e a forma como este endereça a memória foram introduzidos na sessão de trabalho anterior. No fim desta sessão de trabalho os alunos deverão ser capazes de escrever e compreender o código de programas muitos simples desenvolvidos em linguagem Assembly do 8086, bem como estar familiarizados com as ferramentas necessárias ao desenvolvimento de programas nesta linguagem. Linguagens de Programação Para executar um programa, o microcomputador tem de ter na memória a seqüência de instruções que o constituem, na forma binária. Existem múltiplas linguagem de programação alternativas, mas basicamente ela dividem-se em três tipos. São eles: ? Linguagem Máquina. Esta é a linguagem entendida pelo computador. Um programa em linguagem máquina é uma seqüência de códigos binários que correspondem às instruções que se pretende que o computador execute. Esta linguagem é muito pouco usada pelos programadores, uma vez que é muito difícil memorizar os milhares de códigos binários de instruções que tem um microprocessador. ? Linguagens Assembly. Com o objetivo de tornar mais fácil a programação sem perder o controle do hardware muitos programadores utilizam a linguagem Assembly. A linguagem Assembly é constituída por um conjunto de instruções simbólicas que são mapeadas diretamente para linguagem máquina usando assemblers. Os assemblers traduzem, uma a uma, as instruções da linguagem Assembly em instruções da linguagem máquina. Uma instrução em Assembly corresponde a uma instrução em linguagem máquina. ? Linguagens de alto nível. Estas linguagens estão mais próximas da linguagem natural do que da linguagem máquina. O trabalho de traduzir uma linguagem na outra cabe a programas bastante complexos que se designam por compiladores. Uma instrução numa linguagem de alto nível corresponde normalmente a várias instruções em linguagem máquina. A linguagem Assembly do 8086 Para introduzir a linguagem Assembly do 8086 vamos utilizar o programa exemplo.asm. Trata-se de um programa completo, embora muito simples, que tem como objetivo multiplicar dois números de 16 bits. DATA_HERE SEGMENT MULTIPLICANDO DW 204AH MULTIPLICADOR DW 382AH PRODUTO DW 2 DUP(0) DATA_HERE ENDS CODE_HERE SEGMENT ASSUME CS:CODE_HERE, DS:DATA_HERE MOV AX, DATA_HERE MOV DS, AX MOV AX, MULTIPLICANDO MUL MULTIPLICADOR MOV PRODUTO, AX MOV PRODUTO+2, DX MOV AH, 4CH INT 21H CODE_HERE ENDS END Este programa multiplica dois números de 16 bits e dá como resultado um número de 32 bits que é guardado em memória. Instruções de inicialização Como em todas as linguagens de programação existem na linguagem Assembly uma série de instruções que é necessário incluir antes de se começar a escrever o programa propriamente dito. A essas instruções vamos chamar instruções de inicialização. A finalidade destas instruções é efetuar uma série de inicializações que permitem a correta execução do programa. As diretivas SEGMENT e ENDS e a diretiva ASSUME As diretivas SEGMENT e ENDS são usadas para identificar o grupo de dados ou de instruções que pretendemos que façam parte do mesmo segmento. Todas os dados ou instruções que aparecem entre a diretiva SEGMENT e a diretiva ENDS fazem parte do mesmo segmento lógico. Um programa em linguagem Assembly do 8086 pode ter vários segmentos de dados e vários segmentos de código, no entanto no momento em que o executamos só trabalha com 4 segmentos físicos. São eles o code segment, o data segment, o stack segment e o extra segment. A diretiva ASSUME indica ao assembler quais dos segmentos lógicos correspondem a cada um destes segmentos. Inicialização dos registros de segmento Uma das inicializações que precisa de ser sempre feita é a inicialização dos registros de segmento. Estes registros precisam de ser carregados com os endereços de memória onde queremos que os segmentos comecem. Como já foi referido a diretiva ASSUME diz ao assembler quais os nomes dos segmentos lógicos que se pretende usar como code segment, data segment, stack segment e extra segment. Com excepção do registro CS, todos os outros registros de segmento necessitam de ser inicializados. No programa exemplo.asm as instruções MOV AX,DATA_HERE e MOV DS,AX servem para inicializar o segmento de dados. Nomes para os dados Os programas trabalham com três tipos de dados: constantes, variáveis e endereços. O Assembly do 8086 permite dar nomes a qualquer um destes tipos de dados, o que permite referenciá-los de forma muito mais fácil. As diretivas DB, DW e DD são usadas para atribuir nomes a variáveis. A diretiva DB depois de um nome significa que esse nome corresponde a uma variável do tipo byte (8 bits), a diretiva DW significa que o nome corresponde a uma variável do tipo word (16 bits) e a diretiva DD significa que o nome corresponde a uma variável do tipo double word (32 bits). No programa exemplo.asm a instrução MULTIPLICANDO DW 204AH declara uma variável do tipo word e inicializa-a com 204AH. A instrução MULTIPLICADOR DW 3B2AH declara uma variável também do tipo word e inicializa-a com o valor 3B2AH. A declaração PRODUTO DW 2 DUP(0) guarda espaço para duas words em memória, dá ao endereço do inicio da primeira word o nome PRODUTO, e inicializa as duas words com o valor zero. Como acessar os dados As diferentes formas que o microprocessador utiliza para acessar aos dados chamam-se modos de endereçamento. Na linguagem Assembly o modo de endereçamento usado é indicado na própria instrução. No caso do Assembly do 8086 podemos encontrar os seguintes modos de endereçamento. ? Modo de endereçamento imediato. É possível que num programa haja necessidade de carregar um número num registro. Por exemplo, no programa exemplo.asm uma das primeiras instruções tem como objetivo carregar o número 204AH no registro AX. Para fazer isso poderíamos ter usado a instrução MOV AX, 204AH. Este modo de endereçamento chama-se modo endereçamento imediato porque o número que vai ser carregado em AX é posto nas duas posições de memória imediatamente a seguir ao código binário da instrução MOV. ? Modo de endereçamento por registros. Acontece sempre que um registro é usado como operando fonte numa dada instrução, por exemplo, na instrução MOV DS,AX; ? Modo de endereçamento direto. Como já foi referido para aceder a dados que estão em memória o 8086 necessita de especificar um endereço de 20 bits. Esse endereço é normalmente obtido a partir do DS e do endereço efetivo. O endereço efetivo é um número de 16 bits que nos dá um deslocamento em relação à base do segmento de dados. A instrução MOV AX, MULITPLICANDO é um exemplo de endereçamento direto. Esta instrução é equivalente à instrução MOV AX, [0000]. O deslocamento da variável MULTIPLICANDO em relação ao inicio do segmento de dados é 0 uma vez que esta variável é a primeira a ser declarada. Para calcularmos o endereço físico do dado que queremos somar a AX basta-nos fazer um shift de 4 bits ao DS e somar o endereço efetivo, neste caso o 0, ao resultado. ? Modo de endereçamento indireto. Acontece quando o endereço efetivo está contido num registro em vez de aparecer diretamente na instrução, por exemplo na instrução MOV AX, [BX]. Instruções para transferência de dados ? MOV A instrução MOV do 8086 tem o seguinte formato: MOV <destino>, <fonte> Quando executada, esta instrução, copia uma palavra ou um byte da localização especificada por <fonte> paraa localização especificada por <destino>. A fonte pode ser um número, um registro, ou uma posição de memória. O destino pode ser um registro ou uma posição de memória. A fonte e o destino não podem ser simultaneamente posições de memória. Operações aritméticas ? ADD A instrução ADD do 8086 tem o seguinte formato: ADD <destino> <fonte> Quando executada esta instrução soma dois números, o <destino> e a <fonte>. O resultado é colocado no <destino>. ? SUB A instrução SUB do 8086 tem o seguinte formato: SUB <destino> <fonte> Quando executada esta instrução subtrai o número especificado por <fonte> ao número especificado por <destino> e guarda o resultado em <destino>. ? MUL A instrução MUL do 8086 tem o seguinte formato: MUL <fonte> Quando executada esta instrução multiplica um número contido em AL ou em AX pelo número especificado por <fonte>. Quando o número especificado por <fonte> é um número de oito bits esse número é multiplicado pelo conteúdo de AL e o resultado é colocado no registro AX. Quando o número especificado por fonte é um número de 16 bits esse número é multiplicado pelo conteúdo de AX e o resultado (produto) é colocado no registro DX (a palavra mais significativa do produto) e em AX (a palavra menos significativa do produto). No programa exemplo.asm, a instrução MUL MULTIPLICADOR, multiplica o conteúdo do registro AX pelo número 382AH. ? DIV A instrução DIV do 8086 tem o seguinte formato: DIV <fonte> Quando executada esta instrução divide uma double word por uma word ou uma word por um byte. Se o número especificado por <fonte> for um byte, a instrução DIV divide o conteúdo do registro AX por esse número. Depois da divisão concluída, o quociente é colocado no registro AL e o resto no registro AH. Se o número especificado por <fonte> for uma word a instrução DIV forma o dividendo com os registros DX (word mais significativa do dividendo) e AX (word menos significativa do dividendo). Depois da divisão concluída, o quociente é colocado no registro AX e o resto no registro DX. Operações lógicas ? AND A instrução AND do 8086 tem o seguinte formato: AND <destino> <fonte> Quando executada esta instrução faz o e lógico bit a bit da <fonte> e do <destino>. O resultado da operação é colocado no <destino> e a <fonte> não sofre alterações. ? OR A instrução OR do 8086 tem o seguinte formato: OR <destino> <fonte> Quando executada esta instrução faz o ou lógico bit a bit da <fonte> e do <destino>. O resultado da operação é colocado no <destino> e a <fonte> não sofre alterações. ? NOT A instrução NOT do 8086 tem o seguinte formato: A NOT <destino> Quando executada esta instrução inverte todos os bits do <destino> e guarda o resultado no próprio <destino>. Outras operações ? ROL e ROR As instruções ROL e ROR do 8086 têm o seguinte formato: ROL <destino>, <count> ROR <destino>, <count> Estas instruções quando executadas rodam todos os bits de um byte ou de uma word (<destino>) um determinado número de posições para a esquerda (ROL) ou para a direita (ROR). O número de posições a rodar é dado por <count> e o resultado é colocado em destino. ? SHL e SHR As instruções SHL e SHR do 8086 têm o seguinte formato: SHL <destino>, <count> SHR <destino>, <count> Estas instruções quando executadas afastam todos os bits de byte ou de uma word (<destino>) um determinado número de posições para a esquerda (SHL) ou para a direita (SHR) inserindo zeros no lugar dos bits que foram afastados. O número de posições a afastar é dado por <count> e o resultado é colocado em destino. A diretiva END A diretiva END serve para dizer ao assembler para parar de ler o programa uma vez que ele terminou. Quaisquer instruções depois desta diretiva são ignoradas. Ferramentas necessárias para programar em Assembly Como as todas as outras linguagens também a linguagem Assembly pressupõe o domínio de algumas ferramentas. São elas: um editor de texto, um assembler, um linker e um debugger. Editor A primeira coisa a fazer é escrever o programa fonte usando um editor de texto. O formato do programa deve ser ASCII puro para que os dígitos de controle que os processadores de texto geralmente juntam ao documento não apareçam no programa. Depois de escrito o programa deve ser gravado em disco com a extensão ASM. Assembler O assembler é um programa que traduz as instruções da linguagem Assembly nos seus códigos binários correspondentes. Faz normalmente duas passagens pelo código fonte do programa. Na primeira passagem calcula os deslocamentos dos vários itens de dados e dos labels, construindo a tabela se símbolos. Na segunda passagem o assembler produz o código binário para cada instrução e insere os deslocamentos que calculou durante a primeira passagem. O assembler gera os seguintes ficheiros. ? Um ficheiro com a extensão OBJ que contém os códigos binários das instruções e informação acerca dos seus endereços. É este ficheiro que depois de ser processado pelo linker é carregado em memória para ser executado. ? O outro ficheiro produzido pelo assembler tem a extensão LST e é utilizado pelo programador para testar e diagnosticar problemas no programa. Este ficheiro contém o código Assembly e os códigos binários e deslocamentos de cada instrução. Para que o TASM (Turbo Assembler) gere este ficheiro é necessário usar o switch /l. Linker O linker é um programa que além de preparar o código objeto para ser carregado em memória e executado, pode juntar partes de código num único programa executável. O linker gera um ficheiro com a extensão EXE que contém o código máquina necessário à execução do programa. Existem dois tipos de ficheiros executáveis, os ficheiros EXE e os ficheiros COM. Os ficheiros EXE podem utilizar mais do que um segmento de memória na sua execução. Os ficheiros COM, são ficheiros de dimensões mais reduzidas que só utilizam um segmento de memória, e começam geralmente num endereço cujo deslocamento é 0100H. Os ficheiros COM podem ser obtidos a partir de ficheiros EXE utilizando para isso o programa EXE2BIN. Debugger O debugger é um programa que permite carregar um programa executável em memória, e executá-lo testando-o. É possível examinar e até modificar conteúdo dos registros e posições de memória á medida que o programa é executado. Por exemplo se se parar no fim de executar cada instrução, consegue-se analisar todas as alterações nos valores dos registros ou em posições de memória pelas quais o programa em execução é responsável. Em vez de parar no fim da execução de cada instrução podem inserir-se breakpoints nalguns pontos do programa. Nesse caso a execução só pára quando encontra um breakpoint. Para executar um programa usando o Turbo Debugger é necessário usar os switchs /zi quando usa o TASM e o switch /v quando usa o TLINK. Exercícios ? Descreva o funcionamento e o resultado de cada uma das seguintes instruções: MOX AX, BX MOV CL, 37H MOV CX, [246BH] MOV CX, 246BH MOV AX, [BX] ADD AL, DH MUL BX DIV BL SUB AX, DX OR CL, BL NOT AH ROL BX, 1 ROR BX, CL AND AL, 0FH Descubra erros nas seguintes instruções: MOX BH, AX MOV DX, CL ADD AL, 2073H MOV 7632H, CX Explique o funcionamento e o resultado dos seguintes grupos de instruções: ADD BL, AL MOV [0004], BL MOV CL, 04 ROR DI, CL MOV BX, 0004H MOV AL, [BX] SUB AL, CL INC BX MOV [BX], AL ? Escreva um programa em Assembly do 8086 que soma dois números e guarda esse resultado em memória. Utilize o programa desenvolvido para somar os números +115 e +79 e interprete o resultado.? Escreva um programa, em linguagem Assembly do 8086, que calcula a média entre dois números. ? Escreva um programa em Assembly do 8086 que converte dois dígitos decimais representados em ASCII para o código packet BCD. Linguagem Assembly do 8086 - Implementação de Expressões condicionais Introdução e Objetivos Quando temos em mão um problema de programação para resolver, o primeiro passo a dar é pensar no que queremos de fato que o programa faça e de que forma queremos que o faça. Como sabe, à seqüência de operações usadas para resolver um problema no contexto da programação chama-se algoritmo. Depois do algoritmo escrito, o próximo passo é traduzi-lo para a linguagem de programação em causa, neste caso para a linguagem Assembly do 8086. Os programas que implementamos na sessão de trabalho anterior são simples seqüências de instruções. No entanto, muitos dos algoritmos exigem funcionalidades mais complicadas, como por exemplo, escolher entre duas ou mais seqüências de instruções dependendo de alguma condição ou repetir a mesma seqüência de instruções até que uma dada condição ocorra. O objetivo desta sessão de trabalho é mostrar aos alunos como se implementam algoritmos com expressões condicionais na linguagem Assembly do 8086. Saltos, Flags e Saltos Condicionais Como já foi dito numa sessão anterior, o registro CS (Code Segment Register) contém os 16 bits mais significativos do endereço físico onde começa o segmento de código. Esse segmento contém os códigos binários das instruções que constituem o programa que está a ser executado. O registro IP (Instruction Pointer Register) referencia dentro do segmento de código qual é a próxima instrução a ser executada, ou seja, o seu valor representa o deslocamento em relação ao CS que tem a próxima instrução. Se o programa em causa for uma simples seqüência de instruções o endereço da próxima instrução a executar é sempre o imediatamente a seguir à instrução anterior. Isto já não é assim se o programa contiver saltos (jumps). As instruções JMP são usadas para indicar ao processador que a próxima instrução a executar não é a que está imediatamente a seguir mas sim outra. Existem jumps condicionais que ocorrem se uma determinada condição se verificar e jumps incondicionais que ocorrem sempre, sem estarem sujeitos a qualquer condição. O 8086 calcula o endereço físico da próxima instrução a executar adicionando o deslocamento contido no registro IP ao endereço de base representado pelo número contido no registro CS. Quando o 8086 executa a instrução JMP, carrega um novo número (deslocamento) no registro IP e por vezes também um novo número no registro CS. ? JMP A instrução JMP do Assembly do 8086 tem o seguinte formato: JMP <destino> Se o destino está num segmento de código diferente do da própria instrução então é necessário alterar não só o valor do registro IP, mas também o valor do registro CS. Neste caso, diz-se que estamos perante um far jump. Se, pelo contrário o destino está no mesmo segmento diz-se que estamos perante um near jump. Os jumps podem também ser diretos ou indiretos. São diretos se o destino for um número, número esse que referencia um deslocamento em relação à localização atual. Nesse caso o novo valor do IP ficará a ser IP+<destino>. São indiretos se o destino for um registro ou localização de memória. Nesse caso o 8086 terá de ir buscar o conteúdo do registro ou localização de memória, conteúdo esse que passará a ser o novo valor do registro IP. No caso dos jumps diretos do tipo near o destino pode ser um número de 8 bits ou de 16 bits. No caso de ser um número de 16 bits pode variar desde 32767 a -32768. No caso de ser um número de 8 bits diz-se também que é do tipo short e referencia um deslocamento que varia entre 127 e - 128 em relação à localização atual. O 8086 tem seis flags condicionais: a carry flag (CF), a parity flag (PF), a auxiliary parity flag (AF), a zero flag (ZF), a sign flag (SF) e a overflow flag (OF). As várias instruções do Assembly 8086 alteram os valores das várias flags, por exemplo, quando se somam dois números de 16 bits e o resultado não cabe em 16 bits a CF fica com o valor 1. Como já foi referido um jump condicional está sujeito a uma determinada condição. A instruções que especificam jumps condicionais analisam o conteúdo de determinadas flags para decidir se o jump se faz ou não, Todos os jumps condicionais são do tipo short, isto significa que o destino tem de especificar um endereço dentro do mesmo segmento da instrução JMP e além disso o deslocamento em relação à localização atual varia apenas de 127 a -128. Estas instruções aparecem normalmente depois de instruções de operações aritméticas e lógicas (que alteram os valores das flags), e quase sempre depois da instrução CMP. ? CMP A instrução CMP tem o seguinte formato: CMP <destino>, <fonte> Quando executada esta instrução compara o byte ou word especificada por <fonte> pelo byte ou word especificada por <destino>. A fonte pode ser um número, um registro, ou uma posição de memória. O destino pode ser um registro ou uma posição de memória. A fonte e o destino não podem ser simultaneamente posições de memória. Esta instrução é muitas vezes usada com as seguintes instruções que implementam jumps condicionais: ? JA/JNBE - Jump if Above ou Jump if Not Below or Equal ? JAE/JNB - Jump if Above or Equal ou Jump if Not Below ? JB/JNAE - Jump if Below ou Jump if Not Above or Equal ? JBE/JNA - Jump if Below or Equal ou Jump if Not Above ? JG/JNLE - Jump if Greater ou Jump if Not Less or Equal ? JAE/JNB - Jump if Greater or Equal ou Jump if Not Less ? JB/JNAE - Jump if Less ou Jump if Not Greater or Equal ? JBE/JNA - Jump if Less or Equal ou Jump if Not Greater ? JE - Jump if Equal ? JNE - Jump if Not Equal Os termos above e below utilizam-se quando queremos comparar números sem sinal. Os termos greater e less utilizam-se quando queremos comparar números com sinal. Além destas instruções, existem outras que fazem depender os jumps diretamente dos valores das flags. São elas: ? JC - Jump if Carry (CF=1) ? JNC - Jump if Not Carry (CF=0) ? JZ - Jump if Zero (ZF=1) ? JNZ - Jump if Not Zero (ZF=0) ? JO - Jump if Overflow (OF=1) ? JNO - Jump if Not Overflow (OF=0) ? JP - Jump if Parity (PF=1) ? JNP - Jump if Not Parity (PF=0) ? JS - Jump if Sign (SF=1) ? JNS - Jump if Not Sign (SF=0) Expressões Condicionais If-Then A expressão algorítmica If-Then tem o seguinte formato: if <condicao> then accao accao ... endif Esta expressão quer dizer que, se a <condição> se verificar a seqüência de ações que está entre o then e o endif deve ser executada. Na linguagem Assembly do 8086 esta expressão é implementada com um jump condicional. Por exemplo, o algoritmo: if AX=BX then AX=AX+2 CL=7 ... Seria traduzido para linguagem Assembly do 8086 da seguinte forma: CMP AX, BX JNE FIMSE ADD AX, 0002H FIMSE: ADD CL, 07H ... Outra hipótese seria: CMP AX, BX JE ENTAO JMP FIMSE ENTAO: ADD AX, 0002H FIMSE: ADD CL, 07H ... Se a seqüência de instruções entre o then e o endif contiver muitas instruções a segunda hipótese é a mais aconselhável, uma vez que, como já foi referido, os jumps condicionais são do tipo short. If-Then-Else A expressão algorítmica if-then-else tem o seguinte formato: if <condicao> then accao accao ... else accao accao ... endif Esta expressão quer dizer que, se a <condição> se verificara seqüência de ações que está entre o then e o else deve ser executada. Caso contrário a seqüência de ações que está entre o else e o endif deve ser executada. Na linguagem Assembly do 8086 esta expressão é implementada com um jump condicional e um jump incondicional. Por exemplo, o algoritmo: if AX=BX then AX=AX+2 else AX=AX-2 CL=7 ... Seria assim traduzido para linguagem Assembly: CMP AX, BX JNE SENAO ADD AX, 0002H JMP FIMSE SENAO: SUB AX, 0002H FIMSE: MOV CL, 07H ... Exercícios: ? Descubra erros nas seguintes instruções ou grupos de instruções: ? JMP BL ? JNZ [BX] ? CNTDOWN: MOV BL, 72H DEC BL JNZ CNTDOWN ? Escreva um programa em Assembly do 8086 que encontra o maior de dois números e armazena-o numa variável em memória. ? Escreva um programa em Assembly do 8086 que converta um byte (constituído por dois dígitos Hexadecimais) em dois códigos ASCII (os dois códigos ASCII correspondentes aos dois dígitos Hexadecimais). Linguagem Assembly do 8086 - Implementação de Ciclos e Arrays Introdução e Objetivos A resolução de problemas de programação passa, muitas vezes, pela repetição da mesma seqüência de ações. Existem várias expressões algorítmicas que têm como objetivo a implementação de ciclos, ou seja a repetição da mesma seqüência de instruções até que determinada condição se verifique. Exemplos de expressões que implementam ciclos são as expressões While-Do, Repeat-Until e For-To- Do. Estas expressões são particularmente úteis quando se pretende repetir a mesma operação sobre um conjunto de items de dados. O objetivo desta sessão de trabalho é mostrar aos alunos como se implementam algoritmos com ciclos na linguagem Assembly do 8086 Ciclos While-Do A expressão algorítmica While-Do tem o seguinte formato: while <condição> do accao accao ... endwhile Esta expressão quer dizer que, enquanto a <condição> se verificar, a seqüência de ações que está entre o do e o endwhile deve ser repetida. De notar que a <condição> é verificada antes de qualquer das ações ser levada a cabo. Na linguagem Assembly do 8086 esta expressão é implementada com um jump condicional e dois jumps incondicionais. Por exemplo, o algoritmo: while AX<BX do AX=AX+1 CL=7 ... Seria traduzido para linguagem Assembly do 8086 da seguinte forma: ENQUANTO: CMP AX, BX JB FAZER JMP FIMENQUANTO FAZER: INC AX JMP ENQUANTO FIMENQUANTO: ADD CL, 07H ... Repeat-Until A expressão algorítmica Repeat-Until tem o seguinte formato: repeat accao accao ... until <condição> endrepeat Esta expressão tem o mesmo significado que a expressão while-do com a diferença de que a condição é verificada apenas no fim de ser executada a seqüência de ações. Isto faz com que a seqüência de ações seja sempre executada uma vez, quer a condição se verifique, quer não. Na linguagem Assembly do 8086 esta expressão é implementada com um jump condicional. Por exemplo, o algoritmo: repeat AX=AX+1 until AX<BX endrepeat CL=7 ... Seria traduzido para linguagem Assembly do 8086 da seguinte forma: REPETIR: INC AX CMP AX, BX JB REPETIR ADD CL, 07H ... Outra hipótese seria: REPETIR: INC AX CMP AX, BX JNB FIMREPETIR JMP REPETIR FIMREPETIR: ADD CL, 07H ... Se a seqüência de instruções a repetir (seqüência de instruções que está entre o Repeat e o Until contiver muitas instruções a segunda hipótese é a mais aconselhável, uma vez que, como já foi referido, os jumps condicionais são do tipo short. For-To-Do A expressão algorítmica For-To-Do tem o seguinte formato: for <count=1> to <count=n> accao accao ... endfor Esta expressão é uma variante da expressão while-do onde se sabe à partida quantas vezes queremos executar a seqüência de ações. Por cada vez que a seqüência de ações é executada a variável count é incrementada um valor. Quando atingir o valor count o ciclo termina. Na linguagem Assembly do 8086 esta expressão é implementada com a instrução LOOP. ? LOOP A instrução LOOP do 8086 tem o seguinte formato: LOOP <label> Quando executada, esta instrução, repete uma seqüência de instruções um determinado número de vezes contido no registro CX. Cada vez que a instrução LOOP é executada o conteúdo do registro CX é decrementado um valor. Se depois de decrementado o conteúdo do registro CX for diferente de zero a instrução LOOP implementa um jump para o endereço referenciado por <label>. O <label> referencia um deslocamento em relação ao registro CS onde está a primeira instrução do ciclo. Esse valor tem de estar contido entre -128 e +127. Por exemplo, o algoritmo: for count=1 to count=10 AX=AX+1 endfor CL=7 ... Seria traduzido para linguagem Assembly do 8086 da seguinte forma: MOV CX, 0AH REPETIR: INC AX LOOP REPETIR ADD CL, 07H ... Arrays Os ciclos são especialmente úteis quando se pretende implementar a mesma operação ou a mesma seqüência de operações para um conjunto de items de dados guardados em posições contíguas de memória. Um conjunto de items de dados do mesmo tipo armazenados em posições contíguas de memória chama-se um array. Na linguagem Assembly do 8086 um array de 10 bytes podia ser declarado e inicializado da seguinte forma: DATA SEGMENT ... ARRAY DB 00H, 10H, 20H, 30H, 40H, 50H, 60H, 70H, 80H, 90H ... DATA ENDS Neste caso deu-se ao array o nome ARRAY. Quando o programa for carregado em memória para ser executado, dez bytes do segmento de dados vão ser carregados com os valores 00H, 10H, 20H, 30H, 40H, 50H, 60H, 70H, 80H e 90H, em posições contíguas de memória. Para aceder aos items de dados que constituem o array pode-se utilizar endereçamento direto ou indireto. Por exemplo, para mover para o registro AL o quinto elemento do array, utilizando o endereçamento direto, poderíamos usar a seguinte instrução: MOV AX, ARRAY+0005H Utilizando o endereçamento indireto seria necessário a seguinte seqüência de instruções: LEA BX, ARRAY+05H MOV AL, [BX] A instrução LEA é utilizada para ir buscar o endereço efetivo (deslocamento em relação ao registro de segmento) da variável ARRAY. ? LEA A instrução LEA do 8086 tem o seguinte formato: LEA <destino>, <fonte> Quando executada, esta instrução, carrega o deslocamento da posição de memória especificada por <fonte> no registro de 16 bits especificado por <destino>. Também é possível indexar os vários elementos de um array. Neste caso a seqüência de instruções seria: MOV BX, 05H MOV AL, ARRAY[BX] Aqui, o endereço efetivo do elemento do array é calculado somando o endereço do ARRAY e o conteúdo de BX. Exercícios ? Implemente um programa em Assembly do 8086 que calcule a média de 4 bytes armazenados num array em memória. ? Altere o programa anterior para que este seja capaz de calcular a média de qualquer número de bytes armazenados em memória num array, supondo que a dimensão do array está guardada na primeira posição do array. Linguagem Assembly do 8086 - Procedimentos Introdução e Objetivos Na sessão de trabalho anterior estudamos a implementaçãode ciclos usando a linguagem Assembly do 8086. Um ciclo é a repetição da mesma seqüência de ações até que determinada condição se verifique. Por vezes necessitamos de repetir a mesma seqüência de ações não no mesmo ponto, mas sim em diferentes pontos de um programa. Para evitar escrever a mesma seqüência de instruções de cada vez que necessitamos dela podemos escrevê-la como se fosse um “sub-programa” separado do programa principal e, nesse caso, sempre que precisarmos da seqüência de instruções nele contidas, invocamo- lo. Estes “sub-programas” designam-se habitualmente por procedimentos. O objetivo desta sessão de trabalho é mostrar aos alunos como se implementam programas com procedimentos usando a linguagem Assembly do 8086. Escrever e invocar procedimentos Para delimitar o conjunto de instruções que constituem um procedimento utilizam-se as diretivas PROC e ENDP. Se, em conjunto com cada uma destas diretivas usarmos um nome (label), podemos invocar o procedimento usando esse label. A diretiva PROC indica o inicio do procedimento e a diretiva ENDP o fim. Suponhamos que pretendíamos implementar um procedimento para calcular a média entre dois bytes. As fronteiras do procedimento poderão ser estabelecidas da seguinte forma: MEDIA PROC NEAR <conjunto de instruções que constituem o procedimento MEDIA> MEDIA ENDP Sempre que precisamos de executar a seqüência de instruções contida num procedimento usamos a instrução CALL. É esta instrução que indica ao processador qual o endereço da primeira instrução do procedimento. A instrução RET tem que aparecer no fim do procedimento para indicar ao processador que deve voltar ao programa chamador e executar a instrução que está imediatamente a seguir à instrução CALL. ? CALL A instrução CALL do 8086 tem o seguinte formato: CALL <destino> Quando executada, esta instrução, executa duas operações. Primeiro armazena o endereço da instrução que está imediatamente a seguir à instrução CALL. Este endereço é chamado o endereço de retorno porque é o endereço para o qual a execução volta depois do procedimento ser executado. Se a instrução CALL invoca um procedimento dentro do mesmo segmento de código então diz-se que estamos perante um call do tipo near. Nesse caso só é necessário armazenar o conteúdo do registro IP, uma vez que o valor do registro CS se mantém inalterado. Se a instrução CALL invoca um procedimento noutro segmento de código então é necessário armazenar o conteúdo do registro CS e do registro IP. A segunda operação da instrução CALL consiste em alterar o conteúdo do registro IP, e por vezes também o do registro CS, em função do <destino>. O novo par CS e IP deverá apontar para a primeira instrução do procedimento. A maior parte dos programas invoca os procedimentos usando o próprio nome (label). Neste caso diz-se que estamos perante um call direto. No entanto à semelhança do que acontece com a instrução JMP, pode haver calls diretos e indiretos e calls intra ou inter-segmento. ? RET A instrução RET do 8086 não tem operandos. Como já foi dito, quando é executada a instrução CALL, o endereço de retorno é armazenado. Caso se trate de um call do tipo near esse endereço é constituído apenas pelo valor do IP. A instrução RET, quando executada, vai buscar esse valor e carrega-o de novo no registro IP. Escrever e invocar procedimentos Para delimitar o conjunto de instruções que constituem um procedimento utilizam-se as diretivas PROC e ENDP. Se, em conjunto com cada uma destas diretivas usarmos um nome (label), podemos invocar o procedimento usando esse label. A directiva PROC indica o inicio do procedimento e a diretiva ENDP o fim. Suponhamos que pretendíamos implementar um procedimento para calcular a média entre dois bytes. As fronteiras do procedimento poderão ser estabelecidas da seguinte forma: MEDIA PROC NEAR <conjunto de instruções que constituem o procedimento MEDIA> MEDIA ENDP Sempre que precisamos de executar a seqüência de instruções contida num procedimento usamos a instrução CALL. É esta instrução que indica ao processador qual o endereço da primeira instrução do procedimento. A instrução RET tem que aparecer no fim do procedimento para indicar ao processador que deve voltar ao programa chamador e executar a instrução que está imediatamente a seguir à instrução CALL. ? CALL A instrução CALL do 8086 tem o seguinte formato: CALL <destino> Quando executada, esta instrução, executa duas operações. Primeiro armazena o endereço da instrução que está imediatamente a seguir à instrução CALL. Este endereço é chamado o endereço de retorno porque é o endereço para o qual a execução volta depois do procedimento ser executado. Se a instrução CALL invoca um procedimento dentro do mesmo segmento de código então diz-se que estamos perante um call do tipo near. Nesse caso só é necessário armazenar o conteúdo do registro IP, uma vez que o valor do registro CS se mantém inalterado. Se a instrução CALL invoca um procedimento noutro segmento de código então é necessário armazenar o conteúdo do registro CS e do registro IP. A segunda operação da instrução CALL consiste em alterar o conteúdo do registro IP, e por vezes também o do registro CS, em função do <destino>. O novo par CS e IP deverá apontar para a primeira instrução do procedimento. A maior parte dos programas invoca os procedimentos usando o próprio nome (label). Neste caso diz-se que estamos perante um call direto. No entanto à semelhança do que acontece com a instrução JMP, pode haver calls diretos e indiretos e calls intra ou inter-segmento. ? RET A instrução RET do 8086 não tem operandos. Como já foi dito, quando é executada a instrução CALL, o endereço de retorno é armazenado. Caso se trate de um call do tipo near esse endereço é constituído apenas pelo valor do IP. A instrução RET, quando executada, vai buscar esse valor e carrega-o de novo no registro IP. A stack do 8086 Na seção anterior vimos que a instrução CALL necessitava de um local para armazenar o endereço de retorno, para que no fim da execução do procedimento a instrução RET o pudesse ir buscar. Esse local é uma zona de memória de alguma forma especial designada por stack. Além de servir para armazenar o endereço de retorno a stack tem também outras funções, nomeadamente: ? Armazenar o conteúdo de registros que queremos preservar; ? Passar parâmetros para/de procedimentos O 8086 trata a stack como um segmento de memória que à semelhança dos outros tem no máximo 64kbytes. Existem dois registros específicos para trabalhar com a stack: o registro SS (Stack Segment) que à semelhança dos registros DS e CS deverá conter os 16 bits mais significativos do endereço de inicio da stack, e o SP (Stack Pointer) que contém o deslocamento em relação ao SS da última palavra escrita na stack. A posição ocupada pela última palavra escrita na stack, designa-se habitualmente por topo da stack e é para esta posição que aponta o registro SP. O SP é automaticamente decrementado dois valores antes de alguma palavra ser escrita na stack, por isso este registro deve ser inicializado com o tamanho da stack e não com zero. Guardar o endereço de retorno Já foi dito que o endereço de retorno era armazenado na stack pela instrução CALL e de lá retirado pela instrução RET. Quando a instrução CALL é executada para efetuar um call do tipo near o SP é automaticamente decrementado dois valores e o conteúdo do registro IP é copiado para a nova posição apontada por SP. Quando o processador encontra a instrução RET no fim do procedimento, copia o conteúdo da posição de memória apontada por SP para o registro IPe incrementa o SP dois valores. Por exemplo, assumindo que o valor do SS é 7000H e o valor de SP 0050H, então o endereço físico do topo da stack é 70050H. Depois de um call do tipo near o SP ficará com o valor 004EH e o endereço físico do topo da stack passaria a ser 7004EH. Quando a instrução RET for executada, o valor armazenado em stack (a palavra apontada por SP) voltava a ser colocado no registro IP e o registro SP depois de incrementado voltava ao valor 0050H. Para usarmos a stack num programa a primeira coisa a fazer é declará-la. Como já foi dito o 8086 trata a stack como outro segmento qualquer. Então para declararmos uma stack poderíamos usar a seguinte seqüência de instruções: STACK_SEG SEGMENT DW 40 DUP(0) STACK_TOP LABEL WORD STACK_SEG ENDS Esta seqüência de instruções declara uma stack com espaço para 40 words. Como as palavras são escritas na stack a partir do topo é conveniente ter um label que nos permita referenciá-lo. Como todas as instruções que escrevem palavras na stack decrementam primeiro o SP dois valores e só depois é que escrevem a palavra, convém inicializarmos o SP com o primeiro endereço par depois das 40 palavras que constituem a stack. É também necessário inicializar o registro SS para que este aponte para o inicio da stack. Para fazer- mos estas inicializações poderíamos usar as seguinte seqüência de instruções: CODE_SEG SEGMENT ASSUME: CS:CODE_SEG, SS:STACK_SEG MOV AX, STACK_SEG MOV SS, AX LEA SP, STACK_TOP .......... CODE_SEG ENDS Gravar o conteúdo dos registros É por vezes necessário usar registros dentro do procedimento que também estão a ser usados cá fora no programa chamador. Para que o uso de registros dentro dos procedimentos não interfira com a sua utilização aqui fora usa-se a stack para guardar os seus valores originais. A idéia é escrever na stack os valores dos registros que são usados no procedimento antes do procedimento começar a usá-los e consequentemente a alterá-los, e no fim do procedimento voltar a pôr tudo como estava. Para isso usam-se as instruções PUSH e POP. ? PUSH A instrução PUSH do 8086 tem o seguinte formato: PUSH <fonte> Quando executada, esta instrução, executa duas operações. Primeiro decrementa o SP dois valores e depois copia para a posição da stack apontada por SP o conteúdo de <fonte>. A fonte pode ser um registro de 16 bits ou uma palavra em memória. ? POP A instrução POP do 8086 tem o seguinte formato: POP <destino> Quando executada, esta instrução, executa duas operações. Primeiro copia para o conteúdo de <destino> a palavra em stack apontada pelo registro SP e depois incrementa SP dois valores. O destino pode ser um registro de 16 bits ou uma palavra em memória. Como a stack funciona segundo o principio last-in-first-out os pops tem que ser efetuados pela ordem inversa pela qual foram efetuados os pushs. Passagem de argumentos de/para procedimentos Muitos procedimentos atuam sobre dados ou endereços e devolvem resultados ao programa chamador. Aos dados e endereços que passamos para/de procedimentos chamam-se parâmetros. Existem várias formas possíveis de passar parâmetros para e de procedimentos. São elas: ? Através dos registros; ? Através de posições de memória; ? Através de apontadores para posições de memória contidos em registros; ? Através da própria stack Passagem de parâmetros em registros Neste caso é necessária escolher quais os registros que vão ser usados para transportar os parâmetros para o procedimento. e quais os que vão ser usados para devolver os resultados ao programa chamador. Tomando como exemplo o procedimento que calcula a média entre dois bytes, vamos necessitar de dois registros de 8 bits (por exemplo o AL e o AH) para passar os bytes para o procedimento e um registro de 8 bits (por exemplo o AL) para devolver o resultado (média) ao programa chamador. ? Implemente um procedimento que calcula a média entre dois bytes e escreva um programa que o invoca. Utilize registros para passagem de parâmetros e para retornar o resultado. Passagem de parâmetros em posições de memória É também possível pôr um procedimento a aceder a variáveis em memória através do seu próprio nome. Embora se use por vezes, este método não é o mais aconselhável uma vez que torna os procedimentos pouco flexíveis e pouco versáteis. ? Altere o programa anterior para que o procedimento vá buscar os dados diretamente à memória e guarde também diretamente em memória o resultado. Passagem de parâmetros através de apontadores contidos em registros Para ultrapassar as limitações do método anterior usa-se muitas vezes a passagem de parâmetros através de apontadores para posições de memória, apontadores esses contidos em registros. Para isso carregam-se os registros com os endereços das posições de memória que contém as variáveis que queremos manipular dentro dos procedimentos, por exemplo usando a instrução LEA. Dentro do procedimento usamos os parêntesis retos para aceder às posições apontadas pelos registros. Este método é mais versátil que o anterior pois permite que se passe para o procedimento apontadores para quaisquer posições de memória. Além disso tanto é possível passar apontadores para dados simples como para arrays ou strings. ? Altere o programa anterior de forma a que a passagem de parâmetros seja feita através de apontadores contidos em registros. Passagem de parâmetros utilizando a stack Para usar este método é necessário que algures no programa principal antes de se invocar o procedimento se utilize a instrução PUSH para escrever na stack os vários parâmetros. Dentro do procedimento as instruções vão ler os argumentos à stack de acordo com as necessidades. O mesmo se passa para retornar os resultados ao programa chamador, mas desta vez ao contrário. Neste caso são as instruções do procedimento que escrevem os resultados na stack e o programa principal que os vai lá buscar. Para usar este método é necessário fazer uma mapa da stack, para que facilmente se saiba onde é que estão os vários parâmetros e também para onde é que está a apontar, em cada instante, o registro SP. ? Altere o programa anterior de forma a que a passagem de parâmetros seja feita utilizando a stack Exercícios ? Suponha que um determinado programa precisava em vários pontos de converter bytes em packet BCD para bytes com o valor equivalente representados em código binário. ? Escreva um procedimento em linguagem Assembly do 8086 que converte um byte em BCD para o seu equivalente em binário. Como parâmetro de entrada este procedimento deverá receber o byte em BCD a converter. Como resultado este procedimento deverá retornar o byte em binário convertido. Utilize os vários métodos que conhece para efetuar a passagem de parâmetros e para retornar os resultados. ? Escreva um programa em Assembly do 8086 que invoca o procedimento implementado na alínea anterior para converter um número com 4 dígitos BCD. Referências 1 Douglas V. Hall, Microprocessors and Interfacing - Programming and Hardware, McGrawHill International Editions, 1992 2 Antônio Sampaio, HARDWARE para Profissionais, FCA - Editora de Informática, 1998
Compartilhar