ejercicios - truco de diseño de memoria
correspondencia directa memoria cache (3)
He estado siguiendo este curso en youtube y estaba hablando de cómo algunos programadores pueden usar allí el conocimiento de cómo se establece la memoria para hacer cosas inteligentes. Uno de los ejemplos en la conferencia fue algo así
#include <stdio.h>
void makeArray();
void printArray();
int main(){
makeArray();
printArray();
return 0;
}
void makeArray(){
int array[10];
int i;
for(i=0;i<10;i++)
array[i]=i;
}
void printArray(){
int array[10];
int i;
for(i=0;i<10;i++)
printf("%d/n",array[i]);
}
la idea es que mientras las dos funciones tengan el mismo tamaño de registro de activación en el segmento de la pila, funcionará e imprimirá números del 0 al 9 ... pero en realidad imprime algo así
134520820
-1079626712
0
1
2
3
4
5
6
7
siempre hay esos dos valores en la mendicidad ... ¿alguien puede explicar eso? iam usando gcc en linux
la URL de la conferencia exacta a partir de las 5:15
Nunca, nunca, jamás, jamás, jamás, hagan algo como esto. No funcionará de manera confiable. Obtendrás errores extraños. Está lejos de ser portátil.
Formas en que puede fallar:
.1. El compilador agrega código extra y oculto
DevStudio, en modo de depuración, agrega llamadas a funciones que controlan la pila para detectar errores en la pila. Estas llamadas sobrescribirán lo que estaba en la pila, perdiendo así sus datos.
.2. Alguien agrega una llamada Entrar / Salir
Algunos compiladores permiten que el programador defina funciones para llamar a la entrada de funciones y a la salida de funciones. Al igual que (1) estos usan espacio de pila y sobrescribirán lo que ya está allí, perdiendo datos.
.3. Interrupciones
En main (), si obtiene una interrupción entre las llamadas a makeArray y printArray, perderá sus datos. Lo primero que ocurre al procesar una interrupción es guardar el estado de la CPU. Esto generalmente implica presionar los registros de la CPU y las banderas en la pila, y sí, lo adivinaste, sobrescribes tus datos.
.4. Los compiladores son inteligentes
Como ha visto, la matriz en makeArray está en una dirección diferente a la de printArray. El compilador ha colocado sus variables locales en diferentes posiciones en la pila. Utiliza un algoritmo complejo para decidir dónde colocar la variable: en la pila, en un registro, etc., y realmente no vale la pena tratar de descubrir cómo lo hace el compilador, ya que la próxima versión del compilador podría hacerlo de otra manera.
En resumen, este tipo de "trucos inteligentes" no son trucos y ciertamente no son inteligentes. No perderá nada al declarar la matriz en main y pasando una referencia / puntero a ella en las dos funciones. Las pilas son para almacenar variables locales y direcciones de retorno de funciones. Una vez que los datos salen del alcance (es decir, la parte superior de la pila se reduce al pasar los datos), entonces los datos se pierden efectivamente: cualquier cosa puede sucederle.
Para ilustrar este punto más, sus resultados probablemente serían diferentes si tuviera diferentes nombres de funciones (estoy adivinando aquí, OK).
Probablemente, GCC genera código que no envía los argumentos a la pila cuando llama a una función, sino que asigna espacio extra en la pila. Los argumentos para su llamada a la función ''printf'', "% d / n" y array [i] toman 8 bytes en la pila, el primer argumento es un puntero y el segundo es un número entero. Esto explica por qué hay dos enteros que no se imprimen correctamente.
Lo siento, pero no hay absolutamente nada inteligente sobre ese fragmento de código y las personas que lo usan son muy tontas.
Apéndice:
O, a veces, solo algunas veces, muy inteligente. Después de haber visto el video vinculado en la actualización de la pregunta, este no era un código malicioso que rompía las reglas. Este chico entendió lo que estaba haciendo bastante bien.
Requiere una comprensión profunda del código subyacente generado y puede romperse fácilmente (como se menciona y se ve aquí) si su entorno cambia (como compiladores, arquitecturas, etc.).
Pero, si tiene ese conocimiento, probablemente pueda salirse con la suya. No es algo que sugiriera a nadie más que a un veterano, pero puedo ver que tiene su lugar en situaciones muy limitadas y, para ser sincero, sin duda, en ocasiones he sido algo más ... pragmático ... de lo que debería haberlo hecho. estado en mi propia carrera :-)
Ahora volvamos a tu programación habitual ...
No es portátil entre arquitecturas, compiladores, versiones de compiladores y, probablemente, incluso niveles de optimización dentro del mismo release de un compilador, además de ser un comportamiento indefinido (lectura de variables no inicializadas).
La mejor opción para comprender es examinar el código de ensamblador generado por el compilador.
Pero su mejor apuesta en general es olvidarse de eso y codificar según el estándar.
Por ejemplo, esta transcripción muestra cómo gcc puede tener un comportamiento diferente en diferentes niveles de optimización:
pax> gcc -o qq qq.c ; ./qq
0
1
2
3
4
5
6
7
8
9
pax> gcc -O3 -o qq qq.c ; ./qq
1628373048
1629343944
1629097166
2280872
2281480
0
0
0
1629542238
1629542245
En el alto nivel de optimización de gcc (lo que me gusta llamar su nivel de optimización insana), esta es la función makeArray
. Básicamente se descubrió que la matriz no se usa y, por lo tanto, optimizó su inicialización fuera de existencia.
_makeArray:
pushl %ebp ; stack frame setup
movl %esp, %ebp
; heavily optimised function
popl %ebp ; stack frame tear-down
ret ; and return
De hecho, estoy un poco sorprendido de que gcc incluso haya dejado allí el trozo de función.
Actualización: como señala Nicholas Knight en un comentario, la función permanece, ya que debe estar visible para el enlazador, lo que hace que la función sea estática da como resultado que gcc elimine el stub también.
Si comprueba el código de ensamblador en el nivel de optimización 0 a continuación, da una pista (no es el motivo real, consulte más abajo). Examine el siguiente código y verá que la configuración del marco de pila es diferente para las dos funciones a pesar de que tienen exactamente los mismos parámetros pasados y las mismas variables locales:
subl $48, %esp ; in makeArray
subl $56, %esp ; in printArray
Esto se debe a que printArray asigna un espacio extra para almacenar la dirección de la printf
formato printf
y la dirección del elemento de la matriz, cuatro bytes cada uno, que representa la diferencia de ocho bytes (dos valores de 32 bits).
Esa es la explicación más probable de que su matriz en printArray()
esté desactivada por dos valores.
Aquí están las dos funciones en el nivel de optimización 0 para su disfrute :-)
_makeArray:
pushl %ebp ; stack fram setup
movl %esp, %ebp
subl $48, %esp
movl $0, -4(%ebp) ; i = 0
jmp L4 ; start loop
L5:
movl -4(%ebp), %edx
movl -4(%ebp), %eax
movl %eax, -44(%ebp,%edx,4) ; array[i] = i
addl $1, -4(%ebp) ; i++
L4:
cmpl $9, -4(%ebp) ; for all i up to and including 9
jle L5 ; continue loop
leave
ret
.section .rdata,"dr"
LC0:
.ascii "%d/12/0" ; format string for printf
.text
_printArray:
pushl %ebp ; stack frame setup
movl %esp, %ebp
subl $56, %esp
movl $0, -4(%ebp) ; i = 0
jmp L8 ; start loop
L9:
movl -4(%ebp), %eax ; get i
movl -44(%ebp,%eax,4), %eax ; get array[i]
movl %eax, 4(%esp) ; store array[i] for printf
movl $LC0, (%esp) ; store format string
call _printf ; make the call
addl $1, -4(%ebp) ; i++
L8:
cmpl $9, -4(%ebp) ; for all i up to and including 9
jle L9 ; continue loop
leave
ret
Actualización: como señala Roddy en un comentario. esa no es la causa de su problema específico ya que, en este caso, la matriz está realmente en la misma posición en la memoria ( %ebp-44
con %ebp
siendo el mismo en las dos llamadas). Lo que estaba tratando de señalar es que dos funciones con la misma lista de argumentos y los mismos parámetros locales no necesariamente terminan con el mismo diseño de marco de pila.
Todo lo que tomaría sería que printArray
intercambiara la ubicación de sus variables locales (incluidos los temporales no creados explícitamente por el desarrollador) y usted tendría este problema.