Usando instruções da FPU
Aprendendo a usar o x87 para fazer cálculos.
Podemos usar a FPU para fazer cálculos com valores de ponto flutuante. A arquitetura x86 segue a padronização IEEE-754 para a representação de valores de ponto flutuante.
Registradores
As instruções da FPU trabalham com os registradores de st0 até st7, são 8 registradores de 80 bits de tamanho cada. Juntos eles formam uma stack (pilha) onde você pode empilhar valores para trabalhar com eles ou desempilhar para armazenar o resultado das operações em algum lugar.
O empilhamento de valores funciona colocando o novo valor em st0 e todos os outros valores anteriores são "empurrados" para os registradores posteriores. Um exemplo bem leviano dessa operação:
Detalhe que só é possível usar esses registradores em instruções da FPU, algo como esse código está errado:
Formato das instruções
As instruções da FPU todas começam com um prefixo F, e as que operam com valores inteiros (convertendo DE ou PARA inteiro) também tem uma letra I após a letra F. Por fim, instruções que fazem o pop de um valor da pilha, isto é, remove o valor de lá, terminam com um sufixo P. Entendendo isso fica muito mais fácil identificar o que cada mnemônico significa e assim você não perde tempo tentando decorar uma sopa de letrinhas, se essas letras existem é porque tem um significado.
Fazer load de um valor é basicamente carregar um valor da memória para a pilha em st0, é como um push quando estamos falando da pilha convencional. A diferença aqui é a maneira como o valor é colocado na pilha, como já foi explicado anteriormente.
Já o store é pegar o valor da pilha, mais especificamente em st0, e armazenar em algum lugar da memória. Algumas instruções store permitem armazenar o valor em outro registrador da FPU.
Aqui eu vou ensinar a usar a FPU mas sem diretamente trabalhar com a linguagem C e os tipos float ou double, pois como já foi mencionado, não é assim que o compilador trabalha com cálculos de ponto flutuante.
Vou usar a notação memXXfp
e memXXint
para especificar valores na memória que sejam float ou inteiro, respectivamente. Onde XX seria o tamanho do valor em bits. Já a notação st(i)
será usada para se referir a qualquer registrador de st0 até st7. O st(0)
seria o registrador st0 especificamente.
FINIT | Initialization
Normalmente vamos usar essa instrução antes de começar a usar a FPU, pois ela reseta a FPU para o estado inicial. Dessa forma quaisquer operações anteriores com a FPU são descartadas e podemos começar tudo do zero. Assim não é necessário, por exemplo, a gente limpar a pilha da FPU toda vez que terminar as operações com ela. Basta rodar essa instrução antes de usá-la.
FLD, FILD | (Integer) Load
A instrução fld
carrega um valor float de 32, 64 ou 80 bits para st0. Repare como é possível dar load em um dos registradores da pilha, o que torna possível retrabalhar com valores anteriormente carregados. Se você rodar fld st0
estará basicamente duplicando o último valor carregado.
Já fild
carrega um valor inteiro sinalizado de 16, 32 ou 64 bits o convertendo para float de 64 bits.
Load Constant
Existem várias instruções para dar push de valores constantes na pilha da FPU, e elas são:
Instrução
Valor
FLD1
+1.0
FLDZ
+0.0
FLDL2T
log2(10)
FLDL2E
log2(e)
FLDPI
Valor de PI. (3.1415 blabla...)
FLDLG2
log10(2)
FLDLN2
logE(2)
FST, FSTP | Store (and Pop)
Pega o valor float de st0 e copia para o operando destino. A versão com o sufixo P também faz o pop do valor da stack, sendo possível dar store em um float de 80 bits somente com essa instrução.
FIST, FISTP | Integer Store (and Pop)
Pega o valor em st0, converte para inteiro sinalizado e armazena no operando destino. Só é possível dar store em um inteiro de 64 bits na versão da instrução que faz o pop.
Só com essas instruções já podemos converter um float para inteiro e vice-versa. Conforme exemplo:
Se você rodar esse teste irá notar que o valor foi convertido para 24 já que houve um arredondamento.
FADD, FADDP, FIADD | (Integer) Add (and Pop)
As versões de fadd
com operando na memória faz a soma do operando com st0 e armazena o resultado da soma no próprio st0. Já fiadd
com operando em memória faz a mesma coisa, porém convertendo o valor inteiro para float 64 bits antes.
As instruções com registradores fazem a soma e armazenam o resultado no operando mais a esquerda, o operando destino. Enquanto a faddp
sem operandos soma st0 com st1, armazena o resultado em st1 e depois faz o pop.
Exemplo de soma simples:
FSUB, FSUBP, FISUB | (Integer) Subtract (and Pop)
Mesma coisa que as instruções acima, só que fazendo uma operação de subtração.
FDIV, FDIVP, FIDIV | (integer) Division (and Pop)
Mesma coisa que FADD etc. porém faz uma operação de divisão.
FMUL, FMULP, FIMUL | (Integer) Multiply (and Pop)
Cansei de repetir, já sabe né? Operação de multiplicação.
FSUBR, FSUBRP, FISUBR | (Integer) Subtract Reverse (and Pop)
Faz a mesma coisa que a família FSUB só que com os operandos ao contrário. Conforme ilustração:
Ou seja faz a subtração na ordem inversa dos operandos, porém onde o resultado é armazenado continua sendo o mesmo.
FDIVR, FDIVRP, FIDIVRP | (Integer) Division Reverse (and Pop)
Mesma lógica que as instruções acima, porém faz a divisão na ordem inversa dos operandos.
FXCH | Exchange
Seguindo a mesma lógica da instrução xchg
, troca o valor de st0 com st(i). A versão da instrução sem operando especificado faz a troca entre st0 e st1.
FSQRT | Square root
Calcula a raíz quadrada de st0 e armazena o resultado no próprio st0.
FABS | Absolute
Calcula o valor absoluto de st0 e armazena em st0. Basicamente zera o bit de sinalização do valor.
FCHS | Change Sign
Inverte o sinal de st0, se era negativo passa a ser positivo e vice-versa.
FCOS | Cosine
Calcula o cosseno de st0 que deve ser um valor radiano, e armazena o resultado nele próprio.
FSIN | Sine
Calcula o seno de st0, que deve estar em radianos.
FSINCOS | Sine and Cosine
Calcula o seno e o cosseno de st0. O cosseno é armazenado em st0 enquanto o seno estará em st1.
FPTAN | Partial Tangent
Calcula a tangente de st0 e armazena o resultado no próprio registrador, logo após faz o push do valor 1.0 na pilha. O valor em st0 para ser calculado deve estar em radianos.
FPATAN | Partial Arctangent
Calcula o arco-tangente de st1 dividido por st0, armazena o resultado em st1 e depois faz o pop. O resultado tem o mesmo sinal que o operando que estava em st1.
F2XM1 | 2^x - 1
Faz o cálculo de 2 elevado a st0 menos 1, e armazena o resultado em st0.
FYL2X | y * log2(x)
Faz esse cálculo aí com logaritmo de base 2:
Após o cálculo é feito um pop.
FYL2XP1 | y * log2(x + 1)
Mesma coisa que a instrução anterior porém somando 1 a st0.
FRNDINT | Round to Integer
Arredonda st0 para a parte inteira mais próxima e armazena o resultado em st0.
FPREM, FPREM1 | Partial Reminder
As duas instruções armazenam a sobra da divisão entre st0 e st1 no registrador st0. Com a diferença que fprem1
segue a padronização IEEE-754.
FCOMI, FCOMIP, FUCOMI, FUCOMIP | Compare
Faz a comparação entre st0 e st(i) setando as status flags de acordo. A diferença de fucomi
e fucomip
é que essas duas verificam se os valores nos registradores não são NaN, sendo o caso a instrução irá disparar uma exception #IA.
FCMOVcc | Conditional Move
Faz uma operação move condicional levando em consideração as status flags.
Vendo os resultados
Adiantando que um valor float na convenção de chamada do C é retornado no registrador XMM0. Podemos ver o resultado de nossos testes da seguinte forma usando a instrução MOVSS:
A instrução MOVSS e os registradores XMM serão explicados no próximo tópico.
Last updated
Was this helpful?