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 de programación en el que está escrito el código fuente . 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 se remite a la documentación de otro componente de la plataforma (como la ABI o la documentación del traductor ).

En la comunidad de programación en C , el comportamiento indefinido puede ser denominado humorísticamente como " demonios nasales ", en referencia a una publicación en 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 nariz". [1]

Descripción general

Algunos lenguajes de programación permiten que un programa funcione 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 el comportamiento indefinido nunca ocurre durante la ejecución del programa . El 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 entorno de ejecución para adaptar los efectos secundarios a fin de que coincidieran 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 entorno de ejecución puede asumir que el comportamiento indefinido nunca sucede; por lo tanto, no es necesario comprobar algunas condiciones inválidas. Para un compilador , esto también significa que varias transformaciones del programa se vuelven válidas, o sus pruebas de corrección se simplifican; esto permite varios tipos de optimizaciones cuya corrección depende de la suposición de que el estado del programa nunca cumple ninguna de esas condiciones. El compilador también puede eliminar las comprobaciones explícitas que pueden haber estado en el código fuente, sin notificar al programador; por ejemplo, detectar un comportamiento indefinido probando si sucedió no está garantizado que funcione, por definición. 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 la mejora del rendimiento del compilador, ya que se permite que el código fuente de una declaración de código fuente específica se asigne a cualquier cosa en tiempo de ejecución.

En el caso de C y C++, el compilador puede emitir un diagnóstico en tiempo de compilación en estos casos, pero no está obligado a hacerlo: la implementación se considerará correcta independientemente de lo que haga en tales casos, de forma análoga a los términos de "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 actuales tienen indicadores que habilitan dichos diagnósticos; por ejemplo, -fsanitize=undefinedhabilita el "sanitizador 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 compila 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 indefinido 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 establezca que ninguna instrucción accesible al usuario puede causar un agujero en la seguridad del sistema operativo ; por lo tanto, se permitiría que una CPU real corrompiera los registros del usuario en respuesta a una instrucción de este tipo, 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 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 de la ABI pueden proporcionar restricciones sobre el comportamiento indefinido. Confiar en estos detalles de implementación hace que el software no sea portable , pero 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.

Un comportamiento indefinido puede provocar un bloqueo del programa o incluso fallos más difíciles de detectar y que hacen que el programa parezca funcionar 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 suponer que esta operación nunca ocurrirá 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 ( unsigned char x ) { int valor = 2147483600 ; /* asumiendo int de 32 bits y char de 8 bits */ valor += x ; if ( valor < 2147483600 ) bar (); 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 bar, ya que la expresión de prueba en ifno tiene efectos secundarios y su condición nunca se cumplirá. Por lo 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 , 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 llevan a cabo otras optimizaciones, como la inserción en línea . Por ejemplo, otra función puede llamar a la función anterior:

void ejecutar_tareas ( unsigned char * ptrx ) { int z ; z = foo ( * ptrx ); while ( * ptrx > 60 ) { ejecutar_una_tarea ( ptrx , z ); } }                 

El compilador es libre de optimizar el whilebucle aquí aplicando el análisis de rango de valores : al inspeccionar foo(), sabe que el valor inicial al que apunta ptrxno puede superar 47 (ya que cualquier valor superior desencadenaría un comportamiento indefinido en foo()); por lo tanto, la comprobació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 retorna inmediatamente. La desaparición del whilebucle puede ser especialmente sorprendente si foo()se define en un archivo de objeto compilado por separado .

Otro beneficio de permitir que el desbordamiento de enteros con signo no esté definido es que hace posible almacenar y manipular el valor de una variable en un registro de procesador que sea más grande 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 de manera segura un entero con signo de 64 bits 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 entero 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 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 del análisis estático del programa . [ cita requerida ]

Riesgos

Los estándares C y C++ tienen varias formas de comportamiento indefinido en todas partes, 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 depende de un comportamiento indefinido. Por lo tanto, es común que los programadores, incluso los experimentados, dependan del comportamiento indefinido ya sea por error o simplemente porque no están bien versados ​​en las reglas del lenguaje que pueden abarcar cientos de páginas. Esto puede dar lugar a errores que se exponen cuando se utiliza un compilador diferente o diferentes configuraciones. Las pruebas o el fuzzing con comprobaciones dinámicas de comportamiento indefinido habilitadas, por ejemplo, los sanitizadores Clang , pueden ayudar a detectar el comportamiento indefinido no diagnosticado por el compilador o los analizadores estáticos. [5]

Un comportamiento indefinido puede provocar vulnerabilidades de seguridad en el software. Por ejemplo, los desbordamientos de 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 omitió 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ó más tarde para advertir sobre varios compiladores. [8]

Ejemplos en C y C++

Las principales formas de comportamiento indefinido en C se pueden clasificar ampliamente como: [9] violaciones de seguridad de memoria espacial, violaciones de seguridad de memoria temporal, desbordamiento de enteros , violaciones de alias estricto, violaciones de alineación, modificaciones no secuenciadas, carreras de datos y bucles que no realizan E/S ni terminan.

En C, el uso de cualquier variable automática antes de que haya sido inicializada 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 a 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 de enteros por cero da como resultado un comportamiento indefinido: [11]

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

Ciertas operaciones de puntero pueden dar lugar a un comportamiento indefinido: [12]

int arr [ 4 ] = { 0 , 1 , 2 , 3 }; int * p = arr + 5 ; // comportamiento indefinido para indexar fuera de los 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 comparaciones menores o mayores que) solo se define estrictamente si los punteros apuntan a miembros del mismo objeto o elementos de la misma matriz . [13] Ejemplo:

int main ( void ) { int a = 0 ; int b = 0 ; return & 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 el llamador utiliza el valor de 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 lo que causa un 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 , potencia ( 2 , n )); // también comportamiento indefinido       

En C/C++, desplazar un valor bit a bit por una cantidad de bits que sea un número negativo o mayor o igual que la cantidad total de bits de este valor da como resultado un comportamiento indefinido. La forma más segura (independientemente del proveedor del compilador) es mantener siempre la cantidad de bits a desplazar (el operando derecho de los operadores bit a bit<< y ) dentro del rango: [ ] (donde es el operando izquierdo).>> 0, sizeof value * CHAR_BIT - 1value

int num = -1 ; unsigned int val = 1 << num ; // desplazamiento por un número negativo - comportamiento indefinido          num = 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, desplazarse más de 31 bits es un comportamiento indefinido        num = 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, desplazarse más de 63 bits es un comportamiento indefinido           

Ejemplos en Rust

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 = unsafe { std :: mem :: zeroed () }; }         

No es necesario utilizar la referencia; el comportamiento indefinido se invoca simplemente desde la creación de dicha referencia.

Véase también

Referencias

  1. ^ "demonios nasales". Archivo de jerga . Consultado el 12 de junio de 2014 .
  2. ^ Sanitizante de comportamiento indefinido del CCG – ubsan
  3. ^ "Un poco de historia sobre los compiladores que explotan el desbordamiento de signo".
  4. ^ ISO/IEC 9899:2011 §J.2.
  5. ^ John Regehr (19 de octubre de 2017). "Comportamiento indefinido en 2017, cppcon 2017". YouTube .
  6. ^ "Nota de vulnerabilidad VU#162289: gcc descarta silenciosamente algunas comprobaciones de envolvente". Base de datos de notas de vulnerabilidades . CERT. 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». Linux Weekly News .
  8. ^ "Nota de vulnerabilidad VU#162289: los compiladores de C pueden descartar de forma silenciosa algunas comprobaciones de envolvente". Base de datos de notas de vulnerabilidades . CERT. 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, blog Embedded in Academia".
  10. ^ ISO / IEC (2003). ISO/IEC 14882:2003(E): Lenguajes de programación – C++ §2.13.4 Literales de cadena [lex.string] párrafo 2
  11. ^ ISO / IEC (2003). ISO/IEC 14882:2003(E): Lenguajes de programación – C++ §5.6 Operadores multiplicativos [expr.mul] párrafo 4
  12. ^ ISO / IEC (2003). ISO/IEC 14882:2003(E): Lenguajes de programación - C++ §5.7 Operadores aditivos [expr.add] párrafo 5
  13. ^ ISO / IEC (2003). ISO/IEC 14882:2003(E): Lenguajes de programación – C++ §5.9 Operadores relacionales [expr.rel] párrafo 2
  14. ^ ISO / IEC (2007). ISO/IEC 9899:2007(E): Lenguajes de programación – C §6.9 Definiciones externas , párrafo 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 (utilizando 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. ^ "Banderas 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árrafo 2
  20. ^ "Comportamiento considerado indefinido". Referencia de Rust . Consultado el 28 de noviembre de 2022 .

Lectura adicional

Enlaces externos