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

Benutzer-Avatar
Derobert

GCC tut davor warnen. Aber Sie müssen zwei Dinge tun:

  1. 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.
  2. Ä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?

    – Robert Gamble

    20. Dezember 2008 um 14:57 Uhr

  • Die Warnung dafür funktioniert in der Praxis nicht allzu gut – gcc.gnu.org/bugzilla/show_bug.cgi?id=35587 Außerdem gibt es falsch positive Ergebnisse, wenn Sie rohe Arrays mit Algorithmen verwenden forum.gbadev.org/viewtopic.php?t=15505

    – richq

    16. August 2009 um 16:32 Uhr

  • 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

Benutzer-Avatar
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:

[[email protected] cpp]$ gcc -fstack-protector-all -fmudflap -lmudflap mudf.c        
[[email protected] 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
[[email protected] 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:

/^ / {
    file = gensub(/([^(]*).*/, "\\1", 1);
    addr = gensub(/.*\[([x[:xdigit:]]*)\]$/, "\\1", 1);
    if(file && addr) {
        cmd = "addr2line -e " file " " addr
        cmd | getline laddr
        print $0 " (" laddr ")"
        close (cmd)
        next;
    }
}

1 # print all other lines

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 -Wall von 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.

Benutzer-Avatar
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

Benutzer-Avatar
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

Benutzer-Avatar
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.

1257840cookie-checkWarum warnen Compiler nicht vor statischen Array-Indizes außerhalb der Grenzen?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy