Suche…


Einführung

In C führen einige Ausdrücke zu undefiniertem Verhalten . Der Standard wählt explizit aus, nicht zu definieren, wie sich ein Compiler verhalten soll, wenn er auf einen solchen Ausdruck stößt. Daher kann ein Compiler das tun, was er für richtig hält, und kann nützliche Ergebnisse, unerwartete Ergebnisse oder sogar einen Absturz verursachen.

Code, der UB aufruft, funktioniert möglicherweise auf einem bestimmten System mit einem bestimmten Compiler wie beabsichtigt, wird aber wahrscheinlich nicht auf einem anderen System oder mit einem anderen Compiler, einer Compilerversion oder Compilereinstellungen funktionieren.

Bemerkungen

Was ist undefiniertes Verhalten (UB)?

Nicht definiertes Verhalten ist ein Begriff, der im C-Standard verwendet wird. Der C11-Standard (ISO / IEC 9899: 2011) definiert den Begriff undefiniertes Verhalten als

Verhalten bei Verwendung eines nicht portablen oder fehlerhaften Programmkonstrukts oder fehlerhafter Daten, für das diese Internationale Norm keine Anforderungen auferlegt

Was passiert, wenn mein Code UB enthält?

Dies sind die Ergebnisse, die aufgrund von undefiniertem Verhalten gemäß Standard auftreten können:

ANMERKUNG Das mögliche undefinierte Verhalten reicht vom Ignorieren der Situation mit unvorhersehbaren Ergebnissen über das dokumentierte Verhalten der Übersetzung (mit oder ohne Ausgabe einer Diagnosemeldung) während der Übersetzung oder Programmausführung bis zum Abbruch einer Übersetzung oder Ausführung (mit der Option Ausgabe einer Diagnosemeldung).

Das folgende Zitat wird häufig verwendet, um (weniger formell) Ergebnisse zu beschreiben, die auf undefiniertes Verhalten zurückzuführen sind:

"Wenn der Compiler [einem bestimmten undefinierten Konstrukt] begegnet, ist es legal, Dämonen aus der Nase fliegen zu lassen."

Warum gibt es UB?

Wenn es so schlimm ist, warum haben sie es nicht einfach definiert oder durch Implementierung definiert?

Undefiniertes Verhalten bietet mehr Möglichkeiten zur Optimierung; Der Compiler kann zu Recht davon ausgehen, dass jeder Code kein undefiniertes Verhalten enthält, wodurch Laufzeitprüfungen vermieden und Optimierungen durchgeführt werden können, deren Gültigkeit kostspielig ist oder nicht nachgewiesen werden kann.

Warum ist UB schwer zu finden?

Es gibt mindestens zwei Gründe, warum undefiniertes Verhalten Fehler verursacht, die schwer zu erkennen sind:

  • Der Compiler muss nicht - und kann Sie im Allgemeinen nicht zuverlässig - vor undefiniertem Verhalten warnen. Ein entsprechendes Erfordernis würde direkt gegen den Grund für undefiniertes Verhalten verstoßen.
  • Die unvorhersehbaren Ergebnisse beginnen sich möglicherweise nicht genau an der Stelle zu entwickeln, an der das Konstrukt auftritt, dessen Verhalten undefiniert ist. Undefiniertes Verhalten verfälscht die gesamte Ausführung und ihre Auswirkungen können jederzeit auftreten: Während, nach oder sogar vor dem undefinierten Konstrukt.

Betrachten Sie eine Null-Zeiger-Dereferenzierung: Der Compiler ist nicht für die Diagnose einer Null-Zeiger-Dereferenzierung erforderlich und könnte dies auch nicht, da zur Laufzeit jeder in eine Funktion übergebene Zeiger oder in einer globalen Variablen Null sein kann. Und wenn die Null-Zeiger-Dereferenzierung auftritt, schreibt der Standard nicht vor, dass das Programm abstürzen muss. Das Programm kann vielmehr früher, später oder überhaupt nicht abstürzen. Es könnte sich sogar so verhalten, als ob der Nullzeiger auf ein gültiges Objekt zeigte, und sich völlig normal verhält, nur unter anderen Umständen zum Absturz.

Im Falle von Null-Zeiger-Dereferenzierung unterscheidet sich die Sprache C von verwalteten Sprachen wie Java oder C #, in denen das Verhalten der Null-Zeiger-Dereferenzierung definiert ist : Es wird genau zu der Zeit eine Ausnahme ausgelöst ( NullPointerException in Java, NullReferenceException in C #) Diejenigen, die aus Java oder C # stammen, könnten daher fälschlicherweise glauben, dass ein C-Programm mit oder ohne Ausgabe einer Diagnosemeldung abstürzen muss .

Zusätzliche Information

Es gibt mehrere solcher Situationen, die klar unterschieden werden sollten:

  • Ausdrücklich undefiniertes Verhalten. Hier weist der C-Standard explizit darauf hin, dass Sie sich nicht im Grenzbereich befinden.
  • Implizit undefiniertes Verhalten, bei dem der Standard einfach keinen Text enthält, der ein Verhalten für die Situation vorsieht, in die Sie Ihr Programm eingebracht haben.

Bedenken Sie auch, dass das Verhalten bestimmter Konstrukte an vielen Stellen durch den C-Standard absichtlich undefiniert ist, um Raum für Compiler- und Bibliotheksimplementierer zu lassen, um eigene Definitionen zu entwickeln. Ein gutes Beispiel sind Signale und Signalhandler, bei denen Erweiterungen von C, wie beispielsweise der Betriebssystemstandard POSIX, weitaus detailliertere Regeln definieren. In solchen Fällen müssen Sie nur die Dokumentation Ihrer Plattform überprüfen. Der C-Standard kann Ihnen nichts sagen.

Beachten Sie auch, dass wenn undefiniertes Verhalten in einem Programm auftritt, das nicht bedeutet, dass nur der Punkt, an dem undefiniertes Verhalten aufgetreten ist, problematisch ist, vielmehr wird das gesamte Programm bedeutungslos.

Aufgrund solcher Bedenken ist es für die Personenprogrammierung in C wichtig (vor allem, da Compiler uns nicht immer vor UB warnen), zumindest mit den Dingen vertraut zu sein, die undefiniertes Verhalten auslösen.

Es sei darauf hingewiesen, dass es einige Tools gibt (z. B. statische Analysewerkzeuge wie PC-Lint), die das Erkennen von undefiniertem Verhalten unterstützen, aber auch hier nicht alle Vorkommen von undefiniertem Verhalten erkennen können.

Dereferenzieren Sie einen Nullzeiger

Dies ist ein Beispiel für die Dereferenzierung eines NULL-Zeigers, wodurch undefiniertes Verhalten verursacht wird.

int * pointer = NULL;
int value = *pointer; /* Dereferencing happens here */

Der C-Standard garantiert einen NULL Zeiger, um ungleiche Zeiger auf ein gültiges Objekt zu vergleichen, und dereferenziert es, undefiniert dieses Verhalten.

Objekte mehr als einmal zwischen zwei Sequenzpunkten ändern

int i = 42;
i = i++; /* Assignment changes variable, post-increment as well */
int a = i++ + i--;

Code wie dieser führt oft zu Spekulationen über den "Ergebniswert" von i . Anstatt ein Ergebnis anzugeben, geben die C-Standards jedoch an, dass die Auswertung eines solchen Ausdrucks zu undefiniertem Verhalten führt . Vor C2011 formalisierte der Standard diese Regeln in Form sogenannter Sequenzpunkte :

Zwischen dem vorherigen und dem nächsten Sequenzpunkt soll ein gespeicherter Wert eines skalaren Objekts höchstens einmal durch die Auswertung eines Ausdrucks geändert werden. Außerdem soll der vorherige Wert nur gelesen werden, um den zu speichernden Wert zu bestimmen.

(Standard C99, Abschnitt 6.5, Absatz 2)

Dieses Schema erwies sich als etwas zu grob, was dazu führte, dass einige Ausdrücke in Bezug auf C99 undefiniertes Verhalten zeigten, das plausibel nicht funktionieren sollte. C2011 behält die Sequenzpunkte bei, führt jedoch einen differenzierteren Ansatz für diesen Bereich basierend auf der Sequenzierung und einer Beziehung ein, die als "Sequenzierung zuvor" bezeichnet wird:

Wenn ein Nebeneffekt auf ein Skalarobjekt relativ zu einem anderen Nebeneffekt auf demselben Skalarobjekt oder einer Wertberechnung mit dem Wert desselben Skalarobjekts ohne Folgen bleibt, ist das Verhalten undefiniert. Wenn es mehrere zulässige Anordnungen der Unterausdrücke eines Ausdrucks gibt, ist das Verhalten undefiniert, wenn ein solcher nicht aufeinanderfolgender Nebeneffekt in einer der Anordnungen auftritt.

(Standard C2011, Abschnitt 6.5, Absatz 2)

Die vollständigen Details der "Vorher-sequenzierten" Relation sind zu lang, um sie hier zu beschreiben, ergänzen jedoch Sequenzpunkte, anstatt sie zu ersetzen, so dass sie Verhalten für einige Auswertungen definieren, deren Verhalten zuvor undefiniert war. Wenn sich zwischen zwei Auswertungen ein Sequenzpunkt befindet, wird der vor dem Sequenzpunkt vor dem nachfolgenden Sequenzpunkt "sequenziert".

Das folgende Beispiel hat ein genau definiertes Verhalten:

int i = 42;
i = (i++, i+42); /* The comma-operator creates a sequence point */

Das folgende Beispiel hat ein undefiniertes Verhalten:

int i = 42;
printf("%d %d\n", i++, i++); /* commas as separator of function arguments are not comma-operators */

Wie bei jeder Form von undefiniertem Verhalten ist das Beobachten des tatsächlichen Verhaltens bei der Auswertung von Ausdrücken, die gegen die Sequenzierungsregeln verstoßen, nicht aufschlussreich, es sei denn, dies ist rückblickend. Der Sprachstandard bietet keine Grundlage, um zu erwarten, dass solche Beobachtungen sogar das zukünftige Verhalten desselben Programms vorhersagen.

Fehlende return-Anweisung in der Funktion zur Wertrückgabe

int foo(void) {
  /* do stuff */
  /* no return here */
}

int main(void) {
  /* Trying to use the (not) returned value causes UB */
  int value = foo();
  return 0;
}

Wenn für eine Funktion ein Wert zurückgegeben wird, muss dies für jeden möglichen Codepfad erfolgen. Ein undefiniertes Verhalten tritt auf, sobald der Aufrufer (der einen Rückgabewert erwartet) den Rückgabewert 1 verwendet .

Beachten Sie, dass das undefinierte Verhalten nur auftritt , wenn der Aufrufer versucht, den Wert der Funktion zu verwenden / darauf zuzugreifen. Zum Beispiel,

int foo(void) {
  /* do stuff */
  /* no return here */
}

int main(void) {
  /* The value (not) returned from foo() is unused. So, this program
   * doesn't cause *undefined behaviour*. */
  foo();
  return 0;
}
C99

Die main() Funktion ist eine Ausnahme von dieser Regel, da sie ohne return-Anweisung beendet werden kann, da in diesem Fall automatisch ein angenommener Rückgabewert von 0 verwendet wird 2 .


1 ( ISO / IEC 9899: 201x , 6.9.1 / 12)

Wenn das}, das eine Funktion beendet, erreicht wird und der Wert des Funktionsaufrufs vom Aufrufer verwendet wird, ist das Verhalten undefiniert.

2 ( ISO / IEC 9899: 201x , 5.1.2.2.3 / 1)

Wenn Sie das} erreichen, das die Hauptfunktion beendet, wird der Wert 0 zurückgegeben.

Überlauf der signierten Ganzzahl

Gemäß Absatz 6.5 / 5 von C99 und C11 führt die Auswertung eines Ausdrucks zu undefiniertem Verhalten, wenn das Ergebnis kein darstellbarer Wert des Typs des Ausdrucks ist. Für arithmetische Typen wird dies als Überlauf bezeichnet . Die Ganzzahl-Arithmetik ohne Vorzeichen läuft nicht über, weil Absatz 6.2.5 / 9 gilt. Dies führt dazu, dass vorzeichenlose Ergebnisse, die ansonsten außerhalb des Bereichs liegen, auf einen Wert innerhalb des Bereichs reduziert werden. Es gibt jedoch keine analoge Bestimmung für vorzeichenbehaftete Integer-Typen. Diese können und machen einen Überlauf und erzeugen undefiniertes Verhalten. Zum Beispiel,

#include <limits.h>      /* to get INT_MAX */

int main(void) {
    int i = INT_MAX + 1; /* Overflow happens here */
    return 0;
}

Die meisten Fälle dieser Art von undefiniertem Verhalten sind schwieriger zu erkennen oder vorherzusagen. Ein Überlauf kann prinzipiell aus jeder Addition, Subtraktion oder Multiplikation von vorzeichenbehafteten Ganzzahlen (vorbehaltlich der üblichen arithmetischen Konvertierungen) entstehen, bei denen keine wirksamen Grenzen oder eine Beziehung zwischen den Operanden bestehen, um dies zu verhindern. Zum Beispiel diese Funktion:

int square(int x) {
    return x * x;  /* overflows for some values of x */
}

ist vernünftig und tut das Richtige für Argumentwerte, die klein genug sind, aber für größere Argumentwerte ist das Verhalten nicht definiert. Sie können nicht allein anhand der Funktion beurteilen, ob Programme, die sie aufrufen, als Ergebnis undefiniertes Verhalten zeigen. Es hängt davon ab, mit welchen Argumenten sie darüber sprechen.

Betrachten Sie andererseits dieses triviale Beispiel für eine überlaufsichere Ganzzahlarithmetik:

int zero(int x) {
    return x - x;  /* Cannot overflow */
}

Die Beziehung zwischen den Operanden des Subtraktionsoperators stellt sicher, dass die Subtraktion niemals überläuft. Oder betrachten Sie dieses etwas praktischere Beispiel:

int sizeDelta(FILE *f1, FILE *f2) {
    int count1 = 0;
    int count2 = 0;
    while (fgetc(f1) != EOF) count1++;  /* might overflow */
    while (fgetc(f2) != EOF) count2++;  /* might overflow */

    return count1 - count2; /* provided no UB to this point, will not overflow */
}

Solange die Zähler nicht einzeln überlaufen, sind die Operanden der letzten Subtraktion beide nicht negativ. Alle Unterschiede zwischen zwei beliebigen Werten können als int .

Verwendung einer nicht initialisierten Variablen

int a; 
printf("%d", a);

Die Variable a ist ein int mit automatischer Speicherdauer. Der obige Beispielcode versucht, den Wert einer nicht initialisierten Variablen zu drucken ( a wurde nie initialisiert). Automatische Variablen, die nicht initialisiert werden, haben unbestimmte Werte. Der Zugriff auf diese kann zu undefiniertem Verhalten führen.

Anmerkung: Variablen mit lokalem statischem oder Thread-Speicher, einschließlich globaler Variablen ohne das Schlüsselwort static , werden entweder auf Null oder auf ihren initialisierten Wert gesetzt. Daher ist das Folgende legal.

static int b;
printf("%d", b);

Ein sehr häufiger Fehler ist es , nicht die Variablen zu initialisieren , die als Zähler auf 0. Sie dienen Werte zu ergänzen, aber da der Anfangswert Müll ist, werden Sie nicht definiertes Verhalten, wie zum Beispiel in der Frage aufrufen Kompilierung auf Terminal verströmt Zeiger Warnung und seltsame Symbole .

Beispiel:

#include <stdio.h>

int main(void) {
    int i, counter;
    for(i = 0; i < 10; ++i)
        counter += i;
    printf("%d\n", counter);
    return 0;
}

Ausgabe:

C02QT2UBFVH6-lm:~ gsamaras$ gcc main.c -Wall -o main
main.c:6:9: warning: variable 'counter' is uninitialized when used here [-Wuninitialized]
        counter += i;
        ^~~~~~~
main.c:4:19: note: initialize the variable 'counter' to silence this warning
    int i, counter;
                  ^
                   = 0
1 warning generated.
C02QT2UBFVH6-lm:~ gsamaras$ ./main
32812

Die obigen Regeln gelten auch für Zeiger. Das folgende führt beispielsweise zu undefiniertem Verhalten

int main(void)
{
    int *p;
    p++; // Trying to increment an uninitialized pointer.
}

Beachten Sie, dass der obige Code alleine möglicherweise keinen Fehler oder Segmentierungsfehler verursacht. Wenn Sie diesen Zeiger später dereferenzieren, wird dies jedoch zu einem undefinierten Verhalten führen.

Dereferenzieren eines Zeigers auf eine Variable über ihre Lebensdauer hinaus

int* foo(int bar)
{
    int baz = 6;
    baz += bar;
    return &baz; /* (&baz) copied to new memory location outside of foo. */
} /* (1) The lifetime of baz and bar end here as they have automatic storage   
   * duration (local variables), thus the returned pointer is not valid! */

int main (void)
{
    int* p;

    p = foo(5);  /* (2) this expression's behavior is undefined */
    *p = *p - 6; /* (3) Undefined behaviour here */

    return 0;
}

Einige Compiler weisen darauf hin. Zum Beispiel warnt gcc mit:

warning: function returns address of local variable [-Wreturn-local-addr]

und clang warnt mit:

warning: address of stack memory associated with local variable 'baz' returned 
[-Wreturn-stack-address]

für den obigen Code. Compiler können jedoch möglicherweise nicht in komplexem Code helfen.

(1) Das Zurückgeben eines Verweises auf eine als static deklarierte Variable ist ein definiertes Verhalten, da die Variable nach Verlassen des aktuellen Gültigkeitsbereichs nicht gelöscht wird.

(2) Gemäß ISO / IEC 9899: 2011 6.2.4 §2 "Der Wert eines Zeigers wird unbestimmt, wenn das Objekt, auf das er zeigt, das Ende seiner Lebensdauer erreicht."

(3) Der Rückschluss auf den von der Funktion foo Zeiger ist undefiniertes Verhalten, da der Speicher, auf den er verweist, einen unbestimmten Wert enthält.

Durch Null teilen

int x = 0;
int y = 5 / x;  /* integer division */

oder

double x = 0.0;
double y = 5.0 / x;  /* floating point division */

oder

int x = 0;
int y = 5 % x;  /* modulo operation */

Für die zweite Zeile in jedem Beispiel, in der der Wert des zweiten Operanden (x) Null ist, ist das Verhalten undefiniert.

Beachten Sie, dass die meisten Implementierungen der Gleitkomma-Mathematik einem Standard folgen (z. B. IEEE 754). In diesem Fall haben Operationen wie Division durch Null gleichbleibende Ergebnisse (z. B. INFINITY ), obwohl der C-Standard sagt, dass die Operation undefiniert ist.

Zugriff auf Speicherplatz außerhalb des zugewiesenen Blocks

Ein Zeiger auf einen Speicherbereich, der n Elemente enthält, kann nur dereferenziert werden, wenn er sich im Bereich memory und memory + (n - 1) . Wenn Sie einen Zeiger außerhalb dieses Bereichs referenzieren, führt dies zu undefiniertem Verhalten. Betrachten Sie als Beispiel den folgenden Code:

int array[3];
int *beyond_array = array + 3;
*beyond_array = 0; /* Accesses memory that has not been allocated. */

Die dritte Zeile greift auf das vierte Element in einem nur 3 Elemente langen Array zu, was zu undefiniertem Verhalten führt. Ebenso ist das Verhalten der zweiten Zeile in dem folgenden Codefragment nicht gut definiert:

int array[3];
array[3] = 0;

Beachten Sie, dass das Zeigen auf das letzte Element eines Arrays kein undefiniertes Verhalten ist ( beyond_array = array + 3 ist hier gut definiert), aber dereferenzierend ist ( *beyond_array ist undefiniertes Verhalten). Diese Regel gilt auch für dynamisch zugewiesenen Speicher (z. B. durch malloc erzeugte Puffer).

Überlappenden Speicher kopieren

Eine Vielzahl von Standard-Bibliotheksfunktionen hat unter anderem Auswirkungen auf das Kopieren von Bytefolgen von einem Speicherbereich in einen anderen. Die meisten dieser Funktionen haben undefiniertes Verhalten, wenn sich die Quell- und Zielregionen überlappen.

Zum Beispiel das ...

#include <string.h> /* for memcpy() */

char str[19] = "This is an example";
memcpy(str + 7, str, 10);

... versucht, 10 Bytes zu kopieren, wobei sich der Quell- und der Zielspeicherbereich um drei Bytes überschneiden. Visualisieren:

               overlapping area
               |
               _ _
              |   |
              v   v
T h i s   i s   a n   e x a m p l e \0
^             ^
|             |
|             destination
|
source

Aufgrund der Überlappung ist das resultierende Verhalten undefiniert.

Zu den Standard-Bibliotheksfunktionen mit einer Einschränkung dieser Art gehören memcpy() , strcpy() , strcat() , sprintf() und sscanf() . Der Standard sagt von diesen und einigen anderen Funktionen:

Wenn zwischen überlappenden Objekten kopiert wird, ist das Verhalten undefiniert.

Die memmove() Funktion ist die Hauptausnahme dieser Regel. Seine Definition gibt an, dass sich die Funktion so verhält, als ob die Quelldaten zuerst in einen temporären Puffer kopiert und dann an die Zieladresse geschrieben wurden. Es gibt keine Ausnahme für überlappende Quell- und Zielregionen und auch keine Notwendigkeit. memmove() hat memmove() in solchen Fällen ein genau definiertes Verhalten.

Die Unterscheidung spiegelt eine Effizienz vs. allgemeiner Kompromiss. Kopieren, wie diese Funktionen ausgeführt werden, tritt normalerweise zwischen getrennten Speicherbereichen auf, und es ist oft möglich, zur Entwicklungszeit zu wissen, ob eine bestimmte Instanz des Speicherkopierens in dieser Kategorie liegt. Die Annahme, dass keine Überlappung vorliegt, führt zu vergleichsweise effizienteren Implementierungen, die nicht zuverlässig korrekte Ergebnisse liefern, wenn die Annahme nicht zutrifft. Die meisten C-Bibliotheksfunktionen sind für die effizienteren Implementierungen zugelassen, und memmove() füllt die Lücken aus und dient den Fällen, in denen sich Quelle und Ziel möglicherweise überschneiden. Um jedoch in allen Fällen die richtige Wirkung zu erzielen, muss es zusätzliche Tests durchführen und / oder eine vergleichsweise weniger effiziente Implementierung einsetzen.

Lesen eines nicht initialisierten Objekts, das nicht durch den Speicher gesichert wird

C11

Das Lesen eines Objekts führt zu undefiniertem Verhalten, wenn das Objekt 1 ist :

  • nicht initialisiert
  • definiert mit automatischer Speicherdauer
  • Seine Adresse wird nie vergeben

Die Variable a im folgenden Beispiel erfüllt alle diese Bedingungen:

void Function( void )
{
    int a;
    int b = a;
} 

1 (Zitiert aus: ISO: IEC 9899: 201X 6.3.2.1 Werte, Arrays und Funktionsbezeichner 2)
Wenn der Wert "lvalue" ein Objekt mit automatischer Speicherdauer bezeichnet, das mit der Registerspeicherklasse hätte deklariert werden können (seine Adresse wurde nie verwendet), und dieses Objekt ist nicht initialisiert (nicht mit einem Initialisierer deklariert und wurde vor der Verwendung nicht zugewiesen ) ist das Verhalten undefiniert.

Datenrennen

C11

Mit C11 wurde die Unterstützung für mehrere Ausführungsthreads eingeführt, wodurch Datenrennen möglich sind. Ein Programm enthält ein Datenrennen, wenn von zwei verschiedenen Threads auf ein Objekt 1 zugegriffen wird, wobei mindestens einer der Zugriffe nicht atomar ist, mindestens einer das Objekt ändert und die Semantik des Programms nicht gewährleistet, dass sich die beiden Zugriffe nicht überlappen zeitlich. 2 Beachten Sie, dass die tatsächliche Parallelität der beteiligten Zugriffe keine Bedingung für ein Datenrennen ist. Datenrennen decken eine breitere Klasse von Problemen ab, die sich aus (zulässigen) Inkonsistenzen in den Speicheransichten verschiedener Threads ergeben.

Betrachten Sie dieses Beispiel:

#include <threads.h>

int a = 0;

int Function( void* ignore )
{
    a = 1;

    return 0;
}

int main( void )
{
    thrd_t id;
    thrd_create( &id , Function , NULL );

    int b = a;

    thrd_join( id , NULL );
}

Der Haupt-Thread ruft thrd_create auf, um eine neue Thread-Funktion Function zu starten. Der zweite Thread ändert a und der Haupt-Thread liest a . Keiner dieser Zugriffe ist atomar, und die beiden Threads tun weder einzeln noch gemeinsam, um sicherzustellen, dass sie sich nicht überlappen, sodass es zu einem Datenrennen kommt.

Dieses Programm könnte den Datenwettlauf vermeiden

  • der Haupt - Thread könnte seinen Lesevorgang auszuführen a vor dem anderen Thread beginnen;
  • Der Haupt-Thread könnte ein Lesen von a nachdem er via thrd_join sichergestellt thrd_join dass der andere beendet wurde.
  • Die Threads konnten ihre Zugriffe über einen Mutex synchronisieren, wobei jeder diesen Mutex sperrte, bevor er auf a zugreift und ihn anschließend entsperrt.

Wie die Mutex-Option zeigt, muss beim Vermeiden eines Datenrennens nicht eine bestimmte Reihenfolge von Vorgängen sichergestellt werden, z. B. wenn der untergeordnete Thread a ändert, bevor der Hauptthread es liest. es genügt (um ein Datenrennen zu vermeiden), um sicherzustellen, dass für eine gegebene Ausführung ein Zugriff vor dem anderen erfolgt.


1 Objekt ändern oder lesen.

2 (Zitat aus ISO: IEC 9889: 201x, Abschnitt 5.1.2.4 "Ausführungen mit mehreren Threads und Datenrennen")
Die Ausführung eines Programms enthält ein Datenrennen, wenn es zwei widersprüchliche Aktionen in verschiedenen Threads enthält, von denen mindestens eine nicht atomar ist und keine der beiden vor dem anderen auftritt. Ein solches Datenrennen führt zu undefiniertem Verhalten.

Lese den Wert des freigegebenen Zeigers

Selbst wenn Sie nur den Wert eines Zeigers lesen, der freigegeben wurde (dh ohne den Zeiger dereferenzieren zu wollen), ist undefiniertes Verhalten (UB), z

char *p = malloc(5);
free(p);
if (p == NULL) /* NOTE: even without dereferencing, this may have UB */
{

}

Zitieren von ISO / IEC 9899: 2011 , Abschnitt 6.2.4 §2:

[…] Der Wert eines Zeigers wird unbestimmt, wenn das Objekt, auf das er zeigt (oder gerade vorbei ist), das Ende seiner Lebensdauer erreicht.

Die Verwendung von unbestimmtem Speicher für irgendetwas, einschließlich scheinbar harmlosen Vergleichs oder Arithmetik, kann undefiniertes Verhalten aufweisen, wenn der Wert eine Trap-Darstellung für den Typ sein kann.

Ändern Sie das String-Literal

In diesem Codebeispiel wird der Zeichenzeiger p auf die Adresse eines Zeichenfolgenlitals initialisiert. Der Versuch, das String-Literal zu ändern, hat ein undefiniertes Verhalten.

char *p = "hello world";
p[0] = 'H'; // Undefined behavior

Das direkte Ändern eines veränderlichen Arrays von char oder durch einen Zeiger ist natürlich kein undefiniertes Verhalten, auch wenn der Initialisierer eine Literalzeichenfolge ist. Folgendes ist in Ordnung:

char a[] = "hello, world";
char *p = a;

a[0] = 'H';
p[7] = 'W';

Das liegt daran, dass das String-Literal bei jeder Initialisierung des Arrays effektiv in das Array kopiert wird (einmal für Variablen mit statischer Dauer, jedes Mal, wenn das Array für Variablen mit automatischer oder Thread-Dauer erstellt wird - Variablen mit zugewiesener Dauer werden nicht initialisiert) und Es ist in Ordnung, den Inhalt des Arrays zu ändern.

Speicherplatz zweimal freigeben

Das doppelte Freigeben von Speicher ist undefiniertes Verhalten, z

int * x = malloc(sizeof(int));
*x = 9;
free(x);
free(x);

Zitat aus Standard (7.20.3.2. Die freie Funktion von C99):

Andernfalls ist das Verhalten undefiniert, wenn das Argument nicht mit einem Zeiger übereinstimmt, der zuvor von der Funktion calloc, malloc oder realloc zurückgegeben wurde.

Verwendung eines falschen Formatbezeichners in printf

Die Verwendung eines falschen Formatbezeichners im ersten Argument für printf ruft ein undefiniertes Verhalten auf. Der folgende Code ruft beispielsweise ein undefiniertes Verhalten auf:

long z = 'B';
printf("%c\n", z);

Hier ist ein anderes Beispiel

printf("%f\n",0);

Über der Codezeile ist undefiniertes Verhalten. %f erwartet ein Doppel. 0 ist jedoch vom Typ int .

Beachten Sie, dass Ihr Compiler normalerweise dazu beitragen kann, Fälle wie diese zu vermeiden, wenn Sie während des Kompilierens die entsprechenden Flags -Wformat ( -Wformat in clang und gcc ). Aus dem letzten Beispiel:

warning: format specifies type 'double' but the argument has type
      'int' [-Wformat]
    printf("%f\n",0);
            ~~    ^
            %d

Die Konvertierung zwischen Zeigertypen führt zu einem falsch ausgerichteten Ergebnis

Die folgende möglicherweise undefinierten Verhalten durch falsche Zeiger Ausrichtung:

 char *memory_block = calloc(sizeof(uint32_t) + 1, 1);
 uint32_t *intptr = (uint32_t*)(memory_block + 1);  /* possible undefined behavior */
 uint32_t mvalue = *intptr;

Das undefinierte Verhalten tritt auf, wenn der Zeiger konvertiert wird. Wenn gemäß C11 eine Konvertierung zwischen zwei Zeigertypen zu einem Ergebnis führt, das falsch ausgerichtet ist (6.3.2.3), ist das Verhalten undefiniert . Hier kann ein uint32_t eine Ausrichtung von 2 oder 4 erfordern.

calloc hingegen muss einen Zeiger zurückgeben, der für jeden Objekttyp geeignet ausgerichtet ist. Der memory_block ist also richtig ausgerichtet, um einen uint32_t in seinem Anfangsteil zu enthalten. In einem System, in dem uint32_t eine Ausrichtung von 2 oder 4 erfordert, ist memory_block + 1 eine ungerade Adresse und daher nicht richtig ausgerichtet.

Beachten Sie, dass der C-Standard fordert, dass bereits der Cast-Vorgang undefiniert ist. Dies ist memory_block + 1 , da auf Plattformen, auf denen Adressen segmentiert sind, die memory_block + 1 möglicherweise nicht einmal eine korrekte Darstellung als Ganzzahlzeiger hat.

Das Umwandeln von char * in Zeiger auf andere Typen ohne Rücksicht auf die Ausrichtungsanforderungen wird manchmal fälschlicherweise zum Dekodieren gepackter Strukturen wie Dateiheader oder Netzwerkpakete verwendet.

Sie können das undefinierte Verhalten vermeiden, das durch eine falsch ausgerichtete Zeigerkonvertierung entsteht, indem Sie memcpy :

memcpy(&mvalue, memory_block + 1, sizeof mvalue);

Hier findet keine Zeigerumwandlung nach uint32_t* statt und die Bytes werden nacheinander kopiert.

Dieser Kopiervorgang für unser Beispiel führt nur zu einem gültigen Wert von mvalue weil:

  • Wir haben calloc , damit die Bytes ordnungsgemäß initialisiert werden. In unserem Fall haben alle Bytes den Wert 0 , aber jede andere ordnungsgemäße Initialisierung würde dies tun.
  • uint32_t ist ein exakter Breitentyp und hat keine Füllbits
  • Jedes beliebige Bitmuster ist eine gültige Darstellung für jeden vorzeichenlosen Typ.

Addition oder Subtraktion des Zeigers nicht richtig begrenzt

Der folgende Code hat ein undefiniertes Verhalten:

char buffer[6] = "hello";
char *ptr1 = buffer - 1;  /* undefined behavior */
char *ptr2 = buffer + 5;  /* OK, pointing to the '\0' inside the array */
char *ptr3 = buffer + 6;  /* OK, pointing to just beyond */
char *ptr4 = buffer + 7;  /* undefined behavior */

Laut C11 ist das Verhalten undefiniert, wenn die Addition oder Subtraktion eines Zeigers in ein Array-Objekt oder einen Integer-Typ oder etwas darüber hinaus zu einem Ergebnis führt, das nicht auf das gleiche Array-Objekt oder knapp darüber hinausweist (6.5.6 ).

Außerdem ist es natürlich undefiniertes Verhalten, einen Zeiger zu demeferenzieren , der direkt hinter das Array zeigt:

char buffer[6] = "hello";
char *ptr3 = buffer + 6;  /* OK, pointing to just beyond */
char value = *ptr3;       /* undefined behavior */

Eine const-Variable mit einem Zeiger ändern

int main (void)
{
    const int foo_readonly = 10;
    int *foo_ptr;

    foo_ptr = (int *)&foo_readonly; /* (1) This casts away the const qualifier */
    *foo_ptr = 20; /* This is undefined behavior */

    return 0;
}

Zitieren von ISO / IEC 9899: 201x , Abschnitt 6.7.3, §2:

Wenn versucht wird, ein mit einem const-qualifiziertem Typ definiertes Objekt durch Verwendung eines lvalue mit einem nicht const-qualifizierten Typ zu ändern, ist das Verhalten nicht definiert. [...]


(1) In GCC kann dies die folgende Warnung warning: assignment discards 'const' qualifier from pointer target type [-Wdiscarded-qualifiers] : warning: assignment discards 'const' qualifier from pointer target type [-Wdiscarded-qualifiers]

Übergabe eines Nullzeigers an die Konvertierung von printf% s

Die %s Konvertierung von printf besagt, dass das entsprechende Argument ein Zeiger auf das Anfangselement eines Arrays vom Zeichentyp ist . Ein Nullzeiger zeigt nicht auf das Anfangselement eines Arrays von Zeichentypen, und daher ist das Verhalten des Folgenden nicht definiert:

char *foo = NULL;
printf("%s", foo); /* undefined behavior */

Das undefinierte Verhalten bedeutet jedoch nicht immer, dass das Programm abstürzt - einige Systeme ergreifen Schritte, um Abstürze zu vermeiden, die normalerweise auftreten, wenn ein Nullzeiger dereferenziert wird. Zum Beispiel ist bekannt, dass Glibc druckt

(null)

für den Code oben. Fügen Sie jedoch (nur) einen Zeilenumbruch zum Formatstring hinzu, und Sie erhalten einen Absturz:

char *foo = 0;
printf("%s\n", foo); /* undefined behavior */

In diesem Fall geschieht dies, weil GCC über eine Optimierung verfügt, die printf("%s\n", argument); in einen Aufruf an puts mit puts(argument) , und puts in Glibc nicht behandelt Null - Zeiger. All dieses Verhalten ist standardkonform.

Beachten Sie, dass sich der Nullzeiger von einer leeren Zeichenfolge unterscheidet . Das Folgende ist also gültig und hat kein undefiniertes Verhalten. Es wird nur eine neue Zeile gedruckt :

char *foo = "";
printf("%s\n", foo);

Inkonsistente Verknüpfung von Bezeichnern

extern int var;
static int var; /* Undefined behaviour */

In C11, § 6.2.2, 7 heißt es:

Wenn innerhalb einer Übersetzungseinheit derselbe Identifizierer sowohl mit interner als auch mit externer Verknüpfung erscheint, ist das Verhalten nicht definiert.

Wenn eine vorherige Deklaration eines Bezeichners sichtbar ist, wird die Verknüpfung der vorherigen Deklaration angezeigt. C11, §6.2.2, 4 erlaubt es:

Für einen mit dem Speicherklassenspezifizierer extern deklarierten Identifizierer in einem Bereich, in dem eine vorherige Deklaration dieses Identifizierers sichtbar ist, 31) Wenn die vorherige Deklaration interne oder externe Verknüpfung angibt, ist die Verknüpfung des Identifizierers bei der späteren Deklaration die gleiche wie bei die auf der vorherigen Erklärung angegebene Verbindung. Wenn keine vorherige Deklaration sichtbar ist oder wenn in der vorherigen Deklaration keine Verknüpfung angegeben ist, hat der Identifier eine externe Verknüpfung.

/* 1. This is NOT undefined */
static int var;
extern int var; 


/* 2. This is NOT undefined */
static int var;
static int var; 

/* 3. This is NOT undefined */
extern int var;
extern int var; 

Fflush für einen Eingabestrom verwenden

In den fflush und C-Standards wird explizit angegeben, dass die Verwendung von fflush in einem Eingabestrom undefiniertes Verhalten ist. Die fflush ist nur für Ausgabeströme definiert.

#include <stdio.h>

int main()
{
    int i;
    char input[4096];

    scanf("%i", &i);
    fflush(stdin); // <-- undefined behavior
    gets(input);

    return 0;
}

Es gibt keine Standardmethode, um ungelesene Zeichen aus einem Eingabestrom zu löschen. Auf der anderen Seite verwenden einige Implementierungen fflush , um den stdin Puffer zu löschen. Microsoft definiert das Verhalten von fflush in einem Eingabestrom: Wenn der Stream für die Eingabe fflush löscht fflush den Inhalt des Puffers. Gemäß POSIX.1-2008 ist das Verhalten von fflush definiert, es sei denn, die Eingabedatei kann gesucht werden.

Weitere fflush(stdin) .

Bitverschiebung mit negativen Zählwerten oder über die Breite des Typs hinaus

Wenn die Verschiebung Zählwert einen negativen Wert ist dann sowohl Linksverschiebung und rechte Shift - Operationen sind nicht definiert 1:

int x = 5 << -3; /* undefined */
int x = 5 >> -3; /* undefined */

Wenn die Linksverschiebung bei einem negativen Wert ausgeführt wird , ist dies undefiniert:

int x = -5 << 3; /* undefined */

Wenn ein positiver Wert nach links verschoben wird und das Ergebnis des mathematischen Werts nicht im Typ dargestellt werden kann, ist es undefiniert 1 :

/* Assuming an int is 32-bits wide, the value '5 * 2^72' doesn't fit 
 * in an int. So, this is undefined. */
       
int x = 5 << 72;

Beachten Sie, dass die Rechtsverschiebung bei einem negativen Wert (eg -5 >> 3 ) nicht undefiniert, sondern implementierungsdefiniert ist .


1 Zitat von ISO / IEC 9899: 201x , Abschnitt 6.5.7:

Wenn der Wert des rechten Operanden negativ ist oder größer oder gleich der Breite des beförderten linken Operanden ist, ist das Verhalten undefiniert.

Ändern der Zeichenfolge, die von den Funktionen getenv, strerror und setlocale zurückgegeben wird

Das Ändern der Zeichenfolgen, die von den Standardfunktionen getenv() , strerror() und setlocale() ist undefiniert. Implementierungen verwenden daher möglicherweise statischen Speicher für diese Zeichenfolgen.

Die Funktion getenv (), C11, §7.22.4.7, 4 sagt:

Die Funktion getenv gibt einen Zeiger auf eine Zeichenfolge zurück, die dem übereinstimmenden Listenmitglied zugeordnet ist. Die Zeichenfolge, auf die verwiesen wird, darf vom Programm nicht geändert werden, kann jedoch durch einen nachfolgenden Aufruf der Funktion getenv überschrieben werden.

Die Funktion strerror (), C11, §7.23.6.3, 4 sagt:

Die Strerror-Funktion gibt einen Zeiger auf den String zurück, dessen Inhalt localespeci fi c ist. Das Array, auf das gezeigt wird, darf vom Programm nicht geändert werden, kann jedoch durch einen nachfolgenden Aufruf der Strerror-Funktion überschrieben werden.

Die Funktion setlocale (), C11, §7.11.1.1, 8 sagt:

Der Zeiger auf die Zeichenfolge, die von der Funktion setlocale zurückgegeben wird, ist so, dass ein nachfolgender Aufruf mit diesem Zeichenfolgenwert und der zugehörigen Kategorie diesen Teil des Gebietsschemas des Programms wiederherstellt. Die Zeichenfolge, auf die verwiesen wird, darf vom Programm nicht geändert werden, kann jedoch durch einen nachfolgenden Aufruf der Funktion setlocale überschrieben werden.

Ebenso gibt die Funktion localeconv() einen Zeiger auf struct lconv der nicht geändert werden soll.

Die Funktion localeconv (), C11, §7.11.2.1, 8 sagt:

Die Funktion localeconv gibt einen Zeiger auf das ausgefüllte Objekt zurück. Die Struktur, auf die der Rückgabewert zeigt, wird vom Programm nicht geändert, kann jedoch durch einen nachfolgenden Aufruf der localeconv-Funktion überschrieben werden.

Rückkehr von einer Funktion, die mit dem _ _Noreturn`- oder `Noreturn'-Funktionsbezeichner deklariert wurde

C11

Der Funktionsbezeichner _Noreturn wurde in C11 eingeführt. Die Kopfzeile <stdnoreturn.h> enthält ein Makro noreturn das auf _Noreturn erweitert _Noreturn . Die Verwendung von _Noreturn oder noreturn von <stdnoreturn.h> ist also in Ordnung und gleichwertig.

Eine mit _Noreturn (oder noreturn ) deklarierte Funktion darf nicht zu ihrem Aufrufer zurückkehren. Wenn eine solche Funktion an seinen Aufrufer zurückkehrt, ist das Verhalten nicht definiert.

Im folgenden Beispiel wird func() mit dem noreturn deklariert, kehrt jedoch zu seinem Aufrufer zurück.

#include <stdio.h>
#include <stdlib.h>
#include <stdnoreturn.h>

noreturn void func(void);

void func(void)
{
    printf("In func()...\n");
} /* Undefined behavior as func() returns */

int main(void)
{
    func();
    return 0;
}

gcc und clang erzeugen Warnungen für das obige Programm:

$ gcc test.c
test.c: In function ‘func’:
test.c:9:1: warning: ‘noreturn’ function does return
 }
 ^
$ clang test.c
test.c:9:1: warning: function declared 'noreturn' should not return [-Winvalid-noreturn]
}
^

Ein Beispiel mit noreturn , das ein genau definiertes Verhalten aufweist:

#include <stdio.h>
#include <stdlib.h>
#include <stdnoreturn.h>

noreturn void my_exit(void);

/* calls exit() and doesn't return to its caller. */
void my_exit(void)
{
    printf("Exiting...\n");
    exit(0);
}

int main(void)
{
    my_exit();
    return 0;
}


Modified text is an extract of the original Stack Overflow Documentation
Lizenziert unter CC BY-SA 3.0
Nicht angeschlossen an Stack Overflow