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.
Apenas algumas instruções da FPU serão ensinadas aqui, não sendo uma lista completa.
Um adendo que normalmente compiladores de C não trabalham com valores de ponto flutuante desta maneira em x86-64 porque a arquitetura x86 hoje em dia tem maneiras mais eficientes de fazer esses cálculos. Isso será demonstrado no próximo tópico.

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:
st0 = 10
st1 = 20
st2 = 30
* é feito um push do valor 40
st0 = 40
st1 = 10
st2 = 20
st3 = 30
* é feito um pop, o valor 40 é pego.
st0 = 10
st1 = 20
st2 = 30
Detalhe que só é possível usar esses registradores em instruções da FPU, algo como esse código está errado:
mov eax, st1

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.
Caso tenha vindo de uma arquitetura RISC, geralmente o termo load é usado para a operação em que você carrega um valor da memória para um registrador. Já store é usado para se referir a operação contrária, do registrador para a memória.
Nesse caso as operações podem ser feita entre registradores da FPU também, conforme será explicado.
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 memXXfpe 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

finit
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

fld mem32fp
fld mem64fp
fld mem80fp
fld st(i)
fild mem16int
fild mem32int
fild mem64int
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.
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)

fst mem32fp
fst mem64fp
fst st(i)
fstp mem32fp
fstp mem64fp
fstp mem80fp
fstp st(i)
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)

fist mem16int
fist mem32int
fistp mem16int
fistp mem32int
fistp mem64int
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:
assembly.asm
main.c
bits 64
section .data
num: dq 23.87
section .bss
result: resd 1
section .text
global assembly
assembly:
finit
fld qword [num]
fistp dword [result]
mov eax, [result]
ret
#include <stdio.h>
int assembly(void);
int main(void)
{
printf("Resultado: %d\n", assembly());
return 0;
}
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)

fadd mem32fp
fadd mem64fp
fadd st(0), st(i)
fadd st(i), st(0)
faddp st(i), st(0)
faddp
fiadd mem16int
fiadd mem32int
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:
assembly.asm
main.c
bits 64
section .data
num1: dq 24.3
num2: dq 0.7
section .bss
result: resd 1
section .text
global assembly
assembly:
finit
fld qword [num1]
fadd qword [num2]
fist dword [result]
mov eax, [result]
ret
#include <stdio.h>
int assembly(void);
int main(void)
{
printf("Resultado: %d\n", assembly());
return 0;
}

FSUB, FSUBP, FISUB | (Integer) Subtract (and Pop)

fsub mem32fp
fsub mem64fp
fsub st(0), st(i)
fsub st(i), st(0)
fsubp st(i), st(0)
fsubp
fisub mem16int
fisub mem32int
Mesma coisa que as instruções acima, só que fazendo uma operação de subtração.

FDIV, FDIVP, FIDIV | (integer) Division (and Pop)

fdiv mem32fp
fdiv mem64fp
fdiv st(0), st(i)
fdiv st(i), st(0)
fdivp st(i), st(0)
fdivp
fidiv mem16int
fidiv mem32int
Mesma coisa que FADD etc. porém faz uma operação de divisão.

FMUL, FMULP, FIMUL | (Integer) Multiply (and Pop)

fmul mem32fp
fmul mem64fp
fmul st(0), st(i)
fmul st(i), st(0)
fmulp st(i), st(0)
fmulp
fimul mem16int
fimul mem32int
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:
a = a - b // fsub etc.
a = b - a // fsubr etc.
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

fxch st(i)
fxch
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

fsqrt
Calcula a raíz quadrada de st0 e armazena o resultado no próprio st0.

FABS | Absolute

fabs
Calcula o valor absoluto de st0 e armazena em st0. Basicamente zera o bit de sinalização do valor.

FCHS | Change Sign

fchs
Inverte o sinal de st0, se era negativo passa a ser positivo e vice-versa.

FCOS | Cosine

fcos
Calcula o cosseno de st0 que deve ser um valor radiano, e armazena o resultado nele próprio.

FSIN | Sine

fsin
Calcula o seno de st0, que deve estar em radianos.

FSINCOS | Sine and Cosine

fsincos
Calcula o seno e o cosseno de st0. O cosseno é armazenado em st0 enquanto o seno estará em st1.

FPTAN | Partial Tangent

fptan
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

fpatan
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.
st1=arctan(st1÷st0)st1 = \arctan( st1 \div st0 )

F2XM1 | 2^x - 1

f2xm1
Faz o cálculo de 2 elevado a st0 menos 1, e armazena o resultado em st0.
st0=2st01st0 = 2^{st0} - 1

FYL2X | y * log2(x)

fyl2x
Faz esse cálculo aí com logaritmo de base 2:
st1=st1log2(st0)st1 = st1 \cdot \log_2(st0)
Após o cálculo é feito um pop.

FYL2XP1 | y * log2(x + 1)

fyl2xp1
Mesma coisa que a instrução anterior porém somando 1 a st0.
st1=st1log2(st0+1)st1 = st1 \cdot \log_2(st0 + 1)

FRNDINT | Round to Integer

frndint
Arredonda st0 para a parte inteira mais próxima e armazena o resultado em st0.

FPREM, FPREM1 | Partial Reminder

fprem
fprem1
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

fcomi st(0), st(i)
fcomip st(0), st(i)
fucomi st(0), st(i)
fucomip st(0), st(i)
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

fcmovb st(0), st(i)
fcmove st(0), st(i)
fcmovbe st(0), st(i)
fcmovu st(0), st(i)
fcmovnb st(0), st(i)
fcmovne st(0), st(i)
fcmovnbe st(0), st(i)
fcmovnu st(0), st(i)
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:
assembly.asm
main.c
bits 64
section .data
num1: dq 3.0
num2: dq 3.0
section .bss
result: resd 1
section .text
global assembly
assembly:
finit
fld qword [num1]
fmul qword [num2]
fst dword [result]
movss xmm0, [result]
ret
#include <stdio.h>
float assembly(void);
int main(void)
{
printf("Resultado: %f\n", assembly());
return 0;
}
A instrução MOVSS e os registradores XMM serão explicados no próximo tópico.
Export as PDF
Copy link
On this page
Registradores
Formato das instruções
FINIT | Initialization
FLD, FILD | (Integer) Load
Load Constant
FST, FSTP | Store (and Pop)
FIST, FISTP | Integer Store (and Pop)
FADD, FADDP, FIADD | (Integer) Add (and Pop)
FSUB, FSUBP, FISUB | (Integer) Subtract (and Pop)
FDIV, FDIVP, FIDIV | (integer) Division (and Pop)
FMUL, FMULP, FIMUL | (Integer) Multiply (and Pop)
FSUBR, FSUBRP, FISUBR | (Integer) Subtract Reverse (and Pop)
FDIVR, FDIVRP, FIDIVRP | (Integer) Division Reverse (and Pop)
FXCH | Exchange
FSQRT | Square root
FABS | Absolute
FCHS | Change Sign
FCOS | Cosine
FSIN | Sine
FSINCOS | Sine and Cosine
FPTAN | Partial Tangent
FPATAN | Partial Arctangent
F2XM1 | 2^x - 1
FYL2X | y * log2(x)
FYL2XP1 | y * log2(x + 1)
FRNDINT | Round to Integer
FPREM, FPREM1 | Partial Reminder
FCOMI, FCOMIP, FUCOMI, FUCOMIP | Compare
FCMOVcc | Conditional Move
Vendo os resultados