Je suis vraiment curieux en ce moment. Je suis un programmeur Python, et cette question m'a simplement embrouillé: vous écrivez un système d'exploitation. Comment le lancez-vous?
Comment une application peut-elle s'exécuter sans être dans un OS? Comment dites-vous à l'ordinateur d'exécuter, disons, C, et d'exécuter ces commandes à l'écran, s'il n'a pas de système d'exploitation sur lequel s'exécuter?
Cela a-t-il à voir avec un noyau UNIX? Si oui, qu'est-ce qu'un noyau Unix, ou un noyau en général?
Je suis sûr que les OS sont plus compliqués que ça, mais comment ça marche?
Il existe de nombreux sites Web qui passent par le processus de démarrage (tels que Comment les ordinateurs démarrent ). En un mot, c'est un processus en plusieurs étapes qui continue à construire le système un peu à la fois jusqu'à ce qu'il puisse enfin démarrer les processus du système d'exploitation.
Cela commence par le firmware sur la carte mère qui essaie de faire fonctionner le CPU. Il charge ensuite le BIOS qui est comme un mini système d'exploitation qui permet à l'autre matériel de fonctionner. Une fois cela fait, il recherche un périphérique de démarrage (disque, CD, etc.) et, une fois trouvé, il localise le MBR (enregistrement de démarrage principal), le charge en mémoire et l'exécute. C'est ce petit morceau de code qui sait alors comment initialiser et démarrer le système d'exploitation (ou d'autres chargeurs de démarrage car les choses sont devenues plus compliquées). C'est à ce stade que des choses comme le noyau seraient chargées et commenceraient à fonctionner.
C'est assez incroyable que ça marche du tout!
Un système d'exploitation "bare metal" ne fonctionne pas dans quoi que ce soit. Il exécute l'ensemble d'instructions complet sur la machine physique et a accès à toute la mémoire physique, à tous les registres de périphérique et à toutes les instructions privilégiées, y compris celles qui contrôlent le matériel de prise en charge de la mémoire virtuelle.
(Si le système d'exploitation s'exécute sur une machine virtuelle, il peut penser qu'il se trouve dans la même situation que ci-dessus. La différence est que certaines choses sont émulées ou d'une autre manière gérée par l'hyperviseur, c'est-à-dire le niveau qui exécute les machines virtuelles.)
Quoi qu'il en soit, bien que le système d'exploitation puisse être implémenté en (par exemple) C, il ne disposera pas de toutes les bibliothèques C normales. En particulier, il n'aura pas les bibliothèques "stdio" normales. Il implémentera plutôt (par exemple) un pilote de périphérique de disque qui lui permettra de lire et d'écrire des blocs de disque. Il implémentera un système de fichiers au-dessus de la couche de bloc de disque, et en plus il implémentera les appels système que les bibliothèques d'exécution d'une application utilisateur effectuent (par exemple) pour créer, lire et écrire des fichiers ... et ainsi de suite.
Comment une application peut-elle s'exécuter sans être dans un OS?
Il doit s'agir d'un type particulier d'application (par exemple, un système d'exploitation) qui sait comment interagir directement avec le matériel d'E/S, etc.
Comment dites-vous à l'ordinateur d'exécuter, disons, C, et d'exécuter ces commandes à l'écran, s'il n'a pas de système d'exploitation sur lequel s'exécuter?
Non.
L'application (qui était à des fins d'écriture écrite en C) est compilée et liée sur une autre machine pour donner une image de code native. Ensuite, l'image est écrite sur le disque dur à un endroit où le BIOS peut la trouver. Le BIOS charge l'image en mémoire et exécute une instruction pour passer au point d'entrée de l'application.
Il n'y a (généralement) aucune "exécution de C et exécution de commandes" dans l'application, sauf s'il s'agit d'un système d'exploitation complet. Et dans ce cas, il est de la responsabilité du système d'exploitation de mettre en œuvre toutes les infrastructures nécessaires pour y arriver. Pas de magie. Juste beaucoup de code.
réponse de Bill couvre l'amorçage qui est le processus par lequel vous passez d'une machine hors tension à une machine dans laquelle la normale le système d'exploitation est opérationnel. Cependant, il convient de noter que lorsque le BIOS termine ses tâches, il donne (généralement) un contrôle total du matériel au système d'exploitation principal, et ne joue plus aucun rôle - jusqu'au prochain redémarrage du système. Le système d'exploitation principal ne fonctionne certainement pas "dans" le BIOS au sens conventionnel.
Cela a-t-il à voir avec un noyau UNIX? Si oui, qu'est-ce qu'un noyau Unix, ou un noyau en général?
Oui.
Le noyau UNIX est le cœur du système d'exploitation UNIX. C'est la partie d'UNIX qui fait tout le "métal nu" décrit ci-dessus.
L'idée d'un "noyau" est que vous essayez de séparer le logiciel système en trucs de base (qui nécessite un accès au périphérique physique, toute la mémoire, etc.) et des trucs non essentiels. Le noyau se compose du noyau.
En réalité, la distinction entre noyau/noyau et non noyau/non noyau est plus compliquée que cela. Et il y a eu beaucoup de débats sur ce qui appartient vraiment à un noyau, et ce qui ne l'est pas. (Recherchez par exemple le micro-noyau.)
Au début, il n'y avait pas de puissance dans le CPU.
Et l'homme a dit "qu'il y ait du courant", et le CPU a commencé à lire à partir d'une adresse donnée en mémoire et à exécuter l'instruction qui y était présente. Puis le suivant et ainsi de suite jusqu'à la fin du pouvoir.
C'était le démarrage. Sa tâche consistait à charger un autre logiciel pour accéder à l'environnement, où se trouvait le logiciel principal, et à le charger.
Enfin, un écran convivial vous invite à vous connecter.
Désolé d'être en retard, mais je vais le décrire comme tel:
La carte mère est alimentée.
Les circuits de temporisation démarrent et se stabilisent si nécessaire, uniquement en fonction de leurs caractéristiques électriques. Certains appareils plus récents peuvent en fait utiliser un microprocesseur ou un séquenceur très limité.
Il convient de noter que beaucoup de choses comme "les circuits de synchronisation démarrent et se stabilisent si nécessaire" ne se produisent plus vraiment dans le matériel. Une grande partie de ce travail est en fait un logiciel extrêmement spécialisé fonctionnant sur des sous-processeurs/séquenceurs très limités.
Le CPU et la RAM sont alimentés.
Le processeur charge (en fonction de son câblage interne) les données du BIOS. Sur certaines machines, le BIOS peut être mis en miroir à RAM puis exécuté à partir de là mais c'est rare IIRC.
Lorsqu'ils sont activés, les CPU compatibles x86 démarrent à l'adresse 0xFFFFFFF0 dans l'espace d'adressage ...
-Micheal Steil, 17 erreurs commises par Microsoft dans le système de sécurité Xbox ( archive )
Le BIOS appelle les ports matériels et les adresses utilisées par la carte mère pour le disque et les autres matériels IO et fait tourner les disques, obtient le reste de RAM fonctionne, entre autres choses.
Le code BIOS (via les paramètres CMOS, stockés dans le matériel) utilise des commandes de bas niveau IDE ou SATA pour lire le secteur de démarrage de chaque disque, dans un ordre spécifié par le CMOS ou un remplacement utilisateur avec un menu.
Le premier disque avec un secteur de démarrage obtient son secteur de démarrage exécuté. Ce secteur de démarrage est Assembly qui a des instructions pour charger plus de données à partir du disque, charger un NTLDR
plus grand, les étapes ultérieures de GRUB
, etc.
Enfin, le code machine du système d'exploitation est exécuté par le chargeur de démarrage, directement ou indirectement via le chargement en chaîne chargeant un secteur de démarrage à partir d'un emplacement alternatif ou décalé.
Vous obtenez alors une panique amicale du noyau, un pingouin étouffé ou votre disque s'arrête en raison d'un crash de tête. =) Dans le scénario alternatif, votre noyau configure des tables de processus, des structures en mémoire et monte des disques, chargeant des pilotes, des modules et une interface graphique ou un ensemble de services (si sur un serveur). Ensuite, les programmes sont exécutés au fur et à mesure que leurs en-têtes sont lus, et leur assembly est mis en mémoire et mappé en conséquence.
Il y a beaucoup de bonnes réponses mais je voulais ajouter ceci: vous avez mentionné que vous veniez d'un arrière-plan Python. Python est un texte non interprété (ou "interpilé" ou autre) , au moins dans les cas d'utilisation typiques de CPython). Cela signifie que vous avez un autre logiciel (l'interpréteur Python) qui regarde la source et l'exécute d'une certaine manière. Ceci est un modèle fin et permet assez belles langues de haut niveau bien abstraites du matériel réel. L'inconvénient est que vous avez toujours besoin de ce logiciel d'interprétation en premier.
Ces logiciels d'interprétation sont généralement écrits dans un langage qui se compile en code machine, par exemple C ou C++. Le code machine est ce que le CPU peut gérer. Ce qu'un processeur peut faire, c'est lire certains octets de la mémoire et, selon les valeurs d'octets, démarrer une opération spécifique. Ainsi, une séquence d'octets est une commande pour charger des données de la mémoire dans un registre, une autre séquence pour ajouter deux valeurs, une autre pour stocker la valeur d'un registre dans la mémoire principale et bientôt (un registre est une zone de mémoire spéciale qui fait partie du processeur où il peut fonctionner le mieux), la plupart de ces commandes sont assez faibles à ce niveau. Le code lisible par l'utilisateur pour ces instructions de code machine est le code assembleur. Ce code machine, en gros, est ce qui est stocké dans les fichiers .exe ou.com sur Windows ou à l'intérieur des binaires Linux/Unix.
Maintenant, si un ordinateur est démarré, il est stupide, mais il a un câblage qui lira ces instructions de code machine. Sur un PC, il s'agit généralement (actuellement) d'une puce EEPROM sur la carte mère contenant le BIOS (système d'entrée ou de sortie de base), ce système ne peut pas faire grand-chose, il peut faciliter l'accès à du matériel, etc., puis effectuer une opération clé: accédez à la démarrez et copiez les premiers octets (alias l'enregistrement de démarrage principal, MBR) dans la mémoire, puis dites au processeur "ici, il y a votre programme", le processeur traitera ensuite ces octets comme du code machine et l'exécutera. En général, il s'agit d'un chargeur de système d'exploitation qui chargera le noyau avec certains paramètres, puis remettra le contrôle à ce noyau, qui chargera ensuite tous ses pilotes pour accéder à tout le matériel, charger un bureau ou un programme Shell ou autre et permettre à l'utilisateur de se connecter et utiliser le système.
Vous demandez "Comment une application peut-elle s'exécuter sans être dans un système d'exploitation". La réponse simple est "un OS n'est pas une application". Bien qu'un OS puisse être créé avec les mêmes outils qu'une application et fabriqué à partir de la même matière première, ce n'est pas la même chose. Un OS n'a pas à jouer selon les mêmes règles qu'une application.
OTOH, vous pouvez considérer le matériel et le firmware réels comme le "système d'exploitation" dans lequel "l'application" du système d'exploitation s'exécute. Le matériel est un système d'exploitation très simple - il sait comment exécuter des instructions écrites en code machine, et il sait que lors de son démarrage, il doit regarder une adresse mémoire très spécifique pour sa première instruction. Ainsi, il démarre puis exécute immédiatement cette toute première instruction, suivie de la seconde, et ainsi de suite.
Ainsi, le système d'exploitation est simplement un code machine qui existe à un emplacement connu et qui peut interagir directement avec le matériel.
Pour répondre à votre question, vous devez savoir à quoi ressemble le code natif (pour le processeur) et comment il est interprété par le processeur.
Habituellement, tout le processus de compilation est basé sur la traduction des choses que vous écrivez en C, Pascal ou même Python (en utilisant pypy) et C # en des choses que le CPU comprend, c'est-à-dire des instructions simples comme "stocker quelque chose sous [adresse mémoire] "," ajouter des numéros stockés dans les registres eax et ebx "," call function foo "," compare eax to 10 ". Ces instructions, exécutées une par une, font ce que vous vouliez faire avec votre code.
Pensez maintenant à ceci: vous n'avez pas vraiment besoin d'un OS pour exécuter ce code natif! Tout ce dont vous avez besoin est de charger ce code en mémoire et de dire au CPU qu'il est là et que vous voulez qu'il soit exécuté. Mais ne vous en faites pas trop. C'est le travail dont le BIOS devrait s'inquiéter - il charge votre code (un et un secteur uniquement), juste après le démarrage du CPU, sous l'adresse physique 0x7C00. Ensuite, le CPU commence à exécuter ce seul secteur (512 B) de votre code. Et vous pouvez faire tout ce que vous imaginez! Sans, bien sûr, aucun support de l'OS. C'est parce que VOUS êtes le système d'exploitation. Cool hein? Pas de bibliothèque standard, pas de boost, pas de python, pas de programmes, pas de pilotes! Vous devez tout écrire par vous-même.
Et comment communiquez-vous avec le matériel? Eh bien, vous avez deux choix:
Vous demandez maintenant ce qu'est le noyau. En bref, le noyau est tout ce que vous ne voyez pas et ne ressentez pas directement. Il gère, avec les pilotes, tout, depuis votre clavier jusqu'à presque tous les éléments matériels de votre PC. Vous communiquez avec lui par shell graphique ou terminal. Ou par des fonctions à l'intérieur de votre code, maintenant exécutées, heureusement, avec le support du système d'exploitation.
Pour une meilleure compréhension, je peux vous donner un conseil: essayez d'écrire votre propre système d'exploitation. Même si ça va écrire "Hello world" sur l'écran.
Pour comprendre le fonctionnement des systèmes d'exploitation, il peut être utile de les diviser en deux catégories: ceux qui fournissent simplement des services aux applications sur demande, et ceux qui utilisent les fonctionnalités matérielles du CPU pour empêcher les applications de faire des choses qu'elles ne devraient pas faire. MS-DOS était de l'ancien style; toutes les versions de Windows depuis 3.0 ont été le dernier style (au moins lors de l'exécution de quelque chose de plus puissant qu'un 8086).
Le PC IBM original exécutant PC-DOS ou MS-DOS aurait été un exemple de l'ancien style de "OS". Si une application souhaitait afficher un caractère à l'écran, il y aurait eu plusieurs façons de le faire. Il pourrait appeler la routine qui demanderait à MS-DOS de l'envoyer à "sortie standard". S'il le faisait, MS-DOS vérifierait si la sortie était redirigée et, sinon, il appellerait une routine stockée dans ROM (dans une collection de routines IBM appelée Basic Input/Output) System) qui afficherait un caractère à la position du curseur et déplacerait le curseur ("write teletype"). Cette routine du BIOS stockerait alors une paire d'octets quelque part dans la plage 0xB800: 0 à 0xB800: 3999; matériel sur l'adaptateur graphique couleur récupérera à plusieurs reprises des paires d'octets dans cette plage, en utilisant le premier octet de chaque paire pour sélectionner une forme de caractère et le second pour sélectionner les couleurs de premier plan et d'arrière-plan. Les octets sont récupérés et traités en signaux rouge, vert et bleu, dans une séquence ce qui donne un affichage de texte lisible.
Les programmes sur le PC IBM peuvent afficher du texte en utilisant la routine DOS "sortie standard", ou en utilisant la routine "write teletype" du BIOS, ou en le stockant directement dans la mémoire d'affichage. De nombreux programmes qui devaient afficher beaucoup de texte ont rapidement opté pour cette dernière approche, car elle pouvait être des centaines de fois plus rapide que l'utilisation des routines DOS. Ce n'était pas parce que les routines DOS et BIOS étaient exceptionnellement inefficaces; à moins que l'affichage ne soit masqué, il ne peut être écrit qu'à certains moments. La routine du BIOS pour produire un caractère a été conçue pour pouvoir être appelée à tout moment; chaque demande devait donc recommencer à attendre le bon moment pour effectuer une opération d'écriture. En revanche, le code d'application qui savait ce qu'il devait faire pourrait s'organiser autour des opportunités disponibles pour écrire l'affichage.
Un point clé ici est que si le DOS et le BIOS fournissaient un moyen de sortie de texte à l'affichage, il n'y avait rien de particulièrement "magique" à propos de telles capacités. Une application qui voulait écrire du texte sur l'écran pouvait le faire tout aussi efficacement, du moins si le matériel d'affichage fonctionnait comme prévu (si quelqu'un avait installé un adaptateur d'affichage monochrome, qui était similaire au CGA mais avait sa mémoire de caractères situé à 0xB000: 0000-0xB000: 3999), le BIOS y enverrait automatiquement des caractères; une application programmée pour fonctionner avec le MDA ou le CGA pourrait également le faire, mais une application programmée uniquement pour le CGA serait totalement inutile sur le MDA).
Sur les nouveaux systèmes, les choses sont un peu différentes. Les processeurs ont différents modes de "privilège". Ils commencent dans le mode le plus privilégié, où le code est autorisé à faire tout ce qu'il veut. Ils peuvent ensuite basculer dans un mode restreint, où seules des plages de mémoire ou des fonctions d'E/S sélectionnées sont disponibles. Le code ne peut pas passer directement d'un mode restreint au mode privilège, mais le processeur a défini des points d'entrée en mode privilégié, et le code en mode restreint peut demander au processeur de commencer à exécuter du code à l'un de ces points d'entrée en mode privilégié. De plus, il existe des points d'entrée en mode privilégié associés à un certain nombre d'opérations qui seraient interdites en mode restreint. Supposons, par exemple, que quelqu'un veuille exécuter plusieurs applications MS-DOS simultanément, chacune ayant son propre écran. Si les applications pouvaient écrire directement sur le contrôleur d'affichage à 0xB800: 0, il n'y aurait aucun moyen d'empêcher une application d'écraser l'écran d'une autre application. D'un autre côté, un système d'exploitation pourrait exécuter l'application en mode restreint et intercepter tous les accès à la mémoire d'affichage; s'il découvrait qu'une application qui était supposée être en "arrière-plan" essayait d'écrire 0xB800: 160, il pourrait stocker les données dans une mémoire qu'il avait mise de côté comme tampon d'écran d'application d'arrière-plan. Si cette application passe ultérieurement au premier plan, le tampon peut alors être copié sur l'écran réel.
Les éléments clés à noter sont (1) bien qu'il soit souvent pratique d'avoir un ensemble standard de routines pour effectuer divers services standard tels que l'affichage de texte, ils ne font rien qu'une application qui s'exécutait en "mode privilégié" ne pouvait pas faire s'il a été correctement programmé pour gérer le matériel installé; (2) bien que la plupart des applications en cours d'exécution soient empêchées par leur système d'exploitation de faire directement de telles E/S, un programme qui démarre en mode privilégié fait ce qu'il veut et peut configurer toutes les règles qu'il souhaite pour le mode restreint. programmes.
Il existe certaines différences concernant le fonctionnement d'un système d'exploitation qui dépendent extrêmement du système. Pour être utile, un système doit avoir un comportement prévisible au démarrage, tel que "démarrer l'exécution à l'adresse X". Pour les systèmes qui ont un stockage non volatile (comme la mémoire Flash) mappé dans leur espace programme, cela est assez facile car vous vous assurez simplement de placer le code de démarrage au bon endroit dans l'espace programme du processeur. Ceci est extrêmement courant pour les microcontrôleurs. Certains systèmes doivent récupérer leurs programmes de démarrage à partir d'un autre emplacement avant de l'exécuter. Ces systèmes comporteront certaines opérations câblées (ou presque câblées). Certains processeurs récupèrent leur code de démarrage via i2c à partir d'une autre puce, de sorte que le processeur effectue des opérations non triviales sans exécuter aucune instruction d'assemblage, puis commence à exécuter des instructions à une adresse prédéfinie.
Les systèmes utilisant la famille de processeurs x86 utilisent généralement un processus de démarrage en plusieurs étapes qui est assez complexe en raison de son évolution et de ses problèmes de compatibilité descendante. Le système exécute un micrologiciel (appelé BIOS - Basic Input/Output System, ou similaire) qui se trouve dans une mémoire non volatile de la carte mère. Parfois, tout ou partie de ce micrologiciel est copié (déplacé) dans RAM pour le rendre plus rapide. Ce code a été écrit en sachant quel matériel serait présent et utilisable pour le démarrage.
Le firmware de démarrage est généralement rédigé avec des hypothèses sur le matériel qui sera présent sur le système. Il y a des années sur une machine 286, on supposerait probablement qu'il y aurait un contrôleur de lecteur de disquette à l'adresse d'E/S X et chargerait le secteur 0 à un certain emplacement de mémoire si on lui donnait un certain ensemble de commandes (et le code au secteur 0 sait comment utiliser les propres fonctions du BIOS pour charger plus de code, et éventuellement assez de code pour être un OS est chargé). Sur un microcontrôleur, on peut supposer qu'un port série fonctionne avec certains paramètres et qu'il devrait attendre les commandes (pour mettre à jour le micrologiciel plus complexe) pendant X fois avant de poursuivre le processus de démarrage.
Le processus de démarrage exact d'un système donné n'est pas aussi important pour vous que de savoir qu'il diffère sur différents systèmes, mais aussi qu'ils ont tous des choses en commun. Souvent dans le code de démarrage (bootstrapping) lorsque les E/S doivent être effectuées, les périphériques d'E/S sont interrogés plutôt que de compter sur des interruptions. En effet, les interruptions sont complexes, utilisez la pile RAM (qui n'est peut-être pas encore complètement configurée), et vous n'avez pas à vous soucier de bloquer d'autres opérations lorsque vous êtes la seule opération.
Lors du premier chargement, le noyau du système d'exploitation (le noyau est la partie principale de la plupart des systèmes d'exploitation) agira initialement un peu comme le micrologiciel. Il devra être programmé en connaissance ou découvrir le matériel présent, configurer certains RAM comme espace de pile, faire divers tests, configurer diverses structures de données, éventuellement découvrir et monter un système de fichiers, puis lancez probablement un programme qui ressemble davantage aux programmes que vous avez l'habitude d'écrire (un programme qui repose sur la présence d'un système d'exploitation).
Le code du système d'exploitation est généralement écrit dans un mélange de C et d'assemblage. Le tout premier code pour le noyau du système d'exploitation est probablement toujours dans Assembly et fait des choses comme configurer la pile, sur laquelle le code C s'appuie, puis appelle une fonction C. D'autres assemblages écrits à la main seront également là car certaines opérations qu'un OS doit faire ne sont souvent pas exprimables en C (comme les piles de changement/échange de contexte). Souvent, des drapeaux spéciaux doivent être passés au compilateur C pour lui dire de ne pas s'appuyer sur les bibliothèques standard utilisées par la plupart des programmes C et de ne pas s'attendre à ce qu'il y ait une int main(int argc, char *argv[])
dans le programme. De plus, des options spéciales de l'éditeur de liens que la plupart des programmeurs d'applications n'utilisent jamais doivent être utilisées. Celles-ci peuvent faire en sorte que le programme du noyau s'attende à être chargé à une certaine adresse ou à configurer des choses pour ressembler à des variables externes à certains emplacements, même si ces variables n'ont jamais été déclarées dans aucun code C (cela est utile pour les E/S mappées en mémoire ou autres emplacements de mémoire spéciaux).
L'ensemble de l'opération semble magique à première vue, mais après l'avoir examiné et en comprendre certaines parties, la magie devient juste un ensemble de programmes qui nécessitent beaucoup plus de planification et de connaissances système pour être implémentés. Les déboguer, cependant, prend de la magie.
Comme l'a dit Stephen C., il ne s'agit pas seulement de démarrer le système d'exploitation, mais aussi de savoir comment il fonctionne, interagit avec le matériel et les logiciels qui le composent.
Je vais juste ajouter à sa réponse, que vous voudrez peut-être jeter un oeil à "Les éléments des systèmes informatiques" . C'est un livre et quelques outils, qui expliquent comment un ordinateur, un système d'exploitation et des compilateurs interagissent. La particularité est qu'il vous donne les outils pour développer très rapidement votre propre système d'exploitation dans un environnement simulé, en ignorant les nombreux détails requis pour un réel, afin que vous puissiez saisir les concepts . Il fait un excellent travail pour vous permettre de voir la forêt au lieu des arbres.
Si vous souhaitez entrer dans plus de détails sur la façon dont le système d'exploitation interagit avec le matériel, consultez Minix .
Vous écrivez un OS. Il doit être exécuté d'une manière ou d'une autre, et de cette façon est dans un autre système d'exploitation?
Votre application s'exécute dans un système d'exploitation. Ce système d'exploitation fournit des services à votre application, notamment l'ouverture d'un fichier et l'écriture d'octets. Ces services sont généralement fournis via des appels système.
Le système d'exploitation s'exécute dans le matériel. Le matériel fournit des services au système d'exploitation, notamment la définition du débit en bauds d'un port série et l'écriture d'octets. Ces services sont généralement fournis via des registres mappés en mémoire ou des ports d'E/S.
Pour donner un exemple très simplifié de la façon dont cela fonctionne:
Votre application demande au système d'exploitation d'écrire quelque chose dans un fichier. Pour votre application, le système d'exploitation fournit des concepts tels que des fichiers et des répertoires.
Sur le matériel, ces concepts n'existent pas. Le matériel fournit des concepts tels que des disques divisés en blocs fixes de 512 octets. Le système d'exploitation décide des blocs à utiliser pour votre fichier et de certains autres blocs pour les métadonnées comme le nom de fichier, la taille et l'emplacement sur le disque. Il indique ensuite au matériel: écrire ces 512 octets dans le secteur avec ce numéro sur le disque avec ce numéro; écrire ces 512 autres octets dans le secteur avec ce numéro différent sur le disque avec ce même numéro; etc.
La façon dont le système d'exploitation demande au matériel de le faire varie beaucoup. L'une des fonctions d'un système d'exploitation est de protéger les applications de ces différences. Pour l'exemple de disque, sur un type de matériel, le système d'exploitation devrait écrire le disque et le numéro de secteur sur un port d'E/S, puis écrire les octets un par un sur un port d'E/S distinct. Sur un autre type de matériel, le système d'exploitation devrait copier les 512 octets d'un secteur dans une zone de mémoire, écrire l'emplacement de cette zone de mémoire dans un emplacement de mémoire spécial et écrire le disque et le numéro de secteur dans un autre emplacement de mémoire spécial.
Le matériel haut de gamme d'aujourd'hui est extrêmement compliqué. Les manuels donnant tous leurs détails de programmation sont des butoirs de porte avec des milliers de pages; par exemple, le dernier manuel du processeur Intel comprend sept volumes, avec un total de plus de 4000 pages, et ce uniquement pour le processeur. La plupart des autres composants exposent des blocs de mémoire ou des ports d'E/S, que le système d'exploitation peut indiquer au CPU de mapper aux adresses de son espace d'adressage. Plusieurs de ces composants exposent encore plus de choses derrière quelques ports d'E/S ou adresses mémoire; à titre d'exemple, le RTC (horloge en temps réel, le composant qui garde l'heure de l'ordinateur lorsqu'il est hors tension) expose quelques centaines d'octets de mémoire derrière une paire de ports d'E/S, et c'est un composant très simple qui remonte au PC/AT d'origine. Des choses comme les disques durs ont des processeurs séparés, auxquels le système d'exploitation communique via des commandes standardisées. Les GPU sont encore plus compliqués.
Plusieurs personnes dans les commentaires ci-dessus ont suggéré l'Arduino. Je suis d'accord avec eux, c'est beaucoup plus simple à comprendre: l'ATmega328, qui fait tout sur l'Arduino Uno sauf exposer le connecteur USB comme un port série, a un manuel de quelques centaines de pages seulement. Sur l'Arduino, vous exécutez directement sur le matériel, sans système d'exploitation entre les deux; juste quelques petites routines de bibliothèque, que vous n'avez pas à utiliser si vous ne le souhaitez pas.
Exemples exécutables
Techniquement, un programme qui s'exécute sans OS est un OS. Voyons donc comment créer et exécuter de minuscules systèmes d'exploitation Hello World.
Le code de tous les exemples ci-dessous est présent sur ce dépôt GitHub .
Secteur de démarrage
Sur x86, la chose la plus simple et la plus basse que vous pouvez faire est de créer un Master Boot Sector (MBR) , qui est un type de boot sector , puis de l'installer sur un disque.
Ici, nous en créons un avec un seul appel printf
:
_printf '\364%509s\125\252' > main.img
Sudo apt-get install qemu-system-x86
qemu-system-x86_64 -hda main.img
_
Résultat:
Testé sur Ubuntu 18.04, QEMU 2.11.1.
_main.img
_ contient les éléments suivants:
_\364
_ en octal == _0xf4
_ en hexadécimal: l'encodage d'une instruction hlt
, qui indique au CPU de cesser de fonctionner.
Par conséquent, notre programme ne fera rien: seulement démarrer et arrêter.
Nous utilisons octal car _\x
_ les nombres hexadécimaux ne sont pas spécifiés par POSIX.
Nous pourrions obtenir cet encodage facilement avec:
_echo hlt > a.asm
nasm -f bin a.asm
hd a
_
mais l'encodage _0xf4
_ est également documenté dans le manuel d'Intel.
_%509s
_ produit 509 espaces. Nécessaire pour remplir le fichier jusqu'à l'octet 510.
_\125\252
_ en octal == _0x55
_ suivi de _0xaa
_: octets magiques requis par le matériel. Ils doivent être les octets 511 et 512.
S'il n'est pas présent, le matériel ne le traitera pas comme un disque amorçable.
Notez que même sans rien faire, quelques caractères sont déjà imprimés à l'écran. Ceux-ci sont imprimés par le micrologiciel et servent à identifier le système.
Fonctionne sur du matériel réel
Les émulateurs sont amusants, mais le matériel est la vraie affaire.
Notez cependant que cela est dangereux et que vous pourriez effacer votre disque par erreur: ne faites cela que sur des machines anciennes qui ne contiennent pas de données critiques! Ou encore mieux, des devboards tels que le Raspberry Pi, voir l'exemple ARM ci-dessous).
Pour un ordinateur portable typique, vous devez faire quelque chose comme:
Gravez l'image sur une clé USB (cela détruira vos données!):
_Sudo dd if=main.img of=/dev/sdX
_
branchez l'USB sur un ordinateur
allume ça
dites-lui de démarrer à partir de l'USB.
Cela signifie que le micrologiciel doit choisir USB avant le disque dur.
Si ce n'est pas le comportement par défaut de votre machine, continuez à appuyer sur Entrée, F12, ESC ou d'autres clés étranges après la mise sous tension jusqu'à ce que vous obteniez un menu de démarrage où vous pouvez choisir de démarrer à partir de l'USB.
Il est souvent possible de configurer l'ordre de recherche dans ces menus.
Par exemple, sur mon ancien Lenovo Thinkpad T430, UEFI BIOS 1.16, je peux voir:
Bonjour tout le monde
Maintenant que nous avons créé un programme minimal, passons à un monde bonjour.
La question évidente est: comment faire IO? Quelques options:
port série . Il s'agit d'un protocole standardisé très simple qui envoie et récupère des caractères à partir d'un terminal hôte.
Source .
Il n'est malheureusement pas exposé sur la plupart des ordinateurs portables modernes, mais c'est la voie à suivre pour les cartes de développement, voir les exemples ARM ci-dessous).
C'est vraiment dommage, car de telles interfaces sont vraiment utiles pour déboguer le noyau Linux par exemple .
utiliser les fonctionnalités de débogage des puces. ARM appelle la leur semihosting par exemple. Sur le vrai matériel, cela nécessite un support matériel et logiciel supplémentaire, mais sur les émulateurs, cela peut être une option pratique gratuite. Exemple .
Ici, nous allons faire un exemple de BIOS car c'est plus simple sur x86. Mais notez que ce n'est pas la méthode la plus robuste.
main.S
_.code16
mov $msg, %si
mov $0x0e, %ah
loop:
lodsb
or %al, %al
jz halt
int $0x10
jmp loop
halt:
hlt
msg:
.asciz "hello world"
_
link.ld
_SECTIONS
{
. = 0x7c00;
.text :
{
__start = .;
*(.text)
. = 0x1FE;
SHORT(0xAA55)
}
}
_
Assembler et lier avec:
_gcc -c -g -o main.o main.S
ld --oformat binary -o main.img -T linker.ld main.o
_
Résultat:
Testé sur: Lenovo Thinkpad T430, UEFI BIOS 1.16. Disque généré sur un hôte Ubuntu 18.04.
Outre les instructions de montage standard de l'utilisateur, nous avons:
_.code16
_: indique à GAS de sortir du code 16 bits
cli
: désactive les interruptions logicielles. Ceux-ci pourraient faire redémarrer le processeur après le hlt
_int $0x10
_: effectue un appel BIOS. C'est ce qui imprime les caractères un par un.
Les indicateurs de lien importants sont:
--oformat binary
_: sortie du code d'assemblage binaire brut, ne le déformez pas dans un fichier ELF comme c'est le cas pour les exécutables standard de l'espace utilisateur.Utilisez C au lieu de Assembly
Puisque C se compile en Assembly, utiliser C sans la bibliothèque standard est assez simple, vous avez simplement besoin de:
main
, notamment: TODO: liez donc un exemple x86 sur GitHub. Voici un ARM celui que j'ai créé .
Cependant, les choses deviennent plus amusantes si vous souhaitez utiliser la bibliothèque standard, car nous n'avons pas le noyau Linux, qui implémente une grande partie des fonctionnalités de la bibliothèque standard C via POSIX .
Quelques possibilités, sans passer par un système d'exploitation complet comme Linux, incluent:
Exemple détaillé sur: https://electronics.stackexchange.com/questions/223929/c-standard-libraries-on-bare-metal/223931
Dans Newlib, vous devez implémenter les syscalls vous-même, mais vous obtenez un système très minimal et il est très facile de les implémenter.
Par exemple, vous pouvez rediriger printf
vers les systèmes UART ou ARM, ou implémenter exit()
avec semi-hébergement .
systèmes d'exploitation embarqués comme FreeRTOS et Zephyr .
De tels systèmes d'exploitation vous permettent généralement de désactiver la planification préventive, vous donnant ainsi un contrôle total sur l'exécution du programme.
Ils peuvent être vus comme une sorte de Newlib pré-implémenté.
[~ # ~] bras [~ # ~]
Dans ARM, les idées générales sont les mêmes. J'ai téléchargé:
quelques exemples simples de baremetal QEMU ici sur GitHub . exemple Prompt.c prend l'entrée de votre terminal hôte et renvoie la sortie tout au long de l'UART simulé:
_enter a character
got: a
new alloc of 1 bytes at address 0x0x4000a1c0
enter a character
got: b
new alloc of 2 bytes at address 0x0x4000a1c0
enter a character
_
une configuration de clignotant Raspberry Pi entièrement automatisée sur: https://github.com/cirosantilli/raspberry-pi-bare-metal-blinker
Pour le Raspberry Pi, https://github.com/dwelch67/raspberrypi ressemble au tutoriel le plus populaire disponible aujourd'hui.
Certaines différences par rapport à x86 incluent:
IO se fait en écrivant directement aux adresses magiques, il n'y a pas d'instructions in
et out
.
Cela s'appelle IO mappé en mémoire .
pour certains matériels réels, comme le Raspberry Pi, vous pouvez ajouter le micrologiciel (BIOS) vous-même à l'image disque.
C'est une bonne chose, car cela rend la mise à jour de ce firmware plus transparente.
Firmware
En vérité, votre secteur de démarrage n'est pas le premier logiciel qui s'exécute sur le processeur du système.
Ce qui s'exécute en premier est le soi-disant firmware, qui est un logiciel:
Les firmwares bien connus incluent:
Le firmware fait des choses comme:
boucle sur chaque disque dur, USB, réseau, etc. jusqu'à ce que vous trouviez quelque chose d'amorçable.
Lorsque nous exécutons QEMU, _-hda
_ indique que _main.img
_ est un disque dur connecté au matériel, et
hda
est le premier à être essayé, et il est utilisé.
charger les 512 premiers octets dans RAM adresse mémoire _0x7c00
_, y mettre le RIP du CPU, et le laisser s'exécuter
afficher des choses comme le menu de démarrage ou les appels d'impression du BIOS sur l'écran
Le micrologiciel offre des fonctionnalités de type OS dont dépendent la plupart des OS. Par exemple. un Python a été porté pour fonctionner sur BIOS/UEFI: https://www.youtube.com/watch?v=bYQ_lq5dcvM
On peut affirmer que les firmwares sont indiscernables des OS, et que le firmware est la seule "vraie" programmation bare metal que l'on puisse faire.
Comme ceci CoreOS dev le dit :
La partie difficile
Lorsque vous allumez un PC, les puces qui composent le chipset (northbridge, southbridge et SuperIO) ne sont pas encore correctement initialisées. Même si le BIOS ROM est aussi éloigné du CPU qu'il pourrait l'être, il est accessible par le CPU, car il doit l'être, sinon le CPU n'aurait aucune instruction à exécuter. ne signifie pas que le BIOS ROM est complètement mappé, généralement pas. Mais juste assez est mappé pour démarrer le processus de démarrage. Tout autre périphérique, oubliez-le.
Lorsque vous exécutez Coreboot sous QEMU, vous pouvez expérimenter avec les couches supérieures de Coreboot et les charges utiles, mais QEMU offre peu de possibilités d'expérimenter avec le code de démarrage de bas niveau. D'une part, RAM fonctionne juste dès le début.
Après l'état initial du BIOS
Comme beaucoup de choses dans le matériel, la standardisation est faible, et l'une des choses sur lesquelles vous devez pas compter est l'état initial des registres lorsque votre code commence à s'exécuter après le BIOS.
Faites-vous donc une faveur et utilisez un code d'initialisation comme celui-ci: https://stackoverflow.com/a/32509555/895245
Les registres comme _%ds
_ et _%es
_ ont des effets secondaires importants, vous devez donc les mettre à zéro même si vous ne les utilisez pas explicitement.
Notez que certains émulateurs sont plus agréables que le vrai matériel et vous donnent un bon état initial. Ensuite, lorsque vous exécutez sur du vrai matériel, tout se casse.
GNU GRUB Multiboot
Les secteurs de démarrage sont simples, mais ils ne sont pas très pratiques:
C'est pour ces raisons que GNU GRUB a créé un format de fichier plus pratique appelé multiboot.
Exemple de fonctionnement minimal: https://github.com/cirosantilli/x86-bare-metal-examples/tree/d217b180be4220a0b4a453f31275d38e697a99e0/multiboot/hello-world
Je l'utilise également sur mon dépôt d'exemples GitHub pour pouvoir exécuter facilement tous les exemples sur du matériel réel sans graver l'USB un million de fois. Sur QEMU, cela ressemble à ceci:
Si vous préparez votre système d'exploitation en tant que fichier multiboot, GRUB est alors en mesure de le trouver dans un système de fichiers standard.
C'est ce que font la plupart des distributions, en plaçant les images du système d'exploitation sous _/boot
_.
Les fichiers multiboot sont essentiellement un fichier ELF avec un en-tête spécial. Ils sont spécifiés par GRUB at: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html
Vous pouvez transformer un fichier multiboot en un disque amorçable avec _grub-mkrescue
_.
El Torito
Format pouvant être gravé sur CD: https://en.wikipedia.org/wiki/El_Torito_%28CD-ROM_standard%29
Il est également possible de produire une image hybride qui fonctionne sur ISO ou USB. Cela peut être fait avec _grub-mkrescue
_ ( exemple ), et est également fait par le noyau Linux sur _make isoimage
_ en utilisant isohybrid
.
Ressources