From 4f2579468b9b526cd03209a522f105d84e5b57e1 Mon Sep 17 00:00:00 2001 From: Blau Araujo Date: Wed, 18 Jun 2025 09:33:28 -0300 Subject: [PATCH] =?UTF-8?q?anota=C3=A7=C3=B5es=20da=20aula=209?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- curso/aula-09.org | 1811 +++++++++++++++------------------------------ 1 file changed, 595 insertions(+), 1216 deletions(-) diff --git a/curso/aula-09.org b/curso/aula-09.org index 6c5a0f6..6188672 100644 --- a/curso/aula-09.org +++ b/curso/aula-09.org @@ -1,4 +1,4 @@ -#+title: 9 -- Conversão de strings numéricas para inteiros +#+title: 9 -- Conversão de representações de ponto flutuante #+author: Blau Araujo #+email: cursos@blauaraujo.com @@ -6,1357 +6,736 @@ * Objetivos -- Diferenciar as expressões numéricas em texto dos números que representam. -- Conhecer a relação entre tipos numéricos e tamanhos de dados. -- Criar sub-rotinas reaproveitáveis para conversões de caracteres para números. +- Conhecer as formas de codificação binária de números de ponto flutuante. +- Identificar as representações de números de ponto flutuante com strings. +- Implementar funções e sub-rotinas para converter os representações de + ponto flutuante nos valores representados. -* Todos os dados externos são recebidos como caracteres +* Tipos de ponto flutuante em C -Se consideramos como /dados externos/: +A linguagem C define três tipos principais de ponto flutuante (x86_64): -- Argumentos de linha de comando; -- Variáveis exportadas para o ambiente do processo; -- Dados digitados pelo usuário; -- Dados lidos por redirecionamentos e pipes; -- Dados lidos de arquivos em geral... +| Tipo | Precisão | Tamanho | +|-------------+-----------+----------| +| =float= | Simples | 4 bytes | +| =double= | Dupla | 8 bytes | +| =long double= | Estendida | 16 bytes | -Todos eles têm em comum o fato de serem recebidos nos nossos programas -como cadeias de caracteres, mesmo que representem números. Consequentemente, -para utilizá-los em operações numéricas, será necessário convertê-los nos -valores que representam. +A precisão refere-se à quantidade de dígitos que podem ser representados de +forma exata em um número, sendo um parâmetro essencial para determinar a +precisão de resultados de operações aritméticas, ate onde podemos representar +números muito pequenos sem que eles virem zero e distinguir valores em +operações de comparação. -** Entendendo o problema +No entanto, observe o que acontece com a execução do exemplo abaixo... -Para entendermos melhor o problema, vamos analisar o que acontece no programa -abaixo (=exemplo.c=) com o GDB. +Arquivo: [[exemplos/10/precision.c][precision.c]] -Arquivo: [[exemplos/09/exemplo.c][exemplo.c]] - -#+begin_src c :tangle exemplos/09/exemplo.c +#+begin_src c :tangle exemplos/10/precision.c #include +#include // Para as precisões int main(void) { + float f = 10.0f / 3.0f; + double d = 10.0 / 3.0; + long double ld = 10.0L / 3.0L; - int a = 123; - char *b = "123"; - int *c = (int *)b; // Casting do valor no endereço 'b' para inteiro - - printf("Inteiro a: %d\n", a); - printf("String b: %s\n", b); - printf("Inteiro c: %d\n", *c); // Imprime o valor no endereço 'c' + printf("float (%d): %.20f\n", FLT_DIG, f); + printf("double (%d): %.20lf\n", DBL_DIG, d); + printf("long double (%d): %.20Lf\n", LDBL_DIG, ld); return 0; } #+end_src -*Compilação e execução:* +Aqui, nós queremos imprimir os resultados das divisões utilizando os três +tipos de ponto flutuante e, segundo as definições da =glibc=, quantos dos seus +dígitos decimais teriam precisão garantida. Para isso, estamos utilizando as +constantes simbólicas do cabeçalho =float.h=: + +- =FLT_DIG=: Quantidade de dígitos decimais garantidos no tipo =float=. +- =DBL_DIG=: Quantidade de dígitos decimais garantidos no tipo =double=. +- =LDBL_DIG=: Quantidade de dígitos decimais garantidos no tipo =long double=. + +Compilando e executando: #+begin_example -:~$ gcc -g exemplo.c +:~$ gcc -Wall precision.c :~$ ./a.out -Inteiro a: 123 -String b: 123 -Inteiro c: 3355185 +float (6): 3.33333325386047363281 +double (15): 3.33333333333333348136 +long double (18): 3.33333333333333333326 #+end_example -Note que o ponteiro =c=, que recebeu endereço dos bytes da string no ponteiro -=b= (="123"=), aponta para um valor inteiro totalmente diferente do que está -representado pelos dígitos da string (o inteiro =3355185=). - -*Abertura e execução com o GDB:* - -Para examinar os conteúdo da memória em hexadecimal, nós vamos marcar a -unção =main= como ponto de parada, executar o programa e avançar a execução -em 3 linhas, para que as variáveis sejam inciadas, o nos levará ao ponto -anterior à chamada da função =printf=: +De fato, os resultados são precisos até os dígitos decimais esperados: #+begin_example -:~$ gdb a.out -Reading symbols from a.out... -(gdb) break main -Breakpoint 1 at 0x1141: file exemplo.c, line 5. -(gdb) run -Starting program: /home/blau/git/pbn/curso/exemplos/09/a.out -[Thread debugging using libthread_db enabled] -Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". - -Breakpoint 1, main () at exemplo.c:5 -warning: Source file is more recent than executable. -5 int a = 123; -(gdb) next 3 -9 printf("Inteiro a: %d\n", a); -(gdb) + 6 + | +float (6): 3.33333325386047363281 + 15 + | +double (15): 3.33333333333333348136 + 18 + | +long double (18): 3.33333333333333333326 #+end_example -O que queremos observar é a diferença entre os valores numéricos dos três -dados na memória, como se todos eles fossem inteiros (formato =w= no GDB): +* Representações em expressões constantes -#+begin_example -(gdb) x /1wx &a -0x7fffffffdecc: 0x0000007b -(gdb) x /1wx b -0x555555556004: 0x00333231 -(gdb) x /1wx c -0x555555556004: 0x00333231 -#+end_example +Ainda no código do exemplo, observe como os valores de ponto flutuante foram +escritos: -Veja que os bytes da string apontada por 'b' são, como esperado, os mesmos -do inteiro apontado por 'c', mas ambos são totalmente diferentes do valor -em hexadecimal do inteiro em =a= (=123=). +#+begin_src c +float f = 10.0f / 3.0f; +double d = 10.0 / 3.0; +long double ld = 10.0L / 3.0L; +#+end_src -** A ordem dos bytes +Tivemos que escrever assim porque, por padrão, expressões constantes com ponto +decimal são avaliadas como =double=, a menos que sejam seguidas de =f= (=float=) ou +=L= (=long double=), como podemos ver nesta tabela, que inclui as notações com +expoente: -Nós também podemos examinar o conteúdo byte a byte desses dados a partir de -seus endereços iniciais: - -#+begin_example -(gdb) x /4bx &a -0x7fffffffdecc: 0x7b 0x00 0x00 0x00 -(gdb) x /4bx b -0x555555556004: 0x31 0x32 0x33 0x00 -(gdb) x /4bx c -0x555555556004: 0x31 0x32 0x33 0x00 -#+end_example - -Isso ajuda a demonstrar que os bytes de inteiros, como os de qualquer outro -tipo diferente de =char=, são escritos em ordem inversa (/little endian/) na -memória. Portanto, quando os bytes da string foram interpretados como os -4 bytes de um inteiro, eles também foram invertidos: - -#+begin_example -(gdb) x /4bx c -0x555555556004: 0x31 0x32 0x33 0x00 -(gdb) x /1wx c -0x555555556004: 0x00333231 -#+end_example - -O tipo =char= é composto por apenas um byte e a ordem /little endian/ só afeta -a disposição de bytes: é por isso que somente os dados de tipos com dois ou -mais bytes são invertidos. Além disso, na montagem dos dados na memória, -o que importa é o tamanho de cada dado, não o que eles formam. Então, como -strings são formadas por cadeias de caracteres terminadas com o caractere -nulo (='\0'=), a ordem de seus bytes não é invertida. - -** Dígitos na tabela ASCII - -Os valores dos bytes da string ="123"= (além do terminador nulo ='\0'=, de valor -=0x00=) foram determinados pela convenção de representação de caracteres da -tabela ASCII: - -| Caractere | Hexadecimal | Octal | Decimal | -|-----------+-------------+-------+---------| -| =\0= | 0x00 | 0 | 0 | -| =0= | 0x30 | 060 | 48 | -| =1= | 0x31 | 061 | 49 | -| =2= | 0x32 | 062 | 50 | -| =3= | 0x33 | 063 | 51 | -| =4= | 0x34 | 064 | 52 | -| =5= | 0x35 | 065 | 53 | -| =6= | 0x36 | 066 | 54 | -| =7= | 0x37 | 067 | 55 | -| =8= | 0x38 | 070 | 56 | -| =9= | 0x39 | 071 | 57 | - -Em comparação aos valores dos caracteres em outras bases, a representação em -hexadecimal é bastante conveniente, pois basta subtrair =0x30= para saber qual -é o dígito correspondente. - -** Considerações sobre os tipos numéricos - -Em baixo nível, nós trabalhamos diretamente com tamanhos de dados. Os tipos -das linguagens de alto nível, como a linguagem C, são apenas abstrações para -ajudar a definir o tamanho dos dados e as regras de seus usos. Sendo assim, -existe uma equivalência entre as formas de representar tamanhos de dados em -Assembly com os tipos, por exemplo, da linguagem C. - -Considerando a arquitetura x86_64: - -| Tipo em C | Tamanho | Tamanho em Assembly | -|-----------+---------+---------------------| -| =char= | 1 byte | /byte/ | -| =short= | 2 bytes | /word/ | -| =int= | 4 bytes | /double-word/ | -| =long= | 8 bytes | /quad-word/ | -| =float= | 4 bytes | /double-word/ | -| =double= | 8 bytes | /quad-word/ | +| Literal | Tipo | +|------------------+-------------| +| =3.14= | =double= | +| =3.14f= ou =3.14F= | =float= | +| =3.14l= ou =3.14L= | =long double= | +| =3e[+/-]2= | =double= | +| =3e[+/-]2f= (ou =F=) | =float= | +| =3e[+/-]2l= (ou =L=) | =long double= | #+begin_quote -Esses tamanhos são especificados na ABI do System V AM64, usada pelo Linux -em arquitetura x86_64 e garantidos, na prática, pelo GCC e em sistemas -baseados na =glibc=. +Também existem as representações com expoentes binários, introduzidos no C99 +para representar valores em hexadecimal com expoente em base 2 (ex.: =0x1.8p1=), +mas eles não serão estudados neste curso. #+end_quote -Em Assembly, portanto, definir uma lista de /double-words/ (=dd=) tem o mesmo -efeito de definir um vetor de inteiros na linguagem C, inclusive na ordem -em que os bytes são dispostos na memória. Ou então, o dado em um registrador -de 64 bits, se tomado como um todo, terá o tamanho de uma /quad-word/ e será -equivalente ao tipo =long= da linguagem C. +Para os nossos propósitos, porém, a representação literal da linguagem C que +nos interessa é a do tipo =double= (=3.14= ou =0.42=) com ou sem sinal. Sobre isso, +valores negativos são representados da mesma forma que os positivos, mas com +o bit de sinal ativado. -** Pesos posicionais +Sendo assim, os dígitos dos valores convertidos pelas nossas implementações +deverão corresponder a todos os caracteres da string numérica e serão +retornados como =double=, o que implica: -Falando apenas de inteiros sem sinal por enquanto, cada byte de uma string -numérica decimal representa um algarismo do número multiplicado pelo seu -/peso posicional/, por exemplo: +- Valor retornado em 8 bytes (64 bits) com ou sem sinal; +- Limite máximo em DBL_MAX +- Limite mínimo em DBL_TRUE_MIN (menor subnormal). + +* Codificação binária do tipo double (64 bits) + +Um número de ponto flutuante com dupla precisão (/double/) ocupa 64 bits +divididos em três campos: #+begin_example -0x31 0x32 0x33 => '1' '2' '3' - -0x31 - 0x30 * 100 = 100 (centenas) -0x32 - 0x30 * 10 = 20 (dezenas) -0x33 - 0x30 * 1 = 3 (unidades) - -100 + 20 + 3 = 123 +63 62 51 0 +┌───┬──────────┬────────────────────────────┐ +│ S │ EXPOENTE │ MANTISSA │ +└───┴──────────┴────────────────────────────┘ #+end_example -Manualmente, este é um procedimento de conversão bastante simples, mas a sua -implementação em uma rotina de baixo nível não é tão intuitiva, embora também -não seja nada muito complicado. O /"pulo do gato"/ está em alcançar os pesos -posicionais de cada algarismo progressivamente: +De modo a representar números da faixa normal (parte inteira ~>=1~) como: #+begin_example -031 0x32 033 => '1' '2' '3' - -val = 0x31 - 0x30 = 1 -val = (val * 10) + (0x32 - 0x30) = 10 + 2 = 12 -val = (val * 10) + (0x33 - 0x30) = 120 + 3 = 123 +(-1)^sinal × (1 + (mantissa/2^52)) × 2^(expoente - 1023) #+end_example -Ou, de uma forma mais regular: +#+begin_quote +*Nota:* /mantissa/ é como chamamos a parte fracionária do número. +#+end_quote + +** Bits de expoente + +O expoente diz quantas vezes o valor da mantissa será multiplicado ou +dividido por 2. O campo possui 11 bits (bits 62 a 52) e pode receber valores +entre 0 e 2047 que assumirão três significados diferentes: + +| Expoente | Significado | +|----------+-----------------------------------------| +| =0= | Número muito pequenos (subnormais) ou =0= | +| =2047= | Infinito ou não é um número (=NaN=) | +| =1= a =2046= | Números na faixa normal | + +Se o expoente for =0=, ele será interpretado com o valor fixo =-1022= e a +interpretação será feita sem o bit implícito, resultando na expressão +de valores iniciados com =0.= alguma coisa. Neste caso, nós teremos valores +muito pequenos, abaixo dos valores normais e, por isso mesmo, chamados de +/subnormais/. + +*Exemplos:* #+begin_example -val = 0 -val = (val * 10) + (0x31 - 0x30) = 0 + 1 = 1 -val = (val * 10) + (0x32 - 0x30) = 10 + 2 = 12 -val = (val * 10) + (0x33 - 0x30) = 120 + 3 = 123 +0x0000000000000000 => +0.0 +0x0000000000000001 => Menor subnormal positivo #+end_example -Esse procedimento, facilmente implementável em Assembly, contempla tudo que -vimos sobre o problema: +Já a interpretação do expoente =2047= depende do que está na mantissa: se for +=0=, o número é interpretado como infinito; caso contrário, não será um +número. -- Percorre os bytes da string na ordem em que estão na memória; -- Converte os dígitos ASCII para os valores que representam; -- Aplica os pesos posicionais corretos a cada algarismo encontrado. - -Com isso, nós já podemos escrever uma rotina de conversão para strings que -representam números decimais sem sinal de todos os tipos compatíveis com -=int= (=char=, =short= e =long=, por exemplo). - -* Conversão para inteiros sem sinal em alto nível - -Em linguagem C, sem utilizar as funções especializadas da biblioteca padrão -e com base no procedimento que elaboramos, nós poderíamos implementar uma -função de conversão para inteiros sem sinal deste modo: - -#+begin_src c -unsigned int str_to_uint(char *str) { - int dig; // Recebe o dígito convertido - unsigned int conv = 0; // Recebe a parcial da conversão - - for (int i = 0; str[i] != '\0'; i++) { - if (str[i] < '0' || str[i] > '9') break; - dig = str[i] - '0'; - conv = (conv * 10) + dig; - } - return conv; -} -#+end_src - -Embora funcional para a maioria dos casos, essa abordagem tem alguns problemas: - -- Não verifica se o valor convertido excede o limite máximo para inteiros sem - sinal da plataforma; -- Não verifica se o ponteiro para a string é nulo (=NULL=); -- Retorna =0= tanto para a conversão da string ="0"= quanto para uma string sem - dígitos nos primeiros caracteres. - -Todos esses problemas poderiam ser solucionados com verificações e o retorno -de um valor de erro: mas que valor seria esse? Tipicamente, utilizaríamos =-1=, -mas o retorno da função tem que ser =unsigned=. Também poderíamos sacrificar um -número válido, como o valor do próprio limite máximo de inteiros sem sinal, -definido na =glibc= em =UINT_MAX=, no cabeçalho =limits.h=, mas isso requereria -uma nota sobre essa limitação. - -Uma terceira opção, que me parece bem mais razoável, seria incluir um segundo -parâmetro para receber o estado de término da função: - -#+begin_src c -unsigned int str_to_uint(char *str, int *err); -#+end_src - -Deste modo, a função poderia ser utilizada assim: - -#+begin_src c -char *str_num = "123"; // String a ser convertida. -int status; // Recebe o estado de término da função. - -unsigned int num = str_to_uint(str_num, &status); -if (!status) ...; // Consequências do erro... -#+end_src - -Para os casos de uso onde o tratamento de erros não for importante, nós -podemos dar a opção de passar =NULL= como argumento de =*err= e trabalhar a -implementação para definir um estado padrão =0= (erro), condicionado ao -ponteiro recebido, e alterá-lo para =1= somente antes de terminar com -sucesso: - -#+begin_src c -unsigned int str_to_uint(char *str, int *err /* nullable */) { - // Inicia a função com estado de erro, mas só se '*err' não for NULL... - if (err) *err = 0; - - // Outras verificações e rotina de conversão... - - // Altera o estado antes de terminar com sucesso (1)... - if (err) *err = 1; - return conv; -} -#+end_src - -Assim, caso =*err= seja =NULL=, nenhum estado de término será definido. - -** Erro de ponteiro nulo - -Caso o endereço da string (=*str=) seja passada como um ponteiro nulo, nós -simplesmente terminaremos a função com retorno =0= sem alterar o valor de -=*err=, que também é =0= (erro): - -#+begin_src c -// Inicia a função com estado de erro, mas só se '*err' não for NULL... -if (err) *err = 0; - -// Termina com erro (0) e valor 0 se str==NULL... -if (str == NULL) return 0; -#+end_src - -** Erro de byte inicial fora faixa de dígitos - -Apesar de ser uma verificação redundante, por ser repetida mais adiante no -código, ela é necessária para determinar quando a função retorna =0= por ter -convertido apenas uma string ="0"= ou devido a não haver dígitos para converter -no início da string, ou seja: +*Exemplos:* #+begin_example -String: "abc123" --> Converte para 0 com estado 1 -String: "" --> Converte para 0 com estado 1 -String: "0" --> Converte para 0 com estado 1 -Erros --> Retornam conversão para 0, mas com estado 0 +0x7ff0000000000000 => +Infinito (0x7ff = 2047) +0x7ff0000000000001 => NaN #+end_example -A ideia é que os dois primeiros casos (="abc123"= e =""=) retornem =0= om estado -de término =0= (erro), portanto... +Quando o expoente representa números na faixa normal, a interpretação dos +64 bits deve ser feita considerando como se existisse um bit 1 antes da +mantissa, garantindo que a parte inteira do número normalizado nunca seja +zero. -#+begin_src c -// Inicia a função com estado de erro, mas só se '*err' não for NULL... -if (err) *err = 0; - -// Termina com erro (0) e valor 0 se str==NULL... -if (str == NULL) return 0; - -// Termina com erro (0) e valor 0 se primeiro byte não for um dígito... -if (str[0] < '0' || str[0] > '9') return 0; -#+end_src - -Nós podemos, inclusive, combinar as duas verificações numa só: - -#+begin_src c -// Inicia a função com estado de erro, mas só se '*err' não for NULL... -if (err) *err = 0; - -// Termina com erro (0) e valor 0 se str==NULL -// ou se o primeiro byte não for um dígito... -if (str == NULL || str[0] < '0' || str[0] > '9') return 0; -#+end_src - -** Erro de estouro do limite do tipo - -Nós podemos utilizar o valor de =UINT_MAX= como referência para determinar se -o próximo valor parcial do ciclo de conversões excederá ou não o limite -máximo para números inteiros sem sinal. A cada ciclo, a conversão é feita -com: +*Exemplo:* #+begin_example -valor = (valor * 10) + dígito; +123.456 => 0x405edd2f1a9fbe77 + +Primeiros 12 bytes: +0x405 = 0100 0000 0101 + +bit de sinal (+1) +↓ +0 10000000101 + ↑ + 11 bits do expoente (0x405 => 1029) + +Expoente real: +1029 - 1023 = 6 => 2^6 = 64 + +Mantissa: +0xedd2f1a9fbe77 + +Mantissa real: +1 + 0xedd2f1a9fbe77 / 2^52 = 1.929 + +Valor final: ++1 × 1.929 × 64 = +123.456 #+end_example -Para determinar se isso excederá o valor de =UNIT_MAX=, basta calcular qual -seria o valor corrente máximo para que a próxima conversão resulte em algo -dentro do limite: +* Codificação binária do tipo float (32 bits) + +O tipo /float/ armazena números reais com precisão simples em 32 bits, também +divididos em três campos: #+begin_example -valor * 10 + dígito < UINT_MAX -valor * 10 < UINT_MAX - dígito -valor < (UINT_MAX - dígito) / 10 +31 30 22 0 +┌───┬──────────┬────────────────────────────┐ +│ S │ EXPOENTE │ MANTISSA │ +└───┴──────────┴────────────────────────────┘ #+end_example -No código, isso pode ser escrito no loop de conversão como: - -#+begin_src c -// Converte o dígito corrente... -dig = str[i] - '0'; - -// Termina com erro e valor 0 se a próxima conversão exceder UINT_MAX... -if (conv > (UINT_MAX - dig) / 10) return 0; - -// Processa a parcial da conversão... -conv = (conv * 10) + dig; -#+end_src - -** Implementação final em C - -Aqui está a implementação final, com as verificações e os procedimentos -atualizados: - -#+begin_src c -#include // Requerido para obter UINT_MAX da plataforma - -unsigned int str_to_uint(char *str, int *err /* nullable */) { - - if (err) *err = 0; // Estado padrão é de erro (0 = falso)! - - // Termina com erro e valor 0 se str==NULL ou se '0'>str[0]>'9'... - if (str == NULL || str[0] < '0' || str[0] > '9') return 0; - - int dig; // Recebe o dígito convertido - unsigned int conv = 0; // Recebe a parcial da conversão - - for (int i = 0; str[i] != '\0'; i++) { - // Se o caractere não for um dígito, termina a conversão... - if (str[i] < '0' || str[i] > '9') break; - - // Converte o dígito corrente... - dig = str[i] - '0'; - - // Termina com erro e valor 0 se a próxima conversão exceder UINT_MAX... - if (conv > (UINT_MAX - dig) / 10) return 0; - - // Processa a parcial da conversão... - conv = (conv * 10) + dig; - } - - if (err) *err = 1; // Altera estado para sucesso (1 = verdadeiro) - return conv; -} -#+end_src - -** Exemplo de uso - -Arquivo: [[exemplos/09/uint.c][uint.c]] - -#+begin_src c :tangle exemplos/09/uint.c -/* - * Arquivo : uint.c - * Compilação: gcc -Wall uint.c -o uintc - */ -#include -#include // Requerido para obter UINT_MAX da plataforma - -unsigned int str_to_uint(char *str, int *err /* nullable */); - -int main(int argc, char **argv) { - if (argc == 1) { - fprintf(stderr, "Uso: %s NÚMERO\n", argv[0]); - return 1; - } - - int status; - unsigned int num = str_to_uint(argv[1], &status); - - if (status) { - printf("String: %s\nNúmero: %u\n", argv[1], num); - } else { - fprintf(stderr, "Erro de conversão!\n"); - return 1; - } - - return 0; -} - -unsigned int str_to_uint(char *str, int *err /* nullable */) { - if (err) *err = 0; // Estado padrão é de erro (0 = falso)! - - // Termina com erro e valor 0 se str==NULL ou se '0'>str[0]>'9'... - if (str == NULL || str[0] < '0' || str[0] > '9') return 0; - - int dig; // Recebe o dígito convertido - unsigned int conv = 0; // Recebe a parcial da conversão - - for (int i = 0; str[i] != '\0'; i++) { - // Se o caractere não for um dígito, termina a conversão... - if (str[i] < '0' || str[i] > '9') break; - // Converte o dígito corrente... - dig = str[i] - '0'; - // Termina com erro e valor 0 se a próxima conversão exceder UINT_MAX... - if (conv > (UINT_MAX - dig) / 10) return 0; - // Processa a parcial da conversão... - conv = (conv * 10) + dig; - } - - if (err) *err = 1; // Altera estado para sucesso (1 = verdadeiro) - return conv; -} -#+end_src - -Compilação e testes: +Deste modo, valores normalizados (parte inteira ~>=1~) podem ser calculados +pela fórmula: #+begin_example -:~$ gcc -Wall uint.c -o uintc -$ ./uintc -Uso: ./uintc NÚMERO -:~$ ./uintc 123 -String: 123 -Número: 123 -:~$ ./uintc 123abc -String: 123abc -Número: 123 -:~$ ./uintc $((2**32)) -Erro de conversão! -:~$ ./uintc $((2**32 - 1)) -String: 4294967295 -Número: 4294967295 -:~$ ./uintc '' -Erro de conversão! -:~$ ./uintc abc -Erro de conversão! +(-1)^sinal × (1 + (mantissa/2^23)) × 2^(expoente - 127) #+end_example -* Conversão para inteiros sem sinal em baixo nível +** Bits de expoente -Em Assembly, o procedimento de conversão ainda é o mesmo: +O expoente é expresso com 8 bits, o que pode resultar em valores de 0 a 255 +com os seguintes significados: + +| Expoente | Significado | +|----------+-----------------------------------| +| =0= | Número subnormal (=0.xxx=) ou zero. | +| =1= a =254= | Números na faixa normal (=1.xxx=). | +| =255= | Infinito ou NaN. | + +Assim como acontece na representação binária do tipo /double/, expoentes entre +=1= e =254= implicam a soma de 1 à mantissa real (=mantissa/2^23=) na interpretação +do valor representado. Com expoente =0=, a mantissa real ainda é utilizada para +obter a parte fracionária de um número subnormal (=0.xxx=). Contudo, se o +expoente for =255=, utiliza-se a mantissa armazenada para decidir se o valor é +infinito (mantissa =0=) ou não é um número (mantissa diferente de =0=). + +* Unidades de ponto flutuante em x86_64 + +A arquitetura x86_64 representa e manipula números reais por meio de três +mecanismos históricos: + +- FPU (/Float-Point/ Unit): coprocessador matemático; +- Instruções SSE/SSE2: suporte a operações com ponto flutuante; +- Registradores XMM: utilizados com instruções SSE. + +A FPU (x87) surgiu em 1980 com o coprocessador 8087, mais tarde integrado ao +processador 486DX. Ainda é suportado na arquitetura x86_64 por compatibilidade, +mas raramente é utilizado em códigos modernos -- a não ser para lidar com +=long double= (números de ponto flutuante com 128 bits). + +As instruções SSE/SSE2 foram introduzidas nos processadores Pentium III e IV, +dos anos 2000, como mecanismos preferenciais para operações de ponto flutuante. +A partir da arquitetura x86_64, as instruções SSE2 tronam-se obrigatórias e +substituem praticamente toda a FPU em compiladores modernos. + +Mais recentemente, os processadores passaram a oferecer extensões como AVX +(Advanced Vector Extensions), que ampliam o número e a largura de registradores +XMM (passando a YMM e ZMM, com 256 e 512 bits) e otimizam operações vetoriais +e de ponto flutuante em larga escala. Essas instruções são usadas por +compiladores modernos em programas de alto desempenho. + +#+begin_quote +*Nota:* os registradores de propósito geral nunca foram projetados para trabalhar +com ponto flutuante. Mas, ao utilizá-los nas nossas demonstrações de conversões, +nossos objetivos são apresentar o núcleo lógico dessas operações e demonstrar +os problemas e as limitações que os recursos modernos superam. +#+end_quote + +* Conversão de strings para float + +Ao converter uma string como ="12.75"= para um número real no Assembly, as +principais abordagens para representar o resultado são: + +- Formato /escalado/ (também chamado de /fixed-point/); +- Formato empacotado como número IEEE 754 de 32 bits (/float/). + +Ambas as abordagens são úteis em contextos distintos e possuem vantagens e +limitações que devem ser compreendidas e consideradas. + +** Alternativa 1: Representação escalada (fixed-point) + +Em Assembly (especialmente sem FPU), não há suporte direto para números com +parte fracionária. Mas é possível simular valores reais utilizando inteiros, +contanto que todos os números sejam representados na mesma /escala/. A ideia é +preservar as casas decimais multiplicando o valor original por um fator +fixo (como =2^23=) e armazenar o resultado como um inteiro. + +Por exemplo, podemos representar o número =12.34= como: #+begin_example -val = 0 -val = (val * 10) + (0x31 - 0x30) = 0 + 1 = 1 -val = (val * 10) + (0x32 - 0x30) = 10 + 2 = 12 -val = (val * 10) + (0x33 - 0x30) = 120 + 3 = 123 +12.75 × 2^23 = 1065353216 #+end_example -Que pode ser implementado, de forma genérica, com uma sub-rotina assim: +#+begin_quote +O exemplo de fator =2^23= (ou =8.388.608=) está relacionado com o fato deste ser +o número de bits disponíveis para representar a mantissa em um número /float/ +de 32 bits no padrão IEEE 754, como vimos. +#+end_quote + +O valor em escala pode ser armazenado em um registrador de propósito geral e +operado com instruções inteiras comuns, como =add=, =sub=, =imul=, =idiv=, etc. Após +a operação, o resultado pode ser interpretado diretamente em escala ou +/desescalado/ para a forma de ponto flutuante. + +Por exemplo, numa conversão de Celsius para Fahrenheit... #+begin_src asm -_str_to_uitn: - ; ------------------------------------------------------ - ; Entrada: RSI = Endereço da string (char * str) - ; Saída : RAX = Valor convertido (unsigned int) - ; ------------------------------------------------------ - ; Preparação... - ; ------------------------------------------------------ - push rbx ; Salva conteúdo de rbx - xor rax, rax ; rax = 0 (acumula a conversão) - xor rbx, rbx ; rbx = 0 (recebe os caracteres no loop) - ; ------------------------------------------------------ -.conv_loop: - ; ------------------------------------------------------ - ; Condições de término do loop... - ; ------------------------------------------------------ - mov bl, [rsi] ; Lê o primeiro byte - test bl, bl ; Compara primeiro byte com o terminador '\0' - je .done ; Se 0x00, termina o loop - cmp bl, 0x30 ; Compara primeiro byte com '0' - jl .done ; Se menor, termina o loop - cmp bl, 0x39 ; Compara primeiro byte com '9' - jg .done ; Se maior, termina o loop - ; ------------------------------------------------------ - ; Conversão... - ; ------------------------------------------------------ - sub rbx, 0x30 ; Subtrai '0' do byte (dígito convertido) - imul rax, rax, 10 ; rax *= 10 (deslocamento do peso posicional) - add rax, rbx ; rax += dígito convertido - inc rsi ; Avança rdi para o próximo byte - jmp .conv_loop ; Volta ao início do loop de conversão - ; ------------------------------------------------------ -.done: - ; ------------------------------------------------------ - pop rbx ; Restaura rbx - ret -#+end_src - -Isso equivale à primeira versão da função em C e traz os mesmos problemas: - -- Não verifica se o valor convertido excede o limite máximo para inteiros sem - sinal da plataforma; -- Não verifica se o ponteiro para a string é nulo (=NULL=); -- Retorna =0= tanto para a conversão da string ="0"= quanto para uma string sem - dígitos nos primeiros caracteres. - -Além disso, se quisermos adaptar a sub-rotina para uso em um programa em C -(em um objeto compartilhado, por exemplo) ela precisa seguir as convenções -da ABI. Do jeito que está, ela já poderia ser utilizada, porque recebe o -primeiro argumento em =rdi= e retorna o valor convertido em =rax=, mas ainda -precisamos decidir como implementar a sinalização de erros. Na função em C, -isso é feito com um ponteiro opcional (/nullable/) para a variável que receberá -o estado de erro: - -#+begin_src c -unsigned int str_to_uint(char *str, int *err /* nullable */); -#+end_src - -A ABI determina que o segundo argumento deve ser recebido em =rdx=, então a -assinatura da sub-rotina passaria a ser: - -#+begin_src asm -_str_to_uitn: - ; ------------------------------------------------------ - ; Entrada: - ; RSI = Endereço da string (char * str) - ; RDX = Endereço da variável de erro (int *err) - ; Saída : - ; RAX = Valor convertido (unsigned int) - ; Se sucesso: *err = 1 (verdadeiro) - ; Se erro : *err = 0 (falso) - ; ------------------------------------------------------ -#+end_src - -Ainda na função em C, nós decidimos definir o estado de =*err= logo no início -e só alterá-lo antes do retorno com sucesso. Como o argumento pode receber -um ponteiro nulo (=NULL=), o estado de término foi definido condicionalmente: - -#+begin_src c -if (err) *err = 0; // Estado padrão é de erro (0 = falso)! -// ... -if (err) *err = 1; // Altera estado para sucesso (1 = verdadeiro) -return conv; -#+end_src - -Além disso, sempre que a função terminava com erro, o valor retornado era -definido com =0=, e isso também deve ser implementado na sub-rotina: - -#+begin_src asm -_str_to_uitn: - ; ------------------------------------------------------ - ; Entrada: - ; RSI = Endereço da string (char * str) - ; RDX = Endereço da variável de erro (int *err /* nullable */) - ; Saída : - ; RAX = Valor convertido (unsigned int) - ; Se sucesso: *err = 1 (verdadeiro) - ; Se erro : *err = 0 (falso) - ; ------------------------------------------------------ - ; Definição condicional do estado inicial de erro... - ; ------------------------------------------------------ - test rdx, rdx ; Se *err = NULL, rdx = 0 - jz .skip_error ; Se for 0, pula a definição do erro - mov dword [rdx], 0 ; *err = 0 => estado inicial é de erro (falso) - ; ------------------------------------------------------ -.skip_error: - ; ------------------------------------------------------ - - ; ... - - ; ------------------------------------------------------ -.error: - ; ------------------------------------------------------ - xor rax, rax ; Retorna 0 em caso de erro - jmp .done - ; ------------------------------------------------------ -.success: - ; ------------------------------------------------------ - ; Redefinição condicional do estado final de sucesso... - ; ------------------------------------------------------ - test rdx, rdx ; Se *err = NULL, rdx = 0 - jz .done ; Se for 0, pula a definição de sucesso - mov dword [rdx], 1 ; *err = 1 => estado final é de sucesso (verdadeiro) - ; ------------------------------------------------------ -.done: - ; ------------------------------------------------------ - pop rbx ; Restaura rbx - ret -#+end_src - -Com o procedimento de tratamento de erros definido, nós podemos prosseguir -com a implementação das validações necessárias. - -** Erro de ponteiro nulo - -A primeira verificação que faremos é para o caso de =rsi= ter recebido um -ponteiro nulo na chamada: - -#+begin_src asm - ; ------------------------------------------------------ - ; Teste de ponteiro nulo... - ; ------------------------------------------------------ - test rsi, rsi ; verifica se enderçeo é nulo (0) - jz .error ; se for, termina retornando -1 (erro) -#+end_src - -Deste modo, se =rsi= receber =0= (=NULL=, numa chamada em C), a sub-rotina salta -para o rótulo local =.error= e termina retornando =0= em =rax= e =1= no endereço -em =rdx=, se =*err= estiver definido. - -** Erro de byte inicial fora da faixa de dígitos - -Para que o primeiro byte seja verificado, ele precisa ser lido. Portanto, -a verificação da faixa de dígitos deve ser feita logo após a preparação -dos registradores utilizados, antes do /loop/ de conversão: - -#+begin_src asm - ; ------------------------------------------------------ - ; Preparação... - ; ------------------------------------------------------ - push rbx ; salva rbx na pilha - xor rax, rax ; rax = 0 (acumula o resultado) - xor rbx, rbx ; rbx = 0 (recebe os caracteres no loop) - mov bl, [rsi] ; lê o primeiro byte - ; ------------------------------------------------------ - ; Validação do primeiro byte... - ; ------------------------------------------------------ - cmp bl, 0x30 ; compara com menor byte válido - jl .error ; se menor, termina retornando -1 - cmp bl, 0x39 ; compara com maior byte válido - jg .error ; se maior, termina retornando -1 -#+end_src - -Assim, a sub-rotina salta para =.error= se o byte for menor que =0x30= (caractere -='0'=) ou maior que =0x39= (caractere ='9'=). - -** Erro de estouro do limite do tipo - -Como vimos, o cálculo do maior valor que pode ser multiplicado por 10 na -conversão é feito com: - -#+begin_example -(UINT_MAX - dígito) / 10 -#+end_example - -Contudo, essa verificação é um pouco mais complicada em Assembly, por alguns -motivos: - -- Nós não temos um limite definido para a plataforma, como a constante - simbólica =UINT_MAX=, da =glibc=. -- É possível definir =UINT_MAX= com uma macro no Assembly, mas não podemos - contar com isso no caso de reutilização da sub-rotina, a menos que ela - esteja em uma biblioteca que também contenha definições de macros. -- Também teremos que utilizar registradores auxiliares adicionais, porque, - numa divisão, =rax= recebe o quociente e =rdx= o resto, e ambos estão em - uso. - -Mas /"complicada"/ não é /"impossível"/, e nós podemos assumir alguns critérios: - -- A ABI define que inteiros terão 4 bytes (32 bits) em arquiteturas x86_64 - e as implementações da linguagem C no GCC e na Glibc garantem isso. -- O valor de =UINT_MAX= pode ser armazenado em =rcx= como =0xffffffff=, o maior - número sem sinal que pode ser escrito com 4 bytes. -- Os registradores =r8= e =r10= podem ser usados para salvar e restaurar os - dados em =rax= e =rdx=. -- O registrador =r9= pode ser usado para auxiliar nos cálculos. - -Portanto, esta é uma implementação possível: - -#+begin_src asm -; ------------------------------------------------------ -; Limite válido: conv < (UINT_MAX - dígito) / 10 -; ------------------------------------------------------ -mov r8, rax ; Salva parcial da conversão em r8 -mov r10, rdx ; Salva ponteiro de erro em r10 - -mov eax, -1 ; rax = 0x00000000ffffffff (UINT_MAX) -sub eax, ebx ; rax = UINT_MAX - dígito (dividendo) - -xor rdx, rdx ; prepara rdx para receber o resto da divisão -mov r9, 10 ; divisor -div r9 ; limite(rax) = (UINT_MAX - dígito) / 10 - -cmp r8, rax ; se parcial > limite, teremos um estouro -jg .error ; se maior, termina com erro +; ---------------------------------------------------------- +; RAX = Temperatura em Celsius × 2^23 +; Conversão para Fahrenheit: F = C × 9/5 + 32 +; ---------------------------------------------------------- +imul rax, rax, 9 ; rax = rax × 9 +cqo ; estende o sinal de rax em rdx (para imul) +mov rcx, 5 ; divisor +idiv rcx ; rax = rax / 5 +add rax, 32 << 23 ; rax = rax + (32 × 2^23) -mov rdx, r10 ; restaura rdx (pontiero para erros) -mov rax, r8 ; restaura rax (parcial da conversão) +; Resultado final: Fahrenheit × 2^23 #+end_src -** Implementação final em Assembly - -Com alguns ajustes estéticos e na documentação, aqui está a implementação -completa... - -Arquivo: [[exemplos/09/uint.asm][uint.asm]] - -#+begin_src asm :tangle exemplos/09/uint.asm -; ---------------------------------------------------------- -; Arquivo : uint.asm -; Montagem: nasm -g -f elf64 uint.asm -; Ligação : ls uint.o -o uint -; ---------------------------------------------------------- -section .data -; ---------------------------------------------------------- - num_str db "123", 0 ; string numérica -; ---------------------------------------------------------- -section .bss -; ---------------------------------------------------------- - num resd 1 ; 4 bytes para o valor UINT32 - status resd 1 ; 4 bytes para o estado de término -; ---------------------------------------------------------- -section .text -global _start -; ---------------------------------------------------------- -_start: -; ---------------------------------------------------------- -; Teste de chamada (verificar com o GDB)... -; ---------------------------------------------------------- - mov rsi, num_str ; copia endereço da string em rsi - mov rdx, status ; copia o endereço do estado de erro em rdx - call _str_to_uint ; chama a sub-rotina de conversão - mov dword [num], eax ; copia o resultado como int para [num] -; ---------------------------------------------------------- -_exit: -; ---------------------------------------------------------- - mov rax, 60 - mov rdi, 0 - syscall -; ---------------------------------------------------------- -; Sub-rotinas... -; ---------------------------------------------------------- -_str_to_uint: -; ---------------------------------------------------------- -; Converte string numérica em inteiro sem sinal (uint32_t). -; Entradas: -; RSI = Endereço da string (char *str) -; RDX = Endereço para estado de erro (int *err /* nullable */) -; Saída: -; RAX = valor convertido (uint32_t) ou 0, no caso de erro -; Sucesso: *rdx = 1 -; Erro: *rdx = 0 -; ---------------------------------------------------------- - ; Definição condicional do estado inicial de erro... - ; ------------------------------------------------------ - test rdx, rdx ; Se *err = NULL, rdx = 0 - jz .skip_error ; Se for 0, pula a definição do erro - mov dword [rdx], 0 ; *err = 0 => estado inicial é de erro (falso) - ; ------------------------------------------------------ -.skip_error: - ; ------------------------------------------------------ - ; Teste de ponteiro nulo... - ; ------------------------------------------------------ - test rsi, rsi ; verifica se enderçeo é nulo (0) - jz .error ; se for, termina retornando -1 (erro) - ; ------------------------------------------------------ - ; Preparação... - ; ------------------------------------------------------ - push rbx ; salva rbx na pilha - xor rax, rax ; rax = 0 (acumula o resultado) - xor rbx, rbx ; rbx = 0 (recebe os caracteres no loop) - mov bl, [rsi] ; lê o primeiro byte - ; ------------------------------------------------------ - ; Validação do primeiro byte... - ; ------------------------------------------------------ - cmp bl, 0x30 ; compara com menor byte válido - jl .error ; se menor, termina retornando -1 - cmp bl, 0x39 ; compara com maior byte válido - jg .error ; se maior, termina retornando -1 -.conv_loop: - ; ------------------------------------------------------ - ; Condições de término da conversão ... - ; ------------------------------------------------------ - test bl, bl ; verifica se o byte é o terminador 0x00 - je .success ; se for, termina com sucesso - cmp bl, 0x30 ; compara o byte em rbx com o menor dígito - jl .success ; se for menor, termina com sucesso - cmp bl, 0x39 ; compara o byte em rbx com o maior dígito - jg .success ; se for maior, termina com sucesso - ; ------------------------------------------------------ - ; Conversão do dígito corrente... - ; ------------------------------------------------------ - sub rbx, 0x30 ; converte o dígito para seu valor numérico - ; ------------------------------------------------------ - ; Limite válido: conv < (UINT_MAX - dígito) / 10 - ; ------------------------------------------------------ - mov r8, rax ; Salva parcial da conversão em r8 - mov r10, rdx ; Salva ponteiro de erro em r10 - - mov eax, -1 ; eax = 0xffffffff (UINT_MAX - 32 bits) - sub eax, ebx ; rax = UINT_MAX - dígito (dividendo) - - xor rdx, rdx ; prepara rdx para receber o resto da divisão - mov r9, 10 ; divisor - div r9 ; limite(rax) = (UINT_MAX - dígito) / 10 - - cmp r8, rax ; se parcial > limite, teremos um estouro - jg .error ; se maior, termina com erro - - mov rdx, r10 ; restaura rdx (pontiero para erros) - mov rax, r8 ; restaura rax (parcial da conversão) - ; ------------------------------------------------------ - ; Processa a parcial da conversão... - ; ------------------------------------------------------ - imul rax, rax, 10 ; rax *= 10 (deslocamento do peso posicional) - add rax, rbx ; rax += novo algarismo - inc rsi ; avança para o próximo byte - mov bl, [rsi] ; carrega o dígito corrente em rbx - jmp .conv_loop - ; ------------------------------------------------------ -.error: - ; ------------------------------------------------------ - xor rax, rax ; Retorna 0 em caso de erro - jmp .done - ; ------------------------------------------------------ -.success: - ; ------------------------------------------------------ - ; Redefinição condicional do estado final de sucesso... - ; ------------------------------------------------------ - test rdx, rdx ; Se *err = NULL, rdx = 0 - jz .done ; Se for 0, pula a definição de sucesso - mov dword [rdx], 1 ; *err = 1 => estado final é de sucesso (verdadeiro) - ; ------------------------------------------------------ -.done: - ; ------------------------------------------------------ - pop rbx ; Restaura rbx - ret -#+end_src - -** Teste com o GDB - -Nosso objetivo é testar se a string em =num_str= é convertida corretamente -pela análise dos valores em =rax= e =rdx= (=status=) após a chamada da sub-rotina -neste trecho do programa: +Para exibir o resultado como uma string, bastaria dividi-lo por =2^23= +(=8.388.608=). Em Assembly, podemos utilizar =mov= e =idiv= para obter parte +inteira e parte decimal separadas antes de fazer a conversão para texto... #+begin_src asm ; ---------------------------------------------------------- -section .data +; RAX = Fahrenheit × 2^23 ; ---------------------------------------------------------- - num_str db "123", 0 ; string numérica +mov rbx, rax ; copia valor escalado +mov rcx, 8388608 ; fator da escala (2^23) +xor rdx, rdx ; zera rdx para o resto +idiv rcx ; rax = parte inteira, rdx = parte fracionária (resto) +; converter a partte inteira para string... ; ---------------------------------------------------------- -section .bss +; A parte fracionária ainda está em escala... ; ---------------------------------------------------------- - num resd 1 ; 4 bytes para o valor UINT32 - status resd 1 ; 4 bytes para o estado de término -; ---------------------------------------------------------- -section .text -global _start -; ---------------------------------------------------------- -_start: -; ---------------------------------------------------------- -; Teste de chamada (verificar com o GDB)... -; ---------------------------------------------------------- - mov rsi, num_str ; copia endereço da string em rsi - mov rdx, status ; copia o endereço do estado de erro em rdx - call _str_to_uint ; chama a sub-rotina de conversão - mov dword [num], eax ; copia o resultado como int para [num] +mov rbx, 1000 ; multiplicador da parte fracionária +imul rdx, rbx ; x1000 => um inteiro desescalado com 3 algarismos +idiv rcx ; rax = parte fracionária +; converter a parte fracionária para string e concatenar com '.'... #+end_src -*Montagem e execução no GDB:* +A grande vantagem dessa abordagem é sua simplicidade e compatibilidade com +qualquer conjunto de instruções, sem depender da FPU ou de registradores XMM. +Contudo, ela exige que todas as operações preservem a escala, o que requer +muita atenção com as operações aritméticas. Ao final, o resultado pode ser +convertido para exibição ou empacotado como /float/ para ser usado com +instruções de ponto flutuante ou passado para funções externas da Glibc. + +** Alternativa 2: Representação IEEE 754 + +A representação empacotada como float segue o padrão IEEE 754. Um número como +=12.75=, por exemplo, seria convertido para a forma binária através dos +seguintes passos: + +- Determinar o sinal (0 para positivo); +- Converter o número para binário normalizado; +- Calcular o expoente com o bias (deslocamento); +- Codificar a mantissa (sem o bit implícito). + +*Passo 1: determinar o sinal* + +O número é positivo, então o sinal é =0=. + +*Passo 2: converter o número para binário normalizado* + +- Parte inteira (12): =11000= em base 2. +- Parte fracionária (0.75): =11=, obtido por multiplicações sucessivas por 2. +- Resultado da conversão: =1100.11= + +Normalizar um binário de ponto flutuante é representá-lo como: #+begin_example -:~$ nasm -g -f elf64 uint.asm -:~$ ld uint.o -o uint -:~$ gdb ./uint -Reading symbols from ./uint... -(gdb) break _start -Breakpoint 1 at 0x401000: file uint.asm, line 22. -(gdb) run -Starting program: /home/blau/git/pbn/curso/exemplos/09/uint - -Breakpoint 1, _start () at uint.asm:22 -22 mov rsi, num_str ; copia endereço da string em rsi -(gdb) +1.xxxxxx × 2^n #+end_example -*Execução e verificações antes da chamada:* +No caso de =1100.11=, será preciso deslocar o ponto 3 posições para a esquerda. +Portanto, para manter seu valor original, nós temos que multiplicá-lo por +=2^3=: #+begin_example -(gdb) n 2 -24 call _str_to_uint ; chama a sub-rotina de conversão -(gdb) info registers rax rdx rsi -rax 0x0 0 -rdx 0x402008 4202504 -rsi 0x402000 4202496 -(gdb) x /1wx &num_str -0x402000 : 0x00333231 -(gdb) x /1s &num_str -0x402000 : "123" -(gdb) x /1wx &num_ -No symbol "num_" in current context. -(gdb) x /1wx &num -0x402004 : 0x00000000 -(gdb) x /1wx &status -0x402008 : 0x00000000 +1100.11 = 1.10011 × 2^3 #+end_example -*Execução e verificações após a chamada:* +*Passo 3: calcular o expoente com bias* + +Como o deslocamento da normalização foi de =2^3=, o expoente com /bias/ será: #+begin_example -(gdb) n -25 mov dword [num], eax ; copia o resultado para [num] -(gdb) n -_exit () at uint.asm:29 -29 mov rax, 60 -(gdb) info registers rax rdx rsi -rax 0x7b 123 -rdx 0x402008 4202504 -rsi 0x402003 4202499 -(gdb) x /1wx &num -0x402004 : 0x0000007b -(gdb) print (int)num -$1 = 123 -(gdb) x /1wx &status -0x402008 : 0x00000001 +3 + 127 (bias IEEE 754) = 130 +130 = 10000010 na base 2 #+end_example -* Conversão para inteiros com sinal +*Passo 4: codificar a mantissa* -No Assembly x86_64, números com sinal são representados usando o método do -/complemento de dois/. Tomando um inteiro de 32 bits como exemplo, se o bit -mais significativo (MSB, o bit mais a esquerda) for =0=, o número é interpretado -como positivo ou zero; se for =1=, o número é negativo. Em outras palavras, o -bit mais a esquerda de um número é o seu sinal. - -O complemento de dois de um número binário é obtido pela inversão de todos -os seus bits (com uma operação =NOT= bit a bit) seguida da soma de 1: +A mantissa é a parte fracionária do valor normalizado em binário seguida de +tantos zeros quanto forem necessários para totalizar 23 bits, portanto... #+begin_example - 00000000 00000000 00000000 00000101 = 5 (0x0005) -NOT -> 11111111 11111111 11111111 11111010 = -6 (0xFFFA) - +1 -> 11111111 11111111 11111111 11111011 = -5 (0xFFFB) +Mantissa de 1.10011 => 10011 +Mantissa armazenada => 10011000000000000000000 #+end_example -** Interpretação do sinal - -Se o MSB de um número for =1=, o que acontecerá sempre que o byte (não bit) -mais significativo de seu valor em hexadecimal iniciar com =8= ou mais, o -número poderá ser interpretado como negativo ou positivo -- tudo depende -do contexto. Por exemplo, o número 2.147.483.648, se escrito com 4 bytes -em hexa, será =0x80000000= e seu equivalente binário será: +*Empacotamento dos campos* #+begin_example -10000000 00000000 00000000 00000000 = 0x80 00 00 00 = 2.147.483.648 +Sinal : 0 (positivo) +Expoente : 10000010 (130 = 127 + 3) +Mantissa : 10011000000000000000000 +Resultado: 01000001010011000000000000000000 => 0x41480000 em hexa #+end_example -Digamos que esse número seja escrito em =edx= (32 bits) e seja utilizado em -uma chamada de função que espera um argumento inteiro sem sinal. Neste caso, -o valor será tomado como positivo, mesmo que o binário inicie com =1=. Porém, -considere esta situação: +A forma empacotada é utilizada em operações de ponto flutuante com SSE/FPU +e para passar argumentos =float= para funções em C. Ela permite o uso de +instruções especializadas da arquitetura, mas requer decodificação +e empacotamento. -#+begin_src asm -mov eax, 0x80000000 -cmp eax, 0 -#+end_src +** Quando optar por uma ou outra alternativa -A instrução =cmp= calcula =eax - 0= e configura as flags de acordo com o valor -em =eax= e o resultado da operação: +Via de regra, nós só empacotamos para o formato IEEE 754 quando realmente +precisamos interagir com outros componentes que exijam o tipo /float/. Nas +situações mais comuns, a representação em escala é mais que suficiente. -- Flag =SF=: foi definida com =1= porque o MSB do resultado é =1= (negativo). -- Flag =OF=: como não houve /overflow/ na operação, seu valor foi definido como =0=. -- Flag =CF=: como a operação não provocou um "vai um" ou um empréstimo fora - do limite representável em 32 bits (capacidade de =eax=), seu valor foi - definido como =0=. +| Objetivo | Formato recomendado | +|---------------------------------+---------------------| +| Aritmética simples com inteiros | Escalado (/fixed/) | +| Comparações e fórmulas básicas | Escalado (/fixed/) | +| Compatibilidade com SSE/FPU | IEEE 754 | +| Chamada de função com "%f" | IEEE 754 | -Neste ponto, a interpretação do sinal do valor em =eax= vai depender do que -faremos com o resultado da comparação. Observe esta possibilidade: +** Dificuldades comuns -#+begin_src asm - mov eax, 0x80000000 - cmp eax, 0 ; resulta 0x80000000 -> SF=1 e OF=0 - jl .menor ; Se SF != OF, o salto acontece - ; ... -.menor: - ; ... -#+end_src +*** 1. Arredondamento de frações -A instrução =jl= (/jump if less/) avalia se as flags =SF= e =OF= são diferentes: se -forem, o salto é feito. Portanto, =jl= levaria em conta o sinal do resultado -da subtração e salto aconteceria porque =eax < 0=. Mas, e neste caso? +Ao representar "0.333" como inteiro escalado, há perda de precisão. Aumentar o +fator de escala (por exemplo, 2^30 em vez de 2^23) melhora a precisão, mas pode +causar /overflow/ em multiplicações. -#+begin_src asm - mov eax, 0x80000000 - cmp eax, 0 ; resulta 0x80000000 -> SF=1 e OF=0 - jb .menor ; Se CF = 1, o salto acontece - ; ... -.menor: - ; ... -#+end_src +*** 2. Erros ao interpretar IEEE como inteiro -Já a instrução =jb= (/jump if below/) só salta se =CF=1=, mas o resultado da -subtração (=eax-0=) não provocou um "vai um" ou um empréstimo fora do limite -do registrador: portanto, seu valor será =0= e o salto não acontecerá. Isso -significa que, como =jb= não leva em conta o sinal, =0x80000000= foi interpretado -como um valor acima de zero. +Se usarmos um /float/ empacotado (como =0x41200000=, que representa =10.0=) em uma +operação inteira, o valor será incorreto. Isso ocorre porque o padrão IEEE é +uma codificação binária, e não o valor numérico diretamente. -Por outro lado, se utilizássemos =rax= em vez de =eax=... +*** 3. Falta de suporte a ponto flutuante -#+begin_src asm - mov rax, 0x80000000 ; 0x0000000080000000 - cmp rax, 0 ; CF=0 SF=0 e OF=0 - jb .menor ; CF=0, não salta - jl .menor ; SF=OF, não salta - ; ... -.menor: - ; ... -#+end_src +Alguns ambientes minimalistas, como bootloaders ou sistemas bare-metal, +podem não ter suporte a SSE/FPU. Nestes casos, a representação escalada é +a única alternativa viável. -Desta forma, não há ambiguidades quanto à interpretação do sinal. +** Diferenças na conversão para double (64 bits) -** Uma nota sobre as flags +O tipo /double/ (também do padrão IEEE 754) ocupa 64 bits e permite maior +precisão e maior faixa de representação. Sua estrutura é similar à de um +/float/, mas com: -É importante entender corretamente o conceito por detrás da /carry flag/, -porque, em operações sem sinal, é ela que indica se um número é menor do -que outro. +- 1 bit para o sinal +- 11 bits para o expoente (~bias=1023~) +- 52 bits para a mantissa (com bit 1 implícito) -Então, vejamos: =10= é menor do que =14=? +Sendo assim, as operações de empacotamento e desempacotamento seguem o mesmo +processo do /float/, mas precisam trabalhar com registradores de 64 bits +completos. -#+begin_src asm -mov al, 10 ; 10 = 1010 = 0x0A -cmp al, 14 ; 14 = 1110 = 0xFE -#+end_src +A representação escalada também pode ser estendida para o caso de valores que +eventualmente serão convertidos para /double/, com os mesmos benefícios de +usar apenas instruções inteiras durante o processamento. Contudo, quando +demonstramos o uso do tipo /float/, nós adotamos o fator de escala =2^23=, +por ser compatível com o número de bits disponíveis para a mantissa. No caso +de /double/, porém, a mantissa tem 52 bits, o que permitiria escalar até =2^52= +(aproximadamente 4.5 × 10^15) sem perda de precisão por arredondamento. -A instrução =cmp= fará a subtração... +No entanto, escalar por =2^52= pode causar overflow em algumas multiplicações +se o valor base for muito grande. Assim, uma alternativa prática seria +escalar por =2^30=, =2^32= ou outro fator suficientemente alto, mas ainda seguro +para uso com registradores de 64 bits. -#+begin_example - Aqui, temos que fazer um empréstimo e o MSB de 10 será 0... - ↓ - 1010 -- 1110 ------- - 00 +** Sub-rotinas para demonstração - Agora é o MSB que requer um empréstimo! - ↓ - 0110 -- 1110 ------- - ?100 -#+end_example +Aqui estão duas sub-rotinas: -Numa situação desas, onde não há mais de onde emprestar um bit =1= mais a -esquerda, o processador efetua a subtração, como se houvesse de onde tirar -o bit 1 de que precisa, sobe a flag =CF= (indicando que houve um empréstimo -fora do limite dos operandos) e a flag =SF= (indicando que o resultado é -negativo). Ao mesmo tempo, =OF= e =ZF= receberiam =0=, porque não houve estouro -do limite do valor representável com sinal e o resultado não foi zero. +- =str_to_float_scaled=: converte strings no formato =.= para + representações de números de ponto flutuante como inteiros de 32 bits + escalados. -Na operação acima, nós teríamos: +- =str_to_float_ieee754=: converte strings no formato =.= para + representações de números de ponto empacotados no formato IEEE 754 de + 32 bits. -#+begin_example - CF=1 - ↓ - (1)0110 - - 1110 - ------ - 1100 = -4 ← ZF=0 (não é zero) - ↑ - SF=1 - OF=0 (é possível escrever em 4 bits) -#+end_example +Embora seja possível que elas funcionem corretamente, nenhuma delas chegou +a ser testada. Portanto, o exercício proposto para esta aula será: -#+begin_quote -O processador não possui circuitos capazes de efetuar subtrações e, por isso, -não executaria a operação desta forma, e sim calculando o complemento de dois -de =-14= (=0010=) para, em seguida, somar com =10=: ~1010+0010 = 1100 (-4)~. -#+end_quote +- Analisar o código de cada uma delas; +- Compreender e explicar as decisões tomadas; +- Estudar as instruções ainda desconhecidas; +- Elaborar formas de testá-las; +- Corrigir eventuais erros; +- Buscar formas de otimizá-las; +- Implementar versões para 64 bits (/double/). -** Limites de inteiros com sinal - -Assumindo que inteiros são escritos com 4 bytes, o maior valor absoluto -que podemos representar ainda é =0xFFFFFFFF=, mas ele teria que ser visto -como um número negativo. Calculando seu complemento de dois... - -#+begin_example - 11111111 11111111 11111111 11111111 (0xFFFFFFFF) -NOT -> 00000000 00000000 00000000 00000000 - +1 -> 00000000 00000000 00000000 00000001 => (1) -#+end_example - -Portanto, levando em conta o sinal, =0xFFFFFFFF= é =-1=. - -Como o bit mais significativo representa o sinal, o primeiro dígito do byte -mais significativo do maior número positivo que pode ser escrito em hexa tem -que ser =7=: - -#+begin_example -Maior byte positivo: 0x7f = 0111 1111 = 127 -#+end_example - -Se somarmos =1= a =0x7F=, nós chegaremos a =0x80= (128), que será interpretado como -um número negativo: - -#+begin_example -0x7F -> 0111 1111 - +1 -> 1000 0000 = 0x80 = -128 - ↑ ↑ - SF=1 -------------´ -#+end_example - -Então, se o inteiro com sinal tiver 1 byte, seus limites serão: - -#+begin_example -0x7F = 0111 1111 = +127 -0x7E = 0111 1110 = +126 -... -0x01 = 0000 0001 = +1 -0x00 = 0000 0000 = 0 -0xFF = 1111 1111 = -1 -... -0x81 = 1000 0001 = -127 -0x80 = 1000 0000 = -128 -#+end_example - -Transpondo o princípio para o tamanho de um inteiro na arquitetura x86_64, -os limites são: - -#+begin_example -0x7FFFFFFF -> 2.147.483.647 -0x80000000 -> -2.147.483.648 -#+end_example - -** Procedimento de conversão para inteiros com sinal - -As diferenças em relação à conversão de strings para inteiros sem sinal são: - -- Antes da conversão, nós precisamos detectar a presença dos caracteres - de sinal (=+= e =-=); -- Depois do valor absoluto encontrado, nós teremos que verificar se ele está - na faixa de valores inteiros com sinal (entre =0x7FFFFFFF= e =0x80000000=); -- No final, se o número passar na verificação, nós teremos que calcular seu - complemento de dois, o que pode ser feito multiplicando o valor por =-1= ou, - de forma ainda mais compacta e eficiente, utilizando a instrução =neg=. - -Todo o restante do procedimento é idêntico, tanto que podemos reutilizar -as mesmas funções de antes para a etapa de conversão. - -** Conversão em alto nível - -Com base no procedimento geral, esta seria uma forma de implementar a -conversão de inteiros com sinal em linguagem C reutilizando a função que -criamos para converter inteiros sem sinal: - -#+begin_src c -#include // Requerido para obter INT_MAX e INT_MIN da plataforma - -int str_to_sint(char *str, int *err) { - if (err) *err = 0; // Flag de estado de término: 0 = erro - if (str == NULL) return 0; // Teste do ponteiro para a string - - // Verificação e atrubuição do sinal da string... - char sig = '+'; // Sinal padrão - if (str[0] == '+' || str[0] == '-') { - sig = str[0]; - str++; // Avança o ponteiro da string - } - - // Chama a função de conversão de inteiros sem sinal... - unsigned conv = str_to_uint(str, err); - if (!*err) return 0; - - // Verificação da faixa de inteiros com sinal... - if (sig == '-') { - // INT_MIN=-2147483648, que em unsigned é 2147483648 - if (conv > (unsigned)INT_MAX + 1) return 0; - if (err) *err = 1; // Flag de estado de término: 1 = sucesso - return -(int)conv; - } else { - if (conv > INT_MAX) return 0; - if (err) *err = 1; // Flag de estado de término: 1 = sucesso - return (int)conv; - } -} -#+end_src - -#+begin_quote -Agora cabe a você, como parte dos exercícios propostos, elaborar e executar -os testes e os ajustes, se forem necessários. -#+end_quote - -** Conversão em baixo nível - -Esta seria uma solução equivalente implementada como uma sub-rotina que -depende de =_str_to_uint=, criada e testada anteriormente: +*** =str_to_float_scaled= #+begin_src asm ; ---------------------------------------------------------- -_str_to_sint: +; Converte uma string no formato . para float +; Entrada: RDI -> ponteiro para string válida +; Saída: EAX = número codificado como float (IEEE 754, 32 bits) +; Altera: RBX, RCX, RDX, RSI, R8, R9, R10 ; ---------------------------------------------------------- -; Converte string numérica com sinal em inteiro (int32_t). -; Entradas: -; RSI = Endereço da string (char *str) -; RDX = Endereço para estado de erro (int *err /* nullable */) -; Saída: -; RAX = valor convertido (int32_t) -; Sucesso: *rdx = 1 -; Erro: *rdx = 0 +str_to_float_scaled: ; ---------------------------------------------------------- - push rbx ; salva rbx - push rcx ; salva rcx - xor rcx, rcx ; rcx = 0 (sinal: 0 = positivo, 1 = negativo) - ; ------------------------------------------------------ - ; Testa ponteiro nulo - ; ------------------------------------------------------ - test rsi, rsi - jz .error - ; ------------------------------------------------------ - ; Verifica o sinal na string - ; ------------------------------------------------------ - mov bl, [rsi] - cmp bl, '-' ; caractere de sinal negativo? - jne .check_plus - inc rcx ; rcx = 1 => negativo - inc rsi ; avança o ponteiro - jmp .convert + push rbx ; salva rbx na pilha + push rsi ; salva rsi na pilha + mov rsi, rdi ; salva ponteiro da string em rsi + xor rbx, rbx ; RBX = parte inteira + xor rcx, rcx ; RCX = parte fracionária + xor rdx, rdx ; RDX = divisor decimal (potência de 10) + xor r8d, r8d ; sinal (0 = positivo, 1 = negativo) + ; -------------------------------------------------- + ; Verifica e armazena o sinal + ; -------------------------------------------------- + mov al, byte [rsi] + cmp al, '-' + jne .check_plus + inc rsi + mov r8b, 1 + jmp .read_integer .check_plus: - cmp bl, '+' ; caractere de sinal positivo? - jne .convert - inc rsi ; ignora '+' -.convert: - ; ------------------------------------------------------ - ; Chama _str_to_uint - ; ------------------------------------------------------ - call _str_to_uint ; RSI = str, RDX = err, RAX = resultado (uint32_t) - ; ------------------------------------------------------ - ; Verifica se _str_to_uint retornou erro - ; ------------------------------------------------------ - test rdx, rdx - jz .check_sign - cmp dword [rdx], 0 - je .error -.check_sign: - test rcx, rcx ; se rcx = 1, é número negativo - jz .check_range_pos - ; ------------------------------------------------------ - ; Verifica se valor cabe em int32_t negativo - ; ------------------------------------------------------ - cmp eax, 0x80000000 - ja .error ; se maior que INT_MAX + 1 -> erro + cmp al, '+' + jne .read_integer + inc rsi + ; -------------------------------------------------- + ; Lê parte inteira: + ; -------------------------------------------------- +.read_integer: + xor eax, eax ; eax = 0 +.read_int_loop: + mov al, byte [rsi] + cmp al, 0 ; fim da string + je .build_float + cmp al, '.' ; fim da parte inteira + je .read_fraction + sub al, '0' ; converte dígito em número + jb .build_float ; se al < 0x30, fim do número + cmp al, 9 + ja .build_float ; se al > 9, fim do número + imul rbx, rbx, 10 ; rbx = rbx * 10 + add bl, al ; bl = bl + al + inc rsi + jmp .read_int_loop + ; -------------------------------------------------- + ; Lê parte decimal: . + ; -------------------------------------------------- +.read_fraction: + inc rsi ; pula o ponto + xor ecx, ecx ; zera a parte decimal + mov edx, 1 ; divisor = 1 +.read_frac_loop: + mov al, byte [rsi] + cmp al, 0 ; número é xxx.0 + je .build_float + sub al, '0' ; converte dígito em número + jb .build_float ; se al < 0x30, fim do número + cmp al, 9 + ja .build_float ; se al > 9, fim do número + imul rcx, rcx, 10 ; rcx = rcx * 10 + add cl, al ; cl = cl + al + imul rdx, rdx, 10 ; divisor *= 10 + inc rsi + jmp .read_frac_loop + ; -------------------------------------------------- + ; Combina: float = inteiro + (fracao / divisor) + ; rbx = parte inteira (int) + ; rcx = parte fracionária (uint) + ; rdx = divisor decimal + ; converte parte inteira para float (em eax) + ; -------------------------------------------------- +.build_float: + mov eax, ebx + test r8b, r8b + jz .skip_negate_int ; número é positivo + neg eax ; aplica sinal de negativo + ; -------------------------------------------------- + ; converte parte decimal: (rcx / rdx) → aproximação inteira × escala + ; Aqui usamos multiplicação por 2^n e bit shift para simular uma fração + ; Exemplo: 123 / 1000 ≈ (123 << 23) / 1000 + ; -------------------------------------------------- +.skip_negate_int: + shl rcx, 23 + xor r9d, r9d + div rdx ; rcx / rdx (com resto em rdx), quociente em rax + mov r9d, eax ; parte decimal em r9d + ; -------------------------------------------------- + ; soma a parte inteira com a parte decimal (ambos em "escala flutuante") + ; -------------------------------------------------- + shl eax, 23 ; inteiro convertido para a mesma escala + add eax, r9d ; eax = pf aproximado * 2^23 + pop rsi + pop rbx + ret - neg eax ; aplica o sinal negativo - jmp .success -.check_range_pos: - ; ------------------------------------------------------ - ; Verifica se valor cabe em int32_t positivo - ; ------------------------------------------------------ - cmp eax, 0x7fffffff - ja .error ; maior que INT_MAX -> erro -.success: - test rdx, rdx - jz .done - mov dword [rdx], 1 ; *err = 1 - jmp .done -.error: - xor eax, eax ; valor de erro = 0 - test rdx, rdx - jz .done - mov dword [rdx], 0 ; *err = 0 -.done: - pop rcx - pop rbx - ret #+end_src -#+begin_quote -Também faz parte dos exercícios propostos, elaborar e executar os testes -e os ajustes, se forem necessários. -#+end_quote +*** =str_to_float_ieee754= -* Exercícios propostos +#+begin_src asm +; ---------------------------------------------------------- +; Converte uma string no formato . para float IEEE 754 (32 bits) +; Entrada: RDI -> ponteiro para string válida +; Saída: EAX = número codificado como float (IEEE 754, 32 bits) +; Altera: RBX, RCX, RDX, RSI, R8, R9, R10 +; ---------------------------------------------------------- +str_to_float_ieee754: +; ---------------------------------------------------------- + push rbx + push rsi + mov rsi, rdi ; ponteiro da string + xor rbx, rbx ; parte inteira + xor rcx, rcx ; parte fracionária + xor rdx, rdx ; divisor decimal (potência de 10) + xor r8d, r8d ; sinal (0 = positivo, 1 = negativo) + ; -------------------------------------------------- + ; Sinal + ; -------------------------------------------------- + mov al, byte [rsi] + cmp al, '-' + jne .check_plus + inc rsi + mov r8b, 1 + jmp .read_int +.check_plus: + cmp al, '+' + jne .read_int + inc rsi + ; -------------------------------------------------- + ; Parte inteira + ; -------------------------------------------------- +.read_int: + xor eax, eax +.read_int_loop: + mov al, byte [rsi] + cmp al, 0 + je .build_scaled + cmp al, '.' + je .read_frac + sub al, '0' + jb .build_scaled + cmp al, 9 + ja .build_scaled + imul rbx, rbx, 10 + add bl, al + inc rsi + jmp .read_int_loop + ; -------------------------------------------------- + ; Parte fracionária + ; -------------------------------------------------- +.read_frac: + inc rsi + xor ecx, ecx + mov edx, 1 +.read_frac_loop: + mov al, byte [rsi] + cmp al, 0 + je .build_scaled + sub al, '0' + jb .build_scaled + cmp al, 9 + ja .build_scaled + imul rcx, rcx, 10 + add cl, al + imul rdx, rdx, 10 + inc rsi + jmp .read_frac_loop + ; -------------------------------------------------- + ; Combina: inteiro + (frac/divisor) + ; Resultado com fator 2^23 + ; -------------------------------------------------- +.build_scaled: + mov eax, ebx + test r8b, r8b + jz .skip_neg + neg eax +.skip_neg: + shl rcx, 23 + xor r9d, r9d + div rdx ; rcx / rdx -> rax + mov r9d, eax ; parte fracionária escalada + shl eax, 23 ; inteiro * 2^23 + add eax, r9d ; valor total em escala 2^23 + ; -------------------------------------------------- + ; IEEE 754: empacotamento + ; -------------------------------------------------- + xor ecx, ecx + test eax, eax + jns .abs_ready + neg eax + mov ecx, 1 ; sinal = 1 +.abs_ready: + bsr r8d, eax ; MSB + mov edx, r8d + sub edx, 23 + add edx, 127 ; bias + mov ebx, eax + mov ecx, r8d + sub ecx, 23 + shl ebx, cl + and ebx, 0x7FFFFF ; mantissa -1. Tanto a função quanto a sub-rotina têm alguns problemas práticos não contemplados, como: - - Não tratam strings iniciadas com espaços em branco; - - Não tratam números com zeros à esquerda. - Inclua esses tratamentos em ambas as implementações de conversão. -2. Altere o valor em =num_str=, nos testes da sub-rotina =_str_to_uint=, e - analise os resultados com o GDB. -3. Em C, crie as funções =str_to_ulong= (string para inteiros longos sem sinal) - e =str_to_slong= (string para inteiros longos com sinal). -4. Em Assembly, crie as sub-rotinas =_str_to_ulong= e =_str_to_slong=. -5. Implemente predicados (funções que retornam verdadeiro ou falso) para detectar: - - Se a string representa um número (=is_num=); - - Se o número representado é do tipo =int= (=is_int=); - - Se o número representado é do tipo =long= (=is_long=); - - Se o número representado é positivo (=is_pos=). -6. Só para aproveitar o embalo, crie uma função para detectar se uma cadeia de - caracteres é uma string (=is_string=). + shl r8d, 31 ; sinal + shl edx, 23 ; expoente + or eax, edx + or eax, ebx + or eax, r8d + pop rsi + pop rbx + ret +#+end_src + +* Referências + +- [[https://www-users.cse.umn.edu/~vinals/tspot_files/phys4041/2020/IEEE%20Standard%20754-2019.pdf?utm_source=chatgpt.com][IEEE Standard for Floating-Point Arithmetic]] +- [[https://en.wikipedia.org/wiki/IEEE_754?utm_source=chatgpt.com][Wikipedia: IEEE 754]]