A linguagem ASSEMBLY (e näo assemblER!) dá medo em muita gente!
Só näo sei porque! As liguagens ditas de "alto nível" säo MUITO
mais complexas que o assembly! O programador assembly tem que
saber, antes de mais nada, como está organizada a memória da máquina
em que trabalha, a disponibilidade de rotinas pré-definidas na ROM
do micro (que facilita muito a vida de vez em quando!) e os demais
recursos que a máquina oferece.
Uma grande desvantagem do assembly com relaçäo as outras
linguagens é que näo existe tipagem de dados como, por exemplo,
ponto-flutuante... O programador terá que desenvolver as suas
próprias rotinas ou lançar mao do co-processador matemático (o TURBO
ASSEMBLER, da Borland, fornece uma maneira de emular o
co-processador). Näo existem funçöes de entrada-saída como PRINT do
BASIC ou o Write() do PASCAL... Näo existem rotinas que imprimam
dados numéricos ou strings na tela... Enfim... näo existe nada de
útil! (Será?! hehehe)
Pra que serve o assembly entäo? A resposta é: Para que você
possa desenvolver as suas próprias rotinas, sem ter que topar com
bugs ou limitaçöes de rotinas já existentes na ROM-BIOS ou no seu
compilador "C", "PASCAL" ou qualquer outro... Cabe aqui uma
consideraçäo interessante: É muito mais produtivo usarmos uma
liguagem de alto nível juntamente com nossas rotinas em assembly...
Evita-se a "reinvençäo da roda" e näo temos que desenvolver TODAS as
rotinas necessárias para os nossos programas. Em particular, o
assembly é muito útil quando queremos criar rotinas que näo existem
na liguagem de alto-nível nativa! Uma rotina ASM bem desenvolvida
pode nos dar a vantagem da velocidade ou do tamanho mais reduzido em
nossos programas.
O primeiro passo para começar a entender alguma coisa de
assembly é entender como a CPU organiza a memória. Como no nosso
caso a idéia é entender os microprocessadores da família 80x86 da
Intel (presentes em qualquer PC-Compatível), vamos dar uma olhadela
no modelamento de memória usado pelos PCs, funcionando sob o MS-DOS
(Windows, OS/2, UNIX, etc... usam outro tipo de modelamento...
MUITO MAIS COMPLICADO!).
A memória de qualquer PC é dividida em segmentos. Cada segmento
tem 64k bytes de tamanho (65536 bytes) e por mais estranho que
pareça os segmentos näo säo organizados de forma sequencial
(o segmento seguinte näo começa logo após o anterior!). Existe uma
sobreposiçao. De uma olhada:
O segundo segmento começa exatamente 16 bytes depois do
primeiro. Deu pra perceber que o inicio do segundo segmento está
DENTRO do primeiro, já que os segmentos tem 64k de tamanho!
Este esquema biruta confunde bastante os programadores menos
experientes e, até hoje, ninguem sabe porque a Intel resolveu
utilizar essa coisa esquisita. Mas, paciência, é assim que a coisa
funciona!
Para encontrarmos um determinado byte dentro de um segmento
precisamos fornecer o OFFSET (deslocamento, em inglês) deste byte
relativo ao inicio do segmento. Assim, se queremos localizar o
décimo-quinto byte do segmento 0, basta especificar 0:15, ou seja,
segmento 0 e offset 15. Esta notaçäo é usada no restante deste e de
outros artigos.
Na realidade a CPU faz o seguinte cálculo para encontrar o
"endereço físico" ou "endereço efetivo" na memória:
Ilustrando a complexidade deste esquema de endereçamento,
podemos provar que existem diversas formas de especificarmos um
único "endereço efetivo" da memória... Por exemplo, o endereço
0:13Ah pode ser também escrito como:
Basta fazer as contas que você verá que todas estas formas daräo
o mesmo resultado: o endereço-efetivo 0013Ah. Generalizando,
existem, no máximo, 16 formas de especificarmos o mesmo endereço
físico! As únicas faixas de endereços que näo tem equivalentes e só
podem ser especificados de uma única forma säo os desesseis
primeiros bytes do segmento 0 e os últimos desesseis bytes do
segmento 0FFFFh.
Normalmente o programador näo tem que se preocupar com esse tipo
de coisa. O compilador toma conta da melhor forma de endereçamento.
Mas, como a toda regra existe uma excessäo, a informaçäo acima pode
ser útil algum dia.
+-------------------------------------------------------------------+
¦ A BASE NUMÉRICA HEXADECIMAL E BINARIA (para os novatos...) ¦
+-------------------------------------------------------------------+
Alguns talvez näo tenham conhecimento sobre as demais bases
numéricas usadas na área informata. É muito comum dizermos "código
hexadecimal", mas o que significa?
É bastante lógico que usemos o sistema decimal como base para
todos os cálculos matemáticos do dia-a-dia pelo simples fato de
temos DEZ dedos nas mäos... fica facil contar nos dedos quando
precisamos (hehe).
Computadores usam o sistema binário por um outro motimo simples:
Existem apenas dois níveis de tensäo presentes em todos os circuitos
lógicos: níveis baixo e alto (que säo chamados de 0 e 1 por
conveniência... para podermos medi-los sem ter que recorrer a um
multímetro!). O sistema hexadecimal também tem o seu lugar: é a
forma mais abreviada de escrever um conjunto de bits.
Em decimal, o número 1994, por exemplo, pode ser escrito como:
Note a base 10 nas potências. Faço agora uma pergunta: Como
representariamos o mesmo númer se tivessemos 16 dedos nas mäos?
¦ Primeiro teriamos que obter mais digitos... 0 até 9 näo säo
suficientes. Pegaremos mais 6 letras do alfabeto para suprir
esta deficiencia.
¦ Segundo, Tomemos como inspiraçäo um odômetro (equipamento
disponível em qualquer automóvel - é o medidor de
quilometragem!): Quando o algarismo mais a direita (o menos
significativo) chega a 9 e é incrementado, o que ocorre?...
Retorna a 0 e o próximo é incrementado, formando o 10. No
caso do sistema hexadecimal, isto só acontece quando o último
algarismo alcança F e é incrementado! Depois do 9 vem o A,
depois o B, depois o C, e assim por diante... até chegar a
vez do F e saltar para 0, incrementando o próximo algarismo,
certo?
Como contar em base diferente de dez é uma situaçäo näo muito
intuitiva, vejamos a regra de conversäo de bases. Começaremos pela
base decimal para a hexadecimal. Tomemos o número 1994 como
exemplo. A regra é simples: Divide-se 1994 por 16 (base
hexadecimal) até que o quoeficiente seja zero... toma-se os restos
e tem-se o númer convertido para hexadecimal:
Toma-se entäo os restos de baixo para cima, formando o número em
hexadecimal. Neste caso, 1994=7CAh
Acrescente um 'h' no fim do número para sabermos que se trata da
base 16, do contrário, se olharmos um número "7CA" poderiamos
associa-lo a qualquer outra base numérica (base octadecimal por
exemplo!)...
O processo inverso, hexa->decimal, é mais simples... basta
escrever o númer, multiplicando cada digito pela potência correta,
levando-se em conta a equivalencia das letras com a base decimal:
Cada digito na base binária é conhecido como BIT (Binary digIT -
ou digito binário, em inglês)! Note o 'b' no fim do número
convertido...
Faça o processo inverso... Converta 10100110b para decimal.
A vantagem de usarmos um número em base hexadecimal é que cada
digito hexadecimal equivale a exatamente quatro digitos binários!
Faça as contas: Quatro bits podem conter apenas 16 números (de 0 a
15), que é exatamente a quantidade de digitos na base hexadecimal.
Mais alguns conceitos säo necessários para que o pretenso
programador ASSEMBLY saiba o que está fazendo. Em eletrônica
digital estuda-se a algebra booleana e aritimética com números
binários. Aqui esses conceitos também säo importantes... Vamos
começar pela aritimética binária:
A primeira operaçäo básica - a soma - näo tem muitos
mistérios... basta recorrer ao equivalente decimal. Quando somamos
dois números decimais, efetuamos a soma de cada algarismo em
separado, prestando atençäo aos "vai um" que ocorrem entre um
algarismo e outro. Em binário fazemos o mesmo:
Ora, na base decimal, quando se soma - por exemplo - 9 e 2, fica
1 e "vai um"... Tomemos o exemplo do odômetro (aquele indicador de
quilometragem do carro!): 09 -> 10 -> 11
Enquanto na base decimal existem 10 algarismos (0 até 9), na
base binária temos 2 (0 e 1). O odômetro ficaria assim:
00b -> 01b -> 10b -> 11b
Portanto, 1b + 1b = 10b ou, ainda, 0b e "vai um".
A subtraçäo é mais complicada de entender... Na base decimal
existem os números negativos... em binário nao! (Veremos depois
como "representar" um número negativo em binário!). Assim, 1b - 1b
= 0b (lógico), 1b - 0b = 1b (outra vez, evidente!), 0b - 0b = 0b
(hehe... você deve estar achando que eu estou te sacaneando, né?),
mas e 0b - 1b = ?????
A soluçäo é a seguinte: Na base decimal quando subtraimos um
algarismo menor de outro maior costumamos "tomar um emprestado" para
que a conta fique correta. Em binário a coisa funciona do mesmo
jeito, mas se näo tivermos de onde "tomar um emprestado" devemos
indicar que foi tomado um de qualquer forma:
+---------------------------------------------------------------+
¦ 0b - 1b = ? ¦
¦ ¦
¦ 1 <- Tomamos esse um emprestado de algum lugar! ¦
¦ 0b (näo importa de onde!) ¦
¦ - 1b ¦
¦ ------ ¦
¦ 1b ¦
+---------------------------------------------------------------+
Esse "1" que apareceu por mágica é conhecido como BORROW. Em um
número binário maior basta usar o mesmo artificio:
+---------------------------------------------------------------+
¦ 1010b - 0101b = ? ¦
¦ ¦
¦ 1 1 <- Os "1"s que foram tomados emprestados säo ¦
¦ 1010b subtraídos no proximo digito. ¦
¦ - 0101b ¦
¦ --------- ¦
¦ 0101b ¦
+---------------------------------------------------------------+
Faça a conta: 0000b - 0001b, vai acontecer uma coisa
interessante! Faça a mesma conta usando um programa, ou calculadora
cientifica, que manipule números binários... O resultado vai ser
ligairamente diferente por causa da limitaçäo dos digitos suportados
pelo software (ou calculadora). Deixo a conclusäo do "por que"
desta diferença para você... (Uma dica, faça a conta com os "n"
digitos suportados pela calculadora e terá a explicaçäo!).
Um artificio da algebra booleana para representar um número
interiro negativo é usar o último bit como indicador do sinal do
número. Mas, esse artificio gera uma segunda complicaçäo...
Limitemos esse estudo ao tamanho de um byte (8 bits)... Se o
bit 7 (a contagem começa pelo bit 0 - mais a direita) for 0 o número
representado é positivo, se for 1, é negativo. Essa é a diferença
entre um "char" e um "unsigned char" na linguagem C - ou um "char" e
um "byte" em PASCAL (Note que um "unsigned char" pode variar de 0
até 255 - 00000000b até 11111111b - e um "signed char" pode variar
de -128 até 127 - exatamenta a mesma faixa, porém um tem sinal e o
outro näo!).
A complicaçäo que falei acima é com relaçäo à representaçäo dos
números negativos. Quando um número näo é nagativo, basta
convertê-lo para base decimal que você saberá qual é esse número, no
entanto, números negativos precisam ser "complementados" para que
saibamos o número que está sendo representado. A coisa NÄO funciona
da seguinte forma:
Näo basta "esquecermos" o bit 7 e lermos o restante do byte. O
procedimento correto para sabermos que número está sendo
representado negativamente no segundo exemplo é:
¦ Inverte-se todos os bits
¦ Soma-se 1 ao resultado
Com isso podemos explicar a diferença entre os extremos da faixa
de um "signed char":
¦ Os números positivos contam de 00000000b até 01111111b, isto
é, de 0 até 127.
¦ Os números negativos contam de 10000000b até 11111111b, isto
é, de -128 até -1.
Em "C" (ou PASCAL), a mesma lógica pode ser aplicada aos "int" e
"long" (ou INTEGER e LONGINT), só que a quantidade de bits será
maior ("int" tem 16 bits de tamanho e "long" tem 32).
Näo se preocupe MUITO com a representaçäo de números negativos
em binário... A CPU toma conta de tudo isso sozinha... mas, as
vezes, você tem que saber que resultado poderá ser obtido de uma
operaçäo aritimética em seus programas, ok?
As outras duas operaçöes matemáticas básicas (multiplicaçäo e
divisäo) tanbém estäo presentes nos processadores 80x86... Mas, näo
necessitamos ver como o processo é feito a nível binário. Confie na
CPU! Smile
Comecemos a dar uma olhadela na arquitetura dos
microprocessadores da família INTEL 80x86... Vamos aos
registradores!
Entenda os registradores como se fossem variáveis que o
microprocessador disponibiliza ao sistema. TODOS os registradores
têm 16 bits de tamanho e aqui vai a descriçäo deles:
+------+
¦ AX ¦<--+
+------¦ ¦
¦ BX ¦<--¦
+------¦ +- Registradores de uso geral
¦ CX ¦<--¦
+------¦ ¦
¦ DX ¦<--+
+------+
+------+
¦ SI ¦<---- índice FONTE (Source Index)
+------¦
¦ DI ¦<---- índice DESTINO (Destination Index)
+------+
+------+
¦ SP ¦<---- Apontador de pilha (Stack Pointer)
+------¦
¦ BP ¦<---- Apontador de base (Base Pointer)
+------+
+------+
¦ CS ¦<---- Segmento de Cógido (Code Segment)
+------¦
¦ DS ¦<---- Segmento de Dados (Data Segment)
+------¦
¦ ES ¦<---- Segmento de dados Extra (Extra data Segment)
+------¦
¦ SS ¦<---- Segmento de Pilha (Stack Segment)
+------+
+------+
¦ IP ¦<---- Apontador de instruçäo (Instruction Pointer)
+------+
+------+
¦Flags ¦<---- Sinalizadores
+------+
Por enquanto vamos nos deter na descriçäo dos registradores uso
geral... Eles podem ser subdivididos em dois registradore de oito
bits cada:
AH é o byte mais significativo do registrador AX, enquanto que
AL é o menos significativo. Se alterarmos o conteúdo de AL,
estaremos alterando o byte menos significativo de AX ao mesmo
tempo... Näo existem registradores de oito bits em separado...
tudo é uma coisa só. Portanto, ao manipularmos AH, estaremos
manipulando AX ao mesmo tempo!
O nome de cada registrador tem o seu sentido de ser... "A" de
AX quer dizer que este registrador é um "acumulador" (usado por
default em algumas operaçöes matematicas!), por exemplo...
AX -> Acumulador
BX -> Base
CX -> Contador
DX -> Dados
O "X" de AX significa "eXtended". "H" de AH significa "High
byte".
Embora estes registradores possam ser usados sem restriçöes, é
interessante atribuir uma funçäo para cada um deles nos nossos
programas sempre que possível... Isto facilita a leitura do código
e nos educa a seguirmos uma linha de raciocínio mais concisa...
Mas, se for de sua preferência näo seguir qualquer padräo no uso
desses registradores, näo se preocupe... näo haverá qualquer
desvantagem nisso (Well... depende do código, as vezes somos
obrigados a usar determinado registrador!).
Alguns pontos importantes quanto a esses nomes seräo observados
no decorrer do curso... Por exemplo, certas instruçöes usam AX (ou
AL, ou AH) e somente ele, näo permitindo o uso de nenhum outro
registrador... Outras, usam CX para contar, etc... essas
instruçöes específicas seräo vistas em outra oportunidade.
Os registradores SI e DI säo usados como índices para tabelas.
Em particular, SI é usado para leitura de uma tabela e DI para
escrita (fonte e destino... lembra algum procedimento de cópia,
nao?). No entanto, esses registradores podem ser usados com outras
finalidades... Podemos incluí-los no grupo de "registradores de uso
geral", mas assim como alguns registradores de uso geral, eles têm
aplicaçäo exclusiva em algumas instruçöes, SI e DI säo usados
especificamente como índices em instruçöes que manipulam blocos
(também veremos isso mais tarde!).
Os registradores CS, DS, ES e SS armazenam os segmentos onde
estäo o código (programa sendo executado), os dados, os dados
extras, e a pilha, respectivamente. Lembre-se que a memória é
segmentada em blocos de 64kbytes (dê uma olhada na primeira mensagem
dessa série).
Quando nos referimos, através de alguma instruçäo, a um endereço
de memória, estaremos nos referindo ao OFFSET dentro de um segmento.
O registrador de segmento usado para localizar o dado no offset
especificado vai depender da própria instruçäo... Um exemplo em
assembly:
O número hexadecimal entre os colchetes é a indicaçäo de um
offset em um segmento... Por default, a maioria das instruçöes usa
o segmento de dados (valor em DS). A instruçäo acima é equivalente
a:
+----------------------------------------------------------------+
¦ AL = DS:[1D4Ah] ¦
+----------------------------------------------------------------+
Isto é, em AL será colocado o byte que está armazenado no offset
1D4Ah do segmento de dados (valor em DS). Veremos mais sobre os
segmentos e as instruçöes mais tarde Smile
Se quisessemos localizar o byte desejado em outro segmento (mas
no mesmo offset) devemos especificar o registrador de segmento na
instruçäo:
O registrador IP (Instruction Pointer) é o offset do segmento de
código que contém a próxima instruçäo a ser execuatda. Este
registrador näo é acessível por qualquer instruçäo (pelo menos näo
pelas documentadas pela Intel)... é de uso interno do
microprocessador. No entanto existem alguns macetes para
conseguirmos obter o seu conteúdo (o que na maioria das aplicaçöes
näo é necessario... Para que conhecer o endereço da próxima
instruçäo se ela var ser executada de qualquer jeito?).
O registrador SP é o offset do segmento SS (segmento de pilha)
onde o próximo dado vai ser empilhado. A pilha serve para armazenar
dados que posteriormente podem ser recuperados sem que tenhamos que
usar um dos registradores para esse fim. Também é usada para
armazenar o endereço de retorno das sub-rotinas. A pilha "cresce"
de cima para baixo, isto é, SP é decrementado cada vez que um novo
dado é colocado na pilha. Note também que existe um registrador de
segmento exclusivo para a pilha... SP sempre está relacionado a esse
segmento (SS), como foi dito antes.
Para ilustrar o funcionamento da pilha, no gráfico abaixo
simularemos o empilhamento do conteúdo do registrador AX através da
instruçäo:
Observe que SP sempre aponta para um espaço vago na pilha.
Na realidade SP é decrementado de duas posiçöes ao invés de
apenas uma... mas, esse detalhe deixo para mais tarde.
O registrador BP pode ser usado como apontador para a base da
pilha (já que, por default, está relacionado a SS) ou como um
registrador de uso geral... depende do seu programa. Veremos isso
detalhadamente mais tarde.
Um dos registradores mais importantes de qualquer
microprocessador é o de "Flags". Eis uma descriçäo dos bits deste
registrador (a descriçäo abaixo aplica-se ao 8086. Normalmente näo
acessamos diretamente o registrador de flags - embora possamos
fazê-lo - por isso näo é conveniente assumirmos que os bits estäo
sempre no mesmo lugar para qualquer microprocessador da família
80x86!):
+----------------------------------------------------------------+
¦ +-------------------------------+ ¦
¦ ¦ ¦ ¦ ¦ ¦O¦D¦I¦T¦S¦Z¦ ¦A¦ ¦P¦ ¦C¦ ¦
¦ +-------------------------------+ ¦
¦ 15 0 ¦
¦ ¦
¦ C = Carry ¦
¦ P = Parity ¦
¦ A = Auxiliar Carry ¦
¦ Z = Zero ¦
¦ S = Signal ¦
¦ T = Trap ¦
¦ I = Interrupt Enable Flag ¦
¦ D = Direction ¦
¦ O = OverFlow ¦
+----------------------------------------------------------------+
¦ Carry:
Esse flag é setado sempre quando houver "vai um" depois de
uma adiçäo ou quando há BORROW depois de uma subtraçäo. Ou
quando, numa operaçäo de deslocamento de bits, o bit mais ao
extremo for deslocado para fora do dado (suponha um byte... se
todos os bits forem deslocados em uma posiçäo para a direita, o
que acontece com o bit 0?... Resposta: Vai para o carry!)
¦ Parity:
Depois de uma instruçäo aritimética ou lógica este bit
informa se o resultado tem um número par de "1"s ou näo.
¦ Auxiliar Carry:
Igual ao carry, mas indica o "vai um" no meio de um dado (no
caso de um byte, se houve "vai um" do bit 3 para o bit 4!).
¦ Zero:
Depois de uma operaçäo aritimética ou lógica, esse flag
indica se o resultado é zero ou näo.
¦ Signal:
Depois de uma instruçäo aritimética ou lógica, este flag é
uma cópia do bit de mais alta ordem do resultado, isto é, seu
sinal (dê uma olhada na "representaçäo de números negativos em
binário" no texto anterior!).
¦ Trap:
Quando setado (1) executa instruçöes passo-a-passo... Näo
nos interessa estudar esse bit por causa das diferenças de
implementaçäo deste flag em toda a família 80x86.
¦ Interrupt Enable Flag
Habilita/Desabilita o reconhecimento de interrupçöes
mascaráveis pela CPU. Sobre interrupçöes, veremos mais tarde!
¦ Direction:
Quando usamos instruçöes de manipulaçäo de blocos,
precisamos especificar a direçäo que usaremos (do inicio para o
fim ou do fim para o inicio).
Quando D=0 a direçäo é a do início para o fim... D=1, entäo
a direçäo é contrária!
¦ OverFlow:
Depois de uma instruçäo aritimética ou lógica, este bit
indica se houve mudança no bit mais significativo, ou seja, no
sinal. Por exemplo, se somarmos FFFFh + 0001h obteremos 00h. O
bit mais significativo variou de 1 para 0 (o counteúdo inicial
de um registrador era FFFFh e depois da soma foi para 0000h),
indicando que o resultado saiu da faixa (overflow) - ora, FFFFh
+ 0001h = 10000h, porém um registrador tem 16 bits de tamanho e
o resultado cabe em 17 bits. Neste exemplo, o bit de carry
também será setado pois houve "vai um" do bit 15 para o
inexistente bit 16, mas näo confunda o flag de overflow com o
carry!
Quando aos demais bits, näo se pode prever seus estados lógicos
(1 ou 0).
Na próxima mensagem começaremos a ver algumas instruçöes do
microprocessador 8086. Ainda näo escreveremos nenhum programa, a
intençäo é familiarizá-lo com a arquitetura do microprocessador
antes de começarmos a colocar a mäo na massa... tenha um pouco de
paciência! Smile
Começaremos a ver algumas instruçöes do microprocessador 8086
agora. Existem os seguintes tipos de instruçöes:
¦ Instruçöes Aritiméticas
¦ Instruçöes Lógicas
¦ Instruçöes de Controle de Fluxo de Programa
¦ Instruçöes de manipulaçäo de flags
¦ Instruçöes de manipulaçäo da pilha
¦ Instruçöes de manipulaçäo de blocos
¦ Instruçöes de manipulaçäo de registradores/dados
¦ Instruçöes de Entrada/Saída
Vamos começar com as instruçöes de manipulaçäo de
registradores/dados por serem estas as mais fáceis de entender.
MOV tem a finalidade de MOVimentar um dado de um lugar para
outro. Por exemplo, para carregar um registrador com um determinado
valor. Isto é feito com MOV:
Os registradores de segmento näo podem ser inicializados com MOV
tomando um parametro imediato (numérico). Esses registradores säo
inicializados indiretamente:
Carregar um registrador com o conteúdo (byte ou word, depende da
instruçäo!) armazenado em um segmento é simples, basta especificar o
offset do dado entre colchetes. Atençäo que o segmento de dados
(DS) é assumido por default com algumas excessöes:
A instruçäo acima, pega o byte armazenado no endereço DS:FFFFh e
coloca-o em AL. Sabemos que um byte vai ser lido do offset
especificado porque AL tem 8 bits de tamanho.
Ao invés de usarmos um offset imediato podemos usar um
registrador:
Neste caso, BX contém o offset e o byte no endereço DS:BX é
armazenado em CH. Note que o registrador usado como indice
obrigatoriamente deve ser de 16 bits.
Uma observaçäo quanto a essa modalidade: Dependendo do
registrador usado como offset, o segmento default poderá ser DS ou
SS. Se ao invés de BX usassemos BP, o segmento default seria SS e
näo DS - de uma olhada no diagrama de distribuiçäo dos registradores
no texto anterior. BP foi colocado no mesmo bloco de SP, indicando
que ambos estäo relacionados com SS (Segmento de pilha) - Eis uma
tabela das modalidades e dos segmentos default que podem ser usados
como offset:
Você pode evitar o segmento default explicitando um registrador
de segmento na instruçäo:
+----------------------------------------------------------------+
¦ MOV DH,ES:[BX] ;Usa ES ao invés de DS ¦
¦ MOV AL,CS:[SI + 4] ;Usa CS ao invés de DS ¦
+----------------------------------------------------------------+
Repare que tenho usado os registradores de 8 bits para armazenar
os dados... Pode-se usar os de 16 bits também:
+----------------------------------------------------------------+
¦ MOV ES:[BX],AX ; Poe o valor de AX para ES:BX ¦
+----------------------------------------------------------------+
Só que neste caso seräo armazenados 2 bytes no endereço ES:BX.
O primeiro byte é o menos significativo e o segundo o mais
signigicativo. Essa instruçäo equivale-se a:
+----------------------------------------------------------------+
¦ MOV ES:[BX],AL ; Instruçöess que fazem a mesma ¦
¦ MOV ES:[BX + 1],AH ;coisa que MOV ES:[BX],AX ¦
+----------------------------------------------------------------+
Repare também que näo é possível mover o conteúdo de uma posiçäo
da memória para outra, diretamente, usando MOV. Existe outra
instruçäo que faz isso: MOVSB ou MOVSW. Veremos essas instruçöes
mais tarde.
Regra geral: Um dos operandos TEM que ser um registrador! Salvo
no caso da movimentaçäo de um imediato para uma posiçäo de memória:
Para ilustrar o uso da instruçäo MOV, eis um pedaço do código
usado pela ROM-BIOS do IBM PS/2 Modelo 50Z para verificar a
integridade dos registradores da CPU:
Se o conteúdo de BP näo for 0FFFFh entäo a CPU está com algum
problema e o computador näo pode funcionar! Os flags säo testados
de uma outra forma... Smile
Se AH=1Ah e AL=6Dh, após esta instruçäo AH=6Dh e AL=1Ah por
causa da troca...
Pode-se usar uma referência à memória assim como em MOV... com
a mesma restriçäo de que um dos operandos TEM que ser um
registrador. Näo há possibilidade de usar um operando imediato.
Essas instruçöes suprem a deficiência de MOV quanto a
movimentaçäo de dados de uma posiçäo de memória para outra
diretamente. Antes de ser chamada os seguintes registradores tem
que ser inicializados:
+---------------------------------------------------------------+
¦ DS:SI <- DS e SI têm o endereço fonte ¦
¦ ES:DI <- ES e DI têm o endereço destino ¦
+---------------------------------------------------------------+
Dai podemos executar MOVSB ou MOVSW.
MOVSB move um byte, enquanto MOVSW move um word (16 bits).
Os registradores SI e DI sao incrementados ou decrementados de
acordo com o flag D (Direction) - Veja discussäo sobre os flags na
mensagem anterior. No caso de MOVSW, SI e DI serao incrementados
(ou decrementados) de 2 posiçöes de forma que DS:SI e ES:DI apontem
sempre para a próxima word.
Essas instruçöes servem para armazenar um valor que está em AX
ou AL (dependendo da instruçäo usada) no endereço apontado por
ES:DI. Entäo, antes de ser chamada, os seguintes registradores
devem ser inicializados:
+----------------------------------------------------------------+
¦ AX -> Valor a ser armazenado se usarmos STOSW ¦
¦ AL -> Valor a ser armazenado se usarmos STOSB ¦
¦ ES:DI -> Endereço onde o dado será armazenado ¦
+----------------------------------------------------------------+
Depois da execuçäo da instruçäo o registrador DI será
incrementado ou decrementado de acordo com o flag D (Direction). DI
será incrementado de 2 no case de usarmos STOSW, isto garante que
ES:DI aponte para a proxima word.
Essas instruçöes servem para ler um valor que está no endereço
apontado por DS:SI e armazená-lo em AX ou AL (dependendo da
instruçäo usada). Entäo, antes de ser chamada, os seguintes
registradores devem ser inicializados:
+----------------------------------------------------------------+
¦ DS:SI -> Endereço de onde o dado será lido ¦
+----------------------------------------------------------------+
Depois da execuçäo da instruçäo o registrador SI será
incrementado ou decrementado de acordo com o flag D (Direction). No
caso de usarmos LODSW, SI será incrementado de 2 para garantir que
DS:SI aponte para a próxima word.
LEA é, basicamente, igual a instruçäo MOV, com apenas uma
diferença: o operando "fonte" é um endereço (precisamente: um
"offset"). LEA simplesmente calcula o endereço e transfere para o
operando "destino", de forma que as instruçöes abaixo sao
equivalentes:
Repare que apenas uma instruçäo faz o serviço de três!! Nos
processadores 286 e 386 a diferença é significativa, pois, no
exemplo acima, LEA gastará 3 (nos 286) ou 2 (nos 386) ciclos de
máquina enquando o equivalente gastará 11 (nos 286) ou 6 (nos 386)
ciclos de máquina! Nos processadores 8088/8086 a diferença näo é
tao grande...
Obs:
Consideremos cada ciclo de máquina seria, aproximadamente,
num 386DX/40, algo em torno de 300ns - ou 0,0000003s. É uma
medida empirica e näo expressa a grandeza real (depende de
uma série de fatores näo considerados aqui!).
O operando "destino" é sempre um registrador. O operando
"fonte" é sempre um endereço.
¦ LDS e LES
Existe uma forma de carregar um par de registradores
(segmento:offset) de uma só vez. Se quisermos carregar DS:DX basta
usar a instruçäo LDS, caso o alvo seja ES, usa-se LES.
Suponhamos que numa posiçäo da memória tenhamos um double word
(número de 32 bits) armazenado. A word mais significativa
correspondendo a um segmento e a menos signigicativa a um offset
(esse é o caso da tabela dos vetores de interrupçäo, que descreverei
com poucos detalhes em uma outra oportunidade!). Se usamos:
+----------------------------------------------------------------+
¦ LES BX,[SI] ¦
+----------------------------------------------------------------+
O par ES:BX será carregado com o double word armazenado no
endereço apontado por DS:SI (repare no segmento default que
discutimos em um texto anterior!). A instruçäo acima é equivalente
a:
As instruçöes MOVSB, MOVSW, STOSB, STOSW, LODSB e LODSW podem
ser usadas para lidar com blocos de dados. Para isto, basta indicar
no registrador CX a quantidade de dados a serem manipulados e
acrescentar REP na frente da instruçao. Eis um trecho de uma
pequena rotina que apaga o video em modo texto (80 x 25 colorido):
+---------------------------------------------------------------+
¦ MOV AX,0B800h ¦
¦ MOD ES,AX ; Poe em ES o segmento do vídeo ¦
¦ MOV DI,0 ; Começa no Offset 0 ¦
¦ MOV AH,7 ; AH = atributo do caracter ¦
¦ ; 7 = cinza com fundo preto ¦
¦ MOV AL,' ' ; AL = caracter usado para apagar ¦
¦ MOV CX,2000 ; CX = contador (4000 bytes ou ¦
¦ ; 2000 words). ¦
¦ REP STOSW ; Preenche os 2000 words com AX ¦
+---------------------------------------------------------------+
O modificador REP diz a instruçäo que esta deve ser executada CX
vezes. Note que a cada execuçäo de STOSW o registrador DI apontará
para a proxima word.
Suponha que queiramos mover 4000 bytes de alguma posiçäo da
memória para o video, preenchendo a tela com esses 4000 bytes:
+---------------------------------------------------------------+
¦ MOV AX,0B800h ¦
¦ MOD ES,AX ; Poe em ES o segmento do vídeo ¦
¦ MOV AX,SEG TABELA ¦
¦ MOV DS,AX ; Poe em DS o segmento da tabela ¦
¦ MOV SI,OFFSET TABELA ; Começa no offset inicial da tabela ¦
¦ MOV DI,0 ; Começa no Offset 0 ¦
¦ MOV CX,4000 ; CX = contador (4000 bytes) ¦
¦ REP MOVSB ; Copia 4000 bytes de DS:SI para ES:DI ¦
+---------------------------------------------------------------+
Nota: O modificador REP só pode ser preceder as seguintes
instruçöes: MOVSB, MOVSW, STOSB, STOSW, LODSB, LODSW, CMPSB, CMPSW,
SCASB, SCASW, OUTSB, OUTSW, INSB, INSW. As instruçöes nao vistas no
texto acima seräo detalhadas mais tarde...
Existem mais algumas instruçöes de manipulaçäo de
registradores/dados, bem como mais algumas de manipulaçäo de blocos.
Que ficaräo para uma próxima mensagem.
Depois de algumas instruçöes de movimentaçäo de dados vou
mostrar a mecânica da lógica booleana, bem como algumas instruçöes.
A lógica booleana baseia-se nas seguintes operaçöes: AND, OR,
NOT. Para simplificar a minha digitaçäo vou usar a notaçäo
simplificada: & (AND), | (OR) e ~ (NOT). Essa notaçäo é usada na
linguagem C e em muitos manuais relacionados a hardware da IBM.
¦ Operaçäo AND:
A operaçäo AND funciona de acordo com a seguinte tabela-verdade:
+-----------+
¦ S = A & B ¦
ã---Ð---Ð---Á
¦ A ¦ B ¦ S ¦
+---+---+---¦
¦ 0 ¦ 0 ¦ 0 ¦
¦ 0 ¦ 1 ¦ 0 ¦
¦ 1 ¦ 0 ¦ 0 ¦
¦ 1 ¦ 1 ¦ 1 ¦
+-----------+
Note que o resultado (S) será 1 apenas se A "E" B forem 1.
Aplicando esta lógica bit a bit em operaçöes envolvendo dois
bytes obteremos um terceiro byte que será o primeiro AND o segundo:
+----------------------------------------------------------------+
¦ A = 01010111b B = 00001111b ¦
¦ ¦
¦ S = A & B -> 01010111b ¦
¦ & 00001111b ¦
¦ ------------- ¦
¦ 00000111b ¦
+----------------------------------------------------------------+
Uma das utilidades de AND é resetar um determinado bit sem
afetar os demais. Suponha que queira resetar o bit 3 de um
determinado byte. Para tanto basta efetuar um AND do byte a ser
trabalhado com o valor 11110111b (Apenas o bit 3 resetado).
Eis a sintaxe da instruçäo AND:
+---------------------------------------------------------------+
¦ AND AL,11110111b ¦
¦ AND BX,8000h ¦
¦ AND DL,CL ¦
¦ AND [DI],AH ¦
+---------------------------------------------------------------+
Lembrando que o operando destino (o mais a esquerda) deve sempre
ser um registrador ou uma referencia a memória. o operando a
direita (fonte) pode ser um registrador, uma referência a memória ou
um valor imediato, com a restriçäo de que näo podemos usar
referências a memória nos dois operandos.
A instruçäo AND afeta os FLAGS Z, S e P e zera os flags Cy
(Carry) e O (veja os flags em alguma mensagem anterior a esta).
¦ Operaçäo OR:
+-----------+
¦ S = A | B ¦
ã---Ð---Ð---Á
¦ A ¦ B ¦ S ¦
+---+---+---¦
¦ 0 ¦ 0 ¦ 0 ¦
¦ 0 ¦ 1 ¦ 1 ¦
¦ 1 ¦ 0 ¦ 1 ¦
¦ 1 ¦ 1 ¦ 1 ¦
+-----------+
Note que S será 1 se A "OU" B forem 1.
Da mesma forma que AND, aplicamos essa lógica bit a bit
envolvendo um byte ou word através de uma instruçäo em assembly.
Vejamos um exemplo da utilidade de OR:
+----------------------------------------------------------------+
¦ A = 01010111b B = 10000000b ¦
¦ ¦
¦ S = A | B -> 01010111b ¦
¦ | 10000000b ¦
¦ ------------- ¦
¦ 11010111b ¦
+----------------------------------------------------------------+
A operaçäo OR é ideal para setarmos um determinado bit sem
afetar os demais. No exemplo acima B tem apenas o bit 7 setado...
depois da operaçäo OR com A o resultado final foi A com o bit 7
setado! Smile
A sintaxe de OR é a mesma que a de AND (obviamente trocando-se
AND por OR). Os flags afetados säo os mesmos da instruçäo AND!
¦ Operaçäo NOT:
NOT simplesmente inverte todos os bits de um byte ou word:
+-----------+
¦ S = ~A ¦
ã-----Ð-----Á
¦ A ¦ S ¦
+-----+-----¦
¦ 0 ¦ 1 ¦
¦ 1 ¦ 0 ¦
+-----------+
A sintaxe da instruçäo em assembly é a seguinte:
+---------------------------------------------------------------+
¦ NOT AL ¦
¦ NOT DX ¦
¦ NOT [SI] ¦
+---------------------------------------------------------------+
¦ Operaçäo XOR:
A operaçäo XOR é derivada das três acima. A equaçäo booleana
que descreve XOR é:
+----------------------------------------------------------------+
¦ S = (A AND ~B) OR (~A AND B) = A ^ B ¦
+----------------------------------------------------------------+
Que na tabela-verdade fica:
+-----------+
¦ S = A ^ B ¦
ã---Ð---Ð---Á
¦ A ¦ B ¦ S ¦
+---+---+---¦
¦ 0 ¦ 0 ¦ 0 ¦
¦ 0 ¦ 1 ¦ 1 ¦
¦ 1 ¦ 0 ¦ 1 ¦
¦ 1 ¦ 1 ¦ 0 ¦
+-----------+
Uso o simbolo ^ para o XOR aqui. XOR funciona da mesma forma
que OR, só que o resultado será 1 se APENAS A ou APENAS B for 1,
melhor dizendo: Se ambos forem diferentes.
XOR é muito útil quando se quer inverter um determinado bit de
um byte ou word sem afetar os outros:
+----------------------------------------------------------------+
¦ A = 01010111b B = 00001111b ¦
¦ ¦
¦ S = A ^ B -> 01010111b ¦
¦ ^ 00001111b ¦
¦ ------------- ¦
¦ 01011000b ¦
+----------------------------------------------------------------+
No exemplo acima invertemos apenas os quatro bits menos
significativos de A.
A sintaxe e os flags afetados säo os mesmos que AND e OR.
Instruçöes aritiméticas säo o tópico de hoje. Já discuti,
brevemente, os flags e os sistemas de numeraçäo. Aqui vai uma
aplicaçäo prática:
¦ Soma:
A soma é feita através das instruçöes ADD e ADC. A diferença
entre elas é que uma faz a soma normalmente e a outra faz a mesma
coisa acrecentando o conteúdo do flag CARRY. Eis a sintaxe:
As duas primeiras instruçöes fazem exatamente a mesma coisa que
a terceira. Note que na primeiria somamos AL com 10h e o resultado
ficará em AL (se ocorrer "vai um" nesta soma o flag CARRY será
setado). A segunda instruçäo soma AH com 22h MAIS o carry
resultante da primeira instruçäo e o resultado ficará em AH
(novamente setando o flag carry se houver outro "vai um"!). A
terceira instruçäo faz a mesma coisa porque soma 2210h a AX, ficando
o resultado em AX e o possível "vai um" no carry.
Todos os flags säo afetados após a execuçäo de uma das
instruçöes de soma, exceto: I, D e Trap.
¦ Subtraçäo
Semelhante as instruçöes de soma, existem duas instruçöes de
subtraçäo: SUB e SBB. A primeira faz a subtraçäo simples e a
segunda faz a mesma coisa subtraindo também o conteúdo prévio do
flag CARRY (como é uma subtraçäo o CARRY é conhecido como BORROW!).
A sintaxe:
+-----------------------------------------------------------------+
¦ SUB AL,1 ¦
¦ SBB AH,0 ¦
¦ ¦
¦ SUB AX,1 ¦
+-----------------------------------------------------------------+
Como no exemplo anterior, as duas primeiras instruçöes fazem
exatamente o que a terceira faz... Os flags afetados seguem a mesma
regra das instruçöes de soma!
¦ Incremento e decremento:
As instruçöes INC e DEC säo usadas no lugar de ADD e SUB se
quisermos incrementar ou decrementar o conteúdo de algum registrador
(ou de uma posiçäo de memória) de uma unidade. A sintaxe é simples:
+----------------------------------------------------------------+
¦ DEC AX ¦
¦ INC BL ¦
+----------------------------------------------------------------+
Os flags afetados seguem a mesma regra de uma instruçäo de soma
ou uma de subtraçäo!
¦ Multiplicaçäo:
Os processadores da família 80x86 possuem instruçöes de
multiplicaçäo e divisäo inteiras (ponto flutuante fica pro 8087).
Alguns cuidados devem ser tomados quando usarmos uma instruçäo de
divisäo (que será vista mais adiante!).
Uma coisa interessante com a multiplicaçäo é que se
multiplicarmos dois registradores de 16 bits obteremos o resultado
necessariamente em 32 bits. O par de registradores DX e AX säo
usados para armazenar esse número de 32 bits da seguinte forma: DX
será a word mais significativa e AX a menos significativa.
Por exemplo, se multiplicarmos 0FFFFh por 0FFFFh obteremos:
0FFFE0001h (DX = 0FFFEh e AX = 0001h).
Eis a regra para descobrir o tamanho do restultado de uma
operaçäo de multiplicaçäo:
+---------------------------------+
¦ A * B = M ¦
+---------------------------------¦
¦ A ¦ B ¦ M ¦
+----------+-----------+----------¦
¦ 8 bits ¦ 8 bits ¦ 16 bits ¦
¦ ¦ ¦ ¦
¦ 16 bits ¦ 16 bits ¦ 32 bits ¦
+---------------------------------+
A multiplicaçäo sempre ocorrerá entre o acumulador (AL ou AX) e
um outro operando. Eis a sintaxe das instruçöes:
A primeira instruçäo (MUL) näo considera o sinal dos operandos.
Neste caso, como BL é de 8 bits, a multiplicaçäo se dará entre BL e
AL e o resultado será armazenado em AX.
A segunda instruçäo leva em consideraçäo o sinal dos operandos
e, como CX é de 16 bits, a multiplicaçäo se dará entre CX e AX e o
restultado será armazenado em DX e AX. Lembrando que o sinal de um
número inteiro depende do seu bit mais significativo!
¦ Divisäo:
Precisamos tomar cuidado com a divisäo pelo seguinte motivo: Se
o resultado näo couber no registrador destino, um erro de "Division
by zero" ocorrerá (isto näo está perfeitamente documentado nos
diversos manuais que li enquanto estudava assembly 80x86... Vim a
descobrir este 'macete' numa antiga ediçäo da revista PC MAGAZINE
americana). Outro cuidado é com o divisor... se for 0 o mesmo erro
ocorrerá!
A divisäo pode ser feita entre um número de 32 bits e um de 16
ou entre um de 16 e um de 8, veja a tabela:
+--------------------------------+
¦ A / B = Q e resto ¦
+--------------------------------¦
¦ A ¦ B ¦Q e resto¦
+----------+-----------+---------¦
¦ 32 bits ¦ 16 bits ¦ 16 bits ¦
¦ ¦ ¦ ¦
¦ 16 bits ¦ 8 bits ¦ 8 bits ¦
+--------------------------------+
Assim como na multiplicaçäo o número (dividendo) de 32 bits é
armazenado em DX e AX.
Depois da divisäo o quociente é armazenado em AL e o resto em AH
(no caso de divisäo 16/8 bits) ou o quociente fica em AX e o resto
em DX (no caso de divisäo 32/8 bits).
Você não pode enviar mensagens novas neste fórum Você não pode responder mensagens neste fórum Você não pode editar suas mensagens neste fórum Você não pode excluir suas mensagens neste fórum Você não pode votar em enquetes neste fórum