Criando um Sistema Operacional do Zero

Capítulo 1: Introdução à arquitetura x86 e sobre o nosso OS

Qual é a arquitetura x86?

         O termo denota uma família x86 de arquiteturas de conjunto de instruções compatíveis com versões anteriores com base na CPU Intel 8086.
A arquitetura x86 é a arquitetura do conjunto de instruções mais comum desde a sua introdução, em 1981, para o IBM PC. Uma grande quantidade de software, incluindo sistemas operacionais (SO) como o DOS, Windows, Linux, BSD, Solaris e Mac OS X, com função de hardware baseado em x86.

       Nós não estamos indo para a concepção de um sistema operacional para a arquitetura x86-64 mas para x86-32, graças à compatibilidade com versões anteriores, o nosso sistema operacional será compatível com nossos PCs mais recentes (mas tome cuidado se você quiser testá-lo em seu máquina real).

Nosso Sistema Operacional

      O objetivo é construir um sistema operacional baseado em UNIX muito simples em C ++, mas o objetivo não é apenas construir uma “prova de conceito”. O sistema operacional deve ser capaz de inicializar, iniciar um shell userland e ser extensível.

O sistema operacional será construído para a arquitetura x86, rodando em 32 bits, e é compatível com PCs da IBM.

especificações:

Código em C ++
x86, arquitetura de 32 bits
Boot com Grub
Tipo de sistema modular para motoristas
Tipo de estilo UNIX
multitarefa
Executável ELF em espaço de usuário
Módulos (acessíveis no espaço de usuário usando / dev / …):
discos IDE
partições DOS
relógio
EXT2 (somente leitura)
Boch VBE
userland:
POSIX API
LibC
“Pode” executar um shell ou alguns executáveis ​​como Lua, …

Capítulo 2: Configuração do ambiente de desenvolvimento

    O primeiro passo é a configuração de um ambiente bom e viável de desenvolvimento. Usando Vagrant e Virtualbox, você vai ser capaz de compilar e testar o seu sistema operacional a partir de todas as OSs (Linux, Windows ou Mac).

Instale o Vagrant

     Vagrant é um software livre e de código aberto para a criação e configuração de ambientes de desenvolvimento virtuais. Pode ser considerado um invólucro em torno do VirtualBox.
Vagrant vai nos ajudar a criar um ambiente de desenvolvimento virtual limpo em qualquer sistema que você está usando. O primeiro passo é fazer o download e instalar Vagrant para o seu sistema em http://www.vagrantup.com/.

Instale Virtualbox

   Oracle VM VirtualBox é um pacote de software de virtualização para x86 e computadores baseados em Intel64 AMD64 /.
Vagrant precisa Virtualbox para trabalhar, baixe e instale em seu sistema em https://www.virtualbox.org/wiki/Downloads.

Iniciar e testar o ambiente de desenvolvimento

Uma vez que o Vagrant e o Virtualbox está instalado, você precisa baixar a imagem lucid32 ubuntu para Vagrant:

caixa vagabundo adicionar lucid32 http://files.vagrantup.com/lucid32.box
Assim que a imagem lucid32 está pronto, precisamos definir nosso ambiente de desenvolvimento usando uma Vagrantfile, crie um arquivo chamado Vagrantfile. Este arquivo define o que os pré-requisitos nossas necessidades de ambiente: nasm, make, build-essential, grub e qemu.

Comece sua caixa usando:

Vagrant up
Agora você pode acessar sua caixa usando ssh para conectar-se a caixa virtual utilizando:

ssh vagrant
O código estará disponível no diretório / vagrant:

cd /vagrant 
Construir e testar nosso sistema operacional

O arquivo Makefile define algumas regras básicas para a construção do kernel, o usuário libc e alguns programas de nível de usuário.

Constituição:

make all
Teste nosso sistema operacional com qemu:

make run
A documentação para qemu está disponível a documentação QEMU Emulator.

Você pode sair do emulador usando: <Ctrl-a x>.

Capítulo 3: O primeiro boot com o GRUB

Como se dá a inicialização?

          Quando um computador baseado em x86 é ligado, ele começa um caminho complexo para chegar ao estágio em que o controle é transferido à rotina “main” do nosso kernel (kmain ()). Para este curso, vamos apenas considerar o método de inicialização do BIOS e não é sucessor (UEFI).

A seqüência de inicialização do BIOS é: RAM detecção -> Hardware detecção / Inicialização -> seqüência de inicialização.

          O passo mais importante para nós é a “seqüência de inicialização”, onde a BIOS é feito com a sua inicialização e tenta transferir o controle para a próxima etapa do processo de bootloader.

      Durante a “seqüência de inicialização”, o BIOS tentará determinar um “dispositivo de boot” (por exemplo, disquete, disco rígido, CD, dispositivo de memória flash USB ou de rede). Nosso sistema operacional será inicialmente de inicialização do disco rígido (mas será possível iniciá-la a partir de um CD ou um dispositivo de memória flash USB no futuro). Um dispositivo é considerado inicializável, caso o setor de inicialização contém a assinatura válida bytes 0x55 e 0xAA em deslocamentos 511 e 512, respectivamente (chamados bytes mágicos de Master Boot Record (MBR), Esta assinatura é representada (em binário) como 0b1010101001010101. O padrão de bit alternado foi pensado para ser uma proteção contra certas falhas (conduzir ou controlador). Se esse padrão é distorcido ou 0x00, o dispositivo não é considerado de arranque)

      Buscas BIOS fisicamente por um dispositivo de arranque, o carregamento dos primeiros 512 bytes a partir do sector de arranque de cada dispositivo para a memória física, a começar no endereço 0x7C00 (1 KB abaixo da marca KB 32). Quando os bytes de assinatura válidos são detectados, as transferências de BIOS para controlar o endereço de memória 0x7C00 (através de uma instrução de salto), a fim de executar o código do setor de inicialização.

    Durante todo este processo, a CPU foi executado em 16-bit modo real (o estado padrão para processadores x86, a fim de manter a compatibilidade com versões anteriores). Para executar as instruções de 32 bits dentro do nosso kernel, um bootloader é necessário para mudar a CPU em modo protegido.

O que é GRUB?

    GNU GRUB (abreviação de GNU GRand Unified Bootloader) é um pacote de boot loader do Projeto GNU. GRUB é a implementação de referência da especificação Multiboot da Free Software Foundation, que oferece ao usuário a opção de iniciar um dos vários sistemas operacionais instalados em um computador ou selecionar uma configuração do kernel específico disponível em partições de um sistema operacional em particular.
Para tornar mais simples, o GRUB é a primeira coisa arrancado pela máquina (um boot-loader) e irá simplificar o carregamento de nosso kernel armazenados no disco rígido.

Por que estamos usando GRUB?

– GRUB é muito simples de usar
– Torná-lo muito simples para carregar kernels 32bits sem necessidades de código de 16bits
– Multiboot com Linux, Windows e outros
– Tornar mais fácil de carregar módulos externos de memória
Como usar o GRUB?

    GRUB usa a especificação Multiboot, o binário executável deve ser 32bits e deve conter um cabeçalho especial (cabeçalho multiboot) em seus primeiros 8192 bytes. Nosso núcleo será um arquivo executável ELF (“Executable and Linkable Format”, um formato comum de arquivo padrão para arquivos executáveis ​​no sistema UNIX mais).

     A primeira seqüência de inicialização do nosso kernel está escrito em Assembly: start.asm e usamos um arquivo de ligação para definir nossa estrutura executável: linker.ld.

Este processo de inicialização inicializa também algum do nosso tempo de execução C ++, que será descrita no capítulo seguinte.

Estrutura de cabeçalho Multiboot:

struct multiboot_info {
    u32 flags;
    u32 low_mem;
    u32 high_mem;
    u32 boot_device;
    u32 cmdline;
    u32 mods_count;
    u32 mods_addr;
    struct {
        u32 num;
        u32 size;
        u32 addr;
        u32 shndx;
    } elf_sec;
    unsigned long mmap_length;
    unsigned long mmap_addr;
    unsigned long drives_length;
    unsigned long drives_addr;
    unsigned long config_table;
    unsigned long boot_loader_name;
    unsigned long apm_table;
    unsigned long vbe_control_info;
    unsigned long vbe_mode_info;
    unsigned long vbe_mode;
    unsigned long vbe_interface_seg;
    unsigned long vbe_interface_off;
    unsigned long vbe_interface_len;
};

Você pode usar os comandos mbchk kernel.elf para validar seu arquivo kernel.elf contra o padrão de inicialização múltipla. Você também pode usar o comando nm -n kernel.elf  to validate the offset of the different objects in the ELF binary.

Criar uma imagem de disco para o nosso kernel e grub

O diskimage.sh script vai gerar uma imagem de disco rígido, que pode ser usado pelo QEMU.

O primeiro passo é criar uma imagem do disco rígido (c.img) usando qemu-img:

qemu-img create c.img 2M


Precisamos agora particionar o disco usando fdisk:
fdisk ./c.img

# Switch to Expert commands
> x

# Change number of cylinders (1-1048576)
> c
> 4

# Change number of heads (1-256, default 16):
> h
> 16

# Change number of sectors/track (1-63, default 63)
> s
> 63

# Return to main menu
> r

# Add a new partition
> n

# Choose primary partition
> p

# Choose partition number
> 1

# Choose first cylinder (1-4, default 1)
> 1

# Choose last cylinder, +cylinders or +size{K,M,G} (1-4, default 4)
> 4

# Toggle bootable flag
> a

# Choose first partition for bootable flag
> 1

# Write table to disk and exit
> w



Precisamos agora anexar a partição criada para o dispositivo de loop (que permite 
que um arquivo seja de acesso como um dispositivo de bloco), utilizando losetup. 
O deslocamento da partição é passado como um argumento e calculada usando: 
offset= start_sector * bytes_by_sector.

Using  fdisk -l -u c.img,              you get: 63 * 512 = 32256.

 
losetup -o 32256 /dev/loop1 ./c.img


We create a EXT2 filesystem on this new device using:

mke2fs /dev/loop1

We copy our files on a mounted disk:

mount  /dev/loop1 /mnt/
cp -R bootdisk/* /mnt/
umount /mnt/

Install GRUB on the disk:

grub --device-map=/dev/null << EOF
device (hd0) ./c.img
geometry (hd0) 4 16 63
root (hd0,0)
setup (hd0)
quit
EOF

E, finalmente, retirar o dispositivo de loop:

losetup -d /dev/loop1
 

See Also

 

 

Capítulo 4: Backbone da OS e C ++ runtime

 

C ++ do  kernel  em  tempo  de  execução

Um kernel pode ser programado em C ++, é muito semelhante a fazer um kernel em C, exceto que existem algumas armadilhas que você deve levar em conta (o suporte de tempo de execução, construtores, …)

O compilador irá assumir que todo o C ++ runtime suporte necessário está disponível por padrão, mas como não estão ligando em libsupc ++ em seu kernel do C ++, nós precisamos adicionar algumas funções básicas que podem ser encontrados no arquivo cxx.cc.

Atenção: Os operadores new e delete não podem ser usados antes da memória virtual e paginação foram inicializados.

 

Funções básicas C/C++


O código do kernel não pode utilizar as funções das bibliotecas padrão, então precisamos adicionar algumas funções básicas para o gerenciamento de memória e strings:

 

void    itoa(char *buf, unsigned long int n, int base);

void *  memset(char *dst,char src, int n);
void *  memcpy(char *dst, char *src, int n);

int     strlen(char *s);
int     strcmp(const char *dst, char *src);
int     strcpy(char *dst,const char *src);
void    strcat(void *dest,const void *src);
char *  strncpy(char *destString, const char *sourceString,int maxLength);
int     strncmp( const char* s1, const char* s2, int c );


These functions are defined in string.cc, memory.cc, itoa.cc

C types

During the next step, we are going to use different types in our code, most of the types we are going to use unsigned types (all the bits are used to stored the integer, in signed types one bit is used to signal the sign):

 

Compilar nosso kernel

Compilando um kernel não é a mesma coisa que a compilação de um executável linux, não podemos usar a biblioteca padrão e não devem ter dependências para o sistema.

Nossa Makefile irá definir o processo para compilar e linkar nosso kernel.

Para arquitetura x86, os argumentos de partidários serão utilizados para gcc / g ++ / ld:

 

# Linker LD=ld LDFLAG= -melf_i386 -static -L ./ -T ./arch/$(ARCH)/linker.ld # C++ compiler SC=g++ FLAG= $(INCDIR) -g -O2 -w -trigraphs -fno-builtin -fno-exceptions -fno-stack-protector -O0 -m32 -fno-rtti -nostdlib -nodefaultlibs # Assembly compiler ASM=nasm ASMFLAG=-f elf -o

 

Capítulo 5: As classes base para o gerenciamento de arquitetura x86

 

Agora que sabemos como compilar o nosso kernel do C ++ e inicie o binário usando GRUB, podemos começar a fazer algumas coisas legais em C / C ++.

Imprimindo no console tela

Nós estamos indo para usar o modo padrão VGA (03h) para exibir algum texto para o usuário. A tela pode ser acessado diretamente através da memória de vídeo em 0xb8000. A resolução da tela é de 80×25 e cada personagem na tela é definido por dois bytes: um para o código de caracteres, e um para a bandeira de estilo. Isto significa que o tamanho total da memória de vídeo é 4000B (80B 25B * * 2B).

Na classe IO (io.cc),:

x, y: definir a posição do cursor no ecrã
real_screen: definir o ponteiro de memória de vídeo
putc (char c): imprimir um caráter único na tela e controlar a posição do cursor
printf (char * s, …): imprimir uma string
Nós adicionamos um método putc à classe IO de colocar um personagem na tela e atualizar a (x, y) posição.

 

/* put a byte on screen */
void Io::putc(char c){
    kattr = 0x07;
    unsigned char *video;
    video = (unsigned char *) (real_screen+ 2 * x + 160 * y);
    // newline
    if (c == '\n') {
        x = 0;
        y++;
    // back space
    } else if (c == '\b') {
        if (x) {
            *(video + 1) = 0x0;
            x--;
        }
    // horizontal tab
    } else if (c == '\t') {
        x = x + 8 - (x % 8);
    // carriage return
    } else if (c == '\r') {
        x = 0;
    } else {
        *video = c;
        *(video + 1) = kattr;

        x++;
        if (x > 79) {
            x = 0;
            y++;
        }
    }
    if (y > 24)
        scrollup(y - 24);
}


Nós também devemos adicionar um método útil e muito conhecido: printf.


Capítulo 6: GDT 





Graças ao GRUB, o kernel não está mais em modo real, mas já em modo protegido, 
este modo permite-nos usar todas as possibilidades do microprocessador, 
como gerenciamento de memória virtual, paginação e seguro multi-tasking.

Qual é o GDT? 

O GDT ("Descriptor Table global") é uma estrutura de dados utilizada para 
definir as diferentes áreas de memória: o endereço de base, os privilégios 
de tamanho e de acesso, como executar e escrever. Estas áreas de memória 
são chamados "segmentos". 

Nós vamos usar o GDT para definir diferentes segmentos de memória: 

"code": Código do kernel, usado para armazenamento do código binário executável; 
"data": Dados do kernel; 
"stack": Pilha do kernel, usado para armazenado na pilha de chamadas durante a 
execução do kernel; 
"ucode": Código do usuário, utilizado para armazenamento do código binário executá
vel para o programa do usuário; 
"udata": Dados do programa do usuário;
"ustack": Pilha do usuário, usado para armazenado na pilha de chamadas durante a 
execução no espaço de usuário;

Como carregar a nossa GDT? 

GRUB inicializa uma GDT, porém GDT não corresponde ao nosso kernel. 
O GDT é carregado usando as instruções de montagem LGDT. 
Ele espera que a localização de uma descrição da estrutura do GDT:


GDTR


E a estrutura C:


struct gdtr {
    u16 limite;
    u32 base;
} __attribute__ ((packed));



Cuidado: a directiva __attribute__ ((packed)) sinal para gcc que a estrutura deve 
usar o mínimo de memória possível. Sem esta diretiva, gcc inclui alguns bytes para 
otimizar o alinhamento de memória e o acesso durante a execução. 

Agora precisamos definir nossa mesa GDT e depois carregá-la usando LGDT. A tabela 
GDT pode ser armazenada onde quisermos na memória, o endereço deve ser apenas um 
sinal para o processo usando o registro GDTR. 

A tabela GDT é composta de segmentos com a seguinte estrutura:

GDTR

E a estrutura C:


struct gdtdesc {
    u16 lim0_15;
    u16 base0_15;
    u8 base16_23;
    u8 acces;
    u8 lim16_19:4;
    u8 other:4;
    u8 base24_31;
} __attribute__ ((packed));


Como definir nossa mesa GDT? 

Precisamos agora definir a nossa GDT na memória e, finalmente, carregá-lo usando o registro GDTR. 

Estamos indo para armazenar nossa GDT no endereço:

#define GDTBASE 0x00000800

A função init_gdt_desc in x86.cc inicializa um descritor de segmento gdt 

void init_gdt_desc(u32 base, u32 limite, u8 acces, u8 other, struct gdtdesc *desc)
{
    desc->lim0_15 = (limite & 0xffff);
    desc->base0_15 = (base & 0xffff);
    desc->base16_23 = (base & 0xff0000) >> 16;
    desc->acces = acces;
    desc->lim16_19 = (limite & 0xf0000) >> 16;
    desc->other = (other & 0xf);
    desc->base24_31 = (base & 0xff000000) >> 24;
    return;
}

E a função de inicializar o GDT init_gdt, algumas partes da função abaixo serão 
explicados mais tarde, e são usados ​​para multitarefa.

void init_gdt(void)
{
    default_tss.debug_flag = 0x00;
    default_tss.io_map = 0x00;
    default_tss.esp0 = 0x1FFF0;
    default_tss.ss0 = 0x18;

    /* initialize gdt segments */
    init_gdt_desc(0x0, 0x0, 0x0, 0x0, &kgdt[0]);
    init_gdt_desc(0x0, 0xFFFFF, 0x9B, 0x0D, &kgdt[1]);  /* code */
    init_gdt_desc(0x0, 0xFFFFF, 0x93, 0x0D, &kgdt[2]);  /* data */
    init_gdt_desc(0x0, 0x0, 0x97, 0x0D, &kgdt[3]);      /* stack */

    init_gdt_desc(0x0, 0xFFFFF, 0xFF, 0x0D, &kgdt[4]);  /* ucode */
    init_gdt_desc(0x0, 0xFFFFF, 0xF3, 0x0D, &kgdt[5]);  /* udata */
    init_gdt_desc(0x0, 0x0, 0xF7, 0x0D, &kgdt[6]);      /* ustack */

    init_gdt_desc((u32) & default_tss, 0x67, 0xE9, 0x00, &kgdt[7]); /* descripteur de tss */

    /* initialize the gdtr structure */
    kgdtr.limite = GDTSIZE * 8;
    kgdtr.base = GDTBASE;

    /* copy the gdtr to its memory area */
    memcpy((char *) kgdtr.base, (char *) kgdt, kgdtr.limite);

    /* load the gdtr registry */
    asm("lgdtl (kgdtr)");

    /* initiliaz the segments */
    asm("   movw $0x10, %ax \n \
            movw %ax, %ds   \n \
            movw %ax, %es   \n \
            movw %ax, %fs   \n \
            movw %ax, %gs   \n \
            ljmp $0x08, $next   \n \
            next:       \n");
}


*************************

Capítulo 7: IDT e interrupções 

Uma interrupção é um sinal para o processador emitida por hardware ou software que 
indica um acontecimento que necessite de atenção imediata. 

Existem 3 tipos de interrupção: 

Interrupções de hardware: são enviadas para o processador de um dispositivo externo 
(teclado, mouse, disco rígido, ...). Interrupções de hardware foram introduzidos 
como uma forma de reduzir o desperdício de tempo valioso do processador em loops 
de votação, à espera de eventos externos. 
Interrupções de software: são iniciados voluntariamente pelo software. 
Ele é usado para gerenciar as chamadas do sistema. 
Exceções: são usados ​​por erros ou eventos que ocorrem durante a execução 
do programa que são excepcionais o suficiente para que eles não podem ser 
tratadas no próprio programa (divisão por zero, falha de página, ...) 
O exemplo de teclado: 

Quando o usuário pressionar uma tecla no teclado, o controlador do teclado vai 
sinalizar uma interrupção para o controlador de interrupção. Se a interrupção 
não é mascarado, o controlador irá sinalizar a interrupção para o processador, 
o processador irá executar uma rotina para gerenciar a interrupção 
(tecla pressionada ou a chave liberada), esta rotina pode, por exemplo, 
obter a tecla pressionada a partir do controlador de teclado e imprimir 
a chave para a tela. Uma vez que a rotina de processamento de caracteres 
é concluído, o trabalho interrompido pode ser retomado.

Qual é o PIC? 

O PIC (controlador de interrupção programável) é um dispositivo que é 
usado para combinar várias fontes de interrupção para uma ou mais linhas
 de CPU, enquanto permite que os níveis de prioridade a ser atribuído às 
suas saídas de interrupção. Quando o dispositivo tem várias saídas de 
interrupção para afirmar, afirma-los em ordem de prioridade relativa. 

O mais conhecido é o PIC 8259A, 8259A pode lidar com cada oito dispositivos, 
mas a maioria dos computadores tem dois controladores: um mestre e um escravo,
 isso permite que o computador para gerenciar as interrupções a partir de 14 dispositivos. 

Neste capítulo, vamos precisar de programar este controlador para inicializar
 e interrupções de máscara. 

Qual é o IDT? 

A interrupção Descriptor Table (IDT) é uma estrutura de dados utilizada pela 
arquitetura x86 para implementar uma tabela de vetores de interrupção. 
O IDT é usada pelo processador para determinar a resposta correta para 
interrupções e excepções. 
Nosso núcleo vai usar o IDT para definir as diferentes funções a serem 
executadas quando uma interrupção ocorreu. 

Como o GDT, IDT é carregado usando as instruções de montagem LIDTL.
 Ele espera que a localização de uma descrição da estrutura do IDT:


struct idtr {
    u16 limite;
    u32 base;
} __attribute__ ((packed));


A tabela IDT é composta de segmentos de IDT com a seguinte estrutura:
 
struct idtdesc {
    u16 offset0_15;
    u16 select;
    u16 type;
    u16 offset16_31;
} __attribute__ ((packed));


Cuidado: A diretiva __attribute__ ((packed))  sinalizar para gcc que a 
estrutura deve usar o mínimo de memória possível. Sem esta directiva, 
gcc inclui alguns bytes para otimizar o alinhamento de memória eo acesso durante a execução. 

Agora precisamos definir nossa mesa IDT e, em seguida, carregá-lo usando
 LIDTL. A tabela IDT podem ser armazenados onde quisermos na memória, 
o endereço deve ser apenas um sinal para o processo usando o registro IDTR. 

Aqui está uma tabela de interrupções comuns (interrupção de hardware Maskable são chamados de IRQ):

IRQ   Descrição 
0     de interrupção programável temporizador de interrupção 
1     Interrupção de teclado 
2     Cascade (usado internamente pelos dois PICs. Nunca levantou) 
3     COM2 (se habilitado) 
4     COM1 (se habilitado) 
5     LPT2 (se habilitado) 
6     Disquete 
7     LPT1 
8     CMOS relógio de tempo real (se habilitado) 
9     Gratuito para os periféricos / legado SCSI / NIC 
10    Grátis para os periféricos / SCSI / NIC 
11    Grátis para os periféricos / SCSI / NIC 
12    Mouse PS2  
13    FPU / coprocessador / Inter-processador 
14    Primária ATA Hard Disk 
15    ATA Secundária Hard Disk


Como inicializar as interrupções? 

Este é um método simples de definir um segmento de IDT
 
void init_idt_desc(u16 select, u32 offset, u16 type, struct idtdesc *desc)
{
    desc->offset0_15 = (offset & 0xffff);
    desc->select = select;
    desc->type = type;
    desc->offset16_31 = (offset & 0xffff0000) >> 16;
    return;
}


E agora podemos inicializar os interupts:

#define IDTBASE 0x00000000
#define IDTSIZE 0xFF
idtr kidtr;



void init_idt(void)
{
    /* Init irq */
    int i;
    for (i = 0; i < IDTSIZE; i++)
        init_idt_desc(0x08, (u32)_asm_schedule, INTGATE, &kidt[i]); //

    /* Vectors  0 -> 31 are for exceptions */
    init_idt_desc(0x08, (u32) _asm_exc_GP, INTGATE, &kidt[13]);     /* #GP */
    init_idt_desc(0x08, (u32) _asm_exc_PF, INTGATE, &kidt[14]);     /* #PF */

    init_idt_desc(0x08, (u32) _asm_schedule, INTGATE, &kidt[32]);
    init_idt_desc(0x08, (u32) _asm_int_1, INTGATE, &kidt[33]);

    init_idt_desc(0x08, (u32) _asm_syscalls, TRAPGATE, &kidt[48]);
    init_idt_desc(0x08, (u32) _asm_syscalls, TRAPGATE, &kidt[128]); //48

    kidtr.limite = IDTSIZE * 8;
    kidtr.base = IDTBASE;


    /* Copy the IDT to the memory */
    memcpy((char *) kidtr.base, (char *) kidt, kidtr.limite);

    /* Load the IDTR registry */
    asm("lidtl (kidtr)");
}


Após intialization do nosso IDT, precisamos ativar interrupções configurando o PIC. A função a seguir irá configurar os dois PICs pela escrita nos seus registos internos utilizando os portos do io.outb processador de saída. Nós configurar as PICs usando as portas:


  • Master PIC: 0x20 and 0x21
  • Slave PIC: 0xA0 and 0xA1

 

Para um PIC, existem 2 tipos de registros:

ICW (Inicialização Palavra de Comando): reinit o controlador
OCW (Word Controle da Operação): configurar o controlador de uma vez inicializado (usado para mascarar / desmascarar as interrupções)

 

void init_pic(void)
{
    /* Initialization of ICW1 */
    io.outb(0x20, 0x11);
    io.outb(0xA0, 0x11);

    /* Initialization of ICW2 */
    io.outb(0x21, 0x20);    /* start vector = 32 */
    io.outb(0xA1, 0x70);    /* start vector = 96 */

    /* Initialization of ICW3 */
    io.outb(0x21, 0x04);
    io.outb(0xA1, 0x02);

    /* Initialization of ICW4 */
    io.outb(0x21, 0x01);
    io.outb(0xA1, 0x01);

    /* mask interrupts */
    io.outb(0x21, 0x0);
    io.outb(0xA1, 0x0);
}


PIC ICW configurações detalhes 

Os registros devem ser configurados em ordem.


ICW1 (port 0x20 / port 0xA0)

|0|0|0|1|x|0|x|x|
         |   | +--- with ICW4 (1) or without (0)
         |   +----- one controller (1), or cascade (0)
         +--------- triggering by level (level) (1) or by edge (edge) (0)


ICW2 (port 0x21 / port 0xA1)

|x|x|x|x|x|0|0|0|
 | | | | |
 +----------------- base address for interrupts vectors


ICW2 (port 0x21 / port 0xA1)

For the master:

|x|x|x|x|x|x|x|x|
 | | | | | | | |
 +------------------ slave controller connected to the port yes (1), or no (0)


For the slave:

|0|0|0|0|0|x|x|x|  pour l'esclave
           | | |
           +-------- Slave ID which is equal to the master port


ICW4 (port 0x21 / port 0xA1) 

Ele é usado para definir em modo que o controlador deve funcionar.

|0|0|0|x|x|x|x|1|
       | | | +------ mode "automatic end of interrupt" AEOI (1)
       | | +-------- mode buffered slave (0) or master (1)
       | +---------- mode buffered (1)
       +------------ mode "fully nested" (1)


Por que compensar segmentos IDT nossas funções ASM? 

Você deve ter notado que quando eu estou inicializando nossos segmentos de IDT, estou usando deslocamentos para o segmento de código em Assembly. As diferentes funções são definidas no x86int.asm e são do seguinte esquema:


%macro  SAVE_REGS 0
    pushad
    push ds
    push es
    push fs
    push gs
    push ebx
    mov bx,0x10
    mov ds,bx
    pop ebx
%endmacro

%macro  RESTORE_REGS 0
    pop gs
    pop fs
    pop es
    pop ds
    popad
%endmacro

%macro  INTERRUPT 1
global _asm_int_%1
_asm_int_%1:
    SAVE_REGS
    push %1
    call isr_default_int
    pop eax ;;a enlever sinon
    mov al,0x20
    out 0x20,al
    RESTORE_REGS
    iret
%endmacro


Estas macros serão usadas para definir o segmento de interrupção que irá prevenir a corrupção dos registos diferentes, será muito útil para a multitarefa.


Capítulo 8: Gerenciamento de memória: físico e virtual 

No capítulo relacionado com o GDT, vimos que o uso de segmentação um endereço de memória física é calculada usando um seletor de segmento e um deslocamento. 

Neste capítulo, vamos implementar a paginação, paginação irá traduzir um endereço linear de segmentação em um endereço físico. 

Por que precisamos de paginação? 

Paginação permitirá que nosso kernel: 

Use o disco rígido como memória e não ser limitado pelo limite de memória RAM da máquina; 
para ter um único espaço de memória para cada processo de 
para permitir e espaço de memória unallow de forma dinâmica 
Em um sistema paginado, cada processo pode executar em sua própria área de 4GB de memória, sem qualquer chance de efetuar a memória de qualquer outro processo, ou o kernel do. Ele simplifica multitarefa.

Processes memories

Como isso funciona? 

A tradução de um endereço linear para um endereço físico é feita em vários passos: 

O processador usa o CR3 registro para saber o endereço físico do diretório páginas. 
Os primeiros 10 bits do endereço linear representam um deslocamento (entre 0 e 1023), apontando para uma entrada no diretório de páginas. Esta entrada contém o endereço físico de uma tabela de páginas. 
os próximos 10 bits do endereço linear representam um deslocamento, apontando para uma entrada na tabela de páginas. Esta entrada está apontando para uma página 4KO. 
Os últimos 12 bits do endereço representam um deslocamento linear (entre 0 e 4095), o que indica a posição na página 4KO.
Address translation

Como isso funciona? 

A tradução de um endereço linear para um endereço físico é feita em vários passos: 

O processador usa o CR3 registro para saber o endereço físico do diretório páginas. 
Os primeiros 10 bits do endereço linear representam um deslocamento (entre 0 e 1023), apontando para uma entrada no diretório de páginas. Esta entrada contém o endereço físico de uma tabela de páginas. 
os próximos 10 bits do endereço linear representam um deslocamento, apontando para uma entrada na tabela de páginas. Esta entrada está apontando para uma página 4KO. 
Os últimos 12 bits do endereço representam um deslocamento linear (entre 0 e 4095), o que indica a posição na página 4KO.


Page directory entry
Page table entry

P: indicar se a página ou tabela está na memória física 
R / W: indica se a página ou tabela é acessível em escrita (igual a 1) 
L / S: é igual a 1 para permitir o acesso a tarefas não preferidos 
A: indicar se a página ou tabela foi acessada 
D: (somente para a tabela de páginas) indicar se a página foi escrita 
PS (apenas para o diretório de páginas) indicam o tamanho das páginas: 
0 = 4KO 
1 = 4mo

Note: Physical addresses in the pages diretcory or pages table are written using 20 bits because these addresses are aligned on 4ko, so the last 12bits should be equal to 0.

  • A pages directory or pages table used 1024*4 = 4096 bytes = 4k
  • A pages table can address 1024 * 4k = 4 Mo
  • A pages directory can address 1024 * (1024 * 4k) = 4 Go

 

Como habilitar a paginação?

Para ativar a paginação, só precisamos definir bit 31 do CR0registry a 1:

asm("  mov %%cr0, %%eax; \
       or %1, %%eax;     \
       mov %%eax, %%cr0" \
       :: "i"(0x80000000));

Mas antes, é preciso inicializar o nosso diretório de páginas com pelo menos uma tabela de páginas.



Fonte:  https://github.com/SamyPesse/How-to-Make-a-Computer-Operating-System
Anúncios

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s