Pourquoi est-ce que c'est dangereux?
goto
ne provoque pas l'instabilité par lui-même. Malgré environ 100 000 goto
secondes, le noyau Linux reste un modèle de stabilité.
goto
à lui seul ne devrait pas causer de failles de sécurité Cependant, dans certaines langues, le fait de le mélanger avec des blocs de gestion try
/ catch
exception pourrait conduire à des vulnérabilités, comme expliqué dans cette recommandation CERT . Les compilateurs C ++ traditionnels signalent et empêchent de telles erreurs, mais malheureusement, les compilateurs plus anciens ou plus exotiques ne le font pas.
goto
provoque un code illisible et non maintenable. C'est ce qu'on appelle aussi le code spaghetti , parce que, comme dans une assiette à spaghetti, il est très difficile de suivre le flux de contrôle quand il y a trop de gotos.
Même si vous réussissez à éviter le code spaghetti et si vous n'utilisez que quelques gotos, ils facilitent tout de même les problèmes de type bugs et fuites de ressources:
- La programmation de la structure de code, avec des blocs et des boucles ou des commutateurs clairement imbriqués, est facile à suivre; son flux de contrôle est très prévisible. Il est donc plus facile de s'assurer que les invariants sont respectés.
- Avec une
goto
déclaration, vous interrompez ce flux simple et les attentes. Par exemple, vous ne remarquerez peut-être pas qu'il vous reste encore des ressources à libérer.
- Beaucoup
goto
dans des endroits différents peuvent vous envoyer à une cible unique. Il n'est donc pas évident de savoir avec certitude l'état dans lequel vous vous trouvez lorsque vous atteignez cet endroit. Le risque de faire des hypothèses erronées / non fondées est donc assez grand.
Informations complémentaires et citations:
C fournit la goto
déclaration infiniment abusable et les étiquettes vers lesquelles créer une branche. Formellement, cela goto
n’est jamais nécessaire et, dans la pratique, il est presque toujours facile d’écrire du code sans celui-ci. (...)
Néanmoins, nous suggérons quelques situations où les goto peuvent trouver une place. L'utilisation la plus courante consiste à abandonner le traitement dans certaines structures profondément imbriquées, telles que la rupture de deux boucles à la fois. (...)
Bien que nous ne soyons pas dogmatiques à ce sujet, il semble que les déclarations goto devraient être utilisées avec parcimonie, voire pas du tout .
Quand peut-on utiliser?
Comme K & R, je ne suis pas dogmatique à propos de gotos. J'admets qu'il y a des situations où aller se détendre peut être facilité.
Généralement, en C, goto autorise la sortie de boucle multiniveau ou la gestion des erreurs nécessitant d'atteindre un point de sortie approprié qui libère / déverrouille toutes les ressources allouées jusqu'à présent (une allocation multiple en séquence signifie plusieurs libellés). Cet article quantifie les différentes utilisations du goto dans le noyau Linux.
Personnellement, je préfère l'éviter et en 10 ans de C, j'ai utilisé un maximum de 10 gotos. Je préfère utiliser des if
s imbriqués , que je pense plus lisibles. Lorsque cela conduirait à une imbrication trop profonde, je choisirais soit de décomposer ma fonction en parties plus petites, soit d'utiliser un indicateur booléen en cascade. Les compilateurs d'optimisation actuels sont suffisamment intelligents pour générer presque le même code que le même code goto
.
L'utilisation de goto dépend fortement de la langue:
En C ++, l'utilisation correcte de RAII amène le compilateur à détruire automatiquement les objets qui sortent du domaine, de sorte que les ressources / le verrou soient nettoyés de toute façon, et qu'il n'y ait plus vraiment besoin de goto.
En Java , il n'y a pas besoin de goto (voir la citation de l' auteur de Java ci - dessus et cette excellente pile réponse Overflow ): le collecteur d'ordures qui nettoie le désordre, break
, continue
et try
/ catch
couvrir la gestion des exceptions tous les cas où goto
pourrait être utile, mais dans un monde plus sûr et mieux manière. La popularité de Java prouve que la déclaration goto peut être évitée dans un langage moderne.
Zoom sur la fameuse vulnérabilité SSL goto fail
Important Disclaimer: compte tenu de la discussion acharnée dans les commentaires, je tiens à préciser que je ne prétends pas que la déclaration goto est la seule cause de ce bogue. Je ne prétends pas que sans goto il n'y aurait pas de problème. Je veux juste montrer qu'un goto peut être impliqué dans un bug sérieux.
Je ne sais pas à combien de bugs sérieux sont liés goto
dans l'histoire de la programmation: les détails ne sont souvent pas communiqués. Cependant, il y avait un fameux bogue Apple SSL qui affaiblissait la sécurité d'iOS. La déclaration qui a conduit à ce bogue était une goto
déclaration fausse .
Certains avancent que la cause principale du bogue n’était pas l’instruction goto en soi, mais un copier / coller incorrect, une indentation trompeuse, des accolades manquantes autour du bloc conditionnel ou peut-être les habitudes de travail du développeur. Je ne peux en confirmer aucun: tous ces arguments sont des hypothèses probables et une interprétation. Personne ne sait vraiment. ( entre temps, l'hypothèse d'une fusion qui a mal tourné, comme le suggère quelqu'un dans les commentaires, semble être un très bon candidat compte tenu d'autres incohérences d'indentation dans la même fonction ).
Le seul fait objectif est qu'un doublon a goto
conduit à quitter la fonction prématurément. En regardant le code, la seule autre déclaration qui aurait pu causer le même effet aurait été un retour.
L'erreur est en fonction SSLEncodeSignedServerKeyExchange()
dans ce fichier :
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
En effet, des accolades autour du bloc conditionnel auraient pu empêcher le bogue:
cela aurait entraîné soit une erreur de syntaxe lors de la compilation (et donc une correction), soit un goto redondant et inoffensif. À propos, GCC 6 serait en mesure de détecter ces erreurs grâce à son avertissement facultatif pour détecter une indentation incohérente.
Mais en premier lieu, tous ces gotos auraient pu être évités avec un code plus structuré. Donc, goto est au moins indirectement une cause de ce bogue. Il y a au moins deux manières différentes qui auraient pu l'éviter:
Approche 1: si clause ou if
s imbriqué
Au lieu de tester séquentiellement de nombreuses conditions d'erreur et d'envoyer chaque fois à une fail
étiquette en cas de problème, on aurait pu opter pour l'exécution des opérations cryptographiques dans une if
instruction qui ne le ferait que s'il n'y avait pas de précondition erronée:
if ((err = ReadyHash(&SSLHashSHA1, &hashCtx)) == 0 &&
(err = ...) == 0 ) &&
(err = ReadyHash(&SSLHashSHA1, &hashCtx)) == 0) &&
...
(err = ...) == 0 ) )
{
... // Do some cryptographic operations here
}
... // Free resources
Approche 2: utiliser un accumulateur d'erreur
Cette approche est basée sur le fait que presque toutes les instructions ici appellent une fonction pour définir un err
code d'erreur et n'exécutent le reste du code que si err
0 (c'est-à-dire une fonction exécutée sans erreur). Une alternative sûre et lisible est:
bool ok = true;
ok = ok && (err = ReadyHash(&SSLHashSHA1, &hashCtx))) == 0;
ok = ok && (err = NextFunction(...)) == 0;
...
ok = ok && (err = ...) == 0;
... // Free resources
Ici, il n’ya pas un seul goto: aucun risque de sauter rapidement au point de sortie de la panne. Et visuellement, il serait facile de repérer une ligne mal alignée ou une ligne oubliée ok &&
.
Cette construction est plus compacte. Il est basé sur le fait que dans C, la deuxième partie d'un logique and ( &&
) n'est évaluée que si la première partie est vraie. En fait, l'assembleur généré par un compilateur optimiseur est presque équivalent au code original avec gotos: l'optimiseur détecte très bien la chaîne de conditions et génère du code qui, à la première valeur de retour non nulle, saute à la fin ( preuve en ligne ).
Vous pourriez même envisager une vérification de cohérence à la fin de la fonction qui pourrait, au cours de la phase de test, identifier des incohérences entre l'indicateur ok et le code d'erreur.
assert( (ok==false && err!=0) || (ok==true && err==0) );
Des erreurs telles qu'un ==0
remplacement par inadvertance par un !=0
connecteur logique ou des erreurs pourraient facilement être repérées pendant la phase de débogage.
Comme dit: je ne prétends pas que des constructions alternatives auraient évité tout bug. Je veux juste dire qu'ils auraient pu rendre le virus plus difficile à produire.