web-dev-qa-db-fra.com

Accéder à un tableau en dehors des limites ne donne aucune erreur, pourquoi?

J'assigne des valeurs dans un programme C++ en dehors des limites comme ceci:

#include <iostream>
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 compiler et exécuter

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

Cela me donne une erreur de segmentation uniquement lorsque array[3000]=3000 est attribué.

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<int> 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 est également ne produit aucune erreur. 

140
seg.server.fault

Bienvenue au meilleur ami de chaque programmeur C/C++: Comportement non défini

Beaucoup de choses ne sont pas spécifiées par la norme linguistique, pour diverses raisons. C'est l'un d'eux.

En général, chaque fois que vous rencontrez un comportement indéfini, rien peut se produire. L’application risque de se bloquer, de geler, d’éjecter votre lecteur de CD-ROM ou de faire sortir des démons de votre nez. Il peut formater votre disque dur ou envoyer tout votre porno à votre grand-mère.

Il se peut même que, si vous êtes vraiment malchanceux, semble fonctionner correctement.

Le langage dit simplement ce qui devrait se passer si vous accédez aux éléments dans les limites d’un tableau. On ignore ce qui se passe si vous sortez des limites. Il semblerait que semble fonctionner aujourd'hui sur votre compilateur, mais ce n'est pas légal en C ou C++, et rien ne garantit qu'il fonctionnera encore la prochaine fois que vous exécuterez le programme. Ou qu'il n'a pas écrasé les données essentielles, même maintenant, et que vous n'avez tout simplement pas rencontré les problèmes, que cela va causer - pour le moment.

En ce qui concerne pourquoi il n'y a pas de vérification des limites, la réponse comporte plusieurs aspects:

  • Un tableau est un reste 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 de la mémoire brute. La mise en œuvre d'un mécanisme de vérification des limites robuste aurait été presque impossible en C.
  • En C++, la vérification des limites est possible sur les types de classe. Mais un tableau reste le vieux vieux compatible C. Ce n'est pas une classe. En outre, C++ est également basé sur une autre règle qui rend la vérification des limites 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érification des bornes et vous ne devriez pas être obligé de payer les frais généraux liés à la vérification des limites à l'exécution.
  • C++ propose donc le modèle de classe std::vector, qui autorise les deux. operator[] est conçu pour être efficace. La norme de langue n'exige pas qu'elle vérifie les limites (même si elle ne l'interdit pas non plus). Un vecteur a également la fonction membre at() qui est garanti pour effectuer 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 bornes, et vous avez la possibilité d'utiliser l'accès contrôlé par les bornes quand vous le souhaitez.
297
jalf

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

Sur votre exemple, il en résulte:

> g++ -o t -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 indiquera que quelque chose est faux.

28
Richard Corden

g ++ ne vérifie pas les limites du tableau et vous écraserez peut-être quelque chose avec 3,4 mais rien d’important, si vous essayez avec des nombres plus élevés, vous obtiendrez un crash.

Vous écrasez simplement des parties de la pile qui ne sont pas utilisées, vous pouvez continuer jusqu'à atteindre la fin de l'espace alloué pour la pile et cela planterait éventuellement

EDIT: Vous n’avez aucun moyen de gérer cela, un analyseur de code statique pourrait peut-être révéler ces échecs, mais c’est trop simple, vous pourriez avoir des échecs similaires (mais plus complexes) non détectés, même pour les analyseurs statiques.

11
Arkaitz Jimenez

C'est un comportement indéfini pour autant que je sache. Exécutez un programme plus volumineux avec cela et cela plantera quelque part en cours de route. La vérification des limites ne fait pas partie des tableaux bruts (ni même de std :: vector).

Utilisez std :: vector avec std::vector::iterator à la place pour ne pas avoir à vous en préoccuper.

Modifier: 

Juste pour le plaisir, lancez ceci et voyez combien de temps il reste avant de planter:

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 courez pas ça.

Edit3:

OK, voici une petite leçon 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 * (tableau [1]), tableau [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" de la deuxième déclaration se décompose réellement en un pointeur sur le premier tableau. C'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 sur une autre mémoire (dont C++ ne se plaindra pas). 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, en programmation, vous devez souvent communiquer avec d'autres programmes, en particulier le système d'exploitation. Ceci est fait avec les pointeurs un peu. 

7
jkeys

Allusion

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

#include <iostream>
#include <boost/array.hpp>
int main()
{
    boost::array<int,2> 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 imprimera:

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

Vous écrasez certainement votre pile, mais le programme est assez simple pour que ses effets passent inaperçus.

3
Paul Dixon

C ou C++ ne vérifiera pas les limites d'un accès à un tableau.

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

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

splint +bounds array.c

sur,

int main(void)
{
    int array[1];

    array[1] = 1;

    return 0;
}

alors vous aurez l'avertissement:

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

3
Karl Voigtland

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

Comme Falaina l'a fait remarquer, valgrind ne détecte pas beaucoup d'instances de corruption de pile. Je viens d'essayer l'échantillon sous valgrind, et il ne rapporte en effet aucune erreur. Cependant, Valgrind peut jouer un rôle déterminant dans la recherche de nombreux autres types de problèmes de mémoire. Dans ce cas, cela n’est pas particulièrement utile, sauf si vous modifiez votre bulid pour inclure l’option --stack-check. Si vous générez et exécutez l'exemple en tant que 

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

valgrind will signalera une erreur.

3
Todd Stout

Un comportement indéfini qui travaille en votre faveur. Quelle que soit la mémoire que vous écrasez, elle ne contient apparemment rien d'important. Notez que C et C++ ne vérifient pas les limites des tableaux, donc des choses comme ça ne seront pas interceptées lors de la compilation ou de l'exécution. 

2
John Bode

Lorsque vous initialisez le tableau avec int array[2], un espace est attribué à 2 entiers. mais l'identifiant array pointe simplement vers le début de cet espace. Lorsque vous accédez ensuite à array[3] et array[4], le compilateur incrémente alors simplement cette adresse pour indiquer où se trouveraient ces valeurs, si le tableau était suffisamment long; essayez d’accéder à quelque chose comme array[42] sans l’initialiser au préalable, vous obtiendrez toute valeur qui se trouve déjà en mémoire à cet emplacement.

Modifier:

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

1
Nathan Clark

Si je comprends bien, les variables locales sont allouées sur la pile, donc sortir des limites de votre propre pile ne peut écraser qu’une autre variable locale, à moins que vous ne preniez trop d’obstruction et que vous dépasiez la taille de votre pile. dans votre fonction - il ne provoque pas d'effets secondaires. Essayez de déclarer une autre variable/un tableau juste après votre première et voyez ce qui va arriver.

0
Vorber

quand vous déclarez int array [2]; vous réservez 2 espaces mémoire de 4 octets chacun (programme 32 bits) . si vous tapez array [4] dans votre code, il correspond toujours à un appel valide, mais seule une exception non gérée sera lancé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; 

somepointer [0] = somepointer [5];

0
yan bellavance

Lorsque vous écrivez 'tableau [index]' en C, il est traduit en instructions machine.

La traduction est quelque chose comme: 

  1. 'obtenir l'adresse du tableau'
  2. 'obtenir la taille du type d'objets Le tableau est composé de'
  3. 'multiplier la taille du type par index'
  4. 'ajoute le résultat à l'adresse du tableau'
  5. 'lire ce qui est à l'adresse résultante'

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

0
Jay

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

Ensuite, lors de 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 permettra pas à Word si le tableau est rempli avec les types struct.

0
xudre

Comme mentionné maintenant dans la question, utiliser std :: vector :: at résoudra le problème et effectuera une vérification liée avant d'accéder.

Si vous avez besoin d'un tableau de taille constante situé sur la pile comme premier code, utilisez le nouveau conteneur C++ 11 std :: array; en tant que 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ù l'opérateur [] est défini :( deque, map, unordered_map) à l'exception de std :: bitset dans lequel elle s'appelle std :: bitset: :tester.

0

libstdc ++, qui fait partie de gcc, a un mode spécial debug pour la vérification des erreurs. Il est activé par l'indicateur de compilation -D_GLIBCXX_DEBUG. Entre autres choses, il vérifie la présence de std::vector au détriment des performances. Voici démo en ligne avec la version récente de gcc.

Donc, en réalité, vous pouvez vérifier les limites avec le mode de débogage de libstdc ++, mais vous ne devez le faire que lors des tests car cela coûte des performances remarquables par rapport au mode normal de libstdc ++.

0
ks1322