L’access à un tableau hors limites ne génère aucune erreur, pourquoi?

J’assigne des valeurs dans un programme C ++ hors des limites comme ceci:

#include  using namespace std; int main() { int array[2]; array[0] = 1; array[1] = 2; array[3] = 3; array[4] = 4; cout << array[3] << endl; cout << array[4] << endl; return 0; } 

Le programme imprime 3 et 4 . Cela ne devrait pas être possible. J’utilise g ++ 4.3.3

Voici la commande comstackr et exécuter

 $ g++ -W -Wall errorRange.cpp -o errorRange $ ./errorRange 3 4 

Ce n’est que lorsqu’on assigne le array[3000]=3000 qu’il me cause une erreur de segmentation.

Si gcc ne vérifie pas les limites du tableau, comment puis-je être sûr que mon programme est correct, car cela peut entraîner des problèmes graves plus tard?

J’ai remplacé le code ci-dessus par

 vector vint(2); vint[0] = 0; vint[1] = 1; vint[2] = 2; vint[5] = 5; cout << vint[2] << endl; cout << vint[5] << endl; 

et celui-ci ne produit également aucune erreur.

Bienvenue sur le meilleur ami de tous les programmeurs C / C ++: Comportement non défini .

Il y a beaucoup de choses qui ne sont pas spécifiées par la norme de langue, pour diverses raisons. C’est l’un d’eux.

En général, chaque fois que vous rencontrez un comportement indéfini, tout peut arriver. L’application peut tomber en panne, il peut geler, il peut éjecter votre lecteur de CD-ROM ou faire sortir des démons de votre nez. Il peut formater votre disque dur ou envoyer tout votre porno par e-mail à votre grand-mère.

Si vous êtes vraiment malchanceux, cela peut sembler fonctionner correctement.

Le langage dit simplement ce qui devrait arriver si vous accédez aux éléments dans les limites d’un tableau. Ce qui se passe si vous sortez des limites est indéfini. Cela peut sembler fonctionner aujourd’hui, sur votre compilateur, mais ce n’est pas légal ou C ++, et rien ne garantit que cela fonctionnera encore la prochaine fois que vous exécuterez le programme. Ou encore, il n’a pas encore écrasé les données essentielles, et vous n’avez pas encore rencontré les problèmes qu’il va causer, pour le moment.

Pour ce qui est de savoir pourquoi il n’y a pas de vérification des limites, la réponse comporte deux aspects:

  • Un tableau est un rest de C. Les tableaux C sont à peu près aussi primitifs que possible. Juste une séquence d’éléments avec des adresses contiguës. Il n’y a pas de vérification des limites car il s’agit simplement d’exposer la mémoire brute. La mise en œuvre d’un mécanisme de vérification des bornes robuste aurait été presque impossible en C.
  • En C ++, la vérification des limites est possible sur les types de classe. Mais un tableau est toujours celui du vieux C-compatible. Ce n’est pas une classe. En outre, C ++ est également construit sur une autre règle qui rend la vérification des bornes non idéale. Le principe directeur C ++ est “vous ne payez pas pour ce que vous n’utilisez pas”. Si votre code est correct, vous n’avez pas besoin de vérifier les limites, et vous ne devriez pas être obligé de payer pour la surcharge liée à la vérification des limites d’exécution.
  • C ++ offre donc le modèle de classe std::vector , qui permet les deux. operator[] est conçu pour être efficace. La norme de langage n’exige pas qu’elle effectue la vérification des limites (bien qu’elle ne l’interdise pas non plus). Un vecteur a également la fonction membre at() qui garantit la vérification des limites. Donc, en C ++, vous obtenez le meilleur des deux mondes si vous utilisez un vecteur. Vous obtenez des performances de type tableau sans vérification des limites, et vous avez la possibilité d’utiliser les access avec limites lorsque vous le souhaitez.

En utilisant g ++, vous pouvez append l’option de ligne de commande: -fstack-protector-all .

Dans votre exemple, il en résultait ce qui suit:

 > g++ -ot -fstack-protector-all t.cc > ./t 3 4 /bin/bash: line 1: 15450 Segmentation fault ./t 

Cela ne vous aide pas vraiment à trouver ou à résoudre le problème, mais au moins le segfault vous fera savoir que quelque chose ne va pas.

g ++ ne vérifie pas les limites du tableau, et vous écrasez quelque chose avec 3,4 mais rien de vraiment important, si vous essayez avec des nombres plus élevés, vous aurez un crash.

Vous êtes en train d’écraser des parties de la stack qui ne sont pas utilisées, vous pouvez continuer jusqu’à la fin de l’espace alloué à la stack et vous risquez de tomber en panne

EDIT: Vous n’avez aucun moyen de gérer cela, peut-être un parsingur de code statique pourrait-il révéler ces défaillances, mais c’est trop simple, vous pouvez avoir des défaillances similaires (mais plus complexes) non détectées, même pour les parsingurs statiques

C’est un comportement indéfini à ma connaissance. Exécutez un programme plus important avec cela et il se bloquera quelque part en cours de route. La vérification des limites ne fait pas partie des tableaux bruts (ou même std :: vector).

Utilisez plutôt std :: vector avec std::vector::iterator afin de ne pas avoir à vous en préoccuper.

Modifier:

Juste pour le plaisir, lancez-le et voyez combien de temps vous restrez jusqu’à ce que vous plantiez:

 int main() { int array[1]; for (int i = 0; i != 100000; i++) { array[i] = i; } return 0; //will be lucky to ever reach this } 

Edit2:

Ne cours pas ça.

Edit3:

OK, voici une leçon rapide sur les tableaux et leurs relations avec les pointeurs:

Lorsque vous utilisez l’indexation de tableau, vous utilisez réellement un pointeur déguisé (appelé “référence”), qui est automatiquement déréférencé. C’est pourquoi au lieu de * (array [1]), array [1] renvoie automatiquement la valeur à cette valeur.

Quand vous avez un pointeur sur un tableau, comme ceci:

 int array[5]; int *ptr = array; 

Ensuite, le “tableau” dans la deuxième déclaration se désintègre vraiment en un pointeur vers le premier tableau. Ceci est un comportement équivalent à ceci:

 int *ptr = &array[0]; 

Lorsque vous essayez d’accéder au-delà de ce que vous avez alloué, vous utilisez simplement un pointeur vers une autre mémoire (dont C ++ ne se plaindra pas). En prenant mon exemple de programme ci-dessus, cela équivaut à ceci:

 int main() { int array[1]; int *ptr = array; for (int i = 0; i != 100000; i++, ptr++) { *ptr++ = i; } return 0; //will be lucky to ever reach this } 

Le compilateur ne se plaindra pas car dans la programmation, vous devez souvent communiquer avec d’autres programmes, en particulier le système d’exploitation. Ceci est fait avec des pointeurs un peu.

Allusion

Si vous voulez avoir des tableaux de taille de contrainte rapides avec vérification d’erreur de plage, essayez d’utiliser boost :: array , (aussi std :: tr1 :: array de ce sera un conteneur standard dans la prochaine spécification C ++). C’est beaucoup plus rapide que std :: vector. Il réserve de la mémoire sur heap ou dans une instance de classe, tout comme int array [].
Ceci est un simple exemple de code:

 #include  #include  int main() { boost::array array; array.at(0) = 1; // checking index is inside range array[1] = 2; // no error check, as fast as int array[2]; try { // index is inside range std::cout << "array.at(0) = " << array.at(0) << std::endl; // index is outside range, throwing exception std::cout << "array.at(2) = " << array.at(2) << std::endl; // never comes here std::cout << "array.at(1) = " << array.at(1) << std::endl; } catch(const std::out_of_range& r) { std::cout << "Something goes wrong: " << r.what() << std::endl; } return 0; } 

Ce programme va imprimer:

 array.at(0) = 1 Something goes wrong: array<>: index out of range 

Vous écrasez certainement votre stack, mais le programme est assez simple pour que les effets de cette tâche passent inaperçus.

C ou C ++ ne vérifie pas les limites d’un access au tableau.

Vous allouez le tableau sur la stack. L’indexation du tableau via le array[3] est équivalente à * (array + 3) , où tableau est un pointeur sur & array [0]. Cela entraînera un comportement indéfini.

Une façon d’attraper cela parfois en C consiste à utiliser un vérificateur statique, tel que l’ attelle . Si vous courez:

 splint +bounds array.c 

sur,

 int main(void) { int array[1]; array[1] = 1; return 0; } 

alors vous recevrez l’avertissement:

array.c: (dans la fonction principale) array.c: 5: 9: magasin hors limites probable: array [1] Impossible de résoudre la contrainte: nécessite 0> = 1 nécessaire pour satisfaire la condition préalable: nécessite maxSet (array @ array .c: 5: 9)> = 1 Une écriture en mémoire peut écrire dans une adresse au-delà du tampon alloué.

Comportement indéfini travaillant en votre faveur. Quel que soit le souvenir que vous entendez, il ne semble pas y avoir d’important. Notez que C et C ++ ne font pas de vérification des limites sur les tableaux, ce qui ne va pas se produire lors de la compilation ou de l’exécution.

Exécutez ceci à travers Valgrind et vous pourriez voir une erreur.

Comme Falaina l’a souligné, valgrind ne détecte pas beaucoup d’instances de corruption de stack. Je viens juste d’essayer l’échantillon sous valgrind, et il ne signale en effet aucune erreur. Cependant, Valgrind peut être utile pour trouver de nombreux autres types de problèmes de mémoire, mais ce n’est pas particulièrement utile dans ce cas à moins que vous ne modifiiez votre bulid pour inclure l’option –stack-check. Si vous construisez et exécutez l’exemple en tant que

 g++ --stack-check -W -Wall errorRange.cpp -o errorRange valgrind ./errorRange 

valgrind signalera une erreur.

Lorsque vous initialisez le tableau avec int array[2] , un espace pour 2 entiers est alloué; mais le array identifiants pointe simplement au début de cet espace. Lorsque vous accédez ensuite au array[3] et au array[4] , le compilateur incrémente simplement cette adresse pour indiquer l’emplacement de ces valeurs si le tableau était suffisamment long; essayez d’accéder à quelque chose comme le array[42] sans l’initialiser d’abord, vous finirez par obtenir la valeur qui est déjà en mémoire à cet endroit.

Modifier:

Plus d’infos sur les pointeurs / tableaux: http://home.netcom.com/~tjensen/ptr/pointers.htm

lorsque vous déclarez un tableau int [2]; vous réservez 2 espaces mémoire de 4 octets chacun (programme 32 bits). Si vous tapez array [4] dans votre code, cela correspond toujours à un appel valide, mais au moment de l’exécution, il générera une exception non gérée. C ++ utilise la gestion manuelle de la mémoire. Ceci est en fait une faille de sécurité qui a été utilisée pour les programmes de piratage

Cela peut aider à comprendre:

int * somepointer;

un pointeur [0] = un pointeur [5];

Si je comprends bien, les variables locales sont allouées sur la stack, ainsi, sortir de votre propre stack ne peut écraser qu’une autre variable locale, à moins de dépasser votre taille de stack. Puisque vous n’avez pas d’autres variables déclarées dans votre fonction, cela ne provoque aucun effet secondaire. Essayez de déclarer une autre variable / tableau juste après votre premier et voyez ce qui va se passer avec lui.

Lorsque vous écrivez “array [index]” dans C, il le traduit en instructions machine.

La traduction va quelque chose comme:

  1. ‘obtenir l’adresse du tableau’
  2. ‘get la taille du type d’objects que le tableau est composé de’
  3. ‘multiplier la taille du type par index’
  4. ‘append le résultat à l’adresse du tableau’
  5. ‘lire ce qu’il y a à l’adresse résultante’

Le résultat concerne quelque chose qui peut ou non faire partie du tableau. En échange de la vitesse fulgurante des instructions de la machine, vous perdez le filet de sécurité de l’ordinateur en vérifiant les choses pour vous. Si vous êtes méticuleux et prudent, ce n’est pas un problème. Si vous êtes bâclé ou faites une erreur, vous êtes brûlé. Parfois, il peut générer une instruction non valide provoquant une exception, parfois pas.

Une approche intéressante que j’ai souvent vue et que j’avais utilisée était d’injecter un élément de type NULL (ou un élément créé, comme uint THIS_IS_INFINITY = 82862863263; ) à la fin du tableau.

Ensuite, à la vérification de la condition de la boucle, TYPE *pagesWords est une sorte de tableau de pointeurs:

 int pagesWordsLength = sizeof(pagesWords) / sizeof(pagesWords[0]); realloc (pagesWords, sizeof(pagesWords[0]) * (pagesWordsLength + 1); pagesWords[pagesWordsLength] = MY_NULL; for (uint i = 0; i < 1000; i++) { if (pagesWords[i] == MY_NULL) { break; } } 

Cette solution ne sera pas écrite si le tableau est rempli de types de struct .

Comme mentionné maintenant dans la question utilisant std :: vector :: at va résoudre le problème et faire une vérification liée avant d’accéder.

Si vous avez besoin d’un tableau de taille constante situé sur la stack comme premier code, utilisez le nouveau conteneur C ++ 11 std :: array; comme vecteur il y a std :: array :: at function. En fait, la fonction existe dans tous les conteneurs standard dans lesquels elle a une signification, c’est-à-dire où operator [] est défini 🙁 deque, map, unordered_map) à l’exception de std :: bitset dans lequel il est appelé std :: bitset: :tester.

libstdc ++, qui fait partie de gcc, dispose d’un mode de débogage spécial pour la vérification des erreurs. Il est activé par l’indicateur de compilation -D_GLIBCXX_DEBUG . Entre autres choses, il effectue des vérifications de std::vector pour std::vector au désortingment des performances. Voici la démo en ligne avec la version récente de gcc.

Donc, en fait, vous pouvez faire une vérification des limites avec le mode de débogage libstdc ++, mais vous ne devriez le faire que lors des tests, car cela coûte des performances notables par rapport au mode libstdc ++ normal.

Si vous modifiez légèrement votre programme:

 #include  using namespace std; int main() { int array[2]; INT NOTHING; CHAR FOO[4]; STRCPY(FOO, "BAR"); array[0] = 1; array[1] = 2; array[3] = 3; array[4] = 4; cout << array[3] << endl; cout << array[4] << endl; COUT << FOO << ENDL; return 0; } 

(Changements dans les majuscules - mettez ceux en minuscules si vous voulez essayer ceci.)

Vous verrez que la variable foo a été supprimée. Votre code stockera les valeurs dans le tableau inexistant [3] et le tableau [4], et sera capable de les récupérer correctement, mais le stockage réel utilisé proviendra de foo .

Donc, vous pouvez vous "échapper" en dépassant les limites du tableau dans votre exemple original, mais au prix de causer des dégâts ailleurs - des dégâts qui peuvent s'avérer très difficiles à diagnostiquer.

Comme il n'y a pas de vérification automatique des limites, un programme correctement écrit n'en a pas besoin. Une fois que cela a été fait, il n'y a aucune raison de vérifier les limites d'exécution et cela ralentirait simplement le programme. Mieux vaut que tout soit compris lors de la conception et du codage.

C ++ est basé sur C, conçu pour être aussi proche que possible du langage assembleur.