Warum warnen Compiler nicht vor statischen Array-Indizes außerhalb der Grenzen?
Lesezeit: 10 Minuten
Ein Kollege von mir wurde kürzlich schwer gebissen, als er außerhalb der Grenzen in ein statisches Array auf dem Stapel schrieb (er fügte ein Element hinzu, ohne die Arraygröße zu erhöhen). Sollte der Compiler diese Art von Fehlern nicht abfangen? Der folgende Code lässt sich sauber mit gcc kompilieren, sogar mit der -Wall -Wextra Optionen, und doch ist es eindeutig falsch:
int main(void)
{
int a[10];
a[13] = 3; // oops, overwrote the return address
return 0;
}
Ich bin mir sicher, dass dies ein undefiniertes Verhalten ist, obwohl ich im Moment keinen Auszug aus dem C99-Standard finden kann, der dies sagt. Aber im einfachsten Fall, wo die Größe eines Arrays als Kompilierzeit bekannt ist und die Indizes zur Kompilierzeit bekannt sind, sollte der Compiler nicht zumindest eine Warnung ausgeben?
“… ohne die Arraygröße zu erhöhen …” – Ich frage mich, wie er das erreicht hätte, da es sich um ein statisches Array handelt …
– tschüss
20. Dezember 2008 um 6:30 Uhr
Sein Code war wie “int a[2]; a[0] = 0; a[1] = 1;“, und dann fügte er hinzu „a[2] = 2;”, ohne die Größe von a auf 3 zu erhöhen.
– Adam Rosenfield
20. Dezember 2008 um 6:34 Uhr
@Adam: Siehe §6.5.6p8, nämlich (mit dem Verständnis, dass a[13] = *(a+13): “Wenn sowohl der Zeigeroperand als auch das Ergebnis auf Elemente desselben Array-Objekts zeigen oder eins nach dem letzten Element des Array-Objekts, soll die Auswertung keinen Überlauf erzeugen; andernfalls ist das Verhalten nicht definiert.”
– Robert Gamble
20. Dezember 2008 um 23:18 Uhr
Derobert
GCC tut davor warnen. Aber Sie müssen zwei Dinge tun:
Optimierung aktivieren. Ohne mindestens -O2 führt GCC nicht genügend Analysen durch, um zu wissen, was a ist, und dass du über den Rand gelaufen bist.
Ändern Sie Ihr Beispiel so, dass a[] tatsächlich verwendet wird, sonst generiert GCC ein No-Op-Programm und hat Ihre Zuweisung vollständig verworfen.
.
$ cat foo.c
int main(void)
{
int a[10];
a[13] = 3; // oops, overwrote the return address
return a[1];
}
$ gcc -Wall -Wextra -O2 -c foo.c
foo.c: In function ‘main’:
foo.c:4: warning: array subscript is above array bounds
Übrigens: Wenn Sie a[13] In Ihrem Testprogramm würde das auch nicht funktionieren, da GCC das Array wieder optimiert.
Guter Fang mit der Optimierung – ich hatte vergessen, dass GCC keine Datenflussanalyse ohne aktivierte Optimierungen durchführt. Ich muss das Problem meines Kollegen genauer untersuchen, um zu sehen, warum es dort nicht gewarnt hat.
– Adam Rosenfield
20. Dezember 2008 um 8:38 Uhr
Die Frage und Ihr Beispiel verwenden gcc, aber Ihre Einführung spricht von g ++. Können Sie dies in gcc ändern?
Guter Punkt: Ohne Optimierung wird nicht genügend Analyse durchgeführt.
– Laser
28. September 2010 um 19:19 Uhr
Was ist die Standardoptimierung? von gcc?
– Vineet Menon
19. September 2011 um 12:19 Uhr
Johannes Schaub – litb
Hast du es versucht -fmudflap mit GC? Dies sind zwar Laufzeitprüfungen, aber nützlich, da Sie ohnehin meistens mit zur Laufzeit berechneten Indizes zu tun haben. Anstatt stillschweigend weiterzuarbeiten, werden Sie über diese Fehler benachrichtigt.
-fmudflap -fmudflapth -fmudflapir
Instrumentieren Sie für Frontends, die dies unterstützen (C und C++), alle riskanten Pointer-/Array-Dereferenzierungsoperationen, einige Standardbibliotheks-String-/Heap-Funktionen und einige andere zugehörige Konstrukte mit Bereichs-/Gültigkeitstests. Auf diese Weise instrumentierte Module sollten immun gegen Pufferüberläufe, ungültige Heap-Nutzung und einige andere Klassen von C/C++-Programmierfehlern sein. Die Instrumentierung stützt sich auf eine separate Laufzeitbibliothek (libmudflap), die in ein Programm eingebunden wird, wenn -fmudflap zur Linkzeit angegeben wird. Das Laufzeitverhalten des instrumentierten Programms wird durch die Umgebungsvariable MUDFLAP_OPTIONS gesteuert. Siehe “env MUDFLAP_OPTIONS=-help a.out” für seine Optionen.
Verwenden Sie -fmudflapth anstelle von -fmudflap zum Kompilieren und Linken, wenn Ihr Programm Multi-Threaded ist. Verwenden Sie -fmudflapir zusätzlich zu -fmudflap oder -fmudflapth, wenn die Instrumentierung Pointer-Reads ignorieren soll. Dies erzeugt weniger Instrumentierung (und daher eine schnellere Ausführung) und bietet immer noch einen gewissen Schutz gegen direkte speicherkorrumpierende Schreibvorgänge, ermöglicht jedoch, dass sich fälschlicherweise gelesene Daten innerhalb eines Programms ausbreiten.
Hier ist, was Mudflag mir für Ihr Beispiel gibt:
[js@HOST2 cpp]$ gcc -fstack-protector-all -fmudflap -lmudflap mudf.c
[js@HOST2 cpp]$ ./a.out
*******
mudflap violation 1 (check/write): time=1229801723.191441 ptr=0xbfdd9c04 size=56
pc=0xb7fb126d location=`mudf.c:4:3 (main)'
/usr/lib/libmudflap.so.0(__mf_check+0x3d) [0xb7fb126d]
./a.out(main+0xb9) [0x804887d]
/usr/lib/libmudflap.so.0(__wrap_main+0x4f) [0xb7fb0a5f]
Nearby object 1: checked region begins 0B into and ends 16B after
mudflap object 0x8509cd8: name=`mudf.c:3:7 (main) a'
bounds=[0xbfdd9c04,0xbfdd9c2b] size=40 area=stack check=0r/3w liveness=3
alloc time=1229801723.191433 pc=0xb7fb09fd
number of nearby objects: 1
[js@HOST2 cpp]$
Es hat eine Reihe von Optionen. Zum Beispiel kann es einen gdb-Prozess bei Verstößen abzweigen, kann Ihnen zeigen, wo Ihr Programm durchgesickert ist (mithilfe von -print-leaks) oder nicht initialisierte Lesevorgänge von Variablen erkennen. Verwenden MUDFLAP_OPTIONS=-help ./a.out um eine Liste mit Optionen zu erhalten. Da Mudflap nur Adressen und keine Dateinamen und Zeilen der Quelle ausgibt, habe ich ein kleines Gawk-Skript geschrieben:
Leiten Sie die Ausgabe von mudflap hinein, und es zeigt die Quelldatei und die Zeile jedes Backtrace-Eintrags an.
Ebenfalls -fstack-protector[-all] :
-fstack-protector
Geben Sie zusätzlichen Code aus, um nach Pufferüberläufen zu suchen, wie z. B. Stack-Smashing-Angriffen. Dies geschieht durch Hinzufügen einer Guard-Variable zu Funktionen mit anfälligen Objekten. Dazu gehören Funktionen, die alloca aufrufen, und Funktionen mit Puffern, die größer als 8 Bytes sind. Die Wächter werden initialisiert, wenn eine Funktion eingegeben wird, und dann überprüft, wenn die Funktion beendet wird. Schlägt eine Wächterprüfung fehl, wird eine Fehlermeldung ausgegeben und das Programm beendet.
-fstack-protector-all
Wie -fstack-protector, außer dass alle Funktionen geschützt sind.
Sie haben Recht, Das Verhalten ist undefiniert. C99-Zeiger müssen innerhalb von deklarierten oder Heap-zugeordneten Datenstrukturen oder nur um ein Element darüber hinaus zeigen.
Ich konnte nie herausfinden, wie die gcc Menschen entscheiden, wann sie warnen. Ich war schockiert, das zu erfahren -Wallvon selbst warnt nicht vor nicht initialisierten Variablen; mindestens Sie brauchen -Ound selbst dann wird die Warnung manchmal weggelassen.
Ich vermute, dass der Compiler wahrscheinlich keine Möglichkeit in seinen Ausdrucksbäumen hat, weil unbegrenzte Arrays in C so verbreitet sind vertreten ein Array, dessen Größe zur Kompilierzeit bekannt ist. Obwohl die Informationen bei der Deklaration vorhanden sind, vermute ich, dass sie bei der Verwendung bereits verloren gehen.
ich Zweitens die Empfehlung von Valgrind. Wenn Sie in C programmieren, sollten Sie das tun Führen Sie valgrind ständig in jedem Programm aus bis Sie den Leistungseinbruch nicht mehr aushalten.
dkretz
Es ist kein statisches Array.
Undefiniertes Verhalten oder nicht, es schreibt an eine Adresse, die 13 Ganzzahlen vom Anfang des Arrays entfernt ist. Was dort ist, liegt in Ihrer Verantwortung. Es gibt mehrere C-Techniken, die Arrays aus vernünftigen Gründen absichtlich falsch zuweisen. Und diese Situation ist bei unvollständigen Übersetzungseinheiten nicht ungewöhnlich.
Abhängig von Ihren Flag-Einstellungen gibt es eine Reihe von Funktionen dieses Programms, die gekennzeichnet würden, wie zum Beispiel die Tatsache, dass das Array niemals verwendet wird. Und der Compiler könnte es genauso einfach aus der Existenz optimieren und es Ihnen nicht sagen – ein Baum, der in den Wald fällt.
Es ist der C-Weg. Es ist Ihr Array, Ihr Gedächtnis, machen Sie damit, was Sie wollen. 🙂
(Es gibt jede Menge Lint-Tools, die Ihnen helfen, solche Dinge zu finden; und Sie sollten sie großzügig verwenden. Sie funktionieren jedoch nicht alle über den Compiler; Kompilieren und Verknüpfen sind oft schon mühsam genug.)
Der Grund, warum C es nicht tut, ist, dass C nicht über die Informationen verfügt. Eine Aussage wie
int a[10];
macht zwei Dinge: es weist zu sizeof(int)*10 Bytes Speicherplatz (plus möglicherweise ein wenig Totraum für die Ausrichtung) und fügt einen Eintrag in die Symboltabelle ein, der konzeptionell lautet:
a : address of a[0]
oder in C-Termen
a : &a[0]
und das ist alles. Tatsächlich können Sie in C austauschen *(a+i) mit a[i] in (fast*) allen Fällen ohne Auswirkung NACH DEFINITION. Ihre Frage entspricht also der Frage “Warum kann ich diesem (Adress-) Wert eine Ganzzahl hinzufügen?”
* Pop-Quiz: Was ist der eine Fall in diesem Dies ist nicht Stimmt?
Aber der C-Compiler verfolgt die Größe, sonst würde sizeof bei Arrays nicht funktionieren.
– Matthew Crumley
21. Dezember 2008 um 5:57 Uhr
Sehen Sie, was sizeof mit einem Array macht, das nicht in derselben Datei definiert ist.
– Charly Martin
21. Dezember 2008 um 16:23 Uhr
Das ist ein guter Punkt. Ich sage nicht, dass Sie falsch liegen, nur dass sich der Compiler die Größe innerhalb der Kompilierungseinheit merkt und einige Überläufe erkennen könnte.
– Matthew Crumley
21. Dezember 2008 um 18:13 Uhr
“Der Grund, warum C es nicht tut, ist, dass C die Informationen nicht hat” Falsch. Es hat genau die Informationen. Und wie andere anmerken, wenn Sie die richtigen Flags aktivieren, wird es warnen. Es gibt sicher Stellen, an denen Arrays herumgereicht werden, von denen der Compiler nichts weiß. Aber dies ist nicht einer dieser Fälle
– pm100
19. März 2018 um 16:48 Uhr
Unsinn. Betrachten Sie zB a char * buf mit einem Malloc-Puffer, referenziert von extern char[] buf in einer separaten Datei. Der Compiler kann die Grenzen nicht prüfen; die Grenzen sind nicht einmal zur Kompilierzeit bekannt.
– Charly Martin
20. März 2018 um 2:25 Uhr
Vieldenker
Das ist die C-Philosophie Der Programmierer hat immer Recht. So wird es Ihnen stillschweigend erlauben, auf jede Speicheradresse zuzugreifen, die Sie dort angeben, vorausgesetzt, Sie wissen immer, was Sie tun, und werden Sie nicht mit einer Warnung belästigen.
Aber der C-Compiler verfolgt die Größe, sonst würde sizeof bei Arrays nicht funktionieren.
– Matthew Crumley
21. Dezember 2008 um 5:57 Uhr
Sehen Sie, was sizeof mit einem Array macht, das nicht in derselben Datei definiert ist.
– Charly Martin
21. Dezember 2008 um 16:23 Uhr
Das ist ein guter Punkt. Ich sage nicht, dass Sie falsch liegen, nur dass sich der Compiler die Größe innerhalb der Kompilierungseinheit merkt und einige Überläufe erkennen könnte.
– Matthew Crumley
21. Dezember 2008 um 18:13 Uhr
“Der Grund, warum C es nicht tut, ist, dass C die Informationen nicht hat” Falsch. Es hat genau die Informationen. Und wie andere anmerken, wenn Sie die richtigen Flags aktivieren, wird es warnen. Es gibt sicher Stellen, an denen Arrays herumgereicht werden, von denen der Compiler nichts weiß. Aber dies ist nicht einer dieser Fälle
– pm100
19. März 2018 um 16:48 Uhr
Unsinn. Betrachten Sie zB a char * buf mit einem Malloc-Puffer, referenziert von extern char[] buf in einer separaten Datei. Der Compiler kann die Grenzen nicht prüfen; die Grenzen sind nicht einmal zur Kompilierzeit bekannt.
– Charly Martin
20. März 2018 um 2:25 Uhr
Evan Teran
Ich glaube, dass einige Compiler in bestimmten Fällen tun. Zum Beispiel, wenn mein Gedächtnis mich richtig bedient, haben neuere Microsoft-Compiler eine “Buffer Security Check”-Option, die triviale Fälle von Pufferüberläufen erkennt.
Warum machen das nicht alle Compiler? Entweder (wie bereits erwähnt) eignet sich die vom Compiler verwendete interne Darstellung nicht für diese Art der statischen Analyse, oder sie steht einfach nicht hoch genug auf der Prioritätenliste der Autoren. Was ehrlich gesagt so oder so schade ist.
12578400cookie-checkWarum warnen Compiler nicht vor statischen Array-Indizes außerhalb der Grenzen?yes
“… ohne die Arraygröße zu erhöhen …” – Ich frage mich, wie er das erreicht hätte, da es sich um ein statisches Array handelt …
– tschüss
20. Dezember 2008 um 6:30 Uhr
Sein Code war wie “int a[2]; a[0] = 0; a[1] = 1;“, und dann fügte er hinzu „a[2] = 2;”, ohne die Größe von a auf 3 zu erhöhen.
– Adam Rosenfield
20. Dezember 2008 um 6:34 Uhr
@Adam: Siehe §6.5.6p8, nämlich (mit dem Verständnis, dass a[13] = *(a+13): “Wenn sowohl der Zeigeroperand als auch das Ergebnis auf Elemente desselben Array-Objekts zeigen oder eins nach dem letzten Element des Array-Objekts, soll die Auswertung keinen Überlauf erzeugen; andernfalls ist das Verhalten nicht definiert.”
– Robert Gamble
20. Dezember 2008 um 23:18 Uhr