Il semble que je ne trouve aucune information à ce sujet en dehors de "la MMU de la CPU envoie un signal" et "le noyau le dirige vers le programme incriminé, en le mettant fin".
J'ai supposé qu'il envoie probablement le signal au shell et que celui-ci le gère en mettant fin au processus incriminé et en imprimant "Segmentation fault"
. J'ai donc testé cette hypothèse en écrivant un shell extrêmement minimal que j'appelle crsh (crap shell). Ce shell ne fait rien sauf prendre les entrées utilisateur et les alimenter à la system()
méthode.
#include <stdio.h>
#include <stdlib.h>
int main(){
char cmdbuf[1000];
while (1){
printf("Crap Shell> ");
fgets(cmdbuf, 1000, stdin);
system(cmdbuf);
}
}
J'ai donc exécuté cette coquille dans un terminal nu (sans bash
courir en dessous). Ensuite, j'ai procédé à l'exécution d'un programme qui produit une erreur de segmentation. Si mes hypothèses étaient correctes, cela provoquerait a) un crash crsh
, la fermeture du xterm, b) pas d'impression "Segmentation fault"
, ou c) les deux.
braden@system ~/code/crsh/ $ xterm -e ./crsh
Crap Shell> ./segfault
Segmentation fault
Crap Shell> [still running]
Retour à la case départ, je suppose. Je viens de démontrer que ce n’est pas le shell qui fait cela, mais le système situé en dessous. Comment "Erreur de segmentation" est-il même imprimé? "Qui" le fait? Le noyau? Autre chose? Comment le signal et tous ses effets secondaires se propagent-ils du matériel à la fin du programme?
la source
crsh
est une excellente idée pour ce genre d’expérimentation. Merci de nous avoir tous informés de cette idée et de son idée.crsh
, j'ai pensé qu'il serait prononcé "crash". Je ne suis pas sûr que ce soit un nom tout aussi approprié.system()
passe sous le capot. Il s'avère que celasystem()
va engendrer un processus shell! Ainsi, votre processus shell génère un autre processus shell et ce processus (probablement/bin/sh
ou quelque chose du genre) est celui qui exécute le programme. La manière/bin/sh
oubash
fonctionne est en utilisantfork()
etexec()
(ou une autre fonction dans laexecve()
famille).man 2 wait
, elle inclura les macrosWIFSIGNALED()
etWTERMSIG()
.(WIFSIGNALED(status) && WTERMSIG(status) == 11)
avoir imprimer quelque chose de maladroit ("YOU DUN GOOFED AND TRIGGERED A SEGFAULT"
). Quand j'ai exécuté lesegfault
programme de l'intérieurcrsh
, il imprimait exactement cela. En attendant, les commandes qui se terminent normalement ne produisent pas le message d'erreur.Réponses:
Tous les processeurs modernes ont la capacité d' interrompre l'instruction machine en cours d'exécution. Ils sauvegardent suffisamment d’état (généralement, mais pas toujours, sur la pile) pour permettre la reprise ultérieure de l’exécution, comme si rien ne s’était passé (l’instruction interrompue sera redémarrée à partir de zéro, en général). Ensuite, ils commencent à exécuter un gestionnaire d’interruptions , qui est juste plus de code machine, mais placé à un emplacement spécial afin que la CPU sache où elle se trouve à l’avance. Les gestionnaires d'interruption font toujours partie du noyau du système d'exploitation: le composant qui s'exécute avec le plus grand privilège et est responsable de la supervision de l'exécution de tous les autres composants. 1,2
Les interruptions peuvent être synchrones , c'est-à-dire qu'elles sont déclenchées par la CPU elle-même en réponse directe à l'action de l'instruction en cours d'exécution, ou asynchrones , ce qui signifie qu'elles se produisent à un moment imprévisible en raison d'un événement externe, tel que les données arrivant sur le réseau. Port. Certaines personnes réservent le terme "interruption" pour les interruptions asynchrones et appellent des interruptions synchrones "traps", "faults" ou "exceptions", mais ces mots ont tous une autre signification, alors je vais m'en tenir à "interruption synchrone".
Aujourd'hui, la plupart des systèmes d'exploitation modernes ont une notion de processus . Il s’agit essentiellement d’un mécanisme selon lequel l’ordinateur peut exécuter plus d’un programme à la fois, mais c’est aussi un aspect essentiel de la façon dont les systèmes d’exploitation configurent la protection de la mémoire , caractéristique de la plupart des logiciels (mais, hélas, toujours pas tous ) les processeurs modernes. Cela va avec la mémoire virtuelle, qui permet de modifier le mappage entre les adresses de mémoire et les emplacements réels dans la RAM. La protection de la mémoire permet au système d'exploitation de donner à chaque processus son propre bloc de RAM privé, auquel il est le seul à pouvoir accéder. Il permet également au système d’exploitation (agissant pour le compte de certains processus) de désigner des régions de RAM comme étant en lecture seule, exécutables, partagées par un groupe de processus coopérants, etc. Il y aura également un bloc de mémoire uniquement accessible par le serveur. noyau. 3
Tant que chaque processus n'accède à la mémoire que de la manière que le processeur est configuré pour autoriser, la protection de la mémoire est invisible. Lorsqu'un processus enfreint les règles, le processeur génère une interruption synchrone, demandant au noyau de résoudre le problème. Il arrive régulièrement que le processus n'enfreigne pas vraiment les règles, seul le noyau doit effectuer certains travaux avant que le processus puisse continuer. Par exemple, si une page de la mémoire d'un processus doit être "expulsée" dans le fichier d'échange afin de libérer de l'espace dans la RAM pour autre chose, le noyau indiquera que cette page est inaccessible. La prochaine fois que le processus essaiera de l'utiliser, la CPU générera une interruption de protection de la mémoire. le noyau récupérera la page de swap, la remettra à sa place, la marquera de nouveau comme accessible et reprendra son exécution.
Mais supposons que le processus ait réellement enfreint les règles. Il a essayé d'accéder à une page sur laquelle aucune RAM n'a été mappée, ou a essayé d'exécuter une page marquée comme ne contenant pas de code machine, ou autre. La famille de systèmes d'exploitation généralement connue sous le nom "Unix" utilise tous des signaux pour faire face à cette situation. 4 Les signaux ressemblent aux interruptions, mais ils sont générés par le noyau et mis en champs par des processus, plutôt que par le matériel et par le noyau. Les processus peuvent définir des gestionnaires de signauxdans leur propre code, et dire au noyau où ils se trouvent. Ces gestionnaires de signaux s’exécuteront ensuite, interrompant le flux de contrôle normal, si nécessaire. Les signaux ont tous un numéro et deux noms, l'un étant un acronyme cryptique et l'autre une phrase légèrement moins cryptique. Le signal généré lorsque le processus enfreint les règles de protection de la mémoire est le numéro 11 (par convention), ainsi que ses noms
SIGSEGV
et "Défaut de segmentation". 5,6Une différence importante entre les signaux et les interruptions est qu’il existe un comportement par défaut pour chaque signal. Si le système d'exploitation ne parvient pas à définir des gestionnaires pour toutes les interruptions, il s'agit d'un bogue dans le système d'exploitation. Tout l'ordinateur se bloque lorsque le processeur tente d'appeler un gestionnaire manquant. Cependant, les processus ne sont nullement tenus de définir des gestionnaires de signaux pour tous les signaux. Si le noyau génère un signal pour un processus et que celui-ci a conservé son comportement par défaut, le noyau s'exécutera comme il se doit, quelle que soit la valeur par défaut, sans déranger le processus. Les comportements par défaut de la plupart des signaux sont "ne rien faire" ou "mettre fin à ce processus et peut-être aussi produire un vidage de la base".
SIGSEGV
est l'un de ces derniers.Donc, pour récapituler, nous avons un processus qui a brisé les règles de protection de la mémoire. La CPU a suspendu le processus et généré une interruption synchrone. Le noyau a mis en place cette interruption et généré un
SIGSEGV
signal pour le processus. Supposons que le processus n'ait pas configuré de gestionnaire de signalSIGSEGV
, le noyau applique donc le comportement par défaut, qui consiste à mettre fin au processus. Cela a tous les mêmes effets que l'_exit
appel système: les fichiers ouverts sont fermés, la mémoire est désallouée, etc.Jusque-là, rien n'a encore imprimé de message visible par un humain, et le shell (ou plus généralement, le processus parent du processus qui vient d'être terminé) n'a pas été impliqué du tout.
SIGSEGV
va au processus qui a enfreint les règles, pas son parent. L' étape suivante de la séquence consiste toutefois à informer le processus parent que son enfant a été arrêté. Cela peut se produire de plusieurs façons différentes, dont la plus simple est quand le parent attend déjà cette notification, en utilisant l' un deswait
appels système (wait
,waitpid
,wait4
, etc.). Dans ce cas, le noyau ne fera que renvoyer cet appel système et fournira au processus parent un numéro de code appelé statut de sortie.. 7 Le statut de sortie indique au parent pourquoi le processus enfant a été arrêté. dans ce cas, il apprendra que l'enfant a été arrêté en raison du comportement par défaut d'unSIGSEGV
signal.Le processus parent peut ensuite signaler l'événement à un humain en imprimant un message. les programmes shell le font presque toujours. Votre
crsh
code n'inclut pas cela, mais cela arrive quand même, parce que la routine de la bibliothèque Csystem
exécute un shell complet/bin/sh
, "sous le capot".crsh
est le grand - parent dans ce scénario; la notification de processus parent est remplie par/bin/sh
, ce qui affiche son message habituel. Ensuite,/bin/sh
elle quitte elle-même, car elle n'a plus rien à faire, et l'implémentation de la bibliothèque C desystem
reçoit cette notification de sortie. Vous pouvez voir cette notification de sortie dans votre code en inspectant la valeur de retour desystem
; mais cela ne vous dira pas que le processus de petit-enfant est mort sur un segfault, car il a été consommé par le processus de shell intermédiaire.Notes de bas de page
Certains systèmes d'exploitation n'implémentent pas les pilotes de périphérique dans le noyau; Cependant, tous les gestionnaires d'interruptions doivent toujours faire partie du noyau, de même que le code qui configure la protection de la mémoire, car le matériel ne permet rien d'autre que le noyau de faire cela.
Il peut exister un programme appelé "hyperviseur" ou "gestionnaire de machine virtuelle" encore plus privilégié que le noyau, mais aux fins de cette réponse, il peut être considéré comme faisant partie du matériel .
Le noyau est un programme , mais ce n'est pas un processus. cela ressemble plus à une bibliothèque. Tous les processus exécutent des parties du code du noyau, de temps en temps, en plus de leur propre code. Il peut y avoir un certain nombre de "threads du noyau" qui n'exécutent que le code du noyau, mais ils ne nous concernent pas ici.
Le seul et unique système d'exploitation que vous aurez probablement à traiter et qui ne peut pas être considéré comme une implémentation d'Unix est bien entendu Windows. Il n'utilise pas de signaux dans cette situation. ( En effet, il n'a pas avoir des signaux, sous Windows l'
<signal.h>
interface est complètement truqué par la bibliothèque C.) Il utilise ce qu'on appelle « gestion structurée des exceptions » au lieu.Certaines violations de la protection de la mémoire génèrent
SIGBUS
("Erreur de bus") au lieu deSIGSEGV
. La ligne entre les deux est sous-spécifiée et varie d'un système à l'autre. Si vous avez écrit un programme définissant un gestionnaireSIGSEGV
, c’est probablement une bonne idée de définir le même gestionnaireSIGBUS
."Erreur de segmentation" est le nom de l'interruption générée pour les violations de protection de la mémoire par l'un des ordinateurs qui exécutaient le système Unix d'origine , probablement le PDP-11 . La " segmentation " est un type de protection de la mémoire, mais de nos jours le terme " erreur de segmentation " désigne de manière générique toute violation de la protection de la mémoire.
Tous les autres moyens par lesquels le processus parent peut être averti qu'un enfant s'est terminé, aboutissent avec l'appel du parent
wait
et la réception d'un statut de sortie. C'est juste que quelque chose d'autre se passe en premier.la source
mmap
un fichier dans une région de mémoire plus grande que le fichier, puis accédez à des "pages entières" au-delà de la fin du fichier. (POSIX est par ailleurs assez vague quand SIGSEGV / SIGBUS / SIGILL / etc arriver.)Le shell a effectivement quelque chose à voir avec ce message et
crsh
appelle indirectement un shell, ce qui est probablement le casbash
.J'ai écrit un petit programme en C qui fait toujours la distinction entre les fautes:
Quand je le lance à partir de mon shell par défaut
zsh
, je reçois ceci:Quand je le lance à partir de
bash
, je comprends ce que vous avez noté dans votre question:J'allais écrire un gestionnaire de signal dans mon code, puis je me suis rendu compte que l'
system()
appel de bibliothèque utilisé parcrsh
exec est un shell,/bin/sh
selonman 3 system
. C’est/bin/sh
presque certainement l’impression «faute de segmentation», cecrsh
n’est certainement pas le cas.Si vous ré-écrivez
crsh
pour utiliser l'execve()
appel système pour exécuter le programme, vous ne verrez pas la chaîne "Erreur de segmentation". Cela vient du shell invoqué parsystem()
.la source
execvp
et refait le test pour constater que, même si le shell ne plante toujours pas (ce qui signifie que SIGSEGV n'est jamais envoyé au shell), il n'imprime pas "Erreur de segmentation". Rien n'est imprimé du tout. Cela semble indiquer que le shell détecte le moment où ses processus enfants sont tués et est responsable de l’impression "Défaut de segmentation" (ou une de ses variantes).waitpid()
sur chaque fork / exec, et il renvoie une valeur différente pour les processus qui ont une erreur de segmentation, par rapport aux processus qui se terminent avec le statut 0.C'est un peu un résumé tronqué. Le mécanisme de signal Unix est entièrement différent des événements spécifiques à la CPU qui lancent le processus.
En général, quand une adresse incorrecte est accédée (ou écrite dans une zone en lecture seule, une tentative d'exécution d'une section non exécutable, etc.), la CPU génère un événement spécifique à la CPU (sur les architectures traditionnelles non-VM). appelé violation de segmentation, car chaque "segment" (traditionnellement, le "texte" exécutable en lecture seule, les "données" inscriptibles et de longueur variable, et la pile traditionnellement située à l'extrémité opposée de la mémoire) avait une plage d'adresses fixe - sur une architecture moderne, il est plus probable qu'il s'agisse d'une erreur de page [pour la mémoire non mappée] ou d'une violation d'accès [pour les problèmes de lecture, d'écriture et d'exécution, et je me concentrerai sur cela pour le reste de la réponse).
Maintenant, à ce stade, le noyau peut faire plusieurs choses. Des défauts de page sont également générés pour la mémoire valide mais non chargée (par exemple, permutée, ou dans un fichier mmapped, etc.). Dans ce cas, le noyau mappera la mémoire, puis redémarrera le programme utilisateur à partir de l'instruction Erreur. Sinon, cela envoie un signal. Cela ne signifie pas "directement [l'événement d'origine] vers le programme incriminé", car le processus d'installation d'un gestionnaire de signaux est différent et indépendant de l'architecture, plutôt que si le programme devait simuler l'installation d'un gestionnaire d'interruptions.
Si un programme de traitement de signal est installé dans le programme utilisateur, cela signifie que vous créez un cadre de pile et que vous définissez la position d'exécution du programme utilisateur sur le programme de traitement de signal. La même chose est faite pour tous les signaux, mais dans le cas d’une violation de segmentation, les choses sont généralement arrangées de sorte que si le gestionnaire de signaux le renvoie, il relancera l’instruction qui a causé l’erreur. Le programme utilisateur peut avoir corrigé l'erreur, par exemple en mappant la mémoire sur l'adresse incriminée (cela dépend de l'architecture si cela est possible). Le gestionnaire de signaux peut également accéder à un emplacement différent du programme (généralement via longjmp ou en lançant une exception), pour abandonner l'opération, quelle qu'elle soit, à l'origine du mauvais accès à la mémoire.
Si aucun programme de traitement de signal n'est installé dans le programme utilisateur, il est simplement terminé. Sur certaines architectures, si le signal est ignoré, il peut redémarrer l'instruction encore et encore, provoquant une boucle infinie.
la source
#PF(fault-code)
(défaut de page) ou#GP(0)
("Si une adresse effective d'opérande de mémoire est en dehors du CS, Limite de segment DS, ES, FS ou GS. "). Le mode 64 bits supprime les contrôles de limite de segment, car les systèmes d’exploitation utilisent uniquement la pagination et un modèle de mémoire à plat pour l’espace utilisateur.Une erreur de segmentation est un accès à une adresse mémoire non autorisée (ne faisant pas partie du processus, essayant d'écrire des données en lecture seule ou d'exécuter des données non exécutables, ...). Ceci est intercepté par la MMU (unité de gestion de la mémoire, qui fait aujourd'hui partie de la CPU), provoquant une interruption. L'interruption est gérée par le noyau, qui envoie un
SIGSEGFAULT
signal (voirsignal(2)
par exemple) au processus en cause. Le gestionnaire par défaut de ce signal vide le noyau (voircore(5)
) et termine le processus.La coquille n'a absolument aucune main dans ceci.
la source