near - ¿Qué tan indefinido es el comportamiento indefinido?
tag and title near me (8)
No estoy seguro de entender el grado en que un comportamiento indefinido puede poner en peligro un programa.
Digamos que tengo este código:
#include <stdio.h>
int main()
{
int v = 0;
scanf("%d", &v);
if (v != 0)
{
int *p;
*p = v; // Oops
}
return v;
}
¿El comportamiento de este programa está indefinido solo para aquellos casos en los que v
es distinto de cero, o es indefinido incluso si v
es cero?
Cuando declaras variables (especialmente punteros explícitos), se asigna un trozo de memoria (generalmente un int). Esta paz de memoria se marca como free
para el sistema, pero el valor anterior almacenado allí no se borra (esto depende de la asignación de memoria implementada por el compilador, puede llenar el lugar con ceros) por lo que su int *p
tendrá una valor aleatorio (basura) que debe interpretar como integer
. El resultado es el lugar en la memoria donde p
apunta a (puntada de p). Cuando intenta dereference
(es decir, acceder a esta parte de la memoria), estará (casi siempre) ocupado por otro proceso / programa, por lo que intentar alterar / modificar la memoria de otros provocará problemas de access violation
por parte del memory manager
.
Entonces, en este ejemplo, cualquier otro valor luego 0 dará como resultado un comportamiento indefinido, porque nadie sabe a qué *p
apuntará en este momento.
Espero que esta explicación sea de alguna ayuda.
Editar: Ah, lo siento, de nuevo algunas respuestas delante de mí :)
Dado que tiene la etiqueta de language-lawyer , tengo un argumento extremadamente nítido de que el comportamiento del programa no está definido independientemente de la información del usuario, pero no por las razones que podría esperar, aunque puede estar bien definido (cuando v==0
) dependiendo de la implementación.
El programa define main
como
int main()
{
/* ... */
}
C99 5.1.2.2.1 dice que la función principal se definirá como
int main(void) { /* ... */ }
o como
int main(int argc, char *argv[]) { /* ... */ }
o equivalente; o de alguna otra manera definida por la implementación.
int main()
no es equivalente a int main(void)
. El primero, como una declaración, dice que main
toma un número y tipo de argumentos fijos pero no especificados; el último dice que no requiere argumentos. La diferencia es que una llamada recursiva a main
como
main(42);
es una violación de restricción si usa int main(void)
, pero no si usa int main()
.
Por ejemplo, estos dos programas:
int main() {
if (0) main(42); /* not a constraint violation */
}
int main(void) {
if (0) main(42); /* constraint violation, requires a diagnostic */
}
no son equivalentes
Si la implementación documenta que acepta int main()
como una extensión, entonces esto no se aplica a esa implementación .
Este es un punto extremadamente quisquilloso (sobre el que no todos están de acuerdo), y se evita fácilmente al declarar int main(void)
(que debe hacer de todos modos, todas las funciones deben tener prototipos, no declaraciones / definiciones antiguas).
En la práctica, cada compilador que he visto acepta int main()
sin quejas.
Para responder la pregunta que se pretendía:
Una vez que se realiza el cambio, el comportamiento del programa está bien definido si v==0
, y no está definido si v!=0
. Sí, la definición del comportamiento del programa depende de la entrada del usuario. No hay nada particularmente inusual en eso.
Diría que el comportamiento no está definido solo si los usuarios insertan cualquier número diferente de 0. Después de todo, si la sección del código ofensivo no se ejecuta en realidad las condiciones para UB no se cumplen (es decir, no se crea el puntero no inicializado) ni desreferenciados).
Puede encontrar una pista de esto en el estándar, en 3.4.3:
comportamiento, al usar una construcción de programa errónea o no portable o datos erróneos, para los cuales esta Norma Internacional no impone requisitos
Esto parece implicar que, si dichos "datos erróneos" fueran correctos, el comportamiento estaría perfectamente definido, lo que parece más o menos aplicable a nuestro caso.
Ejemplo adicional: desbordamiento de enteros. Cualquier programa que realice una adición con datos proporcionados por el usuario sin hacer una verificación exhaustiva está sujeto a este tipo de comportamiento indefinido, pero una adición es UB solo cuando el usuario proporciona datos tan particulares.
Es simple. Si un fragmento de código no se ejecuta, no tiene un comportamiento !!!, ya sea definido o no .
Si la entrada es 0, entonces el código dentro if
no se ejecuta, entonces depende del resto del programa para determinar si el comportamiento está definido (en este caso está definido).
Si la entrada no es 0, ejecuta código que todos sabemos es un caso de comportamiento indefinido.
Permítanme dar un argumento sobre por qué creo que esto aún no está definido.
En primer lugar, los encuestados que dicen que esto está "en su mayoría definido" o algo así, en función de su experiencia con algunos compiladores, son simplemente incorrectos. Una pequeña modificación de su ejemplo servirá para ilustrar:
#include <stdio.h>
int
main()
{
int v;
scanf("%d", &v);
if (v != 0)
{
printf("Hello/n");
int *p;
*p = v; // Oops
}
return v;
}
¿Qué hace este programa si proporciona "1" como entrada? Si responde "Imprime Hola y luego se cuelga", está equivocado. "Comportamiento no definido" no significa que el comportamiento de un enunciado específico no esté definido; significa que el comportamiento de todo el programa no está definido. El compilador puede suponer que no se involucra en un comportamiento indefinido, por lo que en este caso, puede suponer que v
es distinto de cero y simplemente no emitirá ningún código entre corchetes, incluido el printf
.
Si crees que esto no es probable, piénsalo de nuevo. GCC puede no realizar este análisis exactamente, pero sí realiza muy similares. Mi ejemplo favorito que realmente ilustra el punto de verdad:
int test(int x) { return x+1 > x; }
Intente escribir un pequeño programa de prueba para imprimir INT_MAX
, INT_MAX+1
y test(INT_MAX)
. (Asegúrese de habilitar la optimización.) Una implementación típica podría mostrar INT_MAX
como 2147483647, INT_MAX+1
como -2147483648 y test(INT_MAX)
como 1.
De hecho, GCC compila esta función para devolver una constante 1. ¿Por qué? Como el desbordamiento de enteros es un comportamiento indefinido, el compilador puede suponer que no está haciendo eso, por lo tanto x no puede igualar INT_MAX
, por lo tanto x+1
es mayor que x
, por lo tanto, esta función puede devolver 1 incondicionalmente.
El comportamiento indefinido puede dar como resultado variables que no son iguales a ellos mismos, números negativos que comparan números mayores que positivos (ver el ejemplo anterior) y otros comportamientos extraños. Cuanto más inteligente es el compilador, más extraño es el comportamiento.
De acuerdo, admito que no puedo citar un capítulo y un versículo del estándar para responder la pregunta exacta que me pides. Pero las personas que dicen "Sí, sí, pero en la vida real desreferenciando a NULL solo dan un fallo seg" están más equivocados de lo que posiblemente pueden imaginar, y se equivocan más con cada generación de compiladores.
Y en la vida real, si el código está muerto, debes eliminarlo; si no está muerto, no debes invocar un comportamiento indefinido. Esa es mi respuesta a tu pregunta.
Si v es 0, su asignación de puntero aleatorio nunca se ejecuta, y la función devolverá cero, por lo que no es un comportamiento indefinido
Tu programa está bastante bien definido. Si v == 0 entonces devuelve cero. Si v! = 0, salpica un punto aleatorio en la memoria.
p es un puntero, su valor inicial podría ser cualquier cosa, ya que no lo inicializa. El valor real depende del sistema operativo (algo de memoria cero antes de dársela a su proceso, otros no), su compilador, su hardware y lo que estaba en la memoria antes de ejecutar su programa.
La asignación del puntero solo está escribiendo en una ubicación de memoria aleatoria. Podría tener éxito, podría corromper otros datos o podría fallar por segmentación, depende de todos los factores anteriores.
En lo que respecta a C, está bastante bien definido que las variables no identificadas no tienen un valor conocido, y su programa (aunque podría compilar) no será correcto.
Yo diría que hace que todo el programa esté indefinido.
La clave del comportamiento indefinido es que no está definido . El compilador puede hacer lo que quiera cuando vea esa declaración. Ahora, cada compilador lo manejará como se espera, pero aún tienen todo el derecho de hacer lo que quieran, incluso cambiar partes que no estén relacionadas con él.
Por ejemplo, un compilador puede elegir agregar un mensaje "este programa puede ser peligroso" al programa si detecta un comportamiento indefinido. Esto cambiaría la salida, ya sea que v
sea 0 o no.