¿Cómo se ven qNaN y sNaN experimentalmente?
Primero aprendamos a identificar si tenemos un sNaN o un qNaN.
Usaré C ++ en esta respuesta en lugar de C porque ofrece lo conveniente std::numeric_limits::quiet_NaN
y std::numeric_limits::signaling_NaN
que no pude encontrar en C convenientemente.
Sin embargo, no pude encontrar una función para clasificar si un NaN es sNaN o qNaN, así que imprimamos los bytes sin formato de NaN:
main.cpp
#include <cassert>
#include <cstring>
#include <cmath> // nanf, isnan
#include <iostream>
#include <limits> // std::numeric_limits
#pragma STDC FENV_ACCESS ON
void print_float(float f) {
std::uint32_t i;
std::memcpy(&i, &f, sizeof f);
std::cout << std::hex << i << std::endl;
}
int main() {
static_assert(std::numeric_limits<float>::has_quiet_NaN, "");
static_assert(std::numeric_limits<float>::has_signaling_NaN, "");
static_assert(std::numeric_limits<float>::has_infinity, "");
// Generate them.
float qnan = std::numeric_limits<float>::quiet_NaN();
float snan = std::numeric_limits<float>::signaling_NaN();
float inf = std::numeric_limits<float>::infinity();
float nan0 = std::nanf("0");
float nan1 = std::nanf("1");
float nan2 = std::nanf("2");
float div_0_0 = 0.0f / 0.0f;
float sqrt_negative = std::sqrt(-1.0f);
// Print their bytes.
std::cout << "qnan "; print_float(qnan);
std::cout << "snan "; print_float(snan);
std::cout << " inf "; print_float(inf);
std::cout << "-inf "; print_float(-inf);
std::cout << "nan0 "; print_float(nan0);
std::cout << "nan1 "; print_float(nan1);
std::cout << "nan2 "; print_float(nan2);
std::cout << " 0/0 "; print_float(div_0_0);
std::cout << "sqrt "; print_float(sqrt_negative);
// Assert if they are NaN or not.
assert(std::isnan(qnan));
assert(std::isnan(snan));
assert(!std::isnan(inf));
assert(!std::isnan(-inf));
assert(std::isnan(nan0));
assert(std::isnan(nan1));
assert(std::isnan(nan2));
assert(std::isnan(div_0_0));
assert(std::isnan(sqrt_negative));
}
Compila y ejecuta:
g++ -ggdb3 -O3 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out
salida en mi máquina x86_64:
qnan 7fc00000
snan 7fa00000
inf 7f800000
-inf ff800000
nan0 7fc00000
nan1 7fc00001
nan2 7fc00002
0/0 ffc00000
sqrt ffc00000
También podemos ejecutar el programa en aarch64 con el modo de usuario QEMU:
aarch64-linux-gnu-g++ -ggdb3 -O3 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp
qemu-aarch64 -L /usr/aarch64-linux-gnu/ main.out
y eso produce exactamente el mismo resultado, lo que sugiere que varios arcos implementan de cerca IEEE 754.
En este punto, si no está familiarizado con la estructura de los números de punto flotante IEEE 754, eche un vistazo a: ¿Qué es un número de punto flotante subnormal?
En binario, algunos de los valores anteriores son:
31
|
| 30 23 22 0
| | | | |
-----+-+------+-+---------------------+
qnan 0 11111111 10000000000000000000000
snan 0 11111111 01000000000000000000000
inf 0 11111111 00000000000000000000000
-inf 1 11111111 00000000000000000000000
-----+-+------+-+---------------------+
| | | | |
| +------+ +---------------------+
| | |
| v v
| exponent fraction
|
v
sign
De este experimento observamos que:
qNaN y sNaN parecen diferenciarse solo por el bit 22: 1 significa silencioso y 0 significa señalización
los infinitos también son bastante similares con exponente == 0xFF, pero tienen fracción == 0.
Por esta razón, los NaN deben establecer el bit 21 en 1; de lo contrario, no sería posible distinguir sNaN del infinito positivo.
nanf()
produce varios NaN diferentes, por lo que debe haber varias codificaciones posibles:
7fc00000
7fc00001
7fc00002
Dado que nan0
es lo mismo que std::numeric_limits<float>::quiet_NaN()
, deducimos que todos son NaN silenciosos diferentes.
El borrador estándar C11 N1570 confirma que nanf()
genera NaN silenciosos, porque nanf
reenvía strtod
ay 7.22.1.3 "Las funciones strtod, strtof y strtold" dice:
Una secuencia de caracteres NAN o NAN (n-char-sequence opt) se interpreta como un NaN silencioso, si se admite en el tipo de retorno, o como una parte de la secuencia del sujeto que no tiene la forma esperada; el significado de la secuencia n-char está definido por la implementación. 293)
Ver también:
¿Qué aspecto tienen los qNaN y sNaN en los manuales?
IEEE 754 2008 recomienda que (TODO obligatorio u opcional?):
- cualquier cosa con exponente == 0xFF y fracción! = 0 es un NaN
- y que el bit de la fracción más alta diferencia qNaN de sNaN
pero no parece decir qué bit se prefiere para diferenciar el infinito de NaN.
6.2.1 "Codificaciones NaN en formatos binarios" dice:
Esta subcláusula especifica además las codificaciones de NaN como cadenas de bits cuando son el resultado de operaciones. Cuando se codifican, todos los NaN tienen un bit de signo y un patrón de bits necesarios para identificar la codificación como un NaN y que determina su tipo (sNaN frente a qNaN). Los bits restantes, que se encuentran en el campo de significación final, codifican la carga útil, que podría ser información de diagnóstico (ver arriba). 34
Todas las cadenas de bits binarias NaN tienen todos los bits del campo de exponente polarizado E puestos a 1 (ver 3.4). Una cadena de bits NaN silenciosa debe codificarse con el primer bit (d1) del campo de significación final T siendo 1. Una cadena de bits de señalización NaN debe codificarse con el primer bit del campo de significación final como 0. Si el primer bit del El campo de significación final es 0, algún otro bit del campo de significación final debe ser distinto de cero para distinguir el NaN del infinito. En la codificación preferida que se acaba de describir, una señalización NaN se silenciará poniendo d1 en 1, dejando los bits restantes de T sin cambios. Para formatos binarios, la carga útil se codifica en los p − 2 bits menos significativos del campo de significación final
El Manual del desarrollador de software de arquitecturas Intel 64 e IA-32 - Volumen 1 Arquitectura básica - 253665-056ES Septiembre de 2015 4.8.3.4 "NaNs" confirma que x86 sigue IEEE 754 al distinguir NaN y sNaN por el bit de fracción más alto:
La arquitectura IA-32 define dos clases de NaN: NaN silenciosos (QNaN) y NaN de señalización (SNaN). Un QNaN es un NaN con el bit de fracción más significativo establecido, un SNaN es un NaN con el bit de fracción más significativo libre.
y también lo hace el Manual de referencia de arquitectura ARM - ARMv8, para el perfil de arquitectura ARMv8-A - DDI 0487C.a A1.4.3 "Formato de punto flotante de precisión simple":
fraction != 0
: El valor es un NaN y es un NaN silencioso o un NaN de señalización. Los dos tipos de NaN se distinguen por su bit de fracción más significativa, el bit [22]:
bit[22] == 0
: El NaN es un NaN de señalización. El bit de signo puede tomar cualquier valor y los bits de fracción restantes pueden tomar cualquier valor excepto todos los ceros.
bit[22] == 1
: El NaN es un NaN silencioso. El bit de signo y los bits de fracción restantes pueden tomar cualquier valor.
¿Cómo se generan qNanS y sNaNs?
Una diferencia importante entre qNaN y sNaN es que:
- qNaN se genera mediante operaciones aritméticas incorporadas (software o hardware) regulares con valores extraños
- sNaN nunca se genera mediante operaciones integradas, solo los programadores pueden agregarlo explícitamente, por ejemplo, con
std::numeric_limits::signaling_NaN
No pude encontrar citas claras de IEEE 754 o C11 para eso, pero tampoco puedo encontrar ninguna operación incorporada que genere sNaN ;-)
Sin embargo, el manual de Intel establece claramente este principio en 4.8.3.4 "NaN":
Los SNaN se utilizan normalmente para capturar o invocar un controlador de excepciones. Deben insertarse mediante software; es decir, el procesador nunca genera un SNaN como resultado de una operación de punto flotante.
Esto se puede ver en nuestro ejemplo donde ambos:
float div_0_0 = 0.0f / 0.0f;
float sqrt_negative = std::sqrt(-1.0f);
producir exactamente los mismos bits que std::numeric_limits<float>::quiet_NaN()
.
Ambas operaciones se compilan en una sola instrucción de ensamblaje x86 que genera el qNaN directamente en el hardware (TODO confirmar con GDB).
¿Qué hacen los qNaN y los sNaN de manera diferente?
Ahora que sabemos cómo son los qNaN y sNaN, y cómo manipularlos, ¡finalmente estamos listos para intentar hacer que los sNaN hagan lo suyo y explotar algunos programas!
Así que sin más preámbulos:
blow_up.cpp
#include <cassert>
#include <cfenv>
#include <cmath> // isnan
#include <iostream>
#include <limits> // std::numeric_limits
#include <unistd.h>
#pragma STDC FENV_ACCESS ON
int main() {
float snan = std::numeric_limits<float>::signaling_NaN();
float qnan = std::numeric_limits<float>::quiet_NaN();
float f;
// No exceptions.
assert(std::fetestexcept(FE_ALL_EXCEPT) == 0);
// Still no exceptions because qNaN.
f = qnan + 1.0f;
assert(std::isnan(f));
if (std::fetestexcept(FE_ALL_EXCEPT) == FE_INVALID)
std::cout << "FE_ALL_EXCEPT qnan + 1.0f" << std::endl;
// Now we can get an exception because sNaN, but signals are disabled.
f = snan + 1.0f;
assert(std::isnan(f));
if (std::fetestexcept(FE_ALL_EXCEPT) == FE_INVALID)
std::cout << "FE_ALL_EXCEPT snan + 1.0f" << std::endl;
feclearexcept(FE_ALL_EXCEPT);
// And now we enable signals and blow up with SIGFPE! >:-)
feenableexcept(FE_INVALID);
f = qnan + 1.0f;
std::cout << "feenableexcept qnan + 1.0f" << std::endl;
f = snan + 1.0f;
std::cout << "feenableexcept snan + 1.0f" << std::endl;
}
Compile, ejecute y obtenga el estado de salida:
g++ -ggdb3 -O0 -Wall -Wextra -pthread -std=c++11 -pedantic-errors -o blow_up.out blow_up.cpp -lm -lrt
./blow_up.out
echo $?
Salida:
FE_ALL_EXCEPT snan + 1.0f
feenableexcept qnan + 1.0f
Floating point exception (core dumped)
136
Tenga en cuenta que este comportamiento solo ocurre con -O0
GCC 8.2: con -O3
, GCC precalcula y optimiza todas nuestras operaciones sNaN. No estoy seguro de si existe una forma estándar de prevenir eso.
Entonces deducimos de este ejemplo que:
snan + 1.0
causa FE_INVALID
, pero qnan + 1.0
no
Linux solo genera una señal si está habilitado con feenableexept
.
Esta es una extensión glibc, no pude encontrar ninguna forma de hacerlo en ningún estándar.
Cuando ocurre la señal, es porque el propio hardware de la CPU genera una excepción, que el kernel de Linux manejó e informó a la aplicación a través de la señal.
El resultado es que bash imprime Floating point exception (core dumped)
, y el estado de salida es 136
, que corresponde a la señal 136 - 128 == 8
, que de acuerdo con:
man 7 signal
es SIGFPE
.
Tenga en cuenta que SIGFPE
es la misma señal que obtenemos si intentamos dividir un número entero entre 0:
int main() {
int i = 1 / 0;
}
aunque para enteros:
- dividir cualquier cosa por cero aumenta la señal, ya que no hay representación infinita en números enteros
- la señal ocurre por defecto, sin necesidad de
feenableexcept
¿Cómo manejar el SIGFPE?
Si solo crea un controlador que regresa normalmente, conduce a un bucle infinito, porque después de que el controlador regresa, ¡la división vuelve a ocurrir! Esto se puede verificar con GDB.
La única forma es usar setjmp
y longjmp
saltar a otro lugar como se muestra en: C maneja la señal SIGFPE y continúa la ejecución
¿Cuáles son algunas de las aplicaciones del mundo real de sNaN?
Honestamente, todavía no he entendido un caso de uso súper útil para sNaN, esto se ha preguntado en: ¿ Utilidad de la señalización de NaN?
Los sNaN se sienten particularmente inútiles porque podemos detectar las operaciones no válidas iniciales ( 0.0f/0.0f
) que generan qNaN con feenableexcept
: parece que snan
solo genera errores para más operaciones que qnan
no generan , por ejemplo, ( qnan + 1.0f
).
P.ej:
C Principal
#define _GNU_SOURCE
#include <fenv.h>
#include <stdio.h>
int main(int argc, char **argv) {
(void)argv;
float f0 = 0.0;
if (argc == 1) {
feenableexcept(FE_INVALID);
}
float f1 = 0.0 / f0;
printf("f1 %f\n", f1);
feenableexcept(FE_INVALID);
float f2 = f1 + 1.0;
printf("f2 %f\n", f2);
}
compilar:
gcc -ggdb3 -O0 -std=c99 -Wall -Wextra -pedantic -o main.out main.c -lm
luego:
./main.out
da:
Floating point exception (core dumped)
y:
./main.out 1
da:
f1 -nan
f2 -nan
Vea también: Cómo rastrear un NaN en C ++
¿Qué son las banderas de señales y cómo se manipulan?
Todo está implementado en el hardware de la CPU.
Las banderas viven en algún registro, al igual que el bit que dice si se debe generar una excepción / señal.
Esos registros son accesibles desde la tierra del usuario desde la mayoría de los archivos.
¡Esta parte del código glibc 2.29 es realmente muy fácil de entender!
Por ejemplo, fetestexcept
se implementa para x86_86 en sysdeps / x86_64 / fpu / ftestexcept.c :
#include <fenv.h>
int
fetestexcept (int excepts)
{
int temp;
unsigned int mxscr;
/* Get current exceptions. */
__asm__ ("fnstsw %0\n"
"stmxcsr %1" : "=m" (*&temp), "=m" (*&mxscr));
return (temp | mxscr) & excepts & FE_ALL_EXCEPT;
}
libm_hidden_def (fetestexcept)
por lo que inmediatamente vemos que las instrucciones de uso son stmxcsr
las siglas de "Store MXCSR Register State".
Y feenableexcept
se implementa en sysdeps / x86_64 / fpu / feenablxcpt.c :
#include <fenv.h>
int
feenableexcept (int excepts)
{
unsigned short int new_exc, old_exc;
unsigned int new;
excepts &= FE_ALL_EXCEPT;
/* Get the current control word of the x87 FPU. */
__asm__ ("fstcw %0" : "=m" (*&new_exc));
old_exc = (~new_exc) & FE_ALL_EXCEPT;
new_exc &= ~excepts;
__asm__ ("fldcw %0" : : "m" (*&new_exc));
/* And now the same for the SSE MXCSR register. */
__asm__ ("stmxcsr %0" : "=m" (*&new));
/* The SSE exception masks are shifted by 7 bits. */
new &= ~(excepts << 7);
__asm__ ("ldmxcsr %0" : : "m" (*&new));
return old_exc;
}
¿Qué dice el estándar C sobre qNaN vs sNaN?
El borrador del estándar C11 N1570 dice explícitamente que el estándar no diferencia entre ellos en F.2.1 "Infinitos, ceros con signo y NaN":
1 Esta especificación no define el comportamiento de la señalización de NaN. Generalmente usa el término NaN para denotar NaN silenciosos. Las macros NAN e INFINITY y las funciones nan <math.h>
proporcionan designaciones para IEC 60559 NaN e infinitos.
Probado en Ubuntu 18.10, GCC 8.2. GitHub upstream: