Willst du einen Detektiv spielen? Finden Sie den Fehler in einer Funktion von Midnight Commander

Fehler

In diesem Artikel laden wir Sie ein, zu versuchen, einen Fehler in einer sehr einfachen Funktion aus dem GNU Midnight Commander-Projekt zu finden. Warum? Ohne besonderen Grund. Nur zum Spaß. Okay, es ist eine Lüge. Wir wollten Ihnen tatsächlich einen weiteren Fehler zeigen, den ein menschlicher Prüfer nur schwer finden kann und den der statische Code-Analysator PVS-Studio mühelos erkennen kann.

Ein Benutzer hat uns neulich eine E-Mail gesendet und gefragt, warum er eine Warnung zur Funktion EatWhitespace erhalten hat (siehe Code unten). Diese Frage ist nicht so trivial, wie es scheinen mag. Versuchen Sie selbst herauszufinden, was mit diesem Code nicht stimmt.

static int EatWhitespace (FILE * InFile) /* ----------------------------------------------------------------------- ** * Scan past whitespace (see ctype(3C)) and return the first non-whitespace * character, or newline, or EOF. * * Input: InFile - Input source. * * Output: The next non-whitespace character in the input stream. * * Notes: Because the config files use a line-oriented grammar, we * explicitly exclude the newline character from the list of * whitespace characters. * - Note that both EOF (-1) and the nul character ('\0') are * considered end-of-file markers. * * ----------------------------------------------------------------------- ** */ { int c; for (c = getc (InFile); isspace (c) && ('\n' != c); c = getc (InFile)) ; return (c); } /* EatWhitespace */ 

Wie Sie sehen können, ist EatWhitespace eine winzige Funktion. sein Körper ist noch kleiner als der Kommentar dazu :). Lassen Sie uns nun einige Details überprüfen.

Hier ist die Beschreibung der Funktion getc :

 int getc ( FILE * stream ); 

Gibt das Zeichen zurück, auf das der interne Dateipositionsindikator des angegebenen Streams aktuell zeigt. Die interne Dateipositionsanzeige wird dann zum nächsten Zeichen vorgerückt. Befindet sich der Stream beim Aufruf am Ende der Datei, gibt die Funktion EOF zurück und setzt den Indikator für das Dateiende für den Stream. Wenn ein Lesefehler auftritt, gibt die Funktion EOF zurück und setzt die Fehleranzeige für den Stream (Fehler).

Und hier ist die Beschreibung der Funktion isspace :

 int isspace( int ch ); 

Überprüft, ob das angegebene Zeichen ein Leerzeichen ist, das vom aktuell installierten C-Gebietsschema klassifiziert wird. Im Standardgebietsschema lauten die Leerzeichen wie folgt:

  • Leerzeichen (0x20, '');
  • Formular-Feed (0x0c, '\ f');
  • Zeilenvorschub LF (0x0a, '\ n');
  • Wagenrücklauf CR (0x0d, '\ r');
  • horizontale Lasche (0x09, '\ t');
  • vertikale Registerkarte (0x0b, '\ v').

Rückgabewert Wert ungleich Null, wenn das Zeichen ein Leerzeichen ist; sonst Null.

Es wird erwartet, dass die EatWhitespace- Funktion alle Whitespace-Zeichen außer dem Zeilenvorschub '\ n' überspringt. Die Funktion hört auch auf, aus der Datei zu lesen, wenn sie auf das Dateiende (EOF) stößt.

Versuchen Sie nun, den Fehler zu finden, nachdem Sie das alles wissen!

Die beiden folgenden Einhörner stellen sicher, dass Sie nicht versehentlich auf den Kommentar schauen.

Abbildung 1. Zeit für die Fehlersuche. Die Einhörner warten.


Abbildung 1. Zeit für die Fehlersuche. Die Einhörner warten.

Immer noch kein Glück?

Siehst du, das liegt daran, dass wir dich wegen isspace belogen haben. Bwa-ha-ha! Es ist überhaupt keine Standardfunktion - es ist ein benutzerdefiniertes Makro. Ja, wir sind Bösewichte und wir haben dich verwirrt.

Abbildung 2. Einhorn verwirrt Leser über isspace.


Abbildung 2. Einhorn verwirrt Leser über isspace .

Natürlich sind nicht wir oder unser Einhorn schuld. Der Fehler für all die Verwirrung liegt bei den Autoren des GNU Midnight Commander-Projekts, die ihre eigene Implementierung von isspace in der Datei charset.h vorgenommen haben:

 #ifdef isspace #undef isspace #endif .... #define isspace(c) ((c)==' ' || (c) == '\t') 

Mit diesem Makro haben die Autoren andere Entwickler verwirrt. Der Code wurde unter der Annahme geschrieben, dass isspace eine Standardfunktion ist, die Wagenrücklauf (0x0d, '\ r') als Leerzeichen betrachtet.

Das benutzerdefinierte Makro behandelt seinerseits nur Leerzeichen und Tabulatorzeichen als Leerzeichen. Lassen Sie uns dieses Makro ersetzen und sehen, was passiert.

 for (c = getc (InFile); ((c)==' ' || (c) == '\t') && ('\n' != c); c = getc (InFile)) 

Der Unterausdruck ('\ n'! = C) ist nicht erforderlich (redundant), da er immer als wahr ausgewertet wird. Das warnt Sie vor PVS-Studio, indem Sie die Warnung ausgeben:

V560 Ein Teil des bedingten Ausdrucks ist immer wahr: ('\ n'! = C). params.c 136.

Um dies zu verdeutlichen, untersuchen wir drei mögliche Ergebnisse:

  • Dateiende erreicht. EOF ist kein Leerzeichen oder Tabulatorzeichen. Der Unterausdruck ('\ n'! = C) wird aufgrund einer Kurzschlussauswertung nicht ausgewertet. Die Schleife wird beendet.
  • Die Funktion hat ein Zeichen gelesen, das kein Leerzeichen oder Tabulatorzeichen ist. Der Unterausdruck ('\ n'! = C) wird aufgrund einer Kurzschlussauswertung nicht ausgewertet. Die Schleife wird beendet.
  • Die Funktion hat ein Leerzeichen oder ein horizontales Tabulatorzeichen gelesen. Der Unterausdruck ('\ n'! = C) wird ausgewertet, sein Ergebnis ist jedoch immer wahr.

Mit anderen Worten, der obige Code entspricht dem Folgenden:

 for (c = getc (InFile); c==' ' || c == '\t'; c = getc (InFile)) 

Wir haben festgestellt, dass es nicht auf die gewünschte Weise funktioniert. Nun wollen wir sehen, was die Auswirkungen sind.

Ein Entwickler, der den Aufruf von isspace in den Hauptteil der EatWhitespace- Funktion geschrieben hat, hat erwartet, dass die Standardfunktion aufgerufen wird. Aus diesem Grund haben sie die Bedingung hinzugefügt, die verhindert, dass das LF-Zeichen ('\ n') als Leerzeichen behandelt wird.

Dies bedeutet, dass neben Leerzeichen und horizontalen Tabulatorzeichen auch Formularvorschub- und vertikale Tabulatorzeichen übersprungen werden sollten.

Bemerkenswerter ist, dass das Wagenrücklaufzeichen (0x0d, '\ r') ebenfalls übersprungen werden soll. Es passiert jedoch nicht - die Schleife endet, wenn Sie auf diesen Charakter stoßen. Das Programm verhält sich unerwartet, wenn Zeilenumbrüche durch die CR + LF-Sequenz dargestellt werden, die in einigen Nicht-UNIX-Systemen wie Microsoft Windows verwendet wird.

Weitere Informationen zu den historischen Gründen für die Verwendung von LF oder CR + LF als Zeilenumbruchzeichen finden Sie auf der Wikipedia-Seite " Zeilenumbruch ".

Die EatWhitespace- Funktion sollte Dateien auf die gleiche Weise verarbeiten, unabhängig davon, ob sie LF oder CR + LF als Zeilenumbruchzeichen verwenden. Bei CR + LF schlägt dies jedoch fehl. Mit anderen Worten, wenn Ihre Datei aus der Windows-Welt stammt, sind Sie in Schwierigkeiten :).

Obwohl dies möglicherweise kein schwerwiegender Fehler ist, insbesondere wenn man bedenkt, dass GNU Midnight Commander in UNIX-ähnlichen Betriebssystemen verwendet wird, in denen LF (0x0a, '\ n') als Zeilenumbruchzeichen verwendet wird, führen solche Kleinigkeiten immer noch zu Ärger Probleme mit der Kompatibilität von Daten, die unter Linux und Windows erstellt wurden.

Was diesen Fehler interessant macht, ist, dass Sie ihn bei der Standard-Codeüberprüfung mit ziemlicher Sicherheit übersehen werden. Die Einzelheiten der Implementierung des Makros sind leicht zu vergessen, und einige Projektautoren kennen sie möglicherweise überhaupt nicht. Es ist ein sehr anschauliches Beispiel dafür, wie die statische Code-Analyse zur Codeüberprüfung und anderen Techniken zur Fehlererkennung beiträgt.

Das Überschreiben von Standardfunktionen ist eine schlechte Praxis. Übrigens haben wir einen ähnlichen Fall des Makros #define sprintf std :: printf im kürzlich erschienenen Artikel " Appreciate Static Code Analysis " erörtert.

Eine bessere Lösung wäre gewesen, dem Makro einen eindeutigen Namen zu geben, z. B. is_space_or_tab . Dies hätte dazu beigetragen, die Verwirrung zu vermeiden.

Möglicherweise war die Standard- Isspace- Funktion zu langsam und der Programmierer erstellte eine schnellere Version, die für ihre Anforderungen ausreichend war. Aber sie hätten es trotzdem nicht so machen sollen. Eine sicherere Lösung wäre, isspace so zu definieren, dass Sie nicht kompilierbaren Code erhalten, während die gewünschte Funktionalität als Makro mit einem eindeutigen Namen implementiert werden könnte.

Danke fürs Lesen. Zögern Sie nicht, PVS-Studio herunterzuladen und mit Ihren Projekten zu testen. Zur Erinnerung, wir unterstützen jetzt auch Java.

Source: https://habr.com/ru/post/de439372/


All Articles