En programación informática , el comportamiento indefinido ( UB ) es el resultado de ejecutar un programa cuyo comportamiento se prescribe como impredecible, en la especificación del lenguaje al que se adhiere el código informático . Esto es diferente del comportamiento no especificado , para el cual la especificación del lenguaje no prescribe un resultado, y del comportamiento definido por la implementación que difiere de la documentación de otro componente de la plataforma (como la ABI o la documentación del traductor ).
En la comunidad C , el comportamiento indefinido puede ser referido con humor como " demonios nasales ", después de una publicación de comp.std.c que explicaba que el comportamiento indefinido permitía al compilador hacer lo que quisiera, incluso "hacer que los demonios salieran volando de tu nariz. ". [1]
Descripción general
Algunos lenguajes de programación permiten que un programa funcione de manera diferente o incluso que tenga un flujo de control diferente al del código fuente , siempre y cuando presente los mismos efectos secundarios visibles para el usuario , si el comportamiento indefinido nunca ocurre durante la ejecución del programa . Comportamiento indefinido es el nombre de una lista de condiciones que el programa no debe cumplir.
En las primeras versiones de C , la principal ventaja del comportamiento indefinido era la producción de compiladores de alto rendimiento para una amplia variedad de máquinas: una construcción específica podía asignarse a una característica específica de la máquina y el compilador no tenía que generar código adicional para el tiempo de ejecución. adaptar los efectos secundarios a la semántica impuesta por el lenguaje. El código fuente del programa se redactó con conocimiento previo del compilador específico y de las plataformas que admitiría.
Sin embargo, la estandarización progresiva de las plataformas ha hecho que esto sea una ventaja menor, especialmente en las versiones más nuevas de C. Ahora, los casos de comportamiento indefinido generalmente representan errores inequívocos en el código, por ejemplo, indexar una matriz fuera de sus límites. Por definición, el tiempo de ejecución puede asumir que el comportamiento indefinido nunca ocurre; por lo tanto, no es necesario comprobar algunas condiciones no válidas. Para un compilador , esto también significa que varias transformaciones de programa se vuelven válidas o que sus pruebas de corrección se simplifican; esto permite varios tipos de optimización prematura y microoptimización , que conducen a un comportamiento incorrecto si el estado del programa cumple alguna de estas condiciones. El compilador también puede eliminar comprobaciones explícitas que puedan haber estado en el código fuente, sin notificar al programador; por ejemplo, no se garantiza que funcione la detección de un comportamiento indefinido probando si sucedió, por definición. Esto hace que sea difícil o imposible programar una opción portátil a prueba de fallas (las soluciones no portátiles son posibles para algunas construcciones).
El desarrollo actual del compilador generalmente evalúa y compara el rendimiento del compilador con puntos de referencia diseñados en torno a microoptimizaciones, incluso en plataformas que se utilizan principalmente en el mercado de computadoras de escritorio y portátiles de propósito general (como amd64). Por lo tanto, el comportamiento indefinido proporciona un amplio espacio para la mejora del rendimiento del compilador, ya que el código fuente para una declaración de código fuente específica puede mapearse a cualquier cosa en tiempo de ejecución.
Para C y C ++, el compilador puede dar un diagnóstico en tiempo de compilación en estos casos, pero no está obligado a hacerlo: la implementación se considerará correcta, haga lo que haga en tales casos, de forma análoga a los términos no importa en la lógica digital. . Es responsabilidad del programador escribir código que nunca invoque un comportamiento indefinido, aunque las implementaciones del compilador pueden emitir diagnósticos cuando esto sucede. Los compiladores hoy en día tienen banderas que habilitan tales diagnósticos, por ejemplo, -fsanitize
habilita el "desinfectante de comportamiento indefinido" ( UBSan ) en gcc 4.9 [2] y en clang . Sin embargo, este indicador no es el predeterminado y habilitarlo es una elección de quién crea el código.
En algunas circunstancias, puede haber restricciones específicas sobre el comportamiento indefinido. Por ejemplo, las especificaciones del conjunto de instrucciones de una CPU pueden dejar el comportamiento de algunas formas de una instrucción sin definir, pero si la CPU admite la protección de la memoria, entonces la especificación probablemente incluirá una regla general que indique que ninguna instrucción accesible al usuario puede causar un agujero en la seguridad del sistema operativo ; por lo que a una CPU real se le permitiría corromper los registros de usuario en respuesta a dicha instrucción, pero no se le permitiría, por ejemplo, cambiar al modo supervisor .
La plataforma de tiempo de ejecución también puede proporcionar algunas restricciones o garantías sobre el comportamiento indefinido, si la cadena de herramientas o el tiempo de ejecución documentan explícitamente que las construcciones específicas que se encuentran en el código fuente se asignan a mecanismos específicos bien definidos disponibles en el tiempo de ejecución. Por ejemplo, un intérprete puede documentar un determinado comportamiento para algunas operaciones que no están definidas en la especificación del lenguaje, mientras que otros intérpretes o compiladores del mismo lenguaje pueden no hacerlo. Un compilador produce código ejecutable para una ABI específica , llenando la brecha semántica de formas que dependen de la versión del compilador: la documentación para esa versión del compilador y la especificación ABI pueden proporcionar restricciones sobre el comportamiento indefinido. Confiar en estos detalles de implementación hace que el software no sea portátil ; sin embargo, la portabilidad puede no ser una preocupación si no se supone que el software se use fuera de un tiempo de ejecución específico.
El comportamiento indefinido puede resultar en un bloqueo del programa o incluso en fallas que son más difíciles de detectar y hacer que el programa parezca que está funcionando normalmente, como la pérdida silenciosa de datos y la producción de resultados incorrectos.
Beneficios
Documentar una operación como comportamiento indefinido permite a los compiladores asumir que esta operación nunca sucederá en un programa conforme. Esto le da al compilador más información sobre el código y esta información puede conducir a más oportunidades de optimización.
Un ejemplo para el lenguaje C:
int foo ( unsigned char x ) { int valor = 2147483600 ; / * asumiendo int de 32 bits y char de 8 bits * / value + = x ; si ( valor < 2147483600 ) bar (); valor de retorno ; }
El valor de x
no puede ser negativo y, dado que el desbordamiento de enteros con signo es un comportamiento indefinido en C, el compilador puede asumir que value < 2147483600
siempre será falso. Por lo tanto if
, bar
el compilador puede ignorar la declaración, incluida la llamada a la función , ya que la expresión de prueba en el if
no tiene efectos secundarios y su condición nunca se cumplirá. Por tanto, el código es semánticamente equivalente a:
int foo ( unsigned char x ) { int valor = 2147483600 ; valor + = x ; valor de retorno ; }
Si el compilador se hubiera visto obligado a asumir que el desbordamiento de enteros con signo tiene un comportamiento envolvente , la transformación anterior no habría sido legal.
Tales optimizaciones se vuelven difíciles de detectar por los humanos cuando el código es más complejo y se llevan a cabo otras optimizaciones, como la inserción . Por ejemplo, otra función puede llamar a la función anterior:
void run_tasks ( carácter sin firmar * ptrx ) { int z ; z = foo ( * ptrx ); while ( * ptrx > 60 ) { ejecutar_una_tarea ( ptrx , z ); } }
El compilador es libre de optimizar el while
-loop aquí aplicando análisis de rango de valores : al inspeccionar foo()
, sabe que el valor inicial apuntado por ptrx
no puede exceder 47 (ya que cualquier otro desencadenaría un comportamiento indefinido en foo()
), por lo tanto, la verificación inicial de *ptrx > 60
will siempre sea falso en un programa conforme. Yendo más allá, dado que el resultado z
ahora nunca se usa y foo()
no tiene efectos secundarios, el compilador puede optimizar run_tasks()
para ser una función vacía que regresa inmediatamente. La desaparición del while
-loop puede resultar especialmente sorprendente si foo()
se define en un archivo objeto compilado por separado .
Otro beneficio de permitir que el desbordamiento de enteros con signo no esté definido es que permite almacenar y manipular el valor de una variable en un registro de procesador que es mayor que el tamaño de la variable en el código fuente. Por ejemplo, si el tipo de una variable como se especifica en el código fuente es más estrecho que el ancho del registro nativo (como " int " en una máquina de 64 bits , un escenario común), entonces el compilador puede usar de forma segura un 64- bit entero para la variable en el código de máquina que produce, sin cambiar el comportamiento definido del código. Si un programa dependiera del comportamiento de un desbordamiento de enteros de 32 bits, entonces un compilador tendría que insertar lógica adicional al compilar para una máquina de 64 bits, porque el comportamiento de desbordamiento de la mayoría de las instrucciones de la máquina depende del ancho del registro. [3]
El comportamiento indefinido también permite más comprobaciones en tiempo de compilación por parte de los compiladores y el análisis de programas estáticos . [ cita requerida ]
Riesgos
Los estándares C y C ++ tienen varias formas de comportamiento indefinido, lo que ofrece una mayor libertad en las implementaciones del compilador y las comprobaciones en tiempo de compilación a expensas del comportamiento indefinido en tiempo de ejecución, si está presente. En particular, el estándar ISO para C tiene un apéndice que enumera las fuentes comunes de comportamiento indefinido. [4] Además, los compiladores no están obligados a diagnosticar el código que se basa en un comportamiento indefinido. Por lo tanto, es común que los programadores, incluso los experimentados, confíen en un comportamiento indefinido, ya sea por error o simplemente porque no están bien versados en las reglas del lenguaje que puede abarcar cientos de páginas. Esto puede resultar en errores que se exponen cuando se usa un compilador diferente o configuraciones diferentes. Las pruebas o el fuzzing con comprobaciones dinámicas de comportamiento indefinido habilitadas, por ejemplo, los desinfectantes de Clang , pueden ayudar a detectar el comportamiento indefinido no diagnosticado por el compilador o los analizadores estáticos. [5]
El comportamiento indefinido puede provocar vulnerabilidades de seguridad en el software. Por ejemplo, los desbordamientos del búfer y otras vulnerabilidades de seguridad en los principales navegadores web se deben a un comportamiento indefinido. El problema del año 2038 es otro ejemplo debido a firmado desbordamiento de enteros . Cuando los desarrolladores de GCC cambiaron su compilador en 2008 de modo que omitieron ciertas verificaciones de desbordamiento que se basaban en un comportamiento indefinido, CERT emitió una advertencia contra las versiones más nuevas del compilador. [6] Linux Weekly News señaló que se observó el mismo comportamiento en PathScale C , Microsoft Visual C ++ 2005 y varios otros compiladores; [7] la advertencia se modificó posteriormente para advertir sobre varios compiladores. [8]
Ejemplos en C y C ++
Las principales formas de comportamiento indefinido en C se pueden clasificar en términos generales como: [9] violaciones de seguridad de la memoria espacial, violaciones de seguridad de la memoria temporal, desbordamiento de enteros , violaciones de alias estrictas, violaciones de alineación, modificaciones no secuenciadas, carreras de datos y bucles que no realizan I / O ni terminar.
En C, el uso de cualquier variable automática antes de que se haya inicializado produce un comportamiento indefinido, al igual que la división de enteros por cero , el desbordamiento de enteros con signo, la indexación de una matriz fuera de sus límites definidos (ver desbordamiento de búfer ) o la desreferenciación de puntero nulo . En general, cualquier instancia de comportamiento indefinido deja la máquina de ejecución abstracta en un estado desconocido y hace que el comportamiento de todo el programa sea indefinido.
Intentar modificar un literal de cadena provoca un comportamiento indefinido: [10]
char * p = "wikipedia" ; // C válido, obsoleto en C ++ 98 / C ++ 03, mal formado a partir de C ++ 11 p [ 0 ] = 'W' ; // comportamiento indefinido
La división entera por cero da como resultado un comportamiento indefinido: [11]
int x = 1 ; return x / 0 ; // comportamiento indefinido
Ciertas operaciones de puntero pueden resultar en un comportamiento indefinido: [12]
int arr [ 4 ] = { 0 , 1 , 2 , 3 }; int * p = arr + 5 ; // comportamiento indefinido para indexar fuera de límites p = 0 ; int a = * p ; // comportamiento indefinido para eliminar la referencia a un puntero nulo
En C y C ++, la comparación relacional de punteros a objetos (para comparación menor que o mayor que) solo se define estrictamente si los punteros apuntan a miembros del mismo objeto o elementos de la misma matriz . [13] Ejemplo:
int main ( vacío ) { int a = 0 ; int b = 0 ; volver & a < & b ; / * comportamiento indefinido * / }
Alcanzar el final de una función de devolución de valor (distinta de main()
) sin una declaración de devolución da como resultado un comportamiento indefinido si el llamador utiliza el valor de la llamada a la función: [14]
int f () { } / * comportamiento indefinido si se usa el valor de la llamada a la función * /
La modificación de un objeto entre dos puntos de secuencia más de una vez produce un comportamiento indefinido. [15] Hay cambios considerables en las causas del comportamiento indefinido en relación con los puntos de secuencia a partir de C ++ 11. [16] Los compiladores modernos pueden emitir advertencias cuando encuentran múltiples modificaciones no secuenciadas en el mismo objeto. [17] [18] El siguiente ejemplo provocará un comportamiento indefinido tanto en C como en C ++.
int f ( int i ) { return i ++ + i ++ ; / * comportamiento indefinido: dos modificaciones no secuenciadas a i * / }
Al modificar un objeto entre dos puntos de secuencia, leer el valor del objeto para cualquier otro propósito que no sea determinar el valor que se almacenará también es un comportamiento indefinido. [19]
a [ i ] = i ++ ; // comportamiento indefinido printf ( "% d% d \ n " , ++ n , power ( 2 , n )); // también comportamiento indefinido
En C / C ++, el desplazamiento bit a bit de un valor en un número de bits que es un número negativo o es mayor o igual que el número total de bits en este valor da como resultado un comportamiento indefinido. La forma más segura (independientemente del proveedor del compilador) es mantener siempre el número de bits a cambio (el operando derecho de las <<
y >>
bit a bit operadores ) dentro del rango: < > (donde es el operando de la izquierda).0, sizeof(value)*CHAR_BIT - 1
value
int num = -1 ; unsigned int val = 1 << num ; // cambio por un número negativo - comportamiento indefinidonum = 32 ; // o cualquier número mayor que 31 val = 1 << num ; // el literal '1' se escribe como un entero de 32 bits; en este caso, el desplazamiento de más de 31 bits es un comportamiento indefinidonum = 64 ; // o cualquier número mayor que 63 unsigned long long val2 = 1ULL << num ; // el literal '1ULL' se escribe como un entero de 64 bits; en este caso, cambiar más de 63 bits es un comportamiento indefinido
Ver también
- Compilador
- Detener y prender fuego
- Comportamiento no especificado
Referencias
- ^ "demonios nasales" . Archivo de jerga . Consultado el 12 de junio de 2014 .
- ^ Desinfectante de comportamiento indefinido de GCC - ubsan
- ^ https://gist.github.com/rygorous/e0f055bfb74e3d5f0af20690759de5a7#file-gistfile1-txt-L166
- ^ ISO / IEC 9899: 2011 §J.2.
- ^ John Regehr. "Comportamiento indefinido en 2017, cppcon 2017" .
- ^ "Nota de vulnerabilidad VU # 162289 - gcc descarta silenciosamente algunas comprobaciones envolventes" . Base de datos de notas de vulnerabilidad . CERT. 4 de abril de 2008. Archivado desde el original el 9 de abril de 2008.
- ^ Jonathan Corbet (16 de abril de 2008). "GCC y desbordamientos de puntero" . Noticias semanales de Linux .
- ^ "Nota de vulnerabilidad VU # 162289 - Los compiladores de C pueden descartar silenciosamente algunas comprobaciones envolventes" . Base de datos de notas de vulnerabilidad . CERT. 8 de octubre de 2008 [4 de abril de 2008].
- ^ Pascal Cuoq y John Regehr (4 de julio de 2017). "Comportamiento indefinido en 2017, incrustado en el blog de la academia" .
- ^ ISO / IEC (2003). ISO / IEC 14882: 2003 (E): Lenguajes de programación - C ++ §2.13.4 Literales de cadena [lex.string] párr. 2
- ^ ISO / IEC (2003). ISO / IEC 14882: 2003 (E): Lenguajes de programación - C ++ §5.6 Operadores multiplicativos [expr.mul] párr. 4
- ^ ISO / IEC (2003). ISO / IEC 14882: 2003 (E): Lenguajes de programación - C ++ §5.7 Operadores aditivos [expr.add] párr. 5
- ^ ISO / IEC (2003). ISO / IEC 14882: 2003 (E): Lenguajes de programación - C ++ §5.9 Operadores relacionales [expr.rel] párr. 2
- ^ ISO / IEC (2007). ISO / IEC 9899: 2007 (E): Lenguajes de programación - C §6.9 Definiciones externas párr. 1
- ^ ANSI X3.159-1989 Lenguaje de programación C , nota al pie 26
- ^ "Orden de evaluación - cppreference.com" . en.cppreference.com . Consultado el 9 de agosto de 2016.
- ^ "Opciones de advertencia (usando la colección de compiladores GNU (GCC))" . GCC, la colección de compiladores GNU - Proyecto GNU - Free Software Foundation (FSF) . Consultado el 9 de julio de 2021 .
- ^ "Banderas de diagnóstico en Clang" . Documentación de Clang 13 . Consultado el 9 de julio de 2021 .
- ^ ISO / IEC (1999). ISO / IEC 9899: 1999 (E): Lenguajes de programación - C §6.5 Expresiones párr. 2
Otras lecturas
- Peter van der Linden , Experto de programación C . ISBN 0-13-177429-8
- UB Canaries (abril de 2015), John Regehr (Universidad de Utah, EE. UU.)
- Comportamiento indefinido en 2017 (julio de 2017) Pascal Cuoq (TrustInSoft, Francia) y John Regehr (Universidad de Utah, EE. UU.)
enlaces externos
- Versión corregida del estándar C99 . Mire la sección 6.10.6 para #pragma