├── .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 | 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 |
14 |
15 |
16 | ## Licence
17 |
18 | 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 |
15 |
16 | ## Licence
17 |
18 | 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 |
315 |
--------------------------------------------------------------------------------
/src/x86_64/assets/kernel_higher_lower_half.svg:
--------------------------------------------------------------------------------
1 |
2 |
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 |
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.
--------------------------------------------------------------------------------