0x01 - Le premier boot
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.
Articles Similaires
Ubuntu 24.04 LTS - Une version qui fait débat entre déception et enthousiasme
Ubuntu 24.04 LTS, “Noble Numbat”, a récemment été déployée, apportant son lot de nouveautés et de changements. Cette version suscite à la fois de l’enthousiasme et de la déception au sein de la communauté des utilisateurs et des développeurs. Déception et colère face à la gestion des paquets DEB Plusieurs utilisateur d’Ubuntu ont exprimé leur déception et colère face à la décision de Canonical, la société mère d’ Ubuntu, de favoriser les paquets Snap au détriment des paquets DEB.
Lire la SuiteLe concours de beauté Miss AI : un cauchemar dystopique ou le futur de la beauté ?
Dans un monde où la technologie et la beauté fusionnent, le concours de beauté Miss AI fait son apparition. Ce concours, organisé par The World AI Creator Awards, récompense les créateurs d’images et d’influenceurs générés par intelligence artificielle (IA). Mais qu’est-ce que cela signifie pour les standards de beauté et les femmes ? Le concours Miss AI est ouvert aux créateurs d’images et d’influenceurs générés par IA qui souhaitent montrer leur charme et leur compétence technique.
Lire la SuiteLe gouvernement du Salvador prend un coup dur : les hackers divulguent le code source et les accès VPN du portefeuille bitcoin national Chivo !
Le programme bitcoin du gouvernement du Salvador, Chivo, a été victime d’une série d’attaques informatiques ces derniers jours. Les hackers ont déjà divulgué les données personnelles de plus de 5 millions de Salvadoriens. Maintenant, les mêmes pirates informatiques ont publié des extraits du code source et des informations d’accès VPN du portefeuille bitcoin national Chivo sur un forum de hacking en ligne, CiberInteligenciaSV. Ceci est un coup dur pour El Salvador, qui lutte pour être un pionnier dans l’adoption du bitcoin.
Lire la Suite