¿Por qué esto para el bucle sale en algunas plataformas y no en otras?

240

Recientemente comencé a aprender C y estoy tomando una clase con C como asignatura. Actualmente estoy jugando con bucles y me encuentro con un comportamiento extraño que no sé cómo explicar.

#include <stdio.h>

int main()
{
  int array[10],i;

  for (i = 0; i <=10 ; i++)
  {
    array[i]=0; /*code should never terminate*/
    printf("test \n");

  }
  printf("%d \n", sizeof(array)/sizeof(int));
  return 0;
}

En mi computadora portátil con Ubuntu 14.04, este código no se rompe. Se ejecuta hasta su finalización. En la computadora de mi escuela que ejecuta CentOS 6.6, también funciona bien. En Windows 8.1, el ciclo nunca termina.

Lo que es aún más extraño es que cuando edito la condición del forbucle en:, i <= 11el código solo termina en mi computadora portátil con Ubuntu. Nunca termina en CentOS y Windows.

¿Alguien puede explicar lo que está sucediendo en la memoria y por qué los diferentes sistemas operativos que ejecutan el mismo código dan resultados diferentes?

EDITAR: Sé que el bucle for sale de los límites. Lo estoy haciendo intencionalmente. Simplemente no puedo entender cómo el comportamiento puede ser diferente en diferentes sistemas operativos y computadoras.

JonCav
fuente
147
Como está sobrepasando la matriz, se produce un comportamiento indefinido. El comportamiento indefinido significa que cualquier cosa puede suceder, incluso si parece funcionar. Por lo tanto, "el código nunca debe terminar" no es una expectativa válida.
kaylum
37
Exactamente, bienvenido a C. Su matriz tiene 10 elementos, numerados del 0 al 9.
Yetti99
14
@ JonCav Rompiste el código. Está obteniendo un comportamiento indefinido que es código roto.
kaylum
50
Bueno, el punto es que el comportamiento indefinido es exactamente eso. No puede probarlo de manera confiable y demostrar que algo definido sucederá. Lo que probablemente está sucediendo en su máquina con Windows es que la variable ise almacena justo después del final arrayy la está sobrescribiendo array[10]=0;. Este podría no ser el caso en una compilación optimizada en la misma plataforma, que puede almacenarse ien un registro y nunca referirse a ella en la memoria.
arroz
46
Porque la no previsibilidad es una propiedad fundamental del Comportamiento indefinido. Necesitas entender esto ... Absolutamente todas las apuestas están apagadas.
arroz

Respuestas:

356

En mi computadora portátil con Ubuntu 14.04, este código no se rompe y se ejecuta hasta su finalización. En la computadora de mi escuela que ejecuta CentOS 6.6, también funciona bien. En Windows 8.1, el ciclo nunca termina.

Lo que es más extraño es cuando edito el condicional del forbucle a:, i <= 11el código solo termina en mi computadora portátil con Ubuntu. CentOS y Windows nunca terminan.

Acabas de descubrir la memoria pisando fuerte. Puede leer más sobre esto aquí: ¿Qué es un "pisoteo de memoria"?

Cuando asigna int array[10],i;, esas variables van a la memoria (específicamente, se asignan en la pila, que es un bloque de memoria asociado con la función). array[]y ison probablemente adyacentes entre sí en la memoria. Parece que en Windows 8.1, ise encuentra en array[10]. En CentOS, ise encuentra en array[11]. Y en Ubuntu, no está en ningún lugar (¿tal vez está en array[-1]?).

Intente agregar estas declaraciones de depuración a su código. Debe notar que en la iteración 10 u 11, array[i]apunta a i.

#include <stdio.h>
 
int main() 
{ 
  int array[10],i; 
 
  printf ("array: %p, &i: %p\n", array, &i); 
  printf ("i is offset %d from array\n", &i - array);

  for (i = 0; i <=11 ; i++) 
  { 
    printf ("%d: Writing 0 to address %p\n", i, &array[i]); 
    array[i]=0; /*code should never terminate*/ 
  } 
  return 0; 
} 
Pregunta C
fuente
66
¡Hey gracias! Eso realmente explicaba bastante. En Windows indica que si se compensa 10 de la matriz, mientras que en CentOS y Ubuntu, es -1. Lo más extraño es que si comento su código de depurador, CentOS no puede ejecutar el código (se cuelga), pero con su código de depuración se ejecuta. C parece ser un lenguaje muy hasta ahora X_x
JonCav
12
@JonCav "se cuelga" puede suceder si escribir para array[10]destruir el marco de la pila, por ejemplo. ¿Cómo puede haber una diferencia entre el código con o sin la salida de depuración? Si inunca se necesita la dirección de , el compilador puede optimizar i. en un registro, cambiando así el diseño de la memoria en la pila ...
Hagen von Eitzen
2
No creo que esté colgando, creo que está en un bucle infinito porque está volviendo a cargar el contador del bucle desde la memoria (que acaba de ponerse a cero array[10]=0. Si compilara su código con la optimización activada, esto probablemente no sucedería. reglas de alias que limitan los tipos de acceso a la memoria que se debe suponer que potencialmente se superponen a otra memoria. Como variable local de la que nunca tomas la dirección, creo que un compilador debería ser capaz de asumir que nada lo alias. De todos modos, descartar el final de una matriz es un comportamiento indefinido. Siempre trate de evitarlo dependiendo de eso.
Peter Cordes
44
Otra alternativa es que un compilador optimizador elimina la matriz por completo, ya que no tiene un efecto observable (en el código original de la pregunta). Por lo tanto, el código resultante podría imprimir esa cadena constante once veces, seguido de imprimir el tamaño constante y así hacer que el desbordamiento sea completamente imperceptible.
Holger
99
@ JonCav Yo diría que, en general , no necesita saber más sobre la administración de la memoria y, en su lugar, simplemente no debe escribir código indefinido, específicamente, no escriba más allá del final de una matriz ...
T. Kiley
98

El error se encuentra entre estos fragmentos de código:

int array[10],i;

for (i = 0; i <=10 ; i++)

array[i]=0;

Como arraysolo tiene 10 elementos, en la última iteración array[10] = 0;hay un desbordamiento de búfer. Los desbordamientos de la memoria intermedia son COMPORTAMIENTO INDEFINIDO , lo que significa que pueden formatear su disco duro o hacer que los demonios salgan volando de su nariz.

Es bastante común que todas las variables de pila se distribuyan adyacentes entre sí. Si ise encuentra en array[10]las escrituras a, a continuación, la UB se restablecerá ia 0, lo que conduce al bucle sin terminar.

Para solucionarlo, cambie la condición del bucle a i < 10.

o11c
fuente
66
Nitpick: en realidad no puede formatear el disco duro en ningún SO sano en el mercado a menos que esté ejecutando como root (o el equivalente).
Kevin
26
@Kevin cuando invocas a UB, renuncias a cualquier reclamo de cordura.
o11c
77
No importa si su código es correcto. El sistema operativo no te permitirá hacer eso.
Kevin
2
@Kevin El ejemplo con el formateo de su disco duro se originó mucho antes de que ese fuera el caso. Incluso los unixes de la época (donde se originó C) estaban muy contentos de permitirte hacer cosas así, e incluso hoy, muchas de las distribuciones te permitirán comenzar a eliminar todo rm -rf /incluso cuando no eres root, no "formateando" todo el disco, por supuesto, pero aún destruyendo todos sus datos. Ay.
Luaan
55
@Kevin, pero el comportamiento indefinido puede aprovechar una vulnerabilidad del sistema operativo y luego elevarse para instalar un nuevo controlador de disco duro y luego comenzar a restregar la unidad.
Ratchet Freak
38

En lo que debería ser la última ejecución del ciclo, debe escribir array[10], pero solo hay 10 elementos en la matriz, numerados del 0 al 9. La especificación del lenguaje C dice que este es un "comportamiento indefinido". Lo que esto significa en la práctica es que su programa intentará escribir en la intpieza de memoria del tamaño que se encuentra inmediatamente después arrayen la memoria. Lo que sucede entonces depende de lo que, de hecho, se encuentra allí, y esto depende no solo del sistema operativo sino también del compilador, de las opciones del compilador (como la configuración de optimización), de la arquitectura del procesador, del código circundante , etc. Incluso podría variar de una ejecución a otra, por ejemplo, debido a la aleatorización del espacio de direcciones (probablemente no en este ejemplo de juguete, pero sucede en la vida real). Algunas posibilidades incluyen:

  • La ubicación no fue utilizada. El ciclo termina normalmente.
  • La ubicación se usó para algo que resultó tener el valor 0. El ciclo termina normalmente.
  • La ubicación contenía la dirección de retorno de la función. El ciclo termina normalmente, pero luego el programa se bloquea porque intenta saltar a la dirección 0.
  • La ubicación contiene la variable i. El ciclo nunca termina porque se ireinicia en 0.
  • La ubicación contiene alguna otra variable. El ciclo termina normalmente, pero luego suceden cosas "interesantes".
  • La ubicación es una dirección de memoria no válida, por ejemplo, porque arrayestá justo al final de una página de memoria virtual y la página siguiente no está asignada.
  • Demonios salen volando de tu nariz . Afortunadamente, la mayoría de las computadoras carecen del hardware necesario.

Lo que observó en Windows fue que el compilador decidió colocar la variable iinmediatamente después de la matriz en la memoria, por lo que array[10] = 0terminó asignando a i. En Ubuntu y CentOS, el compilador no se ubicó iallí. Casi todas las implementaciones de C agrupan variables locales en la memoria, en una pila de memoria , con una excepción importante: algunas variables locales se pueden colocar completamente en registros . Incluso si la variable está en la pila, el compilador determina el orden de las variables, y puede depender no solo del orden en el archivo fuente sino también de sus tipos (para evitar desperdiciar memoria en restricciones de alineación que dejarían agujeros) , en sus nombres, en algún valor hash utilizado en la estructura de datos interna de un compilador, etc.

Si desea averiguar qué decidió hacer su compilador, puede pedirle que le muestre el código del ensamblador. Ah, y aprende a descifrar ensamblador (es más fácil que escribirlo). Con GCC (y algunos otros compiladores, especialmente en el mundo Unix), pase la opción -Sde producir código ensamblador en lugar de un binario. Por ejemplo, aquí está el fragmento de ensamblador para que el bucle se compile con GCC en amd64 con la opción de optimización -O0(sin optimización), con comentarios agregados manualmente:

.L3:
    movl    -52(%rbp), %eax           ; load i to register eax
    cltq
    movl    $0, -48(%rbp,%rax,4)      ; set array[i] to 0
    movl    $.LC0, %edi
    call    puts                      ; printf of a constant string was optimized to puts
    addl    $1, -52(%rbp)             ; add 1 to i
.L2:
    cmpl    $10, -52(%rbp)            ; compare i to 10
    jle     .L3

Aquí la variable iestá 52 bytes debajo de la parte superior de la pila, mientras que la matriz comienza 48 bytes debajo de la parte superior de la pila. Por lo tanto, este compilador se coloca ijusto antes de la matriz; sobrescribiría isi le escribiera array[-1]. Si cambia array[i]=0a array[9-i]=0, obtendrá un bucle infinito en esta plataforma particular con estas opciones de compilador particulares.

Ahora compilemos su programa con gcc -O1.

    movl    $11, %ebx
.L3:
    movl    $.LC0, %edi
    call    puts
    subl    $1, %ebx
    jne     .L3

Eso es más corto! El compilador no solo se ha negado a asignar una ubicación de pila i, solo se almacena en el registro ebx, sino que no se ha molestado en asignar memoria arrayo generar código para configurar sus elementos, porque notó que ninguno de los elementos son utilizados alguna vez

Para que este ejemplo sea más revelador, asegurémonos de que las asignaciones de la matriz se realicen proporcionando al compilador algo que no puede optimizar. Una forma sencilla de hacerlo es utilizar la matriz de otro archivo - a causa de compilación separada, el compilador no sabe lo que pasa en otro archivo (a menos que se optimiza en tiempo de enlace, que gcc -O0o gcc -O1no lo hace). Crear un archivo fuente que use_array.ccontenga

void use_array(int *array) {}

y cambia tu código fuente a

#include <stdio.h>
void use_array(int *array);

int main()
{
  int array[10],i;

  for (i = 0; i <=10 ; i++)
  {
    array[i]=0; /*code should never terminate*/
    printf("test \n");

  }
  printf("%zd \n", sizeof(array)/sizeof(int));
  use_array(array);
  return 0;
}

Compilar con

gcc -c use_array.c
gcc -O1 -S -o with_use_array1.c with_use_array.c use_array.o

Esta vez el código del ensamblador se ve así:

    movq    %rsp, %rbx
    leaq    44(%rsp), %rbp
.L3:
    movl    $0, (%rbx)
    movl    $.LC0, %edi
    call    puts
    addq    $4, %rbx
    cmpq    %rbp, %rbx
    jne     .L3

Ahora la matriz está en la pila, 44 bytes desde la parte superior. ¿Qué hay de i? ¡No aparece en ningún lado! Pero el contador de bucle se mantiene en el registro rbx. No es exactamente i, pero la dirección de la array[i]. El compilador ha decidido que, dado que el valor de inunca se usó directamente, no tenía sentido realizar operaciones aritméticas para calcular dónde almacenar 0 durante cada ejecución del ciclo. En cambio, esa dirección es la variable de bucle, y la aritmética para determinar los límites se realizó en parte en tiempo de compilación (multiplique 11 iteraciones por 4 bytes por elemento de matriz para obtener 44) y en parte en tiempo de ejecución, pero de una vez por todas antes de que comience el bucle ( realizar una resta para obtener el valor inicial).

Incluso en este ejemplo muy simple, hemos visto cómo cambiar las opciones del compilador (activar la optimización) o cambiar algo menor ( array[i]a array[9-i]) o incluso cambiar algo aparentemente no relacionado (agregar la llamada a use_array) puede marcar una diferencia significativa en lo que generó el programa ejecutable por el compilador lo hace. Las optimizaciones del compilador pueden hacer muchas cosas que pueden parecer poco intuitivas en los programas que invocan un comportamiento indefinido . Es por eso que el comportamiento indefinido se deja completamente indefinido. Cuando se desvía ligeramente de las pistas, en los programas del mundo real, puede ser muy difícil entender la relación entre lo que hace el código y lo que debería haber hecho, incluso para programadores experimentados.

Gilles 'SO- deja de ser malvado'
fuente
25

A diferencia de Java, C no verifica los límites de la matriz, es decir, no existe ArrayIndexOutOfBoundsException, el trabajo de asegurarse de que el índice de la matriz sea válido se deja al programador. Hacer esto a propósito conduce a un comportamiento indefinido, cualquier cosa podría suceder.


Para una matriz:

int array[10]

los índices solo son válidos en el rango 0de 9. Sin embargo, estás intentando:

for (i = 0; i <=10 ; i++)

acceda array[10]aquí, cambie la condición ai < 10

Yu Hao
fuente
66
Hacerlo no a propósito también conduce a un comportamiento indefinido: ¡el compilador no puede decirlo! ;-)
Toby Speight
1
Simplemente use una macro para emitir sus errores como advertencias: #define UNINTENDED_MISTAKE (EXP) printf ("Advertencia:" #EXP "error \ n");
lkraider
1
Quiero decir, si está cometiendo un error a propósito, podría identificarlo como tal y asegurarse de evitar el comportamiento indefinido; D
lkraider
19

Tiene una violación de límites, y en las plataformas que no terminan, creo que está configurando inadvertidamente ia cero al final del ciclo, para que comience nuevamente.

array[10]no es válido; contiene 10 elementos, a array[0]través array[9], y array[10]es el undécimo. Su ciclo debe escribirse para detenerse antes 10 , de la siguiente manera:

for (i = 0; i < 10; i++)

Donde array[10]land está definido por la implementación, y de manera divertida, en dos de sus plataformas, aterriza en i, que esas plataformas aparentemente presentan directamente después array. ise establece en cero y el ciclo continúa para siempre. Para sus otras plataformas, ipuede estar ubicado antes arrayo arraypuede tener algo de relleno después.

Derek T. Jones
fuente
No creo que valgrind pueda atrapar esto ya que todavía es una ubicación válida, pero ASAN sí.
o11c
13

Usted declara que los int array[10]medios arraytienen un índice 0de 9( 10elementos enteros totales que puede contener). Pero el siguiente bucle,

for (i = 0; i <=10 ; i++)

pasará 0a 10significa 11tiempo. Por i = 10lo tanto, cuándo desbordará el búfer y causará un comportamiento indefinido .

Intenta esto:

for (i = 0; i < 10 ; i++)

o,

for (i = 0; i <= 9 ; i++)
rakeb.mazharul
fuente
7

No está definido en array[10], y proporciona un comportamiento indefinido como se describió anteriormente. Piénsalo así:

Tengo 10 artículos en mi carrito de compras. Son:

0: Una caja de cereal
1: Pan
2: Leche
3: Pastel
4: Huevos
5: Pastel
6: A 2 litros de refresco
7: Ensalada
8: Hamburguesas
9: Helado

cart[10]no está definido y puede dar una excepción fuera de límites en algunos compiladores. Pero, muchos aparentemente no lo hacen. El undécimo elemento aparente es un elemento que no está realmente en el carrito. El undécimo elemento apunta a lo que voy a llamar un "elemento poltergeist". Nunca existió, pero estaba allí.

¿Por qué algunos compiladores dan iun índice de array[10]oarray[11] incluso array[-1]es debido a su declaración de inicialización / declaración? Algunos compiladores interpretan esto como:

  • "Asignar 10 bloques de ints para array[10]y otro intbloque. Para que sea más fácil, colóquelos uno al lado del otro".
  • Igual que antes, pero muévalo uno o dos espacios, de modo que array[10] no apunte i.
  • Haga lo mismo que antes, pero asigne ien array[-1](porque un índice de una matriz no puede, o no debería, ser negativo), o asignarlo en un lugar completamente diferente porque el sistema operativo puede manejarlo, y es más seguro.

Algunos compiladores quieren que las cosas vayan más rápido, y algunos compiladores prefieren la seguridad. Se trata del contexto. Si estaba desarrollando una aplicación para el antiguo sistema operativo BREW (el sistema operativo de un teléfono básico), por ejemplo, no me importaría la seguridad. Si estaba desarrollando para un iPhone 6, podría funcionar rápido sin importar qué, por lo que necesitaría un énfasis en la seguridad. (En serio, ¿has leído las pautas de la tienda de aplicaciones de Apple o has leído sobre el desarrollo de Swift y Swift 2.0?)

DDPWNAGE
fuente
Nota: Escribí la lista para que aparezca "0, 1, 2, 3, 4, 5, 6, 7, 8, 9", pero el lenguaje de marcado de SO fijó las posiciones de mi lista ordenada.
DDPWNAGE
6

Como creó una matriz de tamaño 10, la condición del bucle debe ser la siguiente:

int array[10],i;

for (i = 0; i <10 ; i++)
{

Actualmente está intentando acceder a la ubicación no asignada desde la memoria utilizando array[10]y está causando un comportamiento indefinido . Comportamiento indefinido significa que su programa se comportará de manera indeterminada, por lo que puede dar diferentes resultados en cada ejecución.

Steephen
fuente
5

Bueno, el compilador de C tradicionalmente no verifica los límites. Puede obtener un error de segmentación en caso de que se refiera a una ubicación que no "pertenezca" a su proceso. Sin embargo, las variables locales se asignan en la pila y, dependiendo de la forma en que se asigne la memoria, el área justo más allá de la matriz ( array[10]) puede pertenecer al segmento de memoria del proceso. Por lo tanto, no se lanza una trampa de falla de segmentación y eso es lo que parece experimentar. Como otros han señalado, este es un comportamiento indefinido en C y su código puede considerarse errático. Como estás aprendiendo C, es mejor que te acostumbres a comprobar los límites de tu código.

unxnut
fuente
4

Más allá de la posibilidad de que la memoria pueda establecerse de modo que un intento de escribir a[10]realmente sobrescriba i, también sería posible que un compilador de optimización pueda determinar que la prueba de bucle no puede alcanzarse con un valor imayor que diez sin que el código haya accedido primero al elemento de matriz inexistentea[10] .

Dado que un intento de acceder a ese elemento sería un comportamiento indefinido, el compilador no tendría obligaciones con respecto a lo que el programa podría hacer después de ese punto. Más específicamente, dado que el compilador no tendría la obligación de generar código para verificar el índice de bucle en cualquier caso en que pudiera ser mayor que diez, no tendría la obligación de generar código para verificarlo en absoluto; En su lugar, podría suponer que la <=10prueba siempre dará verdadero. Tenga en cuenta que esto sería cierto incluso si el código lo leería en a[10]lugar de escribirlo.

Super gato
fuente
3

Cuando itera más allá i==9, asigna cero a los 'elementos de la matriz' que realmente se encuentran más allá de la matriz , por lo que está sobrescribiendo algunos otros datos. Lo más probable es que sobrescriba la ivariable, que se encuentra después a[]. De esa manera, simplemente restablece la ivariable a cero y, por lo tanto, reinicia el ciclo.

Puede descubrirlo usted mismo si imprime ien el bucle:

      printf("test i=%d\n", i);

en lugar de solo

      printf("test \n");

Por supuesto, ese resultado depende en gran medida de la asignación de memoria para sus variables, que a su vez depende de un compilador y su configuración, por lo que generalmente es un comportamiento indefinido ; es por eso que los resultados en diferentes máquinas o diferentes sistemas operativos o en diferentes compiladores pueden diferir.

CiaPan
fuente
0

el error está en la matriz de porciones [10] w / c también es la dirección de i (int array [10], i;). cuando la matriz [10] se establece en 0, entonces i sería 0 w / c restablece todo el ciclo y provoca el ciclo infinito. habrá un ciclo infinito si la matriz [10] está entre 0-10. el ciclo correcto debe ser para (i = 0; i <10; i ++) {...} int matriz [10], i; para (i = 0; i <= 10; i ++) matriz [i] = 0;

Jonelle H. Castaneda
fuente
0

Sugeriré algo que no encontraré arriba:

Intente asignar la matriz [i] = 20;

Supongo que esto debería terminar el código en todas partes ... (dado que mantienes i <= 10 o ll)

Si esto funciona, puede decidir firmemente que las respuestas especificadas aquí ya son correctas [la respuesta relacionada con la memoria pisando fuerte por ejemplo].

Lloviendo fuego
fuente
-9

Hay dos cosas mal aquí. El int i es en realidad un elemento de matriz, matriz [10], como se ve en la pila. Debido a que ha permitido que la indexación haga realmente array [10] = 0, el índice de bucle, i, nunca excederá de 10. Hazlo for(i=0; i<10; i+=1).

i ++ es, como K&R lo llamaría, 'mal estilo'. Está incrementando i por el tamaño de i, no 1. i ++ es para matemáticas de puntero e i + = 1 es para álgebra. Si bien esto depende del compilador, no es una buena convención para la portabilidad.

SkipBerne
fuente
55
-1 completamente equivocado. La variable iNO es un elemento de la matriz a[10], no hay obligación ni sugerencia para que un compilador la coloque en la pila inmediatamente después a[] ; también puede ubicarse antes de la matriz o separarse con algo de espacio adicional. Incluso podría asignarse fuera de la memoria principal, por ejemplo en un registro de CPU. Tampoco es cierto que ++sea ​​para punteros y no para enteros. Completamente incorrecto es 'i ++ está incrementando i por el tamaño de i' - ¡lea la descripción del operador en la definición del lenguaje!
CiaPan
Por eso funciona en algunas plataformas y no en otras. Es la única explicación lógica de por qué se repite para siempre en Windows. con respecto a I ++ es matemática de puntero no entero. lea las Escrituras ... el 'lenguaje de programación C'. por Kernigan y Ritche, si quieres tengo una copia autografiada, y he estado programando en c desde 1981.
SkipBerne
1
Lea el código fuente de OP y encuentre la declaración de variable i: es de inttipo. Es un número entero , no un puntero; un número entero, usado como índice para el array,.
CiaPan
1
Lo hice y es por eso que comenté como lo hice. tal vez deberías darte cuenta de que a menos que el compilador incluya comprobaciones de pila y en este caso no importaría como referencia de pila cuando I = 10 realmente estaría haciendo referencia, en algunas compilaciones, al índice de matriz y eso está dentro de los límites de la región de pila. Los compiladores no pueden arreglar estúpidos. las compilaciones pueden hacer una reparación como parece, pero una interpretación pura del lenguaje de programación c no admitiría esta convención y, como dijo el OP, daría como resultado resultados no portátiles.
SkipBerne
@SkipBerne: considere eliminar su respuesta antes de que se le "otorgue" más puntos negativos.
Peter VARGA