J'étudie les CPU et je sais comment il lit un programme de la mémoire et exécute ses instructions. Je comprends également qu'un système d'exploitation sépare les programmes dans les processus, puis alterne entre chacun si rapidement que vous pensez qu'ils s'exécutent en même temps, mais en fait, chaque programme s'exécute seul dans le CPU. Mais, si le système d'exploitation est également un tas de code exécuté dans le processeur, comment peut-il gérer les processus?
J'ai réfléchi et la seule explication que je pouvais penser est: lorsque le système d'exploitation charge un programme de la mémoire externe dans la RAM, il ajoute ses propres instructions au milieu des instructions du programme d'origine, donc le programme est exécuté, le programme peut appeler le système d'exploitation et faire certaines choses. Je crois qu'il y a une instruction que l'OS ajoutera au programme, qui permettra au CPU de revenir au code OS un certain temps. Et aussi, je crois que lorsque le système d'exploitation charge un programme, il vérifie s'il y a des instructions interdites (qui passeraient à des adresses interdites dans la mémoire) et les élimine ensuite.
Suis-je en train de penser juste? Je ne suis pas un étudiant CS, mais en fait, un étudiant en mathématiques. Si possible, je voudrais un bon livre à ce sujet, car je n'ai trouvé personne qui explique comment le système d'exploitation peut gérer un processus si le système d'exploitation est également un tas de code exécuté dans le processeur, et qu'il ne peut pas fonctionner en même temps heure du programme. Les livres disent seulement que le système d'exploitation peut gérer les choses, mais maintenant comment.
la source
Réponses:
Non. Le système d'exploitation ne plaisante pas avec le code du programme qui lui injecte du nouveau code. Cela aurait un certain nombre d'inconvénients.
Cela prendrait beaucoup de temps, car le système d'exploitation devrait parcourir l'intégralité de l'exécutable pour effectuer ses modifications. Normalement, une partie de l'exécutable n'est chargée que si nécessaire. En outre, l'insertion est coûteuse car vous devez déplacer une charge de trucs.
En raison de l'indécidabilité du problème d'arrêt, il est impossible de savoir où insérer vos instructions "Revenir au système d'exploitation". Par exemple, si le code inclut quelque chose comme
while (true) {i++;}
, vous devez absolument insérer un crochet à l'intérieur de cette boucle mais la condition sur la boucle (true
, ici) pourrait être arbitrairement compliquée, vous ne pouvez donc pas décider de la durée de la boucle. D'un autre côté, il serait très inefficace d'insérer des crochets dans chaque boucle: par exemple, sauter en arrière vers l'OS pendantfor (i=0; i<3; i++) {j=j+i;}
ralentirait beaucoup le processus. Et, pour la même raison, vous ne pouvez pas détecter de courtes boucles pour les laisser tranquilles.En raison de l'indécidabilité du problème d'arrêt, il est impossible de savoir si les injections de code ont changé la signification du programme. Par exemple, supposons que vous utilisez des pointeurs de fonction dans votre programme C. L'injection de nouveau code déplacerait les emplacements des fonctions. Ainsi, lorsque vous en appeliez un via le pointeur, vous sautiez au mauvais endroit. Si le programmeur était assez malade pour utiliser des sauts calculés, ceux-ci échoueraient également.
Il jouerait un joyeux enfer avec n'importe quel système anti-virus, car il changerait également le code du virus et gâcherait toutes vos sommes de contrôle.
Vous pouvez contourner le problème du problème d'arrêt en simulant le code et en insérant des crochets dans n'importe quelle boucle qui s'exécute plus d'un certain nombre de fois fixe. Cependant, cela nécessiterait une simulation extrêmement coûteuse de l'ensemble du programme avant de pouvoir s'exécuter.
En fait, si vous vouliez injecter du code, le compilateur serait l'endroit naturel pour le faire. De cette façon, vous n'auriez à le faire qu'une seule fois, mais cela ne fonctionnerait toujours pas pour les deuxième et troisième raisons indiquées ci-dessus. (Et quelqu'un pourrait écrire un compilateur qui ne fonctionnerait pas.)
Il existe trois façons principales pour le système d'exploitation de reprendre le contrôle des processus.
Dans les systèmes coopératifs (ou non préemptifs), il existe une
yield
fonction qu'un processus peut appeler pour redonner le contrôle à l'OS. Bien sûr, si c'est votre seul mécanisme, vous dépendez des processus qui se comportent bien et un processus qui ne cède pas monopolise le processeur jusqu'à ce qu'il se termine.Pour éviter ce problème, une interruption de minuterie est utilisée. Les processeurs permettent au système d'exploitation d'enregistrer des rappels pour tous les différents types d'interruptions que le processeur implémente. Le système d'exploitation utilise ce mécanisme pour enregistrer un rappel pour une interruption de temporisation qui est déclenchée périodiquement, ce qui lui permet d'exécuter son propre code.
Chaque fois qu'un processus essaie de lire un fichier ou d'interagir avec le matériel de toute autre manière, il demande au système d'exploitation de travailler pour lui. Lorsque le système d'exploitation est invité à faire quelque chose par un processus, il peut décider de mettre ce processus en attente et de commencer à en exécuter un autre. Cela peut sembler un peu machiavélique, mais c'est la bonne chose à faire: les E / S de disque sont lentes, vous pouvez donc laisser le processus B s'exécuter pendant que le processus A attend que les morceaux de métal en rotation se déplacent au bon endroit. Les E / S réseau sont encore plus lentes. Les E / S du clavier sont glaciales car les gens ne sont pas des êtres gigahertz.
la source
1
, le CPU arrête tout ce qu'elle fait et commence à traiter l'interruption (ce qui signifie essentiellement "préserver l'état et passer à une adresse en mémoire"). La gestion des interruptions elle-même n'est pasx86
ou quel que soit le code, elle est littéralement câblée. Après avoir sauté, il exécute à nouveau (n'importe quel)x86
code. Les threads sont une abstraction beaucoup plus élevée.Bien que la réponse de David Richerby soit bonne, elle donne un aperçu de la façon dont les systèmes d'exploitation modernes arrêtent les programmes existants. Ma réponse devrait être exacte pour l'architecture x86 ou x86_64, qui est la seule couramment utilisée pour les ordinateurs de bureau et les ordinateurs portables. D'autres architectures devraient avoir des méthodes similaires pour y parvenir.
Lorsque le système d'exploitation démarre, il établit une table d'interruptions. Chaque entrée du tableau pointe vers un peu de code à l'intérieur du système d'exploitation. Lorsque des interruptions se produisent, contrôlées par le CPU, il examine ce tableau et appelle le code. Il existe diverses interruptions, telles que la division par zéro, un code non valide et certaines définies par le système d'exploitation.
C'est ainsi que le processus utilisateur parle au noyau, par exemple s'il veut lire / écrire sur le disque ou autre chose que le noyau du système d'exploitation contrôle. Un système d'exploitation configurera également une minuterie qui appelle une interruption à la fin, de sorte que le code en cours d'exécution est forcé de passer du programme utilisateur au noyau du système d'exploitation, et le noyau peut faire d'autres choses telles que mettre en file d'attente d'autres programmes à exécuter.
De la mémoire, lorsque cela se produit, le noyau du système d'exploitation doit enregistrer où se trouvait le code, et lorsque le noyau a fini de faire ce qu'il doit faire, il restaure l'état précédent du programme. Ainsi, le programme ne sait même pas qu'il a été interrompu.
Le processus ne peut pas changer la table d'interruption pour deux raisons, la première est qu'il s'exécute dans un environnement protégé, donc s'il essaie d'appeler un certain code d'assembly protégé, le processeur déclenchera une autre interruption. La deuxième raison est la mémoire virtuelle. L'emplacement de la table d'interruptions est de 0x0 à 0x3FF dans la mémoire réelle, mais avec les processus utilisateur, cet emplacement n'est généralement pas mappé, et essayer de lire la mémoire non mappée déclenchera une autre interruption, donc sans la fonction protégée et la possibilité d'écrire sur la RAM réelle , le processus utilisateur ne peut pas le modifier.
la source
Le noyau du système d'exploitation reprend le contrôle du processus en cours grâce au gestionnaire d'interruption de l'horloge du processeur, et non en injectant du code dans le processus.
Vous devriez lire sur les interruptions pour obtenir plus de précisions sur leur fonctionnement et la façon dont les noyaux du système d'exploitation les gèrent et implémentent différentes fonctionnalités.
la source
Il existe une méthode similaire à celle que vous décrivez: le multitâche coopératif . Le système d'exploitation n'insère pas d'instructions, mais chaque programme doit être écrit pour appeler des fonctions du système d'exploitation qui peuvent choisir d'exécuter un autre des processus coopératifs. Cela présente les inconvénients que vous décrivez: un programme qui plante plante tout le système. Windows jusqu'à 3.0 inclus fonctionnait comme ceci; 3.0 en "mode protégé" et au-dessus ne l'a pas fait.
Le multitâche préventif (le type normal de nos jours) repose sur une source externe d'interruptions. Les interruptions remplacent le flux de contrôle normal et sauvegardent généralement les registres quelque part, afin que le processeur puisse faire autre chose et reprendre le programme de manière transparente. Bien sûr, le système d'exploitation peut modifier le registre "lorsque vous laissez les interruptions reprendre ici", il reprend donc à l'intérieur d'un processus différent.
(Certains systèmes font des instructions ré - écriture de manière limitée sur la charge du programme, appelé « thunking », et le processeur Transmeta dynamiquement recompilés à son propre jeu d'instructions)
la source
Le multitâche ne nécessite rien comme l'injection de code. Dans un système d'exploitation comme Windows, il existe un composant du code du système d'exploitation appelé le planificateur qui repose sur une interruption matérielle déclenchée par un minuteur matériel. Ceci est utilisé par le système d'exploitation pour basculer entre les différents programmes et lui-même, ce qui semble à notre perception humaine se produire simultanément.
Fondamentalement, le système d'exploitation programme la minuterie matérielle pour qu'elle s'éteigne de temps en temps ... peut-être 100 fois par seconde. Lorsque le minuteur s'éteint, il génère une interruption matérielle - un signal qui indique au CPU d'arrêter ce qu'il fait, de sauvegarder son état sur la pile, de changer son mode en quelque chose de plus privilégié et d'exécuter le code qu'il trouvera dans un système spécialement désigné mettre en mémoire. Ce code fait partie du planificateur, qui décide de ce qui doit être fait ensuite. Il peut s'agir de reprendre un autre processus, auquel cas il devra effectuer ce que l'on appelle un «changement de contexte» - en remplaçant l'intégralité de son état actuel (y compris les tables de mémoire virtuelle) par celui de l'autre processus. En revenant à un processus, il doit restaurer tout le contexte de ce processus,
L'endroit "spécialement désigné" dans la mémoire ne doit être connu que par le système d'exploitation. Les implémentations varient, mais l'essentiel est que le CPU répondra à diverses interruptions en effectuant une recherche de table; l'emplacement de la table se trouve à un endroit spécifique de la mémoire (déterminé par la conception matérielle du CPU), le contenu de la table est défini par le système d'exploitation (généralement au démarrage) et le "type" d'interruption déterminera quelle entrée dans le tableau doit être utilisé comme "routine de service d'interruption".
Rien de tout cela n'implique une "injection de code" ... il est basé sur le code contenu dans le système d'exploitation en coopération avec les caractéristiques matérielles du CPU et ses circuits de support.
la source
Je pense que l'exemple du monde réel le plus proche de ce que vous décrivez est l' une des techniques utilisées par VMware , la virtualisation complète à l'aide de la traduction binaire .
VMware agit comme une couche sous un ou plusieurs systèmes d'exploitation exécutant simultanément sur le même matériel.
La plupart des instructions en cours d'exécution (par exemple dans des applications ordinaires) peuvent être virtualisées à l'aide du matériel, mais un noyau du système d'exploitation lui-même utilise des instructions qui ne peuvent pas être virtualisées, car si le code machine du système d'exploitation devinait exécuté sans modification, il "éclaterait" "du contrôle de l'hôte VMware. Par exemple, un système d'exploitation invité devrait s'exécuter dans l'anneau de protection le plus privilégié et configurer la table d'interruption. Si cela avait été autorisé, VMware aurait perdu le contrôle du matériel.
VMware réécrit ces instructions dans le code du système d'exploitation avant de l'exécuter, en les remplaçant par des sauts dans le code VMware qui simule l'effet souhaité.
Cette technique est donc quelque peu analogue à ce que vous décrivez.
la source
Il existe une variété de cas dans lesquels un système d'exploitation peut "injecter du code" dans un programme. Les versions 68000 du système Apple Macintosh créent une table de tous les points d'entrée de segment (situés immédiatement avant les variables globales statiques, IIRC). Lorsqu'un programme démarre, chaque entrée du tableau se compose d'une instruction d'interruption suivie du numéro de segment et décalée dans le segment. Si l'interruption est exécutée, le système examinera les mots après l'instruction d'interruption pour voir quel segment et décalage est requis, charger le segment (s'il ne l'est pas déjà), ajouter l'adresse de début du segment au décalage et puis remplacez le piège par un saut à cette adresse nouvellement calculée.
Sur les anciens logiciels PC, bien que cela n'ait pas été fait techniquement par le "OS", il était courant que le code soit construit avec des instructions de déroutement au lieu d'instructions mathématiques du coprocesseur. Si aucun coprocesseur mathématique n'était installé, le gestionnaire d'interruptions l'émulerait. Si un coprocesseur a été installé, la première fois qu'une interruption est prise, le gestionnaire remplacera l'instruction d'interruption par une instruction de coprocesseur; les futures exécutions du même code utiliseront directement l'instruction du coprocesseur.
la source