Falla de segmentación

Fallo de la computadora causado por acceso a memoria restringida

En informática , un fallo de segmentación (a menudo abreviado como segfault ) o violación de acceso es un fallo , o condición de fallo, provocado por el hardware con protección de memoria , que notifica a un sistema operativo (OS) que el software ha intentado acceder a un área restringida de la memoria (una violación de acceso a memoria). En las computadoras x86 estándar , esta es una forma de fallo de protección general . El núcleo del sistema operativo , en respuesta, generalmente realizará alguna acción correctiva, generalmente pasando el fallo al proceso infractor enviándole una señal . En algunos casos, los procesos pueden instalar un controlador de señal personalizado, lo que les permite recuperarse por sí solos, [1] pero, de lo contrario, se utiliza el controlador de señal predeterminado del SO, lo que generalmente causa la terminación anormal del proceso (un bloqueo del programa ) y, a veces, un volcado de memoria .

Los fallos de segmentación son una clase común de error en programas escritos en lenguajes como C que proporcionan acceso a memoria de bajo nivel y pocas o ninguna comprobación de seguridad. Surgen principalmente debido a errores en el uso de punteros para el direccionamiento de memoria virtual , en particular accesos ilegales. Otro tipo de error de acceso a memoria es un error de bus , que también tiene varias causas, pero que hoy en día es mucho más raro; estos ocurren principalmente debido a un direccionamiento incorrecto de la memoria física o debido a un acceso a memoria desalineado; se trata de referencias de memoria que el hardware no puede abordar, en lugar de referencias que un proceso no puede abordar.

Muchos lenguajes de programación tienen mecanismos diseñados para evitar fallas de segmentación y mejorar la seguridad de la memoria. Por ejemplo, Rust emplea un modelo basado en la propiedad [2] para garantizar la seguridad de la memoria. [3] Otros lenguajes, como Lisp y Java , emplean la recolección de basura [4] , que evita ciertas clases de errores de memoria que podrían provocar fallas de segmentación. [5]

Descripción general

Ejemplo de señal generada por humanos
Fallo de segmentación que afecta a Krita en el entorno de escritorio KDE
Una desreferencia de puntero nulo en Windows 8

Una falla de segmentación ocurre cuando un programa intenta acceder a una ubicación de memoria a la que no tiene permitido acceder, o intenta acceder a una ubicación de memoria de una manera que no está permitida (por ejemplo, intentando escribir en una ubicación de solo lectura o sobrescribir parte del sistema operativo ).

El término "segmentación" tiene varios usos en informática; en el contexto de "falla de segmentación", se refiere al espacio de direcciones de un programa. [6] Con la protección de memoria, solo el espacio de direcciones del propio programa es legible, y de este, solo la pila y la porción de lectura/escritura del segmento de datos de un programa son escribibles, mientras que los datos de solo lectura asignados en el segmento constante y el segmento de código no son escribibles. Por lo tanto, intentar leer fuera del espacio de direcciones del programa, o escribir en un segmento de solo lectura del espacio de direcciones, da como resultado una falla de segmentación, de ahí el nombre.

En sistemas que utilizan segmentación de memoria de hardware para proporcionar memoria virtual , se produce un fallo de segmentación cuando el hardware detecta un intento de hacer referencia a un segmento inexistente, o de hacer referencia a una ubicación fuera de los límites de un segmento, o de hacer referencia a una ubicación de una manera no permitida por los permisos otorgados para ese segmento. En sistemas que utilizan solo paginación , un fallo de página no válida generalmente conduce a un fallo de segmentación, y los fallos de segmentación y los fallos de página son ambos fallos generados por el sistema de gestión de memoria virtual . Los fallos de segmentación también pueden ocurrir independientemente de los fallos de página: el acceso ilegal a una página válida es un fallo de segmentación, pero no un fallo de página no válida, y los fallos de segmentación pueden ocurrir en medio de una página (por lo tanto, no hay fallo de página), por ejemplo, en un desbordamiento de búfer que permanece dentro de una página pero sobrescribe la memoria ilegalmente.

A nivel de hardware, el error lo genera inicialmente la unidad de gestión de memoria (MMU) en caso de acceso ilegal (si la memoria a la que se hace referencia existe), como parte de su función de protección de memoria, o un error de página no válida (si la memoria a la que se hace referencia no existe). Si el problema no es una dirección lógica no válida sino una dirección física no válida, se genera un error de bus , aunque no siempre se distinguen.

A nivel del sistema operativo, se detecta este error y se envía una señal al proceso infractor, lo que activa el controlador del proceso para esa señal. Los distintos sistemas operativos tienen distintos nombres de señal para indicar que se ha producido un error de segmentación. En los sistemas operativos tipo Unix , se envía una señal denominada SIGSEGV (abreviatura de violación de segmentación ) al proceso infractor. En Microsoft Windows , el proceso infractor recibe una excepción STATUS_ACCESS_VIOLATION .

Causas

Las condiciones en las que se producen las violaciones de segmentación y cómo se manifiestan son específicas del hardware y del sistema operativo: diferentes hardware generan diferentes fallas para determinadas condiciones, y diferentes sistemas operativos las convierten en diferentes señales que se transmiten a los procesos. La causa inmediata es una violación de acceso a la memoria, mientras que la causa subyacente es generalmente un error de software de algún tipo. Determinar la causa raíz ( depurar el error) puede ser simple en algunos casos, donde el programa causará constantemente una falla de segmentación (por ejemplo, desreferenciar un puntero nulo ), mientras que en otros casos el error puede ser difícil de reproducir y depende de la asignación de memoria en cada ejecución (por ejemplo, desreferenciar un puntero colgante ).

Las siguientes son algunas causas típicas de una falla de segmentación:

  • Intentando acceder a una dirección de memoria inexistente (fuera del espacio de direcciones del proceso)
  • Intentar acceder a una memoria a la que el programa no tiene derechos (como las estructuras del núcleo en el contexto del proceso)
  • Intentando escribir en memoria de solo lectura (como un segmento de código)

Estos, a su vez, suelen ser causados ​​por errores de programación que resultan en un acceso no válido a la memoria:

  • Desreferenciar un puntero nulo , que generalmente apunta a una dirección que no es parte del espacio de direcciones del proceso
  • Desreferenciar o asignar a un puntero no inicializado ( puntero salvaje , que apunta a una dirección de memoria aleatoria)
  • Desreferenciar o asignar a un puntero liberado ( puntero colgante , que apunta a la memoria que ha sido liberada/desasignada/eliminada)
  • Un desbordamiento de búfer
  • Un desbordamiento de pila
  • Intentar ejecutar un programa que no se compila correctamente. (Algunos compiladores [ ¿cuáles? ] generarán un archivo ejecutable a pesar de la presencia de errores en tiempo de compilación).

En el código C, los fallos de segmentación se producen con mayor frecuencia debido a errores en el uso de punteros, en particular en la asignación dinámica de memoria en C. Desreferenciar un puntero nulo, que da como resultado un comportamiento indefinido , generalmente causará un fallo de segmentación. Esto se debe a que un puntero nulo no puede ser una dirección de memoria válida. Por otro lado, los punteros salvajes y los punteros colgantes apuntan a una memoria que puede existir o no, y puede ser legible o escribible o no, y por lo tanto pueden dar como resultado errores transitorios. Por ejemplo:

char * p1 = NULL ; // Puntero nulo char * p2 ; // Puntero comodín: no inicializado en absoluto. char * p3 = malloc ( 10 * sizeof ( char )); // Puntero inicializado a memoria asignada // (asumiendo que malloc no falló) free ( p3 ); // p3 ahora es un puntero colgante, ya que se liberó memoria              

Desreferenciar cualquiera de estas variables podría causar un error de segmentación: desreferenciar el puntero nulo generalmente causará un error de segmentación, mientras que leer desde el puntero salvaje puede resultar en datos aleatorios pero sin error de segmentación, y leer desde el puntero colgante puede resultar en datos válidos por un tiempo y luego en datos aleatorios a medida que se sobrescriben.

Manejo

La acción predeterminada para un error de segmentación o de bus es la terminación anormal del proceso que lo activó. Se puede generar un archivo de núcleo para ayudar con la depuración y también se pueden realizar otras acciones que dependen de la plataforma. Por ejemplo, los sistemas Linux que usan el parche grsecurity pueden registrar señales SIGSEGV para monitorear posibles intentos de intrusión mediante desbordamientos de búfer .

En algunos sistemas, como Linux y Windows, es posible que el programa mismo maneje una falla de segmentación. [7] Dependiendo de la arquitectura y el sistema operativo, el programa en ejecución no solo puede manejar el evento, sino que también puede extraer información sobre su estado, como obtener un seguimiento de la pila , valores de registro del procesador , la línea del código fuente cuando se activó, la dirección de memoria a la que se accedió de manera no válida [8] y si la acción fue de lectura o escritura. [9]

Aunque una falla de segmentación generalmente significa que el programa tiene un error que necesita ser corregido, también es posible causar intencionalmente dicha falla con el propósito de realizar pruebas, depurar y también emular plataformas donde se necesita acceso directo a la memoria. En este último caso, el sistema debe poder permitir que el programa se ejecute incluso después de que ocurra la falla. En este caso, cuando el sistema lo permite, es posible manejar el evento e incrementar el contador del programa del procesador para "saltar" sobre la instrucción que falla y continuar la ejecución. [10]

Ejemplos

Fallo de segmentación en un teclado EMV

Escritura en memoria de solo lectura

Escribir en la memoria de solo lectura genera un error de segmentación. A nivel de errores de código, esto ocurre cuando el programa escribe en parte de su propio segmento de código o en la parte de solo lectura del segmento de datos , ya que el sistema operativo los carga en la memoria de solo lectura.

A continuación, se muestra un ejemplo de código ANSI C que generalmente provocará un error de segmentación en plataformas con protección de memoria. Intenta modificar un literal de cadena , lo cual es un comportamiento indefinido según el estándar ANSI C. La mayoría de los compiladores no detectarán esto en el momento de la compilación y, en su lugar, compilarán esto en un código ejecutable que se bloqueará:

int main ( void ) { char * s = "hola mundo" ; * s = 'H' ; }        

Cuando se compila el programa que contiene este código, la cadena "hola mundo" se coloca en la sección rodata del archivo ejecutable del programa : la sección de solo lectura del segmento de datos . Cuando se carga, el sistema operativo la coloca con otras cadenas y datos constantes en un segmento de memoria de solo lectura. Cuando se ejecuta, se establece una variable, s , para que apunte a la ubicación de la cadena y se intenta escribir un carácter H a través de la variable en la memoria, lo que provoca un error de segmentación. La compilación de un programa de este tipo con un compilador que no comprueba la asignación de ubicaciones de solo lectura en el momento de la compilación y su ejecución en un sistema operativo tipo Unix produce el siguiente error de ejecución :

$ gcc  segfault.c  -g  -o  segfault $ ./segfault Error de segmentación

Seguimiento del archivo principal desde GDB :

El programa recibió la señal SIGSEGV , Fallo de segmentación . 0x1c0005c2 en main () en segfault . c : 6 6 * s = 'H' ;             

Este código se puede corregir utilizando una matriz en lugar de un puntero de carácter, ya que esto asigna memoria en la pila y la inicializa con el valor del literal de cadena:

char s [] = "hola mundo" ; s [ 0 ] = 'H' ; // equivalentemente, *s = 'H';      

Aunque los literales de cadena no deben modificarse (esto tiene un comportamiento indefinido en el estándar C), en C son de static char []tipo, [11] [12] [13] por lo que no hay una conversión implícita en el código original (que apunta a char *esa matriz), mientras que en C++ son de static const char []tipo y, por lo tanto, hay una conversión implícita, por lo que los compiladores generalmente detectarán este error en particular.

Desreferencia de puntero nulo

En C y lenguajes similares, los punteros nulos se utilizan para indicar "puntero a ningún objeto" y como indicador de error, y desreferenciar un puntero nulo (una lectura o escritura a través de un puntero nulo) es un error de programa muy común. El estándar C no dice que el puntero nulo sea lo mismo que el puntero a la dirección de memoria  0, aunque ese puede ser el caso en la práctica. La mayoría de los sistemas operativos asignan la dirección del puntero nulo de tal manera que acceder a él provoca un error de segmentación. Este comportamiento no está garantizado por el estándar C. Desreferenciar un puntero nulo es un comportamiento indefinido en C, y una implementación conforme puede asumir que cualquier puntero que se desreferencia no es nulo.

int * ptr = NULL ; printf ( "%d" , * ptr );    

Este código de ejemplo crea un puntero nulo y luego intenta acceder a su valor (leer el valor). Al hacerlo, se produce un error de segmentación en tiempo de ejecución en muchos sistemas operativos.

Desreferenciar un puntero nulo y luego asignarle (escribir un valor en un destino inexistente) también suele provocar una falla de segmentación:

int * ptr = NULL ; * ptr = 1 ;     

El siguiente código incluye una desreferencia de puntero nulo, pero cuando se compila a menudo no genera un error de segmentación, ya que el valor no se utiliza y, por lo tanto, la desreferencia a menudo se optimizará mediante la eliminación del código muerto :

int * ptr = NULL ; * ptr ;   

Desbordamiento de búfer

El código siguiente accede a la matriz de caracteres smás allá de su límite superior. Según el compilador y el procesador, esto puede provocar un error de segmentación.

char s [] = "hola mundo" ; char c = s [ 20 ];      

Desbordamiento de pila

Otro ejemplo es la recursión sin un caso base:

int principal ( void ) { devolver principal (); }   

que hace que la pila se desborde, lo que da como resultado un fallo de segmentación. [14] La recursión infinita puede no necesariamente resultar en un desbordamiento de pila dependiendo del lenguaje, las optimizaciones realizadas por el compilador y la estructura exacta de un código. En este caso, el comportamiento del código inalcanzable (la declaración de retorno) no está definido, por lo que el compilador puede eliminarlo y usar una optimización de llamada de cola que podría resultar en que no se use la pila. Otras optimizaciones podrían incluir la traducción de la recursión en iteración, lo que dada la estructura de la función de ejemplo daría como resultado que el programa se ejecute para siempre, mientras que probablemente no desborde su pila.

Véase también

Referencias

  1. ^ Programación experta en C: secretos profundos de C Por Peter Van der Linden, página 188
  2. ^ "El lenguaje de programación Rust - Propiedad".
  3. ^ "Concurrencia sin miedo con Rust - El blog del lenguaje de programación Rust".
  4. ^ McCarthy, John (abril de 1960). «Funciones recursivas de expresiones simbólicas y su cálculo por máquina, parte I». Comunicaciones de la ACM . 4 (3): 184–195. doi : 10.1145/367177.367199 . S2CID  : 1489409. Consultado el 22 de septiembre de 2018 .
  5. ^ Dhurjati, Dinakar; Kowshik, Sumant; Adve, Vikram; Lattner, Chris (1 de enero de 2003). "Seguridad de la memoria sin comprobaciones en tiempo de ejecución ni recolección de basura" (PDF) . Actas de la conferencia ACM SIGPLAN de 2003 sobre lenguaje, compilador y herramienta para sistemas integrados . Vol. 38. ACM. págs. 69–80. doi :10.1145/780732.780743. ISBN. 1581136471. S2CID  1459540 . Consultado el 22 de septiembre de 2018 .
  6. ^ "Depuración de errores de segmentación y problemas de puntero - Cprogramming.com". www.cprogramming.com . Consultado el 2021-02-03 .
  7. ^ "Recuperación limpia de errores de segmentación en Windows y Linux (32 bits, x86)" . Consultado el 23 de agosto de 2020 .
  8. ^ "Implementación del controlador SIGSEGV/SIGABRT que imprime el seguimiento de la pila de depuración". GitHub . Consultado el 23 de agosto de 2020 .
  9. ^ "¿Cómo identificar operaciones de lectura o escritura de errores de página al utilizar el controlador sigaction en SIGSEGV? (LINUX)" . Consultado el 23 de agosto de 2020 .
  10. ^ "LINUX – ESCRIBIENDO MANEJADORES DE FALLOS". 12 de noviembre de 2017. Consultado el 23 de agosto de 2020 .
  11. ^ "6.1.4 Literales de cadena". ISO/IEC 9899:1990 - Lenguajes de programación -- C .
  12. ^ "6.4.5 Literales de cadena". ISO/IEC 9899:1999 - Lenguajes de programación -- C .
  13. ^ "6.4.5 Literales de cadena". ISO/IEC 9899:2011 - Lenguajes de programación -- C.
  14. ^ "¿Cuál es la diferencia entre una falla de segmentación y un desbordamiento de pila?". Desbordamiento de pila . Consultado el 11 de noviembre de 2023 .
  • Proceso: falla de segmentación y de límite de enfoque [ enlace inactivo ]
  • Preguntas frecuentes: respuestas aportadas por los usuarios con respecto a la definición de una falla de segmentación
  • Explicación de un "puntero nulo"
  • Respuesta a: ¿Se garantiza que NULL es 0, pero el puntero nulo no?
  • Especificaciones básicas de Open Group, número 6 signal.h
Obtenido de "https://es.wikipedia.org/w/index.php?title=Falla_de_segmentación&oldid=1220172090"