Ich habe vor kurzem angefangen, C zu lernen und nehme eine Klasse mit C als Fach. Ich spiele gerade mit Loops herum und stoße auf ein merkwürdiges Verhalten, das ich nicht erklären kann.
#include <stdio.h>
int main()
{
int array[10],i;
for (i = 0; i <=10 ; i++)
{
array[i]=0; /*code should never terminate*/
printf("test \n");
}
printf("%d \n", sizeof(array)/sizeof(int));
return 0;
}
Auf meinem Laptop mit Ubuntu 14.04 bricht dieser Code nicht. Es läuft bis zur Fertigstellung. Auf dem Computer meiner Schule mit CentOS 6.6 läuft es auch einwandfrei. Unter Windows 8.1 wird die Schleife nie beendet.
Noch seltsamer ist, dass der Code nur auf meinem Laptop mit Ubuntu endet , wenn ich den Zustand der for
Schleife auf: bearbeite i <= 11
. Es wird niemals in CentOS und Windows beendet.
Kann jemand erklären, was im Speicher passiert und warum die verschiedenen Betriebssysteme, auf denen derselbe Code ausgeführt wird, unterschiedliche Ergebnisse liefern?
EDIT: Ich weiß, dass die for-Schleife außerhalb der Grenzen liegt. Ich mache es absichtlich. Ich kann einfach nicht herausfinden, wie unterschiedlich das Verhalten zwischen verschiedenen Betriebssystemen und Computern sein kann.
i
direkt nach dem Ende von gespeichertarray
wird und Sie sie mit überschreibenarray[10]=0;
. Dies ist möglicherweise nicht der Fall bei einem optimierten Build auf derselben Plattform, der möglicherweisei
in einem Register gespeichert ist und niemals im Speicher darauf verweist.Antworten:
Sie haben gerade das Stampfen von Erinnerungen entdeckt. Hier können Sie mehr darüber lesen: Was ist ein „Memory Stomp“?
Wenn Sie zuweisen
int array[10],i;
, werden diese Variablen in den Speicher verschoben (insbesondere werden sie auf dem Stapel zugewiesen, der ein mit der Funktion verknüpfter Speicherblock ist).array[]
undi
sind wahrscheinlich nebeneinander im Speicher. Es scheint, dass sich unter Windows 8.1i
bei befindetarray[10]
. Auf CentOSi
befindet sich beiarray[11]
. Und unter Ubuntu ist es an keiner Stelle (vielleicht anarray[-1]
?).Versuchen Sie, diese Debugging-Anweisungen zu Ihrem Code hinzuzufügen. Sie sollten beachten, dass bei Iteration 10 oder 11
array[i]
aufi
.quelle
array[10]
den Stapelrahmen zu zerstören. Wie kann es einen Unterschied zwischen Code mit oder ohne Debugging-Ausgabe geben? Wenn die Adressei
nicht benötigt wird, der Compiler kann optimiereni
entfernt. in ein Register, wodurch das Speicherlayout auf dem Stapel geändert wird ...array[10]=0
. Wenn Sie Ihren Code mit aktivierter Optimierung kompiliert haben, würde dies wahrscheinlich nicht passieren (weil C dies getan hat) Aliasing-Regeln, die einschränken, welche Arten von Speicherzugriffen möglicherweise den anderen Speicher überlappen. Als lokale Variable, deren Adresse Sie nie annehmen, sollte ein Compiler meiner Meinung nach davon ausgehen können, dass nichts sie aliasisiert. Wie auch immer, schreiben Sie das Ende ab eines Arrays ist undefiniertes Verhalten. Versuchen Sie immer, dies zu vermeiden.Der Fehler liegt zwischen diesen Codeteilen:
Da es
array
nur 10 Elemente gibt, ist in der letzten Iterationarray[10] = 0;
ein Pufferüberlauf. Pufferüberläufe sind UNDEFINIERTES VERHALTEN , was bedeutet, dass sie Ihre Festplatte formatieren oder dazu führen können, dass Dämonen aus Ihrer Nase fliegen.Es ist ziemlich üblich, dass alle Stapelvariablen nebeneinander angeordnet werden. Wenn
i
befindet, in demarray[10]
Schreiben auf, dann ist die UB zurückgesetzt wird ,i
um0
so zur ungekündigten Schleife führt.Ändern Sie zum Beheben die Schleifenbedingung in
i < 10
.quelle
rm -rf /
auch wenn Sie nicht root sind, nicht Natürlich das gesamte Laufwerk "formatieren", aber trotzdem alle Ihre Daten zerstören. Autsch.In dem letzten Durchlauf der Schleife, in den Sie schreiben
array[10]
, befinden sich jedoch nur 10 Elemente im Array mit den Nummern 0 bis 9. Die C-Sprachspezifikation besagt, dass dies „undefiniertes Verhalten“ ist. In der Praxis bedeutet dies, dass Ihr Programm versucht, in dasint
unmittelbar darauf liegendearray
Speicherelement zu schreiben . Was dann passiert, hängt davon ab, was tatsächlich dort liegt, und dies hängt nicht nur vom Betriebssystem ab, sondern vor allem vom Compiler, von den Compileroptionen (wie Optimierungseinstellungen), von der Prozessorarchitektur und vom umgebenden Code usw. Es kann sogar von Ausführung zu Ausführung variieren, z. B. aufgrund der Adressraum-Randomisierung (wahrscheinlich nicht in diesem Spielzeugbeispiel, aber es kommt im wirklichen Leben vor). Einige Möglichkeiten umfassen:i
. Die Schleife wird nie beendet, da siei
bei 0 neu gestartet wird.array
sich direkt am Ende einer virtuellen Speicherseite befindet und die nächste Seite nicht zugeordnet ist.Was Sie unter Windows beobachtet haben, war, dass der Compiler beschlossen hat, die Variable
i
unmittelbar nach dem Array im Speicher abzulegen, und siearray[10] = 0
schließlich zugewiesen hati
. Unter Ubuntu und CentOS wurde der Compiler dort nicht platzierti
. Fast alle C-Implementierungen gruppieren lokale Variablen im Speicher auf einem Speicherstapel , mit einer Hauptausnahme: Einige lokale Variablen können vollständig in Registern abgelegt werden . Selbst wenn sich die Variable auf dem Stapel befindet, wird die Reihenfolge der Variablen vom Compiler festgelegt und kann nicht nur von der Reihenfolge in der Quelldatei, sondern auch von deren Typen abhängen (um zu vermeiden, dass Speicher für Ausrichtungsbeschränkungen verschwendet wird, die Löcher hinterlassen würden). , auf ihren Namen, auf einem Hash-Wert, der in der internen Datenstruktur eines Compilers verwendet wird, usw.Wenn Sie herausfinden möchten, was Ihr Compiler beschlossen hat, können Sie ihm sagen, dass er Ihnen den Assembler-Code anzeigen soll. Oh, und lernen Sie, Assembler zu entschlüsseln (es ist einfacher als es zu schreiben). Übergeben Sie mit GCC (und einigen anderen Compilern, insbesondere in der Unix-Welt) die Option
-S
, Assembler-Code anstelle einer Binärdatei zu erstellen. Hier ist beispielsweise das Assembler-Snippet für die Schleife beim Kompilieren mit GCC auf amd64 mit der Optimierungsoption-O0
(keine Optimierung), wobei Kommentare manuell hinzugefügt wurden:Hier befindet sich die Variable
i
52 Byte unter dem oberen Rand des Stapels, während das Array 48 Byte unter dem oberen Rand des Stapels beginnt. Dieser Compiler hat sich alsoi
direkt vor dem Array platziert. Sie würden überschreiben,i
wenn Sie zufällig schreiben würdenarray[-1]
. Wenn Sie ändernarray[i]=0
zuarray[9-i]=0
, werden Sie eine Endlosschleife auf dieser speziellen Plattform mit diesen speziellen Compiler - Optionen erhalten.Jetzt kompilieren wir Ihr Programm mit
gcc -O1
.Das ist kürzer! Der Compiler hat nicht nur abgelehnt, einen Stapelspeicherort zuzuweisen
i
- er wird immer nur im Register gespeichertebx
-, sondern er hat sich auch nicht die Mühe gemacht, Speicher zuzuweisenarray
oder Code zum Festlegen seiner Elemente zu generieren, da er festgestellt hat, dass keines der Elemente vorhanden ist werden immer benutzt.Um dieses Beispiel aussagekräftiger zu gestalten, stellen wir sicher, dass die Array-Zuweisungen ausgeführt werden, indem wir dem Compiler etwas zur Verfügung stellen, das er nicht optimieren kann. Eine einfache Möglichkeit, dies zu tun, besteht darin, das Array aus einer anderen Datei zu verwenden. Aufgrund der separaten Kompilierung weiß der Compiler nicht, was in einer anderen Datei passiert (es sei denn, er optimiert zum Zeitpunkt der Verknüpfung, was
gcc -O0
odergcc -O1
nicht). Erstellen Sie eine Quelldateiuse_array.c
mitund ändern Sie Ihren Quellcode in
Kompilieren mit
Diesmal sieht der Assembler-Code folgendermaßen aus:
Jetzt befindet sich das Array auf dem Stapel, 44 Bytes von oben. Was ist mit
i
? Es erscheint nirgendwo! Der Schleifenzähler bleibt jedoch im Registerrbx
. Es ist nicht genaui
, aber die Adresse derarray[i]
. Der Compiler hat entschieden, dassi
es keinen Sinn macht, eine Arithmetik durchzuführen, um zu berechnen, wo bei jedem Durchlauf der Schleife 0 gespeichert werden soll , da der Wert von nie direkt verwendet wurde. Stattdessen ist diese Adresse die Schleifenvariable, und die Arithmetik zum Bestimmen der Grenzen wurde teilweise zur Kompilierungszeit (multiplizieren Sie 11 Iterationen mit 4 Bytes pro Arrayelement, um 44 zu erhalten) und teilweise zur Laufzeit ausgeführt, jedoch ein für alle Mal, bevor die Schleife beginnt ( Führen Sie eine Subtraktion durch, um den Anfangswert zu erhalten.Selbst in diesem sehr einfachen Beispiel haben wir gesehen, wie das Ändern von Compileroptionen (Aktivierung aktivieren) oder das Ändern von geringfügigen (
array[i]
bisarray[9-i]
) oder sogar das Ändern von scheinbar nicht verwandten Elementen (Hinzufügen des Aufrufs zuuse_array
) einen signifikanten Unterschied zu dem bewirken kann, was das ausführbare Programm generiert hat vom Compiler tut. Compiler-Optimierungen können viele Dinge bewirken, die für Programme, die undefiniertes Verhalten aufrufen, möglicherweise nicht intuitiv erscheinen . Deshalb bleibt undefiniertes Verhalten völlig undefiniert. Wenn Sie in realen Programmen geringfügig von den Spuren abweichen, kann es selbst für erfahrene Programmierer sehr schwierig sein, die Beziehung zwischen dem, was der Code tut, und dem, was er hätte tun sollen, zu verstehen.quelle
Im Gegensatz zu Java führt C keine Überprüfung der Array-Grenzen durch, dh es gibt keine
ArrayIndexOutOfBoundsException
. Die Aufgabe, sicherzustellen, dass der Array-Index gültig ist, bleibt dem Programmierer überlassen. Dies absichtlich zu tun führt zu undefiniertem Verhalten, alles könnte passieren.Für ein Array:
Indizes sind nur im Bereich
0
bis gültig9
. Sie versuchen jedoch:Zugriff
array[10]
hier, ändern Sie die Bedingung ini < 10
quelle
Sie haben eine Grenzverletzung, und auf den nicht terminierenden Plattformen setzen Sie
i
am Ende der Schleife versehentlich auf Null, sodass sie erneut beginnt.array[10]
ist ungültig; es enthält 10 Elementearray[0]
durcharray[9]
undarray[10]
ist das 11 .. Ihre Schleife sollte wie folgt geschrieben werden, um vorher anzuhalten10
:Wo
array[10]
Länder auf zwei Ihrer Plattformen implementierungsdefiniert und amüsanterweise definiert sind, landet es aufi
diesen Plattformen, auf denen diese Plattformen anscheinend direkt danach liegenarray
.i
wird auf Null gesetzt und die Schleife wird für immer fortgesetzt. Für Ihre anderen Plattformeni
kann sich vorher befindenarray
oderarray
dass sie danach gepolstert sind.quelle
Sie erklären
int array[10]
Mittelarray
Index0
zu9
(insgesamt10
ganzzahligen Elemente es halten kann). Aber die folgende Schleife,wird Schleife
0
zu10
bedeutet11
Zeit. Daher, wenni = 10
der Puffer überläuft und undefiniertes Verhalten verursacht .Versuchen Sie Folgendes:
oder,
quelle
Es ist undefiniert bei
array[10]
und gibt undefiniertes Verhalten wie zuvor beschrieben. Denken Sie so darüber nach:Ich habe 10 Artikel in meinem Einkaufswagen. Sie sind:
0: Eine Schachtel Müsli
1: Brot
2: Milch
3: Kuchen
4: Eier
5: Kuchen
6: 2 Liter Soda
7: Salat
8: Burger
9: Eis
cart[10]
ist undefiniert und kann in einigen Compilern eine Ausnahme außerhalb der Grenzen geben. Aber viele anscheinend nicht. Der scheinbare 11. Artikel ist ein Artikel, der sich nicht im Warenkorb befindet. Der 11. Punkt zeigt auf einen "Poltergeist-Gegenstand", wie ich ihn nennen werde. Es existierte nie, aber es war da.Warum einige Compiler
i
einen Index vonarray[10]
oderarray[11]
oder sogararray[-1]
angeben, liegt an Ihrer Initialisierungs- / Deklarationsanweisung. Einige Compiler interpretieren dies als:int
s fürarray[10]
und einen weiterenint
Block zu. Um dies zu vereinfachen, legen Sie sie direkt nebeneinander."array[10]
nicht darauf hinweisti
.i
zuarray[-1]
(weil ein Index eines Arrays nicht negativ sein kann oder sollte) oder ordnen Sie es an einer völlig anderen Stelle zu, weil das Betriebssystem damit umgehen kann und es sicherer ist.Einige Compiler möchten, dass die Dinge schneller gehen, und einige Compiler bevorzugen Sicherheit. Es geht nur um den Kontext. Wenn ich zum Beispiel eine App für das alte BREW-Betriebssystem (das Betriebssystem eines Basistelefons) entwickeln würde, würde es mich nicht um die Sicherheit kümmern. Wenn ich für ein iPhone 6 entwickeln würde, könnte es schnell laufen, egal was passiert, also würde ich einen Schwerpunkt auf Sicherheit legen müssen. (Im Ernst, haben Sie die App Store-Richtlinien von Apple gelesen oder sich über die Entwicklung von Swift und Swift 2.0 informiert?)
quelle
Da Sie ein Array der Größe 10 erstellt haben, sollte die Schleifenbedingung wie folgt lauten:
Derzeit versuchen Sie, mithilfe des Speichers auf den nicht zugewiesenen Speicherort zuzugreifen,
array[10]
und dies verursacht das undefinierte Verhalten . Undefiniertes Verhalten bedeutet, dass sich Ihr Programm unbestimmt verhält, sodass es bei jeder Ausführung unterschiedliche Ausgaben liefern kann.quelle
Nun, der C-Compiler sucht traditionell nicht nach Grenzen. Sie können einen Segmentierungsfehler erhalten, wenn Sie sich auf einen Ort beziehen, der nicht zu Ihrem Prozess gehört. Die lokalen Variablen werden jedoch auf dem Stapel zugewiesen. Abhängig von der Art und Weise, wie der Speicher zugewiesen wird, kann der Bereich direkt hinter dem Array (
array[10]
) zum Speichersegment des Prozesses gehören. Somit wird keine Segmentierungsfehlerfalle ausgelöst, und das scheinen Sie zu erleben. Wie andere bereits betont haben, ist dies ein undefiniertes Verhalten in C, und Ihr Code kann als fehlerhaft angesehen werden. Da Sie C lernen, sollten Sie sich besser angewöhnen, in Ihrem Code nach Grenzen zu suchen.quelle
Abgesehen von der Möglichkeit, dass der Speicher so angelegt ist, dass beim Versuch,
a[10]
tatsächlich zu schreiben, überschrieben wirdi
, kann ein optimierender Compiler möglicherweise auch feststellen, dass der Schleifentest nicht mit einem Wert voni
mehr als zehn erreicht werden kann, ohne dass der Code zuvor auf den Code zugegriffen hat nicht vorhandenes Array-Elementa[10]
.Da ein Versuch, auf dieses Element zuzugreifen, ein undefiniertes Verhalten wäre, hätte der Compiler keine Verpflichtungen hinsichtlich dessen, was das Programm nach diesem Zeitpunkt tun könnte. Da der Compiler in keinem Fall verpflichtet wäre, Code zur Überprüfung des Schleifenindex zu generieren, wenn er größer als zehn sein könnte, wäre er insbesondere nicht verpflichtet, Code zu generieren, um ihn überhaupt zu überprüfen. es könnte stattdessen angenommen werden, dass der
<=10
Test immer wahr ergibt. Beachten Sie, dass dies auch dann zutrifft, wenn der Code ihna[10]
eher liest als schreibt.quelle
Wenn Sie vorbei iterieren
i==9
, weisen Sie den 'Array-Elementen', die sich tatsächlich hinter dem Array befinden, Null zu , sodass Sie einige andere Daten überschreiben. Höchstwahrscheinlich überschreiben Sie diei
Variable, die sich danach befindeta[]
. Auf diese Weise setzen Sie die Variable einfach auf Null zurücki
und starten so die Schleife neu.Sie könnten das selbst entdecken, wenn Sie
i
in der Schleife drucken :statt nur
Natürlich hängt dieses Ergebnis stark von der Speicherzuordnung für Ihre Variablen ab, die wiederum von einem Compiler und seinen Einstellungen abhängt. Daher handelt es sich im Allgemeinen um undefiniertes Verhalten. Aus diesem Grund können die Ergebnisse auf verschiedenen Computern oder verschiedenen Betriebssystemen oder auf verschiedenen Compilern unterschiedlich sein.
quelle
Der Fehler liegt im Teilarray [10]. w / c ist auch die Adresse von i (int array [10], i;). Wenn Array [10] auf 0 gesetzt ist, würde i 0 sein. w / c setzt die gesamte Schleife zurück und verursacht die Endlosschleife. Es wird eine Endlosschleife geben, wenn Array [10] zwischen 0 und 10 liegt. Die richtige Schleife sollte für (i = 0; i <10; i ++) {...} int Array [10], i; für (i = 0; i <= 10; i ++) Array [i] = 0;
quelle
Ich werde etwas vorschlagen, das ich oben nicht finde:
Versuchen Sie, Array [i] = 20 zuzuweisen.
Ich denke, dies sollte den Code überall beenden. (Vorausgesetzt, Sie behalten i <= 10 oder ll)
Wenn dies ausgeführt wird, können Sie fest entscheiden, dass die hier angegebenen Antworten bereits korrekt sind [die Antwort bezieht sich beispielsweise auf das Stampfen des Speichers].
quelle
Hier sind zwei Dinge falsch. Das int i ist tatsächlich ein Array-Element, Array [10], wie auf dem Stapel zu sehen. Da Sie der Indizierung erlaubt haben, Array [10] = 0 zu machen, wird der Schleifenindex i niemals 10 überschreiten. Machen Sie es
for(i=0; i<10; i+=1)
.i ++ ist, wie K & R es nennen würde, "schlechter Stil". Es erhöht i um die Größe von i, nicht um 1. i ++ steht für Zeigermathematik und i + = 1 steht für Algebra. Dies hängt zwar vom Compiler ab, ist jedoch keine gute Konvention für die Portabilität.
quelle
i
ist NOTan Array-Elementa[10]
, es gibt keine Verpflichtung oder sogar einen Vorschlag für einen Compiler, es unmittelbar danach auf den Stapel zu legena[]
- es kann auch vor dem Array platziert oder mit etwas zusätzlichem Speicherplatz getrennt werden. Es könnte sogar außerhalb des Hauptspeichers zugewiesen werden, beispielsweise in einem CPU-Register. Es ist auch falsch, dass++
für Zeiger und nicht für ganze Zahlen. Völlig falsch ist 'i ++ erhöht i um die Größe von i' - lesen Sie die Operatorbeschreibung in der Sprachdefinition!i
- sie ist vomint
Typ. Es ist eine ganze Zahl , kein Zeiger; eine Ganzzahl, die als Index für diearray
,.