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]
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 .
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:
Estos, a su vez, suelen ser causados por errores de programación que resultan en un acceso no válido a la memoria:
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.
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]
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.
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 ;
El código siguiente accede a la matriz de caracteres s
má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 ];
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.