|=[ 0x02 ]=---------=[ Méthode de construction d'OS ]=-------------------=| |=-----------------------------------------------------------------------=| |=--------------=[ Bill Blunden ]=---------------=| --[ Table des matières 0 - Introduction 1 - Le moment critique 1.1 Choisir une plate-forme hôte 1.2 Construire un émulateur 1.3 - Coder un compilateur transversal 1.4 - Coder et porter l'OS 1.5 - Téléporter le compilateur transversal. 2 - Composants de l'OS 2.1 - Gestion des tâches 2.2 - Gestion de la mémoire 2.3 - Interface d'E/S 2.4 - Système de fichiers 2.5 - Notes sur la sécurité 3 - Etude d'un cas simple 3.1 - Plate-forme hôte 3.2 - Points de compilations 3.3 - Démarrage 3.4 - Initialiser l'OS 3.5 - Construire et déployer l'OS 4 - Références et Crédits --[ 0 - Introduction Sur les innombrables livres au sujet des systèmes d'exploitation, il n'y en a que deux ou trois (que je connaisse) qui traitent vraiment de la manière de créer un système d'exploitation complet. Ces livres se focalisent sur un matériel spécifique et les principales étapes (de construction) s'enterrent sous une montagne de détails étouffants. Ce n'est pas toujours une mauvaise chose, mais seulement une conséquence involontaire. Les systèmes d'exploitation sont des logiciels de plus en plus compliqués; en disséquer un mettra à jour d'innombrables détails. Mon but en soumettant cet article est de fournir une série d'étapes génériques qui peuvent être utilisées pour construire un OS, à partir du début et sans penchant pour un fabricant ( d'hardware ). [NDT: OS: Operating System, systeme d'exploitation] "dit oncle Tom, comment on construit un OS..." Ma propre connaissance de la construction d'OS était plutôt sommaire jusqu'à ce que j'aie le privilège de rencontrer quelques vieux embrumés de chez "Control Data". Ce sont eux qui ont travaillé sur le CDC 6600 avec Seymour Cray. La méthode que je vous expose a été utilisée pour faire le système d'exploitation SCOPE76 de Control Data. Bien que quelques-uns uns des ingénieurs avec lesquels j'ai discuté soient maintenant dans leurs 70 ans, je peux vous assurer que l'approche qu'ils m'ont décrite est toujours très utile et efficace. Pendant ces longues heures où j'ai harcelé ces vétérans du CDC pour des détails, j'ai appris plus d'une histoire intéressante. Par exemple, quand Control Data arriva avec le 6600, c'était plus rapide que tout ce qu'IBM vendait. Les dirigeants à Big Blue étaient tellement furieux qu'ils ont fait un tigre de papier: ils ont demandé à tout le monde d'attendre quelques mois. Malheureusement ça a marché; ils ont tous attendu qu'IBM livre (ce qu'ils n'ont pas fait, ces connards) et ça a forcé CDC à baisser le prix du 6600 de moitié pour attirer les clients. Si vous êtes familier des pratiques commerciales d'IBM, ce type de comportement ne vous surprendra pas. Saviez-vous qu'IBM a vendu les calculateurs Hollerith aux nazis pendant la deuxième guerre mondiale? Cet article est découpé en trois parties. La partie 1 présente une approche générale pour créer un système d'exploitation. Je serrai volontairement ambigu. Je veux que mon approche soit utile tout en ne tenant pas compte de la plate-forme matérielle que vous utiliseriez. Pour le détail des procédés eux-même, je les ai différés jusqu'à la partie 2. Celle-ci présente un plan grossier; pour déterminer l'ordre dans lequel les composants de l'OS devraient être implémentés. Pour éclairer un peu les problèmes qu'un ingénieur système rencontrera durant l'implémentation d'un OS, j'ai inclus une brève discussion sur un exemple plus détaillé dans la partie 3. Mon but dans cette partie est d'illustrer quelques remarques que j'aie faites dans la partie 1. Je n'ai pas l'intention d'offrir un produit commercial de qualité, il y en a déjà beaucoup d'excellents disponibles. Les lecteurs intéressés pourront aller chercher les références fournies en fin d'article. --[ 1 - Le moment critique Typiquement, en bourse, vous avez besoin d'argent pour faire de l'argent. Pour les OS, c'est la même chose: vous avez besoin d'un OS pour en faire un autre. Appelons l'OS initial et le matériel sur lequel il tourne la "plate-forme hôte". Je ferai référence à l'OS construit et le matériel sur lequel il fonctionnera sous le nom de "plate-forme cible". --[ 1.1 - Choisir une plate-forme hôte Je me souviens avoir demandé à un éclaireur des marines quelle arme-blanche était la plus efficace selon lui. Sa réponse: "Celle avec laquelle tu es le plus familier." Ca reste vrai pour le choix d'une plate-forme hôte. La meilleur est celle avec laquelle vous êtes le plus familier. Vous allez devoir faire quelques acrobaties et quelques fantaisies dans vos programmes et être intimement lié à votre OS et à vos utilitaires de développement. Dans certains cas pathologiques ça vous aidera aussi d'être familier avec le langage machine de votre matériel. Ca vous permettra de vérifier deux fois ce que votre utilitaire est en train de cracher. Vous allez peut être découvrir qu'il y a des bugs dans votre environnement initial, et être forcé de changer de plate-forme C'est une bonne raison pour choisir une plate-forme assez populaire pour qu'il y ai un assez bon choix. Par exemple, pendant un de mes travaux, sur Windows, j'ai découvert un bug dans l'assembleur (MASM). Quand ça s'est passé, MASM refusait d'assembler un fichier source excédant un certain nombre de lignes. Heureusement que j'ai pu acheter Turbo Assembleur de Borland (TASM) et poursuivre. --[ 1.2 - Construire un émulateur Une fois que vous avez trouvé votre plate-forme hôte et choisi vos logiciels de développement, vous avez besoin de construire un émulateur pour simuler le comportement de la plate-forme cible. Ca peut être plus long que prévu. Non seulement vous devez reproduire la base du matériel mais aussi mimer le BIOS qui est encré dans la ROM. Vous devez aussi simuler les périphériques et les micros contrôleurs. NB: La meilleure façon de vérifier que votre émulateur fonctionne correctement c'est de créer un fichier image d'une de vos partitions et de voir si l'émulateur est capable de faire fonctionner le système qui y est écrit. Par exemple, si vous faites un émulateur pour x86, vous pouvez y tester une image de la partition principale de Linux. Le principal avantage d'un émulateur c'est qu'il vous évitera de travailler dans le noir. Il n'y a rien de pire que d'avoir sa machine crashée et de ne pas savoir pourquoi. Regarder son PC se planter peut s'avérer extrêmement frustrant parce qu'il est presque impossible de diagnostiquer un problème une fois qu'il s'est produit. C'est particulièrement vrai pendant la phase d'amorçage, quand vous n'avez pas encore codé assez d'infrastructures pour déverser des messages sur la console. Un émulateur vous permet de voir ce qu'il se passe dans un environnement stable et contrôlé. Si votre code crash l'émulateur, vous pouvez insérer des procédures de diagnostique pour faciliter le travail de débugage. Vous pouvez aussi lancer l'émulateur dans un mode similaire au débuggeur pour pouvoir exécuter pas à pas pendant la phase problématique. L'alternative, c'est de lancer directement votre OS sur la machine, ce qui vous empêchera d'enregistrer l'état de la machine quand elle crashe. Les techniques de diagnostiques et de debugage que vous utilisiez avec l'émulateur seront remplacée par des tactiques purement spéculatives. Ce n'est pas gai, croyez-moi. Pour un excellent exemple d'émulateur, vous devriez jeter un coup d'oeil à l'émulateur boch x86. Il est disponible à: http://sourceforge.net/projects/bochs/ Encore une chose, c'est mieux d'utiliser bochs avec Linux. Parce que boch travaille avec des images de disque et la commande 'dd' est facilement utilisable et produit facilement des images de disque. Par exemple, la commande suivante prend une disquette et en fait une image nommée floppy.img. dd if=/dev/fd0 of=floppy.img bs=1k Windows ne fournit pas d'équivalent. Quelle surprise ! "plus tôt dans la journée..." Jadis, créer un émulateur était souvent une question de nécessité parce que le matériel cible n'avait pas encore été produit. En ce temps, un crash test était vraiment un crash test... ils allumaient la machine et surveillaient si ça ne fumait pas! --[ 1.3 - Coder un compilateur transversal [NDT: le terme anglais est "Cross-Compiler", souvent utilisé en français mais le terme "transversal" traduit mieux l'idée. ] Une fois l'émulateur codé, vous devriez coder un compilateur transversal. Spécifiquement, vous aurez besoin d'un compilateur qui fonctionne sur la plate-forme hôte mais génère du code pour la plate-forme cible. Initialement, vous utiliserez l'émulateur pour faire tourner tout ce qu'il produit. Quand vous serez à l'aise avec votre environnement, vous pourrez lancer le code directement sur la plate-forme cible. "Speaking words of wisdom, write in C..." [NDT: référence aux Beatles ] Etant donne que le C est le langage pour la programmation système, je recommande fortement l'acquisition du code source d'un compilateur comme gcc et de modifier le fond. Le compilateur gcc est même fourni avec la documentation dédiée à ce travail, c'est pourquoi je recommande gcc. Il y a d'autres compilateurs C publiques, comme small-C, qui obéit à une partie de la norme ANSI et sera plus facile à porter. gcc: http://gcc.gnu.org small-C: http://www.ddjembedded.com/languages/smallc Si vous voulez faire autrement, je suppose que vous pourrez trouver un compilateur Pascal ou Fortran et vous dépatouiller avec. Ce ne serait pas la première fois que quelqu'un prenne le chemin le mois fréquenté. A ses débuts, les ingénieurs de Control Data ont inventé leur propre variation du Pascal pour coder le NOSVE (aka NOSEBLEED) OS. NOSVE fut l'une de ces tours de Babel qui n'a jamais été produite. Chez Control Data, vous n'étiez jamais considéré comme un vrai manager tant que vous n'aviez pas au moins une bonne erreur à votre palmarès. Je suppose que NOS/VE a élevé le manager au statut de VP! --[ 1.4 - Coder et porter l'OS Bien, vous avez fait les préliminaires. Il est temps de coder l'OS proprement dit. Les détails de cette méthode sont expliqués dans la deuxième partie. Une fois que vous avez un prototype de votre OS qui fonctionne bien sur l'émulateur, vous êtes confronté au -gros- problème... faire fonctionner votre code réellement sur la machine cible. Je trouve que c'est un obstacle que vous devriez franchir très tôt. Faites un test sur la machine cible dès que vous avez le minimum de composants. Découvrir que votre code ne démarre pas après 50.000 lignes d'efforts est démoralisant. Si vous avez été méthodique dans l'élaboration et les tests de votre émulateur, la plupart de vos problèmes concerneront le code de votre OS et peut être des parties non documentées de vos périphériques. C'est là qu'investir dans la construction d'un émulateur blindé est vraiment rentable. Savoir que l'émulateur fait correctement son travail vous facilitera le diagnostique des problèmes... et vous économisera des heures de sommeil. Enfin, je vous recommande d'utiliser une disquette de démarrage, comme ça, vous ne prenez pas de risques pour vos disques durs. On peut même arriver à mettre le noyau Linux sur une disquette alors pour l'instant, ne vous inquiétez pas pour les contraintes de tailles du code. --[ 1.5 - Téléporter le compilateur transversal. Félicitations. Vous êtes arrivé où seulement quelques-uns uns sont déjà arrivés. Vous avez construit un système d'exploitation. Toutefois, ne serait-ce pas agréable d'avoir quelques outils de développement qui fonctionneraient sur votre OS? Ca peut se faire en téléportant le compilateur transversal. Voici comment la téléportation fonctionne: vous prennez le code source de votre compilateur et vous le donnez à manger à ce même compilateur sur la plateforme hôte. Le compilateur transversal digère ce code source et produit du code binaire qui s'exécute sur votre système cible. Vous obtenez un compilateur qui tourne sur l'Os cible et qui crée des exécutables qui tournent aussi sur l'OS cible. Naturellement, je fais quelques hypothèses. Spécifiquement, je suppose que les librairies que le compilateur transversal utilise sont aussi disponibles sur l'OS cible. Les compilateurs prennent beaucoup de temps pour accomplir les manipulations de chaînes et les entrées/sorties. Si ce travail d'arrière plan n'est pas présent ni supporté sur la plate-forme cible, le nouveau compilateur n'a pas beaucoup d'utilité. --[ 2 - Composants de l'OS Un OS est un étrange programme en ce qu'il doit se charger et fonctionner lui-même en plus de charger et faire fonctionner les autres programmes. Donc, la première chose dont il a besoin, c'est de se charger et de démarrer ses composants pour pouvoir faire son travail. Je vous recommande de trouver la documentation de votre matériel. Si vous ciblez Intel, vous avez de la chance parce que j'explique le processus d'amorçages du x86 dans la troisième partie. Quel que soit l'architecture, je recommande un profil modulaire et orienté objet. Ca ne veut pas dire que vous devez utiliser le C++. Je vous encourage plutôt à séparer différentes portions de l'OS en parties de données et de code. Que vous utilisiez un compilateur ou non pour renforcer cette séparation est à votre jugement. Cette approche est avantageuse puisqu'elle permet de souligner nettement les séparations entre composants. C'est mieux puisque ça permet de modifier/cacher chaque sous partie. Tenenbaum à pris cette idée à l'extrême en permettant à des composants du noyau, comme le système de fichier et la gestion de la mémoire, d'être chargeable à l'exécution. Avec d'autres systèmes d'exploitations, on aurait dû recompiler le noyau pour échanger deux composants centraux comme la gestion de la mémoire. Avec Minix, ces composants peuvent être échanger à l'exécution. Linux a essayé d'implémenter quelque chose de similaire via des modules du noyau chargeables. Parallèlement, vous voudrez apprendre le langage assembleur de votre matériel cible. Il y a des composants de l'OS directement liés au matériel qui ne peuvent être fournis qu'en exécuter quelques douzaines de ligne de cet assembleur spécifique. L'ensemble d'instruction d'Intel est probablement l'un des plus compliqués. C'est dû principalement à des contraintes historiques qui ont conduit Intel à s'efforcer d'une compatibilité ascendante [NDT: le code qui tournait sur une ancienne machine tournera sur une nouvelle]. L'encodage binaire d'Intel est particulièrement embarrassant. Quel composant de l'OS devez vous implémenter en premier? Dans quel ordre doivent-ils être implémenter ? Je vous recommande d'implémenter les différentes zones de fonctionnalités de la manière décrite dans la section suivante. --[ 2.1 - Gestion des tâches Dans son livre sur l'élaboration d'OS, Richard Burgess affirme qu'on doit commencer par la gestion des tâches, et je tendrais à être d'accord avec lui. Le modèle de gestion que vous choisirez influencera tout ce que vous ferez ensuite. D'abord, et surtout, un système d'exploitation gère des tâches. Qu'est-ce qu'une tâche? La documentation du Pentium d'Intel les définis comme une "unité de travail" (V3 p.6-1). Qu'est-ce qu'ils fumaient? C'est comme dire qu'un chapeau est un vêtement. Ca ne donne aucune perspicacité à la vraie nature d'une tâche. Je préfère penser aux tâches comme étant un ensemble d'instructions exécutées par le CPU en fonction de l'état de la machine que cette exécution produit. Inévitablement, la définition exacte d'une tâche est déterminée par le code source du système d'exploitation. Le noyau Linux (2.4.18) représente chaque tâche par une structure task_struct définie dans /usr/src/linux/include/linux/sched.h. L'ensemble des processus sont regroupés de deux façons. D'abord, ils sont indexés dans une table de hachage de pointeurs: extern struct task_struct *pidhash[PIDHASH_SZ]; La structure des tâches est aussi liée par des pointeurs next_task et prev_task pour former une liste doublement chaînée. struct task_struct { : struct task_struct *next_task, *prev_task; : }; Vous devrez décider si votre OS sera multi-tâches ou non, et alors quelle politique sera appliquée pour choisir quand échanger les tâches ( le changement de tâche est aussi appelé changement de contexte). Isoler cette politique de la procédure est important parce que vous pourriez décider de changer de politique plus tard et vous ne voudriez pas avoir à réécrire tout le code. Mécanisme de changement de contexte: ------------------------------------ Sur les plates-formes Intel, le changement de tâche est facilité par un ensemble de structures de données systèmes et une série d'instructions spéciales. Spécifiquement, les processeurs de la classe des pentium d'Intel ont un registre de Tâche (TR) qui est supposé être chargé (via l'instruction LTR) avec un pointeur 16-bits de segment. Ce pointeur de segment indexe un descripteur dans la table globale des descripteurs (GDT). Les informations d'un descripteur incluent l'adresse de base et la taille du segment d'état de la tâche (TSS). Le TSS est un descriptif d'état de la tâche. Il inclut l'état des registres de donnée (EAX, EBX, etc. ) et garde une trace du segment de mémoire utilisé par cette tâche. En d'autres mots, il conserve le 'contexte' d'une tâche. Le registre TR contient toujours le pointeur du segment pour la tâche en cours. Changer de tâche se fait en sauvant l'état du processus courant dans son TSS et puis en chargeant un nouveau pointeur dans TR. Comment ça se décide, en terme d'implémentation, est souvent relégué à l'horloge interne. La plupart des systèmes multi-tâches assignent un quantum de temps à chaque processus. Le temps qu'une tâche reçoit est une question de politique. Un minuteur intégré, comme le 82C54, peut être configuré pour générer des interruptions à des intervalles de temps réguliers? Chaque fois que ces interruptions son générées, le noyau a une opportunité de vérifier et de voir s'il doit faire un changement de tâche. Si oui, un OS base sur Intel peut initialiser un changement de tâche en exécutant un JMP ou un CALL vers le descripteur, dans la GDT, de la tâche cible. Ca implique le changement de valeur de TR. Utiliser l'horloge est ce qu'on appelle le multi-tâches préemptif. Dans ce cas, l'OS décide quelle tâche va s'exécuter en accord avec sa politique. De l'autre cote, on trouve le multi-tâches coopératif, où chaque tâche décide quand elle laisse le CPU à une autre. Pour un traitement exhaustif de la gestion des tâches, voir le manuel du Pentium d'Intel (Volume 3, Chapitre 6). Politique de changement de contexte: ------------------------------------ Décider quel processus prend le contrôle du CPU, et pour combien de temps, est une question de politique. Cette politique est implémentée par le planificateur. Le noyau Linux à un planificateur implémenté par la fonction schedule() dans /usr/src/linux/kernel/sched.c. Il y a un paquet de petits détails dans la fonction schedule() relatif à la prise en compte de différents scénarios où il y a plusieurs processeurs, et aussi une paire de cas spéciaux. Toutefois, les actions de base prises par le planificateur sont relativement directes. Le planificateur regarde dans l'ensemble des tâches qui peuvent être exécutées. Ces tâches exécutables sont liées "par le chaînage de leur structure" [NDT: by the runqueue data structure ]. Le planificateur cherche la tâche dans la file avec la plus grande "bonté" et la planifie pour s'exécuter. La bonté est une valeur calculée par la fonction goodness(). Elle retourne une valeur qui reflète le besoins qu'a la tâche d'être exécutée. ----------------- -1000: ne jamais exécuter 0: réexaminer toutes les tâches, pas seulement celle de la file d'exécution +ve: Au plus, au mieux +1000: processus en temps réel, sélectionner celui-ci. Si la plus grande valeur de bonté dans la file des tâches est de zéro, le planificateur fait un pas en arrière et recherche dans toutes les tâches, pas seulement celles dans la file d'exécution. Pour vous donner une idée d'implémentation, j'ai inclus un petit morceau de la fonction schedule() et quelques-unes de ses lignes les plus importantes. asmlinkage void schedule(void) { struct schedule_data * sched_data; struct task_struct *prev, *next, *p; struct list_head *tmp; int this_cpu, c; : : /* * Voici le code proprement dit: */ repeat_schedule: /* * Processus par défaut à selectionner... */ next = idle_task(this_cpu); c = -1000; list_for_each(tmp, &runqueue_head) { p = list_entry(tmp, struct task_struct, run_list); if (can_schedule(p, this_cpu)) { int weight = goodness(p, this_cpu, prev->active_mm); if (weight > c){ c = weight, next = p; } } } /* A-t-on besoin de recalculer les compteurs? */ if (unlikely(!c)) { struct task_struct *p; spin_unlock_irq(&runqueue_lock); read_lock(&tasklist_lock); for_each_task(p) { p->counter = (p->counter >> 1) + NICE_TO_TICKS(p->nice); } read_unlock(&tasklist_lock); spin_lock_irq(&runqueue_lock); goto repeat_schedule; } : : --[ 2.2 - Gestion de la mémoire Un processus occupe ET alloue de la mémoire. Une fois que vous avez ébauché votre gestionnaire de tâches, vous allez avoir besoin de leurs donner accès à un sous système de gestion de mémoire. Débrouillez-vous pour garder un interface vers le sous-système de mémoire clair, comme ça, vous pourrez le jeter et le remplacer plus tard, si vous le voulez. Au niveau de l'OS, la protection de la mémoire est mise en oeuvre de deux façons: i- segmentation ii- pagination Vous allez devoir décider si oui ou non vous voulez supporter ces deux caractéristiques. La pagination, en particulier, est une tâche très liée au matériel. Ca veut dire que si vous décider de prendre en compte la pagination, porter l'OS sera difficile. D'après Tanenbaum, c'est la raison principale pour laquelle Minix ne supporte pas la pagination. La segmentation peut être reléguée au matériel ou être implémentée manuellement via une technique de sablier [NDT: sand boxing technique] au niveau du noyau. Presque tout le monde préfère une implémentation plus matérielle parce que c'est plus facile. Comme la pagination, la segmentation basée sur le matériel demandera un paquet de code spécifique et une dose vitale de langage assembleur. Le système d'exploitation MMURTL divise sa mémoire virtuelle en trois parties. Il y en a un segment pour l'OS, un pour l'application et le dernier pour le segment de données. Ca ne protège pas exactement une application des autres, mais ça protège l'OS. MMURTL Segment Valeur de l'adresse -------------- ------------------- OS code 0x08 Apps code 0x18 Apps data 0x10 La gestion de la mémoire de MMURTL est en fait installée dans le secteur de boot! C'est correct, j'ai dit le secteur de boot. Si vous regardez dans le code source de bootlok.asm, que Burgess compile avec TASM, vous constaterez que le code de ce registre le rend nécessaire pour passer en mode protégé. Voici quelques morceaux intéressants du fichier: IDTptr DW 7FFh ;LIMIT 256 IDT Slots DD 0000h ;BASE (Linear) GDTptr DW 17FFh ;LIMIT 768 slots DD 0800h ;BASE (Linear) : : LIDT FWORD PTR IDTptr ;Charge le pointeur ITD du processeur LGDT FWORD PTR GDTptr ;Charge le pointeur GDT du processeur : : MOV EAX,CR0 ;Registre de controle OR AL,1 ;Etablis le bits du mode protégé MOV CR0,EAX JMP $+2 ;Libere deux instruction avec un JMP NOP NOP MOV BX, 10h ;Initialise le registre de segment MOV DS,BX MOV ES,BX MOV FS,BX MOV GS,BX MOV SS,BX ;Définition d'un long saut DB 66h DB 67h DB 0EAh DD 10000h DW 8h ; Maintenant en mode protégé Avant de charger le GDTR et l'IDTR, Burgess charge l'OS en mémoire de manière à ce que les adresses de bases dans le sélecteur pointent vraiment sur des entrées valides du descripteur. Ca lui évite aussi de devoir mettre ces structures de données dans le code de démarrage, ce qui est utile vu la limite de taille de 512 octets. La plupart des systèmes d'exploitation utilisent la pagination comme un moyen d'augmenter l'espace adressable qu'ils gèrent. La pagination est compliquée et implique un paquet de code spécifique, et ce code s'exécute fréquemment... ce qui implique une énorme perte de performances. Les E/S sur disques sont sûrement les opérations les plus coûteuses qu'un PC isolé puisse exécuter. Même si cette gestion était reléguée au matériel, la pagination boufferait encore trop de temps. Barry Brey, un expert en puces d'Intel, m'a dit que la pagination sous Windows mange jusqu'à 10% du temps d'exécution. En fait, la pagination est si coûteuse, en terme de temps d'exécution, et la RAM est si bon marché que c'est souvent une meilleure idée d'acheter plus de mémoire et de désactiver la pagination. Au vu de ceci, vous ne trouverez pas la pagination si nécessaire. Si vous êtes en train de développer un OS embarqué, vous n'en aurez pas besoins de toutes façons. Quand les premières mémoires de bases étaient de 16KB, et que ces petites barrettes étaient si chères, la pagination avait sûrement plus de sens. Aujourd'hui, acheter une paire de SDRAM d'un GB n'est pas si inhabituel et je peux dire que, peut-être, la pagination est une relique du passé. --[ 2.3 - Interface d'E/S C'est la partie cauchemardesque. Vous avez maintenant des processus, et ils vivent dans la mémoire. Mais ils ne peuvent pas interagir avec le monde extérieur sans connections avec les périphériques d'E/S. La connexion vers les périphériques d'E/S est traditionnellement implémentée dans les tripes de l'OS. Comme pour les autres composants de l'OS, vous allez devoir user de votre talent en langages assembleur. En mode protégé, utiliser le BIOS pour afficher des données n'est pas qu'optionnel parce que la vielle méthode de lancer des interruptions et d'adresser la mémoire en mode réel n'existe plus. Une manière d'envoyer des messages à l'écran est d'écrire directement dans la mémoire vidéo. La plupart des écrans, même les plats, se lancent en mode texte VGA 80x25 monochrome et/ou couleur. Zone mémoire adresses en mode réel adresses linéaires du buffer ------------- ----------------- ---------------------- monochrome B000[0]:0000 B0000H couleur B800[0]:0000 B8000H Dans les autres cas, l'écran peut afficher 25 lignes et 80 colonnes de caractères[NDT: dans le texte original, il parle de 80 lignes, erreur que j'ai corrigée.]. Chaque caractère prend deux octets dans la mémoire vidéo (ce qui n'est pas si mal... 80x25=2000 x2=4000 octets ). Vous pouvez placer un caractère à l'écran en altérant simplement le contexte de la mémoire vidéo. L'octet de poids faible contient le code ASCII du caractère, et celui de poids fort contient l'attribut. Les bits d'attributs sont organisés comme suit: bit 7 clignotant --------------- bit 6 bit 5 couleur de fond ( 0H=noir ) bit 4 --------------- bit 3 bit 2 couleur d'avant plan ( 0EH=blanc ) bit 1 bit 0 Pour tenir compte de plusieurs écrans, créez simplement des buffers et placez l'écran virtuel dans la mémoire vidéo quand vous voulez le voir. Par exemple, en mode protégé, le code suivant ( écrit par DJGPP ) placera un 'J' à l'écran. #include #include _farpokeb(_dos_ds, 0xB8000, 'J'); _farpokeb(_dos_ds, 0xB8000+1, 0x0F); Quand j'ai vu le morceau de code suivant dans le fichier console.c de Minix, j'ai su que Minix utilise cette technique pour écrire à l'écran. #define MONO_BASE 0xB0000L /* base de la memoire video monochrome */ #define COLOR_BASE 0xB8000L /* base de la memoire video couleur */ : : PUBLIC void scr_init(tp) tty_t *tp; { : : if (color) { vid_base = COLOR_BASE; vid_size = COLOR_SIZE; } else { vid_base = MONO_BASE; vid_size = MONO_SIZE; } : : Gérer les E/S vers les autres périphériques basé sur Intel n'est jamais aussi facile. C'est là qu'arrive notre vieil ami, le gestionnaire d'interruption programmable 8259(PIC) vient en action. [NDT : Programmable Interrupt Controller] Récemment, j'ai lu beaucoup de documentations Intel sur un PIC avancé (comme APIC), mais tout le monde semble s'accrocher vieux controleur d'interruptions. Le 8259 PIC est la liaison matérielle entre le matériel et le processeur. L'installation la plus commune implique deux 8259 PICs configuré dans un arrangement maître-esclave. Chaque PIC possède huit lignes de requêtes d'interruptions ( lignes d'IRQ ) qui reçoivent les données des périphériques externes ( comme le clavier, disque dur, etc. ). Le 8259 maître utilise sa troisième patte pour se lier au 8259 esclave, comme ça, ils fournissent 15 lignes d'IRQ pour les périphériques externes. Le 8259 maître communique avec le CPU via la patte INTR d'interruption du CPU. L'esclave utilise son slot INTR pour parler au maître via sa troisième ligne d'IRQ. Normalement, le BIOS programme le 8259 quand le PC démarre, mais pour parler aux périphériques matériels en mode protégé, le 8259 doit être re-programmé. C'est parce que le 8259 couple les lignes d'IRQ aux signaux d'interruptions. Programmer le 8259 utilise les instructions IN et OUT. Dans le fond, vous enverrez des valeurs 8-bits vers le registre de commandes d'interruptions (ICR) et vers le registre de masquage d'interruptions (IMR) dans un certain ordre. Un mauvais geste et vous plantez tout. Mon exemple favori de programmation du 8259 vient de MMURTL. Le code suivant se trouve dans INITCODE.INC et est invoqué pendant la séquence d'initialisation dans MOS.ASM. ;========================================================================= ; Ceci initialise les vecteurs IRQ00-0F du 8259 ; pour être l'interruption 20, fonction 2F. ; ; Quand les PICU's sont initialisés, toutes les interruptions matérielles sont masquées. ; Chaque driver qui utilise une interruption matérielle est responsable ; du démasquage de sa ligne d'IRQ. ; PICU1 EQU 0020h PICU2 EQU 00A0h Set8259 PROC NEAR MOV AL,00010001b OUT PICU1+0,AL ;ICW1 - MAITRE jmp $+2 jmp $+2 OUT PICU2+0,AL ;ICW1 - EXCLAVE jmp $+2 jmp $+2 MOV AL,20h OUT PICU1+1,AL ;ICW2 - MAITRE jmp $+2 jmp $+2 MOV AL,28h OUT PICU2+1,AL ;ICW2 - EXCLAVE jmp $+2 jmp $+2 MOV AL,00000100b OUT PICU1+1,AL ;ICW3 - MAITRE jmp $+2 jmp $+2 MOV AL,00000010b OUT PICU2+1,AL ;ICW3 - EXCLAVE jmp $+2 jmp $+2 MOV AL,00000001b OUT PICU1+1,AL ;ICW4 - MAITRE jmp $+2 jmp $+2 OUT PICU2+1,AL ;ICW4 - EXCLAVE jmp $+2 jmp $+2 MOV AL,11111010b ;Masquage de toutes sauf l'orloge et le chainage ; MOV AL,01000000b ;Masquage de la disquette OUT PICU1+1,AL ;Masquage - MAITRE (0= Ints ON) jmp $+2 jmp $+2 MOV AL,11111111b ; MOV AL,00000000b OUT PICU2+1,AL ;Masquage - SLAVE jmp $+2 jmp $+2 RETN SET8259 ENDP ;========================================================================= Notez comment Burgess fait deux sauts COURTS après chaque instruction OUT. C'est pour laisser aux PIS le temps d'exécuter la commande. Ecrire un driver peut devenir une expérience déchirante. C'est parce que les drivers ne sont rien de moins que des membres officiels de l'image mémoire du noyau. Quand vous construisez un driver, vous construisez une partie de l'OS. Ca veut dire que si vous n'implantez pas correctement un driver, vous pouvez tuer votre système en le crashant de la pire manière... tombé sous le feu ami. Construire un driver est aussi effrayant avec toutes sortes d'encodage binaire spécifiques aux fabricants et de codage acrobatique. Le meilleur conseil que je puisse vous donner est de rester fidèle aux matériels les plus utilisés et les plus communs. Une fois que vous avez une console qui fonctionne, vous pouvez essayer de communiquer avec un disque dur et peut-être une carte réseau. Vous pourriez aussi considérer de concevoir votre OS pour que les drivers puissent être chargés et déchargés à l'exécution. Devoir recompiler le noyau pour accommoder un simple driver est une paine. Cela vous obligera à créer un mécanisme d'appels indirects de telle manière que l'OS puisse invoquer le driver, même sans savoir à l'avance où il se trouve. Le noyau Linux autorise du code à être chargé à l'exécution via des modules chargeables (LKM's). Ces modules chargeables dynamiquement ne sont rien de moins que des fichiers objets ELF (ils ont été compile mais l'édition de lien n'a pas été faite officiellement). Il y a plein de services qui peuvent être utilisés pour gérer les LKM's. Deux des plus communs sont insmod et rmmod, qui sont utilisés pour insérer et retirer des LKM's à l'exécution. L'utilitaire insmod agit comme un éditeur de lien/chargeur et assimile le LKM dans l'image mémoire du noyau. Insmod le fait en invoquant l'appel système init_module. Il se trouve dans /usr/src/linux/kernel/module.c. asmlinkage long sys_init_module(const char *name_user, struct module *mod_user){ ... Cette fonction, à son tour, invoque une autre fonction appartenant au LKM qui aussi, ça tombe bien, s'appelle init_module(). Voici un morceau intéressant de sys_init_module(): /* Initialise le module. */ atomic_set(&mod->uc.usecount,1); mod->flags |= MOD_INITIALIZING; if (mod->init && (error = mod->init()) != 0) { atomic_set(&mod->uc.usecount,0); mod->flags &= ~MOD_INITIALIZING; if (error > 0) /* "module buggant"*/ error = -EBUSY; goto err0; } atomic_dec(&mod->uc.usecount); [NDT: "buggy module" à ete traduit par "module buggant"] La fonction init_module() du LKM, qui est appelée par le code du noyau ci-dessus, invoque alors une routine du noyau pour enregistrer les sous-routines du LKM. Voici un exemple: /* Initialise le module - Enregistre le périphérique des caractères ["character device"] */ int init_module() { /* Enregistre le peripherique des caracteres (du moins, essaie) */ Major = module_register_chrdev( 0, DEVICE_NAME, &Fops); /* Valeur négative signifie une erreur */ if (Major < 0) { printk ("%s device failed with %d\n", "Sorry, registering the character", Major); return Major; } printk ("%s The major device number is %d.\n", "Registeration is a success.", Major); printk ("If you want to talk to the device driver,\n"); printk ("you'll have to create a device file. \n"); printk ("We suggest you use:\n"); printk ("mknod c %d \n", Major); printk ("You can try different minor numbers %s", "and see what happens.\n"); return 0; } Les OS Unix, dans le but de simplifier les choses, traitent chaque périphérique comme un fichier. C'est pour minimiser le nombre d'appel système et pour offrir une interface uniforme d'un matériel à l'autre. C'est une approche qui vaut la peine d'être considérée. Bien que, d'autre part, l'approche Unix n'a pas toujours été bien vue en terme de facilité d'utilisation. J'ai entendu beaucoup de plaintes à propos du montage et démontage des utilisateurs de Windows qui se mettaient à Unix. Notez, si vous prenez la route des LKM's, vous devrez faire attention de ne pas laissez de faille de sécurité dans vos drivers chargeables. Au vu de ces détails cinglés et audacieux [nuts-and-bolds], pour la plate-forme Intel, je vous recommanderai le livre de Frank Gilluwe. Si vous ne travaillez pas sur Intel, alors, vous devrez faire quelques fouilles. Prenez le combiné et Internet et contactez les fabricants. --[ 2.4 - Système de fichiers Vous avez maintenant des processus, en mémoire, qui peuvent parler au monde extérieur. L'étape finale est de leur donner une façon de persister et d'organiser des données. En général, vous allez construire le gestionnaire du système de fichier au début du driver de disques que vous avez implémenté à la dernière étape. Si votre OS gère un système embarqué, vous ne pourrez pas toujours implémenter de système de fichiers parce que parfois, aucun disque matériel n'existe. Cependant, j'ai déjà vu un système de fichiers implémenté dans la RAM. Même les systèmes embarqués ont besoin de produire et stocker des fichiers log... Il y a plusieurs spécification de systèmes de fichiers documentées disponibles au publique, comme le système de fichier ext2 rendu célèbre par Linux. Voici le lien principal pour l'implémentation du ext2: http://e2fsprogs.sourceforge.net/ext2.html La documentation sur ce site devrait être suffisante pour que vous puissiez commencer. En particulier, il y a une page nommée "Design ans Implementation of Second Extended File System" [NDT: site en anglais...] que j'ai trouvée être une introduction bien menée. Si vous avez le code source du noyau Linux et que vous voulez regarder les structures de base du ext2fs, alors, regardez dans: /usr/src/linux/include/linux/ext2_fs.h /usr/src/linux/include/linux/ext2_fs_i.h Pour voir les fonctions qui manipulent ces structures de données, regardez dans le répertoire suivant: /usr/src/linux/fs/ext2 Dans ce répertoire, vous trouverez quelque chose comme: #include MODULE_AUTHOR("Remy Card and others"); MODULE_DESCRIPTION("Second Extended Filesystem"); MODULE_LICENSE("GPL"); in inode.c, and in super.c you will see: EXPORT_NO_SYMBOLS; module_init(init_ext2_fs) module_exit(exit_ext2_fs) Evidemment, après la dernière discussion, vous devriez voir que le support du ext2fs peut être assuré par un LKM! Quelques créateurs d'OS, comme Burgess, prennent la route du système de fichier MS-DOS FAT, pour des raisons de facilités, et comme ça, ils ne doivent pas reformater leurs disques durs. Je ne recommande pas le système FAT. En général, vous voudrez garder en mémoire que c'est une bonne idée d'implémenter un système de fichier qui permet l'appartenance des fichiers et les contrôles d'accès. Plus sur le sujet dans la section suivante. --[ 2.5 - Notes sur la sécurité La complication est l'ennemi de la sécurité. Les procédures simples sont faciles à vérifier et sûre, les compliquées ne le sont pas. N'importe quel comptable vous dira que nos taxes byzantines [Byzantine tax laws] laissent de la place pour les abus. Les logiciels, c'est pareil. Un code source compliqué renferme toutes sortes de bugs insidieux potentiels. Comme les OS ont évolués, ils sont devenus plus compliqués. D'après un témoignage d'un cadre de chez Microsoft du 2 février 1999, Windows 98 contient 18 millions de lignes de code. Pensez-vous qu'il y ait un bug quelque part? Oh... non... Microsoft ne vendrait pas du code erroné... La sécurité n'est pas quelque chose que vous pourrez ajouter à votre OS quand vous en aurez presque finis avec lui. La sécurité devrait être une part inhérente aux opérations de bases du système. Gardez ça en tête pendant toutes les phases de constructions, de la gestion de tâches au système de fichier. De plus, vous voudrez peut être qu'un organisme répute une vérification sur les mécanismes de sécurité avant de proclamer votre OS comme 'sécurise'. Par exemple, la NSA évalue le degré de confiance de votre système d'exploitation sur une échelle de C2 à A1. Un OS de confiance est juste un OS qui des mécanismes de sécurité en place. La principale caractéristique de sécurité d'un système est le rang que la NSA lui a attribue. Un système de classe C2 n'a que des contrôles limites d'accès et d'authentifications. Un système de classe A1, de l'autre cote du spectre, a des mécanismes rigoureux et obligatoires de sécurité. Les gens qui imaginent des ennemis sont appelles des paranoïas, ceux qui ont des ennemis imaginatifs sont appelles victimes. C'est souvent dur de faire la différence avant qu'il ne soit trop tard. Si j'avais à confier mon entreprise à un OS, j'en prendrais un qui serait plus du cote de la paranoïa. --[ 3 - Etude d'un cas simple Dans cette section, je vous présente quelques codes fait-maison dans le but d'éclairer les points que j'ai exposés en partie 1. --[ 3.1 - Plate-forme hôte Pour nombre de raisons, j'ai décide de prendre un raccourci et de créer mon OS sur une architecture Intel 8x86. Le coût est un point frappant et c'est pareil pour le choix des systèmes hôtes potentiels (Linux, openBSD, MMURTL, Windows, etc. ). Cependant, le bénéfice principal est que je peux éviter (dans une certaine mesure) de devoir coder un compilateur transversal et un émulateur. En ayant la plate-forme hôte et le système cible tournant sur le même matériel, j'ai pu prendre avantage des outils existant qui génèrent du code binaire x86 et émulent le matériel x86. Pour avoir recours au plus petit dénominateur commun, j'ai décidé d'utiliser Windows comme plate-forme hôte. Windows, mis à part ses bugs, apparaît avoir le plus d'utilisateurs. Presque tout le monde est donc capable de suivre les points et idées que je présente dans la partie 3. Un bénéfice du choix de Windows c'est qu'il est fourni avec sont propre émulateur. La machine virtuelle DOS un émulateur brut. J'ai dit 'brute' parce qu'il ne fournit pas toutes les fonctionnalités que boch fournis. En fait, j'ai testé un paquet de code dans la VM DOS [Virtual Machine]. --[ 3.2 - Points de compilations Il existe des douzaines de compilateurs C qui fonctionnent sous Windows. J'ai fini par avoir trois critères pour en choisir un: i- génère du code binaire brute ( par exemple les fichiers .COM MS ) ii- permet des instructions en lignes spéciales (comme INT, LGDT ) iii- est libre Les PC Intel démarrent en mode réel, ce qui veut dire que je dois commencer la partie avec un compilateur 16-bits. En plus, Le code système doit être du code binaire brute de façon à ce que les adresses à l'exécution ne doivent pas être implémentées manuellement. Ce n'est pas obligatoire mais ça rend la vie plus facile. Le seul compilateur commercial 16-bits qui génère du code binaire brute est passé de mode l'année dernière... j'ai donc du faire des recherches. Après avoir fouillé le net pour trouver un compilateur, j'ai fini par élaborer ce tableau: compilateur décision raison -------- -------- ------ TurboC NO Assembleur en ligne nécessite NASM (€€€) [NDT: $ se traduit par €] Micro-C YES Génère une sortie MASM libre PacificC NO ne supporte pas les petits MM (comme .COM) Borland 4.5C++ NO le coût €€€ VisualC++ 1.52 NO le coût €€€ Watcom NO ne supporte pas les petits MM (comme .COM) DJGPP NO assembleur avec syntaxe AT&T ( zute, flutte et crotte de bique ) [NDT: yuck] J'ai fini par travailler avec Micro-C, bien qu'il ne supporte pas la totalité du standard ANSI. La sortie de Micro-C est en assembleur et peut être routée vers MASM sans trop de problèmes. Micro-C à été créé par Dave Dunfield et peut être trouvé à: ftp://ftp.dunfield.com/mc321pc.zip Ne vous en faites pas pour les dépendances du MAMS. Vous pouvez trouver MASM 6.1 gratuitement comme composant de Windows. Allez voir le lien suivant pour plus de détails: http://www.microsoft.com/ddk/download/98/BINS_DDK.EXE http://download.microsoft.com/download/vc15/Update/1/WIN98/EN-US/Lnk563.exe Le seul mauvais côté d'obtenir cette version libre de MASM (comme les fichiers ML.EXE, ML.err et LINK.EXE ) est qu'il n'y a pas de documentation. Ha Ha, l'Internet vient nous sauver... http://webster.cs.ucr.edu/Page_TechDocs/MASMDoc En utilisant Micro-C, je suis le conseil que j'ai donné en partie 1 et reste fidèle aux outils avec lesquels je suis à l'aise. Je me suis amélioré en utilisant MASM et TASM. J'ai facile à les utiliser en ligne de commande et à lire leurs fichiers de sortie. J'ai préféré MASM à TASM parce qu'il est gratuit, même s'il y a encore quelques bugs. Le problème avec la plupart des compilateurs C pour générer le code Os est qu'ils ajoutent des informations formatées aux fichiers exécutables qu'ils produisent. Par exemple, la version actuelle du Visual C++ crée des exécutables en mode texte qui suivent le format PE (Portable Exécutable). Ce formatage ajouté est utilisé par le programme de lancement par l'OS à l'exécution. Les compilateurs ajoutent aussi du code des librairies à leurs exécutables, même s'ils n'en ont pas besoin. Considérons le fichier texte nommé file.c contenant le code suivant: void main(){} Je vais le compiler en tant que fichier .COM en utilisant TurboC. Regardez la taille du fichier objet et celui exécutable. C:\DOCS\OS\lab\testTCC>tcc -mt -lt -ln file.c C:\DOCS\OS\lab\testTCC>dir . 03-29-02 9:26p . .. 03-29-02 9:26p .. FILE C 19 03-30-02 12:07a file.c FILE OBJ 184 03-30-02 12:09a FILE.OBJ FILE COM 1,742 03-30-02 12:09a file.com Mon Dieu,... il y a un paquet de lest que le compilateur a ajouté. C'est exactement le travail des compilateurs et des linkers. Ces connards! Pour voir à quel point c'est excessif, regardons un fichier .COM codé en assembleur. Par exemple, créons un fichier file.asm qui ressemble à: CSEG SEGMENT start: ADD ax,ax ADD ax,cx CSEG ENDS end start On peut l'assembler avec MASM C:\DOCS\OS\lab\testTCC>ml /AT file.asm C:\DOCS\OS\lab\testTCC>dir . 03-29-02 9:26p . .. 03-29-02 9:26p .. FILE OBJ 53 03-30-02 12:27a file.obj FILE ASM 67 03-30-02 12:27a file.asm FILE COM 4 03-30-02 12:27a file.com 5 file(s) 187 bytes 2 dir(s) 7,463.23 MB free Comme vous pouvez le voir, l'exécutable ne fait que 4 octets! L'assembleur n'a rien ajouté, à l'inverse du compilateur C, qui ajoute tout et n'importe quoi dans le code. L'espace supplémentaire est sûrement pris par des librairies que le linker a ajouté. Le point douloureux, à moins que vous ayez codé votre propre implémentation du compilateur C, est que vous allez vous trouver face à du code et des données supplémentaires dans les binaires de l'OS. Une solution est d'ignorer simplement ces octets supplémentaires. C'est à dire que le chargeur de programme de l'OS passera ces données formatées et ira dans le code que vous avez écrit. Si vous décidez de prendre ce chemin, vous voudrez regarder dans le code hexa. de votre binaire pour déterminer l'offset du fichier auquel votre code commence. J'ai échappé à ce problème parce que les compilateurs C Micro-C (MCC) fournissent un fichier assembleur à la place d'un fichier objet. Ca m'a permis d'ajuster et de supprimer le code supplémentaire avant qu'il ne soit écrit dans l'exécutable. Cependant, j'avais encore des problèmes... Par exemple, les compilateurs MCC ajouteront toujours des segments supplémentaires et placerons les données dans ceux-ci. Les variables traduites en assembleur sont toujours préfixées dans ce segment non désiré (i.e. OFFSET DGRP:_var ). Prenons le programme: char arr[]={'d','e','v','m','a','n','\0'}; void main(){} MCC traitera ce fichier et sortira: DGRP GROUP DSEG,BSEG DSEG SEGMENT BYTE PUBLIC 'IDATA' DSEG ENDS BSEG SEGMENT BYTE PUBLIC 'UDATA' BSEG ENDS CSEG SEGMENT BYTE PUBLIC 'CODE' ASSUME CS:CSEG, DS:DGRP, SS:DGRP EXTRN ?eq:NEAR,?ne:NEAR,?lt:NEAR,?le:NEAR,?gt:NEAR EXTRN ?ge:NEAR,?ult:NEAR,?ule:NEAR,?ugt:NEAR,?uge:NEAR EXTRN ?not:NEAR,?switch:NEAR,?temp:WORD CSEG ENDS DSEG SEGMENT PUBLIC _arr _arr DB 100,101,118,109,97,110,0 DSEG ENDS CSEG SEGMENT PUBLIC _main _main: PUSH BP MOV BP,SP POP BP RET CSEG ENDS END Plutôt que de retravailler le coeur du compilateur, j'ai implémenté une solution plus immédiate en créant un pré-processeur à la va-vitte. L'alternative aurait été d'ajuster manuellement chaque fichier assembleur que MCC à produit, et c'était trop de travail. Le programme suivant ( convert.c ) crée un squelette de fichier .COM de la forme: .486 CSEG SEGMENT BYTE USE16 PUBLIC 'CODE' ORG 100H ; for DOS PSP only, strip and start OS on 0x0000 offset here: JMP _main ; --> Ajouter le code ici <---- EXTRN ?eq:NEAR,?ne:NEAR,?lt:NEAR,?le:NEAR,?gt:NEAR EXTRN ?ge:NEAR,?ult:NEAR,?ule:NEAR,?ugt:NEAR,?uge:NEAR EXTRN ?not:NEAR,?switch:NEAR,?temp:WORD CSEG ENDS END here Il prend ensuite les procédures et données du fichier assembleur original et les place dans le corps du squelette. Voici quelque chose d'ennuyeux, mais un programme efficace qui fait ce boulot: /* convert.c------------------------------------------------------------*/ #include #include /* Lit une ligne de fptr, la place dans buff */ int getNextLine(FILE *fptr,char *buff) { int i=0; int ch; ch = fgetc(fptr); if(ch==EOF){ buff[0]='\0'; return(0); } while((ch=='\n')||(ch=='\r')||(ch=='\t')||(ch==' ')) { ch = fgetc(fptr); if(ch==EOF){ buff[0]='\0'; return(0); } } while((ch!='\n')&&(ch!='\r')) { if(ch!=EOF){ buff[i]=(char)ch; i++; } else { buff[i]='\0'; return(0); } ch = fgetc(fptr); } buff[i]='\r';i++; buff[i]='\n';i++; buff[i]='\0'; return(1); }/*end getNextLine*/ /* changes DGRP:_variable en CSEG:_variable */ void swipeDGRP(char *buff) { int i; i=0; while(buff[i]!='\0') { if((buff[i]=='D')&& (buff[i+1]=='G')&& (buff[i+2]=='R')&& (buff[i+3]=='P')) { buff[i]='C';buff[i+1]='S';buff[i+2]='E';buff[i+3]='G'; } if((buff[i]=='B')&& (buff[i+1]=='G')&& (buff[i+2]=='R')&& (buff[i+3]=='P')) { buff[i]='C';buff[i+1]='S';buff[i+2]='E';buff[i+3]='G'; } i++; } return; }/*end swipeDGRP*/ void main(int argc, char *argv[]) { FILE *fin; FILE *fout; /*MASM permet des lignes longues de 512 caractères, donc voici une borne supp.*/ char buffer[512]; char write=0; fin = fopen(argv[1],"rb"); printf("Opening %s\n",argv[1]); fout = fopen("os.asm","wb"); fprintf(fout,".486P ; enable 80486 instructions\r\n"); fprintf(fout,"CSEG SEGMENT BYTE USE16 PUBLIC \'CODE\'\r\n"); fprintf(fout,";\'USE16\' forces 16-bit offset addresses\r\n"); fprintf(fout,"ASSUME CS:CSEG, DS:CSEG, SS:CSEG\r\n"); fprintf(fout,"ORG 100H\r\n"); fprintf(fout,"here:\r\n"); fprintf(fout,"JMP _main\r\n\r\n"); fprintf(fout,"EXTRN ?eq:NEAR,?ne:NEAR,?lt:NEAR,?le:NEAR,?gt:NEAR\r\n"); fprintf(fout,"EXTRN ?ge:NEAR,?ult:NEAR,?ule:NEAR,?ugt:NEAR,?uge:NEAR\r\n"); fprintf(fout,"EXTRN ?not:NEAR,?switch:NEAR,?temp:WORD\r\n\r\n"); while(getNextLine(fin,buffer)) { if((buffer[0]=='P')&& (buffer[1]=='U')&& (buffer[2]=='B')&& (buffer[3]=='L')&& (buffer[4]=='I')&& (buffer[5]=='C')){ fprintf(fout,"\r\n"); write=1;} if((buffer[0]=='D')&& (buffer[1]=='S')&& (buffer[2]=='E')&& (buffer[3]=='G')){ write=0;} if((buffer[0]=='B')&& (buffer[1]=='S')&& (buffer[2]=='E')&& (buffer[3]=='G')){ write=0;} if((buffer[0]=='R')&& (buffer[1]=='E')&& (buffer[2]=='T')){ fprintf(fout,"%s",buffer); write=0;} if(write) { swipeDGRP(buffer); fprintf(fout,"%s",buffer); } buffer[0]='\0'; } fprintf(fout,"CSEG ENDS\r\n"); fprintf(fout,"END here\r\n"); fclose(fin); fclose(fout); return; }/*end main-------------------------------------------------------------*/ --[ 3.3 - Démarrage Dans la discussion qui suit, je parlerai du démarrage à partir d'une diskette. Démarrer à partir d'un disque dur, d'un CD-ROM ou d'un autre système de stockage est typiquement plus compliqué à cause partitionnement et du formatage. OK, la première chose que je vais faire c'est de coder un programme de démarrage. Il doit être petit. En fait, il doit être plus petit que 512 octets parce qu'il doit tenir sur le premier secteur logique de la diskette. La plupart des diskettes 1.44 ont 80 pistes par face et 18 secteurs par piste. Le BIOS nomme les deux faces ( 0,1 ), les pistes de 0-79 et les secteurs 0-18. Quand une machine Intel démarre, le BIOS (qui se trouve dans la ROM sur la carte mère) regarde après un disque de démarrage. L'ordre dans lequel il procède peut être configuré via un système de menu du BIOS au démarrage. S'il trouve une diskette de démarrage, il chargera le secteur de démarrage (piste 0, face 0, secteur 1) dans la mémoire et l"'exécutera. Parfois, ce code ne fera rien de plus qu'imprimer un message à l'écran: Not a boot disk, you are hosed. Toutes les machines 8x86 démarrent en mode réel, et le secteur de démarrage est situé à l'adresse mémoire 0000[0]:7C00 ( ou 0x07C00 ) en hexadécimal. Une fois que c'est fait, le BIOS se lave les mains en vous laissant tout seul avec votre propre périphérique. Beaucoup de systèmes d'exploitation demandent au secteur de démarrage de charger un programme de démarrage plus grand qui, à son tour, charge l'OS proprement dit. C'est ce qu'on appelle le multi-démarrage. Les grands systèmes d'exploitation, qui utilisent un système de fichier compliqué, et une configuration flexible, utilisent un programme de démarrage multi-étapes. Un exemple classique est le GRand Unified Bootloader ( GRUB ) de GNU. http://www.gnu.org/software/grub Comme d'habitude, je prendrai le chemin le plus court. Le secteur de démarrage charge directement le code système. Ce code de démarrage suppose que le code système se situe directement après le secteur de démarrage (piste 0, face 0, secteur 2). Ca m'évite de devoir inclure des données et des instructions spéciales pour lire le système de fichier. Enfin, vu les contraintes de tailles, tout le code de cette section est écrit en assembleur. Le code de démarrage suit: ;-boot.asm---------------------------------------------------------------- .8086 CSEG SEGMENT start: ; Etape 1) Charge l'OS de la diskette ; vers la zone après la ; tables d'interruption existante (0-3FF) ; et la zone des données du BIOS (400-7FF) MOV AH,02H ; Commande de lecture MOV AL,10H ; 16 secteurs = 8KB de données à charger MOV CH,0H ; les 8 bits de poids faible du numéro de piste MOV CL,2H ; début du secteur ( contigu au secteur de démarrage ) MOV DH,0H ; face MOV DL,0H ; lecteur MOV BX,CS MOV ES,BX ; le segment où charger les données MOV BX,0H MOV BX,800H ; Offset pour charger le code ( après IVT ) INT 13H ; signa que le code à été chargé et on va faire un jump MOV AH,0EH MOV AL,'-' INT 10H MOV AH,0EH MOV AL,'J' INT 10H MOV AH,0EH MOV AL,'M' INT 10H MOV AH,0EH MOV AL,'P' INT 10H MOV AH,0EH MOV AL,'-' INT 10H ; Etape 2) Saut vers l'OS ; bonzai!!! JMP BX CSEG ENDS END start ;-end file---------------------------------------------------------------- Ce charger suppose aussi que le code système se trouve sur les secteurs 2-17 de la première piste. Quand l'OS devient plus grand ( passé 8K ), des instructions supplémentaires sont nécessaires pour charger du code additionnel. Mais pour l'instant, supposons que le code sera plus petit que 8K en taille. OK, vous devrez construire le code précédant dans un fichier .COM et le caser dans le secteur de démarrage. Le fichier boot.asm est assemblé via: C:\> ML /AT boot.asm Comment on le case dans le secteur de démarrage de la diskette? Ah ha! Debug à la rescousse. Notez, pour des plus grands travaux, je recommande plus de rigueur. C'est un petit travail et Debug suffit. Sans parler que j'ai un peut de nostalgie pour Debug. J'ai assemblé mon premier programme avec lui; dans les années 1980, quand c'était comme du parachute. En supposant que le code a été assemblé dans un fichier nommé boot.com, voici comment on l'écrit dans le secteur de démarrage de la diskette. C:\DOCS\OS\lab\bsector>debug showmsg.com -l -w cs:0100 0 0 1 -q C:\DOCS\OS\lab\bsector> La commande 'l' charge le fichier en mémoire à l'adresse CS:0100 en hexa. La commande 'w' écrit cette mémoire vers de disque A ( 0 ) commençant au secteur 0 et écrit un seul secteur. La commande 'w' à la forme générale suivante: w adresse lecteur debut-secteur #-secteurs Notez que le DOS voit les secteurs logiques (commençant par 0) alors que les secteurs physiques (que le BIOS manipule) commencent toujours par 1. Si vous voulez tester toute cette procédure, assemblez le programme suivant en fichier .COM et placez-le dans le secteur de démarrage d'une disquette avec debug. .486 CSEG SEGMENT start: MOV AH,0EH MOV AL,'-' INT 10H MOV AH,0EH MOV AL,'h' INT 10H MOV AH,0EH MOV AL,'i' INT 10H MOV AH,0EH MOV AL,'-' INT 10H lp LABEL NEAR JMP lp CSEG ENDS END start Ceci imprimera '-hi-' à la console et bouclera ensuite. C'est une manière amusante de casser la glace et de s'habituer. Surtout si vous n'avez jamais construit de disque de démarrage. --[ 3.4 - Initialiser l'OS Le secteur de démarrage charge le code exécutable système en mémoire et affecte CS et IP du premier (plus bas) octet des instructions du code. Mon code système ne fait rien de plus que d'imprimer quelques messages à l'écran en mode protégé. L'exécution prend fin dans une boucle infinie. J'ai écrit le programme en utilisant des instructions en mode réel. Les machines Intel démarrent toutes en mode réel. C'est à la responsabilité du code initial de placer le PC en mode mémoire protégée. Une fois en mode protégé, l'OS ajustera son registre de segment, installera une pile et établira un environnement d'exécution pour les applications (table des processus, drivers, etc.). Ca rend la vie plus difficile car je ne peux aller plus loin en utilisant les registres et instructions en mode réel. Finalement, je devrais utiliser les registres étendus (i.e. EAX ) pour accéder à plus de mémoire. Quelques compilateurs n'accepteront pas des instructions 16-bits ni 32-bits ou ils [ get persnickety ] et encoderont incorrectement ces instructions. Si vous regardez au long JMP que j'ai fait à la fin de setUpMemory(), vous verrez que j'ai du l'encoder manuellement. Ma situation était mince parce que j'ai du tout mettre dans un seul segment. Une fois en mode protége, ce n'était plus le cas et j'ai pu enfin faire quelque chose d'intéressant. Une solution serait de mettre mon code système 16-bits dans la deuxième phase de démarrage. En d'autres mots, demander au code système, chargé par le secteur de démarrage, de charger le code 32-bits en mémoire avant de faire la transition en mode protégé. Quand le long JMP est fait, je peux exécuter le code 32-bits... qui peut alors s'exécuter de là-bas. Si vous regardez dans MMURTL, vous verrez que c'est exactement ce que Burgess a fait. Doh, Si je l'avais su plus tôt. J'étais intéressé dans la pensée de manier [leverage] le compilateur Micro-C. Bien que, comme vous le verrez, le plus part du travail d'installation s'est fait en assembleur en ligne. Seulement quelques petites portions ancrées directement dans le matériel, et le mieux que vous pourriez espérer c'est d'enfuir le code assembleur dans les entrailles de l'OS et d'envelopper le reste en C. Voici le code système (os.c), dans toute sa gloire: /* os.c ----------------------------------------------------------------*/ void printBiosCh(ch) char ch; { /* ch = BP + savedBP + retaddress = BP + 4 bytes */ asm "MOV AH,0EH"; asm "MOV AL,+4[BP]"; asm "INT 10H"; return; }/*end printBiosCh---------------------------------------*/ void printBiosStr(cptr,n) char* cptr; int n; { int i; for(i=0;i osPre.asm Notez, mcp est le pré-processeur de Micro-C. Transforme tout en segment 16 bits: convert osPre.asm Une fois que j'ai un fichier .ASM, je l'assemble: ML /Fllist.txt /AT /Zm -c osPre.asm Notez comment je dois utiliser l'option /Zm pour pouvoir assembler le code en obéissant aux conventions plus vieilles de MASM. C'est typiquement l'étape où les problèmes arrivent. Pas besoin d'en parler, je suis fatigué de devoir retirer le début des segments plutôt vite et c'est pour ça que j'ai écrit convert.c. Finalement, après quelques larmes, j'ai édité les liens des fichiers objets de l'OS en un fichier objet de Micro-c. LINK os.obj PC86RL_T.OBJ /TINY Si vous regardez dans convert.c, vous verrez un paquet de chargement de directives EXTRN. Tous ces symboles importés sont des librairies de math, situées dans le fichier PC86RL_T.OBJ. Si vous avez une copie de NASM sur votre machine, vous pouvez vérifier votre travail avec la commande suivante: ndisasmw -b 16 os.com Ça donnera une version désassemblée du code à l'écran. Si vous voulez une version plus permanente, utilisez les options de listing dans des fichiers quand vous invoquez ML.EXE: ML /AT /Zm /Fl -c os.asm Une fois que le code de l'OS et du secteur de d"démarrage est fini, vous devriez les mettre sur disquette. Vous pouvez le faire avec l'utilitaire debug du DOS. C:\DOCS\OS\lab\final>debug boot.com -l -w cs:0100 0 0 1 -q C:\DOCS\OS\lab\final>debug os.com -l -w cs:0100 0 1 2 -q Après ça, vous n'avez qu'à démarrer avec la disquette et accrochez-vous! J'espère que cet article vous a donné quelques idées à expérimenter. Bonne chance et amusez-vous bien. "Contrasting this modest effort [of Seymour Cray in his laboratory to build the CDC 6600] with 34 people including the janitor with our vast development activities, I fail to understand why we have lost our industry leadership position by letting someone else offer the world's most powerful computer." -Thomas J. Watson, IBM President, 1965 "It seems Mr. Watson has answered his own question." -Seymour Cray --[ 4 - References and Credits [1] Operating Systems: Design And Implementation, Andrew S. Tanenbaum, Prentice Hall, ISBN: 0136386776 Ce livre explique comment le système d'exploitation Minix fonctionne. Linux était initialement un essais de Linus de créer un version commerciale de qualité de Minix. Minix est un Os d'Intel. [2] MMURTL V1.0, Richard A. Burgess, Sensory Publishing, ISBN: 1588530000 MMURTL est un autre OS d'Intel. Contrairement à Tenenbaum, Burgess plonge plus profondément dans les sujets sophistiqués, comme la pagination. Une autre chose que j'admire chez Burgess c'est qu'il répondra à vos e-mails sans être pédant [snooty] comme Tenenbaum. Si Minix a donné naissance à Linux, alors MMURTL sera réincarné comme prochain grand projet. [3] Dissecting DOS, Michael Podanoffsky, Addison-Wesley Pub, ISBN: 020162687X Dans ce livre, Podanoffsky décrit un clone du DOS nommé RxDOS. RxDos est présenté comme un OS en mode réel et entièrement écrit en assembleur. [4] FreeDOS Kernel, Pat Villani, CMP Books, ISBN: 0879304367 Un autre clone du DOS ... mais, cette fois, écrit en C, whew! [5] Virtual Machine Design and Implementation In C/C++, Bill Blunden, Wordware Publishing, ISBN: 1556229038 Oui, c'est le moment de l'auto pub. Ecrire une VM [Virtual Machine, machine virtuelle] est juste un petit saut après l'écriture d'un émulateur. Mon livre présente toutes les informations de cet article et un paquet d'autres. Ca inclut une machine virtuelle complète, assembleur et débogueur. [6] Linux Core Kernel Commentary, 2nd Edition, Scott Andrew Maxwell, The Coriolis Group; ISBN: 1588801497 C'est une autre petite promenade à travers le code source de la gestion des tâches et de la mémoire de Linux. [7] The Design and Implementation of the 4.4BSD Operating System, Marshall Kirk McKusick (Editor), Keith Bostic, Michael J. Karels (Editor) Addison-Wesley Pub Co; ISBN: 0201549794 Ces gars-là sont geeks profonds. Si vous ne me croyez pas, regardez la photo de groupe à l'intérieur. Ce livre est une description compréhensive de l'OS FreeBSD. [8] The Undocumented PC : A Programmer's Guide, Frank Van Gilluwe, Addison-Wesley Pub, ISBN: 0201479508 Si vous faites de l'E/S sur Intel, ça vous aidera beaucoup d'avoir ce livre. [9] Control Data Corporation Il y a un nombre de vieux embrumés de Control Data que je voudrais remercier pour m'avoir donné leur aide et leurs conseils. Control Data a été tué par ses gestionnaires, mais il y avait une poignée d'ingénieurs doués, comme Cray, qui étaient sûr que quelques-unes de leurs bonnes idées trouveraient une maison. [10] IBM and the Holocaust: The Strategic Alliance Between Nazi Germany and America's Most Powerful Corporation, Edwin Black, Three Rivers Press; ISBN: 0609808990 Initialement, j'ai entendu parlé de ça à la radio Dave Emory. Mae Brussell serait d'accord avec moi... le profit à tout prix n'est pas une bonne chose. Je voudrais remercier George Matkovitz, qui a écrit le premier noyau basé sur des messages au monde et Mike Adler, un concepteur de compilateurs qui étaient là quand Cray cassait IBM en partageant leurs expériences avec moi. [Traduit de l'anglais par Tbowan pour arsouyes.org]