|
Table of Content | Chapter Six (Part 3) |
O 80x86 provê muitas operações aritméticas: adição, subtração, negação, multiplicação, divisão/módulo (resto), e comparaçaõ de dois valores. As instruções que manuseiam essas operações são add, adc, sub, sbb, mul, imul, div, idiv, cmp, neg, inc, dec, xadd, cmpxchg e algumas instruções de conversões mistas: aaa, aad, aam, aas, daa e das. As seções seguintes descrevem essas instruções em detalhes.
As formas genéricas dessas instruções são
add dest, src dest := dest + src adc dest, src dest := dest + src + C SUB dest, src dest := dest - src sbb dest, src dest := dest - src - C mul src acc := acc * src imul src acc := acc * src imul dest, src1, imm_src dest := src1 * imm_src imul dest, imm_src dest := dest * imm_src imul dest, src dest := dest * src div src acc := xacc /-mod src idiv src acc := xacc /-mod src cmp dest, src dest - src (e seta os flags) neg dest dest := - dest inc dest dest := dest + 1 dec dest dest := dest - 1 xadd dest, src (veja texto) cmpxchg operando1, operando2 (veja texto) cmpxchg8 ax, operando (veja texto) aaa (veja texto) aad (veja texto) aam (veja texto) aas (veja texto) daa (veja texto) das (veja texto)
6.5.1 As Instruções de Adição: ADD, ADC, INC, XADD, AAA, e DAA
Essas instruções tomam as formas:
add reg, reg add reg, mem add mem, reg add reg, dado imediato add mem, dado imediato add eax/ax/al, dado imediato as formas de adc são idênticas às de ADD. inc reg inc mem inc reg16 xadd mem, reg xadd reg, reg aaa daa
Note que aaa e daa usam o modo de endereçamento implícito e não permitem operandos.
6.5.1.1 As instruções ADD e ADC
A sintaxe de add e adc é similar à deo mov. Como mov, há formas especiais para o registrador ax/eax que são mais eficientes. Diferentemente do mov, você não pode adicionar um valor a um registrador de segmentos com essas instruções.
A instrução add soma o conteúdo do operando fonte ao operando destino. Por exemplo, "add ax, bx" adiciona bx a ax deixando a soma no registrador ax. ADD computa destino:=destino+fonte enqanto ADC computa destino:=destino+fonte+C, onde C representa o valor no flag de vai-um (carry). Portanto, se o flag de carry estiver zerado antes da execução, ADC se comporta exatamente como o ADD.
Ambas as instruções afetam os flags idênticamente. Elas setam os flags como se segue:
As instruções ADD e ADC não afetam outros flags.
As instruções ADD e ADC permitem operandos de 8, 16 e (no 80386 em diante) 32 bits. Tanto o operando fonte quanto o destino devem ser do mesmo tamanho. Veja o Capítulo Nove se você quiser adicionar operandos cujo tamanho seja diferente.
Já que não existe adição de memória para memória, você tem que caregar operandos de memória para registradores se quiser adicionar duas variáveis. O seguinte código exemplifica as possíveis formar da instrução add:
; J:= K + M mov ax, K add ax, M mov J, ax
Se você quiser adicionar váris valores, você pode facilmente calcular a soma em único registrador:
; J := K + M + N + P mov ax, K add ax, M add ax, N add ax, P mov J, ax
Se você quiser reduzir o número de perigos num processador 486 ou Pentium, você pode usar código como o seguinte:
mov bx, K mov ax, M add bx, N add ax, P add ax, bx mov J, ax
Uma coisa que programadores principiantes em assembly freqüentemente se esquecem é que você pode somar um registrador a uma posição na memória. Às vezes progamadores principiantes até acreditam que ambos os operadores precisam ser em registradores, esquecendo completamente as lições do Capítulo Quatro. O processador 80x86 é um processador CISC que lhe permite usar os modos de endereçamento de memória com várias instruções, como o ADD. É geralmente mais eficiente usar as vantagens das capacidades de endereçamento de memória do 80x86.
; J := K + J mov ax, K ; Isso funciona porque a adição é add J, ax ; comutativa! ; Freqüentemente, os principiantes vão codificar isso aí acima como uma das seguintes seqüências. ; Isso é desnecesário! mov ax, J ; Modo realmente RUIM de calcular mov bx, K ; J := J + K. add ax, bx mov J, ax mov ax, J ; Melhor, mas ainda nã é um bom modo de add ax, K ; calcular J := J + K mov J, ax
É claro, se você quiser somar uma constante a uma posição de memória, você so precisa de uma instrução simples. O 80x86 deixa você adicionar diretamente uma contante para a memória:
; J := J + 2 add J, 2
Há formas especiais de ADD e ADc que adicionam uma constante imediata a AL, AX ou EAX. Essas formas são mais curtas que a instrução padrão ADD reg, valor imediato. Outras instruções também provêem formas mais curtas quando se usam esses registradores; portanto, você deveria tentar fazer seus cálculos com os registradores o acumulador (al, ax e eax) sempre que possível.
add bl, 2 ;Ocupa três bytes add al, 2 ;Dois bytes add bx, 2 ;Quatro bytes add ax, 2 ;Três bytes etc.
Outra otimização afeta o uso de contantes pequenas com sinal com as instruções ADD e ADC. Se um valor está na escala de -128..+127, as instruções ADD e ADC vão fazer uma extensão de sinal numa constante de 8 bits imediata ao tamanho necessário para o destino (8, 16 ou 32 bits). Portanto, você deveria tentar usar contantes pequenas, se possível, com ADD e ADC.
A instrução INC adiciona 1 ao seu operando. Exceto pelo flag de carry, INC seta os flags do mesmo jeito que "ADD operando, 1" setaria.
Note que há duas formas de INC para 16 e 32 bits. São os "INC reg" e "INC reg16". O "inc reg" e o "inc mem" são a mesma coisa. Essa instrução consiste de um byte da operação seguido por um byte mod-reg-r/m (veja o Apêndice D para detalhes). A instrução INC reg16 têm só um byte de operação (opcode). Sendo assim, é mais curta e geralmente mais rápida.
O operador do INC pode ser um registrador ou posição de memória de 8, 16 ou (a partir do 80386) 32 bits.
A instrução INC é mais compacta e freqüentemente mais rápida que o comparável ADD reg, 1 ou ADD mem, 1. De fato, a instrução INC reg16 é um byte maior, então acontece que duas instruções dessas são mais curtas que o comparável ADD reg,1; contudo, as duas instruções de incremento vão rodar mais lentos na maioria dos membros modernos da família 80x86.
A instrução inc é muito importante porque adicionar 1 a um registro é uma operação muito comum. Incrementar variáveis de controle de loop ou índices em um array são operações muito comuns, perfeitas para o INC. O fato que INC não afeta o flag de carry é muito importante. Isso permite a você incrementar índices de arrays sem afetar o resultado de uma operação aritmética de multiprecisão (veja o Capítulo Nove para mais detalhes sobre aritmética multiprecisão).
Xadd
(Exchange e Add) é outra instrução 80486 (e superior).
Ela não aparece no processador 80386 e mais antigos. Esta instrução
adiciona o operando fonte ao destino e armazena a soma no destino. Contudo,
antes de armazenar a soma, ele copia o valor original do destino dentro do operando
de fonte. O seguinte algoritmo descreve essa operação:
xadd dest, source temp := dest dest := dest + source source := temp
O XADD seta os flags do mesmo modo que o ADD faria. O xadd permite operandos de 8, 16 e 32 bits. Tanto o fonte quanto o destino devem ter o mesmo tamanho.
6.5.1.4 As Instruções AAA e DAA
O AAA (Ajuste ASCII após adição) e o DAA (Ajuste decimal para Adição) suportam aritmética BCD. Além desse capítulo, esse texto não vai cobrir aritmética BCD ou ASCII, já que ele é principalmente para aplicações de controle, não para aplicações de uso geral. Valores BCD são inteiros decimais codificados de forma binária com um dígito decimal (0..9) por nibble. Valores ASCII (numéricos) contêm um dígito decimal por byte, o nybble de ordem superior deve conter zero.
As instruções AAA e DAA modificam o resultado de uma adição binária para corrigi-lo para aritmética ASCII ou decimal. Por exemplo, para adicionar dois valores BCD, você os adicionaria como se eles fosse dois números binários, e executaria DAA depois, para corrigir o resultado. Do mesmo modo, você pode usar AAA para ajustar o resultado de uma adição ASCII após usar o ADD. Por favor, note que essas duas instruções assumem que os operandos no ADD eram valores corretos ASCII ou decimais. Se você adicionar valores binários (não-decimais ou não-ASCII) e tentar ajustá-los com essas instruções, você não vai produzir resultados certos.
A escolha do nome "aritmética ASCII" é infeliz, já que esses valores não são caracteres ASCII verdadeiros. Um nome como "BCD não-empacotado" seria mais apropriado. Contudo, a Intel usao o nome ASCII, então este texto vai fazer o mesmo para evitar confusão. Porém, você vai ouvir freqüentemente o termo "BCD não-empacotado" para descrever esse tipo de dados.
AAA (que você geralmente executa após um add, adc ou xadd) verifica se há overflow BCD no valor em AL. Funciona de acordo com o seguinte algoritmo:
if ( (al and 0Fh) > 9 or (AuxC =1) ) then if (8088 or 8086) then al := al + 6 else ax := ax + 6 endif ah := ah + 1 AuxC := 1 ; Seta os flags de carry Carry := 1 ; e carry auxiliar. else AuxC := 0 ; Limpa os flags de carry Carry := 0 ; e carry auxiliar. endif al := al and 0Fh
A instrução AAA é principalmente útil para adicionar strings de dígitos onde há exatamente um dígito decimal por byte na string de números. Esse texto não vai lidar com números BCD ou ASCII, então você pode ignorar seguramente essa instrução agora. É claro, você pode usar AAA a qualquer tempo que precisar de usar o algoritmo acima, mas aquilo seria uma situação rara.
A instrução DAA funciona como a AAA, exceto que ela manuseia valores BCD (binary code decima) empacotados ao invés de valores com um dígito por byte não empacotados que AAA manuseia. Como no AAA, o propósito do DAA é somar strings de dígitos BCD (com dois dígitos por byte). O algoritmo para o DAA é:
if ( (AL and 0Fh) > 9 or (AuxC = 1)) then al := al + 6 AuxC := 1 ; Seta o flag de carry auxiliar. endif if ( (al > 9Fh) or (Carry = 1)) then al := al + 60h Carry := 1; ; Seta o flag de carry. endif
6.5.2 As Instruções de Subtração: SUB, SBB, DEC, AAS, e DAS
As instruções SUB (subtrair), SBB (subtrair com "pede-emprestado"), DEC (decrementar), AAS (Ajuste ASCII para Subtração), e DAS (Ajuste Decimal para Subtração) trabalham como você imagina. A sintaxe é muito similar às das instruções de adição:
sub reg, reg sub reg, mem sub mem, reg sub reg, dado imediato sub mem, dado imediato sub eax/ax/al, dado imediato as formas de sbb são idênticas às de sub. dec reg dec mem dec reg16 aas das
A instrução SUB computa o valor dest := dest - fonte. O SBB calcula dest : dest - fonte - C. Note que a subtração não é comutativa. Se você quiser calcular o resultado para dest := dest - fonte, vai precisar de usar muitas instruções (asumindo que você queira preservar o operando fonte).
Um último assunto que vale a pena discutir é como o SUB afeta os flags do 80x86. Os sub, sbb e dec afetam o flag desse modo:
A instrução AAS, bem como sua contraparte AAA, deixa você operar em strings de números ASCII com um dígito decimal (na escala de 0..9) por byte. Você usaria essa instrução após um SUB no valor ASCII. Essa instrução usa o seguinte algoritmo:
if ( (al and 0Fh) > 9 or AuxC = 1) then al := al - 6 ah := ah - 1 AuxC := 1 ; Seta o flag de carry auxiliar Carry := 1 ; e o de carry. else AuxC := 0 ; Limpa o flag de carry auxiliar Carry := 0 ; e o de carry. endif al := al and 0Fh
A instrução DAS manuseia a mesma operação para valores BCD, ela usa o seguinte algoritmo:
if ( (al and 0Fh) > 9 or (AuxC = 1)) then al := al -6 AuxC = 1 endif if (al > 9Fh or Carry = 1) then al := al - 60h Carry := 1 ; Seta o carry de flag. endif
Já que a subtração não é comutativa, você não pode usar o SUB tão livremente quanto o ADD. Os seguintes exemplos demonstram alguns problemas que você pode encontrar.
; J := K - J mov ax, K ; Esta é uma boa tentativa, mas ela calcula sub J, ax ; J := J - K, a subtração não é ; comutativa! mov ax, K ; Solução correta. sub ax, J mov J, ax ; J := J - (K + M) -- Não se esqueça que isso é equivalente a J := J - K - M mov ax, K ; Computa AX := K + M add ax, M sub J, ax ; Computa J := J - (K + M) mov ax, J ; Outra solução, só que sub ax, K ; menos eficiente sub ax, M mov J, ax
Note que as instruções sub e sbb, assim como as add e adc, provêem formas curtas para subtrair uma constante de um registrador acumulador (al, ax ou eax). Por essa razão, você deveria tentar fazer operações aritméticas nos acumuladores tanto quanto possível. As instruções sub e sbb também provêem uma forma mais curta quando se subtrai valores entre -127 e +128 de uma posição de memória ou de um registrador. A instrução vai automaticamente estender o sinal de um valor de 8 bits para o tamanho necessário antes de executar a subtração. Veja o Apêndice D para mais detalhes.
Na prática, não há necessidade para uma instrução que subtrai uma constante de um registrador ou de uma posição de memória - adicionar um valor negativo dá no mesmo resultado. Mesmo assim, a Intel provê uma instrução para subtração imediata.
Após a execução de um SUB, os bits de condição (carry, sinal, overflow e zero) no registrador de flags contêm valores que você pode testar para ver se um dos operandos do SUB é igual, diferente, menor que, menor ou igual, maior, ou maior ou igual que o outro operando. Veja a instrução CMP para mais detalhes.
A instrução CMP (compare) é idêntica a um SUB, com uma diferença crucial - ela não armazena a diferença no operando destino. A sintaxe para a CMP é muito similar ao SUB, a forma genérica é
cmp dest, src
As formas específicas são
cmp reg, reg cmp reg, mem cmp mem, reg cmp reg, dado imediato cmp mem, dado imediato cmp eax/ax/al, dado imediato
A CMP atualiza os flags do 80x86 de acordo com o resultado da subtração (destino - fonte). Você pode testar o resultado da comparação checando os flags apropriados no registrador de flags. Para detalhes sobre como isso é feito, veja "As Instruções de Set para Condição" e "As Instruções de Jump Condicionais".
Geralmente você vai querer executar uma instrução de jump condicional depois de um CMP. Esse processo de dois passos, comparar dois valores e setar os bits de flag e então testar os flags com as instruções de jump condicional, é um mecanismo muito eficiente para fazer decisões em um programa.
Provavelmente o primeiro lugar para se começar quando explorar a instrução CMP é dar uma olhada em como a instrução afeta exatamente os flags. Considere a seguinte instrução CMP:
cmp ax, bx
Essa instrução realiza o cálculo AX-BX e seta os flags dependendo do resultado da conta. Os flags são setados de acordo como se segue:
Z: O flag de zero é setado se e somente se AX=BX. Esse é o único momento em que AX-BX produz zero no resultado. Daí, você pode usar o flag de zero para testar igualdade ou desigualdade.
S: O flag de sinal é setado se o resultado for negativo. À primeira vista, você poderia pensar que esse flag seria setado se AX fosse menor que BX, mas isso não é sempre o caso. Se AX=7FFFh e BX=-1(0FFFFh), subtrair Ax de BX produz 8000h, que é negativo (e então o flag de sinal será setado). Então, para comparações com sinal, de qualquer modo, o flag de sinal não contém o status certo. para operandos sem sinal, considerando AX=0FFFh e BX=1, AX é maior que BX, mas sua diferença é 0FFFEh, que ainda é negativo. Assim, o flag de sinal e o de overflow, juntos, podem ser usados para comparar dois valores com sinal.
O: O flag de overflow é setado após um CMP se a diferença de Ax e BX produzir um overflow ou um underflow. Como mencionado acima, o flag de sinal e o de overflow são ambos usados quando se realiza comparações com sinal.
C: O flag de carry é setado após um CMP se a subtraindo BX de AX requer um "pede-emprestado". Isso só ocorre se Ax for menor que BX, quando AX e BX são valores sem sinal.
O CMP também afeta os flags de paridade e carry auxiliar, mas você raramente vai testá-los depois de uma comparação. Dado que o CMP seta os flags desse modo, você pode testar a comparação dos dois operandos com os seguintes flags:
cmp Oprnd1, Oprnd2
Operandos sem sinal: | Operandos com sinal: |
---|---|
Z: igualdade/desigualdade | Z: igualdade/desigualdade |
C: Oprnd1 < Oprnd2 (C=1), Oprnd1 >= Oprnd2 (C=0) | C: sem siginificado |
S: sem siginificado | S: veja abaixo |
O: sem siginificado | O: veja abaixo |
Para comparações com sinal, os flags S (sinal) e O (overflow), tomados juntos, têm o seguinte significado:
Se ((S=0) e (O=1)) ou ((S=1) e (O=0)) então Oprnd1 < Oprnd2 quando se usa uma comparação com sinal.
Se ((S=0) e (O=0)) ou ((S=1) e (O=1)) então Oprnd1 >= Oprnd2 quando se usa uma comparação com sinal.
Para entender por que esses flags são setados dessa maneira, considere os seguintes exemplos:
Oprnd1 menos Oprnd2 S O ------ ------ - - 0FFFF (-1) - 0FFFE (-2) 0 0 08000 - 00001 0 1 0FFFE (-2) - 0FFFF (-1) 1 0 07FFF (32767) - 0FFFF (-1) 1 1
Lembre, o CMP é, na verdade, uma subtração, então, o primeiro exemplo calcula (-1) - (-2), que é (+1). O resultado é positivo e um overflow não ocorreu, então tanto S quanto O são zero. Já que (S xor O) é zero, Oprnd1 é maior ou igual a Oprnd2.
No segundo exemplo, a instrução CMP calcularia (-32768)-(+1), que é (-32769). Já que um inteiro de 16 bits com sinal não pode representar esse valor, o valor é arredondado para 7FFFh (+32767) e seta o flag de overflow. E como o resultado é positivo (pelo menos dentro dos 16 bits) o flag de sinal é zerado. E desde que (S xor O) é 1 aqui, Oprnd1 é menor que Oprnd2.
No terceiro exemplo acima, CMP computa (-2)-(-1), que produz (-1). nenhum overflow ocorreu, então o flag O é zero, e o resultado é negativo, então o flag de sinal é 1. Já que (S xor O) é 1, Oprnd1 é menos que Oprnd2.
No quarto (e último) exemplo, CMP computa (+32767)-(-1). Isso produz (+32768), setando o flag de overflow. Além disso, o valor arredonda para 8000h (-32768) de modo que o flag de sinal também é setado. Já que (s xor O) é zero, Oprnd1 é maior ou igual a Oprnd2.
6.5.4 As Instruções CMPXCHG, e CMPXCHG8B
O instrução CMPXCHG (compara e troca) está disponível apenas a partir do 80486. Ela surporta a seguinte sintaxe:
cmpxchg reg, reg cmpxchg mem, reg
Os operandos devem ser do mesmo tamanho (8, 16, ou 32 bits). Esta instrução também usa o registrador acumulador; ela escolhe automaticamente AL, AX ou EAX de acordo com o tamanho dos operandos.
Esta instrução compara AL, AX ou EAX com o primeiro operando e seta o flag de zero se forem iguais. Se for assim, então a CMPXCHG copia o segundo operando para o primeiro. Se não forem iguais, ela copia o primeiro operando para o acumulador. O seguinte algoritmo descreve esta operação:
cmpxchg operand1, operand2 if ({al/ax/eax} = operand1) then zero := 1 ; Seta o flag de zero operand1 := operand2 else zero := 0 ; Limpa o flag de zero {al/ax/eax} := operand1 endif
CMPXCHG suporta certas estruturas de dados do sistema operacional requerendo operações atômicas e semáforos. É clarom se você puder encaixar o algoritmo acima no seu código, pode usar o CMPXCHG como apropriado.
Note: diferentemente do CMP, o CMPXCHG só afeta o flag de zero. Você não pode testar os outros flags depois dessa instrução, como poderia com o CMP.
O processador Pentium suporta uma instrução de comparação e troca de 64 bits - CMPXCHG8B. Ela usa a sintaxe:
cmpxchg8b ax, mem64
Essa instrução compara o valor de 64bits em EDX:EAX com o valor da memória. Se forem iguais, o Pentium armazena ECX:EBX na posição de memória, senão ele carrega EDX:EAX com a posição da memória. Essa instrução seta o flag de zero de acordo com o resultado. Não afeta nenhum outro flag.
A instrução NEG (negação) faz o complemento a dois de um byte ou word. Ela receve apenas um operando (destino) e o nega. A sintaxe para essa instrução é
neg dest
Ela computa o seguinte:
dest := 0 - dest
Isso inverte efetivamente o sinal do operando destino.
Se o operando for zero, seu sinal não muda, embora isso limpe o flag de carry. Negar qualquer outro valor seta o flag de carry. Negar um byt contendo -128, uma word contendo -32.768, ou uma double word contendo -2.147.483.648 não muda o operando, mas seta o flag de overflow. NEG sempre atualiza os flags A, S, P e Z bem como se você estivesse usando a instrução SUB.
As fomas permitidas são:
neg reg neg mem
Os operandos podem ser valores de 8, 16 ou 32 (a partir do 80386) bits.
Alguns exemplos:
; J := - J neg J ; J := -K mov ax, K neg ax mov J, ax
As instruções de multipicação provêm você com seu primeiro gostinho de irregularidade no conjunto de instruções do 8086. Instruções como ADD, ADC, SBB, e muitas outras do conjunto de instruções do 8086 usam um byte mod-reg-r/m para suportar dois operandos. Infelizmente, não há bits suficientes nos bytes de operação do 8086 para suportar todas as instruções, então o 8086 usa os bits de registro no byte de mod-reg-r/m como extensão aos 8 bits com o código da operação. Por exemplo, INC, DEC e NEG não requerem dois operandos, então as CPUs 80x86 usam os bits reg como extensão ao código da operação de 8 bits. Isso funciona bem para instruções de apenas um operando, permitindo aos projetistas da Intel codificarem várias instruções (oito, na verdade) com um opcode só.
Infelizmente, as instruções de multiplicação requerem tratamento especial e os projetistas da Intel estavam em falta de opcodes, então eles projetaram as instruções de multiplicação de modo a usar só um operando. O campo reg contém uma extensão ao opcode ao invés de um valor. É claro que a multiplicação é uma operação com dois operandos. O 8086 sempre assume que o acumulador (AL, AX, ou EAX) é o operando destino. Essa irregularidade faz o uso da multiplicação no 8086 um pouco mais difícil que as outras instruções, porque um operando tem que estar no acumulador. A Intel adotou essa idéia não-ortogonal porque eles acharam que os programadores usariam a multiplicação bem menos freqüentemente que instruções como ADD e SUB.
Um problema em prover apenas uma forma mod-reg-r/m da instrução é que você não pode multiplicar o acumulador por uma constante; o byte mod-reg-r/m não suporta o endereçamento imediato. A Intel descobriu rapidamente a necessidade de dar suporte à multiplicação por uma constante e provê suporte a isso no procesador 80286. Isso era importante espcialmente para acessos a arrays multidimensionais. Pela época em que o 80386 saiu, a Intel generalizou uma forma de operação de multiplicação, permitindo operandos mod-reg-r/m padrão.
Há duas formas da instrução de multiplicação: uma multiplicação sem sinal (MUL) e uma com sinal (IMUL). Diferentemente da adição e da subtração, você precisa de instruções separadas para essas duas operações.
As instruções de multiplicação levas as seguintes formas:
Multiplicação sem sinal:
mul reg mul mem
Multiplicação (Inteira) com sinal:
imul reg imul mem imul reg, reg, immediate (2) imul reg, mem, immediate (2) imul reg, immediate (2) imul reg, reg (3) imul reg, mem (3)
Operação de Multiplicação BCD:
aam 2- Disponível no 80286 e superior, somente. 3- Disponível no 80386 e superior, somente.
Como você pode ver, as instruções de multiplicação são uma verdadeira bagunça. Pior ainda, você precisa ter um 80386 ou superior para ter perto da funcionalidade completa. Finalmente, há algumas restrições nessas instruções não óbvias acima. O único jeito de lidar com essas instruções é memorizar sua operação.
MUL, disponível em todos os processadores, multiplica um operando sem sinal, de 8, 16 ou 32 bits. Note que quando se multiplica dois valores de n bits, o resultado pode requerer 2*n bits. Portanto, se o operando tem 8 bits, o resultado requer 16 bits. Semelhantemente, um operando de 16 bits produz um resultado com 32 e um operando de 32 bits requer 64 bits para o resultado.
A instrução MUL, com operando de 8 bits, multiplica o registrador AL pelo operando e armazena o resultado de 16 bits em AX. Então,
mul operando8 ou imul operando8
computa:
ax := al * operando8
"*" representa uma multiplicação sem sinal por MUL e uma multiplicação com sinal por IMUL.
Se você especificar um operando de 16 bits, então MUl e IMUL calculam:
dx:ax := ax * operando16
"*" tem o mesmo significado como acima, e DX:AX significa que DX contém a word de ordem superior do resultado de 32 bits e AX contém a word de ordem baixa do resultado de 32 bits.
Se você especificar um operando de 32 bits, então MUl e IMUL calculam:
edx:eax := eax * operand32
"*" tem o mesmo significado como acima, e EDX:EAX significa que EDX contém a double word de ordem superior do resultado de 64 bits e EAX contém a double word de ordem baixa do resultado de 64 bits.
Se um produto de 8x8 bits, 16x16 bits ou 32x32
bits requerer mais que oito, dezesseis ou treita e dois bits (respectivamente),
as instruções mul
e imul
setam os flags de carry e de overflow.
Mul
e imul
bagunçam
os flags A, P, S, e Z. Especialmente, note que os flags de sinal e de zero não
contêm valor significativo após a execução dessas
duas intruções.
Imul
(multiplicação
inteira) opera com operandos com sinal. Há muitas formas diferentes dessa
instrução já que a Intel tentou generalizar essa instrução
com processadores sucessivos. Os parágrafos anteriores descrevem a promeira
forma do imul, com apenas um operando. As próximas três formas
de imul estão disponíveis somente no 80286 e superiores. Elas
provêem a habilidade de se multiplicar um registrador por um valor imediato.
As últimas duas formas, disponíveis somente a partir do 80386,
provêem a capacidade de se multiplicar um registrador arbitrário
por outro registrador ou posição de memória. Expadidas
para mostrar os tamnahos permitidos dos operandos, eslas são:
imul operand1, operand2, immediate ;forma geral imul reg16, reg16, immediate8 imul reg16, reg16, immediate16 imul reg16, mem16, immediate8 imul reg16, mem16, immediate16 imul reg16, immediate8 imul reg16, immediate16 imul reg32, reg32, immediate8 (3) imul reg32, reg32, immediate32 (3) imul reg32, mem32, immediate8 (3) imul reg32, mem32, immediate32 (3) imul reg32, immediate8 (3) imul reg32, immediate32 (3) 3- Disponíveis somente a partir do 80386.
As instruções imul reg, immediate
são uma sintaxe especial que o assembler provê. A codificação
para essas instruções são as mesma de imul reg, reg,
immediate.
O assembler simplesmente coloca o mesmo valor do registrador
para ambos os operandos.
Essas instruções computam:
operand1 := operand2 * immediate operand1 := operand1 * immediate
Além donúmero de operandos, há
muitas diferenças entre essas formas e as intruções simples
mul/imul
:
imul
permite um operando imediato, mas as instruções padrão
mul/imul
não. As últimas duas formas do imul
estão disponíveis somente no 80386 e superiores. Com a adição
desses formatos, a instrução imul
é quase
tão geral quanto a instrução add
:
imul reg, reg imul reg, mem
Essas instruções calculam
reg := reg * reg e reg := reg * mem
Ambos os operandos devem ser do mesmo tamanho.
Portanto, como na forma do 80286 para o imul
, você deve testar
os flags de carry ou de overflow para detectar overflows. Se isso ocorrer, a
CPU perde os bits mais altos do resultado.
Nota Importante: Tenha em mente que o flag de zero contém um resultado indeterminado após a instrução de multiplicação. Você não pode testar o flag de zero para ver o resultado após uma multiplicação. Semelhantemente, essas instruções bagunçam o flag de sinal. Se você precisa de testar esses flags, compare o resultado com zero após testar os flags de carry ou de overflow.
A instrução aam
(ASCII
Adjust after Multiplication / Ajustte ASCII após Multiplicação),
como o aaa
e o aas
, deixa você ajustar um valor
decimal não-empacotado após a multiplicação. Essa
instrução opera diretamente no AX. Ela assume que você multiplicou
dois valores de 8 bits na escala 0..9 e o resultado está em AX (na verdade,
o resultado estará em AL, já que 9*9 é 81, o maior valor
possível; AH vale zero). Essa instrução divide AX por 10
e deixa o quociente em AH e o resto em AL:
ah := ax div 10 al := ax mod 10
Diferente de outras instruções de
ajuste decimal/ASCII, programas em linguagem assembly regularmente usam o aam
já que a conversão entre bases numéricas usam esse algoritmo.
Nota: a instrução aam
consiste de um opcode de dois bytes, o segundo byte dele é a constante
imediata 10. Programadores de assembly descobriram que se você substituir
outro valor por essa constante, você pode mudar o divisor no algoritmo
acima. Isso, contudo, é uma característica não documentada.
Funciona em todos os processadores que a Intel produziu até agora, mas
não há garantias de que a Intel vai dar suporte a isso em processadores
futuros. É claro, os processadores 80286 e superiores deixam você
multiplicar por uma constante, então esse truque é quase desnecessário
em sistemas modernos.
Não existe um dam
(ajuste decimal
para multiplicação) para o processador 80x86
Talvez o uso mais comum da instrução
imul
seja computar deslocamentos em arrays multidimensionais. De
fato, essa é provavelmente a razão principal pela qual a Intel
tenha adicionado a capacidade de se multiplicar um registrador por uma constante
no processador 80286. No Capítulo 4, esse texto usou a instrução
padrão do 8086 mul
para cálculos de índice
de arrays. Porém, a sintaxe estendida do imul
é uma
escolha muito melhor, como o seguinte exemplo demosntra:
MyArray word 8 dup ( 7 dup ( 6 dup (?))) ;8x7x6 array. J word ? K word ? M word ? . . . ; MyArray [J, K, M] := J + K - M mov ax, J add ax, K sub ax, M mov bx, J ;Array index := imul bx, 7 ; ((J*7 + K) * 6 + M) * 2 add bx, K imul bx, 6 add bx, M add bx, bx ;BX := BX * 2 mov MyArray[bx], ax
Não se esqueça de que as instruções de multiplicação são muito lentas; freqüentemente uma ordem de magnitude mais lentas que a instrução de adição. Há modos mais rápidos de multiplicar um valor por uma constante. Veja o Capítulo 9 para todos os detalhes.
6.5.7 As Instruções de Divisão: DIV, IDIV, e AAD
As instruções de divisão do 80x86 realizam uma divisão de 64/32 (80386 and later only), uma de 32/16 ou uma divisão de 16/8. Essas instruções têm a forma:
div reg Para divisão sem sinal div mem idiv reg Para divisão com sinal idiv mem aad Ajuste ASCII para divisão
A instrução div
calcula
uma divisão sem sinal. Se o operando for de oito bits, div
divide o AX pelo operando, deixando o quociente em AL e o resto (modulo) em
AH. Se o operando for de 16 bits, a nstrução div
divide o número de 32 bits formado por DX:AX pelo operando, ficando o
quociente em AX e o resto em DX. Com operandos de 32 bits (80386 e superiores),
o div
divide o valor de 64 bits que está em EDX:EAX pelo
operando, ficando o quociente em EAX e o resto em EDX.
Você não pode, no 80x86, dividir um valor de 8 bits por outro. Se o denominador for um valor de 8 bits, o numerador deve ter 16 bits. Se você precisar dividir um valor de 8 bits sem sinal por outro, você deve estender com zeros o numerador para caber em 16 bits. Você pode fazer isso carregando o numerador em AL e movendo o valor zero para AH. Então você poderá dividir Ax pelo operando denominador para produzir o resultado correto. Falhas em estender com zeros antes de executar o DIV pode causar resultados incorretos no 80x86!
Quando você precisar dividir dois valores sem sinal de 16 bits, você deve estender com zeros o AX (que contém o numerador) em DX. Basta carregar o valor 0 em DX. Se precisar dividir um valor de 32 bits por outro, estenda com zeros EAX em EDX (carregando o valor de EDX com 0) antes da divisão.
Quando for lidar com valores inteiros com sinal,
você vai precisar estender o sinal de AL para AX, AX para DX ou EAX para
EDX, antes de executar o IDIV. Para fazer isso, use as instruções
cbw
, cwd
, cdq
, ou movsx.
Se o bit ou word de ordem alta já não contiver bits significantes,
então você deve estender com sinal o valor no acumulador (AL/AX/EAX)
antes de executar o IDIV. Falhas em fazer isso podem produzir resultados incorretos.
Existe uma outr apegadinha nas instruções de divisão do 80x86: você pode obter um erro fatal quando usar essa instrução. Primeiro, é claro, você pode tentar dividir um valor por zero. Além disso, o quociente pode ser muito grande para caber em EAX, AX ou AL. Por exemplo, a divisão 16/8 dos valores "8000h / 2" produz o quociente 4000h, com resto zero. 4000h não caberá nos oito bits. Se isso acontecer, ou se você tentar dividir por zero, o 80x86 vai gerar um int 0. Isso geralmente significa que a BIOS vai imprimir "division by zero" ou "divide error" e abortar seu programa. Se isso acontecer a você, as chances são de que você não estendeu o sinal ou em zeros o seu numerador antes de executar a operação de divisão. Já que esse erro vai fazer seu programa travar, você deveria ter muito cuidado com os valores que vai selecionar quando for dividir.
Os flags de carry auxiliar, de carry, de overflow, de paridade, de sinal e de zero são indefinidos após uma divisão. Se um overflow acontecer (ou se você tentar uma divisão por zero) então o 80x86 executar uma INT 0 (interrupção zero).
Note que o 80286e processadores mais novos não
têm formas especiais para o idiv
como têm para o imul
.
A maioria dos programas usa a divisão com bem menor freqüência
que a multiplicação, então os designers da Intel não
se preocuparam em cria instruções especiais para a divisão.
Note que não há um modo de se dividir por um valor imediato. Você
tem que carregar o valor num registrador ou posição de memória
e fazer a divisão naquele registrador ou posição de memória.
O aad
(ASCII Adjust before Division)
é outra operação de decimais não empacotados. Ele
separa valores decimais codificados em binário antes de realizar divisões
ASCII. Embora esse texto não vá cobrir aritmética BCD,
a instrução AAD é util para outras operações.
O algoritmo que descreve a instrução é
al := ah*10 + al ah := 0
A instrução é bastante útil para converter strings de dígitos em valores inteiros (veja as questões no fim desse capítulo).
Os seguintes exempls mostram como dividir um valor de 16 bits por outro.
; J := K / M (sem sinal) mov ax, K ;pega o dividendo mov dx, 0 ;estende em zeros o valor sem sinal em AX para DX. < Na prática, deveríamos verificar que M não contenha zero aqui > div M mov J, ax ; J := K / M (com sinal) mov ax, K ;Pega o dividendo cwd ;estende o sinal do valor com sinal em AX para DX. < Na prática, deveríamos verificar que M não contenha zero aqui> idiv M mov J, ax ; J := (K*M)/P mov ax, K ;Note que o imul produz imul M ; um resultado de 32 bitsem DX:AX, então idiv P ; não precisamos de estender o sinal de AX aqui. mov J, ax ;Espere e reze para que o resultado caiba em 16 bits!
|
Table of Content | Chapter Six (Part 3) |
Chapter Six: The 80x86 Instruction
Set (Part 2)
26 SEP 1996
Tradução: Renato Nunes Bastos
15/Out/2004