Développement de système d'exploitation de type Unix - Shell. Conclusion (9)

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é.

/* * Elf entry point */ 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)) { /* show tasks list */ 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 screen */ clear(); flush(); } else if (!strncmp(cmd, cmd_kill, strlen(cmd_kill))) { /* kill task */ 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))) { /* exit */ clear(); printf(prompt); flush(); return false; } else if (!strncmp(cmd, cmd_exec, strlen(cmd_exec))) { /* exec file on intrd */ 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))) { /* show device list */ 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; /* magic number */ char name[8]; /* file name */ unsigned int offset; /* file base */ unsigned int length; /* file length */ }; extern struct initrd_fs_t { int count; /* files count */ struct initrd_node_t node[INITRD_MAX_FILES]; /* files headers */ }; 

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; /* virtual address of entry point */ u32 e_phoff; /* program headers table offset */ u32 e_shoff; /* program headers sections table offset */ u32 e_flags; u16 e_ehsize; /* file header size */ u16 e_phentsize; /* single header size */ u16 e_phnum; /* headers count */ u16 e_shentsize; /* section header size */ u16 e_shnum; /* sections headers count */ 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; /* segment type */ u32 p_offset; /* segment offset from file begin */ u32 p_vaddr; /* target virtual address */ u32 p_paddr; /* target physical address */ u32 p_filesz; /* segment size in file */ u32 p_memsz; /* segment size in memory */ u32 p_flags; /* permissions and etc */ u32 p_align; /* alignment */ } 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.

 /* * Api - execute elf as a task */ 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); // elf_dump(header); size_t elf_base = (size_t)header; size_t entry_point = header->e_entry; struct task_mem_t task_mem; memset(&task_mem, 0, sizeof(struct task_mem_t)); // load sections in memory for (int i = 0; i < header->e_phnum; ++i) { struct elf_program_header_t* p_header = (void*)(header->e_phoff + elf_base + i * header->e_phentsize); task_mem.pages_count = (p_header->p_memsz / MM_PAGE_SIZE) + 1; if (p_header->p_memsz == 0) { continue; } // allocate pages assert(task_mem.pages_count > 0); assert(task_mem.pages == null); task_mem.pages = mm_phys_alloc_pages(task_mem.pages_count); void* section = (void*)(elf_base + p_header->p_offset); memcpy(task_mem.pages, section, p_header->p_memsz); // setup virtual memory task_mem.page_table = mmu_create_user_page_table(); task_mem.page_dir = mmu_create_user_page_directory(task_mem.page_table); for (int i = 0; i < task_mem.pages_count; ++i) { mmu_occupy_user_page(task_mem.page_table, (void*)((size_t)task_mem.pages + i * MM_PAGE_SIZE)); } } // create task u_short tid = next_tid++; assert(task_create(tid, (void*)entry_point, &task_mem)); // run task struct task_t* task; task = task_get_by_id(tid); task->status = TASK_RUNNING; strncpy(task->name, "elf", sizeof(task->name)); printf(MSG_KERNEL_ELF_LOADED); } 

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.

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


All Articles