¿Está accediendo a una matriz global fuera de su comportamiento indefinido?

Acabo de tener un examen en mi clase hoy, leyendo el código C y la entrada, y la respuesta requerida fue lo que aparecerá en la pantalla si el progtwig realmente se ejecuta. Una de las preguntas declaró a[4][4] como una variable global y en un punto de ese progtwig, intenta acceder a[27][27] , así que respondí algo así como ” Acceder a una matriz fuera de sus límites es una comportamiento indefinido “pero el maestro dijo que a[27][27] tendrá un valor de 0 .

Posteriormente, probé un código para verificar si “todas las variables golbal no inicializadas están configuradas en 0 ” son verdaderas o no. Bueno, parece ser cierto.

Entonces ahora mi pregunta:

  • Parece que se borró algo de memoria adicional y se reservó para que se ejecute el código. ¿Cuánta memoria está reservada? ¿Por qué un comstackdor reserva más memoria de la que debería, y para qué sirve?
  • ¿A a[27][27] será 0 para todo el entorno?

Editar:

En ese código, a[4][4] es la única variable global declarada y hay algunas más locales en main() .

Intenté ese código nuevamente en DevC ++. Todos ellos son 0 . Pero eso no es cierto en VSE, en el que la mayoría de los valores son 0 pero algunos tienen un valor aleatorio, como ha señalado Vyktor.

Tenías razón: es un comportamiento indefinido y no puedes contar siempre produciendo 0 .

En cuanto a por qué está viendo cero en este caso: los sistemas operativos modernos asignan memoria a los procesos en fragmentos relativamente gruesos llamados páginas que son mucho más grandes que las variables individuales (al menos 4 KB en x86). Cuando tiene una sola variable global, se ubicará en algún lugar de una página. Suponiendo que a es de tipo int[][] e int s son cuatro bytes en su sistema, a[27][27] se ubicará a unos 500 bytes desde el comienzo de a . Por lo tanto, siempre que a esté cerca del comienzo de la página, el acceso a[27][27] estará respaldado por la memoria real y su lectura no causará una falla de página / violación de acceso.

Por supuesto, no puedes contar con esto. Si, por ejemplo, a es precedido por casi 4 KB de otras variables globales, la memoria no respaldará a[27][27] y su proceso se bloqueará cuando intente leerlo.

Incluso si el proceso no falla, no puede contar con obtener el valor 0 . Si tiene un progtwig muy simple en un sistema operativo multiusuario moderno que no hace más que asignar esta variable e imprimir ese valor, probablemente verá 0 . Los sistemas operativos configuran los contenidos de la memoria en un valor benigno (normalmente todos ceros) cuando se transfiere la memoria a un proceso para que los datos confidenciales de un proceso o usuario no puedan filtrarse a otro.

Sin embargo, no existe una garantía general de que la memoria arbitraria que lea sea cero. Podrías ejecutar tu progtwig en una plataforma donde la memoria no se inicializa en la asignación, y verías cualquier valor que haya estado allí desde su último uso.

Además, si a le siguen suficientes otras variables globales que se inicializan en valores distintos de cero, acceder a[27][27] le mostrará cualquier valor que esté allí.

El acceso a una matriz fuera de límites es un comportamiento indefinido, lo que significa que los resultados son impredecibles, por lo que el resultado de que a[27][27] sea 0 no es confiable en absoluto.

clang te dice esto muy claramente si usamos -fsanitize=undefined :

 runtime error: index 27 out of bounds for type 'int [4][4]' 

Una vez que tiene un comportamiento indefinido, el comstackdor realmente puede hacer cualquier cosa, incluso hemos visto ejemplos donde gcc ha convertido un bucle finito en un bucle infinito basado en optimizaciones en torno a un comportamiento indefinido. Tanto clang como gcc en algunas circunstancias pueden generar un código de operación de instrucción indefinido si detecta un comportamiento indefinido.

¿Por qué es un comportamiento indefinido? ¿Por qué el comportamiento aritmético indefinido del puntero fuera de los límites? proporciona un buen resumen de las razones. Por ejemplo, el puntero resultante puede no ser una dirección válida, el puntero ahora podría apuntar fuera de las páginas de memoria asignadas, podría estar trabajando con hardware mapeado en memoria en lugar de RAM, etc.

Lo más probable es que el segmento donde se almacenan las variables estáticas sea mucho más grande que el conjunto que está asignando o el segmento que está pisando, pero resulta que tiene suerte en este caso, pero de nuevo un comportamiento completamente no confiable. Lo más probable es que el tamaño de su página sea 4k y el acceso de a[27][27] esté dentro de ese límite, lo que probablemente sea el motivo por el que no está viendo un error de segmentación.

Lo que dice el estándar

El borrador del estándar C99 nos dice que este es un comportamiento indefinido en la sección 6.5.6 Operadores aditivos que cubre la aritmética del apuntador, que es a lo que se reduce el acceso a un arreglo. Dice:

Cuando una expresión que tiene un tipo de entero se agrega o se resta de un puntero, el resultado tiene el tipo del operando del puntero. Si el operando puntero apunta a un elemento de un objeto de matriz, y la matriz es lo suficientemente grande, el resultado apunta a un desplazamiento de elemento desde el elemento original tal que la diferencia de los subíndices de los elementos de matriz resultante y original es igual a la expresión entera.

[…]

Si tanto el operando del puntero como el resultado apuntan a elementos del mismo objeto del arreglo, o uno más allá del último elemento del objeto del arreglo, la evaluación no producirá un desbordamiento; de lo contrario, el comportamiento no está definido. Si el resultado señala uno pasado el último elemento del objeto de la matriz, no se utilizará como el operando de un operador unario * que se evalúa.

y la definición de estándares de comportamiento indefinido nos dice que el estándar no impone requisitos sobre el comportamiento y señala que el comportamiento posible es impredecible:

comportamiento, al usar una construcción de progtwig errónea o no portable o datos erróneos, para los cuales esta Norma Internacional no impone requisitos

NOTA El comportamiento indefinido posible va desde ignorar completamente la situación con resultados impredecibles, […]

Aquí está la cita del estándar, que especifica qué es un comportamiento indefinido.

J.2 Comportamiento indefinido

  • Un subíndice de matriz está fuera de rango, incluso si un objeto es aparentemente accesible con el subíndice dado (como en la expresión lvalue a [1] [7] dada la statement int a [4] [5]) (6.5.6).

  • La adición o sustracción de un puntero hacia, o más allá de, un objeto de matriz y un tipo entero produce un resultado que apunta justo más allá del objeto de matriz y se utiliza como el operando de un operador unario * que se evalúa (6.5.6).

En su caso, el subíndice de la matriz está completamente fuera de la matriz. Dependiendo de que el valor será cero, no es confiable.

Además, el comportamiento de todo el progtwig está en cuestión.

Si solo ejecuta su código desde visual studio 2012 y obtiene un resultado como este (diferente en cada ejecución):

 Address of a: 00FB8130 Address of a[4][4]: 00FB8180 Address of a[27][27]: 00FB834C Value of a[27][27]: 0 Address of a[1000][1000]: 00FBCF50 Value of a[1000][1000]: < << Unhandled exception at 0x00FB3D8F in GlobalArray.exe: 0xC0000005: Access violation reading location 0x00FBCF50. 

Cuando mira la ventana Módulos , ve que el rango de memoria de su módulo de aplicación es 00FA0000-00FBC000 . Y a menos que tenga activadas las comprobaciones CRT, nada controlará qué hace dentro de la memoria (siempre que no viole la protección de la memoria ).

Entonces tienes 0 en a[27][27] por casualidad. Cuando abra la vista de memoria desde la posición 00FB8130 ( a ), probablemente verá algo como esto:

 0x00FB8130 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x00FB8140 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x00FB8150 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x00FB8160 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x00FB8170 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x00FB8180 01 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 ................ 0x00FB8190 c0 90 45 00 b0 e9 45 00 00 00 00 00 00 00 00 00 À.E.°éE......... 0x00FB81A0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x00FB81B0 00 00 00 00 80 5c af 0f 00 00 00 00 00 00 00 00 ....€\¯......... 0x00FB81C0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ .......... 0x00FB8330 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x00FB8340 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ < <<< 0x00FB8350 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ .......... ^^ ^^ ^^ ^^ 

Es posible que con su comstackdor siempre obtenga 0 para ese código debido a la forma en que utiliza la memoria, pero a pocos bytes de distancia puede encontrar otra variable.

Por ejemplo, con la memoria mostrada arriba, a[6][0] apunta a la dirección 0x00FB8190 que contiene un valor entero de 4559040 .

Luego haz que tu maestro explique este.

No sé si esto funcionará en su sistema, pero jugando con blatting memory DESPUÉS de que la matriz a con bytes distintos de cero da un resultado diferente para a[27][27] .

En mi sistema, cuando 0xFFFFFFFF contenido de a[27][27] era 0xFFFFFFFF . es decir, -1 convertido a unsigned es todos los bits establecidos en complemento a dos.

 #include  #include  #define printer(expr) { printf(#expr" = %u\n", expr); } unsigned int d[8096]; int a[4][4]; /* assuming an int is 4 bytes, next 4 x 4 x 4 bytes will be initialised to zero */ unsigned int b[8096]; unsigned int c[8096]; int main() { /* make sure next bytes do not contain zero'd bytes */ memset(b, -1, 8096*4); memset(c, -1, 8096*4); memset(d, -1, 8096*4); /* lets check normal access */ printer(a[0][0]); printer(a[3][3]); /* Now we disrepect the machine - undefined behaviour shall result */ printer(a[27][27]); return 0; } 

Este es mi resultado:

 a[0][0] = 0 a[3][3] = 0 a[27][27] = 4294967295 

Vi en comentarios sobre la visualización de memoria en Visual Studio. La manera más fácil es agregar un punto de interrupción en algún punto de tu código (para detener la ejecución) luego entrar en Debug … windows … Menú de memoria, seleccionar, por ejemplo, Memoria 1. A continuación, encontrarás la dirección de memoria de tu matriz a . En mi caso, la dirección era 0x0130EFC0 . por lo tanto, ingrese 0x0130EFC0 en el demonio de dirección y presione Entrar. Esto muestra la memoria en esa ubicación.

Por ejemplo, en mi caso.

 0x0130EFC0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 .................................. 0x0130EFE2 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ff ff ff ff ..............................ÿÿÿÿ 0x0130F004 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ 0x0130F026 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ 0x0130F048 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ 

Los ceros son del curso de la matriz a, que tiene un tamaño de byte de 4 x 4 x sizeof int (4 en mi caso) = 64 bytes. Los bytes de la dirección 0x0130EFC0 son 0xFF cada uno (a partir de los contenidos de b, c o d).

Tenga en cuenta que:

 0x130EFC0 + 64 = 0x130EFC0 + 0x40 = 130F000 

que es el comienzo de todos esos bytes que ves. Probablemente array b .

Para los comstackdores comunes, acceder a una matriz más allá de sus límites puede dar resultados predecibles solo en casos muy especiales, y no debe confiar en eso. Ejemplo:

 int a[4][4]; int b[4][4]; 

Siempre que no exista un problema de alineación, y no solicite una optimización agresiva ni controles de desinfección, a[6][1] debería ser en realidad b[2][1] . Pero por favor, nunca hagas eso en el código de producción.

En un sistema en particular , su maestro puede estar en lo cierto, puede ser el comportamiento de su comstackdor y sistema operativo en particular.

En un sistema genérico (es decir, sin conocimiento “interno”), entonces su respuesta es correcta: esto es UB.

En primer lugar, el lenguaje C no tiene control de límites. En efecto, no tiene ningún control en casi todo. Esta es la alegría y la condena de C.

Volviendo al tema, si desborda la memoria no significa que active una segfault. Veamos más de cerca cómo funciona.

Cuando inicia un progtwig o ingresa una subrutina, el procesador guarda en la stack la dirección a la cual regresa cuando termina la función.

La stack se ha inicializado desde el sistema operativo durante la asignación de memoria de proceso, y tiene un rango de memoria legal donde puede leer o escribir a su gusto, no solo almacenar direcciones de devolución.

La práctica común utilizada por los comstackdores para crear variables locales (automáticas) es reservar espacio en la stack y usar ese espacio para las variables. Observe la conocida secuencia ensambladora de 32 bits, llamada prólogo, que encontrará en cualquier función enter:

 push ebp ;save register on the stack mov ebp,esp ;get actual stack address sub esp,4 ;displace the stack of 4 bytes that will be used to store a 4 chars array 

teniendo en cuenta que la stack crece en la dirección inversa de los datos, el diseño de la memoria es:

 0x0.....1C [Parameters (if any)] ;former function 0x0.....18 [Return Address] 0x0.....14 EBP 0x0.....10 0x0......x ;Local DWORD parameter 0x0.....0C [Parameters (if any)] ;our function 0x0.....08 [Return Address] 0x0.....04 EBP 0x0.....00 0, 'c', 'b', 'a' ;our string of 3 chars plus final nul 

Esto se conoce como marco de stack.

Ahora considere la cadena de cuatro bytes comenzando en 0x0 …. 0 y terminando en 0x …. 3. Si escribimos más de 3 caracteres en la matriz, reemplazaremos secuencialmente: la copia guardada de EBP, la dirección de retorno, los parámetros, las variables locales de la función anterior, luego su EBP, la dirección de retorno, etc.

El efecto más escenográfico que obtenemos es que, al regresar la función, la CPU intenta regresar a una dirección incorrecta generando una segfault . Se puede lograr el mismo comportamiento si una de las variables locales son punteros, en este caso intentaremos leer, o escribir, en ubicaciones incorrectas, lo que desencadenará de nuevo la segfault.

Cuando falla segfault : cuando la variable hinchada no está en la stack, o tiene tantas variables locales que las sobrescribe sin tocar la dirección de retorno (y no son punteros). Otro caso es que el procesador reserva un espacio de guardia entre las variables locales y la dirección de retorno, en este caso el desbordamiento del búfer no llega a la dirección. Otra posibilidad es acceder aleatoriamente a elementos de matriz, en este caso una matriz de gran tamaño puede exceder el espacio de stack y desbordamiento de otros datos, pero afortunadamente no tocamos los elementos que están mapeados donde se guarda la dirección de devolución (todo puede suceder …) .

¿Cuándo podemos tener segfault variables de hinchamiento que no están en la stack? Cuando se desborda la matriz vinculada o punteros.

Espero que estos sean información útil …