¿Por qué es peligroso ir a Goto?
goto
no causa inestabilidad por sí mismo. A pesar de unos 100.000 goto
s, el kernel de Linux sigue siendo un modelo de estabilidad.
goto
por sí solo no debe causar vulnerabilidades de seguridad. Sin embargo, en algunos idiomas, mezclarlo con try
/ catch
bloques de administración de excepciones podría generar vulnerabilidades como se explica en esta recomendación del CERT . Los compiladores de C ++ convencionales señalan y evitan tales errores, pero desafortunadamente, los compiladores más antiguos o más exóticos no lo hacen.
goto
causa código ilegible e imposible de mantener. Esto también se llama código de espagueti , porque, como en un plato de espagueti, es muy difícil seguir el flujo de control cuando hay demasiados gotos.
Incluso si logras evitar el código de espagueti y si usas solo algunos gotos, todavía facilitan errores como la fuga de recursos:
- El código que usa programación de estructura, con bloques anidados claros y bucles o interruptores, es fácil de seguir; Su flujo de control es muy predecible. Por lo tanto, es más fácil garantizar que se respeten los invariantes.
- Con una
goto
declaración, rompes ese flujo directo y rompes las expectativas. Por ejemplo, es posible que no note que todavía tiene que liberar recursos.
- Muchos
goto
en diferentes lugares pueden enviarte a un solo objetivo de goto. Por lo tanto, no es obvio saber con certeza el estado en el que se encuentra al llegar a este lugar. El riesgo de hacer suposiciones erróneas / infundadas es, por lo tanto, bastante grande.
Información adicional y cotizaciones:
C proporciona la goto
declaración y etiquetas infinitamente abusivas a las que ramificarse. Formalmente, goto
nunca es necesario, y en la práctica casi siempre es fácil escribir código sin él. (...)
Sin embargo, sugeriremos algunas situaciones en las que los goto 's pueden encontrar un lugar. El uso más común es abandonar el procesamiento en algunas estructuras profundamente anidadas, como romper dos bucles a la vez. (...)
Aunque no somos dogmáticos sobre el asunto, parece que las declaraciones de goto deben usarse con moderación, si es que lo hacen .
¿Cuándo se puede usar goto?
Como K&R, no soy dogmático sobre los gotos. Admito que hay situaciones en las que goto podría facilitar la vida.
Por lo general, en C, goto permite la salida de bucle multinivel, o el manejo de errores que requiere alcanzar un punto de salida apropiado que libera / desbloquea todos los recursos asignados hasta ahora (es decir, la asignación múltiple en secuencia significa múltiples etiquetas). Este artículo cuantifica los diferentes usos del goto en el kernel de Linux.
Personalmente prefiero evitarlo y en 10 años de C, usé un máximo de 10 gotos. Prefiero usar if
s anidados , que creo que son más legibles. Cuando esto llevaría a un anidamiento demasiado profundo, optaría por descomponer mi función en partes más pequeñas o usar un indicador booleano en cascada. Los compiladores de optimización actuales son lo suficientemente inteligentes como para generar casi el mismo código que el mismo código goto
.
El uso de goto depende en gran medida del idioma:
En C ++, el uso adecuado de RAII hace que el compilador destruya automáticamente los objetos que quedan fuera del alcance, de modo que los recursos / bloqueo se limpiarán de todos modos, y ya no será necesario volver a usarlos.
En Java no hay necesidad de Goto (ver cita de autor de Java anterior y este excelente respuesta de desbordamiento de pila ): el recolector de basura que limpia el desorden, break
, continue
, y try
/ catch
manejo de excepciones cubre todo el caso en el que goto
puedan ser de utilidad, pero en un mejor y más seguro conducta. La popularidad de Java demuestra que la declaración goto se puede evitar en un lenguaje moderno.
Zoom sobre la famosa vulnerabilidad SSL goto fail
Descargo de responsabilidad importante: en vista de la feroz discusión en los comentarios, quiero aclarar que no pretendo que la declaración goto sea la única causa de este error. No pretendo que sin ir a Goto no habría ningún error. Solo quiero mostrar que un goto puede estar involucrado en un error grave.
No sé cuántos errores graves están relacionados goto
en la historia de la programación: los detalles a menudo no se comunican. Sin embargo, hubo un famoso error SSL de Apple que debilitó la seguridad de iOS. La declaración que condujo a este error fue una goto
declaración incorrecta .
Algunos argumentan que la causa raíz del error no fue la declaración goto en sí misma, sino una copia / pegar incorrecta, una sangría engañosa, falta de llaves alrededor del bloque condicional, o tal vez los hábitos de trabajo del desarrollador. Tampoco puedo confirmar ninguno de ellos: todos estos argumentos son hipótesis e interpretación probables. Nadie lo sabe realmente. ( mientras tanto, la hipótesis de una fusión que salió mal como alguien sugirió en los comentarios parece ser un muy buen candidato en vista de algunas otras inconsistencias de sangría en la misma función ).
El único hecho objetivo es que un duplicado goto
condujo a salir prematuramente de la función. Mirando el código, la única otra declaración única que podría haber causado el mismo efecto habría sido una devolución.
El error está en función SSLEncodeSignedServerKeyExchange()
en este archivo :
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
De hecho, las llaves alrededor del bloque condicional podrían haber evitado el error:
habría provocado un error de sintaxis en la compilación (y, por lo tanto, una corrección) o un goto inofensivo redundante. Por cierto, GCC 6 podría detectar estos errores gracias a su advertencia opcional para detectar sangría inconsistente.
Pero en primer lugar, todos estos problemas podrían haberse evitado con un código más estructurado. Entonces goto es al menos indirectamente una causa de este error. Hay al menos dos formas diferentes que podrían haberlo evitado:
Enfoque 1: si la cláusula if
es anidada
En lugar de probar muchas condiciones de error secuencialmente, y cada vez que se envía a una fail
etiqueta en caso de problema, uno podría haber optado por ejecutar las operaciones criptográficas en una if
declaración que lo haría solo si no hubiera una precondición incorrecta:
if ((err = ReadyHash(&SSLHashSHA1, &hashCtx)) == 0 &&
(err = ...) == 0 ) &&
(err = ReadyHash(&SSLHashSHA1, &hashCtx)) == 0) &&
...
(err = ...) == 0 ) )
{
... // Do some cryptographic operations here
}
... // Free resources
Enfoque 2: use un acumulador de errores
Este enfoque se basa en el hecho de que casi todas las declaraciones aquí llaman a alguna función para establecer un err
código de error y ejecutan el resto del código solo si err
fue 0 (es decir, la función se ejecutó sin error). Una buena alternativa segura y legible es:
bool ok = true;
ok = ok && (err = ReadyHash(&SSLHashSHA1, &hashCtx))) == 0;
ok = ok && (err = NextFunction(...)) == 0;
...
ok = ok && (err = ...) == 0;
... // Free resources
Aquí, no hay un solo goto: no hay riesgo de saltar rápidamente al punto de salida de falla. Y visualmente sería fácil detectar una línea desalineada o olvidada ok &&
.
Esta construcción es más compacta. Se basa en el hecho de que en C, la segunda parte de un lógico y ( &&
) se evalúa solo si la primera parte es verdadera. De hecho, el ensamblador generado por un compilador optimizador es casi equivalente al código original con gotos: el optimizador detecta muy bien la cadena de condiciones y genera código, que en el primer valor de retorno no nulo salta al final ( prueba en línea ).
Incluso podría prever una comprobación de coherencia al final de la función que podría identificar durante la fase de prueba las discrepancias entre el indicador ok y el código de error.
assert( (ok==false && err!=0) || (ok==true && err==0) );
Errores tales como un ==0
reemplazo inadvertido por un !=0
error de conector lógico se detectarían fácilmente durante la fase de depuración.
Como dije: no pretendo que las construcciones alternativas hubieran evitado cualquier error. Solo quiero decir que podrían haber hecho que el error sea más difícil de producir.