Articulo de referencia

error de segmentación

En informática , un fallo de segmentación (a menudo abreviado como segfault ) o violación de acceso es una condición de fallo generada por el hardware con protección de memoria ...

En informática , un fallo de segmentación (a menudo abreviado como segfault ) o violación de acceso es una condición de fallo generada por el hardware con protección de memoria , que notifica a un sistema operativo (SO) que el software ha intentado acceder a un área restringida de la memoria (una violación de acceso a la memoria). En los ordenadores x86 estándar , esta es una forma de fallo de protección general . El núcleo del sistema operativo , en respuesta, normalmente 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 manejador de señales personalizado, lo que les permite recuperarse por sí mismos, [ 1 ] pero de lo contrario se utiliza el manejador de señales predeterminado del SO, lo que generalmente provoca la terminación anormal del proceso (un bloqueo del programa ) y, a veces, un volcado de memoria .

Los fallos de segmentación son un tipo común de error en programas escritos en lenguajes que admiten punteros que pueden ser nulos , o que pueden tener un valor arbitrario, o que admiten matrices , y en los que se realizan pocas o ninguna comprobación de memoria. Surgen principalmente debido a errores en el uso de punteros para el direccionamiento de memoria virtual . Otro tipo de error de acceso a memoria es el error de bus , que también tiene diversas causas, pero que hoy en día es mucho menos frecuente; estos se producen principalmente debido a un direccionamiento incorrecto de la memoria física o a un acceso a memoria desalineado: se trata de referencias de memoria que el hardware no puede direccionar, en lugar de referencias a las que un proceso no tiene permitido acceder.

Muchos lenguajes de programación cuentan con mecanismos diseñados para ayudar a los programadores a evitar fallos de segmentación en sus programas y mejorar la comprobación del hardware con protección de memoria. Por ejemplo, Rust emplea un modelo basado en la propiedad [ 2 ] para intentar garantizar la comprobación de 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 fallos de segmentación. [ 5 ]

Descripción general

Ejemplo de señal generada por el ser humano
Error de segmentación que afecta a Krita en el entorno de escritorio KDE.
Desreferenciación de puntero nulo en Windows 8

Se produce un fallo de segmentación cuando un programa intenta acceder a una ubicación de memoria a la que no tiene permiso para acceder, o intenta acceder a una ubicación de memoria de una manera que no está permitida (por ejemplo, intentar escribir en una ubicación de solo lectura o sobrescribir parte del sistema operativo ).

El término «segmentación» tiene diversos usos en informática; en el contexto de «fallo 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 parte de lectura/escritura del segmento de datos de un programa son modificables, mientras que los datos de solo lectura asignados en el segmento const y el segmento de código no son modificables. Por lo tanto, intentar leer fuera del espacio de direcciones del programa, o escribir en un segmento de solo lectura del espacio de direcciones, viola la segmentación del programa, de ahí su nombre.

En sistemas que utilizan segmentación de memoria por hardware para proporcionar memoria virtual , se produce un fallo de segmentación cuando el hardware detecta un intento de referenciar un segmento inexistente, o una ubicación fuera de los límites de un segmento, o una ubicación de una forma no permitida por los permisos otorgados para ese segmento. En sistemas que utilizan solo paginación , un fallo de página no válido generalmente conduce a un fallo de segmentación, y tanto los fallos de segmentación como los de página son 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álido, 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 ilegalmente la memoria.

A nivel de hardware, la unidad de gestión de memoria (MMU) genera inicialmente un fallo por acceso ilegal (si la memoria referenciada existe), como parte de su función de protección de memoria, o por un fallo de página no válida (si la memoria referenciada 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, este fallo se detecta y se pasa una señal al proceso infractor, activando el controlador del proceso para esa señal. Los distintos sistemas operativos tienen distintos nombres de señales para indicar que se ha producido un fallo de segmentación. En los sistemas operativos tipo Unix , se envía una señal llamada SIGSEGV(abreviatura de violación de segmentación ) al proceso infractor. En Microsoft Windows , el proceso infractor recibe una STATUS_ACCESS_VIOLATIONexcepción .

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: distintos hardware generan diferentes fallos para determinadas condiciones, y distintos sistemas operativos los convierten en diferentes señales que se transmiten a los procesos. La causa próxima es una violación de acceso a memoria, mientras que la causa subyacente suele ser algún tipo de error de software . Determinar la causa raíz ( depurar el error) puede ser sencillo en algunos casos, cuando el programa provoca sistemáticamente un fallo de segmentación (por ejemplo, al desreferenciar un puntero nulo ), mientras que en otros casos el error puede ser difícil de reproducir y depender de la asignación de memoria en cada ejecución (por ejemplo, al desreferenciar un puntero colgante ).

A continuación se presentan algunas causas típicas de un fallo de segmentación:

  • Intentando acceder a una dirección de memoria inexistente (fuera del espacio de direcciones del proceso).
  • Intentar acceder a memoria a la que el programa no tiene derechos (como estructuras del kernel en el contexto del proceso).
  • Intentar 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 dan lugar a un acceso a memoria no válido:

  • Desreferenciar un puntero nulo , que normalmente apunta a una dirección que no forma 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).
  • Desreferenciación o asignación a un puntero liberado ( puntero colgante , que apunta a memoria que ha sido liberada/desasignada/eliminada).
  • Un desbordamiento de búfer
  • Un desbordamiento de pila
  • Intentar ejecutar un programa que no compila correctamente. (Algunos compiladores generan un archivo ejecutable a pesar de la presencia de errores de compilación).

En el código C, los fallos de segmentación suelen producirse por errores en el uso de punteros, especialmente en la asignación dinámica de memoria . Desreferenciar un puntero nulo, lo que resulta en un comportamiento indefinido , generalmente provoca 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 comodín y los punteros colgantes apuntan a memoria que puede existir o no, y que puede ser legible o escribible o no, lo que puede provocar errores transitorios. Por ejemplo:

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

Desreferenciar cualquiera de estas variables podría provocar un fallo de segmentación: desreferenciar el puntero nulo generalmente provocará un fallo de segmentación, mientras que leer desde el puntero comodín puede dar como resultado datos aleatorios pero sin fallo de segmentación, y leer desde el puntero colgante puede dar como resultado datos válidos durante un tiempo y luego datos aleatorios a medida que se sobrescriben.

Manejo

La acción predeterminada ante un fallo de segmentación o un error de bus es la terminación anormal del proceso que lo provocó. Se puede generar un archivo de volcado de memoria para facilitar la depuración, y también se pueden realizar otras acciones dependientes de la plataforma. Por ejemplo, los sistemas Linux que utilizan el parche grsecurity pueden registrar señales SIGSEGV para detectar posibles intentos de intrusión mediante desbordamientos de búfer .

En algunos sistemas, como Linux y Windows, es posible que el propio programa gestione un fallo de segmentación. [ 7 ] Dependiendo de la arquitectura y el sistema operativo, el programa en ejecución no solo puede gestionar el evento, sino que también puede extraer información sobre su estado, como obtener un rastreo de pila , los valores de los registros del procesador , la línea del código fuente en la que se activó, la dirección de memoria a la que se accedió de forma inválida [ 8 ] y si la acción fue de lectura o escritura. [ 9 ]

Si bien un fallo de segmentación generalmente significa que el programa tiene un error que necesita ser corregido, también es posible provocar intencionalmente dicho fallo con fines de prueba, depuración y emulación de plataformas donde se requiere acceso directo a la memoria. En este último caso, el sistema debe permitir que el programa continúe ejecutándose incluso después de que ocurra el fallo. En tal caso, cuando el sistema lo permite, es posible gestionar el evento e incrementar el contador de programa del procesador para "saltar" la instrucción fallida y continuar la ejecución. [ 10 ]

Ejemplos

Error de segmentación en un teclado EMV.

Escritura en memoria de solo lectura

Escribir en 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 porción de solo lectura del segmento de datos , ya que el sistema operativo los carga en memoria de solo lectura.

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

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

Cuando se compila el programa que contiene este código, la cadena "hello world" se coloca en la sección rodata del archivo ejecutable del programa : la sección de solo lectura del segmento de datos . Al cargarse, el sistema operativo la coloca junto con otras cadenas y datos constantes en un segmento de memoria de solo lectura. Al ejecutarse, sse establece una variable, , 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 fallo de segmentación. Compilar dicho programa con un compilador que no comprueba la asignación de ubicaciones de solo lectura en tiempo de compilación y ejecutarlo en un sistema operativo tipo Unix produce el siguiente error en tiempo de ejecución :

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

Rastreo de pila del archivo de volcado de memoria 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 un array en lugar de un puntero a caracteres, 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*ese array), 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.

Desreferenciación de puntero nulo

En C y lenguajes similares, los punteros nulos se utilizan para indicar "ningún objeto" y como indicador de error. La desreferenciación de un puntero nulo (lectura o escritura a través de él) es un error de programación muy común. El estándar de C no establece que el puntero nulo sea lo mismo que el puntero a la dirección de memoria  0, aunque en la práctica podría ser así. La mayoría de los sistemas operativos asignan la dirección del puntero nulo de tal manera que acceder a él provoca un fallo de segmentación. Este comportamiento no está garantizado por el estándar de C. La desreferenciación de un puntero nulo es un comportamiento indefinido en C, y una implementación conforme puede asumir que cualquier puntero desreferenciado 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 un valor (escribir un valor en un destino inexistente) también suele provocar un fallo de segmentación:

int * ptr = NULL ; * ptr = 1 ;

El siguiente código incluye una desreferenciación de puntero nulo, pero al compilarse a menudo no dará como resultado un fallo de segmentación, ya que el valor no se utiliza y, por lo tanto, la desreferenciación a menudo se optimizará eliminando el código muerto :

int * ptr = NULL ; * ptr ;

Desbordamiento de búfer

El siguiente código accede a la matriz de caracteres smás allá de su límite superior. Dependiendo del compilador y del 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 main ( void ) { return main (); }

Este código provoca un desbordamiento de la pila , lo que resulta en un fallo de segmentación. [ 14 ] La recursión infinita no necesariamente produce un desbordamiento de pila, dependiendo del lenguaje, las optimizaciones realizadas por el compilador y la estructura exacta del código. En este caso, el comportamiento del código inalcanzable (la instrucción return) no está definido, por lo que el compilador puede eliminarlo y utilizar una optimización de llamada recursiva que podría resultar en la ausencia de uso de la pila. Otras optimizaciones podrían incluir la conversión de la recursión en iteración, lo que, dada la estructura de la función de ejemplo, resultaría en que el programa se ejecutara indefinidamente, aunque probablemente sin desbordar la 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" . Communications of the 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 embebidos . Vol. 38. ACM. págs. 69–80 . doi : 10.1145/780732.780743 . ISBN   1581136471. S2CID 1459540 . Consultado el 22-09-2018 . 
  6. "Depuración de fallos de segmentación y problemas de punteros - Cprogramming.com" . www.cprogramming.com . Consultado el 3 de febrero de 2021 .
  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 manejador SIGSEGV/SIGABRT que imprime el rastreo de pila de depuración" . GitHub . Consultado el 23 de agosto de 2020 .
  9. "¿Cómo identificar operaciones de lectura o escritura de fallo de página al usar el manejador sigaction en SIGSEGV? (LINUX)" . Consultado el 23 de agosto de 2020 .
  10. "LINUX – ESCRIBIR 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 un fallo de segmentación y un desbordamiento de pila?" . Stack Overflow . Consultado el 11/11/2023 .
  • Proceso: límite de enfoque y fallo de segmentación
  • Preguntas frecuentes: Respuestas aportadas por los usuarios sobre la definición de un fallo de segmentación.
  • Explicación de un "puntero nulo"
  • Respuesta a: ¿NULL siempre será 0, pero el puntero nulo no?
  • Especificaciones básicas de The Open Group, edición 6 signal.h