É hora de escrever o primeiro programa separado para o nosso kernel - o shell. Ele será armazenado em um arquivo .elf separado e iniciado pelo processo init quando o kernel iniciar.
Este é o último artigo no ciclo de desenvolvimento do nosso sistema operacional.
Sumário
Construa o sistema (marca, gcc, gás). Inicialização inicial (inicialização múltipla). Iniciar (qemu). Biblioteca C (strcpy, memcpy, strext).
Biblioteca C (sprintf, strcpy, strcmp, strtok, va_list ...). Construindo a biblioteca no modo kernel e no modo de aplicativo do usuário.
O log do sistema do kernel. Memória de vídeo Saída para o terminal (kprintf, kpanic, kassert).
Memória dinâmica, heap (kmalloc, kfree).
Organização da memória e manipulação de interrupções (GDT, IDT, PIC, syscall). Exceções
Memória virtual (diretório e tabela de páginas).
Processo. Planejador Multitarefa. Chamadas do sistema (interrupção, saída, ps).
Drivers de dispositivo de caracteres. Chamadas do sistema (ioctl, fopen, fread, fwrite). Biblioteca C (fopen, fclose, fprintf, fscanf).
O sistema de arquivos do kernel (initrd), elf e seus internos. Chamadas do sistema (exec). Shell como um programa completo para o kernel.Modo de proteção do usuário (anel3). Segmento de status da tarefa (tss).
Shell como um programa completo do kernel
Em um artigo anterior, examinamos os drivers de dispositivo de caracteres e escrevemos um driver de terminal.
Agora, temos tudo o que precisamos para criar o primeiro aplicativo de console.
Escreveremos o próprio aplicativo do console, que iremos compilar em um elfo separado.
void start() { u_int errno; stdio_init(); errno = main(); stdio_deinit(); exit(errno); }
Precisamos inicializar a biblioteca padrão e transferir o controle para a função principal familiar.
int main() { char cmd[255]; while (1) { printf(prompt); flush(); scanf(cmd); if (!execute_command(cmd)) { break; } } return 0; }
Mais adiante, simplesmente lemos a linha e executamos o comando.
Parsim comanda através de strtok_r se houver 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; }
Na verdade, estamos apenas realizando chamadas do sistema.
Deixe-me lembrá-lo sobre a inicialização da biblioteca padrão.
Na última lição, escrevemos a seguinte função na 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); }
Ele simplesmente abre os arquivos especiais do driver do terminal para leitura e gravação, o que corresponde à entrada e saída do teclado na tela.
Depois de montar nosso elf com um shell, ele deve ser colocado no sistema de arquivos do kernel original (initrd).
O disco ram inicial é carregado pelos carregadores do kernel como um módulo de inicialização múltipla; portanto, sabemos o endereço na memória do nosso initrd.
Resta organizar o sistema de arquivos para o initrd, o que é fácil de fazer, de acordo com um artigo de James Molloy.
Portanto, o formato será o seguinte:
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]; };
Em seguida, lembre-se do formato de um 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; };
Aqui estamos interessados no ponto de entrada e no endereço da tabela de cabeçalhos do programa.
A seção de código e dados será o primeiro cabeçalho e a seção de pilha será o segundo (de acordo com os resultados do estudo do elfo através do 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 informação é suficiente para gravar um carregador de arquivos elf.
Já sabemos como selecionar páginas para processos personalizados.
Portanto, precisamos alocar um número suficiente de páginas para os títulos e copiar o conteúdo para eles.
Escreveremos uma função que criará um processo baseado no arquivo elf analisado.
Veja como analisar o elfik no tutorial em vídeo.
Precisamos fazer o download de apenas um cabeçalho do programa com código e dados, para não generalizarmos e focarmos neste 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);
A coisa mais interessante aqui é a criação de um diretório e uma tabela de páginas.
Preste atenção, primeiro selecionamos as páginas físicas (mm_phys_alloc_pages) e depois mapeá-las para as páginas lógicas (mmu_occupy_user_page).
Aqui supõe-se que as páginas na memória física sejam alocadas continuamente.
Só isso. Agora você pode implementar seu próprio shell para o seu kernel! Assista ao tutorial em vídeo e mergulhe nos detalhes.
Conclusão
Espero que você tenha achado útil essa série de artigos.
Ainda não consideramos os anéis de proteção, mas, devido à baixa relevância do tópico e às críticas mistas, continuaremos quebrando.
Acho que agora você está pronto para mais pesquisas, pois você e eu examinamos todas as coisas mais importantes.
Portanto, aperte o cinto e entre na batalha! Escrevendo seu próprio sistema operacional!
Levei cerca de um mês (se considerarmos tempo integral de 6 a 8 horas por dia) para implementar tudo o que você e eu aprendemos do zero.
Portanto, em 2 a 3 meses, você poderá gravar um sistema operacional completo com um sistema de arquivos real, que você e eu não conseguimos implementar.
Apenas saiba que o qemu não sabe trabalhar com initrd de formato arbitrário e o reduz para 4kb; portanto, você precisará fazê-lo como no Linux ou usar o borsch em vez do qemu.
Se você souber como solucionar esse problema, escreva em uma carta pessoal, ficarei muito grato a você.
Isso é tudo! Até que o novo não se encontre mais!
Referências
Assista ao
tutorial em vídeo para obter mais informações.
O código fonte
no repositório git (você precisa da ramificação lição9).
Referências
1. James Molloy. Role seu próprio sistema operacional clone do UNIX de brinquedo.
2. Dentes. Assembler para DOS, Windows, Unix
3. Kalashnikov. Assembler é fácil!
4. Tanenbaum. Sistemas operacionais. Implementação e desenvolvimento.
5. Robert Love. Kernel Linux Descrição do processo de desenvolvimento.