Desenvolvimento de SO semelhante ao Unix - Shell. Conclusão (9)

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

/* * Elf entry point */ 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)) { /* 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; } 

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

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

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

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

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.

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


All Articles