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 bash
ejecutar 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?
fuente
crsh
Es una gran idea para este tipo de experimentación. Gracias por informarnos a todos sobre la idea que hay detrás.crsh
, pensé que se pronunciaría "accidente". No estoy seguro de si ese es un nombre igualmente apropiado.system()
hace debajo del capó. ¡Resulta quesystem()
generará un proceso de shell! Entonces, su proceso de shell genera otro proceso de shell y ese proceso de shell (probablemente/bin/sh
o algo así) es el que ejecuta el programa. La forma/bin/sh
o elbash
trabajo es mediante el uso defork()
yexec()
(u otra función en laexecve()
familia).man 2 wait
, incluirá las macrosWIFSIGNALED()
yWTERMSIG()
.(WIFSIGNALED(status) && WTERMSIG(status) == 11)
que imprima algo tonto ("YOU DUN GOOFED AND TRIGGERED A SEGFAULT"
). Cuando ejecuté elsegfault
programa desde adentrocrsh
, imprimió exactamente eso. Mientras tanto, los comandos que salen normalmente no producen el mensaje de error.Respuestas:
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,6Una 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".
SIGSEGV
Es 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
SIGSEGV
señal para el proceso. Supongamos que el proceso no configuró un controlador de señalSIGSEGV
, por lo que el núcleo lleva a cabo el comportamiento predeterminado, que es finalizar el proceso. Esto tiene los mismos efectos que la_exit
llamada 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.
SIGSEGV
va 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 laswait
llamadas 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 unaSIGSEGV
señal.El proceso principal puede luego informar el evento a un humano imprimiendo un mensaje; Los programas de shell casi siempre hacen esto. Su
crsh
no incluye código para hacer eso, pero sucede de todos modos, debido a que la rutina de biblioteca Csystem
se ejecuta una concha con todas las funciones,/bin/sh
"bajo el capó".crsh
es el abuelo en este escenario; se envía la notificación del proceso principal/bin/sh
, que imprime su mensaje habitual. Luego/bin/sh
se cierra, ya que no tiene nada más que hacer, y la implementación de la biblioteca Csystem
recibe 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
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.
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 .
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í.
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 ".Algunas violaciones de protección de memoria generan
SIGBUS
("Error de bus") en lugar deSIGSEGV
. 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 paraSIGSEGV
, probablemente sea una buena idea definir el mismo controladorSIGBUS
."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.
Todas las otras formas en que el proceso principal puede ser notificado de que un niño ha finalizado, terminan con el padre llamando
wait
y recibiendo un estado de salida. Es solo que algo más sucede primero.fuente
mmap
un 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.)El shell realmente tiene algo que ver con ese mensaje, e
crsh
indirectamente llama a un shell, lo que probablemente seabash
.Escribí un pequeño programa en C que siempre seg falla:
Cuando lo ejecuto desde mi shell predeterminado
zsh
, obtengo esto:Cuando lo ejecuto
bash
, obtengo lo que notó en su pregunta: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 porcrsh
exec es un shell,/bin/sh
segúnman 3 system
. Es/bin/sh
casi seguro imprimir "Falla de segmentación", ya quecrsh
ciertamente no lo es.Si vuelve a escribir
crsh
para utilizar laexecve()
llamada del sistema para ejecutar el programa, no verá la cadena "Fallo de segmentación". Proviene del caparazón invocado porsystem()
.fuente
execvp
e 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).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.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.
fuente
#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.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
SIGSEGFAULT
señal (versignal(2)
por ejemplo) al proceso ofensivo. El controlador predeterminado para esta señal volca el núcleo (vercore(5)
) y finaliza el proceso.El caparazón no tiene absolutamente ninguna mano en esto.
fuente