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 das int
unmittelbar darauf liegende array
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:
- Der Ort wurde nicht genutzt. Die Schleife endet normal.
- Der Ort wurde für etwas verwendet, das zufällig den Wert 0 hatte. Die Schleife endet normal.
- 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 sie i
bei 0 neu gestartet wird.
- Der Speicherort enthält eine andere Variable. Die Schleife endet normal, aber dann passieren „interessante“ Dinge.
- Der Speicherort ist eine ungültige Speicheradresse, z. B. weil er
array
sich direkt am Ende einer virtuellen Speicherseite befindet und die nächste Seite nicht zugeordnet ist.
- Dämonen fliegen aus deiner Nase . Glücklicherweise fehlt den meisten Computern die erforderliche Hardware.
Was Sie unter Windows beobachtet haben, war, dass der Compiler beschlossen hat, die Variable i
unmittelbar nach dem Array im Speicher abzulegen, und sie array[10] = 0
schließlich zugewiesen hat i
. Unter Ubuntu und CentOS wurde der Compiler dort nicht platziert i
. 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:
.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 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 also i
direkt vor dem Array platziert. Sie würden überschreiben, i
wenn Sie zufällig schreiben würden array[-1]
. Wenn Sie ändern array[i]=0
zu array[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
.
movl $11, %ebx
.L3:
movl $.LC0, %edi
call puts
subl $1, %ebx
jne .L3
Das ist kürzer! Der Compiler hat nicht nur abgelehnt, einen Stapelspeicherort zuzuweisen i
- er wird immer nur im Register gespeichert ebx
-, sondern er hat sich auch nicht die Mühe gemacht, Speicher zuzuweisen array
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
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 folgendermaßen 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 Bytes von oben. Was ist mit i
? Es erscheint nirgendwo! Der Schleifenzähler bleibt jedoch im Register rbx
. Es ist nicht genau i
, aber die Adresse der array[i]
. Der Compiler hat entschieden, dass i
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]
bis array[9-i]
) oder sogar das Ändern von scheinbar nicht verwandten Elementen (Hinzufügen des Aufrufs zu use_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.