C Language
Comportamiento indefinido
Buscar..
Introducción
En C, algunas expresiones producen un comportamiento indefinido . El estándar elige explícitamente no definir cómo debe comportarse un compilador si encuentra tal expresión. Como resultado, un compilador es libre de hacer lo que crea conveniente y puede producir resultados útiles, resultados inesperados o incluso fallar.
El código que invoca a UB puede funcionar según lo previsto en un sistema específico con un compilador específico, pero es probable que no funcione en otro sistema, o con un compilador diferente, una versión del compilador o la configuración del compilador.
Observaciones
¿Qué es el comportamiento indefinido (UB)?
Comportamiento indefinido es un término usado en el estándar C. El estándar C11 (ISO / IEC 9899: 2011) define el término comportamiento indefinido como
comportamiento, en el uso de un constructo de programa no portátil o erróneo o de datos erróneos, para los cuales esta Norma Internacional no impone requisitos
¿Qué pasa si hay UB en mi código?
Estos son los resultados que pueden ocurrir debido a un comportamiento indefinido según el estándar:
NOTA El posible comportamiento indefinido abarca desde ignorar la situación completamente con resultados impredecibles, hasta comportarse durante la traducción o la ejecución del programa de una manera documentada característica del entorno (con o sin la emisión de un mensaje de diagnóstico), hasta terminar una traducción o ejecución (con la emisión de un mensaje de diagnóstico).
La siguiente cita se usa a menudo para describir (de manera menos formal) los resultados que suceden a partir de un comportamiento indefinido:
"Cuando el compilador se encuentra con [una construcción indefinida dada] es legal que haga que los demonios salgan volando de tu nariz" (la implicación es que el compilador puede elegir cualquier manera arbitrariamente extraña de interpretar el código sin violar el estándar ANSI C)
¿Por qué existe UB?
Si es tan malo, ¿por qué no lo definieron o lo hicieron definido por la implementación?
El comportamiento indefinido permite más oportunidades de optimización; El compilador puede suponer justificadamente que cualquier código no contiene un comportamiento indefinido, lo que puede permitirle evitar verificaciones en tiempo de ejecución y realizar optimizaciones cuya validez sería costosa o imposible de demostrar de otro modo.
¿Por qué es difícil localizar a UB?
Hay al menos dos razones por las que un comportamiento indefinido crea errores que son difíciles de detectar:
- No se requiere que el compilador, y generalmente no puede advertirle, sobre un comportamiento indefinido. De hecho, exigir que lo haga iría directamente en contra de la razón de la existencia de un comportamiento indefinido.
- Es posible que los resultados impredecibles no comiencen a desplegarse en el punto exacto de la operación donde se produce la construcción cuyo comportamiento no está definido; El comportamiento no definido mancha toda la ejecución y sus efectos pueden ocurrir en cualquier momento: durante, después o incluso antes de la construcción indefinida.
Considere la posibilidad de no hacer referencia al puntero nulo: el compilador no está obligado a diagnosticar la falta de referencia al puntero nulo, e incluso no podría hacerlo, ya que, en el tiempo de ejecución, cualquier puntero pasado a una función o en una variable global podría ser nulo. Y cuando se produce la anulación de referencia a un puntero nulo, la norma no exige que el programa deba bloquearse. Más bien, el programa podría fallar más temprano o más tarde o no fallar; Incluso podría comportarse como si el puntero nulo apuntara a un objeto válido, y se comportara completamente normalmente, solo para bloquearse en otras circunstancias.
En el caso de la desreferenciación de puntero nulo, el lenguaje C difiere de los lenguajes administrados como Java o C #, donde se define el comportamiento de la desreferencia de puntero nulo: se lanza una excepción, en el momento exacto ( NullPointerException
en Java, NullReferenceException
en C #) Por lo tanto, aquellos que vienen de Java o C # pueden creer incorrectamente que, en tal caso, un programa en C debe fallar, con o sin la emisión de un mensaje de diagnóstico .
Información Adicional
Hay varias situaciones de este tipo que deben distinguirse claramente:
- Comportamiento explícitamente indefinido, ahí es donde el estándar C le dice explícitamente que está fuera de los límites.
- Comportamiento implícito indefinido, donde simplemente no hay texto en el estándar que prevea un comportamiento para la situación en la que trajo su programa.
También tenga en cuenta que en muchos lugares el comportamiento de ciertas construcciones está deliberadamente indefinido por el estándar C para dejar espacio para que los implementadores de compiladores y bibliotecas elaboren sus propias definiciones. Un buen ejemplo son las señales y los manejadores de señales, donde las extensiones a C, como el estándar del sistema operativo POSIX, definen reglas mucho más elaboradas. En tales casos, solo tiene que consultar la documentación de su plataforma; El estándar C no te puede decir nada.
También tenga en cuenta que si ocurre un comportamiento indefinido en el programa, esto no significa que solo el punto donde ocurrió un comportamiento indefinido sea problemático, un programa completo deja de tener sentido.
Debido a estas preocupaciones, es importante (sobre todo porque los compiladores no siempre nos advierten acerca de UB) para que la persona programada en C esté al menos familiarizada con el tipo de cosas que desencadenan un comportamiento indefinido.
Cabe señalar que hay algunas herramientas (por ejemplo, herramientas de análisis estático, como PC-Lint) que ayudan a detectar un comportamiento indefinido, pero nuevamente, no pueden detectar todas las apariciones de un comportamiento indefinido.
Desreferenciación de un puntero nulo
Este es un ejemplo de desreferenciación de un puntero NULL, lo que provoca un comportamiento indefinido.
int * pointer = NULL;
int value = *pointer; /* Dereferencing happens here */
El estándar C garantiza un puntero NULL
para comparar desigualdades con cualquier puntero a un objeto válido, y la anulación de la referencia invoca un comportamiento indefinido.
Modificar cualquier objeto más de una vez entre dos puntos de secuencia.
int i = 42;
i = i++; /* Assignment changes variable, post-increment as well */
int a = i++ + i--;
Código como este a menudo conduce a especulaciones sobre el "valor resultante" de i
. Sin embargo, en lugar de especificar un resultado, los estándares de C especifican que la evaluación de tal expresión produce un comportamiento indefinido . Antes de C2011, el estándar formalizó estas reglas en términos de los llamados puntos de secuencia :
Entre el punto de secuencia anterior y el siguiente, un objeto escalar tendrá su valor almacenado modificado a lo sumo una vez por la evaluación de una expresión. Además, el valor anterior se leerá solo para determinar el valor que se almacenará.
(Norma C99, sección 6.5, párrafo 2)
Ese esquema demostró ser un poco demasiado tosco, lo que dio como resultado que algunas expresiones mostraran un comportamiento indefinido con respecto a C99 que no deberían ser verosímiles. C2011 conserva los puntos de secuencia, pero introduce un enfoque más matizado en esta área basado en la secuenciación y una relación que llama "secuenciada antes":
Si un efecto secundario en un objeto escalar no tiene secuencia en relación con un efecto secundario diferente en el mismo objeto escalar o un cálculo de valor utilizando el valor del mismo objeto escalar, el comportamiento no está definido. Si hay varios ordenamientos permitidos de las subexpresiones de una expresión, el comportamiento no está definido si se produce un efecto secundario no secuenciado en cualquiera de los ordenamientos.
(Norma C2011, sección 6.5, párrafo 2)
Los detalles completos de la relación "secuenciada antes" son demasiado largos para describirlos aquí, pero complementan los puntos de la secuencia en lugar de suplantarlos, por lo que tienen el efecto de definir el comportamiento para algunas evaluaciones cuyo comportamiento no estaba definido previamente. En particular, si hay un punto de secuencia entre dos evaluaciones, entonces el que está antes del punto de secuencia está "secuenciado antes" de la siguiente.
El siguiente ejemplo tiene un comportamiento bien definido:
int i = 42;
i = (i++, i+42); /* The comma-operator creates a sequence point */
El siguiente ejemplo tiene un comportamiento indefinido:
int i = 42;
printf("%d %d\n", i++, i++); /* commas as separator of function arguments are not comma-operators */
Al igual que con cualquier forma de comportamiento indefinido, observar el comportamiento real de evaluar expresiones que violan las reglas de secuencia no es informativo, excepto en un sentido retrospectivo. El estándar de lenguaje no proporciona ninguna base para esperar que tales observaciones sean predictivas, incluso del comportamiento futuro del mismo programa.
Falta la declaración de retorno en la función de retorno de valor
int foo(void) {
/* do stuff */
/* no return here */
}
int main(void) {
/* Trying to use the (not) returned value causes UB */
int value = foo();
return 0;
}
Cuando se declara que una función devuelve un valor, debe hacerlo en cada ruta de código posible a través de ella. El comportamiento indefinido ocurre tan pronto como la persona que llama (que espera un valor de retorno) intenta usar el valor de retorno 1 .
Tenga en cuenta que el comportamiento indefinido ocurre solo si la persona que llama intenta usar / acceder al valor de la función. Por ejemplo,
int foo(void) {
/* do stuff */
/* no return here */
}
int main(void) {
/* The value (not) returned from foo() is unused. So, this program
* doesn't cause *undefined behaviour*. */
foo();
return 0;
}
La main()
la función es una excepción a esta regla en la que es posible para que pueda ser terminado sin una instrucción de retorno, ya que un valor de retorno supuesta de 0
automáticamente se utilizará en este caso 2.
1 ( ISO / IEC 9899: 201x , 6.9.1 / 12)
Si se alcanza el} que termina una función, y el llamante utiliza el valor de la llamada a la función, el comportamiento es indefinido.
2 ( ISO / IEC 9899: 201x , 5.1.2.2.3 / 1)
alcanzando el} que termina la función principal devuelve un valor de 0.
Desbordamiento de entero firmado
Según el párrafo 6.5 / 5 de C99 y C11, la evaluación de una expresión produce un comportamiento indefinido si el resultado no es un valor representable del tipo de expresión. Para los tipos aritméticos, eso se llama un desbordamiento . La aritmética de enteros sin signo no se desborda porque se aplica el párrafo 6.2.5 / 9, lo que ocasiona que cualquier resultado sin firmar que de otra forma esté fuera de rango se reduzca a un valor dentro del rango. No hay ninguna disposición análoga para los tipos de enteros con signo, sin embargo; Estos pueden y se desbordan, produciendo un comportamiento indefinido. Por ejemplo,
#include <limits.h> /* to get INT_MAX */
int main(void) {
int i = INT_MAX + 1; /* Overflow happens here */
return 0;
}
La mayoría de los casos de este tipo de comportamiento indefinido son más difíciles de reconocer o predecir. En principio, el desbordamiento puede surgir de cualquier operación de suma, resta o multiplicación en enteros con signo (sujeto a las conversiones aritméticas habituales) donde no existen límites efectivos o una relación entre los operandos para evitarlo. Por ejemplo, esta función:
int square(int x) {
return x * x; /* overflows for some values of x */
}
es razonable, y hace lo correcto para valores de argumento suficientemente pequeños, pero su comportamiento no está definido para valores de argumento más grandes. No se puede juzgar solo por la función si los programas que lo llaman exhiben un comportamiento indefinido como resultado. Depende de qué argumentos le pasen.
Por otro lado, considere este ejemplo trivial de aritmética de enteros con signo de desbordamiento seguro:
int zero(int x) {
return x - x; /* Cannot overflow */
}
La relación entre los operandos del operador de resta asegura que la resta nunca se desborda. O considere este ejemplo algo más práctico:
int sizeDelta(FILE *f1, FILE *f2) {
int count1 = 0;
int count2 = 0;
while (fgetc(f1) != EOF) count1++; /* might overflow */
while (fgetc(f2) != EOF) count2++; /* might overflow */
return count1 - count2; /* provided no UB to this point, will not overflow */
}
Mientras los contadores no se desborden individualmente, los operandos de la resta final serán ambos no negativos. Todas las diferencias entre cualquiera de estos dos valores se pueden representar como int
.
Uso de una variable sin inicializar.
int a;
printf("%d", a);
La variable a
es un int
con duración de almacenamiento automático. El código de ejemplo anterior está tratando de imprimir el valor de una variable no inicializada ( a
no se ha inicializado). Las variables automáticas que no están inicializadas tienen valores indeterminados; El acceso a estos puede llevar a un comportamiento indefinido.
Nota: Las variables con almacenamiento local estático o de subprocesos, incluidas las variables globales sin la palabra clave static
, se inicializan a cero, o su valor inicializado. Por lo tanto lo siguiente es legal.
static int b;
printf("%d", b);
Un error muy común es no inicializar las variables que sirven como contadores a 0. Usted les agrega valores, pero como el valor inicial es basura, invocará un comportamiento indefinido , como en la pregunta Compilación en el terminal emite una advertencia de puntero y símbolos extraños
Ejemplo:
#include <stdio.h>
int main(void) {
int i, counter;
for(i = 0; i < 10; ++i)
counter += i;
printf("%d\n", counter);
return 0;
}
Salida:
C02QT2UBFVH6-lm:~ gsamaras$ gcc main.c -Wall -o main
main.c:6:9: warning: variable 'counter' is uninitialized when used here [-Wuninitialized]
counter += i;
^~~~~~~
main.c:4:19: note: initialize the variable 'counter' to silence this warning
int i, counter;
^
= 0
1 warning generated.
C02QT2UBFVH6-lm:~ gsamaras$ ./main
32812
Las reglas anteriores son aplicables para los punteros también. Por ejemplo, los siguientes resultados en un comportamiento indefinido
int main(void)
{
int *p;
p++; // Trying to increment an uninitialized pointer.
}
Tenga en cuenta que el código anterior por sí solo puede no causar un error o un error de segmentación, pero tratar de eliminar la referencia a este puntero más adelante causaría el comportamiento indefinido.
Desreferenciación de un puntero a una variable más allá de su vida útil
int* foo(int bar)
{
int baz = 6;
baz += bar;
return &baz; /* (&baz) copied to new memory location outside of foo. */
} /* (1) The lifetime of baz and bar end here as they have automatic storage
* duration (local variables), thus the returned pointer is not valid! */
int main (void)
{
int* p;
p = foo(5); /* (2) this expression's behavior is undefined */
*p = *p - 6; /* (3) Undefined behaviour here */
return 0;
}
Algunos compiladores lo señalan de manera útil. Por ejemplo, gcc
advierte con:
warning: function returns address of local variable [-Wreturn-local-addr]
y clang
advierte con:
warning: address of stack memory associated with local variable 'baz' returned
[-Wreturn-stack-address]
para el código anterior. Pero los compiladores pueden no ser capaces de ayudar en código complejo.
(1) La referencia de retorno a la variable static
declarada es un comportamiento definido, ya que la variable no se destruye después de dejar el alcance actual.
(2) De acuerdo con ISO / IEC 9899: 2011 6.2.4 §2, "El valor de un puntero se vuelve indeterminado cuando el objeto al que apunta llega al final de su vida útil".
(3) Eliminar la referencia del puntero devuelto por la función foo
es un comportamiento indefinido ya que la memoria a la que hace referencia tiene un valor indeterminado.
División por cero
int x = 0;
int y = 5 / x; /* integer division */
o
double x = 0.0;
double y = 5.0 / x; /* floating point division */
o
int x = 0;
int y = 5 % x; /* modulo operation */
Para la segunda línea en cada ejemplo, donde el valor del segundo operando (x) es cero, el comportamiento no está definido.
Tenga en cuenta que la mayoría de las implementaciones de matemática de punto flotante seguirán un estándar (p. Ej., IEEE 754), en cuyo caso las operaciones como dividir por cero tendrán resultados consistentes (p. Ej., INFINITY
) aunque el estándar C dice que la operación no está definida.
Accediendo a la memoria más allá del trozo asignado
Un puntero a un fragmento de memoria que contiene n
elementos solo puede ser referenciado si está en el rango de memory
y memory + (n - 1)
. La desreferenciación de un puntero fuera de ese rango da como resultado un comportamiento indefinido. Como ejemplo, considere el siguiente código:
int array[3];
int *beyond_array = array + 3;
*beyond_array = 0; /* Accesses memory that has not been allocated. */
La tercera línea accede al cuarto elemento de una matriz que solo tiene 3 elementos de longitud, lo que lleva a un comportamiento indefinido. Del mismo modo, el comportamiento de la segunda línea en el siguiente fragmento de código tampoco está bien definido:
int array[3];
array[3] = 0;
Tenga en cuenta que apuntar más allá del último elemento de una matriz no es un comportamiento indefinido ( beyond_array = array + 3
está bien definido aquí), pero la *beyond_array
referencia es ( *beyond_array
es un comportamiento indefinido). Esta regla también se aplica a la memoria asignada dinámicamente (como los buffers creados a través de malloc
).
Copiando memoria superpuesta
Una amplia variedad de funciones de biblioteca estándar tienen entre sus efectos la copia de secuencias de bytes de una región de memoria a otra. La mayoría de estas funciones tienen un comportamiento indefinido cuando las regiones de origen y destino se superponen.
Por ejemplo, este ...
#include <string.h> /* for memcpy() */
char str[19] = "This is an example";
memcpy(str + 7, str, 10);
... intenta copiar 10 bytes donde las áreas de memoria de origen y destino se superponen con tres bytes. Visualizar:
overlapping area
|
_ _
| |
v v
T h i s i s a n e x a m p l e \0
^ ^
| |
| destination
|
source
Debido a la superposición, el comportamiento resultante no está definido.
Entre las funciones de la biblioteca estándar con una limitación de este tipo se encuentran memcpy()
, strcpy()
, strcat()
, sprintf()
y sscanf()
. El estándar dice de estas y varias otras funciones:
Si la copia tiene lugar entre objetos que se superponen, el comportamiento no está definido.
La función memmove()
es la excepción principal a esta regla. Su definición especifica que la función se comporta como si los datos de origen se copiaron primero en un búfer temporal y luego se escribieron en la dirección de destino. No hay excepción para la superposición de regiones de origen y destino, ni ninguna necesidad de una, por lo que memmove()
tiene un comportamiento bien definido en tales casos.
La distinción refleja una eficiencia vs. compensación generalidad. La copia como la que realizan estas funciones generalmente ocurre entre regiones separadas de la memoria y, a menudo, es posible saber en el momento del desarrollo si una instancia particular de la copia de la memoria estará en esa categoría. Suponiendo que la no superposición permite implementaciones comparativamente más eficientes que no producen resultados correctos de manera confiable cuando la suposición no se cumple. A la mayoría de las funciones de la biblioteca de C se les permite implementaciones más eficientes, y memmove()
llena los vacíos, atendiendo a los casos en que el origen y el destino pueden o se superponen. Para producir el efecto correcto en todos los casos, sin embargo, debe realizar pruebas adicionales y / o emplear una implementación comparativamente menos eficiente.
Lectura de un objeto sin inicializar que no está respaldado por la memoria
La lectura de un objeto causará un comportamiento indefinido, si el objeto es 1 :
- sin inicializar
- definido con duración de almacenamiento automático
- su dirección nunca se toma
La variable a en el siguiente ejemplo satisface todas esas condiciones:
void Function( void )
{
int a;
int b = a;
}
1 (Citado de: ISO: IEC 9899: 201X 6.3.2.1 Lvalores, matrices y designadores de funciones 2)
Si el lvalue designa un objeto de duración de almacenamiento automático que podría haberse declarado con la clase de almacenamiento de registro (nunca se tomó su dirección), y ese objeto no está inicializado (no se declaró con un inicializador y no se realizó ninguna asignación antes de su uso) ), el comportamiento es indefinido.
Carrera de datos
C11 introdujo el soporte para múltiples hilos de ejecución, lo que ofrece la posibilidad de carreras de datos. Un programa contiene una carrera de datos si se accede a un objeto 1 por dos subprocesos diferentes, donde al menos uno de los accesos no es atómico, al menos uno modifica el objeto y la semántica del programa no garantiza que los dos accesos no se superpongan temporalmente. 2 Tenga en cuenta que la concurrencia real de los accesos involucrados no es una condición para una carrera de datos; las carreras de datos cubren una clase más amplia de problemas que surgen de inconsistencias (permitidas) en vistas de memoria de diferentes hilos.
Considera este ejemplo:
#include <threads.h>
int a = 0;
int Function( void* ignore )
{
a = 1;
return 0;
}
int main( void )
{
thrd_t id;
thrd_create( &id , Function , NULL );
int b = a;
thrd_join( id , NULL );
}
El hilo principal llama a thrd_create
para iniciar una nueva función de ejecución de hilo. Function
. El segundo hilo modifica a
, y el hilo principal lee a
. Ninguno de esos accesos es atómico, y los dos hilos no hacen nada individual ni conjuntamente para garantizar que no se superpongan, por lo que hay una carrera de datos.
Entre las formas en que este programa podría evitar la carrera de datos están
- el hilo principal podría realizar su lectura de
a
antes de iniciar el otro hilo; - el hilo principal podría realizar su lectura de
a
después de asegurar a través dethrd_join
que la otra ha terminado; - los hilos podrían sincronizar sus accesos a través de un mutex, cada uno de ellos bloqueando ese mutex antes de acceder a
a
y desbloquearlo después.
Como lo demuestra la opción de exclusión mutua, evitar una carrera de datos no requiere garantizar un orden específico de operaciones, como la modificación de a
subproceso secundario a
antes de que el subproceso principal lo lea; es suficiente (para evitar una carrera de datos) para asegurar que para una ejecución dada, un acceso suceda antes que el otro.
1 Modificar o leer un objeto.
2 (Citado de ISO: IEC 9889: 201x, sección 5.1.2.4 "Ejecuciones multiproceso y carreras de datos")
La ejecución de un programa contiene una carrera de datos si contiene dos acciones en conflicto en subprocesos diferentes, al menos una de las cuales no es atómica, y ninguna sucede antes que la otra. Cualquier carrera de datos de este tipo resulta en un comportamiento indefinido.
Leer el valor del puntero que fue liberado.
Incluso solo leer el valor de un puntero que se liberó (es decir, sin intentar desreferenciarlo) es un comportamiento indefinido (UB), por ejemplo
char *p = malloc(5);
free(p);
if (p == NULL) /* NOTE: even without dereferencing, this may have UB */
{
}
Citando ISO / IEC 9899: 2011 , sección 6.2.4 §2:
[…] El valor de un puntero se vuelve indeterminado cuando el objeto al que apunta (o simplemente pasa) llega al final de su vida útil.
El uso de memoria indeterminada para cualquier cosa, incluida la comparación aparentemente inofensiva o la aritmética, puede tener un comportamiento indefinido si el valor puede ser una representación de trampa para el tipo.
Modificar cadena literal
En este ejemplo de código, el puntero de carácter p
se inicializa en la dirección de una cadena literal. El intento de modificar la cadena literal tiene un comportamiento indefinido.
char *p = "hello world";
p[0] = 'H'; // Undefined behavior
Sin embargo, modificar una matriz mutable de char
directamente, o mediante un puntero, naturalmente no es un comportamiento indefinido, incluso si su inicializador es una cadena literal. Lo siguiente está bien:
char a[] = "hello, world";
char *p = a;
a[0] = 'H';
p[7] = 'W';
Esto se debe a que la cadena literal se copia efectivamente a la matriz cada vez que se inicializa (una vez para las variables con duración estática, cada vez que se crea la matriz para las variables con duración automática o de subproceso: las variables con la duración asignada no se inicializan), y está bien para modificar el contenido de la matriz.
Liberar la memoria dos veces
Liberar memoria dos veces es un comportamiento indefinido, por ejemplo,
int * x = malloc(sizeof(int));
*x = 9;
free(x);
free(x);
Cita del estándar (7.20.3.2. La función libre de C99):
De lo contrario, si el argumento no coincide con un puntero anterior devuelto por la función calloc, malloc o realloc, o si el espacio ha sido desasignado por una llamada a free o realloc, el comportamiento no está definido.
Usando un especificador de formato incorrecto en printf
El uso de un especificador de formato incorrecto en el primer argumento para printf
invoca un comportamiento indefinido. Por ejemplo, el siguiente código invoca un comportamiento indefinido:
long z = 'B';
printf("%c\n", z);
Aquí hay otro ejemplo.
printf("%f\n",0);
Sobre la línea de código hay un comportamiento indefinido. %f
espera doble. Sin embargo, 0 es de tipo int
.
Tenga en cuenta que su compilador generalmente puede ayudarlo a evitar casos como estos, si -Wformat
indicadores adecuados durante la compilación ( -Wformat
en clang
y gcc
). Del último ejemplo:
warning: format specifies type 'double' but the argument has type
'int' [-Wformat]
printf("%f\n",0);
~~ ^
%d
La conversión entre tipos de puntero produce un resultado alineado incorrectamente
Lo siguiente podría tener un comportamiento indefinido debido a una alineación incorrecta del puntero:
char *memory_block = calloc(sizeof(uint32_t) + 1, 1);
uint32_t *intptr = (uint32_t*)(memory_block + 1); /* possible undefined behavior */
uint32_t mvalue = *intptr;
El comportamiento indefinido ocurre cuando el puntero se convierte. De acuerdo con C11, si una conversión entre dos tipos de punteros produce un resultado que se alinea incorrectamente (6.3.2.3), el comportamiento no está definido . Aquí, un uint32_t
podría requerir una alineación de 2 o 4, por ejemplo.
calloc
otro lado, se requiere calloc
para devolver un puntero que esté alineado adecuadamente para cualquier tipo de objeto; por lo tanto, memory_block
está alineado correctamente para contener un uint32_t
en su parte inicial. Luego, en un sistema donde uint32_t
ha requerido una alineación de 2 o 4, memory_block + 1
será una dirección impar y, por lo tanto, no se alineará correctamente.
Observe que el estándar C solicita que la operación de conversión ya no esté definida. Esto se impone porque en las plataformas donde las direcciones están segmentadas, la dirección de byte memory_block + 1
puede que ni siquiera tenga una representación adecuada como un puntero de entero.
La char *
a punteros a otros tipos sin preocuparse por los requisitos de alineación a veces se usa incorrectamente para decodificar estructuras empaquetadas como encabezados de archivos o paquetes de red.
Puede evitar el comportamiento indefinido derivado de una conversión de puntero desalineada utilizando memcpy
:
memcpy(&mvalue, memory_block + 1, sizeof mvalue);
Aquí no se realiza la conversión del puntero a uint32_t*
y los bytes se copian uno por uno.
Esta operación de copia para nuestro ejemplo solo lleva a un valor válido de mvalue
porque:
- Utilizamos
calloc
, por lo que los bytes están correctamente inicializados. En nuestro caso, todos los bytes tienen un valor de0
, pero cualquier otra inicialización adecuada sería suficiente. -
uint32_t
es un tipo de ancho exacto y no tiene bits de relleno - Cualquier patrón de bits arbitrario es una representación válida para cualquier tipo sin signo.
Suma o resta del puntero no correctamente delimitada.
El siguiente código tiene un comportamiento indefinido:
char buffer[6] = "hello";
char *ptr1 = buffer - 1; /* undefined behavior */
char *ptr2 = buffer + 5; /* OK, pointing to the '\0' inside the array */
char *ptr3 = buffer + 6; /* OK, pointing to just beyond */
char *ptr4 = buffer + 7; /* undefined behavior */
De acuerdo con C11, si la suma o resta de un puntero a, o más allá de un objeto de matriz y un tipo de entero, produce un resultado que no apunta, o simplemente más allá, del mismo objeto de matriz, el comportamiento es indefinido (6.5.6 ).
Además, es naturalmente un comportamiento indefinido desreferenciar un puntero que apunta a algo más allá de la matriz:
char buffer[6] = "hello";
char *ptr3 = buffer + 6; /* OK, pointing to just beyond */
char value = *ptr3; /* undefined behavior */
Modificar una variable const mediante un puntero
int main (void)
{
const int foo_readonly = 10;
int *foo_ptr;
foo_ptr = (int *)&foo_readonly; /* (1) This casts away the const qualifier */
*foo_ptr = 20; /* This is undefined behavior */
return 0;
}
Citando ISO / IEC 9899: 201x , sección 6.7.3 §2:
Si se intenta modificar un objeto definido con un tipo constante calificado mediante el uso de un lvalue con un tipo no calificado, el comportamiento no está definido. [...]
(1) En GCC, esto puede lanzar la siguiente advertencia: warning: assignment discards 'const' qualifier from pointer target type [-Wdiscarded-qualifiers]
Pasar un puntero nulo a printf% s conversión
La conversión %s
de printf
indica que el argumento correspondiente es un puntero al elemento inicial de una matriz de tipo de carácter . Un puntero nulo no apunta al elemento inicial de ninguna matriz de tipo de carácter y, por lo tanto, el comportamiento de los siguientes no está definido:
char *foo = NULL;
printf("%s", foo); /* undefined behavior */
Sin embargo, el comportamiento indefinido no siempre significa que el programa se bloquee; algunos sistemas toman medidas para evitar el bloqueo que normalmente ocurre cuando se elimina la referencia a un puntero nulo. Por ejemplo, se sabe que Glibc imprime
(null)
para el código de arriba. Sin embargo, agregue (solo) una nueva línea a la cadena de formato y obtendrá un bloqueo:
char *foo = 0;
printf("%s\n", foo); /* undefined behavior */
En este caso, sucede porque GCC tiene una optimización que se convierte en printf("%s\n", argument);
en una llamada a puts
con puts(argument)
, y puts
en Glibc no maneja los punteros nulos. Todo este comportamiento es estándar conforme.
Tenga en cuenta que el puntero nulo es diferente de una cadena vacía . Por lo tanto, lo siguiente es válido y no tiene un comportamiento indefinido. Solo imprimirá una nueva línea :
char *foo = "";
printf("%s\n", foo);
Enlace inconsistente de identificadores
extern int var;
static int var; /* Undefined behaviour */
C11, §6.2.2, 7 dice:
Si, dentro de una unidad de traducción, aparece el mismo identi fi cador con enlaces internos y externos, el comportamiento no está definido.
Tenga en cuenta que si una declaración previa de un identificador es visible, tendrá el enlace de la declaración anterior. C11, §6.2.2, 4 lo permite:
Para un identi fi cador declarado con el especi fi cador de clase de almacenamiento externo en un ámbito en el que se puede ver una declaración previa de ese identi fi cador, 31) si la declaración anterior especifica un enlace interno o externo, el enlace del identi fi cador en la declaración posterior es el mismo que El enlace especificado en la declaración anterior. Si no hay una declaración previa visible, o si la declaración anterior no especifica ningún enlace, entonces el identi fi cador tiene un enlace externo.
/* 1. This is NOT undefined */
static int var;
extern int var;
/* 2. This is NOT undefined */
static int var;
static int var;
/* 3. This is NOT undefined */
extern int var;
extern int var;
Usando fflush en un flujo de entrada
Los estándares POSIX y C establecen explícitamente que el uso de fflush
en una secuencia de entrada es un comportamiento indefinido. El fflush
está definido solo para flujos de salida.
#include <stdio.h>
int main()
{
int i;
char input[4096];
scanf("%i", &i);
fflush(stdin); // <-- undefined behavior
gets(input);
return 0;
}
No hay una manera estándar de descartar caracteres no leídos de un flujo de entrada. Por otro lado, algunas implementaciones usan fflush
para borrar el búfer stdin
. Microsoft define el comportamiento de fflush
en una secuencia de entrada: si la secuencia está abierta para la entrada, fflush
borra el contenido del búfer. Según POSIX.1-2008, el comportamiento de fflush
es indefinido a menos que el archivo de entrada se pueda buscar.
Consulte Uso de fflush(stdin)
para obtener muchos más detalles.
Desplazamiento de bits utilizando recuentos negativos o más allá del ancho del tipo
Si el valor del conteo de cambios es negativo, entonces las operaciones de desplazamiento a la izquierda y derecha no están definidas 1 :
int x = 5 << -3; /* undefined */
int x = 5 >> -3; /* undefined */
Si el desplazamiento a la izquierda se realiza en un valor negativo , no está definido:
int x = -5 << 3; /* undefined */
Si el desplazamiento a la izquierda se realiza en un valor positivo y el resultado del valor matemático no se puede representar en el tipo, es indefinido 1 :
/* Assuming an int is 32-bits wide, the value '5 * 2^72' doesn't fit
* in an int. So, this is undefined. */
int x = 5 << 72;
Tenga en cuenta que el desplazamiento a la derecha en un valor negativo (.eg -5 >> 3
) no está indefinido sino que está definido por la implementación .
1 Cotización ISO / IEC 9899: 201x , sección 6.5.7:
Si el valor del operando derecho es negativo o es mayor o igual que el ancho del operando izquierdo promovido, el comportamiento no está definido.
Modificación de la cadena devuelta por las funciones getenv, strerror y setlocale
La modificación de las cadenas devueltas por las funciones estándar getenv()
, strerror()
y setlocale()
no está definida. Por lo tanto, las implementaciones pueden usar almacenamiento estático para estas cadenas.
La función getenv (), C11, §7.22.4.7, 4 , dice:
La función getenv devuelve un puntero a una cadena asociada con el miembro de la lista coincidente. La cadena a la que se apunta no debe ser modificada por el programa, pero puede ser sobrescrita por una llamada posterior a la función getenv.
La función strerror (), C11, §7.23.6.3, 4 dice:
La función strerror devuelve un puntero a la cadena, cuyo contenido es localpeci fi cado. La matriz a la que se apunta no debe ser modificada por el programa, pero puede ser sobrescrita por una llamada posterior a la función strerror.
La función setlocale (), C11, §7.11.1.1, 8 dice:
El puntero a la cadena devuelto por la función setlocale es tal que una llamada posterior con ese valor de cadena y su categoría asociada restaurará esa parte de la configuración regional del programa. La cadena a la que se apunta no debe ser modificada por el programa, pero puede ser sobrescrita por una llamada posterior a la función setlocale.
De manera similar, la función localeconv()
devuelve un puntero a la struct lconv
que no se modificará.
La función localeconv (), C11, §7.11.2.1, 8 dice:
La función localeconv devuelve un puntero al objeto rellenado. La estructura apuntada por el valor de retorno no debe ser modificada por el programa, pero puede ser sobrescrita por una llamada posterior a la función localeconv.
Volviendo de una función declarada con el especificador de función `_Noreturn` o` noreturn`
El especificador de función _Noreturn
se introdujo en C11. El encabezado <stdnoreturn.h>
proporciona una macro noreturn
que se expande a _Noreturn
. Por lo tanto, usar _Noreturn
o noreturn
desde <stdnoreturn.h>
es <stdnoreturn.h>
y equivalente.
Una función que se declara con _Noreturn
(o noreturn
) no tiene permitido regresar a su interlocutor. Si tal función no vuelve a su interlocutor, el comportamiento no está definido.
En el siguiente ejemplo, func()
se declara con el especificador noreturn
pero regresa a su llamador.
#include <stdio.h>
#include <stdlib.h>
#include <stdnoreturn.h>
noreturn void func(void);
void func(void)
{
printf("In func()...\n");
} /* Undefined behavior as func() returns */
int main(void)
{
func();
return 0;
}
gcc
y clang
producen advertencias para el programa anterior:
$ gcc test.c
test.c: In function ‘func’:
test.c:9:1: warning: ‘noreturn’ function does return
}
^
$ clang test.c
test.c:9:1: warning: function declared 'noreturn' should not return [-Winvalid-noreturn]
}
^
Un ejemplo usando noreturn
que tiene un comportamiento bien definido:
#include <stdio.h>
#include <stdlib.h>
#include <stdnoreturn.h>
noreturn void my_exit(void);
/* calls exit() and doesn't return to its caller. */
void my_exit(void)
{
printf("Exiting...\n");
exit(0);
}
int main(void)
{
my_exit();
return 0;
}