2009-07-09 17 views
22

Tome un vistazo a estas dos funciones:Orden de asignación de variables locales en la pila

void function1() { 
    int x; 
    int y; 
    int z; 
    int *ret; 
} 

void function2() { 
    char buffer1[4]; 
    char buffer2[4]; 
    char buffer3[4]; 
    int *ret; 
} 

Si rompo en function1() en gdb, e imprimir las direcciones de las variables, me sale esto:

(gdb) p &x 
$1 = (int *) 0xbffff380 
(gdb) p &y 
$2 = (int *) 0xbffff384 
(gdb) p &z 
$3 = (int *) 0xbffff388 
(gdb) p &ret 
$4 = (int **) 0xbffff38c 

Si hago lo mismo en function2(), me sale esto:

(gdb) p &buffer1 
$1 = (char (*)[4]) 0xbffff388 
(gdb) p &buffer2 
$2 = (char (*)[4]) 0xbffff384 
(gdb) p &buffer3 
$3 = (char (*)[4]) 0xbffff380 
(gdb) p &ret 
$4 = (int **) 0xbffff38c 

Notará que en ambas funciones, ret se almacena más cerca de la parte superior de la pila. En function1(), es seguido por z, y, y finalmente x. En function2(), ret va seguido de buffer1, luego buffer2 y buffer3. ¿Por qué se cambia la orden de almacenamiento? Estamos utilizando la misma cantidad de memoria en ambos casos (4 bytes int s frente a 4 bytes char matrices), por lo que no puede ser un problema de relleno. ¿Qué razones podría haber para este reordenamiento, y además, es posible mirando el código C para determinar de antemano cómo se ordenarán las variables locales?

Ahora sé que la especificación ANSI para C no dice nada sobre el orden en que se almacenan las variables locales y que el compilador puede elegir su propio orden, pero me imagino que el compilador tiene reglas sobre cómo se ocupa de esto, y explicaciones de por qué esas reglas fueron hechas para ser como son.

Como referencia que estoy usando GCC 4.0.1 en Mac OS 10.5.7

+0

¿es importante? ¿necesita las variables que se asignarán en una dirección específica? – stefanB

+8

No, no es importante, es solo un ejercicio académico. – David

+0

¿El nivel de optimización afecta la respuesta? Conjetura pura, pero tal vez sin optimización/baja, las entradas son candidatas para la asignación de registros, pero char [4] no lo es, y dado que se procesan de manera diferente, los dos mecanismos simplemente las ponen en la pila en diferentes órdenes. Incluso si la optimización no hace ninguna diferencia, es plausible que se maneje otra cosa en el modo automático, lo que significa que las entradas siempre bajan por una ruta y las matrices siempre por otra. –

Respuesta

5

Por lo general, tiene que ver con problemas de alineación.

La mayoría de los procesadores son más lentos en la búsqueda de datos que no están alineados con la palabra del procesador. Deben agarrarlo en pedazos y empalmarlo.

Probablemente lo que está sucediendo es que está juntando todos los objetos que son mayores o iguales a la alineación óptima del procesador, y luego ajustando con más fuerza las cosas que pueden no estar alineadas. Da la casualidad de que en su ejemplo todas sus matrices char tienen 4 bytes, pero apuesto a que si las convierte en 3 bytes, igual terminarán en los mismos lugares.

Pero si tenía cuatro matrices de un byte, pueden terminar en un rango de 4 bytes, o alinearse en cuatro separadas.

Se trata de lo más fácil (se traduce en "más rápido") para que el procesador lo agarre.

+1

Bueno, aquí GCC alinea la pila en 16 bytes de forma predeterminada. Además, incluso si estuviéramos lidiando con una alineación de 4 bytes, las matrices y los enteros son del mismo tamaño (4 bytes por pieza), así que no sé por qué obtendrías un nuevo pedido. – David

0

Supongo que esto tiene algo que ver con la forma en que se cargan los datos en los registros. Quizás, con las matrices de caracteres, el compilador funciona algo de magia para hacer cosas en paralelo y esto tiene algo que ver con la posición en la memoria para cargar fácilmente los datos en los registros. Intente compilar con diferentes niveles de optimización e intente usar int buffer1[1].

-1

¿También podría ser un problema de seguridad?

int main() 
{ 
    int array[10]; 
    int i; 
    for (i = 0; i <= 10; ++i) 
    { 
     array[i] = 0; 
    } 
} 

Si la matriz es inferior en la pila de i, este código se repetirá infinitamente (porque erróneamente accesos y ceros array [10], que es i).Al colocar la matriz más arriba en la pila, los intentos de acceder a la memoria más allá del final de la pila tendrán más probabilidades de tocar la memoria no asignada y colgarse, en lugar de causar un comportamiento indefinido.

Experimenté con este mismo código una vez con gcc, y no pude hacerlo fallar excepto con una combinación particular de banderas que no recuerdo ahora ... En cualquier caso, colocó la matriz varios bytes de distancia de i .

+0

No es probable. Hay páginas de guardia para el desbordamiento y el desbordamiento de la pila, pero nada entre los marcos de la pila. – lavinio

+1

El problema de seguridad aquí es código incorrecto. Sí, resulta en un bucle infinito con un combo de compilador/indicador en particular. Pero para mí, eso es un consuelo frío. –

0

Curiosamente, si agrega un int * ret2 extra en function1, entonces en mi sistema el orden es correcto, mientras que está fuera de servicio solo para 3 variables locales. Supongo que está ordenado de esa manera debido a que refleja la estrategia de asignación de registros que se usará. O eso o es arbitrario.

14

No tengo idea why GCC organizes its stack the way it does (aunque supongo que podría descifrar la fuente o this paper y averiguarlo), pero puedo decirle cómo garantizar el orden de las variables de pila específicas si por alguna razón lo necesita. Simplemente ponerlos en una estructura:

void function1() { 
    struct { 
     int x; 
     int y; 
     int z; 
     int *ret; 
    } locals; 
} 

Si mi memoria no me falla, garantiza que las especificaciones &ret > &z > &y > &x. Dejé mi K & R en el trabajo, así que no puedo citar el capítulo y el versículo.

6

La norma ISO C no dice nada sobre el orden de las variables locales en la pila, ni siquiera garantiza que exista una pila. El estándar solo habla sobre el alcance y la duración de las variables dentro de un bloque.

0

Todo depende del compilador. Más allá de esto, ciertas variables de procedimiento podrían nunca colocarse en la pila, ya que pueden pasar toda su vida dentro de un registro.

7

Así que hice más experimentos y esto es lo que encontré. Parece estar basado en si cada variable es o no una matriz. Dada esta entrada:

void f5() { 
     int w; 
     int x[1]; 
     int *ret; 
     int y; 
     int z[1]; 
} 

termino con esto en GDB:

(gdb) p &w 
$1 = (int *) 0xbffff4c4 
(gdb) p &x 
$2 = (int (*)[1]) 0xbffff4c0 
(gdb) p &ret 
$3 = (int **) 0xbffff4c8 
(gdb) p &y 
$4 = (int *) 0xbffff4cc 
(gdb) p &z 
$5 = (int (*)[1]) 0xbffff4bc 

En este caso, int s y punteros se tratan en primer lugar, declaró el último en la parte superior de la pila y la primera declarada más cerca de la parte inferior. Luego, las matrices se manejan, en la dirección opuesta, cuanto antes la declaración, más arriba en la pila. Estoy seguro de que hay una buena razón para esto. Me pregunto qué es.

1

El estándar C no dicta ningún diseño para las otras variables automáticas. Sin embargo, específicamente dice, para evitar dudas, que

[...] El diseño del almacenamiento para los parámetros [función] no está especificado. (C11 6.9.1p9)

Se puede entender de ello que él disposición de almacenamiento para cualquier otro objeto es igualmente no especificada, a excepción de la de los pocos requisitos de la dada por el estándar, incluyendo que el puntero nulo no puede apuntar a cualquier objeto válido o función, y diseños dentro de objetos agregados.

El estándar C no contiene un solo mención a la palabra "pila"; es muy posible hacer, por ejemplo, una implementación de C sin apilamiento, asignando cada registro de activación del montón (aunque quizás se podría entender entonces que forman una pila).

Una de las razones para dar al compilador cierto margen de maniobra es la eficiencia. Sin embargo, los compiladores actuales también usarían esto para seguridad, utilizando trucos como aleatorización de diseño de espacio de direcciones y stack canaries para tratar de hacer que la explotación de comportamiento indefinido sea más difícil. El reordenamiento de los buffers se hace para que el uso de canary sea más efectivo.

Cuestiones relacionadas