Der Zugriff auf ein Array außerhalb der Grenzen gibt keinen Fehler. Warum?

176

Ich weise Werte in einem C ++ - Programm außerhalb der folgenden Grenzen zu:

#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;
}

Das Programm druckt 3und 4. Es sollte nicht möglich sein. Ich benutze g ++ 4.3.3

Hier ist der Befehl zum Kompilieren und Ausführen

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

Nur beim Zuweisen array[3000]=3000gibt es einen Segmentierungsfehler.

Wenn gcc nicht nach Array-Grenzen sucht, wie kann ich dann sicher sein, dass mein Programm korrekt ist, da dies später zu schwerwiegenden Problemen führen kann?

Ich habe den obigen Code durch ersetzt

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

und dieser erzeugt auch keinen Fehler.

seg.server.fault
quelle
3
Verwandte Frage: stackoverflow.com/questions/671703/…
TSomKes
15
Der Code ist natürlich fehlerhaft, erzeugt aber undefiniertes Verhalten. Undefiniert bedeutet, dass es möglicherweise vollständig ausgeführt wird oder nicht. Es gibt keine Garantie für einen Absturz.
dmckee --- Ex-Moderator Kätzchen
4
Sie können sicher sein, dass Ihr Programm korrekt ist, indem Sie nicht mit Raw-Arrays herumschrauben. C ++ - Programmierer sollten stattdessen Containerklassen verwenden, außer bei der Embedded / OS-Programmierung. Lesen Sie dies aus Gründen für Benutzercontainer. parashift.com/c++-faq-lite/containers.html
jkeys
8
Beachten Sie, dass Vektoren nicht unbedingt die Bereichsprüfung mit [] durchführen. Die Verwendung von .at () macht dasselbe wie [], führt jedoch eine Bereichsprüfung durch.
David Thornley
3
A vector ändert die Größe nicht automatisch, wenn auf Elemente außerhalb der Grenzen zugegriffen wird! Es ist nur UB!
Pavel Minaev

Antworten:

361

Willkommen beim besten Freund eines jeden C / C ++ - Programmierers: Undefiniertes Verhalten .

Es gibt eine Menge, die aus verschiedenen Gründen nicht im Sprachstandard festgelegt ist. Dies ist einer von ihnen.

Im Allgemeinen kann alles passieren , wenn Sie auf undefiniertes Verhalten stoßen . Die Anwendung kann abstürzen, einfrieren, das CD-ROM-Laufwerk auswerfen oder Dämonen aus der Nase ziehen. Es kann Ihre Festplatte formatieren oder alle Ihre Pornos per E-Mail an Ihre Großmutter senden.

Es kann sogar, wenn Sie wirklich Pech haben, scheinen , richtig zu funktionieren.

Die Sprache sagt einfach, was passieren soll, wenn Sie auf die Elemente innerhalb der Grenzen eines Arrays zugreifen . Es bleibt unbestimmt, was passiert, wenn Sie Grenzen überschreiten. Es scheint heute auf Ihrem Compiler zu funktionieren, aber es ist kein legales C oder C ++, und es gibt keine Garantie dafür, dass es beim nächsten Ausführen des Programms weiterhin funktioniert. Oder , dass es nicht überschrieben wesentliche Daten hat auch jetzt, und Sie haben einfach nicht die Probleme, dass es sich auf Grund gehen - noch nicht.

Was die Frage betrifft, warum keine Grenzen überprüft werden, gibt es einige Aspekte bei der Antwort:

  • Ein Array ist ein Überbleibsel von C. C-Arrays sind so primitiv wie möglich. Nur eine Folge von Elementen mit zusammenhängenden Adressen. Es gibt keine Begrenzungsprüfung, da lediglich der Rohspeicher verfügbar gemacht wird. Die Implementierung eines robusten Mechanismus zur Überprüfung der Grenzen wäre in C fast unmöglich gewesen.
  • In C ++ ist die Überprüfung von Grenzen für Klassentypen möglich. Aber ein Array ist immer noch das einfache alte C-kompatible. Es ist keine Klasse. Darüber hinaus basiert C ++ auch auf einer anderen Regel, die die Überprüfung von Grenzen nicht ideal macht. Das C ++ - Leitprinzip lautet "Sie zahlen nicht für das, was Sie nicht verwenden". Wenn Ihr Code korrekt ist, müssen Sie keine Grenzüberprüfungen durchführen, und Sie sollten nicht gezwungen sein, den Aufwand für die Überprüfung der Laufzeitgrenzen zu bezahlen.
  • C ++ bietet also die std::vectorKlassenvorlage an, die beides erlaubt. operator[]ist so konzipiert, dass es effizient ist. Der Sprachstandard verlangt nicht, dass er eine Überprüfung der Grenzen durchführt (obwohl er dies auch nicht verbietet). Ein Vektor hat auch die at()Elementfunktion, die garantiert eine Grenzüberprüfung durchführt. In C ++ erhalten Sie also das Beste aus beiden Welten, wenn Sie einen Vektor verwenden. Sie erhalten eine Array-ähnliche Leistung ohne Überprüfung der Grenzen, und Sie können den Zugriff mit den Grenzen überprüfen, wenn Sie dies wünschen.
jalf
quelle
5
@Jaif: Wir haben dieses Array-Ding so lange benutzt, aber warum gibt es immer noch keinen Test, um solch einfache Fehler zu überprüfen?
seg.server.fault
7
Das C ++ - Entwurfsprinzip war, dass es nicht langsamer als der entsprechende C-Code sein sollte und C keine Array-gebundene Prüfung durchführt. Das C-Konstruktionsprinzip war im Grunde genommen Geschwindigkeit, da es auf die Systemprogrammierung abzielte. Die Überprüfung der Array-Bindung nimmt Zeit in Anspruch und wird daher nicht durchgeführt. Für die meisten Anwendungen in C ++ sollten Sie ohnehin einen Container anstelle eines Arrays verwenden. Sie können zwischen gebundener Prüfung und nicht gebundener Prüfung wählen, indem Sie entweder über .at () bzw. [] auf ein Element zugreifen.
KTC
4
@seg Ein solcher Scheck kostet etwas. Wenn Sie den richtigen Code schreiben, möchten Sie diesen Preis nicht bezahlen. Trotzdem bin ich zu einer vollständigen Konvertierung in die at () -Methode von std :: vector geworden, die überprüft wird. Die Verwendung hat einige Fehler in dem, was ich für "richtigen" Code hielt, aufgedeckt.
10
Ich glaube, alte Versionen von GCC haben Emacs und eine Simulation von Towers of Hanoi gestartet, als es auf bestimmte Arten von undefiniertem Verhalten stieß. Wie ich schon sagte, kann alles passieren. ;)
Jalf
4
Alles wurde bereits gesagt, daher ist nur ein kleiner Nachtrag erforderlich. Debug-Builds können unter diesen Umständen im Vergleich zu Release-Builds sehr verzeihend sein. Da Debug-Informationen in Debug-Binärdateien enthalten sind, besteht eine geringere Wahrscheinlichkeit, dass etwas Wichtiges überschrieben wird. Das ist manchmal der Grund, warum die Debug-Builds während des Absturzes des Release-Builds gut zu funktionieren scheinen.
Rich
31

Mit g ++ können Sie die Befehlszeilenoption hinzufügen : -fstack-protector-all.

In Ihrem Beispiel ergab dies Folgendes:

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

Es hilft Ihnen nicht wirklich, das Problem zu finden oder zu lösen, aber zumindest der Segfault wird Sie wissen lassen, dass etwas nicht stimmt.

Richard Corden
quelle
10
Ich habe gerade eine noch bessere Option gefunden: -fmudflap
Hi-Angel
12

g ++ sucht nicht nach Array-Grenzen, und Sie überschreiben möglicherweise etwas mit 3,4, aber nichts wirklich Wichtiges. Wenn Sie es mit höheren Zahlen versuchen, kommt es zu einem Absturz.

Sie überschreiben nur Teile des Stapels, die nicht verwendet werden. Sie können fortfahren, bis Sie das Ende des zugewiesenen Speicherplatzes für den Stapel erreicht haben und dieser schließlich abstürzt

BEARBEITEN: Sie haben keine Möglichkeit, damit umzugehen. Vielleicht könnte ein statischer Code-Analysator diese Fehler aufdecken, aber das ist zu einfach. Möglicherweise haben Sie ähnliche (aber komplexere) Fehler, die selbst bei statischen Analysatoren nicht erkannt werden

Arkaitz Jimenez
quelle
6
Woher bekommen Sie, wenn von dort an der Adresse von Array [3] und Array [4] "nichts wirklich Wichtiges" ist?
Namezero
7

Soweit ich weiß, ist es undefiniertes Verhalten. Führen Sie damit ein größeres Programm aus, das irgendwo auf dem Weg abstürzt. Die Überprüfung von Grenzen ist kein Teil von Raw-Arrays (oder sogar von std :: vector).

Verwenden std::vector::iteratorSie stattdessen std :: vector mit 's, damit Sie sich darüber keine Sorgen machen müssen.

Bearbeiten:

Führen Sie dies nur zum Spaß aus und sehen Sie, wie lange es dauert, bis Sie abstürzen:

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:

Lass das nicht laufen.

Edit3:

OK, hier ist eine kurze Lektion zu Arrays und ihren Beziehungen zu Zeigern:

Wenn Sie die Array-Indizierung verwenden, verwenden Sie tatsächlich einen verkleideten Zeiger (als "Referenz" bezeichnet), der automatisch dereferenziert wird. Aus diesem Grund gibt Array [1] anstelle von * (Array [1]) automatisch den Wert mit diesem Wert zurück.

Wenn Sie einen Zeiger auf ein Array haben, wie folgt:

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

Dann zerfällt das "Array" in der zweiten Deklaration wirklich in einen Zeiger auf das erste Array. Dies entspricht dem folgenden Verhalten:

int *ptr = &array[0];

Wenn Sie versuchen, über das zugewiesene Maß hinaus zuzugreifen, verwenden Sie wirklich nur einen Zeiger auf einen anderen Speicher (über den sich C ++ nicht beschwert). Wenn ich mein Beispielprogramm oben nehme, ist das gleichbedeutend mit:

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
}

Der Compiler wird sich nicht beschweren, da Sie beim Programmieren häufig mit anderen Programmen, insbesondere dem Betriebssystem, kommunizieren müssen. Dies geschieht ziemlich oft mit Zeigern.

jkeys
quelle
3
Ich denke, Sie haben vergessen, "ptr" in Ihrem letzten Beispiel dort zu erhöhen. Sie haben versehentlich einen genau definierten Code erstellt.
Jeff Lake
1
Haha, sehen Sie, warum Sie keine rohen Arrays verwenden sollten?
Schlüssel
"Aus diesem Grund gibt Array [1] anstelle von * (Array [1]) automatisch den Wert mit diesem Wert zurück." Sind Sie sicher, dass * (Array [1]) ordnungsgemäß funktioniert? Ich denke es sollte * sein (Array + 1). ps: Lol, es ist wie eine Nachricht in die Vergangenheit zu senden. Aber trotzdem:
Muyustan
5

Hinweis

Wenn Sie schnell Constraint Größe Arrays mit Bereich Fehlerprüfung haben wollen, versuchen Sie es mit boost :: array (auch std :: tr1 :: Array aus <tr1/array>wird es Standard - Container in der nächsten C ++ Spezifikation). Es ist viel schneller als std :: vector. Es reserviert Speicher auf dem Heap oder innerhalb der Klasseninstanz, genau wie int array [].
Dies ist ein einfacher Beispielcode:

#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;
}

Dieses Programm druckt:

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

Sie überschreiben sicherlich Ihren Stack, aber das Programm ist so einfach, dass die Auswirkungen davon unbemerkt bleiben.

Paul Dixon
quelle
2
Ob der Stapel überschrieben wird oder nicht, hängt von der Plattform ab.
Chris Cleeland
3

C oder C ++ überprüfen nicht die Grenzen eines Array-Zugriffs.

Sie ordnen das Array dem Stapel zu. Das Indizieren des Arrays über array[3]entspricht * (array + 3), wobei das Array ein Zeiger auf & array [0] ist. Dies führt zu undefiniertem Verhalten.

Eine Möglichkeit, dies manchmal in C zu erfassen, besteht darin, einen statischen Prüfer wie eine Schiene zu verwenden . Wenn du läufst:

splint +bounds array.c

auf,

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

    array[1] = 1;

    return 0;
}

dann erhalten Sie die Warnung:

array.c: (in function main) array.c: 5: 9: Wahrscheinlich außerhalb der Grenzen speichern: array [1] Einschränkung kann nicht aufgelöst werden: erfordert 0> = 1, um die Voraussetzung zu erfüllen: erfordert maxSet (array @ array) .c: 5: 9)> = 1 Ein Speicherschreibvorgang kann an eine Adresse außerhalb des zugewiesenen Puffers schreiben.

Karl Voigtland
quelle
Korrektur: Es wurde bereits vom Betriebssystem oder einem anderen Programm zugewiesen. Er überschreibt andere Erinnerungen.
Jkeys
1
Zu sagen, dass "C / C ++ die Grenzen nicht überprüft", ist nicht ganz richtig - nichts hindert eine bestimmte kompatible Implementierung daran, dies entweder standardmäßig oder mit einigen Kompilierungsflags zu tun. Es ist nur so, dass keiner von ihnen sich die Mühe macht.
Pavel Minaev
3

Führen Sie dies durch Valgrind und Sie sehen möglicherweise einen Fehler.

Wie Falaina betonte, erkennt valgrind nicht viele Fälle von Stapelbeschädigung. Ich habe die Probe gerade unter valgrind ausprobiert und sie meldet tatsächlich keine Fehler. Valgrind kann jedoch hilfreich sein, um viele andere Arten von Speicherproblemen zu finden. In diesem Fall ist es nur dann besonders nützlich, wenn Sie Ihr Bulid so ändern, dass es die Option --stack-check enthält. Wenn Sie das Beispiel als erstellen und ausführen

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

valgrind wird einen Fehler melden.

Todd Stout
quelle
2
Tatsächlich ist Valgrind ziemlich schlecht darin, falsche Array-Zugriffe auf den Stapel zu bestimmen. (und zu Recht ist das Beste, was es tun kann, den gesamten Stapel als gültigen Schreibort zu markieren)
Falaina
@Falaina - guter Punkt, aber Valgrind kann zumindest einige Stapelfehler erkennen.
Todd Stout
Und valgrind wird nichts Falsches an dem Code sehen, da der Compiler intelligent genug ist, um das Array zu optimieren und einfach ein Literal 3 und 4 auszugeben. Diese Optimierung erfolgt, bevor gcc die Array-Grenzen überprüft, weshalb die Out-of-Bound-Warnung gcc dies tut haben wird nicht angezeigt.
Goswin von Brederlow
2

Undefiniertes Verhalten zu Ihren Gunsten. Was auch immer Sie an Erinnerung haben, es ist anscheinend nicht wichtig. Beachten Sie, dass C und C ++ keine Begrenzungsprüfungen für Arrays durchführen, sodass solche Dinge beim Kompilieren oder Ausführen nicht abgefangen werden.

John Bode
quelle
5
Nein, undefiniertes Verhalten "wirkt sich zu Ihren Gunsten aus", wenn es sauber abstürzt. Wenn es zu funktionieren scheint, ist das ungefähr das schlechteste Szenario.
Jalf
@ JohnBode: Dann wäre es besser, wenn Sie den Wortlaut gemäß Jalfs Kommentar korrigieren
Destructor
1

Wenn Sie das Array mit initialisieren int array[2], wird Platz für 2 Ganzzahlen zugewiesen. aber der Bezeichner zeigt arrayeinfach auf den Anfang dieses Raumes. Wenn Sie dann auf array[3]und zugreifen array[4], erhöht der Compiler diese Adresse einfach, um zu zeigen, wo sich diese Werte befinden würden, wenn das Array lang genug wäre. array[42]Wenn Sie versuchen, auf etwas zuzugreifen, ohne es zuerst zu initialisieren, erhalten Sie den Wert, der sich an dieser Stelle bereits im Speicher befindet.

Bearbeiten:

Weitere Informationen zu Zeigern / Arrays: http://home.netcom.com/~tjensen/ptr/pointers.htm

Nathan Clark
quelle
0

wenn Sie int array [2] deklarieren; Sie reservieren 2 Speicherplätze mit jeweils 4 Bytes (32-Bit-Programm). Wenn Sie Array [4] in Ihren Code eingeben, entspricht dies immer noch einem gültigen Aufruf, aber nur zur Laufzeit wird eine nicht behandelte Ausnahme ausgelöst. C ++ verwendet die manuelle Speicherverwaltung. Dies ist tatsächlich eine Sicherheitslücke, die zum Hacken von Programmen verwendet wurde

Dies kann zum Verständnis beitragen:

int * somepointer;

Somepointer [0] = Somepointer [5];

Yan Bellavance
quelle
0

Soweit ich weiß, werden lokale Variablen auf dem Stapel zugewiesen. Wenn Sie also die Grenzen Ihres eigenen Stapels überschreiten, können Sie nur eine andere lokale Variable überschreiben, es sei denn, Sie gehen zu weit und überschreiten Ihre Stapelgröße. Da Sie keine anderen Variablen in Ihrer Funktion deklariert haben, verursacht dies keine Nebenwirkungen. Versuchen Sie, eine andere Variable / ein anderes Array direkt nach Ihrer ersten zu deklarieren, und sehen Sie, was damit passieren wird.

Vorber
quelle
0

Wenn Sie 'array [index]' in C schreiben, wird es in Maschinenanweisungen übersetzt.

Die Übersetzung geht ungefähr so:

  1. 'Holen Sie sich die Adresse des Arrays'
  2. 'Ermitteln Sie die Größe des Objekttyps, aus dem das Array besteht.'
  3. 'Multiplizieren Sie die Größe des Typs mit dem Index'
  4. 'füge das Ergebnis zur Adresse des Arrays hinzu'
  5. 'Lesen Sie, was an der resultierenden Adresse ist'

Das Ergebnis adressiert etwas, das Teil des Arrays sein kann oder nicht. Als Gegenleistung für die rasante Geschwindigkeit von Maschinenanweisungen verlieren Sie das Sicherheitsnetz des Computers, der die Dinge für Sie überprüft. Wenn Sie akribisch und vorsichtig sind, ist das kein Problem. Wenn Sie schlampig sind oder einen Fehler machen, werden Sie verbrannt. Manchmal kann eine ungültige Anweisung generiert werden, die eine Ausnahme verursacht, manchmal nicht.

Jay
quelle
0

Ein netter Ansatz, den ich oft gesehen habe und den ich tatsächlich verwendet habe, ist das Einfügen eines Elements vom Typ NULL (oder eines erstellten Elements uint THIS_IS_INFINITY = 82862863263;) am Ende des Arrays.

Bei der Überprüfung der Schleifenbedingungen TYPE *pagesWordsbefindet sich dann eine Art Zeigerarray:

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;
  }
}

Diese Lösung sagt nichts, wenn das Array mit structTypen gefüllt ist .

xudre
quelle
0

Wie jetzt in der Frage erwähnt, löst die Verwendung von std :: vector :: at das Problem und führt vor dem Zugriff eine gebundene Prüfung durch.

Wenn Sie ein Array mit konstanter Größe benötigen, das sich als erster Code auf dem Stapel befindet, verwenden Sie den neuen C ++ 11-Container std :: array. Als Vektor gibt es die Funktion std :: array :: at. Tatsächlich existiert die Funktion in allen Standardcontainern, in denen sie eine Bedeutung hat, dh wo Operator [] definiert ist: (deque, map, unordered_map) mit Ausnahme von std :: bitset, in dem sie std :: bitset heißt: :Prüfung.

Mohamed El-Nakib
quelle
0

libstdc ++, das Teil von gcc ist, verfügt über einen speziellen Debug-Modus zur Fehlerprüfung. Es wird durch das Compiler-Flag aktiviert -D_GLIBCXX_DEBUG. Unter anderem wird std::vectorauf Kosten der Leistung geprüft . Hier ist eine Online-Demo mit der neuesten Version von gcc.

Sie können also im libstdc ++ - Debug-Modus Grenzen überprüfen, dies sollten Sie jedoch nur beim Testen tun, da dies im Vergleich zum normalen libstdc ++ - Modus eine bemerkenswerte Leistung kostet.

ks1322
quelle
0

Wenn Sie Ihr Programm leicht ändern:

#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;
}

(Änderungen in Großbuchstaben - setzen Sie diese in Kleinbuchstaben, wenn Sie dies versuchen möchten.)

Sie werden sehen, dass die Variable foo verworfen wurde. Ihr Code wird Werte speichern in die nicht vorhandene Array [3] und array [4], und in der Lage sein , sie richtig zu holen, aber die tatsächliche Speicherung verwendet wird aus seinen foo .

Sie können also "davonkommen", indem Sie die Grenzen des Arrays in Ihrem ursprünglichen Beispiel überschreiten, jedoch auf Kosten der Verursachung von Schäden an anderer Stelle - Schäden, die sich als sehr schwer zu diagnostizieren erweisen können .

Warum es keine automatische Begrenzungsprüfung gibt - ein korrekt geschriebenes Programm benötigt es nicht. Sobald dies geschehen ist, gibt es keinen Grund, die Laufzeitgrenzen zu überprüfen, und dies würde das Programm nur verlangsamen. Am besten, Sie finden das alles beim Entwerfen und Codieren heraus.

C ++ basiert auf C, das so konzipiert wurde, dass es der Assemblersprache so nahe wie möglich kommt.

Jennifer
quelle