Il est temps d'écrire le premier programme séparé pour notre noyau - le shell. Il sera stocké dans un fichier .elf séparé et lancé par le processus init au démarrage du noyau.
Ceci est le dernier article du cycle de développement de notre système d'exploitation.
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).
Pilotes de périphériques de caractères. Appels système (ioctl, fopen, fread, fwrite). Bibliothèque C (fopen, fclose, fprintf, fscanf).
Le système de fichiers du noyau (initrd), elf et ses composants internes. Appels système (exec). Shell comme programme complet pour le noyau.Mode de protection utilisateur (ring3). Segment d'état de la tâche (tss).
Shell en tant que programme complet du noyau
Dans un article précédent, nous avons examiné les pilotes de périphériques de caractères et écrit un pilote de terminal.
Nous avons maintenant tout ce dont nous avons besoin pour créer la première application console.
Nous allons écrire l'application console elle-même, que nous compilerons dans un elfe séparé.
void start() { u_int errno; stdio_init(); errno = main(); stdio_deinit(); exit(errno); }
Nous devrons initialiser la bibliothèque standard et transférer le contrôle à la fonction principale familière.
int main() { char cmd[255]; while (1) { printf(prompt); flush(); scanf(cmd); if (!execute_command(cmd)) { break; } } return 0; }
Plus loin dans la boucle, nous lisons simplement la ligne et exécutons la commande.
Parsim commande via strtok_r s'il y a des arguments.
static bool execute_command(char* cmd) { if (!strcmp(cmd, cmd_ps)) { struct clist_definition_t *task_list; task_list = ps(); printf(" -- process list\n"); clist_for_each(task_list, print_task_info); } else if (!strcmp(cmd, cmd_clear)) { clear(); flush(); } else if (!strncmp(cmd, cmd_kill, strlen(cmd_kill))) { char* save_ptr = null; strtok_r(cmd, " ", &save_ptr); char* str_tid = strtok_r(null, " ", &save_ptr); u_short tid = atou(str_tid); if (!kill(tid)) { printf(" There is no process with pid %u\n", tid); }; } else if (!strncmp(cmd, cmd_exit, strlen(cmd_exit))) { clear(); printf(prompt); flush(); return false; } else if (!strncmp(cmd, cmd_exec, strlen(cmd_exec))) { char* save_ptr = null; strtok_r(cmd, " ", &save_ptr); char* str_file = strtok_r(null, " ", &save_ptr); exec(str_file); } else if (!strncmp(cmd, cmd_dev, strlen(cmd_dev))) { struct clist_definition_t *dev_list; dev_list = devs(); printf(" -- device list\n"); clist_for_each(dev_list, print_dev_info); } else { printf(" There is no such command.\n Available command list:\n"); printf(" %s %s %s <pid> %s <file.elf> %s %s\n", cmd_ps, cmd_exit, cmd_kill, cmd_exec, cmd_clear, cmd_dev); } return true; }
En fait, nous ne faisons que tirer des appels système.
Permettez-moi de vous rappeler l'initialisation de la bibliothèque standard.
Dans la dernière leçon, nous avons écrit la fonction suivante dans la bibliothèque:
extern void stdio_init() { stdin = fopen(tty_dev_name, MOD_R); stdout = fopen(tty_dev_name, MOD_W); asm_syscall(SYSCALL_IOCTL, stdout, IOCTL_INIT); asm_syscall(SYSCALL_IOCTL, stdin, IOCTL_READ_MODE_LINE); asm_syscall(SYSCALL_IOCTL, stdin, IOCTL_READ_MODE_ECHO); }
Il ouvre simplement les fichiers spéciaux du pilote de terminal pour la lecture et l'écriture, ce qui correspond à l'entrée et à la sortie du clavier à l'écran.
Après avoir assemblé notre elfe avec un shell, il doit être placé sur le système de fichiers du noyau d'origine (initrd).
Le disque RAM initial est chargé par les chargeurs du noyau en tant que module multiboot, nous connaissons donc l'adresse dans la mémoire de notre initrd.
Reste à organiser le système de fichiers pour initrd, ce qui est facile à faire selon un article de James Molloy.
Par conséquent, le format sera le suivant:
extern struct initrd_node_t { unsigned char magic; char name[8]; unsigned int offset; unsigned int length; }; extern struct initrd_fs_t { int count; struct initrd_node_t node[INITRD_MAX_FILES]; };
Ensuite, rappelez-vous le format d'un elfe 32 bits.
struct elf_header_t { struct elf_header_ident_t e_ident; u16 e_type; u16 e_machine; u32 e_version; u32 e_entry; u32 e_phoff; u32 e_shoff; u32 e_flags; u16 e_ehsize; u16 e_phentsize; u16 e_phnum; u16 e_shentsize; u16 e_shnum; u16 e_shstrndx; };
Nous nous intéressons ici au point d'entrée et à l'adresse du tableau des en-têtes de programme.
La section code et données sera la première rubrique, et la section pile sera la seconde (selon les résultats de l'étude des elfes via objdump).
struct elf_program_header_t { u32 p_type; u32 p_offset; u32 p_vaddr; u32 p_paddr; u32 p_filesz; u32 p_memsz; u32 p_flags; u32 p_align; } attribute(packed);
Ces informations sont suffisantes pour écrire un chargeur de fichiers elf.
Nous savons déjà comment sélectionner des pages pour des processus personnalisés.
Par conséquent, nous avons juste besoin d'allouer un nombre suffisant de pages pour les en-têtes et d'y copier le contenu.
Nous allons écrire une fonction qui créera un processus basé sur le fichier elf analysé.
Découvrez comment analyser elfik dans le didacticiel vidéo.
Nous devons télécharger un seul en-tête de programme avec du code et des données, donc nous ne généraliserons pas et nous concentrerons sur ce cas.
extern void elf_exec(struct elf_header_t* header) { assert(header->e_ident.ei_magic == EI_MAGIC); printf(MSG_KERNEL_ELF_LOADING, header->e_phnum);
La chose la plus intéressante ici est la création d'un répertoire de pages et d'une table de pages.
Faites attention, nous sélectionnons d'abord les pages physiques (mm_phys_alloc_pages), puis les mappons aux pages logiques (mmu_occupy_user_page).
Ici, on suppose que les pages de la mémoire physique sont allouées en continu.
C’est tout. Vous pouvez maintenant implémenter votre propre shell pour votre noyau! Regardez le didacticiel vidéo et plongez dans les détails.
Conclusion
J'espère que vous avez trouvé cette série d'articles utile.
Nous n'avons pas encore envisagé les protections avec vous, mais en raison de la faible pertinence du sujet et des critiques mitigées, nous continuerons de rompre.
Je pense que vous-même êtes maintenant prêt pour de nouvelles recherches, pour vous et moi avons examiné toutes les choses les plus importantes.
Par conséquent, serrez la ceinture et partez au combat! En écrivant votre propre système d'exploitation!
Il m'a fallu environ un mois (si l'on considère le temps plein pendant 6 à 8 heures par jour) pour mettre en œuvre tout ce que vous et moi avons appris à partir de zéro.
Par conséquent, en 2-3 mois, vous pourrez écrire un système d'exploitation à part entière avec un véritable système de fichiers, que vous et moi n'avons pas réussi à mettre en œuvre.
Sachez simplement que qemu ne sait pas comment travailler avec initrd au format arbitraire, et le coupe Ă 4 Ko, vous devrez donc le faire comme sous Linux, ou utiliser borsch au lieu de qemu.
Si vous savez comment contourner ce problème, écrivez dans une lettre personnelle, je vous en serai très reconnaissant.
C’est tout! Jusqu'à ce que les nouveaux ne se rencontrent plus!
Les références
Regardez le
didacticiel vidéo pour plus d'informations.
Le code source
dans le référentiel git (vous avez besoin de la branche de leçon 9).
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.