it-swarm.com.de

Warum wird dies für den Loop-Exit auf einigen Plattformen und nicht auf anderen ausgeführt?

Ich habe vor kurzem angefangen, C zu lernen, und ich nehme eine Klasse mit C als das Thema. Momentan spiele ich mit Loops herum und stoße auf 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 funktioniert dieser Code nicht. Es läuft bis zur Fertigstellung. Auf dem Computer meiner Schule, auf dem CentOS 6.6 ausgeführt wird, läuft es ebenfalls einwandfrei. Unter Windows 8.1 wird die Schleife niemals beendet.

Was noch seltsamer ist, ist, dass wenn ich den Zustand der for Schleife bearbeite: i <= 11, der Code wird nur auf meinem Laptop mit Ubuntu beendet. In CentOS und Windows wird es nie 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 nur nicht herausfinden, wie das Verhalten auf verschiedenen Betriebssystemen und Computern unterschiedlich sein kann.

240
JonCav

Auf meinem Laptop mit Ubuntu 14.04 funktioniert dieser Code nicht vollständig. Auf dem Computer meiner Schule, auf dem CentOS 6.6 ausgeführt wird, läuft es ebenfalls einwandfrei. Unter Windows 8.1 wird die Schleife niemals beendet.

Seltsamer ist es, wenn ich die Bedingung der for -Schleife zu i <= 11 Bearbeite. Der Code wird nur auf meinem Laptop mit Ubuntu beendet. CentOS und Windows werden niemals beendet.

Sie haben gerade das Gedächtnisstampfen entdeckt. Sie können hier mehr darüber lesen: Was ist ein "Memory Stomp"?

Wenn Sie int array[10],i; Zuweisen, werden diese Variablen im Speicher abgelegt (insbesondere auf dem Stapel, der ein der Funktion zugeordneter Speicherblock ist). array[] Und i liegen im Speicher wahrscheinlich nebeneinander. Es scheint, dass sich unter Windows 8.1 i unter array[10] Befindet. Unter CentOS befindet sich i unter array[11]. Und auf Ubuntu ist es an keiner Stelle (vielleicht bei array[-1]?).

Versuchen Sie, diese Debuganweisungen zu Ihrem Code hinzuzufügen. Sie sollten beachten, dass bei Iteration 10 oder 11 array[i] Auf i zeigt.

#include <stdio.h>

int main() 
{ 
  int array[10],i; 

  printf ("array: %p, &i: %p\n", array, &i); 
  printf ("i is offset %d from array\n", &i - array);

  for (i = 0; i <=11 ; i++) 
  { 
    printf ("%d: Writing 0 to address %p\n", i, &array[i]); 
    array[i]=0; /*code should never terminate*/ 
  } 
  return 0; 
} 
356
QuestionC

Der Fehler liegt zwischen diesen Codeteilen:

int array[10],i;

for (i = 0; i <=10 ; i++)

array[i]=0;

Da array nur 10 Elemente enthält, wird in der letzten Iteration array[10] = 0; ist ein Pufferüberlauf. Pufferüberläufe sind NDEFINED BEHAVIOR, was bedeutet, dass sie Ihre Festplatte formatieren oder Dämonen aus Ihrer Nase fliegen lassen können.

Es ist ziemlich üblich, dass alle Stapelvariablen nebeneinander angeordnet sind. Wenn i sich befindet, wo array[10] schreibt an, dann setzt die UB i auf 0, was zur nicht abgeschlossenen Schleife führt.

Um dies zu beheben, ändern Sie die Schleifenbedingung in i < 10.

98
o11c

Im letzten Durchlauf der Schleife schreiben Sie nach array[10], Aber das Array enthält nur 10 Elemente 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 den Speicher mit der Größe int zu schreiben, der unmittelbar nach array im Speicher liegt. Was dann passiert, hängt davon ab, was tatsächlich dort liegt, und dies hängt nicht nur vom Betriebssystem, sondern auch vom Compiler, von den Compileroptionen (wie den Optimierungseinstellungen), von der Prozessorarchitektur und vom umgebenden Code ab usw. Es kann sogar von Ausführung zu Ausführung variieren, z aufgrund von Adressraum Randomisierung (wahrscheinlich nicht in diesem Spielzeug Beispiel, aber es passiert im wirklichen Leben). Einige Möglichkeiten umfassen:

  • Die Location wurde nicht genutzt. Die Schleife wird normal beendet.
  • Die Position wurde für etwas verwendet, das zufällig den Wert 0 hatte. Die Schleife wird normal beendet.
  • Der Speicherort enthielt die Absenderadresse der Funktion. Die Schleife wird normal beendet, aber dann stürzt das Programm ab, weil es versucht, zur Adresse 0 zu springen.
  • Der Speicherort enthält die Variable i. Die Schleife wird nie beendet, da i bei 0 neu gestartet wird.
  • Der Speicherort enthält eine andere Variable. Die Schleife wird normal beendet, aber dann passieren „interessante“ Dinge.
  • Der Ort ist eine ungültige Speicheradresse, z. weil array direkt am Ende einer virtuellen Speicherseite steht und die nächste Seite nicht zugeordnet ist.
  • Dämonen fliegen aus deiner Nase . Glücklicherweise fehlt den meisten Computern die erforderliche Hardware.

Unter Windows haben Sie festgestellt, dass der Compiler beschlossen hat, die Variable i unmittelbar nach dem Array im Speicher abzulegen, sodass array[10] = 0 Schließlich i zugewiesen wurde. Unter Ubuntu und CentOS hat der Compiler i nicht dort abgelegt. Fast alle C-Implementierungen gruppieren lokale Variablen im Speicher auf einem Speicherstapel , mit einer wichtigen Ausnahme: Einige lokale Variablen können vollständig in Register platziert werden. Auch wenn sich die Variable im Stapel befindet, wird die Reihenfolge der Variablen vom Compiler festgelegt. Dies hängt möglicherweise nicht nur von der Reihenfolge in der Quelldatei, sondern auch von deren Typ ab (um zu vermeiden, dass Speicherplatz für Ausrichtungsbeschränkungen verschwendet wird, die Löcher hinterlassen würden). nach ihren Namen nach einem in der internen Datenstruktur eines Compilers verwendeten Hash-Wert usw.

Wenn Sie herausfinden möchten, wofür sich Ihr Compiler entschieden hat, können Sie ihn anweisen, Ihnen den Assembler-Code anzuzeigen. Oh, und lerne Assembler zu entziffern (es ist einfacher als es zu schreiben). Übergeben Sie bei GCC (und einigen anderen Compilern, insbesondere in der Unix-Welt) die Option -S, Um Assembler-Code anstelle einer Binärdatei zu erzeugen. Hier ist zum Beispiel das Assembler-Snippet für die Schleife aus der Kompilierung mit GCC auf AMD64 mit der Optimierungsoption -O0 (Keine Optimierung), wobei Kommentare manuell hinzugefügt wurden:

.L3:
    movl    -52(%rbp), %eax           ; load i to register eax
    cltq
    movl    $0, -48(%rbp,%rax,4)      ; set array[i] to 0
    movl    $.LC0, %edi
    call    puts                      ; printf of a constant string was optimized to puts
    addl    $1, -52(%rbp)             ; add 1 to i
.L2:
    cmpl    $10, -52(%rbp)            ; compare i to 10
    jle     .L3

Hier ist die Variable i 52 Bytes unterhalb des Stapels, während das Array 48 Bytes unterhalb des Stapels beginnt. Dieser Compiler hat also i zufällig direkt vor dem Array platziert. Sie würden i überschreiben, wenn Sie zufällig an array[-1] schreiben würden. Wenn Sie array[i]=0 In array[9-i]=0 Ändern, erhalten Sie eine Endlosschleife auf dieser bestimmten Plattform mit diesen bestimmten Compileroptionen.

Jetzt kompilieren wir Ihr Programm mit gcc -O1.

    movl    $11, %ebx
.L3:
    movl    $.LC0, %edi
    call    puts
    subl    $1, %ebx
    jne     .L3

Das ist kürzer! Der Compiler hat es nicht nur abgelehnt, i einen Stack-Speicherort zuzuweisen - er wird immer nur im Register ebx gespeichert -, sondern er hat sich auch nicht die Mühe gemacht, array Speicher zuzuweisen. , oder um Code zum Setzen seiner Elemente zu generieren, da festgestellt wurde, dass keines der Elemente jemals verwendet wird.

Um dieses Beispiel aussagekräftiger zu gestalten, sollten Sie sicherstellen, dass die Array-Zuweisungen ausgeführt werden, indem Sie dem Compiler etwas zur Verfügung stellen, das nicht optimiert werden kann. Eine einfache Möglichkeit besteht darin, das Array aus einer anderen Datei zu verwenden. Aufgrund der getrennten Kompilierung weiß der Compiler nicht, was in einer anderen Datei geschieht (es sei denn, es wird zur Verbindungszeit optimiert, mit gcc -O0 Oder gcc -O1 Nicht). Erstellen Sie eine Quelldatei use_array.c Mit

void use_array(int *array) {}

und ändern Sie Ihren Quellcode in

#include <stdio.h>
void use_array(int *array);

int main()
{
  int array[10],i;

  for (i = 0; i <=10 ; i++)
  {
    array[i]=0; /*code should never terminate*/
    printf("test \n");

  }
  printf("%zd \n", sizeof(array)/sizeof(int));
  use_array(array);
  return 0;
}

Kompilieren mit

gcc -c use_array.c
gcc -O1 -S -o with_use_array1.c with_use_array.c use_array.o

Diesmal sieht der Assembler-Code so aus:

    movq    %rsp, %rbx
    leaq    44(%rsp), %rbp
.L3:
    movl    $0, (%rbx)
    movl    $.LC0, %edi
    call    puts
    addq    $4, %rbx
    cmpq    %rbp, %rbx
    jne     .L3

Jetzt befindet sich das Array auf dem Stapel, 44 Byte von oben. Was ist mit i? Es erscheint nirgendwo! Der Schleifenzähler wird jedoch im Register rbx gespeichert. Es ist nicht genau i, sondern die Adresse des array[i]. Der Compiler hat entschieden, dass, da der Wert von i nie direkt verwendet wurde, es keinen Grund gab, eine Arithmetik durchzuführen, um zu berechnen, wo 0 während jedes Durchlaufs der Schleife gespeichert werden soll. Stattdessen ist diese Adresse die Schleifenvariable, und die Arithmetik zur Bestimmung der Grenzen wurde teilweise zur Kompilierungszeit ausgeführt (11 Iterationen mit 4 Bytes pro Arrayelement multiplizieren, um 44 zu erhalten) und teilweise zur Laufzeit, jedoch ein für alle Mal, bevor die Schleife startet ( Subtraktion durchführen, um den Anfangswert zu erhalten).

Sogar in diesem sehr einfachen Beispiel haben wir gesehen, wie Compiler-Optionen geändert (Optimierung einschalten) oder geringfügige Änderungen (array[i] In array[9-i]) Oder sogar scheinbar nicht zusammenhängende Änderungen (Aufruf hinzufügen) vorgenommen wurden use_array) Kann einen wesentlichen Unterschied zu dem machen, was das vom Compiler erzeugte ausführbare Programm macht. Compiler-Optimierungen können eine Menge 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 Tracks abweichen, kann es sehr schwierig sein, die Beziehung zwischen der Funktion des Codes und der Leistung zu verstehen, die er selbst für erfahrene Programmierer hätte haben müssen.

38
Gilles

Im Gegensatz zu Java führt C keine Überprüfung der Array-Grenzen durch, d. H. Es gibt kein 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 kann passieren.


Für ein Array:

int array[10]

indizes sind nur im Bereich 0 bis 9 gültig. Sie versuchen jedoch:

for (i = 0; i <=10 ; i++)

greifen Sie hier auf array[10] zu, ändern Sie die Bedingung in i < 10.

25
Yu Hao

Sie haben eine Grenzverletzung und auf den nicht terminierenden Plattformen setzen Sie i am Ende der Schleife versehentlich auf Null, so dass die Schleife von vorne beginnt.

array[10] ist ungültig; es enthält 10 Elemente, array[0] durch array[9], und array[10] ist der 11.. Deine Schleife sollte so geschrieben sein, dass sie stoppt vorher10, wie folgt:

for (i = 0; i < 10; i++)

Woher array[10]lands ist implementierungsdefiniert, und amüsanterweise landet es auf zwei Ihrer Plattformen auf i, die offenbar direkt nach array angeordnet sind. i wird auf Null gesetzt und die Schleife wird für immer fortgesetzt. Auf Ihren anderen Plattformen befindet sich i möglicherweise vor array, oder array ist möglicherweise etwas aufgefüllt.

19
Derek T. Jones

Sie erklären int array[10] bedeutet array hat Index 0 bis 9 (gesamt 10 Integer-Elemente, die es aufnehmen kann). Aber die folgende Schleife,

for (i = 0; i <=10 ; i++)

wird Schleife 0 bis 10 meint 11 Zeit. Daher, wenn i = 10 Überläuft den Puffer und verursacht ndefiniertes Verhalten .

Also versuche folgendes:

for (i = 0; i < 10 ; i++)

oder,

for (i = 0; i <= 9 ; i++)
12
rakeb.mazharul

Es ist undefiniert bei array[10] Und gibt ndefiniertes Verhalten wie zuvor beschrieben. Überleg es dir so:

Ich habe 10 Artikel in meinem Einkaufswagen. Sie sind:

0: Eine Schachtel Müsli
1: Brot
2: Milch
3: Pie
4 Eier
5: Kuchen
6: 2 Liter Soda
7: Salat
8: Burger
9: Eis

cart[10] Ist undefiniert und kann in einigen Compilern eine Ausnahmebedingung darstellen. Aber eine Menge anscheinend nicht. Der scheinbare 11. Gegenstand ist ein Gegenstand, der nicht tatsächlich im Einkaufswagen Der 11. Gegenstand zeigt auf einen "Poltergeist-Gegenstand", wie ich ihn nennen werde. Es hat nie existiert, aber es war da.

Warum einige Compiler i einen Index von array[10] Oder array[11] Oder sogar array[-1] Geben, liegt an Ihrer Initialisierungs-/Deklarationsanweisung. Einige Compiler interpretieren dies als:

  • "Ordnen Sie 10 Blöcke von ints für array[10] Und einen anderen int -Block zu. m es einfacher zu machen Stellen Sie sie direkt nebeneinander."
  • Das Gleiche wie zuvor, aber verschieben Sie es ein oder zwei Leerzeichen, damit array[10] Nicht auf i zeigt.
  • Tun Sie dasselbe wie zuvor, aber weisen Sie i bei array[-1] Zu (da ein Index eines Arrays nicht negativ sein kann oder sollte), oder weisen Sie ihn an einer völlig anderen Stelle zu, weil das Betriebssystem kann damit umgehen, und es ist sicherer.

Einige Compiler möchten, dass die Dinge schneller gehen, und andere bevorzugen Sicherheit. Es dreht sich alles um den Kontext. Wenn ich zum Beispiel eine App für das alte BREW-Betriebssystem (das Betriebssystem eines Basistelefons) entwickeln würde, würde die Sicherheit keine Rolle spielen. Wenn ich für ein iPhone 6 entwickeln würde, könnte es schnell laufen, egal was passiert. Daher würde ich einen Schwerpunkt auf Sicherheit legen. (Im Ernst, haben Sie Apples App Store-Richtlinien gelesen oder sich über die Entwicklung von Swift und Swift 2.0?)

7
DDPWNAGE

Da Sie ein Array der Größe 10 erstellt haben, sollte für die Schleifenbedingung Folgendes gelten:

int array[10],i;

for (i = 0; i <10 ; i++)
{

Momentan versuchen Sie, mit array[10] Aus dem Speicher auf den nicht zugewiesenen Speicherort zuzugreifen, und dies verursacht das ndefiniertes Verhalten. Undefiniertes Verhalten bedeutet, dass sich Ihr Programm unbestimmt verhält, sodass es bei jeder Ausführung unterschiedliche Ausgaben liefern kann.

6
Steephen

Nun, C-Compiler prüft traditionell nicht auf Grenzen. Sie können einen Segmentierungsfehler erhalten, wenn Sie sich auf einen Speicherort beziehen, der nicht zu Ihrem Prozess gehört. Die lokalen Variablen werden jedoch im Stapel zugewiesen, und je nach Art der Speicherzuweisung wird der Bereich direkt hinter dem Array (array[10]) kann zum Speichersegment des Prozesses gehören. Es wird also keine Segmentierungsfehlerfalle ausgelöst, und genau das scheinen Sie zu erleben. Wie bereits erwähnt, ist dieses Verhalten in C undefiniert und Ihr Code kann als fehlerhaft angesehen werden. Da Sie C lernen, sollten Sie es sich zur Gewohnheit machen, in Ihrem Code nach Grenzen zu suchen.

5
unxnut

Über die Möglichkeit hinaus, dass der Speicher so angelegt wird, dass ein Versuch, in a[10] tatsächlich überschreibt i, es wäre auch möglich, dass ein optimierender Compiler feststellt, dass der Schleifentest mit einem Wert von i größer als zehn nicht erreicht werden kann, ohne dass der Code zuvor auf den Nicht-Compiler zugegriffen hat. existierendes Array-Element a[10].

Da ein Versuch, auf dieses Element zuzugreifen, ein undefiniertes Verhalten wäre, hätte der Compiler keine Verpflichtungen in Bezug darauf, was das Programm nach diesem Zeitpunkt tun könnte. Genauer gesagt, da der Compiler nicht verpflichtet wäre, Code zu generieren, um den Schleifenindex in jedem Fall zu überprüfen, in dem er größer als zehn ist, wäre er nicht verpflichtet, Code zu generieren, um ihn überhaupt zu überprüfen. es könnte stattdessen davon ausgegangen werden, dass die <=10 test ergibt immer true. Beachten Sie, dass dies auch dann zutrifft, wenn der Code a[10] anstatt es zu schreiben.

4
supercat

Wenn Sie nach i==9 Iterieren, weisen Sie den 'Array-Elementen', die sich tatsächlich befinden, eine Null zu nach dem Array, sodass Sie einige andere Daten überschreiben. Höchstwahrscheinlich überschreiben Sie die Variable i, die sich nach a[] Befindet. Auf diese Weise können Sie einfach setzen Sie die Variable i auf Null zurück und starten Sie die Schleife neu.

Sie könnten das selbst herausfinden, wenn Sie i in der Schleife drucken:

      printf("test i=%d\n", i);

statt nur

      printf("test \n");

Das Ergebnis hängt natürlich stark von der Speicherzuordnung für Ihre Variablen ab, was wiederum von einem Compiler und seinen Einstellungen abhängt. Daher ist es im Allgemeinen ndefiniertes Verhalten -, weshalb sich dies auf verschiedenen Computern oder verschiedenen Betriebssystemen oder ergibt auf verschiedenen Compilern können sich unterscheiden.

3
CiaPan

Ich werde etwas vorschlagen, das ich nicht weiter oben finde:

Versuchen Sie, array [i] = 20;

Ich denke, dies sollte den Code überall beenden. (Vorausgesetzt, Sie behalten i <= 10 oder ll)

Wenn dies ausgeführt wird, können Sie sicher entscheiden, dass die hier angegebenen Antworten bereits korrekt sind.

0
Raining fire

der Fehler ist in Abschnitt Array [10] w/c ist auch die Adresse von i (int Array [10], i;). Wenn Array [10] auf 0 gesetzt ist, ist i = 0 w/c, wodurch die gesamte Schleife zurückgesetzt und die Endlosschleife ausgelöst wird. 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 sein. für (i = 0; i <= 10; i ++) Array [i] = 0;