Siguiendo una pregunta mía anterior , la mayoría de los comentarios dicen "simplemente no lo hagas, estás en un estado de limbo, tienes que matar todo y empezar de nuevo". También hay una solución alternativa "segura" .
Lo que no entiendo es por qué una falla de segmentación es inherentemente irrecuperable.
Momento en el que se capta la escritura en memoria protegida, de lo contrario no se enviaría el SIGSEGV .
Si se puede capturar el momento de escribir en la memoria protegida, no veo por qué, en teoría, no se puede revertir, en un nivel bajo, y convertir el SIGSEGV en una excepción de software estándar.
Explique por qué después de una falla de segmentación, el programa está en un estado indeterminado, ya que, obviamente, la falla se produce antes de que se cambie la memoria (probablemente estoy equivocado y no veo por qué). Si se hubiera lanzado después, se podría crear un programa que cambie la memoria protegida, un byte a la vez, obteniendo fallas de segmentación y, finalmente, reprogramando el kernel, un riesgo de seguridad que no está presente, como podemos ver, el mundo sigue en pie.
SIGSEGV )?¿Cuándo ocurre exactamente la falla de segmentación (= cuándo se envía SIGSEGV)?
Cuando intenta acceder a la memoria a la que no tiene acceso, como acceder a una matriz fuera de los límites o eliminar la referencia de un puntero no válido. La señal SIGSEGV está estandarizada, pero diferentes sistemas operativos pueden implementarla de manera diferente. "Error de segmentación" es principalmente un término utilizado en los sistemas * nix, Windows lo llama "infracción de acceso".
¿Por qué el proceso está en un estado de comportamiento indefinido después de ese punto?
Porque una o varias de las variables del programa no se comportaron como se esperaba. Digamos que tiene una matriz que se supone que debe almacenar una cantidad de valores, pero no asignó suficiente espacio para todos ellos. Entonces, solo aquellos para los que asignó espacio se escriben correctamente, y el resto escrito fuera de los límites de la matriz puede contener cualquier valor. ¿Cómo sabe exactamente el sistema operativo qué tan críticos son esos valores fuera de los límites para que su aplicación funcione? No sabe nada de su propósito.
Además, escribir fuera de la memoria permitida a menudo puede corromper otras variables no relacionadas, lo que obviamente es peligroso y puede causar cualquier comportamiento aleatorio. Dichos errores a menudo son difíciles de rastrear. Los desbordamientos de pila, por ejemplo, son fallas de segmentación propensas a sobrescribir variables adyacentes, a menos que los mecanismos de protección detecten el error.
Si observamos el comportamiento de los sistemas de microcontroladores "bare metal" sin ningún sistema operativo y sin características de memoria virtual, solo memoria física sin procesar, simplemente harán exactamente lo que se les dice, por ejemplo, sobrescribir variables no relacionadas y continuar. Lo que a su vez podría causar un comportamiento desastroso en caso de que la aplicación sea de misión crítica.
¿Por qué no es recuperable?
Porque el sistema operativo no sabe lo que se supone que debe hacer su programa.
Aunque en el escenario "bare metal" anterior, el sistema podría ser lo suficientemente inteligente como para colocarse en un modo seguro y continuar. Las aplicaciones críticas como la automoción y la tecnología médica no pueden simplemente detenerse o reiniciarse, ya que eso en sí mismo podría ser peligroso. Más bien intentarán "cojear a casa" con una funcionalidad limitada.
¿Por qué esta solución evita ese estado irrecuperable? ¿Incluso?
Esa solución es simplemente ignorar el error y continúa. No soluciona el problema que lo causó. Es un parche muy sucio y setjmp/longjmp en general son funciones muy peligrosas que deben evitarse por cualquier motivo.
Tenemos que darnos cuenta de que una falla de segmentación es un síntoma de un error, no la causa .
Es posible que esta no sea una respuesta completa, y de ninguna manera es completa o precisa, pero no cabe en un comentario.
Entonces, un SIGSEGV puede ocurrir cuando intenta acceder a la memoria de una manera que no debería (como escribir en ella cuando es de solo lectura o leer desde un rango de direcciones que no está asignado). Tal error solo podría ser recuperable si sabe lo suficiente sobre el medio ambiente.
Pero, ¿cómo quiere determinar por qué ocurrió ese acceso no válido en primer lugar?
En un comentario a otra respuesta dices:
práctica a corto plazo, creo que mis errores son solo acceso a nulo y nada más.
Ninguna aplicación está libre de errores, entonces, ¿por qué supone que si puede ocurrir un acceso de puntero nulo que su aplicación no tiene, por ejemplo, también tiene una situación en la que ocurre un uso después de libre o un acceso fuera de los límites a ubicaciones de memoria "válidas"? inmediatamente dará como resultado un error o un SIGSEGV .
Un acceso use-after-free o fuera de los límites también podría modificar un puntero para que apunte a una ubicación no válida o para que sea un nullptr, pero también podría haber cambiado otras ubicaciones en la memoria al mismo tiempo. Si ahora solo asume que el puntero simplemente no se inicializó y su manejo de errores solo considera esto, continúa con una aplicación que se encuentra en un estado que no coincide con sus expectativas o con uno de los compiladores al generar el código.
En ese caso, la aplicación, en el mejor de los casos, se bloqueará poco después de la "recuperación". En el peor de los casos, algunas variables tienen valores defectuosos, pero continuará ejecutándose con ellos. Este descuido podría ser más dañino para una aplicación crítica que reiniciarla.
Sin embargo, si sabe que una determinada acción puede dar lugar, en determinadas circunstancias, a un SIGSEGV , puede manejar ese error, por ejemplo, que sabe que la dirección de la memoria es válida, pero que el dispositivo al que está asignada la memoria podría no ser totalmente fiable y podría causar un SIGSEGV debido a que recuperarse de un SIGSEGV podría ser un enfoque válido.
Hasta ahora, las respuestas y los comentarios han respondido a través de la lente de un modelo de programación de alto nivel, que limita fundamentalmente la creatividad y el potencial del programador para su conveniencia. Dichos modelos definen su propia semántica y no manejan fallas de segmentación por razones propias, ya sean de simplicidad, eficiencia o cualquier otra. Desde esa perspectiva, una falla de segmento es un caso inusual que indica un error del programador, ya sea el programador del espacio de usuario o el programador de la implementación del lenguaje. La pregunta, sin embargo, no se trata de si es una buena idea o no, ni tampoco de sus opiniones al respecto.
En realidad, lo que dices es correcto: las fallas de segmentación son recuperables. Puede, como cualquier señal normal, adjuntarle un controlador con sigaction . Y, sí, su programa sin duda se puede hacer de tal manera que el manejo de fallas de segmentación sea una característica normal.
Un obstáculo es que una falla de segmentación es una falla , no una excepción, que es diferente con respecto a dónde regresa el flujo de control después de que se ha manejado la falla. Específicamente, un controlador de fallas regresa a la misma instrucción de falla, que continuará fallando indefinidamente. Sin embargo, esto no es un problema real, ya que puede omitirse manualmente, puede regresar a una ubicación específica, puede intentar parchear la instrucción de falla para que se vuelva correcta o puede asignar dicha memoria si confía en el código de falla . Con el conocimiento adecuado de la máquina, nada te detendrá, ni siquiera esos caballeros que manejan especificaciones.
A nivel de código de máquina, muchas plataformas permitirían que los programas que "esperan" fallas de segmentación en ciertas circunstancias ajusten la configuración de la memoria y reanuden la ejecución. Esto puede ser útil para implementar cosas como el monitoreo de pilas. Si se necesita determinar la cantidad máxima de pila que alguna vez usó una aplicación, se podría configurar el segmento de pila para permitir el acceso solo a una pequeña cantidad de pila y luego responder a las fallas de segmentación ajustando los límites del segmento de pila y reanudar la ejecución del código.
Sin embargo, en el nivel del lenguaje C, admitir dicha semántica impediría en gran medida la optimización. Si uno tuviera que escribir algo como:
void test(float *p, int *q) { float temp = *p; if (*q += 1) function2(temp); } un compilador podría considerar la lectura de *p y la secuencia de lectura-modificación-escritura en *q como no secuenciadas entre sí, y generar código que solo lea *p en los casos en que el valor inicial de *q no fuera -1 . Esto no afectaría en nada el comportamiento del programa si p fuera válido, pero si p no fuera válido, este cambio podría resultar en la falla del segmento debido al acceso a *p que ocurre después de que se incrementó *q aunque el acceso que desencadenó la falla se realizó antes del incremento.
Para que un lenguaje admita fallas de segmento recuperables de manera eficiente y significativa, tendría que documentar el rango de optimizaciones permitidas y no permitidas con mucho más detalle que el estándar C, y no veo ninguna razón para esperar futuras versiones de C. Estándar para incluir tal detalle.
Es recuperable, pero suele ser una mala idea. Por ejemplo, el compilador de Microsoft C++ tiene la opción de convertir las fallas de segmento en excepciones.
Puede ver la documentación de Microsoft SEH , pero incluso ellos no sugieren usarlo.
Explique por qué después de una falla de segmentación el programa está en un estado indeterminado
Creo que este es su malentendido fundamental: el SEGV no causa el estado indeterminado, es un síntoma de él. Entonces, el problema es (generalmente) que el programa está en un estado ilegal e irrecuperable MUCHO ANTES de que ocurra el SIGSEGV, y la recuperación del SIGSEGV no cambiará eso.
- ¿Cuándo ocurre exactamente la falla de segmentación (= cuándo se envía SIGSEGV)?
La única forma estándar en la que ocurre un SIGSEGV es con el raise(SIGSEGV); . Si esta es la fuente de un SIGSEGV, entonces obviamente es recuperable usando un salto largo. Pero este es un caso trivial que nunca sucede en la realidad. Hay formas específicas de la plataforma de hacer las cosas que pueden dar como resultado SEGV bien definidos (p. ej., usar mprotect en un sistema POSIX), y estos SEGV pueden recuperarse (pero probablemente requerirán una recuperación específica de la plataforma). Sin embargo, el peligro de SEGV relacionado con el comportamiento indefinido generalmente significa que el controlador de la señal verificará con mucho cuidado la información (dependiente de la plataforma) que viene junto con la señal para asegurarse de que sea algo que se espera.
- ¿Por qué el proceso está en un estado de comportamiento indefinido después de ese punto?
Estaba (generalmente) en un estado de comportamiento indefinido antes de ese punto; simplemente no se notó. Ese es el gran problema con el Comportamiento indefinido tanto en C como en C++: no hay un comportamiento específico asociado con él, por lo que es posible que no se note de inmediato.
- ¿Por qué esta solución evita ese estado irrecuperable? ¿Incluso?
No lo hace, simplemente vuelve a un punto anterior, pero no hace nada para deshacer o incluso identificar el comportamiento indefinido que causa el problema.
Lo que debe comprender acerca de las fallas de segmentación es que no son un problema. Son un ejemplo de la misericordia casi infinita del Señor (según un viejo profesor que tuve en la universidad). Una falla de segmentación es una señal de que algo anda muy mal, y su programa pensó que era una buena idea acceder a la memoria donde no había memoria disponible. Ese acceso no es en sí mismo el problema; el problema surgió en algún momento indeterminado antes, cuando algo salió mal, lo que eventualmente hizo que su programa pensara que este acceso era una buena idea. Acceder a la memoria inexistente es solo un síntoma en este punto, pero (y aquí es donde entra la misericordia del Señor) es un síntoma fácil de detectar . Podría ser mucho peor; podría estar accediendo a la memoria donde hay memoria para tener, simplemente, la memoria equivocada. El sistema operativo no puede salvarte de eso.
El sistema operativo no tiene forma de averiguar qué hizo que su programa creyera algo tan absurdo, y lo único que puede hacer es cerrar todo, antes de que haga otra locura de una manera que el sistema operativo no puede detectar tan fácilmente. Por lo general, la mayoría de los sistemas operativos también proporcionan un volcado del núcleo (una copia guardada de la memoria del programa), que en teoría podría usarse para averiguar qué pensaba que estaba haciendo el programa. Esto no es realmente sencillo para ningún programa no trivial, pero es por eso que el sistema operativo lo hace, por si acaso.
Su programa es un estado subdeterminado porque C no puede definir el estado. Los errores que causan estos errores son un comportamiento indefinido. Esta es la clase más desagradable de malos comportamientos.
El problema clave con la recuperación de estas cosas es que, al ser un comportamiento indefinido, el cumplidor no está obligado a respaldarlas de ninguna manera. En particular, puede haber realizado optimizaciones que, si solo ocurren comportamientos definidos, probablemente tengan el mismo efecto. El compilador tiene todo el derecho de reordenar líneas, omitir líneas y hacer todo tipo de trucos sofisticados para que su código se ejecute más rápido. Todo lo que tiene que hacer es demostrar que el efecto es el mismo según el modelo de máquina virtual de C++.
Cuando ocurre un comportamiento indefinido, todo eso se va por la ventana. Puede encontrarse en situaciones difíciles en las que el compilador ha reordenado las operaciones y ahora no puede llevarlo a un estado al que podría llegar ejecutando su programa durante un período de tiempo. Recuerde que las asignaciones borran el valor antiguo. Si una asignación se movió antes de la línea que falló, no puede recuperar el valor anterior para "retirar" la optimización.
De hecho, el comportamiento de este código reordenado era idéntico al original, siempre que no se produjera un comportamiento indefinido . Una vez que ocurrió el comportamiento indefinido, expone el hecho de que ocurrió el reordenamiento y podría cambiar los resultados.
La compensación aquí es la velocidad. Debido a que el compilador no camina sobre cáscaras de huevo, aterrorizado por algún comportamiento del sistema operativo no especificado, puede hacer un mejor trabajo al optimizar su código.
Ahora, debido a que el comportamiento indefinido siempre es un comportamiento indefinido, no importa cuánto desee que no lo sea, no puede haber una forma específica de C++ para manejar este caso. El lenguaje C ++ nunca puede introducir una forma de resolver esto, al menos sin hacer que tenga un comportamiento definido y pagando los costos por eso. En una plataforma y compilador dados, es posible que pueda identificar que este comportamiento indefinido en realidad está definido por su compilador, generalmente en forma de extensiones. De hecho, la respuesta que vinculé anteriormente muestra una forma de convertir una señal en una excepción, que de hecho funciona en al menos un par de plataforma/compilador.
Pero siempre tiene que estar al margen así. Los desarrolladores de C++ valoran la velocidad del código optimizado sobre la definición de este comportamiento indefinido.
Honestamente, si pudiera decirle a la computadora que ignore una falla de segmentación. Yo no tomaría esta opción.
Por lo general, el error de segmentación se produce porque está desreferenciando un puntero nulo o un puntero desasignado. Al desreferenciar nulo, el comportamiento es completamente indefinido. Al hacer referencia a un puntero desasignado, los datos que está extrayendo pueden ser el valor anterior, basura aleatoria o, en el peor de los casos, valores de otro programa. En cualquier caso, quiero que el programa tenga una falla de segmento y no continúe y reporte cálculos no deseados.
Es absolutamente posible, pero esto duplicaría la funcionalidad existente de una manera menos estable.
El kernel ya recibirá una excepción de falla de página cuando un programa acceda a una dirección que aún no está respaldada por la memoria física, y luego asignará y potencialmente inicializará una página de acuerdo con las asignaciones existentes, y luego volverá a intentar la instrucción infractora.
Un controlador de SEGV hipotético haría exactamente lo mismo: decidir qué se debe asignar en esta dirección, crear el mapeo y volver a intentar la instrucción, pero con la diferencia de que si el controlador incurriera en otro SEGV, podríamos entrar en un bucle sin fin aquí. , y la detección sería difícil ya que esa decisión necesitaría analizar el código, por lo que estaríamos creando un problema de detención aquí.
El kernel ya asigna páginas de memoria de forma perezosa, permite mapear el contenido de los archivos y admite mapeos compartidos con semántica de copia en escritura, por lo que no hay mucho que ganar con este mecanismo.
Si bien su pregunta se refiere específicamente a las fallas de segmentación, la verdadera pregunta es:
Si se ordena a un componente de software o hardware que haga algo absurdo o incluso imposible, ¿qué debe hacer? ¿No hacer nada en absoluto? Adivina lo que realmente hay que hacer y hacer eso? ¿O usar algún mecanismo (como "lanzar una excepción") para detener el cálculo de nivel superior que emitió el comando sin sentido?
El gran peso de la experiencia acumulada por muchos ingenieros, durante muchos años, está de acuerdo en que la mejor respuesta es detener el cálculo general y producir información de diagnóstico que pueda ayudar a alguien a descubrir qué es lo que está mal .
Además del acceso ilegal a la memoria protegida o inexistente, otros ejemplos de "comandos sin sentido" incluyen decirle a una CPU que divida un número entero por cero o que ejecute bytes basura que no se decodifican en ninguna instrucción válida. Si se utiliza un lenguaje de programación con verificación de tipos en tiempo de ejecución, tratar de invocar cualquier operación que no esté definida para los tipos de datos involucrados es otro ejemplo.
Pero, ¿por qué es mejor obligar a un programa que intenta dividir por cero a fallar? Nadie quiere que sus programas se bloqueen. ¿No podríamos definir la división por cero para que sea igual a algún número, como cero o 73? ¿Y no podríamos crear CPU que pasaran por alto las instrucciones no válidas sin fallar? Tal vez nuestras CPU también podrían devolver algún valor especial, como -1, para cualquier lectura de una dirección de memoria protegida o no asignada. Y simplemente podrían ignorar las escrituras en direcciones protegidas. ¡No más fallas de segmento! ¡Vaya!
Ciertamente, todas esas cosas se podrían hacer, pero en realidad no se ganaría nada. Este es el punto: si bien nadie quiere que sus programas se bloqueen, el hecho de que no se bloquee no significa éxito. Las personas escriben y ejecutan programas de computadora para hacer algo, no solo para "no colapsar". Si un programa tiene suficientes errores para leer o escribir direcciones de memoria aleatorias o intentar dividir por cero, las posibilidades de que haga lo que realmente desea son muy bajas, incluso si se le permite continuar ejecutándose. Por otro lado, si el programa no se detiene cuando intenta cosas locas, puede terminar haciendo algo que no desea, como corromper o destruir sus datos.
Históricamente, algunos lenguajes de programación han sido diseñados para siempre "simplemente hacer algo" en respuesta a comandos sin sentido, en lugar de generar un error fatal. Esto se hizo en un intento equivocado de ser más amigable con los programadores novatos, pero siempre terminó mal. Lo mismo sería válido para su sugerencia de que los sistemas operativos nunca deben bloquear programas debido a fallas de segmento.
Como usa el término SIGSEGV, creo que está usando un sistema con un sistema operativo y que el problema ocurre en su aplicación de tierra de usuario.
Cuando la aplicación obtiene el SIGSEGV, es un síntoma de que algo salió mal antes del acceso a la memoria. A veces se puede señalar exactamente dónde salieron mal las cosas, generalmente no. Entonces algo salió mal, y un tiempo después este mal fue la causa de un SIGSEGV. Si el error ocurriera "en el sistema operativo", mi reacción sería apagar el sistema. Con excepciones muy específicas: cuando el sistema operativo tiene una función específica para verificar si hay una tarjeta de memoria o una tarjeta IO instalada (o tal vez eliminada).
En el terreno de los usuarios, probablemente dividiría mi aplicación en varios procesos. Uno o más procesos harían el trabajo real. Otro proceso monitorearía los procesos de trabajo y podría descubrir cuándo falla uno de ellos. El proceso de supervisión podría descubrir un SIGSEGV en un proceso de trabajo, lo que podría reiniciar el proceso de trabajo o realizar una conmutación por error o lo que se considere apropiado en el caso específico. Esto no recuperaría el acceso real a la memoria, pero podría recuperar la función de la aplicación.
Puede consultar la filosofía de Erlang de "fallar temprano" y la biblioteca OTP para obtener más inspiración sobre esta forma de hacer las cosas. Sin embargo, no maneja SIGSEGV, sino varios otros tipos de problemas.
Depende de lo que entiendas por recuperación. La única recuperación sensata en caso de que el sistema operativo le envíe la señal SEGV es limpiar su programa y hacer girar otro desde el principio, con suerte no caer en el mismo escollo.
No tienes forma de saber cuánto se corrompió tu memoria antes de que el sistema operativo pusiera fin al caos. Lo más probable es que si intenta continuar desde la siguiente instrucción o algún punto de recuperación arbitrario, su programa se comportará mal aún más.
Lo que parece que muchas de las respuestas votadas a favor están olvidando es que hay aplicaciones en las que pueden ocurrir fallas de segmento en producción sin un error de programación. Y donde se espera alta disponibilidad, décadas de vida útil y cero mantenimiento. En esos entornos, lo que normalmente se hace es que el programa se reinicia si falla por algún motivo, incluido el error de segmento. Además, se utiliza una función de vigilancia para garantizar que el programa no se quede atascado en un bucle infinito no planificado.
Piense en todos los dispositivos integrados en los que confía que no tienen botón de reinicio. Se basan en hardware imperfecto, porque ningún hardware es perfecto. El software tiene que lidiar con las imperfecciones del hardware. En otras palabras, el software debe ser robusto contra el mal comportamiento del hardware.
Embebido no es la única área donde esto es crucial. Piense en la cantidad de servidores que manejan solo StackOverflow. La posibilidad de que la radiación ionizante provoque un solo evento alterado es pequeña si observa cualquier operación a nivel del suelo, pero esta probabilidad deja de ser trivial si observa una gran cantidad de computadoras que funcionan las 24 horas del día, los 7 días de la semana. La memoria ECC ayuda contra esto, pero no todo se puede proteger.
Su programa no puede recuperarse de una falla de segmentación porque no tiene idea de en qué estado se encuentra algo .
Considere esta analogía.
Tiene una bonita casa en Maine con un hermoso jardín delantero y un sendero de peldaños que lo atraviesa. Por alguna razón, ha elegido conectar cada piedra con la siguiente con una cinta (es decir, las ha convertido en una lista de enlaces individuales).
Una mañana, saliendo de la casa, pisas la primera piedra, luego sigues la cinta hasta la segunda, luego de nuevo hasta la tercera pero, cuando pisas la cuarta piedra, de repente te encuentras en Albuquerque.
Ahora cuéntanos, ¿cómo te recuperas de eso ?
Su programa tiene el mismo dilema.
Algo salió espectacularmente mal, pero su programa no tiene idea de qué fue, qué lo causó o cómo hacer algo útil al respecto.
Por lo tanto: se estrella y se quema.