stringtranslate.com

Comportamiento indefinido

En programación informática , el comportamiento indefinido ( UB ) es el resultado de ejecutar un programa cuyo comportamiento está prescrito 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 de programación de C , el comportamiento indefinido puede denominarse con humor " demonios nasales ", después de una publicación de comp.std.c que explicaba que el comportamiento indefinido permite al compilador hacer lo que quiera, incluso "hacer que los demonios salgan volando de tu computadora". nariz". [1]

Descripción general

Algunos lenguajes de programación permiten que un programa opere de manera diferente o incluso tenga un flujo de control diferente al del código fuente , siempre que presente los mismos efectos secundarios visibles para el usuario , si nunca ocurre un comportamiento indefinido 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 para que coincidan con la semántica impuesta por el lenguaje. El código fuente del programa se escribió con conocimiento previo del compilador específico y de las plataformas que soportarí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 del programa se vuelven válidas o se simplifican sus pruebas de corrección; esto permite varios tipos de optimizaciones cuya exactitud depende de la suposición de que el estado del programa nunca cumple dicha condición. El compilador también puede eliminar comprobaciones explícitas que puedan haber estado en el código fuente, sin notificarlo al programador; por ejemplo, no se garantiza que funcione, por definición, detectar un comportamiento indefinido probando si ocurrió. Esto hace que sea difícil o imposible programar una opción portátil a prueba de fallos (las soluciones no portátiles son posibles para algunas construcciones).

El desarrollo actual de compiladores 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 uso general (como amd64). Por lo tanto, el comportamiento indefinido proporciona un amplio margen para mejorar el rendimiento del compilador, ya que el código fuente de una declaración de código fuente específica puede asignarse 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 de "no importa" en 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. Hoy en día, los compiladores tienen indicadores que permiten dichos diagnósticos, por ejemplo, -fsanitize=undefinedhabilita 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 la persona que crea el código.

En algunas circunstancias, puede haber restricciones específicas sobre comportamientos indefinidos. Por ejemplo, las especificaciones del conjunto de instrucciones de una CPU pueden dejar sin definir el comportamiento de algunas formas de una instrucción, 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 tanto, 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 comportamiento particular para algunas operaciones que no están definidas en la especificación del lenguaje, mientras que otros intérpretes o compiladores para el mismo lenguaje pueden no hacerlo. Un compilador produce código ejecutable para una ABI específica , llenando el vacío semántico de maneras 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. Depender de estos detalles de implementación hace que el software no sea portátil , pero la portabilidad puede no ser una preocupación si se supone que el software no debe usarse fuera de un tiempo de ejecución específico.

El comportamiento no definido puede provocar un bloqueo del programa o incluso fallos que son más difíciles de detectar y hacen que el programa parezca funcionar con normalidad, como una 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 brinda al compilador más información sobre el código y esta información puede generar más oportunidades de optimización.

Un ejemplo para el lenguaje C:

int foo ( carbón sin firmar x ) { valor int = 2147483600 ; /* suponiendo int de 32 bits y char de 8 bits */ value += x ; si ( valor < 2147483600 ) barra (); valor de retorno ; }                  

El valor de xno puede ser negativo y, dado que el desbordamiento de enteros con signo es un comportamiento indefinido en C, el compilador puede asumir que value < 2147483600siempre será falso. Por lo tanto , el compilador puede ignorar la ifdeclaración, incluida la llamada a la función , ya que la expresión de prueba en no tiene efectos secundarios y su condición nunca se cumplirá. Por tanto, el código es semánticamente equivalente a:barif

int foo ( carbón sin firmar x ) { valor int = 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 , entonces la transformación anterior no habría sido legal.

Estas optimizaciones se vuelven difíciles de detectar para los humanos cuando el código es más complejo y se realizan otras optimizaciones, como la inserción . Por ejemplo, otra función puede llamar a la función anterior:

void run_tasks ( car unsigned * ptrx ) { int z ; z = foo ( * ptrx ); mientras ( * ptrx > 60 ) { run_one_task ( ptrx , z ); } }                 

El compilador es libre de optimizar el whilebucle -aquí aplicando un análisis de rango de valores : al inspeccionar foo(), sabe que el valor inicial señalado por ptrxno puede exceder 47 (ya que más desencadenaría un comportamiento indefinido en foo()); por lo tanto, la verificación inicial de *ptrx > 60siempre será falsa en un programa conforme. Yendo más allá, dado que el resultado zahora nunca se usa y foo()no tiene efectos secundarios, el compilador puede optimizarlo run_tasks()para que sea una función vacía que regrese inmediatamente. La desaparición del whilebucle 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 del 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 inten una máquina de 64 bits , un escenario común), entonces el compilador puede usar con seguridad un entero de 64 bits con signo 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 tanto por parte de los compiladores como por el análisis de programas estáticos . [ cita necesaria ]

Riesgos

Los estándares C y C++ tienen varias formas de comportamiento indefinido, que ofrecen mayor libertad en las implementaciones del compilador y 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 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, dependan de un comportamiento indefinido, ya sea por error o simplemente porque no conocen bien las reglas del lenguaje que puede abarcar cientos de páginas. Esto puede provocar errores que quedan expuestos cuando se utiliza un compilador diferente o configuraciones diferentes. Las pruebas o la confusión con comprobaciones dinámicas de comportamiento indefinido habilitadas, por ejemplo, los desinfectantes Clang , pueden ayudar a detectar comportamientos indefinidos no diagnosticados 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. Cuando los desarrolladores de GCC cambiaron su compilador en 2008 de modo que omitieron ciertas comprobaciones de desbordamiento que dependían de 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 del 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]

carbón * 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 de enteros por cero da como resultado un comportamiento indefinido: [11]

int x = 1 ; devolver x / 0 ; // comportamiento indefinido       

Ciertas operaciones de puntero pueden dar como resultado un comportamiento indefinido: [12]

int arreglo [ 4 ] = { 0 , 1 , 2 , 3 }; int * p = arreglo + 5 ; // comportamiento indefinido para indexar fuera de límites p = NULL ; int a = * p ; // comportamiento indefinido para desreferenciar un puntero nulo                  

En C y C++, la comparación relacional de punteros a objetos (para comparación menor o mayor que) solo está estrictamente definida si los punteros apuntan a miembros del mismo objeto o elementos de la misma matriz . [13] Ejemplo:

int principal ( vacío ) { int a = 0 ; int b = 0 ; retorno & a < & b ; /* comportamiento indefinido */ }              

Llegar al final de una función que devuelve un valor (que no sea main()) sin una declaración de retorno da como resultado un comportamiento indefinido si la persona que llama utiliza el valor de la llamada a la función: [14]

int f () { } /* comportamiento indefinido si se utiliza el valor de la llamada a la función*/  

Modificar 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 ) { retorno 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 [ yo ] = yo ++ ; // 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 una cantidad de bits que es un número negativo o es mayor o igual que la cantidad 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 desplazar (el operando derecho de los operadores bit<< a bit ) dentro del rango: [ ] (donde está el operando izquierdo).>> 0, sizeof value * CHAR_BIT - 1value

número int = -1 ; valor int sin signo = 1 << número ; // desplazamiento por un número negativo - comportamiento indefinido          número = 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 indefinido        número = 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, el desplazamiento de más de 63 bits es un comportamiento indefinido           

Ejemplos en óxido

Si bien el comportamiento indefinido nunca está presente en Rust seguro , es posible invocar un comportamiento indefinido en Rust inseguro de muchas maneras. [20] Por ejemplo, crear una referencia no válida (una referencia que no hace referencia a un valor válido) invoca un comportamiento indefinido inmediato:

fn  main () { // La siguiente línea invoca un comportamiento indefinido inmediato. let _null_reference : & i32 = inseguro { std :: mem :: puesto a cero () }; }         

Tenga en cuenta que no es necesario utilizar la referencia; El comportamiento indefinido se invoca simplemente desde la creación de dicha referencia.

Ver también

Referencias

  1. ^ "demonios nasales". Archivo de jerga . Consultado el 12 de junio de 2014 .
  2. ^ Desinfectante de comportamiento indefinido GCC - ubsan
  3. ^ "Un poco de información sobre los compiladores que explotan el desbordamiento firmado".
  4. ^ ISO/IEC 9899:2011 §J.2.
  5. ^ John Regehr. "Comportamiento indefinido en 2017, cppcon 2017". YouTube .
  6. ^ "Nota de vulnerabilidad VU#162289: gcc descarta silenciosamente algunas comprobaciones integrales". Base de datos de notas de vulnerabilidad . CERTIFICADO. 4 de abril de 2008. Archivado desde el original el 9 de abril de 2008.
  7. ^ Jonathan Corbet (16 de abril de 2008). "GCC y desbordamientos de puntero". Noticias semanales de Linux .
  8. ^ "Nota de vulnerabilidad VU#162289: los compiladores de C pueden descartar silenciosamente algunas comprobaciones integrales". Base de datos de notas de vulnerabilidad . CERTIFICADO. 8 de octubre de 2008 [4 de abril de 2008].
  9. ^ Pascal Cuoq y John Regehr (4 de julio de 2017). "Comportamiento indefinido en 2017, integrado en el blog académico".
  10. ^ ISO / IEC (2003). ISO/IEC 14882:2003(E): Lenguajes de programación – C++ §2.13.4 Literales de cadena [lex.string] párr. 2
  11. ^ ISO / IEC (2003). ISO/IEC 14882:2003(E): Lenguajes de programación – C++ §5.6 Operadores multiplicativos [expr.mul] párr. 4
  12. ^ ISO / IEC (2003). ISO/IEC 14882:2003(E): Lenguajes de programación - C++ §5.7 Operadores aditivos [expr.add] párr. 5
  13. ^ ISO / IEC (2003). ISO/IEC 14882:2003(E): Lenguajes de programación – C++ §5.9 Operadores relacionales [expr.rel] párr. 2
  14. ^ ISO / IEC (2007). ISO/IEC 9899:2007(E): Lenguajes de programación – C §6.9 Definiciones externas párr. 1
  15. ^ ANSI X3.159-1989 Lenguaje de programación C , nota al pie 26
  16. ^ "Orden de evaluación - cppreference.com". es.cppreference.com . Consultado el 9 de agosto de 2016 .
  17. ^ "Opciones de advertencia (uso de 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 .
  18. ^ "Indicadores de diagnóstico en Clang". Documentación de Clang 13 . Consultado el 9 de julio de 2021 .
  19. ^ ISO / IEC (1999). ISO/IEC 9899:1999(E): Lenguajes de programación – C §6.5 Expresiones párr. 2
  20. ^ "Comportamiento considerado indefinido". La referencia del óxido . Consultado el 28 de noviembre de 2022 .

Otras lecturas

enlaces externos