c++ - ¿En qué plataformas el número entero dividido por cero desencadena una excepción de punto flotante?
error-handling x86-64 (3)
En otra pregunta, alguien se preguntaba por qué estaban obteniendo un "error de coma flotante" cuando, de hecho, tenían una división entera por cero en su programa C ++. Surgió una discusión sobre esto, con algunas afirmaciones de que las excepciones de coma flotante nunca se plantean para la división flotante por cero, sino que solo surgen en la división entera por cero.
Esto me suena extraño, porque sé que:
-
El código compilado por MSVC en x86 y x64 en todas las plataformas Windows informa una división interna por cero como "0xc0000094: división entera por cero", y división flotante por cero como 0xC000008E "división de punto flotante por cero" (cuando está habilitado)
-
Los ISA IA-32 y AMD64 especifican
#DE
(excepción de división de enteros) como interrupción 0. Las excepciones de punto flotante activan la interrupción 16 (punto flotante x87) o la interrupción 19 (punto flotante SIMD). -
Otro hardware tiene interrupciones igualmente diferentes ( por ejemplo, PPC aumenta 0x7000 en float-div-by-zero y no atrapa para int / 0 en absoluto).
-
Nuestra aplicación desenmascara las excepciones de coma flotante para dividir por cero con el
_controlfp_s
intrínseco (en última instancia,stmxcsr
op) y luego las captura con fines de depuración. Así que definitivamente he visto excepciones IEEE754 de división por cero en la práctica.
Así que concluyo que hay algunas plataformas que informan excepciones int como excepciones de coma flotante, como x64 Linux (elevar SIGFPE para todos los errores aritméticos independientemente de la tubería ALU) .
¿Qué otros sistemas operativos (o tiempos de ejecución C / C ++ si usted es el sistema operativo) informan el número entero div-by-zero como una excepción de punto flotante?
¿Qué otros sistemas operativos (o tiempos de ejecución C / C ++ si usted es el sistema operativo) informan el número entero div-by-zero como una excepción de punto flotante?
La respuesta depende de si está en el
espacio del kernel
o en
el espacio del usuario
.
Si está en el espacio del kernel, puede poner "i / 0" en
kernel_main()
, hacer que su controlador de interrupciones llame a un controlador de excepciones y detenga su kernel.
Si está en el espacio de usuario, la respuesta depende de su sistema operativo y la configuración del compilador.
El hardware AMD64 especifica la división entera entre cero como interrupción 0, diferente de la interrupción 16 (excepción de punto flotante x87) y la interrupción 19 (excepción de punto flotante SIMD).
La excepción "Dividir por cero" es dividir por cero con la instrucción
div
.
Discutir la x87 FPU está fuera del alcance de esta pregunta.
Otro hardware tiene interrupciones igualmente diferentes (por ejemplo, PPC aumenta 0x7000 en float-div-by-zero y no atrapa para int / 0 en absoluto).
Más específicamente,
00700
se asigna al tipo de excepción "Programa", que
incluye
una excepción habilitada de coma flotante.
Dicha excepción se genera si se intenta dividir por cero utilizando una instrucción de coma flotante.
Por otro lado, la división de enteros es un comportamiento indefinido según el PPC PEM:
8-53 divw
Si se intenta realizar cualquiera de las divisiones: 0x8000_0000 ÷ –1 o ÷ 0, entonces el contenido de rD no está definido, al igual que el contenido de los bits LT, GT y EQ del campo CR0 (si Rc = 1 ) En este caso, si OE = 1, entonces se establece OV.
Nuestra aplicación desenmascara las excepciones de coma flotante para dividir por cero con el
_controlfp_s
intrínseco (en última instancia, stmxcsr op) y luego las captura con fines de depuración. Así que definitivamente he visto excepciones IEEE754 de división por cero en la práctica.
Creo que es mejor gastar su tiempo capturando dividir por cero en tiempo de compilación en lugar de en tiempo de ejecución.
No estoy seguro de cómo surgió la situación actual, pero actualmente es el caso de que el soporte de detección de excepciones de FP es muy diferente del entero.
Es común que la división de enteros quede atrapada.
POSIX requiere que
SIGFPE
si genera una excepción.
Sin embargo, puede resolver qué tipo de SIGFPE era, para ver que en realidad era una excepción de división.
(Sin embargo, no necesariamente divide por cero: las trampas de división
INT_MIN
/
-1
del complemento de 2, y
div
e
idiv
también se atrapan cuando el cociente de la división 64b / 32b no cabe en el registro de salida 32b).
El
manual de glibc explica
que los sistemas BSD y GNU entregan un
SIGFPE
adicional al manejador de señal para
SIGFPE
, que será
FPE_INTDIV_TRAP
para dividir por cero.
POSIX documenta
FPE_INTDIV_TRAP
como un posible valor para el
siginfo_t
int si_code
, en sistemas donde
siginfo_t
incluye ese miembro.
IDK si Windows ofrece una excepción diferente en primer lugar, o si agrupa cosas en diferentes sabores de la misma excepción aritmética como lo hace Unix. Si es así, el controlador predeterminado decodifica la información adicional para decirle qué tipo de excepción fue.
POSIX y Windows usan la frase "división por cero" para cubrir todas las excepciones de división entera, por lo que aparentemente esto es una abreviatura común. Para las personas que saben que INT_MIN / -1 (con el complemento de 2) es un problema, la frase "división por cero" se puede tomar como sinónimo de una excepción de división. La frase señala de inmediato el caso común para las personas que no saben por qué la división de enteros podría ser un problema.
Semántica de excepciones FP
Las excepciones de FP están enmascaradas de manera predeterminada para los procesos de espacio de usuario en la mayoría de los sistemas operativos / C ABI.
Esto tiene sentido, porque el punto flotante IEEE puede representar infinitos y tiene NaN para propagar el error a todos los cálculos futuros utilizando el valor.
-
0.0/0.0
=>NaN
-
Si
x
es finito:x/0.0
=>+/-Inf
con el signo de x
Esto incluso permite que cosas como esta produzcan un resultado sensible cuando se ocultan excepciones:
double x = 0.0;
double y = 1.0/x; // y = +Inf
double z = 1.0/y; // z = 1/Inf = 0.0, no FP exception
FP vs. detección de errores enteros
La forma FP de detectar errores es bastante buena: cuando se ocultan las excepciones, establecen una bandera en el registro de estado de FP en lugar de atrapar. (p. ej., MXCSR de x86 para instrucciones SSE). El indicador permanece establecido hasta que se borra manualmente, por lo que puede verificar una vez (después de un ciclo, por ejemplo) para ver qué excepciones ocurrieron, pero no dónde ocurrieron.
Ha habido propuestas para tener indicadores de desbordamiento de enteros "pegajosos" similares para registrar si se produjo un desbordamiento en algún momento durante una secuencia de cálculos. Permitir que se enmascaren las excepciones de división de enteros sería bueno en algunos casos, pero peligroso en otros casos (por ejemplo, en un cálculo de dirección, debe atrapar en lugar de almacenar potencialmente en una ubicación falsa).
Sin embargo, en x86, detectar si se produjo un desbordamiento de enteros durante una secuencia de cálculos requiere colocar una rama condicional después de cada uno de ellos, porque las banderas simplemente se sobrescriben.
MIPS tiene una instrucción de
add
que atrapará en el desbordamiento firmado, y una instrucción sin firmar que nunca atrapa.
Por lo tanto, la detección y el manejo de excepciones enteras es mucho menos estandarizada.
La división de enteros no tiene la opción de producir resultados de NaN o Inf, por lo que tiene sentido que funcione de esta manera .
Cualquier patrón de bits entero producido por la división entera será incorrecto, ya que representará un valor finito específico.
Sin embargo, en x86, la conversión de un valor de punto flotante fuera de rango a entero con
cvtsd2si
o cualquier instrucción de conversión similar
produce el valor "entero indefinido" si se enmascara la excepción "punto flotante no válido".
El valor es todo cero excepto el bit de signo.
es decir,
INT_MIN
.
(Consulte los manuales de Intel, enlaces en el wiki de etiquetas x86 .
Para el espacio de usuario, esto sucede en AIX con POWER, HP-UX con PA-RISC, Linux con x86-64, macOS con x86-64, Tru64 con Alpha y Solaris con SPARC.
Evitar divisiones por cero en tiempo de compilación es mucho mejor.