├── .gitignore ├── LICENSE.md ├── README.md ├── book.toml ├── contributing.md └── src ├── SUMMARY.md ├── allocateur-de-mémoire-physique.md ├── allocateur-de-page.md ├── cross-compilation └── creer-un-cross-compiler.md ├── introduction.md ├── types-de-kernel.md ├── wiki.md └── x86_64 ├── acpi └── MADT.md ├── assets ├── devse.jpg ├── frame_buffer_pixels_bpp.svg ├── kernel_higher_lower_half.svg ├── tutoriel-hello-world-result.png └── tutoriel-hello-world-stivale2-linked-list.svg ├── exceptions.md ├── index.md ├── premiers-pas ├── 00-introduction.md ├── 01-hello-world.md ├── 02-segmentation.md ├── 03-interruptions.md ├── 03-interuptions.md ├── 04-Memoire.md ├── 04-memoire.md ├── 05-paging.md ├── 06-epilogue.md ├── 06-multitache.md ├── 06-tache-utilisateur.md ├── 07-tache-utilisateur.md └── 08-epilogue.md ├── périphériques ├── APIC.md ├── COM.md ├── PIC.md ├── PIT.md └── framebuffer.md ├── smp ├── SMP.md └── locks.md └── structures ├── GDT.md └── IDT.md /.gitignore: -------------------------------------------------------------------------------- 1 | book 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Licence Creative Commons Attribution 2.0 France 2 | 3 | Licence Creative Commons
Cette œuvre est mise à disposition selon les termes de la Licence Creative Commons Attribution 2.0 France. 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wiki DEVSE 2 | 3 | Ce guide est disponible à l'adresse [devse.wiki](https://devse.wiki). 4 | 5 | ## Documentation 6 | 7 | Ce répertoire GitHub a été créé pour fournir une documentation sur le développement de systèmes d'exploitation en Français. 8 | N'hésitez pas à contribuer à la documentation, rajouter des exemples, etc ... Cette documentation est open source et est disponible à l'adresse [github.com/devse-org/documentation](https://github.com/devse-org/documentation). 9 | 10 | Nous ne sommes pas affiliés au site internet OSDEV, mais au serveur Discord francophone [DEVSE](https://discord.gg/3XjkM6q). 11 | 12 | 13 | Discord Banner 3 14 | 15 | 16 | ## Licence 17 | 18 | Licence Creative Commons
Cette œuvre est mise à disposition selon les termes de la Licence Creative Commons Attribution 2.0 France. 19 | -------------------------------------------------------------------------------- /book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Communauté DEVSE"] 3 | language = "fr" 4 | multilingual = false 5 | src = "src" 6 | title = "Développement de système d'exploitation" 7 | [output.html] 8 | default-theme = "ayu" 9 | git-repository-url="https://github.com/developpement-systeme-exploitation/documentation" -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contribuer au wiki 2 | 3 | ## Avant toute contribution, la lecture de ce document est obligatoire 4 | 5 | - Veuillez employer un français correct. Notre langue n'est pas des plus simples, mais son bon emploi standard nous permet de nous comprendre mutuellement de façon claire, d'autant plus dans un domaine aussi spécifique que le développement de systèmes d'exploitation. 6 | - Les langages de programmation principalement utilisés dans les exemples sont le C, le C++ et l'assembleur x86, avec une syntaxe Intel. En effet, ce sont des langages couramment utilisé lorsque l'on programme un noyau, un OS, ou un pilote. 7 | - Veuillez produire votre propre contenu. Les copier-collers sont contre-productifs pour vous. C'est en réfléchissant par soi-même et en interprétant soi-même ce que l'on évolue. 8 | - Nous évitons d'utiliser les architectures en 32 bits car elles ne sont plus forcément d'actualité. 9 | 10 | ## La structure suivante est de rigueur pour l'ensemble des documents 11 | 12 | - La partie "haute" du document regroupe le sommaire de l'article, ainsi que les liens qui permettent de s'y balader. 13 | - Vous retrouverez ensuite, factuellement, une liste détaillée et argumentée des préréquis pour la compréhension d'un article, ou l'application d'un tutoriel. 14 | - Le reste du document est constitué du sujet de l'article. Typiquement: introduction au sujet, explication, illustration par les exemples/métaphores/comparaisons, conclusion et ressenti personnel. 15 | - L'article doit impérativement donner accès aux ressources qui lui ont permis d'être développé. Ces ressources peuvent être d'autres articles vérifiés, des livres, des vidéos ou des topics dans des forums. 16 | 17 | ## Commits / Pull requests 18 | 19 | Vous devez suivre les règles suivantes pour la rédaction des noms de commits / pull requests. 20 | 21 | ## Type de la modification: ce que vous avez fait / rajouté 22 | 23 | Les types de modification peuvent être : 24 | 25 | - correction 26 | - x64 27 | - arm 28 | - misc (on y compte par exemple les ports COM, les systèmes de fichiers ou tout autre chose qui ne rentre pas dans les catégories d'architecture) 29 | - exemple (ajout d'exemple) 30 | - autre (pour autre chose qui n'y entre pas) 31 | 32 | Il est recommandé de ne pas faire plus d'un commit par Pull Request. 33 | 34 | ## Marche à suivre pour les exemples 35 | 36 | - Suivez la structure des documents, pour que l'exemple en question soit cohérent avec le reste de l'article. 37 | - Appliquez-vous sur votre code (lisibilité, commentaires, vérification). 38 | - Même si les langages majoritairement utilisés sont le C, le C++ et l'assembleur x86 (voir la liste en haut de la page), il peut être utile d'utiliser d'autres langages de programmation/schématiques qui permettraient d'interpréter clairement une information. 39 | -------------------------------------------------------------------------------- /src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [wiki](./wiki.md) 4 | - [Introduction](./introduction.md) 5 | - [BootLoader](./bootloader.md) 6 | - [Types de noyau](./types-de-kernel.md) 7 | - [cross compilateur]() 8 | - [creer un cross compilateur](cross-compilation/creer-un-cross-compiler.md) 9 | - [gestion de la mémoire]() 10 | - [allocateur de mémoire physique](allocateur-de-mémoire-physique.md) 11 | - [x86_64](x86_64/index.md) 12 | - [périphériques]() 13 | - [APIC](x86_64/périphériques/APIC.md) 14 | - [framebuffer](x86_64/périphériques/framebuffer.md) 15 | - [COM](x86_64/périphériques/COM.md) 16 | - [PIC](x86_64/périphériques/PIC.md) 17 | - [PIT](x86_64/périphériques/PIT.md) 18 | - [exceptions](x86_64/exceptions.md) 19 | - [SMP]() 20 | - [SMP](x86_64/smp/SMP.md) 21 | - [locks](x86_64/smp/locks.md) 22 | - [structures]() 23 | - [gdt](x86_64/structures/GDT.md) 24 | - [idt](x86_64/structures/IDT.md) 25 | - [tutoriels]() 26 | - [Premiers Pas](x86_64/premiers-pas/00-introduction.md) 27 | - [Hello, world!](x86_64/premiers-pas/01-hello-world.md) 28 | - [Segmentation](x86_64/premiers-pas/02-segmentation.md) 29 | - [Interruptions](x86_64/premiers-pas/03-interuptions.md) 30 | - [Mémoire](x86_64/premiers-pas/04-memoire.md) 31 | - [Pagging](x86_64/premiers-pas/05-paging.md) 32 | - [Multitâche](x86_64/premiers-pas/06-multitache.md) 33 | - [Tache utilisateur](x86_64/premiers-pas/06-tache-utilisateur.md) 34 | - [Epilogue](x86_64/premiers-pas/06-epilogue.md) 35 | -------------------------------------------------------------------------------- /src/allocateur-de-mémoire-physique.md: -------------------------------------------------------------------------------- 1 | # Allocateur de mémoire physique 2 | 3 | Un `allocateur de mémoire physique` est un algorithme d'allocation 'basique' qui est généralement utilisé par le kernel pour allouer et libérer des pages. 4 | 5 | > Note : tout au long de ce document, le terme `page` est utilisé comme zone de mémoire qui à pour taille 4096 byte 6 | > Cette taille peut changer mais pour l'instant il est mieux d'utiliser la même taille de page entre le paging et l'allocateur de mémoire physique 7 | 8 | Il doit pouvoir : 9 | 10 | - Allouer une/plusieurs page libre 11 | - Libérer une page allouée 12 | - Gérer quelle zone de la mémoire est utilisable ou non 13 | 14 | Voici un code C basique présentant les fonctions de base à implémenter pour un allocateur de mémoire physique: 15 | 16 | ```c 17 | void* alloc_page(uint64_t page_count); 18 | 19 | void free_page(void* page_addr, uint64_t page_count); 20 | 21 | void init_pmm(memory_map_t memory_map); // PMM = Physical Memory Manager 22 | ``` 23 | 24 | ## L'Allocateur de mémoire physique avec une bitmap 25 | 26 | Cette partie du document explique comment mettre en place un allocateur de mémoire physique avec une bitmap. 27 | 28 | La bitmap est une table de uint64/32/16 ou uint8_t avec chaque bit qui représente une page libre (quand le bit est à 0) ou utilisée (quand le bit est à 1). 29 | 30 | Vous pouvez facilement convertir une adresse en index/bit de la table, par exemple : 31 | 32 | ```c 33 | static inline uint64_t get_bitmap_array_index(uint64_t page_addr) 34 | { 35 | return page_addr/8; // ici c'est 8 car c'est une bitmap avec des uint8_t (soit 8bit) 36 | } 37 | 38 | static inline uint64_t get_bitmap_bit_index(uint64_t page_addr) 39 | { 40 | return page_addr%8; 41 | } 42 | ``` 43 | 44 | La bitmap a l'avantage d'être petite. Par exemple, pour une mémoire de 4Go on a : 45 | 46 | `((2^32 / 4096) / 8)` = 131 072 byte soit 47 | une bitmap de 128 kb 48 | 49 | Il faut aussi savoir que la bitmap à l'avantage d'être très rapide, on peut facilement libérer/allouer une page. 50 | 51 | ## Changer l'état d'une page dans la bitmap 52 | 53 | Pour cette partie vous devez placer une variable temporairement nulle... Cette variable est la bitmap qui serra initialisée plus tard, mais vous devez tout d'abord savoir comment changer l'état d'une page. 54 | 55 | ici la variable est : 56 | 57 | ```c 58 | uint8_t* bitmap = NULL; 59 | ``` 60 | 61 | Avant d'allouer/libérer des pages, il faut les changer d'état, donc mettre un bit précis de la bitmap à 0 ou à 1. 62 | 63 | Il suffit de 2 fonctions qui permettent de soit mettre un bit de la bitmap à 0 soit de le mettre à 1 par rapport à une page. 64 | 65 | ```c 66 | static inline void bitmap_set_bit(uint64_t page_addr) 67 | { 68 | uint64_t bit = get_bitmap_bit_index(page_addr); 69 | uint64_t byte = get_bitmap_array_index(page_addr); 70 | 71 | bitmap[byte] |= (1 << bit); 72 | } 73 | 74 | static inline void bitmap_clear_bit(uint64_t page_addr) 75 | { 76 | uint64_t bit = get_bitmap_bit_index(page_addr); 77 | uint64_t byte = get_bitmap_array_index(page_addr); 78 | 79 | bitmap[byte] &= ~(1 << bit); 80 | } 81 | ``` 82 | 83 | ## Initialiser l'allocateur de mémoire physique 84 | 85 | L'allocateur de mémoire physique doit être initialisé le plus tôt possible, vous devez avoir au moins la carte de la mémoire (quelle zone est libre et quelle zone ne l'est pas) généralement fournie par le bootloader. 86 | 87 | cependant vous devez calculer avant la future taille de la bitmap, générallement la taille de la mémoire est la fin de la dernière entrée de la carte de la mémoire. 88 | 89 | ```c 90 | uint64_t memory_end = memory_map[memory_map_size].end; 91 | uint64_t bitmap_size = memory_end / (PAGE_SIZE*8); 92 | ``` 93 | 94 | Après avoir obtenu la taille de la future bitmap vous devez trouver une place pour la positionner. 95 | 96 | Vous devez trouver une entrée valide de la carte de la mémoire et placer la bitmap au début de cette entrée. 97 | 98 | ```c 99 | for(int i = 0; i < mem_map.size && bitmap==NULL; i++) 100 | { 101 | mem_map_entry_t entry = mem_map.entry[i]; 102 | if(entry.is_free && entry.size >= bitmap_size) 103 | { 104 | bitmap = entry.start; 105 | } 106 | } 107 | ``` 108 | 109 | Ensuite, pour chaque entrée de la carte de la mémoire vous devez mettre la région de la bitmap en utilisée ou libre. 110 | On peut mettre par défaut toute la bitmap comme utilisée ainsi que la mettre libre seulement quand c'est nécessaire. 111 | 112 | ```c 113 | uint64_t free_memory = 0; 114 | 115 | memset(bitmap, 0xff, bitmap_size); // mettre toutes les pages comme utilisées 116 | 117 | for(int i = 0; i < mem_map.size; i++) 118 | { 119 | mem_map_entry_t entry = mem_map.entry[i]; 120 | // en espérant ici que entry.start et entry.end sont déjà aligné par rapport à une page 121 | if(entry.is_free) 122 | { 123 | for(uint64_t j = entry.start; j < entry.end; j+=PAGE_SIZE) 124 | { 125 | 126 | bitmap_clear_bit(j/PAGE_SIZE); 127 | free_memory += PAGE_SIZE; 128 | } 129 | } 130 | } 131 | 132 | ``` 133 | 134 | Cependant, la zone où est placée la bitmap est marquée comme libre. Une tâche peut donc écraser cette zone et causer des problèmes... Vous devez par conséquent marquer la zone de la bitmap comme utilisée : 135 | 136 | ```c 137 | uint64_t bitmap_start = (uint64_t)bitmap; 138 | uint64_t bitmap_end = bitmap_start + bitmap_size; 139 | for (uint64_t i = bitmap_start; i <= bitmap_end; i+= PAGE_SIZE) 140 | { 141 | bitmap_set_bit(i/PAGE_SIZE); 142 | } 143 | ``` 144 | 145 | ## L'allocation, la recherche et la libération de pages 146 | 147 | Une fois votre bitmap initialisée vous pouvez mettre une page comme libre ou utilisée. Ainsi, vous pouvez commencer à implémenter des fonction d'allocation et de libération de pages. 148 | Cependant, vous devez commencer par vérifier si une page est utilisée ou libérée (ou si le bit d'une page est à 0 où à 1) : 149 | 150 | ```c 151 | static inline bool bitmap_is_bit_set(uint64_t page_addr) 152 | { 153 | uint64_t bit = get_bitmap_bit_index(page_addr); 154 | uint64_t byte = get_bitmap_array_index(page_addr); 155 | 156 | return bitmap[byte] & (1 << bit); 157 | } 158 | ``` 159 | 160 | ### L'allocation de page 161 | 162 | Une fonction d'allocation de page doit avoir comme argument le nombre de pages allouées et doit retourner des pages qui seront marquées comme utilisées. 163 | 164 | Pour commencer, vous devez mettre en place une fonction qui cherche et trouve de nouvelles pages: 165 | 166 | ```c 167 | // note ici c'est la fonction brut, il y a plusieurs optimizations possiblent qui serront abordés plus tard 168 | uint64_t find_free_pages(uint64_t count) 169 | { 170 | uint64_t free_count = 0; // le nombre de pages libres de suite 171 | 172 | for(int i = 0; i < (mem_size/PAGE_SIZE); i++) 173 | { 174 | if(!bitmap_is_bit_set(i)) 175 | { 176 | free_count++; // on augmente le nombre de page trouvées d'affilée de 1 177 | if(free_count == count) 178 | { 179 | return i; 180 | } 181 | } 182 | else 183 | { 184 | free_count = 0; 185 | } 186 | } 187 | return -1; // il n'y a pas de page libres 188 | } 189 | ``` 190 | 191 | `find_free_page` donne donc `count` pages libre 192 | 193 | Après avoir trouvé les pages, vous devrez les mettre comme utilisées: 194 | 195 | ```c 196 | void* alloc_page(uint64_t count) 197 | { 198 | uint64_t page = find_free_pages(count); // ici pas de gestion d'erreur mais vous pouvez vérifier si il n'y a plus de pages disponibles 199 | 200 | for(int i = page; i < count+page; i++) 201 | { 202 | bitmap_set_bit(i); 203 | } 204 | return (void*)(page*PAGE_SIZE); 205 | } 206 | ``` 207 | 208 | Vous avez désormais un allocateur de mémoire physique fonctionnel ! 209 | 210 | ### La libération de page 211 | 212 | Après avoir alloué des pages vous devez pouvoir les libérer. 213 | 214 | Le fonctionnement est plus simple que l'allocation, vous devez juste mettres les bits des pages à 0. 215 | 216 | **Note** : Ici il n'y a pas de vérification d'erreur car c'est un exemple. 217 | 218 | ```c 219 | void free_page(void* addr, uint64_t page_count) 220 | { 221 | uint64_t target= ((uint64_t)addr) / PAGE_SIZE; 222 | for(int i = target; i<= target+page_count; i++) 223 | { 224 | bitmap_clear_bit(i); 225 | } 226 | } 227 | ``` 228 | 229 | Cette fonction met juste les bit de la bitmap à 0. 230 | 231 | ### Les optimisations 232 | 233 | L'allocation de pages comme ici est très lente, à chaque fois on revient à 0 pour chercher une page et cela peut ralentir énormément le système. 234 | On peut donc mettre en place plusieurs optimizations: 235 | 236 | Une optimisation basique serait de créer une variable last_free_page qui donne la dernière page libre à la place de toujours revenir à la page 0 pour en chercher une nouvelle. Cela améliore largement les performances et est relativement simple à mettre en place: 237 | 238 | ```c 239 | uint64_t last_free_page = 0; 240 | uint64_t find_free_pages(uint64_t count) 241 | { 242 | uint64_t free_count = 0; 243 | for(int i = last_free_page; i < (mem_size/PAGE_SIZE); i++) 244 | { 245 | if(!bitmap_is_bit_set(i)) 246 | { 247 | free_count++; trouvées d'affilée de 1 248 | if(free_count == count) 249 | { 250 | last_free_page = i; // la dernière page libre 251 | return i; 252 | } 253 | } 254 | else 255 | { 256 | free_count = 0; 257 | } 258 | } 259 | 260 | return -1; // il n'y a pas de page libres 261 | } 262 | ``` 263 | 264 | Cependant, si on ne trouve pas de page libre à partir de la dernière page libre, il peut en avoir avant. Il faut donc réésayer en mettant le nombre de page libre à zéro. 265 | 266 | ```c 267 | // à la fin de la fonction find_free_pages() 268 | if(last_free_page != 0) 269 | { 270 | last_free_page = 0; 271 | return find_free_pages(count); // juste réésayer mais avec la dernière page libre en 0x0 272 | } 273 | return -1; 274 | ``` 275 | 276 | Vous pouvez aussi faire en sorte que la dernière page libre soit automatiquement remise à la dernière page libérée dans free_page: 277 | 278 | ```c 279 | // free_page() 280 | last_free_page = page_addr; 281 | ``` 282 | 283 | --- 284 | une autre optimisation serait dans find_free_page; on peut utiliser la capacité du processeur à faire des vérification avec des nombres 64, 32, 16 et 8 bits pour que cela soit plus rapide. En sachant que dans une bitmap, quand il y a une entrée de la table totallement pleine, tous les bits sont à 1 donc ils sont donc à `0b11111111` = `0xff` 285 | 286 | On peut donc rajouter 287 | (sans le code pour last_free_page pour que cela soit plus compréhensible) 288 | 289 | ```c 290 | uint64_t find_free_pages(uint64_t count){ 291 | int i = 0; 292 | 293 | for(int i = 0; i < (mem_size/PAGE_SIZE); i++) 294 | { 295 | // vous pouvez aussi utiliser des uint64_t ou n'importe quel autres types 296 | while(bitmap[i/8] == 0xff && i < (mem_size/PAGE_SIZE)-8) 297 | { 298 | free_count = 0; // en sachant que les pages sont utilisées, alors on reset le nombre de page libres de suite 299 | i += 8- (i % 8); // rajouter mettre i au prochain index de la bitmap 300 | } 301 | 302 | if(!bitmap_is_bit_set(i)) 303 | { 304 | free_count++; // trouvées d'affilée de 1 305 | if(free_count == count) 306 | { 307 | return i; 308 | } 309 | } 310 | else 311 | { 312 | free_count = 0; 313 | } 314 | } 315 | return -1; 316 | } 317 | ``` 318 | 319 | --- 320 | Maintenant vous pouvez utiliser votre allocateur de mémoire physique principalement pour le paging ou pour un allocateur plus 'intelligent' (malloc/free/realloc) ! 321 | -------------------------------------------------------------------------------- /src/allocateur-de-page.md: -------------------------------------------------------------------------------- 1 | # Allocateur de page 2 | -------------------------------------------------------------------------------- /src/cross-compilation/creer-un-cross-compiler.md: -------------------------------------------------------------------------------- 1 | 2 | # Créer un cross compilateur GCC (C/C++) 3 | 4 | ## Pourquoi faire un cross compilateur ? 5 | 6 | Il faut faire un cross compilateur car le compilateur fournis avec votre système est configuré pour une plateforme cible (CPU, système d'exploitation etc). 7 | 8 | Par exemple, comparons deux platformes différentes (Ubuntu x64 et Debian GNU/Hurd i386). 9 | La commande`gcc -dumpmachine` nous indique la platforme que cible le compilateur, sur Ubuntu GNU/Linux la commande me retourne `x86_64-linux-gnu` 10 | tandis que sur Debian GNU/Hurd nous avons `i686-gnu`. 11 | 12 | Le resultat obtenu n'est pas surprennant, nous avons deux systèmes d'exploitation différent sur du materiel différent. 13 | 14 | Ne pas faire un cross compilateur et utiliser le compilateur fournis avec le système c'est allez au devant de toute une série de problèmes. 15 | 16 | ## Quel plateforme cible ? 17 | 18 | Tout cela va dépendre de l'architecture que vous ciblez (x86, risc-v) et du format de vos binaires (ELF, mach-o, PE). 19 | 20 | Par exemple pour un système x86-64 en utilisant le format ELF: `x86_64-elf` 21 | Ou encore `i686-elf` pour x86 (32bit) 22 | 23 | Bien sur en attendant d'avoir notre propre toolchain. 24 | 25 | ## Compiler GCC et les binutils 26 | 27 | Maintenant que la théorie à été rapidement esquissée nous allons pouvoir passer à la pratique. 28 | 29 | créons un dossier toolchain/local à la racine de notre projet. C'est dans ce dossier que sera notre cross compilateur une fois compilé. 30 | 31 | créons donc une variable `$prefix`: 32 | 33 | ```bash 34 | prefix="/toolchain/local" 35 | ``` 36 | 37 | Profitons en pour modifier notre `$PATH`: 38 | ```bash 39 | export PATH="$PATH:$prefix/bin" 40 | ``` 41 | 42 | Puis nous allons définir une variable `$target` (qui contiendra notre platforme cible). 43 | Comme dans notre guide nous nous concentrons sur x86-64 notre variable sera définis comme ceci: 44 | ```bash 45 | target="x86_64-elf" 46 | ``` 47 | 48 | Nos variables d'environment étant définis nous pouvons passer à l'installation des dépendances. 49 | 50 | ### Dépendance 51 | 52 | Pour pouvoir compiler gcc et binutils sous Debian GNU/Linux il nous faut les paquets suivant: 53 | 54 | - build-essential 55 | - bison 56 | - flex 57 | - texinfo 58 | - libgmp3-dev 59 | - libmpc-dev 60 | - libmpfr-dev 61 | 62 | Que l'on peut les installer simplement comme ceci: 63 | 64 | ```bash 65 | sudo apt install build-essential bison flex libgmp3-dev \ 66 | libmpc-dev libmpfr-dev texinfo 67 | ``` 68 | 69 | Nous allons pouvoir passer à la compilation. 70 | 71 | ### binutils 72 | 73 | Commençons par télécharger et décompresser les sources de binutils. 74 | 75 | Ici dans ce tutoriel nous compilerons binutils `2.35`. 76 | 77 | ```bash 78 | binutils_version="2.35" 79 | wget "https://ftp.gnu.org/gnu/binutils/binutils-$binutils_version.tar.xz" 80 | tar -xf "binutils-$binutils_version.tar.xz" 81 | ``` 82 | 83 | Maintenant que l'archive est décompressé nous allons passer à la compilation. 84 | 85 | ```bash 86 | cd "binutils-$binutils_version" 87 | mkdir build && cd build 88 | ../configure --prefix="$prefix" --target="$target" \ 89 | --with-sysroot --disable-nls --disable-werror 90 | make all -j $(nproc) 91 | make install -j $(nproc) 92 | ``` 93 | 94 | Comme la compilation risque de prendre un moment, vous pouvez en profiter pour vous faire un café. 95 | 96 | ### gcc 97 | 98 | Maintenant les binutils sont compilé, nous allons pouvoir passer à gcc. 99 | 100 | Ici nous compilerons gcc `10.2.0`. 101 | 102 | ```bash 103 | gcc_version="10.2.0" 104 | wget http://ftp.gnu.org/gnu/gcc/gcc-$gcc_version/gcc-$gcc_version.tar.xz 105 | tar -xf gcc-$gcc_version.tar.xz 106 | ``` 107 | 108 | Puis on passe à la compilation: 109 | 110 | ```bash 111 | cd "gcc-$gcc_version" 112 | mkdir build && cd build 113 | ../configure --prefix="$prefix" --target="$target" --with-sysroot \ 114 | --disable-nls --enable-languages=c,c++ --with-newlib 115 | make -j all-gcc 116 | make -j all-target-libgcc 117 | make -j install-gcc 118 | make -j install-target-libgcc 119 | ``` 120 | 121 | La encore ça va prendre un certain temps, on peut donc s'accorder une deuxième pause café. 122 | 123 | Une fois la compilation terminée vous pouvez utilisez votre cross compilateur, dans le cas de ce tutoriel `x86_64-elf-gcc`. 124 | 125 | Cependant il faudrait plus tard implémenter une toolchain spécifique pour votre os. 126 | C'est une toolchain modifiée pour votre système d'exploitation. -------------------------------------------------------------------------------- /src/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | -------------------------------------------------------------------------------- /src/types-de-kernel.md: -------------------------------------------------------------------------------- 1 | # Types de noyaux 2 | 3 | Les kernels sont classés en plusieurs catégories, certaines sont plus complexes que d'autres... 4 | 5 | ## Les micro-kernels 6 | 7 | Les micro-kernel, sont minimalistes, élégants et **résilients** aux crashs. Les systèmes basés sur un microkernel sont composés d'une collection de services exécutés dans l'userspace qui communiquent entre eux. Si un service crash il peut être redémarré sans reboot la machine entière. Les premières générations avaient l'inconvénient d'être plus lentes que les kernels monolithiques. Mais cela n'est plus vrai de nos jours: les kernels de la famille L4 n'ont rien à envier en terme de rapidité à leurs homologues monolithiques. 8 | 9 | **Exemples**: Minix, L4, march, fushia 10 | 11 | ## Les exo-kernels 12 | 13 | Les exo-kernels, sont une forme plus poussée de micro-kernels, en effet, les exo-kernels ont pour but de placer le noyau dans l'espace utilisateur, essayant de supprimer toutes abstraction entre le kernel et la machine. Générallement le lien entre la machine et l'application et faite à travers une librairie lié dès le démarrage de l'application (par exemple LibOSes). Les applications peuvent donc gérer elles mêmes certaines parties bas niveau et donc avoir de meilleure performance. Cependant le développement d'exo-kernel est très dur. 14 | 15 | **Exemples**: Xen, Glaze 16 | 17 | ## Les kernels monolithiques 18 | 19 | La méthode monolithique est la manière classique de structurer un kernel. Un kernel monolithique contient les drivers, le système de fichier, etc, dans l'espace superviseur. Contrairement aux microkernels ils sont gros et lourds, si il y a un crash dans le kernel ou si un service crash, tout crash et il faut reboot la machine. 20 | Les kernels monolithiques peuvent être modulaire (comme Linux), cependant les modules sont directements intégrés au noyaux et non à l'espace utilisateur comme le ferrait un micro kernel. 21 | 22 | **Exemples**: Linux, BSDs 23 | 24 | ## Les unikernels 25 | 26 | Les unikernels sont spéciaux car ils n'ont pas comme but d'être utilisés sur une machine de travail, mais plutôt sur un serveur. Les unikernels sont souvent utilisés à des fins de virtualisation, comme Docker. 27 | 28 | **Exemples**: IncludeOS, MirageOS, HaLVM, Runtime.js 29 | 30 | ## Références 31 | 32 | - [The Exokernel Operating System Architecture](https://u.cs.biu.ac.il/~wisemay/2os/microkernels/exokernel.pdf) 33 | -------------------------------------------------------------------------------- /src/wiki.md: -------------------------------------------------------------------------------- 1 | # Wiki DEVSE 2 | 3 | Ce guide est disponible à l'adresse [https://devse.wiki](devse.wiki). 4 | 5 | 6 | 7 | ## Documentation 8 | 9 | Ce répertoire GitHub a été créé pour fournir une documentation sur le développement de systèmes d'exploitation en Français. 10 | N'hésitez pas à contribuer à la documentation, rajouter des exemples, etc ! 11 | 12 | Nous ne sommes pas affiliés au site Internet OSDEV, mais au serveur Discord Français [DEVSE](https://discord.gg/3XjkM6q). 13 | 14 | Discord Banner 3 15 | 16 | ## Licence 17 | 18 | Licence Creative Commons
Cette œuvre est mise à disposition selon les termes de la Licence Creative Commons Attribution 2.0 France. 19 | -------------------------------------------------------------------------------- /src/x86_64/acpi/MADT.md: -------------------------------------------------------------------------------- 1 | # MULTIPLE APIC DESCRIPTION TABLE 2 | -------------------------------------------------------------------------------- /src/x86_64/assets/devse.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devse-org/wiki/5dacf9604764feb6232f6e80bd147e981ce6b903/src/x86_64/assets/devse.jpg -------------------------------------------------------------------------------- /src/x86_64/assets/frame_buffer_pixels_bpp.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 38 | 40 | 41 | 43 | image/svg+xml 44 | 46 | 47 | 48 | 49 | 50 | 56 | 63 | 70 | 77 | 84 | 91 | 98 | 105 | 112 | 119 | 126 | 133 | 140 | 147 | 154 | 161 | 168 | 175 | pixel 1 186 | pixel 2 197 | pixel 2 208 | pixel 1 219 | 8 bit 230 | 237 | 8 bit 248 | 255 | 32 bit 266 | 273 | 280 | 24 bit 291 | 32 bits par pixel: 302 | 24 bits par pixel: 313 | 314 | 315 | -------------------------------------------------------------------------------- /src/x86_64/assets/kernel_higher_lower_half.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 25 | 31 | 37 | 43 | 49 | 55 | 61 | 67 | 68 | 88 | 90 | 91 | 93 | image/svg+xml 94 | 96 | 97 | 98 | 99 | 100 | 104 | 113 | 122 | Code du kernel 135 | Memoire (kernel higher half) 147 | C 154 | 163 | 172 | Code du kernel 185 | 191 | 0xffffffff80001000 201 | 0x1000 211 | 220 | 229 | Code ou mémoire des applications utilisatrices 242 | Code ou mémoire des applications utilisatrices 255 | Memoire (kernel lower half) 267 | 268 | 269 | -------------------------------------------------------------------------------- /src/x86_64/assets/tutoriel-hello-world-result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devse-org/wiki/5dacf9604764feb6232f6e80bd147e981ce6b903/src/x86_64/assets/tutoriel-hello-world-result.png -------------------------------------------------------------------------------- /src/x86_64/assets/tutoriel-hello-world-stivale2-linked-list.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/x86_64/exceptions.md: -------------------------------------------------------------------------------- 1 | # Codes d'erreur d'interruption 2 | 3 | Toutes les interruptions entre 0 et 32 sont des interruptions d'erreur. 4 | 5 | Certains codes d'erreur peuvent être corrigés après le retour de l'interruption. 6 | Cependant, d'autres ne peuvent pas l'être. 7 | 8 | | Id | Nom | contient un code d'erreur ? | Descriptions | 9 | | ------- | -------------------------------------- | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 10 | | 0 | Division par 0 | non | Cette erreur est produite quand l'instruction DIV/IDIV est utilisée avec un 0 | 11 | | 1 | Debug | non | Cette erreur __intentionnelle__ est généralement utilisée pour déboguer | 12 | | 2 | Interruption NMI | non | L'interruption NMI est une interruption causée par des éléments externes comme la RAM | 13 | | 3 | Breakpoint | non | Cette erreur __intentionnelle__ est généralement utilisée pour le débogage | 14 | | 4 | Dépassement | non | L'interruption 4 est causée lorsque l'instruction `INTO` est éxécuté alors que le bit 11 de RFLAGS est mis à 1.
Note : l'erreur n'est pas possible en 64 bits car l'instruction `INTO` n'est pas disponible en mode `long`. | 15 | | 5 | Dépassement de table | non | L'interruption 5 est causée lorsque l'instruction `BOUND` est exécutée quand opérateur 1 n'est pas dans la taille de table définie dans l'opérateur 2.
Note : l'erreur n'est pas possible en 64 bits car l'instruction `BOUND` n'est pas disponible en mode `long`. | 16 | | 6 | Instruction non valide | non | L'interruption 6 est causée lorsque :
- On essaye d'accéder à un registre non existant
- On essaye d'exécuter une instruction non disponible
- UD est exécuté | 17 | | 7 | Appareil non disponible | non | L'interruption 7 est appelée lorsqu'on essaye d'initialiser le FPU alors qu'il n'existe pas | 18 | | 8 | Faute Double | oui | La faute double est appelée lorsqu'il y a une erreur pendant que l'interruption d'erreur est appelée (une erreur dans une erreur) | 19 | | 9 | Erreur de Segment de coprocesseur | non | Cette erreur n'est plus utilisée. | 20 | | 10 | TSS invalide | oui (code d'erreur de segment) | L'interruption TSS invalide est exécutée lorsque le sélecteur de segment pour la TSS est invalide.
Causée pendant un changement de tâche ou pendant l'accès de la TSS | 21 | | 11 | Segment non présent | oui (code d'erreur de segment) | L'interruption "Segment non présent" est exécutée lorsqu'on essaye de charger un segment qui a son bit présent à 0 | 22 | | 12 | Segment de pile invalide | oui (code d'erreur de segment) | L'interruption "Segment de pile" invalide est causée lorsque :
- On charge un segment de pile qui n'est pas présent
- La vérification de la limite de pile n'est pas possible
- (64bit) On essaye de faire une opération qui fait une référence à la mémoire en utilisant le pointeur de pile (RSP) qui contient une adresse mémoire non canonique
- Le segment de pile n'est pas présent pendant une opération qui fait référence au registre `SS`, (comme `pop`, `push`, `iret` ...) | 23 | | 13 | Faute générale de protection | oui (code d'erreur de segment) | L'interruption n°13 peut être causée par beaucoup de raisons, comme :
- L'écriture d'un 1 dans une zone du registre CR4 réservée
- L'utilisation une instruction SSE qui essaye d'accéder une zone de la mémoire 128 bits qui n'est pas alignée en 16bit
- Une pile de mémoire non alignée en 16bit \[...].

Voir le manuel Intel pour plus d'informations (chap 3 6.15.13) | 24 | | 14 | Faute de page | oui (code d'erreur de page) | L'interruption n°14 peut être causée lorsque :
- Il y a une erreur en relation avec le paging
- On essaye d'accéder à une zone de la mémoire qui n'a pas de table présente
- On essaye de charger une table et que la zone ou on éxécute le code n'est pas exécutable dans la page
- Un problème d'autorisation est causé (ex: écrire dans une zone de la mémoire qui ne peut pas être écrite) \[...]

Voir le manuel Intel pour plus d'informations (chap 3 6.15.14) | 25 | | 15 | Réservé | non | // | 26 | | 16 | Faute du FPU x87 | non | L'interruption n°16 est causée lorsqu'il y a une erreur pendant une instruction du FPU, une opération invalide, une division par 0, un dépassement numérique, un résultat non exact, ou lorsque le bit 5 du registre CR0 = 1 | 27 | | 17 | Faute d'alignement | oui | Produite lorsque le bit 18 de CR0 et `RFLAGS` sont égaux à 1. L'erreur est causée lorsqu'une référence de mémoire est non alignée. | 28 | | 18 | Faute de vérification de machine | non | Produite lorsque le bit 6 du CR4 est égal à 1. L'erreur est causée lorsque le CPU détecte une erreur de machine, comme un problème de bus, cache, mémoire, ou une erreur interne. | 29 | | 19 | Exception de variable a virgule `SIMD` | non | L'interruption n°19 est appelé lorsqu'il y a une erreur avec les nombres à virgule pendant une opération `SSE` : division par 0, dépassement numérique, résultat non exact \[...] | 30 | | 20 | Exception de virtualisation | non | L'exception de virtualisation est appelée lorsqu'il y a une violation de droits avec une instruction `EPT` | 31 | | 21 à 31 | réservé | non | // | 32 | | /// | Faute triple | non | L'exception faute triple est exécutée lorsqu'il y a une erreur pendant l'interruption de faute double (une erreur dans une erreur dans une erreur). L'interruption faute triple cause un redémarrage de la machine. | 33 | 34 | 35 | ## Codes d'erreur d'une faute de page 36 | 37 | | BIT | NOM | DESCRIPTION | 38 | | ---- | --- | --------------------------------------------------------------------------- | 39 | | 0 | P | (p=1)
Violation de protection

(p=0)
la page n'est pas présente | 40 | | 1 | W | (W=0)
Causée par une lecture

(W=1)
Causée par une écriture | 41 | | 2 | U | (U=1)
La page n'est pas utilisateur alors que CPL = 3 | 42 | | 3 | R | (R=1)
La page contient un bit réservé | 43 | | 4 | I | (I=1)
Lecture à cause d'une instruction | 44 | | 5 | PK | (PK=1)
Violation de droit de clé | 45 | | 6 | SS | (SS=1)
Accès à "l'ombre de la pile" | 46 | | 7-31 | // | Réservé | 47 | 48 | Lors d'une faute de page, l'addresse qui a causé l'exception est stockée dans `CR2`. 49 | 50 | ## Codes d'erreur d'une faute générale de protection 51 | 52 | | BIT | NOM | TAILLE | DESCRIPTION | 53 | | --- | ----- | ------ | -------------------------------------------------------------------------------------------------------------------- | 54 | | 0 | E | 1 | (E=1)
Provient d'un appareil externe au processeur | 55 | | 1 | TBL | 2 | (TBL=0)
Provient de la `GDT`

(TBL=1)
Provient de l'`IDT`

(TBL=2 & TBL=3)
Provient de la `LDT` | 56 | | 3 | Index | 13 | Index de la table sélectionnée dans TBL | 57 | -------------------------------------------------------------------------------- /src/x86_64/index.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devse-org/wiki/5dacf9604764feb6232f6e80bd147e981ce6b903/src/x86_64/index.md -------------------------------------------------------------------------------- /src/x86_64/premiers-pas/00-introduction.md: -------------------------------------------------------------------------------- 1 | # 0 - Introduction 2 | 3 | ## Préface 4 | 5 | Ce tutoriel vous expliquera les __bases__ du fonctionnement d'un système d'exploitation par la réalisation pas à pas d'un kernel minimaliste. 6 | 7 | ⚠️ Pour suivre ce tutoriel, il vous est recommandé d'utiliser un système UNIX-like tel que GNU/Linux. Bien que vous puissiez utiliser Windows, cela demande un peu plus de travail et nous n'aborderons pas les étapes nécessaires à l'installation d'un environnement de développement sous Windows. 8 | 9 | Avant de se lancer, il faut garder en tête que le développement de système d'exploitation est très long. Il faut donc être conscient qu'il ne s'agit pas d'un petit projet de quelques jours. Beaucoup de systèmes d'exploitation sont abandonnés faute de motivation dans la durée. Aussi, n'ayez pas les yeux plus gros que le ventre: vous n'inventerez pas le nouveau Windows ou OS X. 10 | 11 | Pour pouvoir mener à bien ce type de projet il faut déjà posséder des bases en programmation, mais pas besoin d'être un expert avec 30 ans d'expérience en C, rassurez vous. 12 | 13 | Une erreur commune est de se lancer dans de gros projet tels qu'un MMORPG ou dans le cas présent un kernel sans connaître la programmation. 14 | 15 | Bien que dans ce tutoriel nous utiliserons assez peu l'assembleur, en connaître les bases est un sérieux plus. 16 | 17 | Bref. Vous l'aurez compris. Ne vous lancez pas dans un tel projet si vous n'avez pas un minimum de bases (n'essayez pas d'apprendre sur le tas, prenez du recul, apprennez à programmer et revennez). 18 | 19 | Aussi, gardez en tête que vous ne pouvez pas programmer un système d'exploitation dans n'importe quel langage et la majorité des ressources que vous trouverez sur le net tournent autours du C, C++ et peut-être du Rust. 20 | 21 | Il est important que vous preniez le temps de bien lire les explications plutôt de vous jeter directement sur le code et faire de bêtes copier/coller. Si vous ne comprennez pas du premier coup, ce n'est pas grave, pensez à faire vos propres recherches et à relire plus tard à tête reposée. 22 | 23 | ## Introduction 24 | 25 | ### Qu'est ce qu'un kernel (ou noyau) ? 26 | 27 | Le kernel est l'élément central d'un système d'exploitation, il est chargé par le bootloader. 28 | 29 | Le kernel a plusieurs responsabilités comme celle de gérer la mémoire, le multitâche, etc. Il existe plusieurs types de noyaux qui changent grandement la manière d'aborder les systèmes d'exploitation. 30 | 31 | La conception du kernel et ses responsabilités changent en fonction du type de [kernel](types-de-kernel.md) et du point de vue de l'auteur. 32 | 33 | ### Qu'est ce qu'un bootloader ? 34 | 35 | Un bootloader un programme permettant de démarrer votre kernel. 36 | 37 | Un bootloader peut aussi charger des éléments important pour le kernel, comme des modules présents sur le disque, l'A20, etc. 38 | 39 | Dans ce tutoriel nous utiliserons [Limine](https://github.com/limine-bootloader/limine). 40 | 41 | ### L'architecture 42 | 43 | L'architecture c'est la façon dont un processeur est structuré, sa façon de fonctionner, son [ISA](https://en.wikipedia.org/wiki/Instruction_set_architecture). 44 | Il y a plusieurs architectures et un kernel peut en supporter plusieurs en même temps : 45 | 46 | - x86 47 | - RISC-V 48 | - ARM 49 | - PowerPC 50 | - Et bien d'autres... 51 | 52 | L'architecture est importante, ici nous prenons le x86 car c'est l'architecture la plus utilisée. 53 | 54 | Le x86 est divisé en *modes* : 55 | 56 | | nom anglais | nom français | taille de registre | 57 | | -------------- | ------------ | ------------------ | 58 | | real mode | mode réel | 16/20 bit | 59 | | protected mode | mode protégé | 32bit | 60 | | long mode | mode long | 64bit | 61 | 62 | Nous utiliserons ici le mode long, car il est le plus récent, même si il a moins de documentation que le mode protégé. 63 | 64 | ### Comment coder un kernel ? 65 | 66 | On peut prendre la route qu'on veut, mais il y a des éléments importants qu'il faut faire dans un ordre assez précis. 67 | 68 | Vous pouvez dans certains cas le faire dans l'ordre que vous voulez, mais il faut quand même une route... car parfois on se pose la question : "Que faire ensuite ?". 69 | 70 | La route ci-dessous est recommandée mais vous pouvez le faire de la manière dont vous l'entendez: 71 | 72 | - démarrage 73 | - COM (ou serial) pour le debugging 74 | - GDT (Global Descriptor Table) utilisée à l'époque pour la [segmentation de la mémoire](https://fr.wikipedia.org/wiki/Segmentation_(informatique)) 75 | - IDT (Interrupt Descriptor Table) utilisée pour gérer les [interruptions](https://fr.wikipedia.org/wiki/Interruption_(informatique)) 76 | - Les interruptions pour le debugging d'erreur 77 | - PIT 78 | - Gestion de mémoire physique 79 | - Pagination 80 | - Multitâche 81 | 82 | À partir d'ici, tout devient très subjectif; vous pouvez enchaîner sur le SMP, le système de fichiers, les tâches utilisateur, etc. 83 | 84 | ## Références 85 | 86 | - [wikipedia ISA](https://en.wikipedia.org/wiki/Instruction_set_architecture) 87 | - [wikipedia interruptions](https://fr.wikipedia.org/wiki/Interruption_(informatique)) 88 | - [wikipedia segmentation de la mémoire](https://fr.wikipedia.org/wiki/Segmentation_(informatique)) 89 | -------------------------------------------------------------------------------- /src/x86_64/premiers-pas/01-hello-world.md: -------------------------------------------------------------------------------- 1 | # 01 - Hello world 2 | 3 | 4 | 5 | > résultat à la fin de ce tutoriel 6 | 7 | Dans cette partie vous allez faire un "hello world !" en 64bit. 8 | 9 | Pour ce projet vous utiliserez donc : 10 | 11 | - [un cross compilateur](/cross-compilation/creer-un-cross-compiler.md) 12 | - [Limine](https://github.com/limine-bootloader/limine) comme bootloader 13 | - [Echfs](https://github.com/echfs/echfs) comme système de fichier 14 | 15 | Pour commencer vous devez mettre un place un [cross compilateur](/cross-compilation/creer-un-cross-compiler.md) dans votre projet. 16 | 17 | Vous utiliserez echfs comme système de fichier il est assez simple d'utilisation pour les débutants, normalement sans echfs, il faut créer un disque, le partitionner, le monter, installer un système de fichier, ajouter nos fichier... En utilisant echfs avec son outil `echfs-utils`, c'est bien plus simple. 18 | 19 | Vous devez donc cloner limine dans la source de votre projet (ou en le rajoutant en sous module git), il est fortement recommandé d'utiliser la [branche qui contient les binaires](https://github.com/limine-bootloader/limine/tree/latest-binary). 20 | 21 | ## Le Fichier Makefile 22 | 23 | > Note: vous pouvez utiliser d'autres système de build, il faut juste suivre les même commandes et arguments pour gcc/ld. 24 | 25 | ### Compilation 26 | 27 | Pour commencer vous devez obtenir tout les fichier '.c' avec find et obtenir le fichier objet '.o' équivalent à ce fichier c. 28 | 29 | > Ici le dossier "src" est là où vous mettez le code de votre kernel. 30 | 31 | ```makefile 32 | SRCS := $(wildcard ./src/**.c) 33 | OBJS := $(SRCS:.c=.o) 34 | ``` 35 | 36 | Ensuite, juste avant de compiler les fichiers `.c`, il faut changer certains flags du compilateur: 37 | 38 | - `-ffreestanding`: Active l'environnement freestanding, cela signifie que le compilateur désactive les librairies standards du C (faites pour GNU/linux). Il signifie aussi que les programmes ne commencent pas forcément à `main`. 39 | - `-O1`: Vous pouvez utiliser -O2 ou même -O3 même si rarement le compilateur peut retirer des bouts de code qui ne devraient pas être retiré. 40 | - `-m64`: Active le 64bit. 41 | - `-mno-red-zone`: Désactive la red-zone (en mode 64bit). 42 | - `-mno-sse`: Désactive l'utilisation de l'sse. 43 | - `-mno-avx`: Désactive l'utilisation de l'avx. 44 | - `-fno-stack-protector`: Désactive la protection de la stack. 45 | - `-fno-pic`: produit un code qui n'est pas '*indépendant de la position*'. 46 | - `-no-pie`: Ne produit pas un executable avec une position indépendante. 47 | - `-masm=intel`: Utilise l'asm intel pour la génération de code. 48 | 49 | ```makefile 50 | CFLAGS := \ 51 | -Isrc \ 52 | -std=c11 \ 53 | -ffreestanding \ 54 | -fno-stack-protector \ 55 | -fno-pic \ 56 | -no-pie \ 57 | -O1 \ 58 | -m64 \ 59 | -g \ 60 | -masm=intel \ 61 | -mno-red-zone \ 62 | -mno-sse \ 63 | -mno-avx 64 | ``` 65 | 66 | Maintenant vous pouvez rajouter une target a votre makefile pour compiler vos fichier C en objet: 67 | 68 | > Ici, vous utiliserez la variable make CC qui aura le path de votre cross-compilateur. 69 | 70 | ```makefile 71 | .SUFFIXE: .c 72 | .o: $(SRCS) 73 | $(CC) $(CFLAGS) -c $< -o $@ 74 | ``` 75 | 76 | ### Linking 77 | 78 | Après avoir compilé tout les fichier C en fichier objet, vous devez les lier pour créer le fichier du kernel. 79 | 80 | Vous utiliserez `ld` (celui fourni par le binutils de votre cross-compilateur). 81 | 82 | Avant il vous faut un fichier de linking, qui définit la position de certaines parties du code. Vous le mettrez dans le chemins `src/link.ld`. 83 | 84 | Il faut commencer par définir le point d'entrée, où commence le code... Ici la fonction: `kernel_start` pour commencer, donc : 85 | 86 | ```ld 87 | ENTRY(kernel_start) 88 | ``` 89 | 90 | Il faut ensuite définir la position des sections du code (pour les données (data/rodata/bss) et le code (text)), soit la position 0xffffffff80100000. Étant donné que c'est un kernel "higher-half", il est donc placé dans la moitié haute de la mémoire : 0xffffffff80000000. Ici, vous rajoutez un décalage de 1M (0x100000) pour éviter de toucher l'adresse 0 en physique. 91 | 92 | Vous devez aussi positionner le header pour le bootloader (ici dans la section `stivale2hdr`), il permet de donner des informations importantes quand le bootloader lit le kernel. Le bootloader demande à cette section d'être la première dans le kernel. 93 | 94 | Pour finir vous devez avoir : 95 | 96 | ```ld 97 | ENTRY(kernel_start) 98 | 99 | SECTIONS 100 | { 101 | kernel_phys_offset = 0xffffffff80100000; 102 | . = kernel_phys_offset; 103 | 104 | .stivale2hdr ALIGN(4K): 105 | { 106 | KEEP(*(.stivale2hdr)) 107 | } 108 | 109 | .text ALIGN(4K): 110 | { 111 | *(.text*) 112 | } 113 | 114 | .rodata ALIGN(4K): 115 | { 116 | *(.rodata*) 117 | } 118 | 119 | .data ALIGN(4K): 120 | { 121 | *(.data*) 122 | } 123 | 124 | .bss ALIGN(4K) : 125 | { 126 | *(COMMON) 127 | *(.bss*) 128 | } 129 | } 130 | ``` 131 | 132 | Comme pour la compilation des fichiers C, vous devez passer des arguments spécifiques : 133 | 134 | - `-z max-page-size=0x1000`: Signifie que la taille max d'une page ne peut pas dépasser `0x1000` (4096). 135 | - `-nostdlib` Demande à ne pas utiliser la librairie standard. 136 | - `-T{CHEMIN_DU_FICHIER_DE_LINKING}`: Demande à utiliser le fichier de linking. 137 | 138 | Donc ici : 139 | 140 | ```makefile 141 | LD_FLAGS := \ 142 | -nostdlib \ 143 | -Tsrc/link.ld \ 144 | -z max-page-size=0x1000 145 | 146 | ``` 147 | 148 | En utilisant une nouvelle target dans le fichier Makefile, vous pouvez désormais lier les fichiers objets en un kernel.elf : 149 | 150 | ```makefile 151 | kernel.elf: $(OBJS) 152 | $(LD) $(LD_FLAGS) $(OBJS) -o $@ 153 | ``` 154 | 155 | ### Création Du Fichier De Configuration Du Bootloader 156 | 157 | Avant de continuer, vous devez créer un fichier `limine.cfg`. C'est un fichier lu par le bootloader qui paramètre certaines options et permet de pointer où se trouve le kernel dans le disque : 158 | 159 | ```s 160 | :mykernel 161 | PROTOCOL=stivale2 162 | KERNEL_PATH=boot:///kernel.elf 163 | ``` 164 | 165 | Ici vous voulez définir l'entrée `mykernel` qui a le protocole `stivale2` et qui a comme fichier elf pour le kernel: `/kernel.elf` dans la partition de `boot`. 166 | 167 | Ensuite, vous pouvez mettre en place la création du disque: 168 | 169 | ### Création Du Disque 170 | 171 | Pour commencer il faut créer un path pour le disk, (ici `disk.hdd`). 172 | 173 | ```makefile 174 | KERNEL_DISK := disk.hdd 175 | ``` 176 | 177 | Ensuite dans la target de création du disque du makefile: 178 | Vous créez un fichier disk.hdd vide de taille 8M (avec `dd`). 179 | 180 | ```makefile 181 | dd if=/dev/zero bs=8M count=0 seek=64 of=$(KERNEL_DISK) 182 | ``` 183 | 184 | Vous formatez le disque pour utiliser un système de partition `MBR` avec 1 seule partition (qui prend tout le disque). 185 | 186 | ```makefile 187 | parted -s $(KERNEL_DISK) mklabel msdos 188 | parted -s $(KERNEL_DISK) mkpart primary 1 100% 189 | ``` 190 | 191 | Vous utilisez echfs-utils pour formater la partition en echfs et pour rajouter le fichier kernel, le fichier config pour limine, et un fichier système pour limine (`limine.sys`). 192 | 193 | ```makefile 194 | echfs-utils -m -p0 $(KERNEL_DISK) quick-format 4096 # taille de block de 4096 195 | echfs-utils -m -p0 $(KERNEL_DISK) import kernel.elf kernel.elf 196 | echfs-utils -m -p0 $(KERNEL_DISK) import limine.cfg limine.cfg 197 | echfs-utils -m -p0 $(KERNEL_DISK) import ./limine/limine.sys limine.sys 198 | ``` 199 | 200 | Puis vous installez limine sur la partition echfs: 201 | 202 | ```makefile 203 | ./limine/limine-install-linux-x86_64 $(KERNEL_DISK) 204 | ``` 205 | 206 | Ce qui donne comme résultat: 207 | 208 | ```makefile 209 | $(KERNEL_DISK): kernel.elf 210 | rm -f $(KERNEL_DISK) 211 | dd if=/dev/zero bs=8M count=0 seek=64 of=$(KERNEL_DISK) 212 | parted -s $(KERNEL_DISK) mklabel msdos 213 | parted -s $(KERNEL_DISK) mkpart primary 1 100% 214 | echfs-utils -g -p0 $(KERNEL_DISK) quick-format 4096 215 | echfs-utils -g -p0 $(KERNEL_DISK) import kernel.elf kernel.elf 216 | echfs-utils -g -p0 $(KERNEL_DISK) import limine.cfg limine.cfg 217 | echfs-utils -m -p0 $(KERNEL_DISK) import ./limine/limine.sys limine.sys 218 | ./limine/limine-install-linux-x86_64 $(KERNEL_DISK) 219 | ``` 220 | 221 | ### L'Execution 222 | 223 | Une fois le disque créé, vous allez faire une cible : `run`. Elle servira plus tard quand vous pourrez enfin tester votre kernel. 224 | 225 | Elle est assez simple: vous lançez qemu-system-x86_64, avec une mémoire de `512M`, on active `kvm` (une accélération pour l'émulation), on utilise le disque `disk.hdd`, et des options de debug, comme : 226 | 227 | - `-serial stdio`: Redirige la sortie de qemu dans `stdio` . 228 | - `-d cpu_reset`: Signale dans la console quand le cpu se réinitialise après une erreur. 229 | - `-device pvpanic`: signale quand il y a des évenements de panic. 230 | - `-s`: Permet de debug avec gdb. 231 | 232 | ```makefile 233 | run: $(KERNEL_DISK) 234 | qemu-system-x86_64 -m 512M -s -device pvpanic -serial stdio -enable-kvm -d cpu_reset -hda ./disk.hdd 235 | ``` 236 | 237 | ## Le Code 238 | 239 | Après avoir tout configuré avec le makefile, vous pouvez commencer à coder ! 240 | 241 | Vous commencerez par créer un fichier kernel.c dans le dossier src (le nom du fichier n'est pas obligé d'être kernel.c). 242 | 243 | Mais avant vous devez rajouter le header du bootloader, qui permet de donner des informations/configurer le bootloader quand il charge le kernel, ici nous utilisons le protocole stivale 2, nous recommandons d'utiliser [le code/header fournis par stivale2](https://github.com/stivale/stivale/blob/master/stivale2.h) qui facilite la création du header. 244 | 245 | Vous allez créer une variable dans le `kernel.c` du type `stivale2_header`, vous demandez au linker de la positioner dans la section "`.stivale2hdr`" et de forcer le fait qu'elle soit utilisée (pour éviter que le compilateur vire l'entrée automatiquement). 246 | 247 | ```c 248 | __attribute__((section(".stivale2hdr"), used)) 249 | struct stivale2_header header = { /* entrées */ }; 250 | ``` 251 | 252 | Puis vous remplissez toutes les entrées du header: 253 | Il faut commencer par créer une variable pour définir la [stack](https://fr.wikipedia.org/wiki/Pile_(informatique)) du kernel. Vous utiliserez une stack de taille 32768 (32K) soit : 254 | 255 | ```c 256 | #define STACK_SIZE 32768 257 | char kernel_stack[STACK_SIZE]; 258 | ``` 259 | 260 | Et : 261 | 262 | ```c 263 | struct stivale2_header header = {.stack = (uintptr_t)kernel_stack + (STACK_SIZE) }// la stack tend vers le bas, donc vous voulez donner le dessus de cette stack 264 | ``` 265 | 266 | Le header doit spécifier le point d'entrée du kernel par la variable `entry_point`, il faut le mettre à 0 pour demander au bootloader d'utiliser le point d'entrée spécifié par le fichier elf. 267 | 268 | La spécification de stivale2 demande **pour l'instant** à mettre `flags` à 0 car il n'y a aucun flag implémenté. 269 | 270 | ```c 271 | __attribute__((section(".stivale2hdr"), used)) 272 | static struct stivale2_header stivale_hdr = { 273 | .stack = (uintptr_t)kernel_stack + STACK_SIZE, 274 | .entry_point = 0, 275 | .flags = 0, 276 | }; 277 | ``` 278 | 279 | Maintenant il faut mettre en place des tags pour le bootloader, les tags sont une liste liée, c'est à dire que chaque entrée doit indiquer où est la prochaine entrée : 280 | 281 | 282 | 283 | Il y a plusieurs valeurs valides pour l'`identifier` qui identifie l'entrée et vous pouvez avoir plusieurs tags. Pour l'instant vous allez en utiliser qu'un seul : celui pour définir le framebuffer. 284 | 285 | Il faut créer une nouvelle variable statique qui contient le premier (*et le seul pour l'instant* )tag de la liste qui aura comme type `stivale2_header_tag_framebuffer` : 286 | 287 | ```c 288 | static struct stivale2_header_tag_framebuffer framebuffer_header_tag = 289 | { 290 | .tag = 291 | { 292 | }, 293 | }; 294 | ``` 295 | 296 | Ici, la valeur de la variable `.tag.identifier` doit être `STIVALE2_HEADER_TAG_FRAMEBUFFER_ID`. Cela signifie que ce tag donne des informations au bootloader à propos du framebuffer (taille en largeur/hauter, ...). 297 | 298 | La variable `.tag.next` est à `0` pour le moment, car vous utilisez qu'une seule entrée dans la liste. 299 | 300 | Ce qui donne: 301 | 302 | ```c 303 | static struct stivale2_header_tag_framebuffer framebuffer_header_tag = 304 | { 305 | .tag = 306 | { 307 | .identifier = STIVALE2_HEADER_TAG_FRAMEBUFFER_ID, 308 | .next = 0 // fin de la liste 309 | }, 310 | }; 311 | ``` 312 | 313 | Maintenant vous allez configurer le [framebuffer](/x86_64/périphériques/framebuffer.md). Pour le moment, vous voulez le mettre en pixel et non en texte : car vous allez essayez de remplir l'écran en bleu. 314 | Vous devez définir la longueur et largeur du framebuffer (ici vous utiliserez une résolution de: `1440`x`900`) et 32 bit par pixel (donc ̀`framebuffer_bpp=32`). 315 | 316 | ```c 317 | static struct stivale2_header_tag_framebuffer framebuffer_header_tag = 318 | { 319 | .tag = 320 | { 321 | .identifier = STIVALE2_HEADER_TAG_FRAMEBUFFER_ID, 322 | .next = 0 // fin de la liste 323 | }, 324 | .framebuffer_width = 1440, 325 | .framebuffer_height = 900, 326 | .framebuffer_bpp = 32 327 | }; 328 | ``` 329 | 330 | Ensuite, initialisez variable `tags` du `stivale2_header` à l'adresse du tag du framebuffer soit : 331 | 332 | ```c 333 | __attribute__((section(".stivale2hdr"), used)) 334 | static struct stivale2_header stivale_hdr = 335 | { 336 | .stack = (uintptr_t)kernel_stack + STACK_SIZE, 337 | .entry_point = 0, 338 | .flags = 0, 339 | .tags = (uintptr_t)&framebuffer_header_tag 340 | }; 341 | ``` 342 | 343 | Pour finir vous devriez avoir ceci : 344 | 345 | ```c 346 | #define STACK_SIZE 32768 347 | char kernel_stack[STACK_SIZE]; 348 | 349 | static struct stivale2_header_tag_framebuffer framebuffer_header_tag = 350 | { 351 | .tag = { 352 | .identifier = STIVALE2_HEADER_TAG_FRAMEBUFFER_ID, 353 | .next = 0 // fin de la liste 354 | }, 355 | .framebuffer_width = 1440, 356 | .framebuffer_height = 900, 357 | .framebuffer_bpp = 32 358 | }; 359 | 360 | __attribute__((section(".stivale2hdr"), used)) 361 | static struct stivale2_header stivale_hdr = { 362 | .stack = (uintptr_t)kernel_stack + STACK_SIZE, 363 | .entry_point = 0, 364 | .flags = 0, 365 | .tags = (uintptr_t)&framebuffer_header_tag 366 | }; 367 | ``` 368 | 369 | ### L'Entrée 370 | 371 | Après la mise en place du header pour le bootloader vous devez programmer le point d'entrée, `kernel_start`, c'est une fonction qui ne retourne rien mais qui a un `struct stivale2_struct*` comme argument. Cet argument (ici bootloader_data) représente les informations passées par le bootloader. 372 | 373 | ```c 374 | void kernel_start(struct stivale2_struct *bootloader_data) 375 | { 376 | while(1); // vous ne voulez pas sortir de kernel_start 377 | } 378 | ``` 379 | 380 | Maintenant il est conseillé de compiler et de tester le kernel, avant de continuer. Faites un `make run`, il faut qu'il n'y ait aucune erreur ; ni du bootloader, ni de Qemu. 381 | 382 | ### Lire Le Bootloader_data 383 | 384 | Il est important avant de continuer de mettre en place quelques fonctions utilitaires qui permettent de lire le `bootloader_data` car il doit être lu comme une liste lié (comme le header stivale2). Par exemple si on veut obtenir l'entrée qui contient des informations à propos du framebuffer, vous devez regarder toutes les entrées et trouver celle qui a un identifiant pareil à celle du framebuffer. 385 | 386 | ```c 387 | void *stivale2_find_tag(struct stivale2_struct *bootloader_data, uint64_t tag_id) 388 | { 389 | struct stivale2_tag *current = (void *)bootloader_data->tags; 390 | while(current != NULL) 391 | { 392 | if (current->identifier == tag_id) // est ce que cette entrée est bien celle que l'on cherche ? 393 | { 394 | return current; 395 | } 396 | 397 | current = (void *)current->next; // avance d'une entrée dans la liste 398 | } 399 | return NULL; // aucune entrée trouvé 400 | } 401 | ``` 402 | 403 | Ce qui permettra plus tard d'obtenir le tag contenant des informations à propos du framebuffer comme ceci: 404 | 405 | ```c 406 | stivale2_find_tag(bootloader_data, STIVALE2_STRUCT_TAG_FRAMEBUFFER_ID); 407 | ``` 408 | 409 | ## Le Framebuffer 410 | 411 | Vous allez remplir l'écran en bleu pour essayer de debug, le framebuffer est structuré comme ceci: 412 | 413 | ```c 414 | struct framebuffer_pixel 415 | { 416 | uint8_t blue; 417 | uint8_t green; 418 | uint8_t red; 419 | uint8_t __unused; 420 | } __attribute__((packed)); 421 | ``` 422 | 423 | > voir: [framebuffer](/x86_64/périphériques/framebuffer.md) pour plus d'information 424 | 425 | Vous rajoutez ensuite dans kernel_start du code pour remplir le framebuffer en bleu. 426 | 427 | Pour commencer il faut obtenir le tag du framebuffer, il est passé dans le tag `STIVALE2_STRUCT_TAG_FRAMEBUFFER_ID` du `bootloader_data` 428 | 429 | Il faut utiliser `stivale2_find_tag`: 430 | 431 | ```c 432 | struct stivale2_struct_tag_framebuffer *framebuffer_tag; 433 | framebuffer_tag = stivale2_find_tag(bootloader_data, STIVALE2_STRUCT_TAG_FRAMEBUFFER_ID); 434 | ``` 435 | 436 | Maintenant le tag contient la taille du framebuffer, et son adresse. 437 | 438 | Pour utiliser l'adresse il faut la convertir en un pointeur `framebuffer_pixel`: 439 | 440 | ```c 441 | struct framebuffer_pixel* framebuffer = framebuffer_tag->framebuffer_addr; 442 | ``` 443 | 444 | Nous avons une table qui contient chaque pixel de `framebuffer_tag->framebuffer_width` de longueur et de `framebuffer_tag->framebuffer_height` de hauteur, donc vous allez faire une boucle : 445 | 446 | ```c 447 | for(size_t x = 0; x < framebuffer_tag->framebuffer_width; x++) 448 | { 449 | for(size_t y = 0; y < framebuffer_tag->framebuffer_height; y++) 450 | { 451 | size_t raw_position = x + y*framebuffer_tag->framebuffer_width; // convertit les valeurs x et y en position 'brute' dans la table 452 | framebuffer[raw_position].blue = 255; // met la couleur à bleu 453 | } 454 | } 455 | ``` 456 | 457 | Si vous le voulez vous pouvez faire quelque chose de plus compliqué : 458 | 459 | ```c 460 | for(size_t x = 0; x < framebuffer_tag->framebuffer_width; x++) 461 | { 462 | for(size_t y = 0; y < framebuffer_tag->framebuffer_height; y++) 463 | { 464 | size_t raw_position = x + y * framebuffer_tag->framebuffer_width; 465 | 466 | framebuffer[raw_position].blue = x ^ y; 467 | framebuffer[raw_position].red = (y * 2) ^ (x * 2); 468 | framebuffer[raw_position].green = (y * 4) ^ (x * 4); 469 | } 470 | } 471 | ``` 472 | 473 | Qui donneras ce motif si tout fonctionne: 474 | 475 | 476 | ## Conclusion 477 | Cette partie du tutoriel est terminée ! vous avez maintenant un kernel qui boot, cependant dans le prochain tutoriel vous implémenterez un driver COM, qui donnera la possibilité d'écrire des informations dans la console, ce qui est très pratique pour debugger. 478 | 479 | ## Références 480 | 481 | - [wiki.osdev.org](https://wiki.osdev.org/Main_Page) 482 | - [wiki.osdev.org barebones](https://wiki.osdev.org/Bare_Bones) 483 | - [wiki.osdev.org stivale-barebones](https://wiki.osdev.org/Stivale) 484 | - [gnu/make documentation](https://www.gnu.org/software/make/manual/html_node/index.html) 485 | - [specification/headers de stivale](https://github.com/stivale/stivale) 486 | - [barebones limine](https://github.com/limine-bootloader/limine-barebones/tree/master/src-stivale2) 487 | - [gcc manpage](https://linux.die.net/man/1/gcc) 488 | - [ld manpage](https://linux.die.net/man/1/ld) 489 | - [qemu manpage](https://linux.die.net/man/1/qemu-kvm) 490 | - [echfs-utils information](https://github.com/echfs/echfs) 491 | - [wikipedia la stack](https://fr.wikipedia.org/wiki/Pile_(informatique)) 492 | -------------------------------------------------------------------------------- /src/x86_64/premiers-pas/02-segmentation.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devse-org/wiki/5dacf9604764feb6232f6e80bd147e981ce6b903/src/x86_64/premiers-pas/02-segmentation.md -------------------------------------------------------------------------------- /src/x86_64/premiers-pas/03-interruptions.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devse-org/wiki/5dacf9604764feb6232f6e80bd147e981ce6b903/src/x86_64/premiers-pas/03-interruptions.md -------------------------------------------------------------------------------- /src/x86_64/premiers-pas/03-interuptions.md: -------------------------------------------------------------------------------- 1 | # Interruptions 2 | -------------------------------------------------------------------------------- /src/x86_64/premiers-pas/04-Memoire.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devse-org/wiki/5dacf9604764feb6232f6e80bd147e981ce6b903/src/x86_64/premiers-pas/04-Memoire.md -------------------------------------------------------------------------------- /src/x86_64/premiers-pas/04-memoire.md: -------------------------------------------------------------------------------- 1 | # Memoire 2 | -------------------------------------------------------------------------------- /src/x86_64/premiers-pas/05-paging.md: -------------------------------------------------------------------------------- 1 | # Paging -------------------------------------------------------------------------------- /src/x86_64/premiers-pas/06-epilogue.md: -------------------------------------------------------------------------------- 1 | # Epilogue 2 | -------------------------------------------------------------------------------- /src/x86_64/premiers-pas/06-multitache.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devse-org/wiki/5dacf9604764feb6232f6e80bd147e981ce6b903/src/x86_64/premiers-pas/06-multitache.md -------------------------------------------------------------------------------- /src/x86_64/premiers-pas/06-tache-utilisateur.md: -------------------------------------------------------------------------------- 1 | # Tâches utilisateur 2 | -------------------------------------------------------------------------------- /src/x86_64/premiers-pas/07-tache-utilisateur.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devse-org/wiki/5dacf9604764feb6232f6e80bd147e981ce6b903/src/x86_64/premiers-pas/07-tache-utilisateur.md -------------------------------------------------------------------------------- /src/x86_64/premiers-pas/08-epilogue.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devse-org/wiki/5dacf9604764feb6232f6e80bd147e981ce6b903/src/x86_64/premiers-pas/08-epilogue.md -------------------------------------------------------------------------------- /src/x86_64/périphériques/APIC.md: -------------------------------------------------------------------------------- 1 | # Advanced Programmable Interrupt Controller 2 | 3 | ## Local APIC 4 | 5 | Le local apic est une entrée de la [MADT](documentation/x86_64/périphériques/MADT/), son type est 0. 6 | 7 | Le nombre d'entrées locales APIC dans la MADT équivaut au nombre de CPUs, chaque CPU a son local APIC. 8 | 9 | La structure de l'entrée du local APIC est: 10 | 11 | | offset/taille (en byte) | nom | 12 | | ----------------------- | ---------------- | 13 | | 2 / 1 | identifiant ACPI | 14 | | 3 / 1 | identifiant APIC | 15 | | 4 / 4 | flag du cpu | 16 | -------------------------------------------------------------------------------- /src/x86_64/périphériques/COM.md: -------------------------------------------------------------------------------- 1 | # Le port 2 | 3 | # Introduction 4 | 5 | Les ports COM étaient, à l'époque, couramment utilisés comme ports de communication. 6 | Même si aujourd'hui, l'USB a remplacé le port COM, il reste néanmoins très utile et toujours supporté par nos machines. 7 | 8 | Même s'ils sont obsolètes, les ports COM sont encore beaucoup utilisés pour le développement de systèmes d'exploitation. 9 | Ils sont très simples à implémenter et sont très utiles pour le débogage, car, dans presque toutes les machines virtuelles, on peut obtenir la sortie d'un port COM vers un fichier, un terminal ou autre. 10 | Ils sont aussi très utiles car on peut les initialiser très tôt et donc avoir des informations de débogage efficacement. 11 | 12 | Par exemple, les ports série peuvent envoyer des données et en recevoir, ce qui pourrait, par exemple nous permettre de faire un terminal externe en utilisant uniquement ce port. 13 | 14 | La norme RS-232 (qui a été révisée maintes et maintes fois) est une norme qui standardise les ports série. 15 | Existant depuis 1981, elle standardise les noms (COM1, COM2, COM3, etc), limite la vitesse à 19200 Baud (cela représente théoriquement un débit de 19200 bits par seconde), ce qui pourrait être largement assez pour un petit terminal. 16 | 17 | la limite étant calculée en Baud, celui-ci s'exprimant en bit/s, 1 baud correspond donc à 1 bit par seconde. 18 | La limite dépend également de la distance du raccord avec le fil, un fil long a une capacité moindre qu'un fil court. 19 | 20 | # Initialisation 21 | 22 | Chaque port a besoin d'être initialisé avant son utilisation. 23 | 24 | Pour commencer, il y a quelques valeurs constantes à connaître pour chaque port COM. 25 | 26 | | Le port Com | L'id du port | Son IRQ | 27 | | ----------- | ------------ | ------- | 28 | | COM1 | 0x3F8 | 4 | 29 | | COM2 | 0x2F8 | 3 | 30 | | COM3 | 0x3E8 | 4 | 31 | | COM4 | 0x2E8 | 3 | 32 | 33 | Puis, il y a l'offset. 34 | Chaque offset a certaines particularités. 35 | (= ID DU PORT + OFFSET) 36 | 37 | | offset | action | 38 | | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 39 | | 0 | Le port Data du COM, il est utilisé pour envoyer et recevoir des données, si le bit DLAB = 1 alors c'est pour mettre le diviseur du Baud (les bits inférieurs) | 40 | | 1 | Le port Interrupt du COM, il est utilisé pour activer les Interrupt du port, si le bit DLAB = 1 alors c'est pour mettre la valeur du diviseur (du Baud aussi mais pour les bits supérieurs) | 41 | | 2 | L'identificateur d'Interrupt ou le controleur FIFO | 42 | | 3 | le control de ligne (Le bit le plus haut est celui pour DLAB) | 43 | | 4 | Le control de Modem | 44 | | 5 | Le status de la ligne | 45 | | 6 | Le status de Modem | 46 | | 7 | Le scratch register | 47 | 48 | Pour mettre DLAB il faut mettre le port comme indiqué : 49 | `PORT + 3 = 0x80 = 128 = 0b10000000` 50 | 51 | ```c 52 | outb(COM_PORT + 3, 0x80); 53 | ``` 54 | 55 | Pour le désactiver, il faut juste remettre le bit 8 à 0. 56 | 57 | ## Les Baud 58 | 59 | Le port COM se met à jour 115200 fois par seconde. 60 | Pour controller la vitesse, il faut mettre en place un diviseur, que l'on peut utiliser en activant le DLAB. 61 | 62 | Ensuite, il faut passer la valeur par l'offset 0 (les bits inférieurs) et 1 (les bits supérieurs). 63 | 64 | Exemple permettant de mettre un diviseur de 5 (alors le port auras un 'rate' de 115200 / 5) : 65 | 66 | ```c 67 | outb(COM_PORT + 3, 0x80); // activer le DLAB 68 | outb(COM_PORT + 0, 5); // les bits les plus petits 69 | outb(COM_PORT + 1, 0); // les bits les plus hauts 70 | ``` 71 | 72 | ## La taille des données 73 | 74 | On peut mettre la taille des données envoyées au port COM par update. 75 | Celle-ci peut aller de 5 bits à 8 bits 76 | 77 | 5bits = 0 0 (0x0) 78 | 79 | 6bits = 0 1 (0x1) 80 | 81 | 7bits = 1 0 (0x2) 82 | 83 | 8bits = 1 1 (0x3) 84 | 85 | Pour définir la taille des données, vous devez l'écrire dans le port de contrôle de ligne (les bits les plus petits) avoir configuré le rate du port (et donc d'avoir activé le DLAB). 86 | 87 | ```c 88 | outb(COM_PORT + 3, 0x3); // désactiver le DLAB + mettre la taille de donnée à 8 donc un char/unsigned char en c++ 89 | ``` 90 | 91 | ## Références 92 | 93 | - [The Serial Port rel. 14, part 1/3](https://www.sci.muni.cz/docs/pc/serport.txt) 94 | -------------------------------------------------------------------------------- /src/x86_64/périphériques/PIC.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devse-org/wiki/5dacf9604764feb6232f6e80bd147e981ce6b903/src/x86_64/périphériques/PIC.md -------------------------------------------------------------------------------- /src/x86_64/périphériques/PIT.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devse-org/wiki/5dacf9604764feb6232f6e80bd147e981ce6b903/src/x86_64/périphériques/PIT.md -------------------------------------------------------------------------------- /src/x86_64/périphériques/framebuffer.md: -------------------------------------------------------------------------------- 1 | # Les framebuffer 2 | 3 | Le framebuffer est fourni par le bootloader, le bootloader doit fournir aussi la taille de ce frambuffer, en largeur et en hauteur, il fournit aussi le nombre de bit par pixel. Ces framebuffers utilisent le VGA (ou le vbe). 4 | 5 | Il y a deux *type* de framebuffers: 6 | 7 | - Les framebuffers de textes: l'écran est une grille de caractère, on peut seulement faire du texte, on le nomme aussi le vga en mode texte. 8 | - Les framebuffers de pixels: l'écran est une grille de pixels, on peut éditer pixels par pixels. 9 | 10 | C'est un moyen basique de dessiner l'écran dans un kernel, cependant pour faire certaines choses plus compliqués nous sommes obligé d'utiliser, soit un driver gpu, soit un driver de gpu virtuel (seulement utile dans une machine virtuelle, comme `qemu`). C'est donc au CPU de faire le rendu. 11 | 12 | ## Les framebuffers textes 13 | 14 | Les framebuffers de textes utilisent 16bit pour chaque caractères: 8 pour la couleur, et 8 pour le caractère: 15 | 16 | | bits | significations | 17 | | ----- | ---------------- | 18 | | 0-7 | caractère ASCII | 19 | | 8-11 | couleur du texte | 20 | | 12-15 | couleur de fond | 21 | 22 | Les couleurs sont formées comme ceci: 23 | 24 | | valeur | couleur | 25 | | ------ | :----------------------------------------------------------------------------------: | 26 | | 0 |
noir
| 27 | | 1 |
bleu
| 28 | | 2 |
vert
| 29 | | 3 |
cyan
| 30 | | 4 |
rouge
| 31 | | 5 |
magenta
| 32 | | 6 |
marron
| 33 | | 7 |
gris clair
| 34 | | 8 |
gris
| 35 | | 9 |
bleu clair
| 36 | | 10 |
vert clair
| 37 | | 11 |
cyan clair
| 38 | | 12 |
rouge clair/rose
| 39 | | 13 |
magenta clair
| 40 | | 14 |
jaune
| 41 | | 15 |
blanc
| 42 | 43 | ## Les framebuffers de pixels 44 | 45 | Les framebuffers de pixels sont généralement plus simple, cependant ici nous prenons en compte que si le nombre de bit par pixels sont à 24 ou a 32, car les autres valeurs ne sont plus utilisés. 46 | Il est plus facile d'utiliser un framebuffer de 32 bit de pixels car l'alignement est automatique, mais il utilise 33% plus de mémoire, contre celui à 24 bit par pixels qui économise de la mémoire mais l'accès aux pixels est plus compliquée. 47 | 48 | | byte | couleur | 49 | | ---- | :-------------------------------------------------------: | 50 | | 0 | valeur du bleu (0-255) | 51 | | 1 | valeur du vert (0-255) | 52 | | 2 | valeur du rouge (0-255) | 53 | | 3 | byte utilisé pour l'alignement (seulement quand bpp = 32) | 54 | 55 | 56 | 57 | 58 | L'utilisation d'un framebuffer 32bpp est plus rapide car nous pouvons utiliser le framebuffer comme une table de `uint32_t`, contre le 24bpp ou nous sommes obligé de le convertir en table de `uint8_t` pour ensuite accéder aux couleurs. 59 | -------------------------------------------------------------------------------- /src/x86_64/smp/SMP.md: -------------------------------------------------------------------------------- 1 | # Symmetric Multiprocessing 2 | 3 | ## Un peu de vocabulaire 4 | 5 | Les termes "coeurs" et "CPU" seront utilisés tout au long de ce tutoriel. Ils représentent tous deux la même entité, à savoir, une unité centrale de traitement. Vous aurez remarqué que ce groupe nominal barbare peut être littéralement traduit par "Central Processing Unit", ou CPU. 6 | 7 | Le terme "thread" désigne un fil d'instructions, exécuté en parallèle à d'autres threads ; ou, autrement dit, un flot d'instructions dont l'exécution n'interfère généralement pas avec l'exécution d'un autre flot d'instructions. 8 | 9 | ## Prérequis 10 | 11 | Dans ce tutoriel, pour implémenter le SMP, nous prenons en compte que vous avez déjà implémenté la base de votre noyau : 12 | 13 | - [IDT](/x86_64/structures/IDT.md) 14 | - [GDT](/x86_64/structures/GDT.md) 15 | - [MADT](/x86_64/acpi/MADT.md) 16 | - [APIC](/x86_64/périphériques/APIC.md) 17 | - Paging 18 | 19 | On considère aussi que la structure de votre noyau est composée de ces caractéristiques : 20 | 21 | - Une architecture higher-half 22 | - Un support du 64 bits 23 | - Un système de temporisation 24 | 25 | ## Introduction 26 | 27 | Qu'est ce que le SMP ? 28 | 29 | SMP est un sigle signifiant "Symetric Multi Processing", que l'on pourrait littéralement traduire par "Multi-traîtement symétrique". On utilise ce terme pour parler d'un système multiprocesseur, qui exploite plusieurs CPUs de façon parallèle. Un noyau qui supporte le SMP peut bénéficier d'énormes améliorations de performances. 30 | 31 | En sachant que - __généralement__ - un processeur possède 2 threads par coeur, pour un processeur de 8 coeurs il y aura 16 threads exploitables. 32 | 33 | Le SMP est différent de NUMA, les processeurs NUMA sont des processeurs dont certains de leurs coeurs n'ont pas accès à toute la mémoire. 34 | 35 | Il est utile de savoir qu'il faudra implémenter les interruptions [APIC](/x86_64/périphériques/APIC.md) pour les autres CPUs, ce qui n'est pas abordé dans ce tutoriel (pour l'instant). 36 | 37 | ## Obtenir le numéro du coeur actuel 38 | 39 | Obtenir le numero du coeur actuel est très important pour plus tard, il permet d'identifier le CPU sur lequel on travaille. 40 | 41 | Pour obtenir l'identifiant du CPU actuel on doit utiliser l'[APIC](/x86_64/périphériques/APIC.md). Le numéro du CPU est contenu dans le registre 20 de l'APIC, et il est situé du 24ème au 32ème bit, il faut donc décaler à droite la valeur lue de 24 bits. 42 | 43 | ```cpp 44 | #define LAPIC_REGISTER 20 45 | uint32_t get_current_processor_id() 46 | { 47 | return apic_read(LAPIC_REGISTER) >> 24; 48 | } 49 | ``` 50 | 51 | ## Obtenir les entrées Local APIC 52 | 53 | Voir : [LAPIC](/x86_64/périphériques/APIC.md) 54 | 55 | Pour commencer à utiliser le SMP, il faut obtenir les entrées LAPIC de la table MADT. Chaque CPU posède une entrée LAPIC. 56 | 57 | Pour connaitre le nombre total de CPUs il suffit donc de compter le nombre de LAPIC dans la MADT. 58 | 59 | Ces entrées LAPIC ont deux valeurs importantes: 60 | 61 | - __`ACPI_ID`__ : un identifiant utilisé par l'ACPI, 62 | - __`ACIC_ID`__ : un identifiant utilisé par l'APIC pendant l'initialisation. 63 | 64 | Généralement, sur les processeurs modernes, `ACPI_ID` et `APIC_ID` sont égaux, mais ce n'est pas toujours le cas. 65 | 66 | Pour utiliser les autres CPU, il faudra faire attention : le CPU principal (celui sur lequel votre kernel démarre) est aussi dans la liste. Il faut donc vérifier que le CPU que l'on souhaite utiliser est libre. Pour cela, il suffit de comparer l'identifiant du CPU actuel avec l'identifiant du CPU de l'entrée `LAPIC`. 67 | 68 | ```cpp 69 | // lapic_entry : entrée LAPIC que l'on est en train de manipuler 70 | if (get_current_processor_id() == lapic_entry.apic_id) { 71 | // On est actuellement en train de traiter le CPU principal, attention à ne pas faire planter votre kernel! 72 | } else { 73 | // Ce CPU n'est pas le CPU principal, on peut donc s'en servir librement. 74 | } 75 | ``` 76 | 77 | ## Pre-Initialisation 78 | 79 | Pour utiliser les CPUs, il faut d'abord les préparer, en particulier préparer l'IDT, la table de page, la GDT, le code d'initialisation... 80 | 81 | On place donc tout ceci de cette façon : 82 | 83 | | Entrée | Adresse | 84 | | ------------------ | ------- | 85 | | Code du trampoline | 0x1000 | 86 | | Pile | 0x570 | 87 | | GDT | 0x580 | 88 | | IDT | 0x590 | 89 | | Table de page | 0x600 | 90 | | Adresse de saut | 0x610 | 91 | 92 | Il faut savoir que tout ceci est temporaire, tout devra être remplacé plus tard. 93 | 94 | ### GDT + IDT 95 | 96 | Pour stocker la GDT et l'IDT, c'est assez simple. 97 | Il existe deux instructions en 64 bits qui sont dédiées: 98 | 99 | - `sgdt [adresse]` pour stocker la GDT à une adresse précise, 100 | - `sidt [adresse]` pour stocker l'IDT à une adresse précise. 101 | 102 | Dans notre cas on a donc: 103 | 104 | ```x86asm 105 | sgdt [0x580] ; stockage de la GDT 106 | sidt [0x590] ; stockage de l'IDT 107 | ``` 108 | 109 | ### Pile 110 | 111 | Pour initialiser la pile on doit stocker une adresse valide à l'adresse `0x570`: 112 | 113 | ```cpp 114 | POKE(570) = stack_address + stack_size; 115 | ``` 116 | 117 | ### Code du trampoline 118 | 119 | Pour le trampoline nous avons besoin d'un code écrit en assembleur, délimité par `trampoline_start` et `trampoline_end`. 120 | 121 | Le code trampoline doit être chargé à partir de l'adresse `0x1000`, ce qui donne pour la partie cpp : 122 | 123 | ```c 124 | #define TRAMPOLINE_START 0x1000 125 | 126 | // On calcule la taille du programme trampoline pour copier son contenu 127 | uint64_t trampoline_len = (uint64_t)&trampoline_end - (uint64_t)&trampoline_start; 128 | 129 | // On copie le code trampoline au bon endroit 130 | memcpy((void *)TRAMPOLINE_START, &trampoline_start, trampoline_len); 131 | ``` 132 | 133 | et dans le code assembleur, on spécifie le code trampoline avec : 134 | 135 | ```x86asm 136 | trampoline_start: 137 | ; code du trampoline 138 | trampoline_end: 139 | ``` 140 | 141 | ### Addresse de saut 142 | 143 | L'addresse de saut est l'adresse à laquelle va se rendre le CPU juste après son initialisaiton, on y met donc le programme principal. 144 | 145 | ### Table de page pour le futur CPU 146 | 147 | Pour le futur CPU on peut choisir de prende une copie de la table de page actuelle, mais attention il faut effectuer une copie, et pas simplement une référence à l'ancienne, sinon des évènements étranges peuvent avoir lieu. 148 | 149 | ## Chargement du CPU 150 | 151 | Pour initialiser le nouveau CPU, il faut demander à l'APIC de le charger. 152 | Pour ce faire, on utilise les deux registres de commande d'interuptions `ICR1` (registre `0x0300`) et `ICR2`. 153 | 154 | Pour initialiser le nouveau CPU il faut envoyer à l'APIC l'identifiant du nouveau CPU dans `ICR2` et l'interuption d'initialisation dans `ICR1` : 155 | 156 | ```cpp 157 | // On écrit l'identifiant du nouveau CPU dans ICR2, attention à bien utiliser son identifiant APIC 158 | write(icr2, (apic_id << 24)); 159 | // On envoie la demande d'initialisation 160 | write(icr1, 0x500); 161 | ``` 162 | 163 | L'initialisation peut être un peu longue, il faut donc attendre au moins 10 millisecondes avant de l'utiliser. 164 | 165 | On commence par envoyer le nouveau CPU à l'adresse trampoline, là encore à travers l'APIC. L'identifiant du CPU va encore dans `ICR2`, et l'instruction à écrire dans `ICR1` devient `0x0600 | (trampoline_addr >> 12)` : 166 | 167 | ```cpp 168 | // Chargement de l'identifiant du nouveau CPU 169 | write(icr2, (apic_id << 24)); 170 | // Chargement de l'adresse trampoline 171 | write(icr1, 0x600 | ((uint32_t)trampoline_addr / 4096)); 172 | ``` 173 | 174 | ## Le code du trampoline 175 | 176 | Pour commencer, on peut simplement utiliser le code suivant, qui envoie le caractère `a` sur le port `COM0`. 177 | Ce code est bien sûr temporaire, mais permet de vérifier que le nouveau CPU démarre correctement. 178 | 179 | ```x86asm 180 | mov al, 'a' 181 | mov dx, 0x3F8 182 | out dx, al 183 | ``` 184 | 185 | Lorsque le CPU est initialisé il est en 16 bits, il le sera donc aussi lors de l'exécution du trampoline. 186 | Il faut donc penser à modifier la configuration du CPU pour le passer en 64 bits. 187 | On aura donc 3 parties dans le trampoline : pour passer de 16 à 32 bits, puis de 32 à 64 bits et enfin le trampoline final en 64 bits : 188 | 189 | ```x86asm 190 | [16 bits] 191 | trampoline_start: 192 | 193 | trampoline_16: 194 | ;... 195 | 196 | [32 bits] 197 | trampoline_32: 198 | ;... 199 | 200 | [64 bits] 201 | trampoline_64: 202 | ;... 203 | 204 | trampoline_end: 205 | ``` 206 | 207 | ### Le code 16 bits 208 | 209 | *Note : trampoline_addr est l'addresse ou vous avez placé votre trampoline, dans ce cas, `0x1000`.* 210 | 211 | On commence par passer de 16 bits à 32 bits. 212 | Pour cela, il faut initialiser une nouvelle GDT et mettre le bit 0 du `cr0` à 1 pour activer le mode protégé : 213 | 214 | ```x86asm 215 | cli ; On désactive les interrupt, c'est important pendant le passage de 16 à 32 bits 216 | mov ax, 0x0 ; On initialise tous les registres à 0 217 | mov ds, ax 218 | mov es, ax 219 | mov fs, ax 220 | mov gs, ax 221 | mov ss, ax 222 | ``` 223 | 224 | On doit créer une GDT 32 bits pour le 32 bit, on procède donc ainsi : 225 | 226 | ```x86asm 227 | align 16 228 | gdt_32: 229 | dw gdt_32_end - gdt_32_start - 1 230 | dd gdt_32_start - trampoline_start + trampoline_addr 231 | 232 | align 16 233 | gdt_32_start: 234 | ; descripteur NULL 235 | dq 0 236 | ; descripteur de code 237 | dq 0x00CF9A000000FFFF 238 | ; descripteur de donné 239 | dq 0x00CF92000000FFFF 240 | gdt_32_end: 241 | ``` 242 | 243 | Et on doit maintenant charger cette GDT : 244 | 245 | ```x86asm 246 | lgdt [gdt_32 - trampoline_start + trampoline_addr] 247 | ``` 248 | 249 | On peut donc activer le mode protégé : 250 | 251 | ```x86asm 252 | mov eax, cr0 253 | or al, 0x1 254 | mov cr0, eax 255 | ``` 256 | 257 | ...Puis sauter en changeant le *segment code* vers l'entrée `0x8` de la GDT : 258 | 259 | ```x86asm 260 | jmp 0x8:(trampoline32 - trampoline_start + trampoline_addr) 261 | ``` 262 | 263 | ### Le code 32 bits 264 | 265 | On doit dans un premier temps charger la table de page dans le `cr3`, puis activer le paging et le PAE du `cr4` en activant les bits 5 et 7 du registre `cr4` : 266 | 267 | ```x86asm 268 | ; Chargement de la table de page : 269 | mov eax, dword [0x600] 270 | mov cr3, eax 271 | ; Activation du paging et du PAE 272 | mov eax, cr4 273 | or eax, 1 << 5 274 | or eax, 1 << 7 275 | mov cr4, eax 276 | ``` 277 | 278 | On active maintenant le mode long, en activant le 8ème bit de l'EFER (*Extended Feature Enable Register*) : 279 | 280 | ```x86asm 281 | mov ecx, 0xc0000080 ; registre efer 282 | rdmsr 283 | 284 | or eax,1 << 8 285 | wrmsr 286 | ``` 287 | 288 | On active ensuite le paging en écrivant le 31ème bit du registre `cr0` : 289 | 290 | ```x86asm 291 | mov eax, cr0 292 | or eax, 1 << 31 293 | mov cr0, eax 294 | ``` 295 | 296 | Et pour finir il faut créer puis charger une GDT 64 bits : 297 | 298 | ```x86asm 299 | align 16 300 | gdt_64: 301 | dw gdt_64_end - gdt_64_start - 1 302 | dd gdt_64_start - trampoline_start + trampoline_addr 303 | 304 | align 16 305 | gdt_64_start: 306 | ; null selector 0x0 307 | dq 0 308 | ; cs selector 8 309 | dq 0x00AF98000000FFFF 310 | ; ds selector 16 311 | dq 0x00CF92000000FFFF 312 | gdt_64_end: 313 | 314 | ; Chargement de la nouvelle GDT 315 | lgdt [gdt_64 - trampoline_start + trampoline_addr] 316 | ``` 317 | 318 | On peut ensuite passer à la section 64 bits, en utilisant l'instruction `jmp` comme précédement : 319 | 320 | ```x86asm 321 | ; jmp 0x8 : permet de charger le segment de code de la GDT 322 | jmp 0x8:(trampoline64 - trampoline_start + trampoline_addr) 323 | ``` 324 | 325 | ### Le code 64 bits 326 | 327 | On commence par définir les valeurs des registre `ds`, `ss` et `es` en fonction de la nouvelle GDT : 328 | 329 | ```x86asm 330 | mov ax, 0x10 331 | mov ds, ax 332 | mov es, ax 333 | mov ss, ax 334 | mov ax, 0x0 335 | mov fs, ax 336 | mov gs, ax 337 | ``` 338 | 339 | Et on charge ensuite la GDT, l'IDT et la stack au bon endroit : 340 | 341 | ```x86asm 342 | ; Chargement de la GDT 343 | lgdt [0x580] 344 | ; Chargement de l'IDT 345 | lidt [0x590] 346 | ; Chargement de la stack 347 | mov rsp, [0x570] 348 | mov rbp, 0x0 349 | ``` 350 | 351 | On doit ensuite passer du code trampoline au code physique à exécuter sur ce nouveau CPU. 352 | C'est à ce moment que on doit activer certains bits de `cr4` et `cr0` et surtout le SSE ! 353 | 354 | ```x86asm 355 | jmp virtual_code 356 | 357 | virtual_code: 358 | mov rax, cr0 359 | ; Activation du monitoring de multi-processeur et de l'émulation 360 | btr eax, 2 361 | bts eax, 1 362 | mov cr0, rax 363 | ``` 364 | 365 | Enfin, pour terminer l'initialisation de ce nouveau CPU il faut finir par : 366 | 367 | ```x86asm 368 | mov rax, [0x610] 369 | jmp rax 370 | ``` 371 | 372 | ## Note de fin 373 | 374 | Le nouveau CPU est maintenant fonctionnel, mais ce n'est pas encore fini. 375 | Il faut mettre en place un système de lock pour la communication inter-CPU, mettre à jour le multitasking pour utiliser ce nouveau CPU, charger une GDT, un IDT et une stack unique... 376 | 377 | ## Références 378 | 379 | - [manuel intel](https://software.intel.com/content/www/us/en/develop/articles/intel-sdm.html) 380 | - [osdev](https://wiki.osdev.org/Main_Page) 381 | -------------------------------------------------------------------------------- /src/x86_64/smp/locks.md: -------------------------------------------------------------------------------- 1 | # Verrou 2 | 3 | Le verrou est utilisé pour qu'un même code soit exécuté par un thread à la fois. 4 | 5 | On peut, par exemple, utiliser un verrou pour un driver ATA, afin qu'il n'y ait plusieurs écritures en même temps. On utilise alors un verrou au début de l'opération que l'on débloque à la fin. 6 | 7 | Un équivalent en code serait: 8 | 9 | ```c 10 | struct Lock lock; 11 | 12 | void ata_read(/* ... */) 13 | { 14 | acquire(&lock); 15 | 16 | /* ... */ 17 | 18 | release(&lock); 19 | }; 20 | ``` 21 | 22 | ## Prérequis 23 | 24 | Même si le verrou utilise l'instruction `lock` il peut être utilisé même si la machine ne possède qu'un seul processeur. 25 | Pour comprendre le verrou il faut avoir un minimum de base en assembleur. 26 | 27 | ## L'instruction `LOCK` 28 | 29 | l'instruction `lock` est utilisée juste avant une autre instruction qui accède / écrit dans la mémoire. 30 | 31 | Elle permet d'obtenir la possession exclusive de la partie du cache concernée le temps que l'instruction s'exécute. Un seul CPU à la fois peut exécuter l'instruction. 32 | 33 | Exemple de code utilisant le lock : 34 | 35 | ```asm 36 | lock bts dword [rdi], 0 37 | ``` 38 | 39 | ## Verrouillage & Déverrouillage 40 | 41 | ### Code assembleur 42 | 43 | pour verrouiller on doit implémenter une fonction qui vérifie le vérrou, 44 | si il est à 1, alors le verrou est bloqué, on doit attendre. 45 | si il est à 0, alors le verrou est débloqué, c'est notre tour. 46 | 47 | pour le déverrouiller on doit juste mettre le vérou à 0. 48 | 49 | pour le verrouillage le code pourrait ressembler à ceci : 50 | ```x86asm 51 | locker: 52 | lock bts dword [rdi], 0 53 | jc spin 54 | ret 55 | 56 | spin: 57 | pause ; pour éviter au processeur de surchauffer 58 | test dword [rdi], 0 59 | jnz spin 60 | jmp locker 61 | ``` 62 | 63 | Ce code test le bit 0 de l'addresse contenu dans le registre `rdi` (registre utilisé pour les arguments de fonctions en 64bit) 64 | 65 | ```x86asm 66 | lock bts dword [rdi], 0 67 | jc spin 68 | ``` 69 | si le bit est à 0 il le met à 1 et CF à 0 70 | si le bit est à 1 il met CF à 1 71 | 72 | jc spin jump à spin seulement si CF == 1 73 | 74 | pour le déverrouillage le code pourrait ressembler à ceci : 75 | 76 | ```x86asm 77 | unlock: 78 | lock btr dword [rdi], 0 79 | ret 80 | ``` 81 | 82 | il réinitialise juste le bit contenu dans `rdi` 83 | 84 | Maintenant on doit rajouter un temps mort 85 | 86 | parfois si un CPU a crash ou a oublié de déverrouiller un verrou il peut arriver que les autres CPU soient bloqués? Il est donc recommandé de rajouter un temps mort pour signaler l'erreur. 87 | 88 | ```x86asm 89 | locker: 90 | mov rax, 0 91 | lock bts dword [rdi], 0 92 | jc spin 93 | ret 94 | 95 | spin: 96 | inc rax 97 | cmp rax, 0xfffffff 98 | je timed_out 99 | 100 | pause ; pour gagner des performances 101 | test dword [rdi], 0 102 | jnz spin 103 | jmp locker 104 | 105 | timed_out: 106 | ; code du time out 107 | ``` 108 | Le temps pris ici est stocké dans le registre `rax`. 109 | Il incrémente chaque fois et si il est égal à `0xfffffff` alors il saute à `timed_out` 110 | 111 | On peut utiliser une fonction C/C++ dans timed_out 112 | 113 | ### Code C 114 | 115 | Dans le code C on peut se permettre de rajouter des informations au verrou. On peut rajouter le fichier, la ligne, le cpu etc... 116 | cela permet de mieux débugger si il y a une erreur dans le code 117 | 118 | 119 | Les fonction en c doivent être utilisées comme ceci : 120 | 121 | ```cpp 122 | void lock(volatile uint32_t* lock); 123 | void unlock(volatile uint32_t* lock); 124 | ``` 125 | 126 | Si on veut rajouter plus d'informations au lock on doit faire une structure contenant un membre 32bit 127 | 128 | ```cpp 129 | struct verrou 130 | { 131 | uint32_t data; // ne doit pas être changé 132 | const char* fichier; 133 | uint64_t line; 134 | uint64_t cpu; 135 | } __attribute__(packed); 136 | ``` 137 | Vous devez maintenant rajouter des fonction verrouiller et déverrouiller qui appelleront respectivement lock et unlock 138 | 139 | > Note : si vous voulez avoir la ligne/le fichier, vous devez utiliser des #define et non des fonction 140 | 141 | ```cpp 142 | void verrouiller(verrou* v) 143 | { 144 | // code pour remplir les données du vérrou 145 | 146 | lock(&(v->data)); 147 | } 148 | 149 | void deverrouiller(verrou* v) 150 | { 151 | unlock(&(v->data)); 152 | } 153 | ``` 154 | 155 | Maintenant vous devez implementer la fonction qui serra appelé dans `timed_out` 156 | 157 | ```cpp 158 | void crocheter_le_verrou(verrou* v) 159 | { 160 | // vous pouvez log des informations importantes ici 161 | } 162 | ``` 163 | 164 | maintenant vous pouvez choisir entre 2 possibilité : 165 | 166 | * dans la fonction crocheter_le_verrou vous continuez en attandant jusqu'à ce que le verrou soit déverrouillé 167 | 168 | * dans la fonction crocheter_le_verrou vous devez mettre le membre `data` du vérou v à 0, ce qui forcera le verrou à être déverrouiller 169 | 170 | ## Utilisation 171 | 172 | Maintenant, pour utiliser votre verrou, vous pouvez juste faire 173 | 174 | ```c 175 | struct Lock lock; 176 | 177 | void ata_read(/* ... */) 178 | { 179 | acquire(&lock); 180 | 181 | /* ... */ 182 | 183 | release(&lock); 184 | } 185 | ``` 186 | 187 | Le code sera désormais exécuté seulement sur 1 cpu à la fois ! 188 | 189 | Il est important d'utiliser les verrou quand il le faut, dans un allocateur de frame, le changement de contexte, l'utilisation d'appareils... 190 | -------------------------------------------------------------------------------- /src/x86_64/structures/GDT.md: -------------------------------------------------------------------------------- 1 | # Global Descriptor Table 2 | 3 | La table de descripteur globale à été introduite avec le processeur 16bit d'intel (le 80286) pour gérer la mémoire sous forme de segments. 4 | 5 | La segmentation ne devrais plus être utilisé, elle a été remplacé par le paging. Le paging est toujours obligatoire pour passer du 32 au 64 bit avec l'architecture x86. 6 | 7 | Cependant la `GDT` est aussi utilisée pour contenir la tss. La structure est différente entre le 32 et 64 bit. 8 | 9 | La table globale de descripteur est principalement formée de 2 structures: 10 | 11 | - la gdtr (le registre de segments) 12 | - le segment 13 | 14 | # Le registre de segments 15 | 16 | le registre de segments en mode long (x86_64) doit être construit comme ceci: 17 | 18 | | nom | taille | 19 | | ------------------- | ------ | 20 | | taille | 16 bit | 21 | | adresse de la table | 64 bit | 22 | 23 | __taille__: Le registre taille doit contenir la taille de la table de segment, soit le nombre de segment multiplié par la taille du segment, cependant en 64bit la taille du segment de la TSS est doublé, il faut alors compter le double. 24 | 25 | __adresse de la table__: L'adresse de la table doit pointer directement vers la table de segments. 26 | 27 | # Les segments 28 | 29 | Un segment en x86_64 est formé comme ceci: 30 | 31 | | nom | taille | 32 | | -------------------- | ------ | 33 | | limite basse (0-15) | 16 bit | 34 | | base basse (0-15) | 16 bit | 35 | | base milieu (16-23) | 8 bit | 36 | | flag | 8 bit | 37 | | limite haute (16-19) | 4 bit | 38 | | granularité | 4 bit | 39 | | base haute (24-31) | 8 bit | 40 | 41 | ## Les registres base 42 | 43 | Le registre base est le début du segment, en mode long il faut le mettre à 0. 44 | 45 | ## Les registres limite 46 | 47 | Le registre limite est une adresse 20bit, il représente la fin du segment. 48 | Il est multiplié par 4096 si le bit `granularité` est à 1. 49 | En mode long (64 bit) il faut le mettre à 0xfffff pour demander à ce que le segment prenne toute la mémoire. 50 | 51 | ## Le registre flag 52 | 53 | Les flags d'un segment est formé comme ceci: 54 | 55 | | nom | taille | 56 | | -------------------- | ------ | 57 | | accédé | 1 bit | 58 | | écriture/lisible | 1 bit | 59 | | direction/conformité | 1 bit | 60 | | executable | 1 bit | 61 | | type de descripteur | 1 bit | 62 | | niveau de privilège | 2 bit | 63 | | segment présent | 1 bit | 64 | 65 | __accédé__ : Doit être à 0, il est mit à 1 quand le processeur l'utilise. 66 | 67 | __écriture/lisible__: 68 | 69 | - Si c'est un segment de donnée: si le bit est à 1 alors l'écriture est autorisé avec le segment, si le bit est à 0 alors le segment est seulement lisible. 70 | - Si c'est un segment de code: si le bit est à 1 alors on peut lire le segment sinon le segment ne peut pas être lu. 71 | 72 | __direction/conformité__: 73 | 74 | - Pour les descripteurs de données: 75 | - Le bit défini le sens du segment, si il est mit alors le sens du segment est vers le bas, il doit être à 0 pour le 64 bit. 76 | 77 | - Pour les descripteurs de code: 78 | - Si le bit est à 1 alors le code peut être éxécuté par un niveau de privilège plus bas ou égal au registre `niveau de privilège`. 79 | - Si le bit est à 0 alors le code peut seulement être éxecuté par le registre `niveau de privilège`. 80 | 81 | __executable__: Définis si le segment est éxécutable ou non, si il est à 0 alors le segment ne peut pas être exécuté (c'est un segment de donné `data`) mais s'il est à 1 alors c'est un segment qui peut être exécuté (c'est un segment de code `code`). 82 | 83 | __type de descripteur__: Doit être mit à 1 pour les segment de code/data et il doit être à 0 pour la tss. 84 | 85 | __niveau de privilège__: Représente le niveau de privilège du descripteur (de 0 à 3). 86 | 87 | __segment présent__: Doit être mit à 1 pour tout descripteur (sauf pour le descripteur null). 88 | 89 | ## Le registre granularité 90 | 91 | Le registre granularité d'un segment est formé comme ceci: 92 | 93 | | nom | taille | 94 | | ----------- | ------ | 95 | | granularité | 1 bit | 96 | | taille | 1 bit | 97 | | mode long | 1 bit | 98 | | zéro | 1 bit | 99 | 100 | __granularité__: Le bit granularité doit être mit quand la limite est fixe, cependant si le bit est à 1 alors la limite est multipliée par 4096. 101 | 102 | __taille__: Le bit taille doit être mit à 0 pour le 16bit/64bit, 1 pour le 32bit. 103 | 104 | __mode long__: Le bit doit être à 1 pour les descripteur de code en 64bit sinon il reste à 0. 105 | 106 | ## Types de segment 107 | 108 | Il y a différents type de segments: 109 | 110 | ### Le segment null 111 | 112 | L'entrée 0 d'une gdt est une entrée nulle, tout le segment est à 0. 113 | 114 | ### Le segment code du kernel 115 | 116 | La première entrée doit être un segment pour le kernel éxecutable soit un segment de code: 117 | 118 | - Dans le type il faut que le bit 'type de descripteur' soit à 1. 119 | - Il faut que le segment ait l'accès en écriture. 120 | - Il faut que le bit executable soit mit. 121 | - Le niveau de privilège doit être à 0. 122 | 123 | Cela produit un type pour le mode x86_64: 124 | `0b10011010` 125 | 126 | La granularité doit être à `0b10` 127 | 128 | ### Le segment data du kernel 129 | 130 | La seconde entrée doit être un segment de donnée pour le kernel. 131 | 132 | - Il faut utiliser la même démarche que le segment de code sauf qu'il faut mettre le bit executable à 0. 133 | 134 | Cela produit un type pour le mode x86_64: 135 | `0b10010010` 136 | 137 | La granularité doit être à `0` 138 | 139 | ### Le segment code des utilisateurs 140 | 141 | La troisième entrée doit être un segment pour les applications éxecutable depuis l'anneau (niveau de privilège) 3. 142 | 143 | - Il faut reproduire la même démarche que pour le segment code du kernel sauf que le niveau de privilège doit être à 3 pour le segment. 144 | 145 | Cela produit un type pour le mode x86_64: 146 | `0b11111010` 147 | 148 | La granularité doit être à `0b10`. 149 | 150 | ### Le segment données des utilisateurs 151 | 152 | La quatrième entrée doit être un segment pour les données d'applications depuis l'anneau (niveau de privilège) 3. 153 | Il faut reproduire la même démarche que pour le segment data du kernel sauf que le niveau de privilège doit être à 3. 154 | 155 | Cela produit un type pour le mode x86_64: 156 | `0b11110010`. 157 | 158 | La granularité doit être à `0`. 159 | 160 | # Le chargement d'une gdt 161 | 162 | Pour charger un registre d'une gdt il faut utiliser l'instruction: 163 | 164 | ```x86asm 165 | lgdt [registre] 166 | ``` 167 | 168 | Avec le registre contenant l'adresse du registre de la gdt. 169 | Cependant en 64bit il faut charger les registre du segment de code et de donnée. Ici nous allons utiliser l'instruction `retf` qui permet de charger un segment de code: 170 | 171 | ```x86asm 172 | gdtr_install: 173 | lgdt [rdi] 174 | ; met tout les segments avec leurs valeurs ciblants le segment de données 175 | mov ax, 0x10 176 | 177 | mov ds, ax 178 | mov es, ax 179 | mov ss, ax 180 | 181 | mov rax, qword .trampoline ; addresse de retour 182 | push qword 0x8 ; segment de code 183 | push rax 184 | 185 | o64 retf ; fait un far return 186 | 187 | .trampoline: 188 | ret 189 | ``` 190 | 191 | ## Références 192 | 193 | - [wikipedia gdt](https://en.wikipedia.org/wiki/Global_Descriptor_Table) 194 | - [osdev gdt](https://wiki.osdev.org/GDT) 195 | - [documentation intel](https://www.intel.com/content/www/us/en/architecture-and-technology/64-ia-32-architectures-software-developer-vol-3a-part-1-manual.html) 196 | -------------------------------------------------------------------------------- /src/x86_64/structures/IDT.md: -------------------------------------------------------------------------------- 1 | # Interrupt Descriptor Table 2 | 3 | La table de description des interruptions est une table qui permet au cpu 4 | de pouvoir savoir ou aller (jump) quand il y a une interruption. 5 | 6 | Il y a deux structures utilisées (en 64bit) : 7 | 8 | - La table d'entrée d'interruptions 9 | - L'entrée de la table d'interruptions 10 | 11 | ## Table d'entrée 12 | 13 | La table d'entrée contient une adresse qui situe une table d'entrée d'IDT et la taille de la table (en mémoire). 14 | 15 | Pour la table d'entrée la structure est comme ceci : 16 | 17 | | nom | taille | 18 | | ------------------- | ------ | 19 | | taille | 16 bit | 20 | | adresse de la table | 64 bit | 21 | 22 | La table d'entrée peut être définie comme ceci: 23 | ```c 24 | IDT_entry_count = 64; 25 | IDT_Entry_t ent[IDT_entry_count]; 26 | IDT_table.addr = (uint64_t)ent; 27 | IDT_table.size = sizeof(IDT_Entry_t) * IDT_entry_count; 28 | ``` 29 | 30 | ## entrée d'IDT 31 | 32 | l'entrée d'une IDT en mode long doit être structurée comme ceci : 33 | 34 | | nom | taille | 35 | | --------------- | ------ | 36 | | offset (0-16) | 16 bit | 37 | | segment de code | 16 bit | 38 | | index de l'ist | 8 bit | 39 | | attributs | 8 bit | 40 | | offset (16-32) | 16 bit | 41 | | offset (32-64) | 32 bit | 42 | | zéro | 32 bit | 43 | 44 | Le `segment de code` étant le segment de code utilisé pendant l'interruption. 45 | 46 | L'`offset` est l'adresse où le CPU va jump si il y a une interruption. 47 | 48 | ### Les attributs 49 | l'attribut d'une entrée d'une IDT est formée comme ceci : 50 | 51 | | nom | bit | 52 | | ------------------- | ----- | 53 | | type d'interruption | 0 - 3 | 54 | | zéro | 4 | 55 | | niveau de privilège | 5 - 6 | 56 | | présent | 7 | 57 | 58 | Le `niveau de privilège` (aka DPL) est le niveau de privilège requis pour que l'interruption soit appelée. 59 | 60 | Il est utilisé pour éviter à ce que une application utilisatrice puisse appellée une interruption qui est réservée au kernel 61 | 62 | 63 | ### Types d'interruptions 64 | 65 | Les types d'interruptions sont les mêmes que cela soit en 64bit ou en 32bit. 66 | 67 | | valeur | signification | 68 | | ----------- | --------------------------- | 69 | | 0b0111 (7) | trappe d'interruption 16bit | 70 | | 0b0110 (6) | porte d'interruption 16bit | 71 | | 0b1110 (14) | porte d'interruption 32bit | 72 | | 0b1111 (15) | trappe d'interruption 32bit | 73 | 74 | La différence entre une `trappe`(aka trap) et une `porte` (aka gate) est que la gate désactive `IF`, ce qui veut dire que vous devrez réactiver les interruptions à la fin de l'ISR. 75 | 76 | La trappe ne désactive pas `IF` donc vous pouvez désactiver / réactiver vous même dans l'isr les interrupts. 77 | 78 | ### Index de l'IST 79 | 80 | L'ist (`Interrupt Stack Table`) est utile au changement de stack avant une interrupt: 81 | 82 | | nom | bit | 83 | | -------------- | ----- | 84 | | index de l'ist | 0 - 3 | 85 | | zéro | 4 - 7 | 86 | 87 | Si l'index de l'ist est à 0 alors l'ist n'est pas actif. 88 | Si il n'est pas à 0 il chargeras alors la stack (`RSP`) à partir de l'ist correspondant dans la tss. --------------------------------------------------------------------------------