No artigo anterior, implementamos um gerenciador de memória dinâmico.
Hoje, abordaremos o básico do trabalho no modo protegido do processador Intel i386.
Nomeadamente: a tabela do descritor global e a tabela do vetor de interrupção.
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).
O sistema de arquivos do kernel (initrd), elf e seus internos. Chamadas do sistema (exec).
Drivers de dispositivo de caracteres. Chamadas do sistema (ioctl, fopen, fread, fwrite). Biblioteca C (fopen, fclose, fprintf, fscanf).
Shell como um programa completo para o kernel.
Modo de proteção do usuário (anel3). Segmento de status da tarefa (tss).
Endereçamento linear
Os processadores Intel têm 2 modos operacionais principais: Modo protegido x32 e IA-32e x64.
Em geral, Zubkov escreve muito bem e de maneira compreensível sobre isso, recomendo a leitura, embora, em princípio, o Manual da Intel também seja possível, não seja complicado, mas redundante e grande.
Eles têm um volume separado para programação do sistema, eu recomendo e leio.
Há muito mais informações em língua russa no primeiro, portanto, consideraremos brevemente os principais pontos.
Existem dois tipos de endereçamento: linear e página. Linear significa que todo o espaço físico é descrito continuamente e coincide com o físico, pois, como regra, as bases dos descritores de segmentos são nulas, pois é mais fácil.
Nesse caso, para o modo kernel, você precisa criar três descritores que descrevem a memória: para código, pilha e dados. Eles são diferenciados por alguma proteção de hardware.
Cada segmento desse tipo tem uma base de zero e um limite endereçado pelo tamanho máximo de uma palavra de máquina. A pilha cresce na direção oposta e, para isso, também há um sinalizador no descritor.
Portanto, com três registros desse formato, abordamos tudo o que precisamos:
struct GDT_entry_t { u16 limit_low: 16; u16 base_low: 16; u8 base_middle: 8; u8 type: 4; u8 s: 1; u8 dpl: 2; u8 p: 1; u8 limit_high: 4; u8 a: 1; u8 zero: 1; u8 db: 1; u8 g: 1; u8 base_high: 8; } attribute(packed);
Cada registrador de segmento (cs, ds, ss) possui seu próprio descritor no GDT; portanto, quando escrevemos algo na seção de código, obtemos um erro, porque há proteção por escrito no descritor.
Para que isso funcione, precisamos carregar uma estrutura do seguinte formato no registro GDTR:
struct GDT_pointer_t { u16 limit; u32 base; } attribute(packed);
O limite é o final da tabela GDT menos 1, a base é o início na memória.
O GDT é carregado no registro da seguinte maneira:
/*
* Load global descriptor table
* void asm_gdt_load(void *gdt_ptr)
*/
asm_gdt_load:
mov 4(%esp),%eax # eax = gdt_ptr
lgdt (%eax)
mov $0x10,%eax
mov %ax,%ds
mov %ax,%es
mov %ax,%fs
mov %ax,%gs
mov %ax,%ss
jmp $0x08,$asm_gdt_load_exit
asm_gdt_load_exit:
ret
E imediatamente depois disso, carregamos os seletores de dados do kernel nos registradores de todos os segmentos, indicando o descritor de dados (anel de proteção zero).
Depois disso, tudo estará pronto para incluir paginação, mas mais sobre isso mais tarde.
A propósito, os gerenciadores de inicialização com inicialização múltipla recomendam configurar imediatamente seu GDT, embora eles mesmos o façam, dizem isso de maneira mais confiável.
Veja como fazer tudo isso tecnicamente corretamente no tutorial em vídeo.
Manuseio de interrupção
Por analogia com o GDT, a tabela de interrupção possui seu próprio registro IDTR, no qual você também precisa carregar um ponteiro semelhante, mas já no IDT.
A tabela de interrupção em si é descrita pelas seguintes entradas:
struct IDT_entry_t { u16 offset_lowerbits; u16 selector; u8 zero; u8 type_attr; u16 offset_higherbits; };
O gateway de interrupção geralmente atua como um tipo, pois queremos lidar com interrupções especificamente. Ainda não consideramos traps e um gateway de chamadas, pois está mais próximo do TSS e dos anéis de proteção.
Vamos criar uma interface para trabalhar com essas tabelas com você. Eles só precisam ser configurados e esquecidos uma vez.
extern void gdt_init(); extern void idt_init();
E agora declararemos os manipuladores de interrupção listados nos registros IDT.
Primeiro, escreva os manipuladores de erro de hardware:
extern void ih_double_fault(); extern void ih_general_protect(); extern void ih_page_fault(); extern void ih_alignment_check(); extern void asm_ih_double_fault(); extern void asm_ih_general_protect(); extern void asm_ih_page_fault(); extern void asm_ih_alignment_check();
Em seguida, o manipulador de interrupções do teclado:
extern void ih_keyboard(); extern void asm_ih_keyboard();
É hora de inicializar a tabela IDT.
Parece algo como isto:
extern void idt_init() { size_t idt_address; size_t idt_ptr[2]; pic_init(); idt_fill_entry(INT_DOUBLE_FAULT, (size_t)asm_ih_double_fault); idt_fill_entry(INT_GENERAL_PROTECT, (size_t)asm_ih_general_protect); idt_fill_entry(INT_ALIGNMENT_CHECK, (size_t)asm_ih_alignment_check); idt_fill_entry(INT_KEYBOARD, (size_t)asm_ih_keyboard); idt_address = (size_t)IDT; idt_ptr[0] = (LOW_WORD(idt_address) << 16) + (sizeof(struct IDT_entry_t) * IDT_SIZE); idt_ptr[1] = idt_address >> 16; asm_idt_load(idt_ptr); }
Aqui, registramos três manipuladores de erro de hardware e uma interrupção.
Para que isso comece a funcionar, precisamos carregar um ponteiro especial com a base e o limite no registro IDTR:
/*
* Load interrupt table
* void asm_idt_load(unsigned long *addr)
*/
asm_idt_load:
push %edx
mov 8(%esp), %edx
lidt (%edx)
pop %edx
ret
São necessários limites para entender quantos registros existem na tabela.
É hora de escrever um manipulador de interrupção do teclado:
/*
* Handle IRQ1
* void asm_ih_keyboard(unsigned int)
*/
asm_ih_keyboard:
pushal
call ih_keyboard
popal
iretl
Nota: daqui em diante e em todo o código as “metades inferiores” são equivalentes às “metades superiores” no Linux. E o "superior", respectivamente, o oposto. Peço desculpas, o oposto foi colocado na minha cabeça: D
Na verdade, ele passará o código para um manipulador de alto nível.
Isso, por sua vez, chamará o manipulador das metades inferiores do driver correspondente que registrou a solicitação de processamento dessa interrupção.
No nosso caso, será um driver de dispositivo de caracteres.
As metades inferiores são necessárias para processar rapidamente as interrupções sem diminuir a velocidade das outras e, quando houver tempo, o processador das metades superiores executará gradualmente um trabalho adicional, porque esse processador já pode estar cheio (interrompido).
extern void ih_keyboard() { printf("[IH]: irq %u\n", 1); u_char status = asm_read_port(KEYBOARD_STATUS_PORT); if (status & 0x01) { char keycode = asm_read_port(KEYBOARD_DATA_PORT); if (keycode < 1) { goto end; } } end: asm_write_port(PIC1_CMD_PORT, 0x20); }
Agora, quando pressionamos a tecla do teclado, sempre vemos a entrada correspondente no log do sistema do kernel.
Referências
Agora,
abra o tutorial em vídeo deste artigo.E assista o
repositório git em paralelo
(você precisa de um ramo da lição5)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.