in ,

0x01 – Le premier boot

Cet article fait parti de la série Créer un système d'exploitation pour Raspberry Pi ( 2 / 5 )

boot.S – Le point d’entrée du noyau

boot.S va être la première portion de code que le matériel exécute dans notre noyau. Cela doit être écrit en assembleur. Lorsque le CPU charge le noyau, il ne configure pas encore d’environnement d’exécution du langage C. Il ne sait même pas à quoi ressemble l’environnement d’exécution C! Ce code configure et met en place cela afin que nous puissions passer en langage C le plus rapidement possible. Voici le code:

Explication du code

Ce sont des notes pour le linker. La première concerne l’endroit où ce code appartient au binaire compilé. La seconde spécifie que _start  est le nom qui doit être visible depuis l’extérieur du binaire.

Ce sont les premières instructions de notre noyau. Ces lignes arrêteront trois des quatre cœur du CPU. Ecrire un système d’exploitation est difficile, écrire un système d’exploitation multicœur est encore plus difficile.

Cette instruction indique que notre pile (stack) C commencera à l’adresse 0x8000 et croît vers le bas. Pourquoi 0x8000? Parce que quand le matériel charge notre noyau dans la mémoire, il ne le charge pas à l’adresse 0, mais à l’adresse 0x8000. Comme ça notre noyau utilise des adresses à partir de 0x8000 et plus, et notre pile peut fonctionner en toute sécurité à partir de 0x8000 vers le bas sans écraser notre noyau.

Ces instructions chargent les adresses de début et de fin de la section BSS dans des registres. les variables globales non initialisées se retrouve dans BSS lors de la compilation. L’environnement d’exécution C nécessite que les variables globales non initialisées soient égales à zéro, nous devons donc nous-mêmes mettre cette section à zéro. Les symboles  __bss_start  et __bss_end  vont être définis plus tard lorsque nous travaillerons avec le linker, donc ne vous inquiétez pas d’où ils viennent pour l’instant.

Ce code est ce qui met à zéro la section BSS. D’abord, il charge 0 dans les quatre registres r5, r6, r7, r8 consécutifs. Ensuite, il vérifie si l’adresse stockée dans r4 est inférieure à celle de r9. Si c’est le cas, alors il exécute stmia r4!, {r5-r8}. L’instruction stm stocke le deuxième opérande dans l’adresse contenue dans le premier. Le suffixe  ia sur l’instruction signifie incrémenté après, ou incrémenter l’adresse dans r4 à l’adresse après la dernière adresse écrite par l’instruction. Le ! signifie stocker cette adresse dans r4, au lieu de la supprimer. {r5-r8}  signifie que stm devrait stocker les valeurs dans les registres consécutifs r5, r6, r7, r8 (donc 16 octets) dans r4. Donc l’instruction stocke 16 octets de zéros dans l’adresse de r4, puis incrémente cette adresse de 16 octets. Et repète cela jusqu’à ce que r4 soit supérieur ou égal à r9, et toute la section BSS est mise à zéro.

Ceci charge l’adresse de la fonction C appelée kernel_main  dans un registre et saute à cet endroit. Lorsque la fonction C a fini de s’exécuter et retourne une valeur, elle entre dans la procédure halt  où elle effectue une boucle infini pour ne rien faire.

kernel.c – Le code C

La plus grande partie de ce code concerne la configuration du matériel pour les E/S (Entrées-sorties) de base.

Dans un système à base d’un processeur, d’un microprocesseur, d’un microcontrôleur ou d’un automate, on appelle entrées-sorties les échanges d’informations entre le processeur et les périphériques qui lui sont associés. De la sorte, le système peut réagir à des modifications de son environnement, voire le contrôler. Elles sont parfois désignées par l’acronyme I/O, issu de l’anglais Input/Output ou encore E/S pour Entrées/Sorties.

L’E/S est effectuée via le matériel UART (Universal asynchronous receiver-transmitter), ce qui nous permet d’envoyer et de recevoir des données de texte via les ports série. La seule façon de profiter de cela sur le matériel réel est d’obtenir un câble série USB. Comme nous n’avons pas un de ces câbles, nous allons interagir avec le noyau via la VM jusqu’à ce que nous ayons des E/S plus sophistiquées comme la sortie HDMI ou le clavier USB.

Mis à part la configuration matérielle, il y a quelques fonctions auxiliaires pour faire abstraction du matériel et, bien sûr, de la fonction principale.

Voici le code:

Explication du code

mmio_write et mmio_read  prennent en entrée un registre, qui est une adresse absolue qui va ressembler à  0x20000000 + peripheral base + register offset .  mmio_write  prend une chaine de caractère de 4 octets pour écrire dans le registre, tandis que mmio_readrenvoie n’importe quel chaine de 4 octets dans le registre. delay  est juste une boucle pour occuper le kernel pendant un moment. C’est une manière très imprécise de donner au matériel le temps de répondre aux écritures que nous aurions pu faire.

L’enum définit le décalage périphérique du GPIO (General Purpose Input/Output) et des systèmes matériels UART, ainsi que certains de leurs registres. Ne vous inquiétez pas de savoir la fonction de chaque registre, car nous allons les expliquer tels qu’ils sont utilisés.

Si vous n’êtes pas familier avec Memory Mapped IO sur le Raspberry Pi, nous vous recommandons de lire Memory Mapped IO, périphériques et registres avant de continuer.

Configuration du matériel

Cette fonction  configure le matériel UART à utiliser. Il consiste simplement à définir les flags de configuration dans divers registres.

Cette ligne désactive tous les aspects du matériel UART. UART0_CR est le registre de contrôle de l’UART.

Ces lignes désactivent les broches 14 et 15 du GPIO. Ecrire 0 à GPPUD marque que les pins doivent être désactivés. L’écriture de (1 << 14) | (1 << 15)  dans GPPUDCLK0 marque les broches qui doivent être désactivées et l’écriture de 0 dans GPPUDCLK0  rend l’ensemble effectif. Puisque nous n’utilisons pas les broches GPIO, cette partie n’est pas vraiment importante

Cette ligne définit tous les flags dans Interrupt Clear Register. Cela a pour effet de supprimer toutes les interruptions en attente du matériel UART.

Ceci définit le débit de la connexion. C’est essentiellement le nombre de bit par seconde qui peut traverser le port série. Ce code essaie d’obtenir un débit de 115200. Pour définir le débit, nous devons effectuer un calcul et mettre le résultat dans certains registres. La formule utilisé est  UART_CLOCK_SPEED/(16 * DESIRED_BAUD) . La partie entière de ce calcul va dans IBRD, le registre de l’UART. Ce calcul ne donne probablement pas un nombre entier comme dans ce cas 1,67. Cela signifie que nous devons stocker 1 dans la IBRD, puis nous devons également calculer un diviseur à partir de la partie fractionnaire du calcul précédent en utilisant cette formule (.67 * 64) + .5 . Cela donne environ 40, donc nous avons mis le registre FBRD à 40.

Cela met le 4ème, 5ème et 6ème bit à 1 dans le registre de contrôle (Line control register). Le réglage du 4 ème bit signifie que le matériel UART conservera les données dans un FIFO jusqu’à 8 éléments, au lieu d’un registre à 1 élément. Le réglage 5 et 6 à 1 signifie que les données envoyées ou reçues auront des chaines de 8 bits.

Ce ligne de code désactive toutes les interruptions de l’UART en écrivant aux bits correspondants du registre d’effacement du masque d’interruption (Interrupt Mask Set Clear register).

Cela écrit le bit 0, 8 et 9 dans le registre de contrôle. Le bit 0 active le matériel UART, le bit 8 permet la réception de données et le bit 9 permet la transmission de données.

Lecture et écriture de texte

Ce code permet de lire et d’écrire des caractères depuis et vers l’UART. FR est le registre des flags, et il nous permet de savoir si notre structure FIFO a des données à lire, et si il peut accepter n’importe quelle donnée. DR est le registre de données. C’est le registre dans lequel les données sont lues et écrites. la fonction uart_puts  encapsule simplement putc  dans une boucle pour que nous puissions écrire des chaînes entières.

le cœur du kernel

C’est la fonction principale de notre noyau. Tout ce qu’il fait est d’appeler la fonction init_uart, qui affiche “Hello, kernel World!”, Et retourne tout caractère que vous tapez. C’est là que nous allons ajouter des appels à de nombreuses autres fonctions d’initialisation.

Les arguments de cette fonction semblent un peu bizarres. Normalement, la fonction main en C ressemble à int main(int argc, char ** argv), mais le notre kernel_main  n’a rien à voir avec cela. En ARM, la convention est que les trois premiers paramètres d’une fonction passent par les registres r0, r1 et r2. Lorsque le bootloader charge notre noyau, il place également des informations sur le matériel et la ligne de commande utilisée pour exécuter le noyau en mémoire. Cette information est appelée atags, et un pointeur sur atags est placé dans r2 juste avant que boot.S s’exécute. Donc, pour notre kernel_main, r0 et r1 sont simplement les paramètres à la fonction par convention, mais nous ne nous en soucions pas. r2 contient le pointeur atags, donc le troisième argument à kernel_main  est le pointeur atags.

linker.ld – Lier les pièces ensemble

Il y a trois étapes principales dans le processus de compilation C. La première est le preproccessing, où toutes vos instructions  #define sont étendues. Le second est la compilation en fichiers objets, où les fichiers de code individuels sont convertis en binaires individuels appelés fichiers objets. Le troisième est la liaison, où ces fichiers objet individuels sont liés ensemble dans un seul exécutable.

Par défaut, GCC lie votre programme comme s’il s’agissait d’un code de niveau utilisateur. Nous devons remplacer la valeur par défaut, car notre noyau n’est pas un programme pour un utilisateur ordinaire. Nous faisons cela avec un script d’éditeur de liens (linker). Voici le script que nous utiliserons:

Explication du code

  • Dans un linker, . signifie l’ adresse actuelle. Vous pouvez attribuer l’adresse actuelle et affecter aussi des valeur à l’adresse actuelle.
  • .text  la section du code
  • .rodata  (read only data) où les constantes globales sont placées.
  • .data  est l’endroit où les variables globales qui sont initialisés au moment de la compilation sont placés.
  • .bss est l’endroit où les variables globales non initialisées sont placés.

Ceci déclare que le symbole _start  de boot.S qui est le point d’entrée à notre code.

Ceci définit les symboles __start  et __text_start  à 0x8000. Il déclare alors la section .text  pour commencer juste après. La première partie de la section .text  est .text.boot , où le code de boot.S réside. KEEP signifie que le linker ne doit pas essayer d’optimiser le code dans .text.boot  même si elle n’est pas référencé nulle part. La deuxième partie de la section .text  insère toute les sections de tous les autres objets, dans un ordre quelconque. Puis déclarer __text_end  et lui assigner la deuxième plus grande adresse divisible par 4096 après la section .text. Cet arrondi au 4096 le plus proche est appelé alignement de la page, et c’est important quand on commence à travailler avec la mémoire.

De même, nous déclarons __rodata_start  à la même adresse que __text_end , nous déclarons la section .rodata , qui se compose de toutes les sections .rodata  de tous les fichiers d’objet. Puis nous déclarons la section __rodata_end  à la prochaine adresse de page alignée après .rodata. Nous recommencons ensuite pour les sections .data  et .bss.

Compilation et exécution

Pour compiler ce code pour la machine virtuelle, nous devons exécuter les commandes suivantes:

Les deux premières lignes de commandes compilent boot.S et kernel.c en fichier objet. La troisième ligne lie ces fichiers objet en un fichier elf exécutable.

Jetons un coup d’oeil à ces paramètres de gcc moins utilisées. -mcpu=cortex-a7 definie le cpu ARM cible en cortex-a7, qui est le processeur du raspberry pi model 2 a, et ce que notre VM émule. -fpic  crée un code indépendant de la position. C’est-à-dire que les références à toute les fonctions, variable ou symbole doivent être calculé par rapport à l’instruction en cours, et non par une adresse absolue. -ffreestanding  specifie à gcc de ne pas dépendre de la disponibilité de la libc au moment de l’exécution, et qu’il ne peut pas y avoir de fonction main comme point d’entrée. -nostdlib  indique au linker  qu’il ne devrait pas essayer de lier avec la libc.

Pour exécuter le code dans la machine virtuelle, exécutez cette commande:

Cela exécute une machine virtuelle qui émule le modèle 2 de raspberry pi avec 256 MO de mémoire. Il est configuré pour lire et écrire des données depuis et vers votre terminal normal comme s’il était connecté au Raspberry Pi via une connexion série. Il spécifie notre fichier elf comme le noyau à exécuter dans la machine virtuelle.

Après avoir exécuté ceci, vous devriez voir “Hello, kernel World!” dans votre terminal. Si vous tapez dans votre terminal, il fera écho à tous les caractères.

Le code est publiquement disponible sur GitHub.

Maintenant que nous avons un noyau qui démarre, nous devrions organiser notre projet.

Navigation<< 0x00 – Mise en place de l’environnement de développement0x02 – Organiser notre projet >>

What do you think?

23 Points
Upvote Downvote

commentaires

Laisser un commentaire

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.

GIPHY App Key not set. Please check settings

Chargement & hellip;

Comment accédez à notre site gratuitement?

Intelligence artificielle : c’est quoi ?