Un système d'exploitation injecte-t-il son propre code machine lorsque vous ouvrez un programme?


32

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.


7
Voir: Changement de contexte Le système d'exploitation effectue un changement de contexte vers l'application. L'application peut alors demander des services OS qui font un contexte vers l'OS. À la fin de l'application, le contexte revient au système d'exploitation.
Guy Coder

4
Voir aussi "syscall".
Raphael


1
Si les commentaires et les réponses ne répondent pas à votre question pour votre compréhension ou votre satisfaction, veuillez demander plus d'informations en tant que commentaire et expliquer ce que vous pensez ou où vous êtes perdu, ou sur quoi vous avez spécifiquement besoin de plus de détails.
Guy Coder

2
Je pense que l' interruption , le raccordement (d'une interruption), le minuteur matériel (avec crochet de planification et de gestion) et la pagination (réponse partielle à votre remarque sur la mémoire interdite) sont les principaux mots clés dont vous avez besoin. Le système d'exploitation doit coopérer étroitement avec le processeur pour exécuter son code uniquement en cas de besoin. Ainsi, la majeure partie de la puissance du processeur peut être utilisée pour le calcul réel, pas pour sa gestion.
Palec

Réponses:


35

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.

  1. 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.

  2. 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 pendant for (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.

  3. 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.

  4. 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.

  1. Dans les systèmes coopératifs (ou non préemptifs), il existe une yieldfonction 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.

  2. 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.

  3. 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.


5
Pouvez-vous développer davantage sur votre 2. dernier point? Je suis curieux de cette question, et je sens que l'explication est ignorée ici. Il me semble que la question est "comment le système d'exploitation récupère le processeur du processus" et votre réponse dit "Le système d'exploitation le gère". mais comment? Prenez la boucle infinie dans votre premier exemple: comment ne gèle-t-il pas l'ordinateur?
BiAiB

3
Certains systèmes d'exploitation le font, la plupart des systèmes d'exploitation au moins se moquent du code pour effectuer la "liaison", de sorte que le programme peut être chargé à n'importe quelle adresse
Ian Ringrose

1
@BiAiB Le mot clé ici est "interrompre". Le CPU n'est pas seulement quelque chose qui traite un flux d'instructions donné, il peut également être interrompu de manière asynchrone à partir d'une source distincte - plus important pour nous, les E / S et les interruptions d'horloge. Étant donné que seul le code de l'espace noyau peut gérer les interruptions, Windows peut être sûr de pouvoir "voler" le travail de n'importe quel processus en cours d'exécution à tout moment. Les gestionnaires d'interruption peuvent exécuter le code qu'ils souhaitent, y compris "stocker les registres du processeur quelque part et les restaurer à partir d'ici (un autre thread)". Extrêmement simplifié, mais c'est le changement de contexte.
Luaan

1
Ajout à cette réponse; le style de multitâche mentionné aux points 2 et 3 est appelé "multitâche préemptif", le nom fait référence à la capacité du système d'exploitation à préempter un processus en cours d'exécution. Le multitâche coopératif était fréquemment utilisé sur les anciens systèmes d'exploitation; sur Windows, au moins le multitâche préemptif n'a été introduit que sous Windows 95. J'ai lu au moins un système de contrôle industriel utilisé aujourd'hui qui utilise toujours Windows 3.1 uniquement pour son comportement multitâche coopératif en temps réel.
Jason C

3
@BiAiB En fait, vous vous trompez. Les processeurs de bureau n'exécutent pas de code de manière séquentielle et synchrone depuis environ le i486. Cependant, même les CPU plus anciens avaient toujours des entrées asynchrones - des interruptions. Imaginez une demande d'interruption matérielle (IRQ) comme une broche sur le CPU lui-même - quand elle arrive 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 pas x86ou quel que soit le code, elle est littéralement câblée. Après avoir sauté, il exécute à nouveau (n'importe quel) x86code. Les threads sont une abstraction beaucoup plus élevée.
Luaan

12

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.


4
Les interruptions ne sont pas définies par le système d'exploitation, elles sont fournies par le matériel. Et la plupart des architectures actuelles ont des instructions spéciales pour appeler le système d'exploitation. i386 a utilisé une interruption (générée par logiciel) pour cela, mais ce n'est plus le cas sur ses successeurs.
vonbrand

2
Je sais que les interruptions sont définies par le processeur, mais le noyau configure les pointeurs. Je l'ai peut-être mal expliqué. Je pensais également que Linux utilisait toujours int 9 pour parler au noyau, mais il y a peut-être de meilleures façons maintenant.
Programmdude

C'est une réponse assez trompeuse bien que la notion que les planificateurs préemptifs soient entraînés par des interruptions de temporisation est correcte. Tout d'abord, il convient de noter que la minuterie est matérielle. Également pour clarifier que le processus "save ... restore" est appelé un changement de contexte et implique principalement la sauvegarde de tous les registres CPU (qui inclut le pointeur d'instruction), entre autres choses. De plus, les processus peuvent modifier efficacement les tables d'interruption, c'est ce qu'on appelle le "mode protégé", qui définit également la mémoire virtuelle, et existe depuis le 286 - un pointeur vers la table d'interruption est stocké dans un registre accessible en écriture.
Jason C

(De plus, même la table des interruptions en mode réel a été déplaçable - pas verrouillée sur la première page de mémoire - depuis le 8086.)
Jason C

1
Cette réponse manque un détail critique. Lorsqu'une interruption se déclenche, le CPU ne bascule pas directement vers le noyau. Au lieu de cela, il enregistre d'abord les registres existants, puis passe à une autre pile, et ce n'est qu'alors que le noyau est appelé. Appeler le noyau avec une pile aléatoire à partir d'un programme aléatoire serait une assez mauvaise idée. De plus, la dernière partie est trompeuse. Vous n'obtiendrez pas d'interruption en "essayant" de lire la mémoire non mappée; c'est tout simplement impossible. Vous lisez à partir d'adresses virtuelles et la mémoire non mappée n'a tout simplement pas d'adresse virtuelle.
MSalters

5

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.


Pas seulement l'interruption d'horloge: toute interruption. Et aussi des instructions de changement de mode.
Gilles 'SO- arrête d'être méchant'

3

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)


L'AFAICR 3.1 était également coopérative. Win95 a été le point de départ du multitâche préemptif. Le mode protégé a principalement apporté l'isolement de l'espace d'adressage (ce qui améliore la stabilité, mais pour des raisons largement indépendantes).
cHao

Thunking ne réécrit pas ou n'injecte pas de code dans l'application. Le chargeur modifié est basé sur le système d'exploitation et n'est pas un produit de l'application. Les langages d'interprétation qui sont compilés tels que l'utilisation de compilateurs JIT ne modifient pas le code, ni n'injectent quoi que ce soit dans le code. Ils traduisent le code source en un exécutable. Encore une fois, ce n'est pas la même chose que d'injecter du code dans une application.
Dave Gordon

Transmeta a pris le code exécutable x86 comme source, pas un langage interprétatif. Et j'ai pensé à un cas dans lequel du code est injecté: s'exécuter sous un débogueur. Les systèmes X86 écrasent généralement l'instruction au point d'arrêt avec "INT 03", qui intercepte le débogueur. À la reprise, l'opcode d'origine est rétabli.
pjc50

Le débogage n'est guère la façon dont quelqu'un exécute une application; au-delà de celle du développeur de l'application. Je ne pense donc pas que cela aide vraiment le PO.
Dave Gordon

3

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.


2

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.


2

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 méthode FP est toujours utilisée sur les processeurs ARM, qui contrairement aux processeurs x86 ont toujours des variantes sans FP. Mais c'est rare car la plupart des ARM sont utilisés dans des appareils dédiés. Dans ces environnements, il est généralement connu si le processeur aura des capacités FP.
MSalters

Dans aucun de ces cas, le système d'exploitation n'a injecté de code dans l'application. Pour que le système d'exploitation injecte du code, il aurait besoin d'une licence du fournisseur du logiciel pour "modifier" l'application qu'il n'obtient pas. Le système d'exploitation NE PAS injecter de code.
Dave Gordon

On peut raisonnablement dire que les instructions @DaveGordon Trapped sont le code d'injection du système d'exploitation dans l'application.
Gilles 'SO- arrête d'être méchant'

@MSalters Les instructions piégées se produisent généralement dans les machines virtuelles.
Gilles 'SO- arrête d'être méchant'
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.