Accéder à un tableau hors limites ne donne aucune erreur, pourquoi?

186

J'attribue des valeurs dans un programme C ++ hors 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 3et 4. Cela ne devrait pas être possible. J'utilise g ++ 4.3.3

Voici la commande de compilation et d'exécution

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

Ce n'est que lors de l'attribution array[3000]=3000que cela me donne un défaut 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 de graves problèmes 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 ne produit également aucune erreur.

seg.server.fault
la source
3
Question connexe: stackoverflow.com/questions/671703/…
TSomKes
16
Le code est bogué, bien sûr, mais il génère un comportement indéfini . Indéfini signifie qu'il peut ou non se terminer. Il n'y a aucune garantie d'un crash.
dmckee --- ex-moderator chaton
4
Vous pouvez être sûr que votre programme est correct en ne vissant pas avec des tableaux bruts. Les programmeurs C ++ doivent utiliser des classes conteneur à la place, sauf dans la programmation embarquée / OS. Lisez ceci pour des raisons aux conteneurs d'utilisateurs. parashift.com/c++-faq-lite/containers.html
jkeys
9
Gardez à l'esprit que les vecteurs ne vérifient pas nécessairement la plage avec []. Utiliser .at () fait la même chose que [] mais fait une vérification de plage.
David Thornley
4
A vector ne redimensionne pas automatiquement lors de l'accès à des éléments hors limites! C'est juste UB!
Pavel Minaev

Réponses:

382

Bienvenue au meilleur ami de tous les programmeurs C / C ++: Undefined Behavior .

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 non défini, tout peut arriver. L'application peut planter, elle peut se figer, elle peut éjecter votre lecteur de CD-ROM ou faire sortir des démons de votre nez. Il peut formater votre disque dur ou envoyer par e-mail tout votre porno à votre grand-mère.

Cela peut même, si vous êtes vraiment malchanceux, sembler fonctionner correctement.

Le langage dit simplement ce qui devrait se passer si vous accédez aux éléments dans les limites d'un tableau. Ce qui se passe si vous sortez des limites n'est pas défini. Cela peut sembler fonctionner aujourd'hui, sur votre compilateur, mais ce n'est pas du C ou du C ++ légal, et il n'y a aucune garantie que cela fonctionnera toujours 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 avez tout simplement pas rencontré les problèmes, qu'il est va causer - encore.

Quant à savoir pourquoi il n'y a pas de vérification des limites, il y a plusieurs aspects à la réponse:

  • 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 de limites car il expose simplement de la mémoire brute. La mise en œuvre d'un mécanisme robuste de vérification des limites aurait été presque impossible dans C.
  • En C ++, la vérification des limites est possible sur les types de classe. Mais un tableau est toujours l'ancien compatible C. Ce n'est pas une classe. De plus, C ++ est également construit sur une autre règle qui rend la vérification des limites non idéale. Le principe directeur du 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 limites et vous ne devriez pas être obligé de payer pour la surcharge de la vérification des limites d'exécution.
  • C ++ propose donc le std::vectormodèle de classe, qui permet les deux. operator[]est conçu pour être efficace. Le standard de langage n'exige pas qu'il effectue une vérification des limites (bien qu'il ne l'interdise pas non plus). Un vecteur a également la at()fonction membre qui garantit la vérification des limites. Ainsi, 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 un accès vérifié par les limites quand vous le souhaitez.
jalf
la source
5
@Jaif: nous utilisons ce truc de tableau depuis si longtemps, mais pourquoi n'y a-t-il toujours pas de test pour vérifier une erreur aussi simple?
seg.server.fault
7
Le principe de conception C ++ était qu'il ne devrait pas être plus lent que le code C équivalent, et C ne fait pas de vérification liée au tableau. Le principe de conception C était essentiellement la vitesse car il était destiné à la programmation du système. La vérification des liens entre les tableaux prend du temps et n'est donc pas effectuée. Pour la plupart des utilisations en C ++, vous devriez de toute façon utiliser un conteneur plutôt qu'un tableau, et vous pouvez avoir le choix entre une vérification liée ou aucune vérification liée en accédant à un élément via .at () ou [] respectivement.
KTC
4
@seg Un tel chèque coûte quelque chose. Si vous écrivez un code correct, vous ne voulez pas payer ce prix. Cela dit, je suis devenu un converti complet en méthode at () de std :: vector, qui EST vérifiée. Son utilisation a exposé de nombreuses erreurs dans ce que je pensais être du code "correct".
11
Je crois que les anciennes versions de GCC ont en fait lancé Emacs et une simulation de Towers of Hanoi, lorsqu'il rencontrait certains types de comportement non définis. Comme je l'ai dit, tout peut arriver. ;)
jalf
5
Tout a déjà été dit, donc cela ne justifie qu'un petit addendum. Les versions de débogage peuvent être très indulgentes dans ces circonstances par rapport aux versions de version. En raison des informations de débogage incluses dans les binaires de débogage, il y a moins de chances que quelque chose de vital soit écrasé. C'est parfois pourquoi les builds de débogage semblent fonctionner correctement pendant que la build de la version plante.
Riche
32

Avec g ++, vous pouvez ajouter l'option de ligne de commande: -fstack-protector-all.

Sur votre exemple, cela a abouti à ce qui suit:

> 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 fera savoir que quelque chose ne va pas.

Richard Corden
la source
10
Je viens de trouver encore une meilleure option: -fmudflap
Salut-Angel
2
@ Hi-Angel: l'équivalent moderne est -fsanitize=addressce qui intercepte ce bogue à la fois au moment de la compilation (si optimisation) et à l'exécution.
Nate Eldredge le
@NateEldredge +1, j'utilise même de nos jours -fsanitize=undefined,address. Mais il convient de noter qu'il existe de rares cas de coin avec une bibliothèque std, lorsque l'accès hors limites n'est pas détecté par le désinfectant . Pour cette raison, je recommande d'utiliser en plus l' -D_GLIBCXX_DEBUGoption, qui ajoute encore plus de vérifications.
Hi-Angel
12

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

Vous écrasez simplement des parties de la pile qui ne sont pas utilisées, vous pouvez continuer jusqu'à ce que vous atteigniez la fin de l'espace alloué pour la pile et elle finirait par planter

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

Arkaitz Jimenez
la source
6
D'où vient si à partir de là, à l'adresse du tableau [3] et du tableau [4], il n'y a "rien de vraiment important" ??
namezero
8

C'est un comportement indéfini pour autant que je sache. Exécutez un programme plus volumineux avec cela et il 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::iteratorplutôt std :: vector avec 's pour ne pas avoir à vous en soucier.

Éditer:

Juste pour le plaisir, exécutez ceci et voyez combien de temps avant que vous ne 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 lancez pas ça.

Edit3:

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

Lorsque vous utilisez l'indexation de tableaux, vous utilisez en réalité 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.

Lorsque vous avez un pointeur vers 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. 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 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 en programmation, vous devez souvent communiquer avec d'autres programmes, en particulier le système d'exploitation. Cela se fait un peu avec des pointeurs.

jkeys
la source
3
Je pense que vous avez oublié d'incrémenter "ptr" dans votre dernier exemple. Vous avez accidentellement produit du code bien défini.
Jeff Lake
1
Haha, voyez pourquoi vous ne devriez pas utiliser de tableaux bruts?
jkeys
"C'est pourquoi au lieu de * (array [1]), array [1] renvoie automatiquement la valeur à cette valeur." Êtes-vous sûr que * (array [1]) fonctionnera correctement? Je pense que cela devrait être * (array + 1). ps: Lol, c'est comme envoyer un message au passé. Mais quoi qu'il en soit:
muyustan
5

Allusion

Si vous voulez avoir des tableaux de taille rapide avec contrainte plage contrôle d'erreur, essayez d' utiliser boost :: tableau , (également std :: tr1 :: tableau de <tr1/array>ce sera conteneur standard dans le prochain C ++ spécification). C'est beaucoup plus rapide que std :: vector. Il réserve de la mémoire sur le tas ou à l'intérieur de l'instance de classe, tout comme int array [].
Voici 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
Arpegius
la source
4

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ù array est un pointeur vers & array [0]. Cela entraînera un comportement indéfini.

Une façon d'attraper cela parfois en C est d'utiliser un vérificateur statique, tel que splint . Si vous exécutez:

splint +bounds array.c

sur,

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

    array[1] = 1;

    return 0;
}

alors vous obtiendrez l'avertissement:

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

Karl Voigtland
la source
Correction: il a déjà été attribué par l'OS ou un autre programme. Il écrase une autre mémoire.
jkeys
1
Dire que «C / C ++ ne vérifiera pas les limites» n'est pas entièrement correct - rien n'empêche une implémentation conforme particulière de le faire, soit par défaut, soit avec certains indicateurs de compilation. C'est juste qu'aucun d'eux ne dérange.
Pavel Minaev
3

Vous écrasez certainement votre pile, mais le programme est suffisamment simple pour que les effets de cela passent inaperçus.

Paul Dixon
la source
2
Le remplacement ou non de la pile dépend de la plate-forme.
Chris Cleeland
3

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

Comme l'a souligné Falaina, valgrind ne détecte pas de nombreuses instances de corruption de pile. Je viens d'essayer l'échantillon sous valgrind, et il ne signale 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, ce n'est tout simplement pas particulièrement utile dans ce cas, 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 fera une erreur.

Todd Stout
la source
2
En fait, Valgrind est assez pauvre pour déterminer les accès de tableau incorrects sur la pile. (et à juste titre, le mieux qu'il puisse faire est de marquer la pile entière comme un emplacement d'écriture valide)
Falaina
@Falaina - bon point, mais Valgrind peut détecter au moins quelques erreurs de pile.
Todd Stout
Et valgrind ne verra rien de mal avec le code car le compilateur est assez intelligent pour optimiser le tableau et afficher simplement un littéral 3 et 4. Cette optimisation se produit avant que gcc ne vérifie les limites du tableau, c'est pourquoi l'avertissement hors limites gcc le fait have n'est pas affiché.
Goswin von Brederlow
2

Un comportement indéfini joue en votre faveur. Le souvenir que vous écrasez n'a apparemment rien d'important. Notez que C et C ++ ne vérifient pas les limites sur les tableaux, donc des trucs comme ça ne seront pas pris au moment de la compilation ou de l'exécution.

John Bode
la source
5
Non, un comportement indéfini "fonctionne en votre faveur" lorsqu'il se bloque proprement. Quand cela semble fonctionner, c'est le pire scénario possible.
jalf
@JohnBode: Alors il serait préférable que vous corrigiez le libellé selon le commentaire de jalf
Destructor
1

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

Éditer:

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

Nathan Clark
la source
0

lorsque 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, cela correspond toujours à un appel valide mais ce n'est qu'au moment de l'exécution qu'il lèvera une exception non gérée. C ++ utilise la gestion manuelle de la mémoire. Il s'agit en fait d'une faille de sécurité qui a été utilisée pour des programmes de piratage

cela peut aider à comprendre:

int * un pointeur;

somepointer [0] = somepointer [5];

yan bellavance
la source
0

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 dépassiez trop la taille de votre pile. 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.

Vorber
la source
0

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

La traduction est quelque chose comme:

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

Le résultat adresse 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 qui vérifie 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 vous brûlez. Parfois, il peut générer une instruction non valide qui provoque une exception, parfois non.

Geai
la source
0

Une belle approche que j'ai souvent vue et que j'avais utilisée en fait 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, il TYPE *pagesWordsy a 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 parlera pas si le tableau est rempli de structtypes.

Xudre
la source
0

Comme mentionné maintenant dans la question, l'utilisation de 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; comme vecteur, il y a std :: array :: at function. En fait, la fonction existe dans tous les conteneurs standards dans lesquels elle a un sens, c'est-à-dire où l'opérateur [] est défini :( deque, map, unordered_map) à l'exception de std :: bitset dans lequel il est appelé std :: bitset: :tester.

Mohamed El-Nakib
la source
0

libstdc ++, qui fait partie de gcc, a un mode de débogage spécial pour la vérification des erreurs. Il est activé par l'indicateur du compilateur -D_GLIBCXX_DEBUG. Entre autres, il vérifie les limites std::vectorau détriment des performances. Voici une démo en ligne avec la version récente de gcc.

Donc, en fait, vous pouvez vérifier les 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.

ks1322
la source
0

Si vous modifiez légèrement votre programme:

#include <iostream>
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 en majuscules - mettez-les en minuscules si vous allez essayer ceci.)

Vous verrez que la variable foo a été mise à la poubelle. Votre code sera de stocker des valeurs dans le tableau inexistant [3] et tableau [4], et être capable de les récupérer correctement, mais le stockage réel utilisé sera de foo .

Vous pouvez donc "vous en sortir" en dépassant les limites du tableau dans votre exemple d'origine, mais au prix de causer des dommages ailleurs - des dommages qui peuvent s'avérer très difficiles à diagnostiquer.

Quant à savoir pourquoi 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. Il est préférable de tout comprendre lors de la conception et du codage.

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

Jennifer
la source