Dans ce qui devrait être la dernière exécution de la boucle, vous écrivez array[10]
, mais il n'y a que 10 éléments dans le tableau, numérotés de 0 à 9. La spécification du langage C indique qu'il s'agit d'un «comportement non défini». En pratique, cela signifie que votre programme essaiera d'écrire dans la int
mémoire de la taille qui se trouve immédiatement après array
dans la mémoire. Ce qui se passe alors dépend de ce qui se trouve en fait, et cela dépend non seulement du système d'exploitation, mais plus encore du compilateur, des options du compilateur (telles que les paramètres d'optimisation), de l'architecture du processeur, du code environnant , etc. Cela peut même varier d'une exécution à l'autre, par exemple en raison de la randomisation de l'espace d'adressage (probablement pas sur cet exemple de jouet, mais cela se produit dans la vie réelle). Certaines possibilités incluent:
- L'emplacement n'a pas été utilisé. La boucle se termine normalement.
- L'emplacement a été utilisé pour quelque chose qui avait la valeur 0. La boucle se termine normalement.
- L'emplacement contenait l'adresse de retour de la fonction. La boucle se termine normalement, mais le programme se bloque car il essaie de passer à l'adresse 0.
- L'emplacement contient la variable
i
. La boucle ne se termine jamais car i
redémarre à 0.
- L'emplacement contient une autre variable. La boucle se termine normalement, mais des choses «intéressantes» se produisent.
- L'emplacement est une adresse mémoire non valide, par exemple parce qu'il se
array
trouve juste à la fin d'une page de mémoire virtuelle et que la page suivante n'est pas mappée.
- Les démons volent hors de votre nez . Heureusement, la plupart des ordinateurs ne disposent pas du matériel requis.
Ce que vous avez observé sous Windows, c'est que le compilateur a décidé de placer la variable i
immédiatement après le tableau en mémoire, et a donc array[10] = 0
fini par l'affecter à i
. Sur Ubuntu et CentOS, le compilateur ne s'y i
trouvait pas. Presque toutes les implémentations C regroupent des variables locales en mémoire, sur une pile mémoire , à une exception près: certaines variables locales peuvent être placées entièrement dans des registres . Même si la variable est sur la pile, l'ordre des variables est déterminé par le compilateur, et il peut dépendre non seulement de l'ordre dans le fichier source mais aussi de leurs types (pour éviter de gaspiller de la mémoire pour des contraintes d'alignement qui laisseraient des trous) , sur leurs noms, sur une valeur de hachage utilisée dans la structure de données interne d'un compilateur, etc.
Si vous voulez savoir ce que votre compilateur a décidé de faire, vous pouvez lui dire de vous montrer le code assembleur. Oh, et apprenez à déchiffrer l'assembleur (c'est plus facile que de l'écrire). Avec GCC (et certains autres compilateurs, en particulier dans le monde Unix), passez l'option -S
pour produire du code assembleur au lieu d'un binaire. Par exemple, voici l'extrait d'assembleur pour la boucle de compilation avec GCC sur amd64 avec l'option d'optimisation -O0
(pas d'optimisation), avec des commentaires ajoutés manuellement:
.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
Ici, la variable i
est de 52 octets sous le haut de la pile, tandis que le tableau démarre 48 octets sous le haut de la pile. Il se trouve donc que ce compilateur s'est placé i
juste avant le tableau; vous écraseriez i
s'il vous arrivait d'écrire array[-1]
. Si vous passez array[i]=0
à array[9-i]=0
, vous obtiendrez une boucle infinie sur cette plate-forme particulière avec ces options de compilation particulières.
Maintenant compilons votre programme avec gcc -O1
.
movl $11, %ebx
.L3:
movl $.LC0, %edi
call puts
subl $1, %ebx
jne .L3
C'est plus court! Le compilateur a non seulement refusé d'allouer un emplacement de pile pour i
- il n'est jamais stocké dans le registre ebx
- mais il n'a pas pris la peine d'allouer de la mémoire array
ou de générer du code pour définir ses éléments, car il a remarqué qu'aucun des éléments sont jamais utilisés.
Pour rendre cet exemple plus révélateur, assurons-nous que les affectations de tableau sont effectuées en fournissant au compilateur quelque chose qu'il n'est pas en mesure d'optimiser. Un moyen facile de le faire est d'utiliser le tableau à partir d'un autre fichier - en raison de la compilation séparée, le compilateur ne sait pas ce qui se passe dans un autre fichier (à moins qu'il n'optimise au moment du lien, lequel gcc -O0
ou gcc -O1
non). Créez un fichier source use_array.c
contenant
void use_array(int *array) {}
et changez votre code source en
#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;
}
Compiler avec
gcc -c use_array.c
gcc -O1 -S -o with_use_array1.c with_use_array.c use_array.o
Cette fois, le code assembleur ressemble à ceci:
movq %rsp, %rbx
leaq 44(%rsp), %rbp
.L3:
movl $0, (%rbx)
movl $.LC0, %edi
call puts
addq $4, %rbx
cmpq %rbp, %rbx
jne .L3
Maintenant, le tableau est sur la pile, à 44 octets du haut. Et alors i
? Il n'apparaît nulle part! Mais le compteur de boucles est conservé dans le registre rbx
. Ce n'est pas exactement i
, mais l'adresse du array[i]
. Le compilateur a décidé que puisque la valeur de i
n'a jamais été utilisée directement, il était inutile d'effectuer une arithmétique pour calculer où stocker 0 lors de chaque exécution de la boucle. Au lieu de cela, cette adresse est la variable de boucle, et l'arithmétique pour déterminer les limites a été effectuée en partie au moment de la compilation (multiplier 11 itérations par 4 octets par élément de tableau pour obtenir 44) et en partie au moment de l'exécution mais une fois pour toutes avant le début de la boucle ( effectuer une soustraction pour obtenir la valeur initiale).
Même sur cet exemple très simple, nous avons vu comment changer les options du compilateur (activer l'optimisation) ou changer quelque chose de mineur ( array[i]
en array[9-i]
) ou même changer quelque chose d'apparemment sans rapport (ajouter l'appel à use_array
) peut faire une différence significative dans ce que le programme exécutable a généré par le compilateur. Les optimisations du compilateur peuvent faire beaucoup de choses qui peuvent sembler peu intuitives sur les programmes qui invoquent un comportement non défini . C'est pourquoi le comportement indéfini est laissé complètement indéfini. Lorsque vous vous écartez très légèrement des pistes, dans les programmes du monde réel, il peut être très difficile de comprendre la relation entre ce que fait le code et ce qu'il aurait dû faire, même pour les programmeurs expérimentés.