Saltar a contenido

Punto Flotante (Floating Point)

Introducción

Los números de punto flotante son fundamentales en programación para representar valores decimales. En C/C++, existen dos tipos principales: float (precisión simple) y double (precisión doble). Aunque ambos almacenan números con decimales, difieren significativamente en precisión, tamaño en memoria y rango de valores que pueden representar.

Float vs Double: Comparación Básica

Float (Precisión Simple)

  • Tamaño: 4 bytes (32 bits)
  • Precisión: Hasta 7 decimales sin pérdida de precisión
  • Rango: 3.4 × 10⁻³⁸ a 3.4 × 10³⁸
  • Especificador de formato: %f
  • Identificador literal: f (ejemplo: 789.123456f)

Double (Precisión Doble)

  • Tamaño: 8 bytes (64 bits)
  • Precisión: Hasta 15 decimales sin pérdida de precisión
  • Rango: 1.7 × 10⁻³⁰⁸ a 1.7 × 10³⁰⁸
  • Especificador de formato: %lf
  • Identificador literal: Sin sufijo (por defecto son double)

Diferencias de Precisión

Un ejemplo práctico muestra claramente la diferencia:

float f = 789.123456f;   // Se redondea a 789.123474
double d = 789.123456;   // Se mantiene como 789.123456

El float pierde precisión en los últimos dígitos debido a su limitación de bits. Cuando trabajamos con valores decimales sin especificar el tipo, C los toma como double por defecto. Para forzar a float, debe agregarse la letra f al final del literal.


Representación en Memoria (IEEE 754)

C sigue el estándar IEEE 754 para representar números de punto flotante en memoria. A diferencia de los enteros que se almacenan directamente en binario, los números flotantes se dividen en componentes específicos.

Componentes de Representación IEEE 754

Todos los números flotantes (float o double) constan de tres partes:

  1. Bit de Signo (Sign Bit): El bit más significativo (MSB)
  2. 0 = número positivo
  3. 1 = número negativo

  4. Exponente Sesgado (Biased Exponent): Determina la magnitud del número

  5. No se almacena directamente porque el exponente puede ser negativo o positivo
  6. Se suma un sesgo (bias) para convertirlo en positivo

  7. Mantisa Normalizada (Mantissa/Significand): Los bits de precisión del número

  8. Representa la parte fraccionaria del número en notación científica

Float - Estructura de 32 bits

Componente Bits Total
Signo 1 1 bit
Exponente 8 8 bits
Mantisa 23 23 bits
Total - 32 bits

Sesgo para float: 127

Estructura visual:

[S] [EEEEEEEE] [MMMMMMMMMMMMMMMMMMMMMMM]
 1      8              23

Double - Estructura de 64 bits

Componente Bits Total
Signo 1 1 bit
Exponente 11 11 bits
Mantisa 52 52 bits
Total - 64 bits

Sesgo para double: 1023

Estructura visual:

[S] [EEEEEEEEEEE] [MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM]
 1       11                              52


Proceso de Conversión: Decimal → Flotante → Hexadecimal

Para entender cómo se almacena un número decimal en memoria como número de punto flotante, seguimos estos pasos:

Ejemplo: Convertir 789.123456 a Float

Paso 1: Convertir a Binario

Dividimos el número en parte entera y parte decimal:

  • 789 (entero) en binario: 1100010101
  • 0.123456 (decimal) en binario: 0001111110010111...

Combinado: 1100010101.0001111110010111...

Paso 2: Normalizar a Notación Científica Binaria

La notación científica binaria requiere que el número esté en la forma 1.XXXXX × 2^n

Corremos el punto decimal hasta tener un 1 como primer dígito:

1100010101.0001111110010111...
= 1.1000101010001111110010111... × 2^9

Se desplazó 9 lugares, por lo que el exponente es 9.

Paso 3: Extraer Componentes

  • Signo: 0 (es positivo)
  • Exponente (sin sesgo): 9
  • Exponente sesgado (para float): 9 + 127 = 136
  • Exponente sesgado en binario: 10001000
  • Mantisa: Tomamos los bits después del punto: 10001010000111111... (rellenado a 23 bits)

Paso 4: Armar el Número en Binario

Signo | Exponente    | Mantisa
  0   | 10001000     | 10001010000111111100000

Combinado en 32 bits (sin espacios):

01000100010001010000111111100000

Paso 5: Convertir a Hexadecimal

Agrupamos en bloques de 4 bits:

0100 0100 0100 0101 0000 1111 1110 0000
  4    4    4    5    0    F    E    0

Resultado: 0x4445FE00 (aproximadamente, puede variar según el redondeo)

Conversión de Double

El proceso es idéntico, pero con diferencias en los tamaños:

  • Sesgo: 1023 (en lugar de 127)
  • Exponente: 11 bits
  • Mantisa: 52 bits

Para el mismo número 789.123456 como double:

Exponente sesgado: 9 + 1023 = 1032 = 10000000100 (binario)


Inspección en Bajo Nivel

Usando IDA o Debugger

Cuando se trabaja a nivel de ensamblador, los registros XMM (registros de punto flotante) almacenan estos valores. Usando herramientas como IDA Pro o Visual Studio Debugger, podemos:

  1. Ver el valor en hexadecimal: Nos muestra exactamente cómo se representa en memoria
  2. Ver el valor como float/double: La herramienta convierte automáticamente al decimal
  3. Cambiar la visualización: Podemos cambiar entre tipos de datos en el inspector

Instrucciones Comunes de Punto Flotante

Instrucción Operación
CVTSS2SD Convertir Single (float) a Double
MOVSD Mover Double
MOVSS Mover Single (float)
ADDSD Sumar Double
MULSS Multiplicar Single (float)

Extracción de Componentes en Código

En C, podemos extraer los componentes de un float usando operaciones de bits:

// Obtener el bit de signo
int sign = (value >> 31) & 1;

// Obtener el exponente (8 bits)
int exponent = (value >> 23) & 0xFF;

// Obtener la mantisa (23 bits)
int mantissa = value & 0x7FFFFF;

Para double, los rangos son diferentes:

// En un double (64 bits)
int sign = (value >> 63) & 1;
int exponent = (value >> 52) & 0x7FF;  // 11 bits
long long mantissa = value & 0xFFFFFFFFFFFFF;  // 52 bits


Perdida de Precisión

Por Qué Ocurre

El número de bits limitado en la mantisa restringe cuántos dígitos significativos se pueden almacenar con exactitud:

  • Float: 23 bits de mantisa = aproximadamente 7-8 dígitos significativos
  • Double: 52 bits de mantisa = aproximadamente 15-17 dígitos significativos

Si un número tiene más dígitos significativos que los que caben en la mantisa, se debe redondear.

Ejemplo Práctico

789.123456f  →  789.123474 (pérdida en el 6º decimal)
789.123456   →  789.123456 (se mantiene exacto)

Recomendaciones

  • Usa float cuando el espacio en memoria es limitado y la precisión puede comprometerse
  • Usa double para cálculos científicos, financieros o que requieran alta precisión
  • Por defecto, siempre es double a menos que especifiques f

Tabla Comparativa Detallada

Característica Float Double
Precisión 7 decimales 15 decimales
Tamaño 32 bits (4 bytes) 64 bits (8 bytes)
Rango 3.4 × 10⁻³⁸ a 3.4 × 10³⁸ 1.7 × 10⁻³⁰⁸ a 1.7 × 10³⁰⁸
Especificador %f %lf
Bits de signo 1 1
Bits de exponente 8 11
Bits de mantisa 23 52
Sesgo del exponente 127 1023
Literal valor_f valor

Notas Importantes para Reverse Engineering

  1. Los registros XMM: Las operaciones de punto flotante usan registros XMM (SSE/AVX), no los registros de propósito general

  2. Conversiones automáticas: Cuando ves CVTSS2SD, se está convirtiendo de float a double. El compilador puede hacer esto automáticamente

  3. Visualización en herramientas: IDA y debuggers modernos pueden mostrar automáticamente valores hex como float/double

  4. Tamaños en ensamblador:

  5. dword = 32 bits = float
  6. qword = 64 bits = double

  7. Cálculos manuales: No necesitas memorizar la conversión. Las herramientas hacen el trabajo, pero entender el proceso te ayuda a comprender qué está sucediendo en bajo nivel


Referencias