¿Cómo funciona una falla de segmentación bajo el capó?


266

Parece que no puedo encontrar ninguna información sobre esto aparte de "la MMU de la CPU envía una señal" y "el núcleo lo dirige al programa infractor y lo termina".

Supuse que probablemente envía la señal al shell y el shell lo maneja al terminar el proceso ofensivo y la impresión "Segmentation fault". Así que probé esa suposición escribiendo un shell extremadamente mínimo que llamo crsh (crap shell). Este shell no hace nada excepto tomar la entrada del usuario y alimentarlo al system()método.

#include <stdio.h>
#include <stdlib.h>

int main(){
    char cmdbuf[1000];
    while (1){
        printf("Crap Shell> ");
        fgets(cmdbuf, 1000, stdin);
        system(cmdbuf);
    }
}

Así que ejecuté este shell en una terminal desnuda (sin bashejecutar debajo). Luego procedí a ejecutar un programa que produce un segfault. Si mis suposiciones fueran correctas, esto podría a) bloquearse crsh, cerrar el xterm, b) no imprimir "Segmentation fault", o c) ambos.

braden@system ~/code/crsh/ $ xterm -e ./crsh
Crap Shell> ./segfault
Segmentation fault
Crap Shell> [still running]

De vuelta al punto de partida, supongo. Acabo de demostrar que no es el shell lo que hace esto, sino el sistema subyacente. ¿Cómo se imprime "Fallo de segmentación"? "Quién" lo está haciendo? El grano? ¿Algo más? ¿Cómo se propaga la señal y todos sus efectos secundarios desde el hardware hasta la finalización del programa?


43
crshEs una gran idea para este tipo de experimentación. Gracias por informarnos a todos sobre la idea que hay detrás.
Bruce Ediger

30
Cuando lo vi por primera vez crsh, pensé que se pronunciaría "accidente". No estoy seguro de si ese es un nombre igualmente apropiado.
jpmc26

56
Este es un buen experimento ... pero debes saber qué system()hace debajo del capó. ¡Resulta que system()generará un proceso de shell! Entonces, su proceso de shell genera otro proceso de shell y ese proceso de shell (probablemente /bin/sho algo así) es el que ejecuta el programa. La forma /bin/sho el bashtrabajo es mediante el uso de fork()y exec()(u otra función en la execve()familia).
Dietrich Epp

44
@BradenBest: Exactamente. Lea la página del manual man 2 wait, incluirá las macros WIFSIGNALED()y WTERMSIG().
Dietrich Epp

44
@DietrichEpp ¡Tal como dijiste! Intenté agregar un cheque para (WIFSIGNALED(status) && WTERMSIG(status) == 11)que imprima algo tonto ( "YOU DUN GOOFED AND TRIGGERED A SEGFAULT"). Cuando ejecuté el segfaultprograma desde adentro crsh, imprimió exactamente eso. Mientras tanto, los comandos que salen normalmente no producen el mensaje de error.
Braden Best

Respuestas:


248

Todas las CPU modernas tienen la capacidad de interrumpir las instrucciones de la máquina que se está ejecutando actualmente. Guardan suficiente estado (generalmente, pero no siempre, en la pila) para que sea posible reanudar la ejecución más tarde, como si nada hubiera sucedido (la instrucción interrumpida se reiniciará desde cero, generalmente). Luego comienzan a ejecutar un controlador de interrupciones , que es solo más código de máquina, pero colocado en una ubicación especial para que la CPU sepa dónde está por adelantado. Los controladores de interrupción son siempre parte del núcleo del sistema operativo: el componente que se ejecuta con el mayor privilegio y es responsable de supervisar la ejecución de todos los demás componentes. 1,2

Las interrupciones pueden ser síncronas , lo que significa que son activadas por la CPU como respuesta directa a algo que hizo la instrucción que se está ejecutando actualmente, o asíncronas , lo que significa que suceden en un momento impredecible debido a un evento externo, como los datos que llegan a la red. Puerto. Algunas personas reservan el término "interrupción" para las interrupciones asíncronas, y llaman a las interrupciones sincrónicas "trampas", "fallas" o "excepciones", pero todas esas palabras tienen otros significados, así que me voy a quedar con "interrupción sincrónica".

Ahora, la mayoría de los sistemas operativos modernos tienen una noción de procesos . En su forma más básica, este es un mecanismo por el cual la computadora puede ejecutar más de un programa al mismo tiempo, pero también es un aspecto clave de cómo los sistemas operativos configuran la protección de memoria , que es una característica de la mayoría (pero, por desgracia, todavía no todas ) CPU modernas. Va junto con la memoria virtual, que es la capacidad de alterar la asignación entre direcciones de memoria y ubicaciones reales en RAM. La protección de memoria permite que el sistema operativo otorgue a cada proceso su propia porción de RAM privada, a la que solo él puede acceder. También permite que el sistema operativo (que actúa en nombre de algún proceso) designe regiones de RAM como de solo lectura, ejecutables, compartidas entre un grupo de procesos cooperantes, etc. También habrá una porción de memoria a la que solo puede acceder el núcleo. 3

Siempre que cada proceso acceda a la memoria solo de la forma en que la CPU está configurada para permitir, la protección de la memoria es invisible. Cuando un proceso rompe las reglas, la CPU generará una interrupción síncrona, pidiéndole al núcleo que resuelva las cosas. Regularmente sucede que el proceso realmente no rompió las reglas, solo el núcleo necesita hacer un trabajo antes de que el proceso pueda continuar. Por ejemplo, si una página de la memoria de un proceso necesita ser "expulsada" al archivo de intercambio para liberar espacio en la RAM para otra cosa, el núcleo marcará esa página como inaccesible. La próxima vez que el proceso intente usarlo, la CPU generará una interrupción de protección de memoria; el núcleo recuperará la página del intercambio, la volverá a colocar donde estaba, la marcará como accesible nuevamente y reanudará la ejecución.

Pero supongamos que el proceso realmente rompió las reglas. Trató de acceder a una página que nunca tuvo RAM asignada, o intentó ejecutar una página que está marcada como que no contiene código de máquina, o lo que sea. La familia de sistemas operativos generalmente conocida como "Unix" usa señales para hacer frente a esta situación. 4 Las señales son similares a las interrupciones, pero son generadas por el kernel y enviadas por procesos, en lugar de ser generadas por el hardware y enviadas por el kernel. Los procesos pueden definir manejadores de señalen su propio código, y decirle al kernel dónde están. Esos manejadores de señal se ejecutarán, interrumpiendo el flujo normal de control, cuando sea necesario. Todas las señales tienen un número y dos nombres, uno de los cuales es un acrónimo críptico y el otro una frase un poco menos críptica. La señal que se genera cuando un proceso rompe las reglas de protección de memoria es (por convención) el número 11, y sus nombres son SIGSEGV"Fallo de segmentación". 5,6

Una diferencia importante entre señales e interrupciones es que existe un comportamiento predeterminado para cada señal. Si el sistema operativo no puede definir controladores para todas las interrupciones, eso es un error en el sistema operativo, y toda la computadora se bloqueará cuando la CPU intente invocar un controlador perdido. Pero los procesos no tienen la obligación de definir manejadores de señal para todas las señales. Si el kernel genera una señal para un proceso, y esa señal se ha dejado en su comportamiento predeterminado, el kernel simplemente seguirá adelante y hará lo que sea el predeterminado y no molestará al proceso. Los comportamientos predeterminados de la mayoría de las señales son "no hacer nada" o "terminar este proceso y tal vez también producir un volcado de núcleo". SIGSEGVEs uno de los últimos.

Entonces, para recapitular, tenemos un proceso que rompió las reglas de protección de memoria. La CPU suspendió el proceso y generó una interrupción síncrona. El núcleo envió esa interrupción y generó una SIGSEGVseñal para el proceso. Supongamos que el proceso no configuró un controlador de señal SIGSEGV, por lo que el núcleo lleva a cabo el comportamiento predeterminado, que es finalizar el proceso. Esto tiene los mismos efectos que la _exitllamada al sistema: los archivos abiertos están cerrados, la memoria está desasignada, etc.

Hasta este momento, nada ha impreso ningún mensaje que un humano pueda ver, y el shell (o, más en general, el proceso padre del proceso que acaba de terminar) no ha estado involucrado en absoluto. SIGSEGVva al proceso que rompió las reglas, no a su padre. Sin embargo, el siguiente paso en la secuencia es notificar al proceso padre que su hijo ha finalizado. Esto puede suceder de varias maneras diferentes, de los cuales el más simple es cuando el padre ya está a la espera de esta notificación, usando una de las waitllamadas al sistema ( wait, waitpid, wait4, etc.). En ese caso, el kernel solo hará que la llamada del sistema regrese y proporcionará al proceso padre un número de código llamado estado de salida. 7 El estado de salida informa al padre por qué se terminó el proceso hijo; en este caso, aprenderá que el niño fue terminado debido al comportamiento predeterminado de una SIGSEGVseñal.

El proceso principal puede luego informar el evento a un humano imprimiendo un mensaje; Los programas de shell casi siempre hacen esto. Su crshno incluye código para hacer eso, pero sucede de todos modos, debido a que la rutina de biblioteca C systemse ejecuta una concha con todas las funciones, /bin/sh"bajo el capó". crshes el abuelo en este escenario; se envía la notificación del proceso principal /bin/sh, que imprime su mensaje habitual. Luego /bin/shse cierra, ya que no tiene nada más que hacer, y la implementación de la biblioteca C systemrecibe esa notificación de salida. Puede ver esa notificación de salida en su código inspeccionando el valor de retorno desystem; pero no le dirá que el proceso de nieto murió por una falla de seguridad, porque eso fue consumido por el proceso de shell intermedio.


Notas al pie

  1. Algunos sistemas operativos no implementan controladores de dispositivos como parte del núcleo; sin embargo, todos los manejadores de interrupciones aún tienen que ser parte del núcleo, y también el código que configura la protección de la memoria, porque el hardware no permite nada más que el núcleo para hacer estas cosas.

  2. Puede haber un programa llamado "hipervisor" o "administrador de máquina virtual" que sea aún más privilegiado que el kernel, pero a los fines de esta respuesta puede considerarse parte del hardware .

  3. El núcleo es un programa , pero es no un proceso; Es más como una biblioteca. Todos los procesos ejecutan partes del código del núcleo, de vez en cuando, además de su propio código. Puede haber una serie de "hilos de kernel" que solo ejecutan código de kernel, pero no nos conciernen aquí.

  4. El único sistema operativo con el que probablemente tenga que lidiar y que no pueda considerarse una implementación de Unix es, por supuesto, Windows. No utiliza señales en esta situación. (De hecho, no tiene señales; en Windows, la <signal.h>interfaz está completamente falsificada por la biblioteca C). En su lugar, utiliza algo llamado " manejo de excepciones estructuradas ".

  5. Algunas violaciones de protección de memoria generan SIGBUS("Error de bus") en lugar de SIGSEGV. La línea entre los dos está subespecificada y varía de un sistema a otro. Si ha escrito un programa que define un controlador para SIGSEGV, probablemente sea una buena idea definir el mismo controlador SIGBUS.

  6. "Falla de segmentación" era el nombre de la interrupción generada por violaciones de protección de memoria por una de las computadoras que ejecutaban el Unix original , probablemente el PDP-11 . " Segmentación " es un tipo de protección de memoria, pero hoy en día el término " falla de segmentación " se refiere genéricamente a cualquier tipo de violación de la protección de memoria.

  7. Todas las otras formas en que el proceso principal puede ser notificado de que un niño ha finalizado, terminan con el padre llamando waity recibiendo un estado de salida. Es solo que algo más sucede primero.


@zvol: anuncio 2) No creo que sea correcto decir que la CPU sabe algo sobre procesos. Debería decir que invoca un controlador de interrupciones, que transfiere el control.
user323094

99
@ user323094 Las CPU multinúcleo modernas realmente saben bastante sobre los procesos; suficiente para que, en esta situación, puedan suspender solo el hilo de ejecución que activó la falla de protección de memoria. Además, estaba tratando de no entrar en detalles de bajo nivel. Desde la perspectiva del programador de espacio de usuario, lo más importante que debe comprender sobre el paso 2 es que es el hardware el que detecta las violaciones de la protección de la memoria; menos así la división precisa del trabajo entre el hardware, el firmware y el sistema operativo cuando se trata de identificar el "proceso ofensivo".
zwol

Otra sutileza que podría confundir a un lector ingenuo es "El núcleo envía al proceso ofensivo una señal SIGSEGV". el cual utiliza la jerga habitual, pero en realidad significa que el kernel le dice a sí mismo para hacer frente a foo señal en la barra de proceso (es decir, el código de espacio de usuario no se involucra a menos que haya instalado un manejador de señales, una pregunta que se resuelve por el núcleo). A veces prefiero "elevar una señal SIGSEGV en el proceso" por esa razón.
dmckee

2
La diferencia significativa entre SIGBUS (error del bus) y SIGSEGV (falla de segmentación) es esta: SIGSEGV ocurre cuando la CPU sabe que no debe acceder a una dirección (y, por lo tanto, no realiza ninguna solicitud de bus de memoria externa). SIGBUS ocurre cuando la CPU solo se entera del problema de direccionamiento una vez que ha puesto su solicitud en su bus de direcciones externo. Por ejemplo, pedir una dirección física a la que no responde nada en el autobús, o pedir leer datos en un límite mal alineado (que requeriría dos solicitudes físicas para obtener en lugar de una)
Stuart Caie

2
@StuartCaie Estás describiendo el comportamiento de las interrupciones ; de hecho, muchas CPU hacen la distinción que delineas (aunque algunas no, y la línea entre las dos varía). La señales SIGSEGV y SIGBUS, sin embargo, son no asignan de forma fiable a esas dos condiciones a nivel de CPU. La única condición en la que POSIX requiere SIGBUS en lugar de SIGSEGV es cuando coloca mmapun archivo en una región de memoria que es más grande que el archivo y luego accede a "páginas enteras" más allá del final del archivo. (POSIX es bastante impreciso sobre cuándo suceden SIGSEGV / SIGBUS / SIGILL / etc.)
zwol

42

El shell realmente tiene algo que ver con ese mensaje, e crshindirectamente llama a un shell, lo que probablemente sea bash.

Escribí un pequeño programa en C que siempre seg falla:

#include <stdio.h>

int
main(int ac, char **av)
{
        int *i = NULL;

        *i = 12;

        return 0;
}

Cuando lo ejecuto desde mi shell predeterminado zsh, obtengo esto:

4 % ./segv
zsh: 13512 segmentation fault  ./segv

Cuando lo ejecuto bash, obtengo lo que notó en su pregunta:

bediger@flq123:csrc % ./segv
Segmentation fault

Iba a escribir un controlador de señal en mi código, luego me di cuenta de que la system()llamada a la biblioteca utilizada por crshexec es un shell, /bin/shsegún man 3 system. Es /bin/shcasi seguro imprimir "Falla de segmentación", ya que crshciertamente no lo es.

Si vuelve a escribir crshpara utilizar la execve()llamada del sistema para ejecutar el programa, no verá la cadena "Fallo de segmentación". Proviene del caparazón invocado por system().


55
Estaba discutiendo esto con Dietrich Epp. Me corté juntos una versión de CRSH que utiliza execvpe hice la prueba de nuevo para encontrar que mientras que la cáscara todavía no se bloquea (lo que significa SIGSEGV nunca se envía a la cáscara), esto no se imprime "Segmentation Fault". Nada está impreso en absoluto. Esto parece indicar que el shell detecta cuándo se matan sus procesos hijos y es responsable de imprimir "Fallo de segmentación" (o alguna variante del mismo).
Braden Best

2
@BradenBest: hice lo mismo, mi código es más descuidado que tu código. No recibí ningún mensaje, y mi caparazón aún más horrible no imprime nada. Utilicé waitpid()en cada fork / exec, y devuelve un valor diferente para los procesos que tienen una falla de segmentación, que los procesos que salen con el estado 0.
Bruce Ediger

21

Parece que no puedo encontrar ninguna información sobre esto aparte de "la MMU de la CPU envía una señal" y "el núcleo lo dirige al programa infractor y lo termina".

Este es un poco un resumen confuso. El mecanismo de señal de Unix es completamente diferente de los eventos específicos de la CPU que inician el proceso.

En general, cuando se accede a una dirección incorrecta (o se escribe en un área de solo lectura, intente ejecutar una sección no ejecutable, etc.), la CPU generará algún evento específico de la CPU (en arquitecturas tradicionales que no son VM) llamó una violación de segmentación, ya que cada "segmento" (tradicionalmente, el "texto" ejecutable de solo lectura, los "datos" de escritura y longitud variable, y la pila tradicionalmente en el extremo opuesto de la memoria) tenían un rango fijo de direcciones - en una arquitectura moderna, es más probable que sea un error de página [para memoria no asignada] o una infracción de acceso [para problemas de lectura, escritura y ejecución de permisos], y me centraré en esto para el resto de la respuesta).

Ahora, en este punto, el núcleo puede hacer varias cosas. Las fallas de página también se generan para la memoria que es válida pero no cargada (por ejemplo, intercambiada, o en un archivo mmapped, etc.), y en este caso el kernel asignará la memoria y luego reiniciará el programa del usuario desde la instrucción que causó el error. error. De lo contrario, envía una señal. Esto no "dirige [el evento original] exactamente al programa infractor", ya que el proceso para instalar un controlador de señal es diferente y en su mayoría independiente de la arquitectura, en comparación con si el programa simulara la instalación de un controlador de interrupción.

Si el programa de usuario tiene instalado un controlador de señal, esto significa crear un marco de pila y establecer la posición de ejecución del programa de usuario en el controlador de señal. Lo mismo se hace para todas las señales, pero en el caso de una violación de segmentación, las cosas generalmente se arreglan para que si el controlador de señal regresa, reinicie la instrucción que causó el error. El programa de usuario puede haber solucionado el error, por ejemplo, asignando memoria a la dirección infractora; depende de la arquitectura si esto es posible). El controlador de señal también puede saltar a una ubicación diferente en el programa (generalmente a través de longjmp o lanzando una excepción), para abortar cualquier operación que haya causado el mal acceso a la memoria.

Si el programa de usuario no tiene instalado un controlador de señal, simplemente se termina. En algunas arquitecturas, si se ignora la señal, puede reiniciar la instrucción una y otra vez, causando un bucle infinito.


+1, solo respuesta que agrega algo al aceptado. Buena descripción de la historia de "segmentación". Dato curioso: x86 en realidad todavía tiene límites de segmento en modo protegido de 32 bits (con o sin paginación (memoria virtual) habilitada), por lo que las instrucciones que la memoria de acceso puede generar #PF(fault-code)(error de página) o #GP(0)("Si la dirección efectiva de un operando de memoria está fuera del CS, DS, ES, FS o GS límite de segmento "). El modo de 64 bits elimina las comprobaciones de límite de segmento, ya que los sistemas operativos solo utilizaron la paginación y un modelo de memoria plana para el espacio del usuario.
Peter Cordes

En realidad, creo que la mayoría de los sistemas operativos en x86 usan paginación segmentada: un montón de grandes segmentos dentro de un espacio de direcciones plano y paginado. Así es como protege y mapea la memoria del núcleo en cada espacio de direcciones: los anillos (niveles de protección) están vinculados a segmentos, no a páginas
Lorenzo Dematté

Además, en NT (¡pero me encantaría saber si en la mayoría de los Unixes es lo mismo!) "Error de segmentación" podría ocurrir con bastante frecuencia: hay un segmento protegido de 64k al comienzo del espacio del usuario, por lo que desreferenciar un puntero NULL genera un (¿correcto?) falla de segmentación
Lorenzo Dematté

1
@ LorenzoDematté Sí, todos o casi todos los Unix modernos dejarán una porción de direcciones permanentemente sin asignar al comienzo del espacio de direcciones para capturar las desreferencias NULL. Puede ser bastante grande: en sistemas de 64 bits, de hecho, puede ser de cuatro gigabytes , por lo que el truncamiento accidental de punteros a 32 bits se detectará rápidamente. Sin embargo, la segmentación en el estricto sentido x86 apenas se usa en absoluto; hay un segmento plano para el espacio de usuario y otro para el kernel, y tal vez un par para trucos especiales como el uso de FS y GS.
zwol

1
@ LorenzoDematté NT utiliza excepciones en lugar de señales; en este caso STATUS_ACCESS_VIOLATION.
Random832

18

Una falla de segmentación es un acceso a una dirección de memoria que no está permitido (no es parte del proceso, o intenta escribir datos de solo lectura, o ejecutar datos no ejecutables, ...). Esto es detectado por la MMU (Unidad de administración de memoria, hoy parte de la CPU), lo que provoca una interrupción. La interrupción es manejada por el núcleo, que envía una SIGSEGFAULTseñal (ver signal(2)por ejemplo) al proceso ofensivo. El controlador predeterminado para esta señal volca el núcleo (ver core(5)) y finaliza el proceso.

El caparazón no tiene absolutamente ninguna mano en esto.


3
Entonces, ¿su biblioteca C, como glibc en un escritorio, define la cadena?
drewbenn el

77
También vale la pena señalar que SIGSEGV se puede manejar / ignorar. Por lo tanto, es posible escribir un programa que no sea terminado por él. La máquina virtual Java es un ejemplo notable que utiliza SIGSEGV internamente para diferentes propósitos, como se menciona aquí: stackoverflow.com/questions/3731784/…
Karol Nowak

2
Del mismo modo, en Windows, .NET no se molesta en agregar comprobaciones de puntero nulo en la mayoría de los casos, solo detecta violaciones de acceso (equivalente a segfaults).
immibis
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.