Ordre d’allocation de variables locales sur la stack

Jetez un coup d’œil à ces deux fonctions:

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

Si je casse at function1() dans gdb et affiche les adresses des variables, je reçois ceci:

 (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 je fais la même chose avec function2() , j’obtiens ceci:

 (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 

Vous remarquerez que dans les deux fonctions, ret est stocké plus près du haut de la stack. Dans function1() , il est suivi de z , y et enfin x . Dans function2() , ret est suivi de buffer1 , puis de buffer2 et de buffer3 . Pourquoi l’ordre de stockage a-t-il changé? Nous utilisons la même quantité de mémoire dans les deux cas (tableaux de caractères sur 4 octets), donc il ne peut s’agir d’un problème de remplissage. Quelles raisons pourrait-il y avoir pour cette réorganisation, et de plus, est-il possible, en consultant le code C, de déterminer à l’avance comment les variables locales seront ordonnées?

Maintenant, je suis conscient que la spécification ANSI pour C ne dit rien sur l’ordre dans lequel les variables locales sont stockées et que le compilateur est autorisé à choisir son propre ordre, mais j’imagine que le compilateur a des règles sur la manière dont il prend en charge ceci, et des explications sur la raison pour laquelle ces règles ont été conçues pour être telles qu’elles sont.

Pour référence, j’utilise GCC 4.0.1 sur Mac OS 10.5.7

Je ne sais pas pourquoi GCC organise sa stack de cette manière (bien que je suppose que vous puissiez ouvrir son source ou ce document pour le découvrir), mais je peux vous dire comment garantir l’ordre de variables de stack spécifiques si, pour une raison quelconque tu dois. Il suffit de les mettre dans une structure:

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

Si ma mémoire est fidèle, les spécifications garantissent que &ret > &z > &y > &x . J’ai laissé mes K & R au travail, je ne peux donc pas citer de chapitre ni de verset.

Alors, j’ai fait d’autres expériences et voici ce que j’ai trouvé. Cela semble être basé sur le fait que chaque variable est ou non un tableau. Compte tenu de cette entrée:

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

Je me retrouve avec ceci dans 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 

Dans ce cas, les int et les pointeurs sont traités en premier, déclarés pour la dernière fois en haut de la stack et d’abord déclarés plus près du bas. Ensuite, les tableaux sont traités, dans le sens opposé, plus la déclaration est précoce, plus haut sur la stack. Je suis sûr qu’il y a une bonne raison à cela. Je me demande ce que c’est.

Non seulement ISO C ne dit rien sur l’ordre des variables locales sur la stack, il ne garantit même pas qu’une stack existe même. La norme ne fait que parler de la scope et de la durée de vie des variables dans un bloc.

Habituellement, cela concerne les problèmes d’alignement.

La plupart des processeurs lisent plus lentement des données non alignées. Ils doivent le prendre en morceaux et le coller ensemble.

Ce qui se passe probablement, c’est de rassembler tous les objects dont la taille est supérieure ou égale à l’alignement optimal du processeur, puis d’emballer plus étroitement les éléments qui ne sont peut-être pas alignés. Il se trouve que, dans votre exemple, tous vos tableaux de caractères ont 4 octets, mais je parie que si vous leur atsortingbuez 3 octets, ils se retrouveront toujours aux mêmes endroits.

Toutefois, si vous avez quatre tableaux d’un octet, ils peuvent se retrouver dans une plage de 4 octets ou alignés dans quatre plages distinctes.

Il s’agit de ce qui est le plus facile (se traduit par “le plus rapide”) par le processeur.

La norme C ne dicte aucune disposition pour les autres variables automatiques. Toutefois, il est dit spécifiquement, pour éviter tout doute, que

[…] La disposition de la mémoire pour les parameters [fonction] n’est pas spécifiée. (C11 6.9.1p9)

On peut en déduire que la disposition du stockage pour tout autre object est également non spécifiée, à l’exception des quelques exigences spécifiées par la norme, notamment que le pointeur null ne peut pas pointer sur un object ou une fonction valide, et les dispositions au sein de l’agrégat. objects.

La norme C ne contient pas une seule mention pour le mot “stack”; il est tout à fait possible de faire, par exemple, une implémentation C sans stack, allouant chaque enregistrement d’activation à partir du tas (bien que ceux-ci puissent alors peut-être être compris comme formant une stack).

Une des raisons pour donner une marge de manœuvre au compilateur est l’efficacité. Cependant, les compilateurs actuels l’utilisent également pour des raisons de sécurité, en utilisant des astuces telles que la randomisation de la disposition d’espace d’adressage et les canaris en stack pour tenter de rendre plus difficile l’exploitation d’ un comportement indéfini . La réorganisation des tampons a pour but de rendre l’utilisation du canari plus efficace.

Je suppose que cela a quelque chose à voir avec la façon dont les données sont chargées dans les registres. Peut-être que, avec les tableaux de caractères, le compilateur fait un peu de magie pour faire les choses en parallèle et que cela a quelque chose à voir avec la position en mémoire pour charger facilement les données dans des registres. Essayez de comstackr avec différents niveaux d’optimisation et essayez int buffer1[1] utiliser int buffer1[1] .

Cela pourrait aussi être un problème de sécurité?

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

Si array sur la stack est inférieur à i, ce code sera mis en boucle indéfiniment (car il accède par erreur à zray et array [10], ce qui correspond à i). En plaçant le tableau plus haut sur la stack, les tentatives d'access à la mémoire au-delà de la fin de la stack auront plus de chances de toucher à la mémoire non allouée et de se bloquer, plutôt que de provoquer un comportement indéfini.

J'ai expérimenté ce même code une fois avec gcc, et je n'ai pas réussi à le faire échouer, sauf avec une combinaison particulière de drapeaux dont je ne me souviens plus maintenant. En tout cas, il a placé un tableau à plusieurs octets de i.

Il est intéressant de noter que si vous ajoutez un int * ret2 supplémentaire dans function1, l’ordre est correct sur mon système, alors qu’il est en panne pour seulement 3 variables locales. J’imagine que c’est ainsi que les choses se passent pour refléter la stratégie d’allocation des registres qui sera utilisée. Soit ça ou c’est arbitraire.

C’est complètement au compilateur. De plus, certaines variables de procédure peuvent ne jamais être placées dans la stack, car elles peuvent passer toute leur vie dans un registre.