Cuando una operación da como resultado un NaN silencioso, no hay indicios de que algo sea inusual hasta que el programa verifica el resultado y ve un NaN. Es decir, el cálculo continúa sin ninguna señal de la unidad de punto flotante (FPU) o biblioteca si el punto flotante está implementado en el software. Un NaN de señalización producirá una señal, generalmente en forma de excepción de la FPU. Si se lanza la excepción depende del estado de la FPU.
C ++ 11 agrega algunos controles de lenguaje sobre el entorno de punto flotante y proporciona formas estandarizadas de crear y probar NaN . Sin embargo, si los controles se implementan no está bien estandarizado y las excepciones de punto flotante no se detectan de la misma manera que las excepciones estándar de C ++.
En los sistemas POSIX / Unix, las excepciones de punto flotante generalmente se detectan usando un controlador para SIGFPE .
¿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
ystd::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
Compila y ejecuta:
salida en mi máquina x86_64:
También podemos ejecutar el programa en aarch64 con el modo de usuario QEMU:
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:
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:Dado que
nan0
es lo mismo questd::numeric_limits<float>::quiet_NaN()
, deducimos que todos son NaN silenciosos diferentes.El borrador estándar C11 N1570 confirma que
nanf()
genera NaN silenciosos, porquenanf
reenvíastrtod
ay 7.22.1.3 "Las funciones strtod, strtof y strtold" dice:Ver también:
¿Qué aspecto tienen los qNaN y sNaN en los manuales?
IEEE 754 2008 recomienda que (TODO obligatorio u opcional?):
pero no parece decir qué bit se prefiere para diferenciar el infinito de NaN.
6.2.1 "Codificaciones NaN en formatos binarios" dice:
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:
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:
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":
Esto se puede ver en nuestro ejemplo donde ambos:
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
Compile, ejecute y obtenga el estado de salida:
Salida:
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
causaFE_INVALID
, peroqnan + 1.0
noLinux 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 es136
, que corresponde a la señal136 - 128 == 8
, que de acuerdo con:es
SIGFPE
.Tenga en cuenta que
SIGFPE
es la misma señal que obtenemos si intentamos dividir un número entero entre 0:aunque para enteros:
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
ylongjmp
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 confeenableexcept
: parece quesnan
solo genera errores para más operaciones queqnan
no generan , por ejemplo, (qnan + 1.0f
).P.ej:
C Principal
compilar:
luego:
da:
y:
da:
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 :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 :¿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":
Probado en Ubuntu 18.10, GCC 8.2. GitHub upstream:
fuente