Desarrollo de sistemas operativos tipo Unix: Shell. Conclusión (9)

Es hora de escribir el primer programa separado para nuestro núcleo: el shell. Se almacenará en un archivo .elf separado y se iniciará mediante el proceso init cuando se inicie el núcleo.

Este es el último artículo en el ciclo de desarrollo de nuestro sistema operativo.

Tabla de contenidos


Sistema de construcción (marca, gcc, gas). Arranque inicial (arranque múltiple). Lanzamiento (qemu). Biblioteca C (strcpy, memcpy, strext).

Biblioteca C (sprintf, strcpy, strcmp, strtok, va_list ...). Creación de la biblioteca en modo kernel y modo de aplicación de usuario.

El registro del sistema del núcleo. Memoria de video Salida a la terminal (kprintf, kpanic, kassert).
Memoria dinámica, montón (kmalloc, kfree).

Organización de memoria y manejo de interrupciones (GDT, IDT, PIC, syscall). Excepciones
Memoria virtual (directorio de páginas y tabla de páginas).

Proceso. Planificador Multitarea Sistema de llamadas (kill, exit, ps).

Controladores de dispositivos de caracteres. Llamadas del sistema (ioctl, fopen, fread, fwrite). Biblioteca C (fopen, fclose, fprintf, fscanf).

El sistema de archivos del kernel (initrd), elf y sus componentes internos. Sistema de llamadas (exec). Shell como un programa completo para el kernel.

Modo de protección del usuario (anillo3). Segmento de estado de la tarea (tss).

Shell como un programa completo de kernel


En un artículo anterior, analizamos los controladores de dispositivos de caracteres y escribimos un controlador de terminal.

Ahora tenemos todo lo que necesitamos para crear la primera aplicación de consola.

Escribiremos la aplicación de consola en sí, que compilaremos en un duende separado.

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

Tendremos que inicializar la biblioteca estándar y transferir el control a la función principal familiar.

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

Además en el ciclo, simplemente leemos la línea y ejecutamos el comando.

Parsim ordena a través de strtok_r si hay argumentos.

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

De hecho, solo estamos retirando las llamadas del sistema.
Permítame recordarle acerca de la inicialización de la biblioteca estándar.

En la última lección, escribimos la siguiente función en la biblioteca:

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

Simplemente abre los archivos especiales del controlador de terminal para leer y escribir, que corresponde a la entrada y salida del teclado a la pantalla.

Después de haber ensamblado nuestro duende con un shell, debe colocarse en el sistema de archivos del núcleo original (initrd).

Los cargadores de kernel cargan el disco RAM inicial como un módulo de arranque múltiple, por lo que conocemos la dirección en la memoria de nuestro initrd.

Queda por organizar el sistema de archivos para initrd, que es fácil de hacer según un artículo de James Molloy.

Por lo tanto, el formato será el siguiente:

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

A continuación, recuerde el formato de un elfo de 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; }; 

Aquí nos interesa el punto de entrada y la dirección de la tabla de encabezados de programa.

La sección de código y datos será el primer encabezado, y la sección de pila será el segundo (según los resultados de estudiar elf a través de 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); 

Esta información es suficiente para escribir un cargador de archivos elf.
Ya sabemos cómo seleccionar páginas para procesos personalizados.
Por lo tanto, solo necesitamos asignar un número suficiente de páginas para encabezados y copiar el contenido en ellas.
Escribiremos una función que creará un proceso basado en el archivo elf analizado.
Vea cómo analizar elfik en el video tutorial.
Necesitamos descargar solo un encabezado de programa con código y datos, por lo que no generalizaremos y nos centraremos en este caso.

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

Lo más interesante aquí es la creación de un directorio de páginas y una tabla de páginas.
Preste atención, primero seleccionamos las páginas físicas (mm_phys_alloc_pages), y luego las asignamos a las páginas lógicas (mmu_occupy_user_page).
Aquí se supone que las páginas en la memoria física se asignan continuamente.
Eso es todo. ¡Ahora puede implementar su propio shell para su núcleo! Mira el video tutorial y profundiza en los detalles.

Conclusión


Espero que hayas encontrado útil esta serie de artículos.
Todavía no hemos considerado los anillos de protección con usted, pero debido a la baja relevancia del tema y las críticas mixtas, continuaremos rompiendo.
Creo que usted mismo está listo para una mayor investigación, ya que usted y yo hemos examinado todas las cosas más importantes.

¡Por lo tanto, aprieta el cinturón y entra en batalla! Al escribir su propio sistema operativo!
Me tomó alrededor de un mes (si consideramos el tiempo completo durante 6-8 horas al día) para implementar todo lo que usted y yo aprendimos desde cero.

Por lo tanto, en 2-3 meses podrá escribir un sistema operativo completo con un sistema de archivos real, que usted y yo no pudimos implementar.

Solo sepa que qemu no sabe cómo trabajar con initrd de formato arbitrario y lo corta a 4kb, por lo que deberá hacerlo como en Linux o usar borsch en lugar de qemu.
Si sabe cómo solucionar este problema, escriba una carta personal, le estaré muy agradecido.

Eso es todo! Hasta que lo nuevo ya no se encuentre!

Referencias


Mire el video tutorial para más información.

El código fuente en el repositorio de git (necesita la rama de la lección 9).

Referencias


1. James Molloy. Haga rodar su propio sistema operativo de clones UNIX de juguete.
2. Dientes. Ensamblador para DOS, Windows, Unix
3. Kalashnikov. ¡Ensamblador es fácil!
4. Tanenbaum. Sistemas operativos Implementación y desarrollo.
5. Robert Love. Kernel de Linux Descripción del proceso de desarrollo.

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


All Articles