Unix-ähnliche Betriebssystementwicklung - Shell. Schlussfolgerung (9)

Es ist Zeit, das erste separate Programm für unseren Kernel zu schreiben - die Shell. Es wird in einer separaten .elf-Datei gespeichert und beim Start des Kernels vom Init-Prozess gestartet.

Dies ist der letzte Artikel im Entwicklungszyklus unseres Betriebssystems.

Inhaltsverzeichnis


Build-System (make, gcc, gas). Erster Start (Multiboot). Starten Sie (qemu). C-Bibliothek (strcpy, memcpy, strext).

C-Bibliothek (sprintf, strcpy, strcmp, strtok, va_list ...). Erstellen der Bibliothek im Kernelmodus und im Benutzeranwendungsmodus.

Das Kernel-Systemprotokoll. Videospeicher Ausgabe an das Terminal (kprintf, kpanic, kassert).
Dynamischer Speicher, Heap (kmalloc, kfree).

Organisation der Speicher- und Interrupt-Behandlung (GDT, IDT, PIC, Syscall). Ausnahmen
Virtueller Speicher (Seitenverzeichnis und Seitentabelle).

Prozess. Planer Multitasking. Systemaufrufe (kill, exit, ps).

Zeichengerätetreiber. Systemaufrufe (ioctl, fopen, fread, fwrite). C-Bibliothek (fopen, fclose, fprintf, fscanf).

Das Dateisystem des Kernels (initrd), elf und seiner Interna. Systemaufrufe (exec). Shell als komplettes Programm für den Kernel.

Benutzerschutzmodus (Ring3). Aufgabenstatussegment (tss).

Shell als komplettes Kernelprogramm


In einem früheren Artikel haben wir uns Zeichengerätetreiber angesehen und einen Terminaltreiber geschrieben.

Jetzt haben wir alles, was wir zum Erstellen der ersten Konsolenanwendung benötigen.

Wir werden die Konsolenanwendung selbst schreiben, die wir in einen separaten Elfen kompilieren werden.

/* * Elf entry point */ void start() { u_int errno; stdio_init(); errno = main(); stdio_deinit(); exit(errno); } 

Wir müssen die Standardbibliothek initialisieren und die Steuerung auf die bekannte Hauptfunktion übertragen.

 int main() { char cmd[255]; while (1) { printf(prompt); flush(); scanf(cmd); if (!execute_command(cmd)) { break; } } return 0; } 

Weiter in der Schleife lesen wir einfach die Zeile und führen den Befehl aus.

Parsim-Befehle über strtok_r, wenn Argumente vorhanden sind.

 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; } 

Tatsächlich ziehen wir nur Systemaufrufe ab.
Ich möchte Sie an die Initialisierung der Standardbibliothek erinnern.

In der letzten Lektion haben wir die folgende Funktion in die Bibliothek geschrieben:

 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); } 

Es werden einfach die speziellen Terminal-Treiberdateien zum Lesen und Schreiben geöffnet, die der Tastatureingabe und -ausgabe auf dem Bildschirm entsprechen.

Nachdem wir unseren Elfen mit einer Shell zusammengesetzt haben, muss er auf dem ursprünglichen Kernel-Dateisystem (initrd) abgelegt werden.

Die anfängliche RAM-Disk wird von Kernel-Loadern als Multiboot-Modul geladen, sodass wir die Adresse im Speicher unserer initrd kennen.

Es bleibt, das Dateisystem für initrd zu organisieren, was laut einem Artikel von James Molloy einfach zu tun ist.

Daher lautet das Format wie folgt:

 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 */ }; 

Erinnern Sie sich als nächstes an das Format eines 32-Bit-Elfen.
 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; }; 

Hier interessieren uns der Einstiegspunkt und die Adresse der Programm-Header-Tabelle.

Der Code- und Datenabschnitt ist die erste Überschrift und der Stapelabschnitt die zweite (gemäß den Ergebnissen der Untersuchung von Elfen durch 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); 

Diese Informationen reichen aus, um einen Elf File Loader zu schreiben.
Wir wissen bereits, wie Seiten für benutzerdefinierte Prozesse ausgewählt werden.
Daher müssen wir nur eine ausreichende Anzahl von Seiten für Überschriften zuweisen und den Inhalt in diese kopieren.
Wir werden eine Funktion schreiben, die einen Prozess basierend auf der analysierten Elf-Datei erstellt.
Sehen Sie im Video-Tutorial, wie Sie elfik analysieren.
Wir müssen nur einen Programmheader mit Code und Daten herunterladen, daher werden wir nicht verallgemeinern und uns auf diesen Fall konzentrieren.

 /* * 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); } 

Das Interessanteste dabei ist die Erstellung eines Seitenverzeichnisses und einer Seitentabelle.
Achten Sie darauf, dass wir zuerst die physischen Seiten (mm_phys_alloc_pages) auswählen und sie dann den logischen Seiten (mmu_occupy_user_page) zuordnen.
Hierbei wird davon ausgegangen, dass die Seiten im physischen Speicher kontinuierlich zugeordnet werden.
Das ist alles. Jetzt können Sie Ihre eigene Shell für Ihren Kernel implementieren! Sehen Sie sich das Video-Tutorial an und gehen Sie auf die Details ein.

Fazit


Ich hoffe, Sie fanden diese Artikelserie hilfreich.
Wir haben die Schutzringe bei Ihnen noch nicht in Betracht gezogen, aber aufgrund der geringen Relevanz des Themas und der gemischten Bewertungen werden wir weiterhin brechen.
Ich denke, Sie selbst sind jetzt bereit für weitere Forschungen, denn Sie und ich haben alle wichtigen Dinge untersucht.

Deshalb den Gürtel enger schnallen und in die Schlacht ziehen! Indem Sie Ihr eigenes Betriebssystem schreiben!
Ich habe ungefähr einen Monat gebraucht (wenn wir 6-8 Stunden am Tag Vollzeit in Betracht ziehen), um alles umzusetzen, was Sie und ich von Grund auf gelernt haben.

Daher können Sie in 2-3 Monaten ein vollwertiges Betriebssystem mit einem echten Dateisystem schreiben, das wir nicht implementiert haben.

Wisse nur, dass qemu nicht weiß, wie man mit initrd eines beliebigen Formats arbeitet, und schneide es auf 4 KB, also musst du es entweder wie unter Linux machen oder borsch anstelle von qemu verwenden.
Wenn Sie wissen, wie Sie dieses Problem umgehen können, schreiben Sie in einem persönlichen Brief, ich werde Ihnen sehr dankbar sein.

Das ist alles! Bis sich die Neuen nicht mehr treffen!

Referenzen


Weitere Informationen finden Sie im Video-Tutorial .

Der Quellcode im Git-Repository (Sie benötigen den Lektion9-Zweig).

Referenzliste


1. James Molloy. Rollen Sie Ihr eigenes UNIX-Klon-Betriebssystem.
2. Zähne. Assembler für DOS, Windows, Unix
3. Kalaschnikow. Assembler ist einfach!
4. Tanenbaum. Betriebssysteme. Implementierung und Entwicklung.
5. Robert Love. Linux-Kernel Beschreibung des Entwicklungsprozesses.

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


All Articles