Warum ist es gefährlich?
goto
verursacht keine Instabilität von selbst. Trotz etwa 100.000 goto
s ist der Linux-Kernel immer noch ein Stabilitätsmodell.
goto
an sich sollte keine Sicherheitslücken verursachen. In einigen Sprachen kann das Mischen mit try
/ catch
exception-Verwaltungsblöcken jedoch zu Schwachstellen führen, wie in dieser CERT-Empfehlung erläutert . Mainstream-C ++ - Compiler kennzeichnen und verhindern solche Fehler, ältere oder exotischere Compiler jedoch nicht.
goto
verursacht unlesbaren und nicht wartbaren Code. Dies wird auch als Spaghetti-Code bezeichnet , da es wie bei einer Spaghetti-Platte sehr schwierig ist, den Kontrollfluss zu verfolgen, wenn zu viele GOTOS vorhanden sind.
Selbst wenn Sie es schaffen, Spaghetti-Code zu vermeiden, und wenn Sie nur ein paar GOTOS verwenden, können Fehler wie und Ressourcenlecks auftreten:
- Code unter Verwendung der Strukturprogrammierung mit klar verschachtelten Blöcken und Schleifen oder Schaltern ist leicht zu befolgen. Der Kontrollfluss ist sehr vorhersehbar. Es ist daher einfacher sicherzustellen, dass Invarianten respektiert werden.
- Mit einer
goto
Aussage brechen Sie diesen direkten Fluss und brechen die Erwartungen. Beispielsweise werden Sie möglicherweise nicht bemerken, dass Sie noch Ressourcen freigeben müssen.
- Viele
goto
an verschiedenen Orten können Sie zu einem einzelnen Ziel senden. Daher ist es nicht offensichtlich, dass Sie genau wissen, in welchem Zustand Sie sich befinden, wenn Sie diesen Ort erreichen. Das Risiko, falsche / unbegründete Annahmen zu treffen, ist daher recht groß.
Zusätzliche Informationen und Zitate:
C bietet die unbegrenzt missbräuchliche goto
Anweisung und Beschriftungen, zu denen verzweigt werden kann. Formal goto
ist das nie notwendig und in der Praxis ist es fast immer einfach, Code ohne es zu schreiben. (...)
Dennoch werden wir einige Situationen vorschlagen, in denen Goto einen Platz finden kann. Am häufigsten wird die Verarbeitung in einigen tief verschachtelten Strukturen abgebrochen, z. B. wenn zwei Schleifen gleichzeitig abgebrochen werden. (...)
Obwohl wir in dieser Angelegenheit nicht dogmatisch sind, sollten goto-Anweisungen, wenn überhaupt, sparsam verwendet werden .
Wann kann verwendet werden?
Wie K & R bin ich nicht dogmatisch in Bezug auf Gotos. Ich gebe zu, dass es Situationen gibt, in denen man sich das Leben erleichtern könnte.
In der Regel ermöglicht goto in C das Verlassen einer mehrstufigen Schleife oder die Fehlerbehandlung, wobei ein geeigneter Austrittspunkt erreicht werden muss, der alle bisher zugewiesenen Ressourcen freigibt / entsperrt (eine mehrfache Zuweisung in Folge bedeutet mehrere Bezeichnungen). In diesem Artikel werden die verschiedenen Verwendungen von goto im Linux-Kernel quantifiziert.
Persönlich bevorzuge ich es zu vermeiden und in 10 Jahren von C habe ich maximal 10 gotos verwendet. Ich bevorzuge geschachtelte if
s, die meiner Meinung nach besser lesbar sind. Wenn dies zu einer zu tiefen Verschachtelung führen würde, würde ich entweder meine Funktion in kleinere Teile zerlegen oder einen Booleschen Indikator in einer Kaskade verwenden. Die heutigen Compiler zur Optimierung sind clever genug, um fast den gleichen Code wie mit demselben Code zu generieren goto
.
Die Verwendung von goto hängt stark von der Sprache ab:
In C ++ führt die ordnungsgemäße Verwendung von RAII dazu, dass der Compiler Objekte, die außerhalb des Gültigkeitsbereichs liegen, automatisch zerstört, sodass die Ressourcen / Sperren ohnehin bereinigt werden und keine Notwendigkeit mehr besteht, weiterzugehen.
In Java gibt es keine Notwendigkeit für goto (siehe Java des Autor Zitat oben und diese ausgezeichnete Stack - Überlaufes Antwort ): der Garbage Collector, der das Chaos reinigt, break
, continue
und try
/ catch
Ausnahmebehandlung alles den Fall abzudecken , wo goto
hilfreich sein könnte, aber in einem sichereren und besser Weise. Javas Popularität beweist, dass man in einer modernen Sprache eine goto-Aussage vermeiden kann.
Zoomen Sie auf die berühmte Sicherheitsanfälligkeit " SSL goto fail"
Wichtiger Haftungsausschluss: Angesichts der heftigen Diskussion in den Kommentaren möchte ich klarstellen, dass ich nicht vorgebe, die goto-Anweisung sei die einzige Ursache für diesen Fehler. Ich behaupte nicht, dass es ohne goto keinen Bug geben würde. Ich möchte nur zeigen, dass ein Goto in einen schwerwiegenden Fehler verwickelt sein kann.
Ich weiß nicht, wie viele schwerwiegende Fehler goto
in der Geschichte der Programmierung vorkommen: Details werden oft nicht mitgeteilt. Es gab jedoch einen berühmten Apple SSL-Fehler , der die Sicherheit von iOS beeinträchtigte. Die Aussage, die zu diesem Fehler führte, war eine falsche goto
Aussage.
Einige argumentieren, dass die Hauptursache des Fehlers nicht die goto-Anweisung an sich war, sondern ein falsches Kopieren / Einfügen, ein irreführender Einzug, fehlende geschweifte Klammern um den bedingten Block oder möglicherweise die Arbeitsgewohnheiten des Entwicklers. Ich kann keines von ihnen bestätigen: Alle diese Argumente sind wahrscheinliche Hypothesen und Interpretationen. Niemand weiß es wirklich. (In der Zwischenzeit scheint die in den Kommentaren vorgeschlagene Hypothese einer fehlerhaften Zusammenführung angesichts einiger anderer Inkonsistenzen bei Einrückungen in derselben Funktion ein sehr guter Kandidat zu sein ).
Die einzige objektive Tatsache ist, dass ein Duplikat dazu goto
geführt hat, die Funktion vorzeitig zu verlassen. Betrachtet man den Code, wäre die einzige andere Anweisung, die den gleichen Effekt hätte erzielen können, eine Rückkehr gewesen.
Der Fehler ist SSLEncodeSignedServerKeyExchange()
in dieser Datei in Funktion :
if ((err = ReadyHash(&SSLHashSHA1, &hashCtx)) != 0)
goto fail;
if ((err =...) !=0)
goto fail;
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
goto fail;
goto fail; // <====OUCH: INDENTATION MISLEADS: THIS IS UNCONDITIONDAL!!
if (...)
goto fail;
... // Do some cryptographic operations here
fail:
... // Free resources to process error
In der Tat hätten geschweifte Klammern um den bedingten Block den Fehler verhindern können:
Es hätte entweder zu einem Syntaxfehler beim Kompilieren (und damit zu einer Korrektur) oder zu einem redundanten harmlosen goto geführt. Übrigens könnte GCC 6 diese Fehler dank seiner optionalen Warnung zur Erkennung inkonsistenter Einrückungen erkennen.
Aber in erster Linie hätten alle diese Goto mit strukturiertem Code vermieden werden können. Goto ist also zumindest indirekt eine Ursache für diesen Bug. Es gibt mindestens zwei verschiedene Möglichkeiten, um dies zu vermeiden:
Ansatz 1: if-Klausel oder verschachtelte if
s
Anstatt viele Bedingungen nacheinander auf Fehler zu prüfen und fail
im Problemfall jedes Mal an ein Etikett zu senden , hätte man sich dafür entscheiden können, die kryptografischen Operationen in einer if
-Anweisung auszuführen, die dies nur dann tun würde, wenn es keine falsche Vorbedingung gäbe:
if ((err = ReadyHash(&SSLHashSHA1, &hashCtx)) == 0 &&
(err = ...) == 0 ) &&
(err = ReadyHash(&SSLHashSHA1, &hashCtx)) == 0) &&
...
(err = ...) == 0 ) )
{
... // Do some cryptographic operations here
}
... // Free resources
Ansatz 2: Verwenden Sie einen Fehlerakku
Dieser Ansatz basiert auf der Tatsache, dass fast alle Anweisungen hier eine Funktion aufrufen, um einen err
Fehlercode festzulegen, und den Rest des Codes nur ausführen, wenn err
0 war (dh die Funktion wurde ohne Fehler ausgeführt). Eine schöne sichere und lesbare Alternative ist:
bool ok = true;
ok = ok && (err = ReadyHash(&SSLHashSHA1, &hashCtx))) == 0;
ok = ok && (err = NextFunction(...)) == 0;
...
ok = ok && (err = ...) == 0;
... // Free resources
Hier gibt es kein einziges Goto: Kein Risiko, schnell zum Ausfallausgangspunkt zu springen. Und visuell wäre es einfach, eine falsch ausgerichtete Linie oder eine vergessene zu erkennen ok &&
.
Dieses Konstrukt ist kompakter. Es basiert auf der Tatsache, dass in C der zweite Teil eines logischen und ( &&
) nur ausgewertet wird, wenn der erste Teil wahr ist. Tatsächlich entspricht der von einem optimierenden Compiler erzeugte Assembler fast dem ursprünglichen Code mit gotos: Der Optimierer erkennt die Kette von Bedingungen sehr gut und generiert Code, der beim ersten Rückgabewert ungleich Null ans Ende springt ( online proof ).
Sie können sich sogar eine Konsistenzprüfung am Ende der Funktion vorstellen, mit der während der Testphase Unstimmigkeiten zwischen dem OK-Flag und dem Fehlercode festgestellt werden können.
assert( (ok==false && err!=0) || (ok==true && err==0) );
Fehler wie ein ==0
versehentlich durch einen !=0
oder logische Konnektorfehler werden während der Debugging-Phase leicht erkannt.
Wie gesagt: Ich behaupte nicht, dass alternative Konstrukte jeden Fehler vermieden hätten. Ich möchte nur sagen, dass sie das Auftreten des Fehlers erschweren könnten.