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.
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)) { 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; }
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; char name[8]; unsigned int offset; unsigned int length; }; extern struct initrd_fs_t { int count; struct initrd_node_t node[INITRD_MAX_FILES]; };
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; 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; };
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; u32 p_offset; u32 p_vaddr; u32 p_paddr; u32 p_filesz; u32 p_memsz; u32 p_flags; u32 p_align; } 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.
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);
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.