Développement d'un système d'exploitation monolithique de type Unix - GDT & IDT (5)

Dans l'article précédent, nous avons implémenté un gestionnaire de mémoire dynamique.
Aujourd'hui, nous aborderons les bases du travail en mode protégé du processeur Intel i386.
A savoir: la table des descripteurs globaux et la table des vecteurs d'interruption.


Table des matières


Construisez le système (make, gcc, gas). Démarrage initial (multiboot). Lancez (qemu). Bibliothèque C (strcpy, memcpy, strext).
Bibliothèque C (sprintf, strcpy, strcmp, strtok, va_list ...). Construction de la bibliothèque en mode noyau et en mode application utilisateur.
Le journal système du noyau. Mémoire vidéo Sortie vers le terminal (kprintf, kpanic, kassert).
Mémoire dynamique, tas (kmalloc, kfree).
Organisation de la mémoire et gestion des interruptions (GDT, IDT, PIC, syscall). Exceptions
Mémoire virtuelle (répertoire de pages et table de pages).
Processus. Planificateur Multitâche. Appels système (kill, exit, ps).
Le système de fichiers du noyau (initrd), elf et ses composants internes. Appels système (exec).
Pilotes de périphériques de caractères. Appels système (ioctl, fopen, fread, fwrite). Bibliothèque C (fopen, fclose, fprintf, fscanf).
Shell comme programme complet pour le noyau.
Mode de protection utilisateur (ring3). Segment d'état de la tâche (tss).

Adressage linéaire


Les processeurs Intel ont 2 modes de fonctionnement principaux: le mode protégé x32 et IA-32e x64.
En général, Zubkov écrit très bien et compréhensible à ce sujet, je recommande de le lire, bien qu'en principe Intel Manual soit également possible, ce n'est pas compliqué, mais redondant et volumineux.
Ils ont un volume séparé pour la programmation du système, je le recommande et le lis.
Il y a beaucoup plus d'informations en russe sur le premier, nous allons donc brièvement examiner les principaux points.
Il existe deux types d'adressage: linéaire et page. Linéaire signifie que tout l'espace physique est décrit en continu et coïncide avec l'espace physique, car en règle générale, les bases des descripteurs de segment sont nulles, car c'est plus facile.
Dans ce cas, pour le mode noyau, vous devez créer trois descripteurs décrivant la mémoire: pour le code, la pile et les données. Ils se distinguent par une certaine protection matérielle.
Chacun de ces segments a une base de zéro et une limite adressée par la taille maximale d'un mot machine. La pile se développe dans la direction opposée, et pour cela il y a aussi un drapeau dans le descripteur.
Ainsi, avec trois enregistrements de ce format, nous abordons tout ce dont nous avons besoin:

/* * Global descriptor table entry */ struct GDT_entry_t { u16 limit_low: 16; u16 base_low: 16; u8 base_middle: 8; u8 type: 4; /* whether code (0b1010), data (0b0010), stack (0b0110) or tss (0b1001) */ u8 s: 1; /* whether system descriptor */ u8 dpl: 2; /* privilege level */ u8 p: 1; /* whether segment prensent */ u8 limit_high: 4; u8 a: 1; /* reserved for operation system */ u8 zero: 1; /* zero */ u8 db: 1; /* whether 16 or 32 segment */ u8 g: 1; /* granularity */ u8 base_high: 8; } attribute(packed); 


Chaque registre de segment (cs, ds, ss) a son propre descripteur dans GDT, donc quand nous écrivons quelque chose dans la section de code, nous obtiendrons une erreur, car il y a une protection écrite dans le descripteur.
Pour que cela fonctionne, nous devons charger une structure du format suivant dans le registre GDTR:

 /* * Global descriptor table pointer */ struct GDT_pointer_t { u16 limit; u32 base; } attribute(packed); 


La limite est la fin de la table GDT moins 1, la base est son début en mémoire.
GDT est chargé dans le registre comme ceci:

/*
* Load global descriptor table
* void asm_gdt_load(void *gdt_ptr)
*/
asm_gdt_load:
mov 4(%esp),%eax # eax = gdt_ptr
lgdt (%eax)
mov $0x10,%eax
mov %ax,%ds
mov %ax,%es
mov %ax,%fs
mov %ax,%gs
mov %ax,%ss
jmp $0x08,$asm_gdt_load_exit
asm_gdt_load_exit:
ret


Et immédiatement après cela, nous chargeons les sélecteurs de données du noyau dans le descripteur de données (anneau de protection zéro) dans tous les registres de segments.
Après cela, tout est prêt à inclure la pagination, mais plus à ce sujet plus tard.
Par ailleurs, les chargeurs de démarrage multiboot recommandent de configurer immédiatement leur GDT, bien qu'ils le fassent eux-mêmes, ils le disent de manière plus fiable.
Découvrez comment procéder techniquement correctement dans le didacticiel vidéo.

Interruption de la manipulation


Par analogie avec GDT, la table d'interruption a son propre registre IDTR, dans lequel vous devez également charger un pointeur similaire mais déjà sur IDT.
La table d'interruption elle-même est décrite par les entrées suivantes:

 /* * Interrupt table entry */ struct IDT_entry_t { u16 offset_lowerbits; u16 selector; u8 zero; u8 type_attr; u16 offset_higherbits; }; 


La passerelle d'interruption agit généralement comme un type, car nous voulons gérer spécifiquement les interruptions. Nous ne considérons pas encore les pièges et une passerelle d'appel, car il est plus proche du TSS et des anneaux de protection.
Créons une interface pour travailler avec ces tables avec vous. Ils ont juste besoin d'être installés et oubliés une fois.

 /* * Api */ extern void gdt_init(); extern void idt_init(); 


Et maintenant, nous allons déclarer les gestionnaires d'interruption répertoriés dans les enregistrements IDT eux-mêmes.
Tout d'abord, écrivez les gestionnaires d'erreurs matérielles:

 /* * Api - IDT */ extern void ih_double_fault(); extern void ih_general_protect(); extern void ih_page_fault(); extern void ih_alignment_check(); extern void asm_ih_double_fault(); extern void asm_ih_general_protect(); extern void asm_ih_page_fault(); extern void asm_ih_alignment_check(); 


Ensuite, le gestionnaire d'interruption du clavier:

 /* * Api - IRQ */ extern void ih_keyboard(); extern void asm_ih_keyboard(); 


Il est temps d'initialiser la table IDT.
Cela ressemble à ceci:

 extern void idt_init() { size_t idt_address; size_t idt_ptr[2]; pic_init(); /* fill idt */ idt_fill_entry(INT_DOUBLE_FAULT, (size_t)asm_ih_double_fault); idt_fill_entry(INT_GENERAL_PROTECT, (size_t)asm_ih_general_protect); idt_fill_entry(INT_ALIGNMENT_CHECK, (size_t)asm_ih_alignment_check); idt_fill_entry(INT_KEYBOARD, (size_t)asm_ih_keyboard); /* load idt */ idt_address = (size_t)IDT; idt_ptr[0] = (LOW_WORD(idt_address) << 16) + (sizeof(struct IDT_entry_t) * IDT_SIZE); idt_ptr[1] = idt_address >> 16; asm_idt_load(idt_ptr); } 


Ici, nous avons enregistré trois gestionnaires d'erreurs matérielles et une interruption.
Pour que cela commence à fonctionner, nous devons charger un pointeur spécial avec la base et limiter dans le registre IDTR:

/*
* Load interrupt table
* void asm_idt_load(unsigned long *addr)
*/
asm_idt_load:
push %edx
mov 8(%esp), %edx
lidt (%edx)
pop %edx
ret


Des limites sont nécessaires pour comprendre le nombre d'enregistrements dans la table.
Il est temps d'écrire un gestionnaire d'interruption clavier:

/*
* Handle IRQ1
* void asm_ih_keyboard(unsigned int)
*/
asm_ih_keyboard:
pushal
call ih_keyboard
popal
iretl


Remarque: ci-après et partout dans le code, les «moitiés inférieures» sont équivalentes aux «moitiés supérieures» sous Linux. Et le «supérieur», respectivement, le contraire. Je m'excuse, le contraire a été mis dans ma tête: D

En fait, il passera du code à un gestionnaire de haut niveau.
Cela, à son tour, appellera le gestionnaire des moitiés inférieures du pilote correspondant qui a enregistré la demande de traitement de cette interruption.
Dans notre cas, ce sera un pilote de périphérique de caractères.
Les moitiés inférieures sont nécessaires pour traiter rapidement les interruptions sans ralentir les autres, puis, lorsque le temps le permet, le processeur des moitiés supérieures effectuera progressivement un travail supplémentaire, car un tel processeur peut déjà être évincé (interrompu).

 /* * Api - Keyboard interrupt handler */ extern void ih_keyboard() { printf("[IH]: irq %u\n", 1); u_char status = asm_read_port(KEYBOARD_STATUS_PORT); if (status & 0x01) { char keycode = asm_read_port(KEYBOARD_DATA_PORT); if (keycode < 1) { goto end; } /* call low half (bottom) interrupt handler */ } end: asm_write_port(PIC1_CMD_PORT, 0x20); /* end of interrupt */ } 


Maintenant, lorsque nous appuyons sur la touche du clavier, nous verrons à chaque fois l'entrée correspondante dans le journal système du noyau.

Les références


Maintenant, ouvrez le didacticiel vidéo de cet article.
Et regardez le dépôt git en parallèle (vous avez besoin d'une branche de leçon 5)

Les références


1. James Molloy. Faites rouler votre propre système d'exploitation jouet UNIX-clone.
2. Dents. Assembleur pour DOS, Windows, Unix
3. Kalachnikov. L'assembleur est facile!
4. Tanenbaum. Systèmes d'exploitation. Mise en œuvre et développement.
5. Robert Love. Noyau Linux Description du processus de développement.

Source: https://habr.com/ru/post/fr467289/


All Articles